Optimize autotile layers

This commit is contained in:
2025-07-18 23:14:16 +02:00
parent 4cb710a1b5
commit 9f463cf553
12 changed files with 465 additions and 541 deletions

View File

@@ -1,9 +1,9 @@
package com.bartlomiejpluta.base.engine.world.autotile.manager;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.TextureManager;
import com.bartlomiejpluta.base.engine.error.AppException;
import com.bartlomiejpluta.base.engine.project.config.ProjectConfiguration;
import com.bartlomiejpluta.base.engine.util.mesh.MeshManager;
import com.bartlomiejpluta.base.engine.world.autotile.asset.AutoTileSetAsset;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import lombok.RequiredArgsConstructor;
@@ -19,14 +19,12 @@ import java.util.Map;
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class DefaultAutoTileSetManager implements AutoTileManager {
private final TextureManager textureManager;
private final MeshManager meshManager;
private final Map<String, AutoTileSet> autoTiles = new HashMap<>();
private final Map<String, AutoTileSet> tileSets = new HashMap<>();
private final Map<String, AutoTileSetAsset> assets = new HashMap<>();
private final ProjectConfiguration configuration;
@Override
public void init() {
// TODO: this.mesh = meshManager.createQuad(1, 1, 0, 0);
}
@Override
@@ -42,9 +40,9 @@ public class DefaultAutoTileSetManager implements AutoTileManager {
@Override
public AutoTileSet loadObject(String uid) {
var autoTile = autoTiles.get(uid);
var tileSet = tileSets.get(uid);
if (autoTile == null) {
if (tileSet == null) {
var asset = assets.get(uid);
if (asset == null) {
@@ -53,11 +51,12 @@ public class DefaultAutoTileSetManager implements AutoTileManager {
var source = configuration.projectFile("autotiles", asset.getSource());
var texture = textureManager.loadTexture(source, asset.getRows() * asset.getLayout().getRows() * 2, asset.getColumns() * asset.getLayout().getColumns() * 2);
autoTile = new AutoTileSet(texture, asset.getRows(), asset.getColumns(), asset.getLayout());
tileSet = new AutoTileSet(texture, asset.getLayout());
log.info("Loading auto tile set from assets to cache under the key: [{}]", uid);
autoTiles.put(uid, autoTile);
tileSets.put(uid, tileSet);
}
return autoTile;
return tileSet;
}
}

View File

@@ -1,62 +0,0 @@
package com.bartlomiejpluta.base.engine.world.autotile.model;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.world.object.Sprite;
import lombok.Getter;
import lombok.NonNull;
import org.joml.Vector2f;
import org.joml.Vector2fc;
import org.joml.Vector2ic;
@Getter
public class AutoSubTile extends Sprite {
private final Vector2fc tileSpriteSize;
private final Vector2f tileScale = new Vector2f(1, 1);
public AutoSubTile(AutoTileSet autoTileSet) {
super(Material.textured(autoTileSet.getTexture()));
tileSpriteSize = getMaterial().getTexture().getSpriteSize();
super.setScale(tileSpriteSize.x() * tileScale.x, tileSpriteSize.y() * tileScale.y);
}
public void recalculate(@NonNull Vector2ic spritePosition) {
setFrame(spritePosition.x(), spritePosition.y());
}
@Override
public void setScale(float scale) {
this.tileScale.x = scale;
this.tileScale.y = scale;
super.setScale(tileSpriteSize.x() * scale, tileSpriteSize.y() * scale);
}
public void setScale(float scaleX, float scaleY) {
this.tileScale.x = scaleX;
this.tileScale.y = scaleY;
super.setScale(tileSpriteSize.x() * scaleX, tileSpriteSize.y() * scaleY);
}
@Override
public float getScaleX() {
return tileScale.x;
}
@Override
public void setScaleX(float scaleX) {
this.tileScale.x = scaleX;
super.setScaleX(tileSpriteSize.x() * scaleX);
}
@Override
public float getScaleY() {
return tileScale.y;
}
@Override
public void setScaleY(float scaleY) {
this.tileScale.y = scaleY;
super.setScaleY(tileSpriteSize.x() * scaleY);
}
}

View File

@@ -1,72 +0,0 @@
package com.bartlomiejpluta.base.engine.world.autotile.model;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.Getter;
import lombok.NonNull;
public class AutoTile implements Renderable {
private final AutoTileSet autoTileSet;
private final AutoSubTile topLeftSubTile;
private final AutoSubTile topRightSubTile;
private final AutoSubTile bottomLeftSubTile;
private final AutoSubTile bottomRightSubTile;
@Getter
private int setId;
public AutoTile(@NonNull AutoTileSet autoTileSet, int setId) {
this.topLeftSubTile = new AutoSubTile(autoTileSet);
this.topRightSubTile = new AutoSubTile(autoTileSet);
this.bottomLeftSubTile = new AutoSubTile(autoTileSet);
this.bottomRightSubTile = new AutoSubTile(autoTileSet);
this.autoTileSet = autoTileSet;
this.setId = setId;
}
public void setCoordinates(int column, int row) {
var x = column * autoTileSet.getTileSize().x() * 2;
var y = row * autoTileSet.getTileSize().y() * 2;
topLeftSubTile.setPosition(x, y);
topRightSubTile.setPosition(x + autoTileSet.getTileSize().x(), y);
bottomLeftSubTile.setPosition(x, y + autoTileSet.getTileSize().y());
bottomRightSubTile.setPosition(x + autoTileSet.getTileSize().x(), y + autoTileSet.getTileSize().y());
}
public void regularTile(Integer setId, int topLeft, int topRight, int bottomLeft, int bottomRight) {
if (setId == null) {
setId = this.setId;
}
topLeftSubTile.recalculate(autoTileSet.getTopLeftSubTiles()[setId][topLeft]);
topRightSubTile.recalculate(autoTileSet.getTopRightSubTiles()[setId][topRight]);
bottomLeftSubTile.recalculate(autoTileSet.getBottomLeftSubTiles()[setId][bottomLeft]);
bottomRightSubTile.recalculate(autoTileSet.getBottomRightSubTiles()[setId][bottomRight]);
}
public void islandTile(Integer setId) {
if (setId == null) {
setId = this.setId;
}
topLeftSubTile.recalculate(autoTileSet.getIslandSubTiles()[setId][0]);
topRightSubTile.recalculate(autoTileSet.getIslandSubTiles()[setId][1]);
bottomLeftSubTile.recalculate(autoTileSet.getIslandSubTiles()[setId][2]);
bottomRightSubTile.recalculate(autoTileSet.getIslandSubTiles()[setId][3]);
}
public void shiftTileSet() {
this.setId = (setId + 1) % autoTileSet.getSetsCount();
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
topLeftSubTile.render(screen, camera, shaderManager);
topRightSubTile.render(screen, camera, shaderManager);
bottomLeftSubTile.render(screen, camera, shaderManager);
bottomRightSubTile.render(screen, camera, shaderManager);
}
}

View File

@@ -4,201 +4,22 @@ import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import lombok.Getter;
import lombok.NonNull;
import org.joml.Vector2fc;
import org.joml.Vector2i;
import org.joml.Vector2ic;
import java.util.LinkedList;
// Algorithm source: https://love2d.org/forums/viewtopic.php?t=7826
@Getter
public class AutoTileSet {
private static final Vector2ic ISLAND_TILE_2x3 = new Vector2i(0, 0);
private static final Vector2ic CROSS_TILE_2x3 = new Vector2i(1, 0);
private static final Vector2ic TOP_LEFT_TILE_2x3 = new Vector2i(0, 1);
private static final Vector2ic TOP_RIGHT_TILE_2x3 = new Vector2i(1, 1);
private static final Vector2ic BOTTOM_LEFT_TILE_2x3 = new Vector2i(0, 2);
private static final Vector2ic BOTTOM_RIGHT_TILE_2x3 = new Vector2i(1, 2);
private static final Vector2ic TOP_LEFT_TILE_2x2 = new Vector2i(0, 0);
private static final Vector2ic TOP_RIGHT_TILE_2x2 = new Vector2i(1, 0);
private static final Vector2ic BOTTOM_LEFT_TILE_2x2 = new Vector2i(0, 1);
private static final Vector2ic BOTTOM_RIGHT_TILE_2x2 = new Vector2i(1, 1);
private final int textureSubTilesRows;
private final int textureSubTilesColumns;
@Getter
private Vector2ic[][] islandSubTiles;
@Getter
private Vector2ic[][] topLeftSubTiles;
@Getter
private Vector2ic[][] topRightSubTiles;
@Getter
private Vector2ic[][] bottomLeftSubTiles;
@Getter
private Vector2ic[][] bottomRightSubTiles;
@Getter
private final Texture texture;
@Getter
private final Vector2fc tileSize;
@Getter
private final AutoTileLayout layout;
private final int rows;
private final int columns;
@Getter
private final int setsColumns;
private final int setsRows;
private final int setsCount;
public AutoTileSet(@NonNull Texture texture, int rows, int columns, @NonNull AutoTileLayout layout) {
public AutoTileSet(@NonNull Texture texture, @NonNull AutoTileLayout layout) {
this.texture = texture;
this.rows = rows;
this.columns = columns;
this.tileSize = texture.getSpriteSize();
this.layout = layout;
this.setsCount = rows * columns;
this.textureSubTilesRows = layout.getRows() * 2;
this.textureSubTilesColumns = layout.getColumns() * 2;
switch (layout) {
case LAYOUT_2X2 -> init2x2();
case LAYOUT_2X3 -> init2x3();
}
}
private void init2x3() {
var islandSubTiles = new LinkedList<Vector2ic[]>();
var topLeftSubTiles = new LinkedList<Vector2ic[]>();
var topRightSubTiles = new LinkedList<Vector2ic[]>();
var bottomLeftSubTiles = new LinkedList<Vector2ic[]>();
var bottomRightSubTiles = new LinkedList<Vector2ic[]>();
for (int setId = 0; setId < setsCount; ++setId) {
var crossSubTiles = cutSubTiles(setId, CROSS_TILE_2x3);
var topLeftTileSubTiles = cutSubTiles(setId, TOP_LEFT_TILE_2x3);
var topRightTileSubTiles = cutSubTiles(setId, TOP_RIGHT_TILE_2x3);
var bottomLeftTileSubTiles = cutSubTiles(setId, BOTTOM_LEFT_TILE_2x3);
var bottomRightTileSubTiles = cutSubTiles(setId, BOTTOM_RIGHT_TILE_2x3);
/*
* Indexes:
* 0 - No connected tiles
* 1 - Left tile is connected
* 2 - Right tile is connected
* 3 - Left and Right tiles are connected
* 4 - Left, Right, and Center tiles are connected.
*/
var tl3 = crossSubTiles[0];
var tr3 = crossSubTiles[1];
var bl3 = crossSubTiles[2];
var br3 = crossSubTiles[3];
var tl0 = topLeftTileSubTiles[0];
var tr2 = topLeftTileSubTiles[1];
var bl1 = topLeftTileSubTiles[2];
var br4 = topLeftTileSubTiles[3];
var tl1 = topRightTileSubTiles[0];
var tr0 = topRightTileSubTiles[1];
var bl4 = topRightTileSubTiles[2];
var br2 = topRightTileSubTiles[3];
var tl2 = bottomLeftTileSubTiles[0];
var tr4 = bottomLeftTileSubTiles[1];
var bl0 = bottomLeftTileSubTiles[2];
var br1 = bottomLeftTileSubTiles[3];
var tl4 = bottomRightTileSubTiles[0];
var tr1 = bottomRightTileSubTiles[1];
var bl2 = bottomRightTileSubTiles[2];
var br0 = bottomRightTileSubTiles[3];
islandSubTiles.add(cutSubTiles(setId, ISLAND_TILE_2x3));
topLeftSubTiles.add(new Vector2ic[]{tl0, tl1, tl2, tl3, tl4});
topRightSubTiles.add(new Vector2ic[]{tr0, tr1, tr2, tr3, tr4});
bottomLeftSubTiles.add(new Vector2ic[]{bl0, bl1, bl2, bl3, bl4});
bottomRightSubTiles.add(new Vector2ic[]{br0, br1, br2, br3, br4});
}
this.islandSubTiles = islandSubTiles.toArray(new Vector2ic[islandSubTiles.size()][]);
this.topLeftSubTiles = topLeftSubTiles.toArray(new Vector2ic[topLeftSubTiles.size()][]);
this.topRightSubTiles = topRightSubTiles.toArray(new Vector2ic[topRightSubTiles.size()][]);
this.bottomLeftSubTiles = bottomLeftSubTiles.toArray(new Vector2ic[bottomLeftSubTiles.size()][]);
this.bottomRightSubTiles = bottomRightSubTiles.toArray(new Vector2ic[bottomRightSubTiles.size()][]);
}
private void init2x2() {
var topLeftSubTiles = new LinkedList<Vector2ic[]>();
var topRightSubTiles = new LinkedList<Vector2ic[]>();
var bottomLeftSubTiles = new LinkedList<Vector2ic[]>();
var bottomRightSubTiles = new LinkedList<Vector2ic[]>();
for (int setId = 0; setId < setsCount; ++setId) {
var topLeftTileSubTiles = cutSubTiles(setId, TOP_LEFT_TILE_2x2);
var topRightTileSubTiles = cutSubTiles(setId, TOP_RIGHT_TILE_2x2);
var bottomLeftTileSubTiles = cutSubTiles(setId, BOTTOM_LEFT_TILE_2x2);
var bottomRightTileSubTiles = cutSubTiles(setId, BOTTOM_RIGHT_TILE_2x2);
/*
* Indexes:
* 0 - No connected tiles
* 1 - Left tile is connected
* 2 - Right tile is connected
* 3 - Left, Right, and Center tiles are connected.
*/
var tl0 = topLeftTileSubTiles[0];
var tr2 = topLeftTileSubTiles[1];
var bl1 = topLeftTileSubTiles[2];
var br3 = topLeftTileSubTiles[3];
var tl1 = topRightTileSubTiles[0];
var tr0 = topRightTileSubTiles[1];
var bl3 = topRightTileSubTiles[2];
var br2 = topRightTileSubTiles[3];
var tl2 = bottomLeftTileSubTiles[0];
var tr3 = bottomLeftTileSubTiles[1];
var bl0 = bottomLeftTileSubTiles[2];
var br1 = bottomLeftTileSubTiles[3];
var tl3 = bottomRightTileSubTiles[0];
var tr1 = bottomRightTileSubTiles[1];
var bl2 = bottomRightTileSubTiles[2];
var br0 = bottomRightTileSubTiles[3];
topLeftSubTiles.add(new Vector2ic[]{tl0, tl1, tl2, tl3});
topRightSubTiles.add(new Vector2ic[]{tr0, tr1, tr2, tr3});
bottomLeftSubTiles.add(new Vector2ic[]{bl0, bl1, bl2, bl3});
bottomRightSubTiles.add(new Vector2ic[]{br0, br1, br2, br3});
}
this.topLeftSubTiles = topLeftSubTiles.toArray(new Vector2ic[topLeftSubTiles.size()][]);
this.topRightSubTiles = topRightSubTiles.toArray(new Vector2ic[topRightSubTiles.size()][]);
this.bottomLeftSubTiles = bottomLeftSubTiles.toArray(new Vector2ic[bottomLeftSubTiles.size()][]);
this.bottomRightSubTiles = bottomRightSubTiles.toArray(new Vector2ic[bottomRightSubTiles.size()][]);
}
private Vector2ic[] cutSubTiles(int setId, Vector2ic tile) {
var topLeft = getTile(setId, tile.y() * 2, tile.x() * 2);
var topRight = getTile(setId, tile.y() * 2, tile.x() * 2 + 1);
var bottomLeft = getTile(setId, tile.y() * 2 + 1, tile.x() * 2);
var bottomRight = getTile(setId, tile.y() * 2 + 1, tile.x() * 2 + 1);
return new Vector2ic[]{topLeft, topRight, bottomLeft, bottomRight};
}
private Vector2ic getTile(int setId, int row, int column) {
return new Vector2i(((setId / columns) * textureSubTilesRows) + row, ((setId % columns) * textureSubTilesColumns) + column);
}
public AutoTile createTile(int setId) {
return new AutoTile(this, setId);
this.setsColumns = texture.getColumns() / layout.getColumns() / 2;
this.setsRows = texture.getRows() / layout.getRows() / 2;
this.setsCount = setsRows * setsColumns;
}
}

View File

@@ -0,0 +1,110 @@
package com.bartlomiejpluta.base.engine.world.map.layer.autotile;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
/*
* 0 1 2 3
*
* +-------------------+---------+---------+
* | . | . |
* 0 | TL0 . TR2 | TL1 . TR0 |
* | . | . |
* + . . . . + . . . . + . . . . + . . . . +
* | . | . |
* 1 | BL1 . BR3 | BL3 . BR2 |
* | . | . |
* +---------+---------+---------+---------+
* | . | . |
* 2 | TL2 . TR3 | TL3 . TR1 |
* | . | . |
* + . . . . + . . . . + . . . . + . . . . +
* | . | . |
* 3 | BL0 . BR1 | BL2 . BR0 |
* | . | . |
* +---------+---------+---------+---------+
*
* Legend:
* TL - top left
* TR - top right
* BL - bottom left
* BR - bottom right
* 0 - Not connected
* 1 - Left tile connected
* 2 - Right tile connected
* 3 - Left and right tiles connected
* 4 - Left, right and center tiles connected
*/
public class AutoTileLayer2x2 extends BaseAutoTileLayer {
private static final int ROWS = 2;
private static final int COLUMNS = 2;
private static final int SUB_ROWS = ROWS * 2;
private static final int SUB_COLUMNS = COLUMNS * 2;
// 0 1 2 3
private static final int[][] TOP_LEFT = {{0, 0}, {2, 0}, {0, 2}, {2, 2}};
private static final int[][] TOP_RIGHT = {{3, 0}, {3, 2}, {1, 0}, {1, 2}};
private static final int[][] BOTTOM_LEFT = {{0, 3}, {0, 1}, {2, 3}, {2, 1}};
private static final int[][] BOTTOM_RIGHT = {{3, 3}, {1, 3}, {3, 1}, {1, 3}};
private final int tileSetColumns;
public AutoTileLayer2x2(@NonNull GameMap map, @NonNull AutoTileSet tileSet, int rows, int columns, boolean connect, boolean animated, double animationDuration) {
super(map, tileSet, rows, columns, connect, animated, animationDuration);
this.tileSetColumns = tileSet.getSetsColumns();
}
@Override
protected void recalculateTile(int row, int column) {
if (layer[row][column] == EMPTY_TILE) {
return;
}
var topLeft = 0;
var topRight = 0;
var bottomLeft = 0;
var bottomRight = 0;
var tile = layer[row][column];
// Top
if (row > 0 && (layer[row - 1][column] != EMPTY_TILE && (connect || layer[row - 1][column] == tile))) {
topLeft += 2;
topRight += 1;
}
// Bottom
if (row < map.getRows() - 1 && (layer[row + 1][column] != EMPTY_TILE && (connect || layer[row + 1][column] == tile))) {
bottomLeft += 1;
bottomRight += 2;
}
// Left
if (column > 0 && (layer[row][column - 1] != EMPTY_TILE && (connect || layer[row][column - 1] == tile))) {
topLeft += 1;
bottomLeft += 2;
}
// Right
if (column < map.getColumns() - 1 && (layer[row][column + 1] != EMPTY_TILE && (connect || layer[row][column + 1] == tile))) {
topRight += 2;
bottomRight += 1;
}
setSubTiles(
tile % tileSetColumns * SUB_COLUMNS,
tile / tileSetColumns * SUB_ROWS,
row,
column,
TOP_LEFT[topLeft],
TOP_RIGHT[topRight],
BOTTOM_LEFT[bottomLeft],
BOTTOM_RIGHT[bottomRight]);
}
}

View File

@@ -0,0 +1,138 @@
package com.bartlomiejpluta.base.engine.world.map.layer.autotile;
import com.bartlomiejpluta.base.api.map.layer.autotile.AutoTileLayer;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
/*
* 0 1 2 3
*
* +-------------------+---------+---------+
* | | . |
* 0 | | TL3 . TR3 |
* | | . |
* | ISLAND + . . . . + . . . . +
* | | . |
* 1 | | BL3 . BR3 |
* | | . |
* +-------------------+---------+---------+
* | . | . |
* 2 | TL0 . TR2 | TL1 . TR0 |
* | . | . |
* + . . . . + . . . . + . . . . + . . . . +
* | . | . |
* 3 | BL1 . BR4 | BL4 . BR2 |
* | . | . |
* +---------+---------+---------+---------+
* | . | . |
* 4 | TL2 . TR4 | TL4 . TR1 |
* | . | . |
* + . . . . + . . . . + . . . . + . . . . +
* | . | . |
* 5 | BL0 . BR1 | BL2 . BR0 |
* | . | . |
* +---------+---------+---------+---------+
*
* Legend:
* TL - top left
* TR - top right
* BL - bottom left
* BR - bottom right
* 0 - Not connected
* 1 - Left tile connected
* 2 - Right tile connected
* 3 - Left and right tiles connected
* 4 - Left, right and center tiles connected
*/
public class AutoTileLayer3x2 extends BaseAutoTileLayer implements AutoTileLayer {
private static final int ROWS = 3;
private static final int COLUMNS = 2;
private static final int SUB_ROWS = ROWS * 2;
private static final int SUB_COLUMNS = COLUMNS * 2;
// 0 1 2 3 4
private static final int[][] TOP_LEFT = {{0, 2}, {2, 2}, {0, 4}, {2, 0}, {2, 4}};
private static final int[][] TOP_RIGHT = {{3, 2}, {3, 4}, {1, 2}, {3, 0}, {1, 4}};
private static final int[][] BOTTOM_LEFT = {{0, 5}, {0, 3}, {2, 5}, {2, 1}, {2, 3}};
private static final int[][] BOTTOM_RIGHT = {{3, 5}, {1, 5}, {3, 3}, {3, 1}, {1, 3}};
private final int tileSetColumns;
public AutoTileLayer3x2(@NonNull GameMap map, @NonNull AutoTileSet tileSet, int rows, int columns, boolean connect, boolean animated, double animationDuration) {
super(map, tileSet, rows, columns, connect, animated, animationDuration);
this.tileSetColumns = tileSet.getSetsColumns();
}
@Override
protected void recalculateTile(int row, int column) {
if (layer[row][column] == EMPTY_TILE) {
return;
}
byte topLeft = 0;
byte topRight = 0;
byte bottomLeft = 0;
byte bottomRight = 0;
var tile = layer[row][column];
// Top
if (row > 0 && (layer[row - 1][column] != EMPTY_TILE && (connect || layer[row - 1][column] == tile))) {
topLeft += 2;
topRight += 1;
}
// Bottom
if (row < map.getRows() - 1 && (layer[row + 1][column] != EMPTY_TILE && (connect || layer[row + 1][column] == tile))) {
bottomLeft += 1;
bottomRight += 2;
}
// Left
if (column > 0 && (layer[row][column - 1] != EMPTY_TILE && (connect || layer[row][column - 1] == tile))) {
topLeft += 1;
bottomLeft += 2;
}
// Right
if (column < map.getColumns() - 1 && (layer[row][column + 1] != EMPTY_TILE && (connect || layer[row][column + 1] == tile))) {
topRight += 2;
bottomRight += 1;
}
// Top left
if (row > 0 && column > 0 && (layer[row - 1][column - 1] != EMPTY_TILE && (connect || layer[row - 1][column - 1] == tile)) && topLeft == 3) {
topLeft = 4;
}
// Top right
if (row > 0 && column < map.getColumns() - 1 && (layer[row - 1][column + 1] != EMPTY_TILE && (connect || layer[row - 1][column + 1] == tile)) && topRight == 3) {
topRight = 4;
}
// Bottom left
if (row < map.getRows() - 1 && column > 0 && (layer[row + 1][column - 1] != EMPTY_TILE && (connect || layer[row + 1][column - 1] == tile)) && bottomLeft == 3) {
bottomLeft = 4;
}
// Bottom right
if (row < map.getRows() - 1 && column < map.getColumns() - 1 && (layer[row + 1][column + 1] != EMPTY_TILE && (connect || layer[row + 1][column + 1] == tile)) && bottomRight == 3) {
bottomRight = 4;
}
setSubTiles(
tile % tileSetColumns * SUB_COLUMNS,
tile / tileSetColumns * SUB_ROWS,
row,
column,
TOP_LEFT[topLeft],
TOP_RIGHT[topRight],
BOTTOM_LEFT[bottomLeft],
BOTTOM_RIGHT[bottomRight]);
}
}

View File

@@ -0,0 +1,117 @@
package com.bartlomiejpluta.base.engine.world.map.layer.autotile;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import com.bartlomiejpluta.base.engine.world.map.layer.base.BaseLayer;
import com.bartlomiejpluta.base.engine.world.map.layer.tile.ChunkManager;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.NonNull;
import org.joml.Vector2f;
import static java.util.Arrays.fill;
import static java.util.Arrays.stream;
// Algorithm source: https://love2d.org/forums/viewtopic.php?t=7826
public abstract class BaseAutoTileLayer extends BaseLayer {
protected static final int EMPTY_TILE = -1;
protected final int[][] layer;
protected final ChunkManager chunkManager;
protected final boolean connect;
protected final boolean animated;
protected final double animationDuration;
private final int setsCount;
private float accumulator = 0f;
private boolean dirty = true;
public BaseAutoTileLayer(@NonNull GameMap map, @NonNull AutoTileSet tileSet, int rows, int columns, boolean connect, boolean animated, double animationDuration) {
super(map);
this.layer = new int[rows][columns];
this.connect = connect;
this.animated = animated;
this.animationDuration = animationDuration;
this.setsCount = tileSet.getSetsCount();
this.chunkManager = new ChunkManager(
tileSet.getTexture(),
rows * 4,
columns * 4,
new Vector2f(tileSet.getTexture().getSpriteSize()).div(2)
);
stream(layer).forEach(tiles -> fill(tiles, EMPTY_TILE));
}
public void setTile(int row, int column, int setId) {
layer[row][column] = setId;
dirty = true;
}
public void clearTile(int row, int column) {
layer[row][column] = EMPTY_TILE;
dirty = true;
}
public void recalculate() {
for (int row = 0; row < map.getRows(); ++row) {
for (int column = 0; column < map.getColumns(); ++column) {
recalculateTile(row, column);
}
}
dirty = false;
}
protected abstract void recalculateTile(int row, int column);
protected void setSubTiles(int tileSetColumnOffset, int tileSetRowOffset, int row, int column, int[] topLeft, int[] topRight, int[] bottomLeft, int[] bottomRight) {
var y = row * 4;
var x = column * 4;
chunkManager.upsertTile(y, x, topLeft[0] + tileSetColumnOffset, topLeft[1] + tileSetRowOffset);
chunkManager.upsertTile(y, x + 2, topRight[0] + tileSetColumnOffset, topRight[1] + tileSetRowOffset);
chunkManager.upsertTile(y + 2, x, bottomLeft[0] + tileSetColumnOffset, bottomLeft[1] + tileSetRowOffset);
chunkManager.upsertTile(y + 2, x + 2, bottomRight[0] + tileSetColumnOffset, bottomRight[1] + tileSetRowOffset);
}
private void nextAnimationFrame() {
for (int row = 0; row < map.getRows(); ++row) {
for (int column = 0; column < map.getColumns(); ++column) {
var tile = layer[row][column];
if (tile != EMPTY_TILE) {
layer[row][column] = (tile + 1) % setsCount;
}
}
}
dirty = true;
}
@Override
public void update(float dt) {
super.update(dt);
if (animated) {
accumulator += dt;
if (accumulator > animationDuration) {
nextAnimationFrame();
accumulator = 0;
}
}
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
if (dirty) {
recalculate();
}
chunkManager.render(screen, camera, shaderManager);
// super.render(screen, camera, shaderManager);
}
}

View File

@@ -1,198 +0,0 @@
package com.bartlomiejpluta.base.engine.world.map.layer.autotile;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.map.layer.autotile.AutoTileLayer;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTile;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import com.bartlomiejpluta.base.engine.world.map.layer.base.BaseLayer;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.NonNull;
import java.util.Arrays;
// Algorithm source: https://love2d.org/forums/viewtopic.php?t=7826
public class DefaultAutoTileLayer extends BaseLayer implements AutoTileLayer {
private final AutoTileSet autoTileSet;
private final AutoTile[][] layer;
private final boolean animated;
private final double animationDuration;
private final boolean connect;
private double accumulator;
public DefaultAutoTileLayer(@NonNull GameMap map, @NonNull AutoTileSet autoTileSet, int rows, int columns, boolean animated, double animationDuration, boolean connect) {
super(map);
this.autoTileSet = autoTileSet;
this.animated = animated;
this.animationDuration = animationDuration;
this.connect = connect;
layer = new AutoTile[rows][columns];
Arrays.stream(layer).forEach(tiles -> Arrays.fill(tiles, null));
}
public void setTile(int row, int column, int setId) {
var tile = autoTileSet.createTile(setId);
tile.setCoordinates(column, row);
layer[row][column] = tile;
}
public void clearTile(int row, int column) {
layer[row][column] = null;
}
public void recalculate(Integer setId) {
for (int row = 0; row < map.getRows(); ++row) {
for (int column = 0; column < map.getColumns(); ++column) {
recalculateTile(setId, row, column);
}
}
}
private void recalculateTile(Integer setId, int row, int column) {
switch(autoTileSet.getLayout()) {
case LAYOUT_2X2 -> recalculateTile2x2(setId, row, column);
case LAYOUT_2X3 -> recalculateTile2x3(setId, row, column);
}
}
private void recalculateTile2x2(Integer setId, int row, int column) {
if (layer[row][column] == null) {
return;
}
var topLeft = 0;
var topRight = 0;
var bottomLeft = 0;
var bottomRight = 0;
var tile = layer[row][column];
var centerSetId = tile.getSetId();
if (animated) {
tile.shiftTileSet();
}
// Top
if (row > 0 && (layer[row - 1][column] != null && (connect || layer[row - 1][column].getSetId() == centerSetId))) {
topLeft += 2;
topRight += 1;
}
// Bottom
if (row < map.getRows() - 1 && (layer[row + 1][column] != null && (connect || layer[row + 1][column].getSetId() == centerSetId))) {
bottomLeft += 1;
bottomRight += 2;
}
// Left
if (column > 0 && (layer[row][column - 1] != null && (connect || layer[row][column - 1].getSetId() == centerSetId))) {
topLeft += 1;
bottomLeft += 2;
}
// Right
if (column < map.getColumns() - 1 && (layer[row][column + 1] != null && (connect || layer[row][column + 1].getSetId() == centerSetId))) {
topRight += 2;
bottomRight += 1;
}
tile.regularTile(setId, topLeft, topRight, bottomLeft, bottomRight);
}
private void recalculateTile2x3(Integer setId, int row, int column) {
if (layer[row][column] == null) {
return;
}
var topLeft = 0;
var topRight = 0;
var bottomLeft = 0;
var bottomRight = 0;
var tile = layer[row][column];
var centerSetId = tile.getSetId();
if (animated) {
tile.shiftTileSet();
}
// Top
if (row > 0 && (layer[row - 1][column] != null && (connect || layer[row - 1][column].getSetId() == centerSetId))) {
topLeft += 2;
topRight += 1;
}
// Bottom
if (row < map.getRows() - 1 && (layer[row + 1][column] != null && (connect || layer[row + 1][column].getSetId() == centerSetId))) {
bottomLeft += 1;
bottomRight += 2;
}
// Left
if (column > 0 && (layer[row][column - 1] != null && (connect || layer[row][column - 1].getSetId() == centerSetId))) {
topLeft += 1;
bottomLeft += 2;
}
// Right
if (column < map.getColumns() - 1 && (layer[row][column + 1] != null && (connect || layer[row][column + 1].getSetId() == centerSetId))) {
topRight += 2;
bottomRight += 1;
}
// Top left
if (row > 0 && column > 0 && (layer[row - 1][column - 1] != null && (connect || layer[row - 1][column - 1].getSetId() == centerSetId)) && topLeft == 3) {
topLeft = 4;
}
// Top right
if (row > 0 && column < map.getColumns() - 1 && (layer[row - 1][column + 1] != null && (connect || layer[row - 1][column + 1].getSetId() == centerSetId)) && topRight == 3) {
topRight = 4;
}
// Bottom left
if (row < map.getRows() - 1 && column > 0 && (layer[row + 1][column - 1] != null && (connect || layer[row + 1][column - 1].getSetId() == centerSetId)) && bottomLeft == 3) {
bottomLeft = 4;
}
// Bottom right
if (row < map.getRows() - 1 && column < map.getColumns() - 1 && (layer[row + 1][column + 1] != null && (connect || layer[row + 1][column + 1].getSetId() == centerSetId)) && bottomRight == 3) {
bottomRight = 4;
}
if (topLeft == 0 && topRight == 0 && bottomLeft == 0 && bottomRight == 0) {
tile.islandTile(setId);
return;
}
tile.regularTile(setId, topLeft, topRight, bottomLeft, bottomRight);
}
@Override
public void update(float dt) {
super.update(dt);
if (animated) {
accumulator += dt;
if (accumulator > animationDuration) {
accumulator = 0;
recalculate(null);
}
}
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
for (var row : layer) {
for (var tile : row) {
if (tile != null) {
tile.render(screen, camera, shaderManager);
}
}
}
super.render(screen, camera, shaderManager);
}
}

View File

@@ -6,6 +6,8 @@ import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.internal.gc.Disposable;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.NonNull;
import org.joml.Vector2fc;
import java.util.HashMap;
import java.util.Map;
@@ -25,12 +27,20 @@ public class ChunkManager implements Renderable, Disposable {
this(tileSet, rows, columns, DEFAULT_CHUNK_SIZE);
}
public ChunkManager(Texture tileSet, int rows, int columns, Vector2fc tileSize) {
this(tileSet, rows, columns, tileSize, DEFAULT_CHUNK_SIZE);
}
public ChunkManager(Texture tileSet, int rows, int columns, int chunkSize) {
this(tileSet, rows, columns, tileSet.getSpriteSize(), chunkSize);
}
public ChunkManager(@NonNull Texture tileSet, int rows, int columns, @NonNull Vector2fc tileSize, int chunkSize) {
this.tileSet = tileSet;
this.mapRows = rows;
this.mapColumns = columns;
this.tileWidth = tileSet.getSpriteSize().x();
this.tileHeight = tileSet.getSpriteSize().y();
this.tileWidth = tileSize.x();
this.tileHeight = tileSize.y();
this.chunkSize = chunkSize;
}
@@ -63,6 +73,21 @@ public class ChunkManager implements Renderable, Disposable {
return chunk;
}
public void setTile(int row, int column, int tileX, int tileY) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
throw new IllegalArgumentException("Tile coordinates out of bounds");
}
clearTile(row, column);
var chunk = getOrCreateChunk(row, column);
var chunkKey = getChunkKey(row, column);
var localTileKey = getLocalTileKey(row, column);
var quadId = chunk.addTile(column % chunkSize * (int) this.tileHeight, row % chunkSize * (int) this.tileWidth, tileX, tileY);
chunkTileIds.get(chunkKey).put(localTileKey, quadId);
}
public void setTile(int row, int column, int tileId) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
@@ -80,6 +105,33 @@ public class ChunkManager implements Renderable, Disposable {
chunkTileIds.get(chunkKey).put(localTileKey, quadId);
}
public void updateTile(int row, int column, int tileId) {
var chunk = getOrCreateChunk(row, column);
var chunkKey = getChunkKey(row, column);
var localTileKey = getLocalTileKey(row, column);
var quadId = chunkTileIds.get(chunkKey).get(localTileKey);
chunk.updateTile(quadId, tileId);
}
public void upsertTile(int row, int column, int tileX, int tileY) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
throw new IllegalArgumentException("Tile coordinates out of bounds");
}
var chunk = getOrCreateChunk(row, column);
var chunkKey = getChunkKey(row, column);
var localTileKey = getLocalTileKey(row, column);
var tileMap = chunkTileIds.get(chunkKey);
var quadId = tileMap.get(localTileKey);
if (quadId == null) {
quadId = chunk.addTile(column % chunkSize * (int) this.tileHeight, row % chunkSize * (int) this.tileWidth, tileX, tileY);
chunkTileIds.get(chunkKey).put(localTileKey, quadId);
} else {
chunk.updateTile(quadId, tileX, tileY);
}
}
public void clearTile(int row, int column) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
return;

View File

@@ -35,10 +35,22 @@ public class TileChunk extends Model implements Placeable, Renderable, Disposabl
this.originY = originY;
}
public int addTile(int x, int y, int tileX, int tileY) {
return mesh.addQuad(template, x, y, tileSet.getTextureCoordinates(tileX, tileY));
}
public int addTile(int x, int y, int tileId) {
return mesh.addQuad(template, x, y, tileSet.getTextureCoordinates(tileId));
}
public void updateTile(int quadId, int tileX, int tileY) {
mesh.setQuadTextureCoordinates(quadId, tileSet.getTextureCoordinates(tileX, tileY));
}
public void updateTile(int quadId, int tileId) {
mesh.setQuadTextureCoordinates(quadId, tileSet.getTextureCoordinates(tileId));
}
public void removeTile(int quadId) {
mesh.removeQuad(quadId);
}

View File

@@ -12,10 +12,12 @@ import com.bartlomiejpluta.base.api.map.layer.object.PassageAbility;
import com.bartlomiejpluta.base.api.map.layer.tile.TileLayer;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.UniformName;
import com.bartlomiejpluta.base.engine.util.mesh.MeshManager;
import com.bartlomiejpluta.base.engine.world.autotile.model.AutoTileSet;
import com.bartlomiejpluta.base.engine.world.map.layer.autotile.DefaultAutoTileLayer;
import com.bartlomiejpluta.base.engine.world.map.layer.autotile.AutoTileLayer2x2;
import com.bartlomiejpluta.base.engine.world.map.layer.autotile.AutoTileLayer3x2;
import com.bartlomiejpluta.base.engine.world.map.layer.color.DefaultColorLayer;
import com.bartlomiejpluta.base.engine.world.map.layer.image.DefaultImageLayer;
import com.bartlomiejpluta.base.engine.world.map.layer.object.DefaultObjectLayer;
@@ -139,8 +141,15 @@ public class DefaultGameMap implements Renderable, Updatable, GameMap {
return layer;
}
public DefaultAutoTileLayer createAutoTileLayer(@NonNull AutoTileSet autoTileSet, boolean animated, double animationDuration, boolean connect) {
var layer = new DefaultAutoTileLayer(this, autoTileSet, rows, columns, animated, animationDuration, connect);
public AutoTileLayer3x2 createAutoTileLayer3x2(@NonNull AutoTileSet tileSet, boolean animated, double animationDuration, boolean connect) {
var layer = new AutoTileLayer3x2(this, tileSet, rows, columns, connect, animated, animationDuration);
layers.add(layer);
return layer;
}
public AutoTileLayer2x2 createAutoTileLayer2x2(@NonNull AutoTileSet tileSet, boolean animated, double animationDuration, boolean connect) {
var layer = new AutoTileLayer2x2(this, tileSet, rows, columns, connect, animated, animationDuration);
layers.add(layer);
return layer;

View File

@@ -73,21 +73,19 @@ public class ProtobufMapDeserializer extends MapDeserializer {
var animationDuration = proto.getAutoTileLayer().getAnimationDuration();
var connect = proto.getAutoTileLayer().getConnect();
var layer = map.createAutoTileLayer(autoTileSet, animated, animationDuration, connect);
var layer = switch(autoTileSet.getLayout()) {
case LAYOUT_2X3 -> map.createAutoTileLayer3x2(autoTileSet, animated, animationDuration, connect);
case LAYOUT_2X2 -> map.createAutoTileLayer2x2(autoTileSet, animated, animationDuration, connect);
};
var columns = map.getColumns();
var tiles = proto.getAutoTileLayer().getTilesList();
for (var i = 0; i < tiles.size(); ++i) {
var tile = tiles.get(i);
if (tile == 0) {
layer.clearTile(i / columns, i % columns);
} else {
layer.setTile(i / columns, i % columns, tile - 1);
}
layer.setTile(i / columns, i % columns, tile - 1);
}
layer.recalculate(null);
}
private void deserializeObjectLayer(DefaultGameMap map, GameMapProto.Layer proto) {