From 5eae6f29a80cd47e268cb6eaefa96a0ab0a63e54 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Thu, 8 Nov 2018 22:03:08 +0100 Subject: Implement more flexible handling of entities and collidables --- dist/resources/entity/green_circle.json | 3 + dist/resources/entity/red_circle.json | 14 ++++ dist/resources/sprite/entity/green_circle.png | Bin 0 -> 1776 bytes dist/resources/sprite/entity/red_circle.png | Bin 0 -> 1844 bytes dist/resources/sprite/entity/simple_circle.png | Bin 1776 -> 0 bytes src/controller/collision.ts | 59 +++++++++++++ src/controller/entitycontext.ts | 45 ++++++++++ src/controller/gamecontext.ts | 110 +++++++++++-------------- src/model/data/collision.ts | 6 ++ src/model/data/entity.ts | 19 +++++ src/model/data/map.ts | 11 +-- src/view/entity.ts | 10 +-- 12 files changed, 204 insertions(+), 73 deletions(-) create mode 100644 dist/resources/entity/green_circle.json create mode 100644 dist/resources/entity/red_circle.json create mode 100644 dist/resources/sprite/entity/green_circle.png create mode 100644 dist/resources/sprite/entity/red_circle.png delete mode 100644 dist/resources/sprite/entity/simple_circle.png create mode 100644 src/controller/collision.ts create mode 100644 src/controller/entitycontext.ts create mode 100644 src/model/data/collision.ts create mode 100644 src/model/data/entity.ts diff --git a/dist/resources/entity/green_circle.json b/dist/resources/entity/green_circle.json new file mode 100644 index 0000000..14952b2 --- /dev/null +++ b/dist/resources/entity/green_circle.json @@ -0,0 +1,3 @@ +{ + "sprite": "green_circle" +} diff --git a/dist/resources/entity/red_circle.json b/dist/resources/entity/red_circle.json new file mode 100644 index 0000000..c9479ae --- /dev/null +++ b/dist/resources/entity/red_circle.json @@ -0,0 +1,14 @@ +{ + "sprite": "red_circle", + "collision": [ + { + "type": "polygon", + "vertices": [ + [-0.5, -0.5], + [-0.5, 0.5], + [0.5, 0.5], + [0.5, -0.5] + ] + } + ] +} diff --git a/dist/resources/sprite/entity/green_circle.png b/dist/resources/sprite/entity/green_circle.png new file mode 100644 index 0000000..71cdadb Binary files /dev/null and b/dist/resources/sprite/entity/green_circle.png differ diff --git a/dist/resources/sprite/entity/red_circle.png b/dist/resources/sprite/entity/red_circle.png new file mode 100644 index 0000000..1bdf36c Binary files /dev/null and b/dist/resources/sprite/entity/red_circle.png differ diff --git a/dist/resources/sprite/entity/simple_circle.png b/dist/resources/sprite/entity/simple_circle.png deleted file mode 100644 index 71cdadb..0000000 Binary files a/dist/resources/sprite/entity/simple_circle.png and /dev/null differ diff --git a/src/controller/collision.ts b/src/controller/collision.ts new file mode 100644 index 0000000..e1bd9f9 --- /dev/null +++ b/src/controller/collision.ts @@ -0,0 +1,59 @@ +import { Collision } from '../model/data/collision'; + +import { Collidable } from '../math/collision'; +import { LineSegment, Movement } from '../math/line'; +import { Point } from '../math/point'; + +import { vec2 } from 'gl-matrix'; + +export function mkCollision(collision: Collision[]): Collidable[] { + const ret: Collidable[] = []; + + for (const c of collision) { + switch (c.type) { + case 'polygon': + if (!c.vertices.length) + continue; + + let prev = c.vertices[c.vertices.length - 1]; + + for (const v of c.vertices) { + ret.push(LineSegment.fromPoints(vec2.clone(prev), vec2.clone(v))); + prev = v; + } + + for (const v of c.vertices) { + ret.push(new Point(vec2.clone(v))); + prev = v; + } + } + } + + return ret; +} + +export interface CollidableGroup { + getTranslation(): vec2|null; + getCollidables(): Collidable[]; +} + +export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean { + const t = collision.getTranslation(); + if (t) + move = move.translate(vec2.negate(vec2.create(), t)); + + for (const c of collision.getCollidables()) { + if (!c.collide(out, move, radius)) + continue; + + if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) + continue; + + if (t) + vec2.add(out, out, t); + + return true; + } + + return false; +} diff --git a/src/controller/entitycontext.ts b/src/controller/entitycontext.ts new file mode 100644 index 0000000..c11c698 --- /dev/null +++ b/src/controller/entitycontext.ts @@ -0,0 +1,45 @@ +import { EntityData } from '../model/data/entity'; + +import { loadEntity } from '../view/entity'; +import { Renderer } from '../view/renderer/renderer'; +import { SpriteView } from '../view/sprite'; + +import { Collidable } from '../math/collision'; + +import { getJSON } from '../util'; +import { CollidableGroup, mkCollision } from './collision'; + +import { vec2 } from 'gl-matrix'; + +export class EntityContext implements CollidableGroup { + public static async load(renderer: Renderer, name: string): Promise { + const entity = new EntityData(await getJSON(`resources/entity/${name}.json`)); + + return new EntityContext( + renderer, + await loadEntity(renderer, entity), + mkCollision(entity.collision), + ); + } + + public readonly pos: vec2 = vec2.create(); + + private constructor( + private readonly renderer: Renderer, + private readonly view: SpriteView, + public readonly collision: Collidable[], + ) {} + + public render() { + this.renderer.setTranslation(this.pos); + this.view.render(); + } + + public getTranslation(): vec2 { + return this.pos; + } + + public getCollidables(): Collidable[] { + return this.collision; + } +} diff --git a/src/controller/gamecontext.ts b/src/controller/gamecontext.ts index b69b306..8ec54b5 100644 --- a/src/controller/gamecontext.ts +++ b/src/controller/gamecontext.ts @@ -1,60 +1,44 @@ -import { Collision, MapData } from '../model/data/map'; +import { CollidableGroup, collide, mkCollision } from './collision'; +import { EntityContext } from './entitycontext'; + +import { MapData } from '../model/data/map'; -import { loadSimpleEntity } from '../view/entity'; import { DirectionHandler } from '../view/input/directionhandler'; import { loadMap } from '../view/map'; import { Renderer } from '../view/renderer/renderer'; import { SpriteView } from '../view/sprite'; import { Collidable } from '../math/collision'; -import { LineSegment, Movement } from '../math/line'; -import { Point } from '../math/point'; +import { Movement } from '../math/line'; import { getJSON } from '../util'; import { vec2 } from 'gl-matrix'; -export class GameContext { +export class GameContext implements CollidableGroup { public static async load(renderer: Renderer): Promise { - const entity = loadSimpleEntity(renderer, 'simple_circle'); - const [mapView, collision] = await this.loadMap(renderer); + const map = this.loadMap(renderer, 'test'); + const loadPlayer = EntityContext.load(renderer, 'green_circle'); + const loadEntity = EntityContext.load(renderer, 'red_circle'); + + const [mapView, mapCollision] = await map; + const player = await loadPlayer; + const entity = await loadEntity; + + vec2.set(player.pos, 6, 6); + vec2.set(entity.pos, 3, 3); return new GameContext( renderer, mapView, - await entity, - collision, + player, + [entity], + mapCollision, ); } - private static mkCollision(collision: Collision[]): Collidable[] { - const ret: Collidable[] = []; - - for (const c of collision) { - switch (c.type) { - case 'polygon': - if (!c.vertices.length) - continue; - - let prev = c.vertices[c.vertices.length - 1]; - - for (const v of c.vertices) { - ret.push(LineSegment.fromPoints(vec2.clone(prev), vec2.clone(v))); - prev = v; - } - - for (const v of c.vertices) { - ret.push(new Point(vec2.clone(v))); - prev = v; - } - } - } - - return ret; - } - - private static async loadMap(renderer: Renderer): Promise<[SpriteView, Collidable[]]> { - const map = new MapData(await getJSON('resources/map/test.json')); - return [await loadMap(renderer, map), this.mkCollision(map.collision)]; + private static async loadMap(renderer: Renderer, name: string): Promise<[SpriteView, Collidable[]]> { + const map = new MapData(await getJSON(`resources/map/${name}.json`)); + return [await loadMap(renderer, map), mkCollision(map.collision)]; } private time: number|null = null; @@ -65,28 +49,36 @@ export class GameContext { private readonly input: DirectionHandler; - private readonly entityPos: vec2 = vec2.clone([6, 6]); - private readonly entityMovement: vec2 = vec2.create(); + private readonly playerMovement: vec2 = vec2.create(); private readonly collisionRadius = 7 / 16; private constructor( private readonly renderer: Renderer, private readonly mapView: SpriteView, - private readonly entity: SpriteView, + private readonly player: EntityContext, + private readonly entities: EntityContext[], private readonly collision: Collidable[], ) { this.input = new DirectionHandler(); this.input.addListener((v) => { if (vec2.sqrLen(v) > 0) - vec2.normalize(this.entityMovement, v); + vec2.normalize(this.playerMovement, v); else - vec2.copy(this.entityMovement, [0, 0]); + vec2.copy(this.playerMovement, [0, 0]); }); window.requestAnimationFrame(this.render); } + public getTranslation(): null { + return null; + } + + public getCollidables(): Collidable[] { + return this.collision; + } + private updateTime(time: number): number { const diff = this.time !== null ? time - this.time : 0; this.time = time; @@ -95,41 +87,39 @@ export class GameContext { } private updateStepCollide(out: vec2, dest: vec2): boolean { - const move = new Movement(this.entityPos, dest); - - for (const c of this.collision) { - if (!c.collide(out, move, this.collisionRadius)) - continue; + const move = new Movement(this.player.pos, dest); - if (vec2.squaredDistance(this.entityPos, out) >= vec2.squaredDistance(this.entityPos, dest)) - continue; + for (const c of [this, ...this.entities]) { + if (collide(c, out, move, this.collisionRadius)) { + if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) + continue; - return true; + return true; + } } return false; } private updateStep(): void { - const dest = vec2.scaleAndAdd(vec2.create(), this.entityPos, this.entityMovement, this.speed); + const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerMovement, this.speed); const newDest = vec2.create(); while (this.updateStepCollide(newDest, dest)) { - if (vec2.equals(newDest, this.entityPos)) + if (vec2.equals(newDest, this.player.pos)) return; vec2.copy(dest, newDest); - } - vec2.copy(this.entityPos, dest); + vec2.copy(this.player.pos, dest); } private update(time: number): void { const diff = Math.min(this.maxSkip, this.updateTime(time)); - if (vec2.sqrLen(this.entityMovement) === 0) { - this.renderer.snapToGrid(this.entityPos, this.entityPos); + if (vec2.sqrLen(this.playerMovement) === 0) { + this.renderer.snapToGrid(this.player.pos, this.player.pos); return; } @@ -140,13 +130,13 @@ export class GameContext { private render = (time: number) => { this.update(Math.round(time / this.tick)); - this.renderer.setCenter(this.entityPos); + this.renderer.setCenter(this.player.pos); this.renderer.clear(); this.mapView.render(); - this.renderer.setTranslation(this.entityPos); - this.entity.render(); + for (const r of [...this.entities, this.player]) + r.render(); window.requestAnimationFrame(this.render); } diff --git a/src/model/data/collision.ts b/src/model/data/collision.ts new file mode 100644 index 0000000..5008fa3 --- /dev/null +++ b/src/model/data/collision.ts @@ -0,0 +1,6 @@ +export interface CollisionPolygon { + readonly type: 'polygon'; + readonly vertices: Array<[number, number]>; +} + +export type Collision = CollisionPolygon; diff --git a/src/model/data/entity.ts b/src/model/data/entity.ts new file mode 100644 index 0000000..3474a38 --- /dev/null +++ b/src/model/data/entity.ts @@ -0,0 +1,19 @@ +import { Collision } from './collision'; + +export interface EntityDataInput { + readonly sprite: string; + readonly anchor?: [number, number]; + readonly collision?: Collision[]; +} + +export class EntityData { + public readonly sprite: string; + public readonly anchor: [number, number]; + public readonly collision: Collision[]; + + constructor(input: EntityDataInput) { + this.sprite = input.sprite; + this.anchor = input.anchor || [0.5, 0.5]; + this.collision = input.collision || []; + } +} diff --git a/src/model/data/map.ts b/src/model/data/map.ts index 4d9ecda..b36f6b5 100644 --- a/src/model/data/map.ts +++ b/src/model/data/map.ts @@ -1,11 +1,6 @@ -export interface CollisionPolygon { - readonly type: 'polygon'; - readonly vertices: Array<[number, number]>; -} - -export type Collision = CollisionPolygon; +import { Collision } from './collision'; -export interface Input { +export interface MapDataInput { readonly tiles: string[]; readonly layers: number[][][]; readonly collision: Collision[]; @@ -19,7 +14,7 @@ export class MapData { public readonly width: number; public readonly height: number; - constructor(data: Input) { + constructor(data: MapDataInput) { this.tiles = data.tiles; this.layers = data.layers; this.collision = data.collision; diff --git a/src/view/entity.ts b/src/view/entity.ts index 6241387..421a238 100644 --- a/src/view/entity.ts +++ b/src/view/entity.ts @@ -1,16 +1,16 @@ +import { EntityData } from '../model/data/entity'; import { Renderer } from './renderer/renderer'; import { SpriteView, SpriteViewBuilder } from './sprite'; import { loadImage, mkTexture } from './util/image'; -export async function loadSimpleEntity( +export async function loadEntity( r: Renderer, - sprite: string, - anchor: [number, number] = [0.5, 0.5], + data: EntityData, ): Promise { - const tile = await loadImage(`resources/sprite/entity/${sprite}.png`); + const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`); const texture = mkTexture(r.getContext(), tile); - const [x, y] = anchor; + const [x, y] = data.anchor; const builder = new SpriteViewBuilder(r, texture); builder.addSprite([-x, -y, -x + 1, -y + 1], [0, 0, 1, 1]); -- cgit v1.2.3