Skip to main content

Make Tilemap Layer

This time, we'll going to make layers by stacking several tile maps. In general, maps that use all tile maps are required to layered.

Add Island Layer

We will add one more tile map and overlap it.

src/asset/Bootstrapper.ts
.withChild(instantiater.buildGameObject("tilemap")
.withChild(instantiater.buildGameObject("background", new Vector3(0, 0, -2))
.withComponent(CssTilemapChunkRenderer, c => {
//...
}))

.withChild(instantiater.buildGameObject("islands", new Vector3(0, 0, -1))
.withComponent(CssTilemapChunkRenderer, c => {
c.chunkSize = 15;
c.filter.brightness = 1.5;
c.tileResolutionX = 16;
c.tileResolutionY = 16;
AsyncImageLoader.loadImageFromPath(OverworldTileset).then(image => {
if (!c.exists) return;

c.imageSources = [ new TileAtlasItem(image, 18, 13) ];

});
})))

Tile maps were parented on a single game object "tilemap" and then added the "islands" layer.

Let us draw an island on the tile map.

I'll create a separate function to make it procedural.

src/asset/script/MakeIsland.ts
import { Vector2 } from "three/src/Three";

const topLeft = { i: 0 as const, a: 4 };
const topCenter = { i: 0 as const, a: 5 };
const topRight = { i: 0 as const, a: 7};
const left = { i: 0 as const, a: 22 };
const right = { i: 0 as const, a: 25 };
const bottomLeft = { i: 0 as const, a: 40 };
const bottomCenter = { i: 0 as const, a: 41 };
const bottomRight = { i: 0 as const, a: 43 };

export class MakeIsland {
public static make(
width: number,
height: number,
topEntry: number|null,
bottomEntry: number| null
): ({ i: 0; a: number; }|null)[][] {
const array2d: ({ i: 0; a: number; }|null)[][] = Array(height);
for (let i = 0; i < height; i++) {
array2d[i] = Array(width);
for (let j = 0; j < width; j++) {
array2d[i][j] = null;
}
}

array2d[0][0] = topLeft;
for (let i = 1; i < width - 1; i++) {
array2d[0][i] = topCenter;
}
array2d[0][width - 1] = topRight;
for (let i = 1; i < height - 1; i++) {
array2d[i][0] = left;
array2d[i][width - 1] = right;
}
array2d[height - 1][0] = bottomLeft;
for (let i = 1; i < width - 1; i++) {
array2d[height - 1][i] = bottomCenter;
}
array2d[height - 1][width - 1] = bottomRight;

if (topEntry !== null) {
array2d[0][topEntry] = null;
}

if (bottomEntry !== null) {
array2d[height - 1][bottomEntry] = null;
}

return array2d;
}

public static computeEntryPosition(
height: number,
topEntry: number|null,
bottomEntry: number| null,
x: number,
y: number
): { top: Vector2|null, bottom: Vector2|null } {
const top = topEntry !== null ? new Vector2(x + topEntry, y + height - 1) : null;
const bottom = bottomEntry !== null ? new Vector2(x + bottomEntry, y) : null;
return { top, bottom };
}
}
src/asset/Bootstrapper.ts
const islandList = [
{ width: 16, height: 6, topEntry: 1, bottomEntry: 2, x: -4, y: 3 },
{ width: 15, height: 8, topEntry: 8, bottomEntry: 3, x: -8, y: -9 }
];

return this.sceneBuilder
.withChild(instantiater.buildGameObject("tilemap")
.withChild(instantiater.buildGameObject("background", new Vector3(0, 0, -2))
.withComponent(CssTilemapChunkRenderer, c => {
//...
}))

.withChild(instantiater.buildGameObject("islands", new Vector3(0, 0, -1))
.withComponent(CssTilemapChunkRenderer, c => {
c.chunkSize = 15;
c.filter.brightness = 1.5;
c.tileResolutionX = 16;
c.tileResolutionY = 16;

AsyncImageLoader.loadImageFromPath(OverworldTileset).then(image => {
if (!c.exists) return;

c.imageSources = [ new TileAtlasItem(image, 18, 13) ];

for (const island of islandList) {
c.drawTileFromTwoDimensionalArray(
MakeIsland.make(island.width, island.height, island.topEntry, island.bottomEntry),
island.x, island.y
);
}
});
})
.withComponent(CssTilemapChunkRenderer, c => {
c.chunkSize = 15;
c.filter.brightness = 1.5;
c.tileResolutionX = 16;
c.tileResolutionY = 16;

const topEntry = 6;
const bottomEntry = 42;

AsyncImageLoader.loadImageFromPath(OverworldTileset).then(image => {
if (!c.exists) return;

c.imageSources = [ new TileAtlasItem(image, 18, 13) ];

for (const island of islandList) {
const entry = MakeIsland.computeEntryPosition(
island.height, island.topEntry, island.bottomEntry, island.x, island.y);

if (entry.top) c.drawTile(entry.top.x, entry.top.y, 0, topEntry);
if (entry.bottom) c.drawTile(entry.bottom.x, entry.bottom.y, 0, bottomEntry);
}
});
})))

look at the code, the entry is divided into components and rendered.

The reason for rendering the entrance separately is that this is more convenient for later collision processing. We'll learn more about this later.

Detail Layer

we'll going to make another layer and add small details.

This is completely simple labor, so you can just paste the code that I'm giving you.

src/asset/Bootstrapper.ts
.withChild(instantiater.buildGameObject("background", new Vector3(0, 0, -3))
.withComponent(CssTilemapChunkRenderer, c => {
//...
}))

.withChild(instantiater.buildGameObject("islands", new Vector3(0, 0, -2))
.withComponent(CssTilemapChunkRenderer, c => {
//...
})
.withComponent(CssTilemapChunkRenderer, c => {
//...
}))

.withChild(instantiater.buildGameObject("detail", new Vector3(0, 0, -1))
.withComponent(CssTilemapChunkRenderer, c => {
c.chunkSize = 15;
c.filter.brightness = 1.5;
c.tileResolutionX = 16;
c.tileResolutionY = 16;

AsyncImageLoader.loadImageFromPath(OverworldTileset).then(image => {
if (!c.exists) return;

c.imageSources = [ new TileAtlasItem(image, 18, 13) ];

function f(a: number): { i: 0; a: number; } {
return { i: 0, a: a };
}

const converter = {
//rock
"o": () => f(93),
"O": () => f(94),
//sign
"#": () => f(61),
//stump
"@": () => f(75),
"$": () => f(76),
//bush
"*": () => f(58),
"^": () => f(59),

" ": () => null
};

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
" ",
" $ ",
" # ",
" ",
" "
],
converter
),
-3, 4
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
" ",
" o ",
" @ ",
" # ",
" ",
" ",
"O "
],
converter
),
-7, -8
);
});
})
.withComponent(CssTilemapChunkRenderer, c => {
c.chunkSize = 15;
c.filter.brightness = 1.5;
c.tileResolutionX = 16;
c.tileResolutionY = 16;

AsyncImageLoader.loadImageFromPath(OverworldTileset).then(image => {
if (!c.exists) return;

c.imageSources = [ new TileAtlasItem(image, 18, 13) ];

function f(a: number): { i: 0; a: number; } {
return { i: 0, a: a };
}

const roadConverter = {
"ㅡ": () => f(47),
"ㅣ": () => f(46),
"ㅕ": () => f(11),
"ㅑ": () => f(29),
"ㅏ": () => f(44),
"ㅓ": () => f(45),
"ㅗ": () => f(62),
"ㅜ": () => f(63),
"ㄱ": () => f(8),
"ㄴ": () => f(26),
"ㄲ": () => f(9),
"ㄹ": () => f(27),
"ㅛ": () => f(28),
"ㅠ": () => f(10),
"ㅇ": () => null
};

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"ㅠ",
"ㄴㄱ",
"ㅇㅏㅡㅡㅡㅡㅡㅕ",
"ㅇㅛ"
],
roadConverter
),
-3, 4
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"ㅇㅇㅇㅇㅇㅇㅠ",
"ㅇㅇㅇㅇㅇㅇㅣ",
"ㅑㅡㅡㅡㅡㅡㅗㅡㅜㅡㅡㅡㅡㅡㅡㅕ",
"ㅇㅇㅇㅇㅇㅇㅇㅇㅛ"
],
roadConverter
),
-8, -1
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"ㅇㅇㅇㅇㅇㅠ",
"ㅇㅇㅇㅇㅇㅣ",
"ㅇㅇㅇㅇㅇㅣ",
"ㄲㅡㅡㅡㅡㄹ",
"ㅣㅇㅇㅇㅇㅇ",
"ㅛㅇㅇㅇㅇㅇ"
],
roadConverter
),
-5, -8
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"ㅠ",
"ㅣ",
"ㅣ",
"ㅣ",
"ㅣ",
"ㅛㅇㅇㅇㅇㅇ"
],
roadConverter
),
-5, -15
);

const foliageConverter = {
//grass
"^": () => f(56),
"%": () => f(57),
//flower
"*": () => f(72),
"&": () => f(73),
"!": () => f(90),
"~": () => f(91),
//rock
"o": () => f(74),
"O": () => f(92),

" ": () => null
};

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
" ^",
"",
"^ % ~*"
],
foliageConverter
),
3, 4
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"O% ^",
" ^",
"",
"^ %"
],
foliageConverter
),
1, -1
);

c.drawTileFromTwoDimensionalArray(
TwoDimensionalStringMapper.map(
[
"% ^",
" ",
" *~",
" % !~",
" !**",
"o !**~"
],
foliageConverter
),
-2, -8
);
});
}))