Жидкости в Inner Core, или как я НЕ изменил мир моддинга MCPE

Жидкости в Inner Core, или как я НЕ изменил мир моддинга MCPE

С самого момента создания Inner Core существовала одна значительная проблема — отсутствие физических жидкостей. Сегодня я расскажу про свою попытку исправить этот недостаток лаунчера.

Первым делом следует рассказать, по какой причине жидкости не были реализованы на уровне лаунчера. Жидкости в Майнкрафте — блоки, каждый из которых занимает два ID. Поскольку в версии 1.0.3, на которой построен Inner Core, количество ID блоков ограничено, создавать полноценные жидкости означает занимать драгоценные реальные ID блоков.

С другой стороны, благодаря возможностям ICRender и BlockRenderer, а также классу RenderMesh, в Inner Core появилась возможность создавать блоки любой формы и размера. Для меня это стало ниточкой надежды на создание полноценной жидкости. Первое, что я сделал — это написал следующую функцию:

<pre lang="Javascript">function buildFlowMesh(texture, direction, from, to){
    var mesh = new RenderMesh();
    mesh.setBlockTexture(texture.name, texture.data || 0);
    
    var vertices = [
        [
            [0, 0, 0],
            [0, from, 0],
            [1, 0, 0],
        ],[ 
            [1, from, 0],
            [1, 0, 0],
            [0, from, 0],
        ],[ 
            [0, 0, 0],
            [0, 0, 1],
            [0, from, 0],
        ],[ 
            [0, to, 1],
            [0, from, 0],
            [0, 0, 1],
        ],[ 
            [1, 0, 1],
            [1, 0, 0],
            [0, 0, 1],
        ],[  
            [1, 0, 1],
            [0, 0, 1],
            [1, 0, 0],
        ],[  
            [0, 0, 1],
            [1, 0, 1],
            [0, to, 1],
        ],[  
            [1, to, 1],
            [0, to, 1],
            [1, 0, 1],
        ],[  
            [1, 0, 0],
            [1, from, 0],
            [1, 0, 1],
        ],[  
            [1, to, 1],
            [1, 0, 1],
            [1, from, 0],
        ],[  
            [0, from, 0],
            [0, to, 1],
            [1, from, 0],
        ],[  
            [1, to, 1],
            [1, from, 0],
            [0, to, 1]
        ]
    ];
    
    let rotation = getRotationMatrix(direction);
    
    for(var i = 0; i < vertices.length; i++){
        var vertex = vertices[i];
        let coords = numbers.matrix.multiply(vertex, rotation);
        if(direction == 2 || direction == 3){
            for(var j = 0; j < 3; j++){
                coords[j][0] += 1;
                coords[j][2] += 1;
            }
        }
        if(direction == 0 || direction == 2){
            mesh.addVertex(coords[0][0], coords[0][1], coords[0][2], i % 2, i % 2);
            mesh.addVertex(coords[1][0], coords[1][1], coords[1][2], (i + 1) % 2, i % 2);
            mesh.addVertex(coords[2][0], coords[2][1], coords[2][2], i % 2, (i + 1) % 2);
        } else {
            mesh.addVertex(coords[0][0], coords[0][1], coords[0][2], i % 2, i % 2);
            mesh.addVertex(coords[2][0], coords[2][1], coords[2][2], i % 2, (i + 1) % 2);
            mesh.addVertex(coords[1][0], coords[1][1], coords[1][2], (i + 1) % 2, i % 2);
        }
        
    }
    
    mesh.rebuild();
    return mesh;
}

function getRotationMatrix(direction){
    switch(direction){
        case 0: return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
        case 1: return [[0, 0, 1], [0, 1, 0], [1, 0, 0]];
        case 2: return [[-1, 0, 0], [0, 1, 0], [0, 0, -1]];
        case 3: return [[0, 0, -1], [0, 1, 0], [-1, 0, 0]];
    }
}</pre>

Эта функция построена на основе матрицы поворота и позволяет получить рендер наклонного блока под любым углом, указав высоту (в частях от целого) одной стороны и другой стороны. Следующим пунктом было создать рендеры для всех необходимых поворотов и склонов. Для рассчёта их пригодится параметр fluidity (текучесть) — чем она больше, тем дальше будет течь жидкость, и тем более пологие отдельные детали.

<pre lang="Javascript">var models = [];
// Partial liquid
var step = 15 / fluidity;
for(var i = 0; i < fluidity; i++){
    for(var j = 0; j < 4; j++){
        let render = new ICRender.Model();
        render.addEntry(buildFlowMesh(texture, j, (i + 1) * step, i * step));
        models.push(render);
    }        
}
        
// Full liquid
let render = new ICRender.Model();
render.addEntry(buildFlowMesh(texture, 3, 15/16, 15/16));
models.push(render);
        
BlockRenderer.setStaticICRender(BlockID.liquidFlow, 0, render);
BlockRenderer.setStaticICRender(BlockID.liquidSource, 0, render);
BlockRenderer.enableCoordMapping(BlockID.liquidFlow, 0, render);</pre>

Тут мы набираем набор моделей, чтобы потом маппать (накладывать) на блоки на определённых координатах.

Для удобства разделения источников и течений зарегистрированы два блока: источник и течение (как и с ванильными жидкостями). А вот направление течения будет решать уже рендер.

<pre lang="Javascript">var BLOCK_TYPE_LIQUID = Block.createSpecialType({
    base: 8,
    opaque: false
}, "liquid");


IDRegistry.genBlockID("liquidSource");
Block.createBlock("liquidSource", [
    {name: "Liquid Source", inCreative: false}
], BLOCK_TYPE_LIQUID);


IDRegistry.genBlockID("liquidFlow");
Block.createBlock("liquidFlow", [
    {name: "Liquid Flow", inCreative: false}
], BLOCK_TYPE_LIQUID);</pre>

И вот тут уже сказывается первое важное ограничение RenderMesh — отсутствие прозрачности. Впрочем, это меня не остановило и я начал продумывать дальнейшую систему работы жидкостей. Вкратце, это должно работать следующим образом:

  • При установке/удалении жидкости или установке/удалении блока рядом с жидкостью добавить координаты жидкости в массив обновлений.
  • В тике раз в секунду (или в настраиваемый для каждой жидкости отдельно промежуток времени) проверять все обновляемые блоки:
    • Если блок — источник, то заменить воздух блоком течения снизу и по сторонам. При этом блоку задать уровень (в частях от единицы) меньше, чем у источника.
    • Если блок — течение, то сначала проверить, есть ли соседний источник с большим уровнем. Если нету, этот источник заменить на источник меньший, чем ближайший соседний. Если есть, то снизу и по сторонам заменить воздух на источники меньшего уровня.
    • Удалить текущий блок из массива обновлений.
    • Все вновь добавленные источники также добавить в массив обновлений.

Эта схема может показаться сложной на первый взгляд, но она достаточно точно отображает механику лавы и воды в Майнкрафте.

Тем не менее, после формулировки подобного алгоритма, появился следующий вопрос — угловые источники. Ведь склон воды не обязательно прямой. Иными словами, я представлял течение так:

А реально оно может быть и таким:

Собственно, это ещё не самая сложная форма, которую может принимать вода. Есть варианты, когда верхняя сторона вообще не плоская, а имеет определённое ребро.

Кроме ограничений визуальной части, проблема может возникнуть с физическими свойствами, а именно с вязкостью. Я вижу решение этой проблемы в постоянном уменьшении ускорения игрока и установке режима полёта.

Таким образом для реализации жидкостей в Inner Core требуется намного больше времени и сил, чем я мог ожидать, и сейчас я не располагаю соответствующими ресурсами. Для тех, кто хочет перенять эстафету и попробовать закончить начатое мной, оставляю уже написанный код, и (не знаю как красиво сказать по-русски) feel free to use it as you like.

<pre lang="Javascript">LIBRARY({
    name: "LiquidLib",
    version: 1,
    shared: true,
    api: "CoreEngine"
});


IMPORT("numbers.js");


var BLOCK_TYPE_LIQUID = Block.createSpecialType({
    base: 8,
    opaque: false
}, "liquid");


IDRegistry.genBlockID("liquidSource");
Block.createBlock("liquidSource", [
    {name: "Liquid Source", inCreative: false}
], BLOCK_TYPE_LIQUID);


IDRegistry.genBlockID("liquidFlow");
Block.createBlock("liquidFlow", [
    {name: "Liquid Flow", inCreative: false}
], BLOCK_TYPE_LIQUID);


var LiquidLib = {
    liquids: {},
    /*
     * Creates liquid block
     * @param liquidId string liquid id registered using LiquidRegistry
     * @param texture texture information
     * @param fluidity determines how many blocks will liquid level 
     * reduce before stopping
     */
    registerLiquid: function(liquidId, texture, fluidity){
        if(!LiquidRegistry.isExists(liquidId)){
            throw new Excepion("Liquid id doesn't exist!");
        }
        
        var models = [];
        var step = 15 / fluidity;
        for(var i = 0; i < fluidity; i++){
            for(var j = 0; j < 4; j++){
                let render = new ICRender.Model();
                render.addEntry(buildFlowMesh(texture, j, (i + 1) * step, i * step));
                models.push(render);
            }
            
        }
        
        // full liquid
        let render = new ICRender.Model();
        render.addEntry(buildFlowMesh(texture, 3, 15/16, 15/16));
        models.push(render);
        
        BlockRenderer.setStaticICRender(BlockID.liquidFlow, 0, render);
        BlockRenderer.setStaticICRender(BlockID.liquidSource, 0, render);
        BlockRenderer.enableCoordMapping(BlockID.liquidFlow, 0, render);
        
        
        LiquidLib.liquids[liquidId] = {
            fluidity: fluidity,
            models: models
        }
    }
};


function buildFlowMesh(texture, direction, from, to){
    var mesh = new RenderMesh();
    mesh.setBlockTexture(texture.name, texture.data || 0);
    
    var vertices = [
        [
            [0, 0, 0],
            [0, from, 0],
            [1, 0, 0],
        ],[ 
            [1, from, 0],
            [1, 0, 0],
            [0, from, 0],
        ],[ 
            [0, 0, 0],
            [0, 0, 1],
            [0, from, 0],
        ],[ 
            [0, to, 1],
            [0, from, 0],
            [0, 0, 1],
        ],[ 
            [1, 0, 1],
            [1, 0, 0],
            [0, 0, 1],
        ],[  
            [1, 0, 1],
            [0, 0, 1],
            [1, 0, 0],
        ],[  
            [0, 0, 1],
            [1, 0, 1],
            [0, to, 1],
        ],[  
            [1, to, 1],
            [0, to, 1],
            [1, 0, 1],
        ],[  
            [1, 0, 0],
            [1, from, 0],
            [1, 0, 1],
        ],[  
            [1, to, 1],
            [1, 0, 1],
            [1, from, 0],
        ],[  
            [0, from, 0],
            [0, to, 1],
            [1, from, 0],
        ],[  
            [1, to, 1],
            [1, from, 0],
            [0, to, 1]
        ]
    ];
    
    let rotation = getRotationMatrix(direction);
    
    for(var i = 0; i < vertices.length; i++){
        var vertex = vertices[i];
        let coords = numbers.matrix.multiply(vertex, rotation);
        if(direction == 2 || direction == 3){
            for(var j = 0; j < 3; j++){
                coords[j][0] += 1;
                coords[j][2] += 1;
            }
        }
        if(direction == 0 || direction == 2){
            mesh.addVertex(coords[0][0], coords[0][1], coords[0][2], i % 2, i % 2);
            mesh.addVertex(coords[1][0], coords[1][1], coords[1][2], (i + 1) % 2, i % 2);
            mesh.addVertex(coords[2][0], coords[2][1], coords[2][2], i % 2, (i + 1) % 2);
        } else {
            mesh.addVertex(coords[0][0], coords[0][1], coords[0][2], i % 2, i % 2);
            mesh.addVertex(coords[2][0], coords[2][1], coords[2][2], i % 2, (i + 1) % 2);
            mesh.addVertex(coords[1][0], coords[1][1], coords[1][2], (i + 1) % 2, i % 2);
        }
        
    }
    
    mesh.rebuild();
    return mesh;
}

function getRotationMatrix(direction){
    switch(direction){
        case 0: return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
        case 1: return [[0, 0, 1], [0, 1, 0], [1, 0, 0]];
        case 2: return [[-1, 0, 0], [0, 1, 0], [0, 0, -1]];
        case 3: return [[0, 0, -1], [0, 1, 0], [-1, 0, 0]];
    }
}


var SIDES = [
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1],
    [-1, 0, 0],
    [0, -1, 0],
    [0, 0, -1]
];


var LiquidUpdater = {
    blocks: [],
    updates: [],
    
    update: function(x, y, z){
        updates.push({x: x, y: y, z: z});
    },
    
    tick: function(){
        
    }
}


Callback.addCallback("tick", function(){
    LiquidUpdater.tick();
});


LiquidRegistry.registerLiquid("myLiquid", "My Liquid", []);
LiquidLib.registerLiquid("myLiquid", {name: "my_liquid"}, 16);

Callback.addCallback("ItemUse", function(coords, item, block){
    let x = coords.relative.x;
    let y = coords.relative.y;
    let z = coords.relative.z;
    if(item.id == 280){
        World.setBlock(x, y, z, BlockID.liquidSource, 0);  
        LiquidUpdater.blocks.push({x: x, y: y, z: z, id: "myLiquid"});
        LiquidUpdater.update(x, y, z);
    }
});

EXPORT("LiquidLib", LiquidLib);</pre>
Запись опубликована в рубрике InnerCore. Добавьте в закладки постоянную ссылку.

Comments

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *