Add support for simple periodic sprite animations
This commit is contained in:
parent
426b2c37ff
commit
b83b596b0f
7 changed files with 90 additions and 22 deletions
23
dist/resources/entity/square.json
vendored
Normal file
23
dist/resources/entity/square.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"sprite": "square",
|
||||||
|
"collision": [
|
||||||
|
{
|
||||||
|
"type": "polygon",
|
||||||
|
"vertices": [
|
||||||
|
[-0.46875, -0.46875],
|
||||||
|
[-0.46875, 0.46875],
|
||||||
|
[0.46875, 0.46875],
|
||||||
|
[0.46875, -0.46875]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"frames": 4,
|
||||||
|
"animation": {
|
||||||
|
"sequence": [
|
||||||
|
[500, 0],
|
||||||
|
[500, 1],
|
||||||
|
[500, 2],
|
||||||
|
[500, 3]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
dist/resources/sprite/entity/square.png
vendored
BIN
dist/resources/sprite/entity/square.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 808 B After Width: | Height: | Size: 528 B |
|
@ -1,4 +1,4 @@
|
||||||
import { EntityData } from '../model/data/entity';
|
import { EntityAnimation, EntityData } from '../model/data/entity';
|
||||||
|
|
||||||
import { loadEntity } from '../view/entity';
|
import { loadEntity } from '../view/entity';
|
||||||
import { Renderer } from '../view/renderer/renderer';
|
import { Renderer } from '../view/renderer/renderer';
|
||||||
|
@ -20,21 +20,30 @@ export class EntityContext implements CollidableGroup {
|
||||||
name,
|
name,
|
||||||
await loadEntity(renderer, entity),
|
await loadEntity(renderer, entity),
|
||||||
mkCollision(entity.collision),
|
mkCollision(entity.collision),
|
||||||
|
entity.animation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly pos: vec2 = vec2.create();
|
public readonly pos: vec2 = vec2.create();
|
||||||
|
|
||||||
|
private readonly totalTime: number;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly renderer: Renderer,
|
private readonly renderer: Renderer,
|
||||||
private readonly name: string,
|
private readonly name: string,
|
||||||
private readonly view: SpriteView,
|
private readonly sprites: SpriteView[],
|
||||||
private readonly collision: Collidable[],
|
private readonly collision: Collidable[],
|
||||||
) {}
|
private readonly animation?: EntityAnimation,
|
||||||
|
) {
|
||||||
|
if (animation)
|
||||||
|
this.totalTime = animation.sequence.reduce((a, s) => a + s[0], 0);
|
||||||
|
else
|
||||||
|
this.totalTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render(time: number) {
|
||||||
this.renderer.setTranslation(this.pos);
|
this.renderer.setTranslation(this.pos);
|
||||||
this.view.render();
|
this.getSprite(time).render();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTranslation(): vec2 {
|
public getTranslation(): vec2 {
|
||||||
|
@ -48,4 +57,18 @@ export class EntityContext implements CollidableGroup {
|
||||||
public interact() {
|
public interact() {
|
||||||
alert(`You've interacted with ${this.name}!`);
|
alert(`You've interacted with ${this.name}!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSprite(time: number): SpriteView {
|
||||||
|
time %= this.totalTime;
|
||||||
|
|
||||||
|
if (this.animation) {
|
||||||
|
for (const [len, sprite] of this.animation.sequence) {
|
||||||
|
time -= len;
|
||||||
|
if (time < 0)
|
||||||
|
return this.sprites[sprite];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sprites[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,22 @@ export class GameContext implements CollidableGroup {
|
||||||
const map = this.loadMap(renderer, 'test');
|
const map = this.loadMap(renderer, 'test');
|
||||||
const loadPlayer = EntityContext.load(renderer, 'green_circle');
|
const loadPlayer = EntityContext.load(renderer, 'green_circle');
|
||||||
const loadEntity = EntityContext.load(renderer, 'red_circle');
|
const loadEntity = EntityContext.load(renderer, 'red_circle');
|
||||||
|
const loadEntity2 = EntityContext.load(renderer, 'square');
|
||||||
|
|
||||||
const [mapView, mapCollision] = await map;
|
const [mapView, mapCollision] = await map;
|
||||||
const player = await loadPlayer;
|
const player = await loadPlayer;
|
||||||
const entity = await loadEntity;
|
const entity = await loadEntity;
|
||||||
|
const entity2 = await loadEntity2;
|
||||||
|
|
||||||
vec2.set(player.pos, 6, 6);
|
vec2.set(player.pos, 6, 6);
|
||||||
vec2.set(entity.pos, 3, 3);
|
vec2.set(entity.pos, 3, 3);
|
||||||
|
vec2.set(entity2.pos, 3, 8);
|
||||||
|
|
||||||
return new GameContext(
|
return new GameContext(
|
||||||
renderer,
|
renderer,
|
||||||
mapView,
|
mapView,
|
||||||
player,
|
player,
|
||||||
[entity],
|
[entity, entity2],
|
||||||
mapCollision,
|
mapCollision,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -168,7 +171,7 @@ export class GameContext implements CollidableGroup {
|
||||||
this.mapView.render();
|
this.mapView.render();
|
||||||
|
|
||||||
for (const r of [...this.entities, this.player])
|
for (const r of [...this.entities, this.player])
|
||||||
r.render();
|
r.render(time);
|
||||||
|
|
||||||
window.requestAnimationFrame(this.render);
|
window.requestAnimationFrame(this.render);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
import { Collision } from './collision';
|
import { Collision } from './collision';
|
||||||
|
|
||||||
|
export interface EntityAnimation {
|
||||||
|
readonly sequence: ReadonlyArray<[number, number]>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EntityDataInput {
|
export interface EntityDataInput {
|
||||||
readonly sprite: string;
|
readonly sprite: string;
|
||||||
readonly anchor?: [number, number];
|
readonly anchor?: [number, number];
|
||||||
readonly collision?: Collision[];
|
readonly collision?: Collision[];
|
||||||
|
readonly frames?: number;
|
||||||
|
readonly animation?: EntityAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EntityData {
|
export class EntityData {
|
||||||
public readonly sprite: string;
|
public readonly sprite: string;
|
||||||
public readonly anchor: [number, number];
|
public readonly anchor: [number, number];
|
||||||
public readonly collision: Collision[];
|
public readonly collision: Collision[];
|
||||||
|
public readonly frames: number;
|
||||||
|
public readonly animation?: EntityAnimation;
|
||||||
|
|
||||||
constructor(input: EntityDataInput) {
|
constructor(input: EntityDataInput) {
|
||||||
this.sprite = input.sprite;
|
this.sprite = input.sprite;
|
||||||
this.anchor = input.anchor || [0.5, 0.5];
|
this.anchor = input.anchor || [0.5, 0.5];
|
||||||
this.collision = input.collision || [];
|
this.collision = input.collision || [];
|
||||||
|
this.frames = input.frames || 1;
|
||||||
|
this.animation = input.animation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,13 @@ import { vec2 } from 'gl-matrix';
|
||||||
export async function loadEntity(
|
export async function loadEntity(
|
||||||
r: Renderer,
|
r: Renderer,
|
||||||
data: EntityData,
|
data: EntityData,
|
||||||
): Promise<SpriteView> {
|
): Promise<SpriteView[]> {
|
||||||
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
|
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
|
||||||
const [texture, size, coords] = mkTexture(r, tile);
|
|
||||||
|
const sprites: SpriteView[] = [];
|
||||||
|
|
||||||
|
for (let frame = 0; frame < data.frames; frame++) {
|
||||||
|
const [texture, size, coords] = mkTexture(r, tile, frame, data.frames);
|
||||||
|
|
||||||
const offset = vec2.mul(vec2.create(), data.anchor, size);
|
const offset = vec2.mul(vec2.create(), data.anchor, size);
|
||||||
r.snapToGrid(offset, offset);
|
r.snapToGrid(offset, offset);
|
||||||
|
@ -24,5 +28,8 @@ export async function loadEntity(
|
||||||
|
|
||||||
const builder = new SpriteViewBuilder(r, texture);
|
const builder = new SpriteViewBuilder(r, texture);
|
||||||
builder.addSprite(anchorCoords, [0, 0, 1, 1]);
|
builder.addSprite(anchorCoords, [0, 0, 1, 1]);
|
||||||
return builder.build();
|
sprites.push(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprites;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,15 @@ export function loadImage(url: string): Promise<HTMLImageElement> {
|
||||||
export function mkTexture(
|
export function mkTexture(
|
||||||
r: Renderer,
|
r: Renderer,
|
||||||
src: HTMLCanvasElement|HTMLImageElement,
|
src: HTMLCanvasElement|HTMLImageElement,
|
||||||
|
frame: number = 0,
|
||||||
|
total: number = 1,
|
||||||
): [WebGLTexture, [number, number], SpriteCoords] {
|
): [WebGLTexture, [number, number], SpriteCoords] {
|
||||||
const gl = r.getContext();
|
const gl = r.getContext();
|
||||||
const texture = gl.createTexture();
|
const texture = gl.createTexture();
|
||||||
if (!texture)
|
if (!texture)
|
||||||
throw new Error('unable to create texture');
|
throw new Error('unable to create texture');
|
||||||
|
|
||||||
const w = src.width, h = src.height;
|
const w = src.width, h = src.height / total;
|
||||||
const w2 = nextPowerOf2(w), h2 = nextPowerOf2(h);
|
const w2 = nextPowerOf2(w), h2 = nextPowerOf2(h);
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
@ -28,7 +30,7 @@ export function mkTexture(
|
||||||
canvas.height = h2;
|
canvas.height = h2;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||||
ctx.drawImage(src, 0, 0);
|
ctx.drawImage(src, 0, frame * h, w, h, 0, 0, w, h);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
||||||
|
|
Reference in a new issue