Allow using animated entities as map tiles
A more optimized implementation for animated tiles is planned.
This commit is contained in:
parent
4443ca2058
commit
2c6cd362c3
5 changed files with 133 additions and 42 deletions
12
dist/resources/entity/water.json
vendored
Normal file
12
dist/resources/entity/water.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"sprite": "water",
|
||||||
|
"frames": 4,
|
||||||
|
"animation": {
|
||||||
|
"sequence": [
|
||||||
|
[500, 0],
|
||||||
|
[500, 1],
|
||||||
|
[500, 2],
|
||||||
|
[500, 3]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
34
dist/resources/map/test.json
vendored
34
dist/resources/map/test.json
vendored
|
@ -1,22 +1,22 @@
|
||||||
{
|
{
|
||||||
"tiles": [
|
"tiles": [
|
||||||
"stone/floor",
|
"-stone/floor",
|
||||||
"stone/plate",
|
"-stone/plate",
|
||||||
"stone/wall/top",
|
"-stone/wall/top",
|
||||||
"stone/wall/right",
|
"-stone/wall/right",
|
||||||
"stone/wall/bottom",
|
"-stone/wall/bottom",
|
||||||
"stone/wall/left",
|
"-stone/wall/left",
|
||||||
"stone/wall/top_left",
|
"-stone/wall/top_left",
|
||||||
"stone/wall/top_right",
|
"-stone/wall/top_right",
|
||||||
"stone/wall/bottom_right",
|
"-stone/wall/bottom_right",
|
||||||
"stone/wall/bottom_left",
|
"-stone/wall/bottom_left",
|
||||||
"stone/wall/top_left_inner",
|
"-stone/wall/top_left_inner",
|
||||||
"stone/wall/top_right_inner",
|
"-stone/wall/top_right_inner",
|
||||||
"stone/wall/bottom_right_inner",
|
"-stone/wall/bottom_right_inner",
|
||||||
"stone/wall/bottom_left_inner",
|
"-stone/wall/bottom_left_inner",
|
||||||
"water",
|
"@water",
|
||||||
"stone/border/bottom_right",
|
"-stone/border/bottom_right",
|
||||||
"stone/border/bottom_left"
|
"-stone/border/bottom_left"
|
||||||
],
|
],
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
|
|
BIN
dist/resources/sprite/entity/water.png
vendored
Normal file
BIN
dist/resources/sprite/entity/water.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
dist/resources/sprite/tile/water.png
vendored
BIN
dist/resources/sprite/tile/water.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 778 B |
129
src/view/map.ts
129
src/view/map.ts
|
@ -1,3 +1,4 @@
|
||||||
|
import { EntityView } from './entity';
|
||||||
import { Renderer } from './renderer/renderer';
|
import { Renderer } from './renderer/renderer';
|
||||||
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
|
||||||
import { loadImage, mkTexture } from './util/image';
|
import { loadImage, mkTexture } from './util/image';
|
||||||
|
@ -6,39 +7,91 @@ import { MapData } from '../model/data/map';
|
||||||
|
|
||||||
import { nextPowerOf2 } from '../util';
|
import { nextPowerOf2 } from '../util';
|
||||||
|
|
||||||
interface Tileset {
|
import { vec2 } from 'gl-matrix';
|
||||||
texture: WebGLTexture;
|
|
||||||
tiles: SpriteCoords[];
|
interface StaticMapTile {
|
||||||
|
type: 'static';
|
||||||
|
image: HTMLImageElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTiles(tiles: string[]): Promise<HTMLImageElement[]> {
|
interface EntityTile {
|
||||||
return Promise.all(tiles.map((t) => loadImage(`resources/sprite/tile/${t}.png`)));
|
type: 'entity';
|
||||||
|
entity: EntityView;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapTile = StaticMapTile | EntityTile;
|
||||||
|
|
||||||
|
interface StaticTilesetTile {
|
||||||
|
type: 'static';
|
||||||
|
coords: SpriteCoords;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TilesetTile = StaticTilesetTile | EntityTile;
|
||||||
|
|
||||||
|
interface Tileset {
|
||||||
|
texture: WebGLTexture;
|
||||||
|
tiles: TilesetTile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTile(r: Renderer, tile: string): Promise<MapTile> {
|
||||||
|
const name = tile.substr(1);
|
||||||
|
switch (tile[0]) {
|
||||||
|
case '-':
|
||||||
|
return {
|
||||||
|
type: 'static',
|
||||||
|
image: await loadImage(`resources/sprite/tile/${name}.png`),
|
||||||
|
};
|
||||||
|
|
||||||
|
case '@':
|
||||||
|
return {
|
||||||
|
type: 'entity',
|
||||||
|
entity: await EntityView.load(r, name),
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('invalid tile specifier');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
|
||||||
|
return Promise.all(tiles.map((tile) => loadTile(r, tile)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mkTileset(
|
function mkTileset(
|
||||||
r: Renderer,
|
r: Renderer,
|
||||||
tiles: HTMLImageElement[],
|
mapTiles: MapTile[],
|
||||||
): Tileset {
|
): Tileset {
|
||||||
const tileSize = 32;
|
const tileSize = 32;
|
||||||
|
|
||||||
const canvasDim = nextPowerOf2(Math.sqrt(tiles.length));
|
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
|
||||||
const canvasSize = canvasDim * tileSize;
|
const canvasSize = canvasDim * tileSize;
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = canvas.height = canvasSize;
|
canvas.width = canvas.height = canvasSize;
|
||||||
|
|
||||||
let x = 0, y = 0;
|
let x = 0, y = 0;
|
||||||
const tileCoords: SpriteCoords[] = [];
|
const tiles: TilesetTile[] = [];
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
|
||||||
for (const tile of tiles) {
|
for (const tile of mapTiles) {
|
||||||
ctx.drawImage(tile, x * tileSize, y * tileSize);
|
switch (tile.type) {
|
||||||
tileCoords.push([x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim]);
|
case 'static':
|
||||||
|
ctx.drawImage(tile.image, x * tileSize, y * tileSize);
|
||||||
|
tiles.push({
|
||||||
|
type: 'static',
|
||||||
|
coords: [x / canvasDim, y / canvasDim, (x + 1) / canvasDim, (y + 1) / canvasDim],
|
||||||
|
});
|
||||||
|
|
||||||
x++;
|
x++;
|
||||||
if (x === canvasDim) {
|
if (x === canvasDim) {
|
||||||
x = 0;
|
x = 0;
|
||||||
y++;
|
y++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'entity':
|
||||||
|
tiles.push(tile);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,40 +99,66 @@ function mkTileset(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
texture,
|
texture,
|
||||||
tiles: tileCoords,
|
tiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSprite(builder: SpriteViewBuilder, tileset: Tileset, x: number, y: number, tile: number) {
|
function addSprite(
|
||||||
if (tile === 0)
|
builder: SpriteViewBuilder,
|
||||||
|
entityTiles: Array<[[number, number], EntityView]>,
|
||||||
|
tileset: Tileset,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
tile: number) {
|
||||||
|
if (!tile)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const tilePos = tileset.tiles[tile - 1];
|
const tilesetTile = tileset.tiles[tile - 1];
|
||||||
builder.addSprite([x, y, x + 1, y + 1], tilePos);
|
|
||||||
|
switch (tilesetTile.type) {
|
||||||
|
case 'static':
|
||||||
|
builder.addSprite([x, y, x + 1, y + 1], tilesetTile.coords);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'entity':
|
||||||
|
entityTiles.push([[x + 0.5, y + 0.5], tilesetTile.entity]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
|
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
|
||||||
const builder = new SpriteViewBuilder(r, tileset.texture);
|
const builder = new SpriteViewBuilder(r, tileset.texture);
|
||||||
|
const entityTiles: Array<[[number, number], EntityView]> = [];
|
||||||
|
|
||||||
for (let x = 0; x < layer[0].length; x++)
|
for (let x = 0; x < layer[0].length; x++)
|
||||||
for (let y = 0; y < layer.length; y++)
|
for (let y = 0; y < layer.length; y++)
|
||||||
addSprite(builder, tileset, x, y, layer[y][x]);
|
addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
|
||||||
|
|
||||||
return new MapLayerView(builder.build());
|
return new MapLayerView(r, builder.build(), entityTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapLayerView {
|
class MapLayerView {
|
||||||
public constructor(private sprites: SpriteView) {
|
public constructor(
|
||||||
|
private r: Renderer,
|
||||||
|
private staticTiles: SpriteView,
|
||||||
|
private entityTiles: Array<[[number, number], EntityView]>,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(time: number): void {
|
public render(time: number): void {
|
||||||
this.sprites.render();
|
this.r.setTranslation([0, 0]);
|
||||||
|
this.staticTiles.render();
|
||||||
|
|
||||||
|
for (const [coords, entity] of this.entityTiles) {
|
||||||
|
this.r.setTranslation(coords);
|
||||||
|
entity.renderByTime(time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MapView {
|
export class MapView {
|
||||||
public static async load(r: Renderer, map: MapData): Promise<MapView> {
|
public static async load(r: Renderer, map: MapData): Promise<MapView> {
|
||||||
const tiles = await loadTiles(map.tiles);
|
const tiles = await loadTiles(r, map.tiles);
|
||||||
const tileset = mkTileset(r, tiles);
|
const tileset = mkTileset(r, tiles);
|
||||||
|
|
||||||
const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));
|
const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));
|
||||||
|
|
Reference in a new issue