Жидкости в 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>
Comments