Implement more flexible handling of entities and collidables
This commit is contained in:
parent
40339947d1
commit
5eae6f29a8
11 changed files with 204 additions and 73 deletions
3
dist/resources/entity/green_circle.json
vendored
Normal file
3
dist/resources/entity/green_circle.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"sprite": "green_circle"
|
||||||
|
}
|
14
dist/resources/entity/red_circle.json
vendored
Normal file
14
dist/resources/entity/red_circle.json
vendored
Normal file
|
@ -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]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
dist/resources/sprite/entity/red_circle.png
vendored
Normal file
BIN
dist/resources/sprite/entity/red_circle.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
59
src/controller/collision.ts
Normal file
59
src/controller/collision.ts
Normal file
|
@ -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;
|
||||||
|
}
|
45
src/controller/entitycontext.ts
Normal file
45
src/controller/entitycontext.ts
Normal file
|
@ -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<EntityContext> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { DirectionHandler } from '../view/input/directionhandler';
|
||||||
import { loadMap } from '../view/map';
|
import { loadMap } from '../view/map';
|
||||||
import { Renderer } from '../view/renderer/renderer';
|
import { Renderer } from '../view/renderer/renderer';
|
||||||
import { SpriteView } from '../view/sprite';
|
import { SpriteView } from '../view/sprite';
|
||||||
|
|
||||||
import { Collidable } from '../math/collision';
|
import { Collidable } from '../math/collision';
|
||||||
import { LineSegment, Movement } from '../math/line';
|
import { Movement } from '../math/line';
|
||||||
import { Point } from '../math/point';
|
|
||||||
import { getJSON } from '../util';
|
import { getJSON } from '../util';
|
||||||
|
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
export class GameContext {
|
export class GameContext implements CollidableGroup {
|
||||||
public static async load(renderer: Renderer): Promise<GameContext> {
|
public static async load(renderer: Renderer): Promise<GameContext> {
|
||||||
const entity = loadSimpleEntity(renderer, 'simple_circle');
|
const map = this.loadMap(renderer, 'test');
|
||||||
const [mapView, collision] = await this.loadMap(renderer);
|
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(
|
return new GameContext(
|
||||||
renderer,
|
renderer,
|
||||||
mapView,
|
mapView,
|
||||||
await entity,
|
player,
|
||||||
collision,
|
[entity],
|
||||||
|
mapCollision,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static mkCollision(collision: Collision[]): Collidable[] {
|
private static async loadMap(renderer: Renderer, name: string): Promise<[SpriteView, Collidable[]]> {
|
||||||
const ret: Collidable[] = [];
|
const map = new MapData(await getJSON(`resources/map/${name}.json`));
|
||||||
|
return [await loadMap(renderer, map), mkCollision(map.collision)];
|
||||||
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 time: number|null = null;
|
private time: number|null = null;
|
||||||
|
@ -65,28 +49,36 @@ export class GameContext {
|
||||||
|
|
||||||
private readonly input: DirectionHandler;
|
private readonly input: DirectionHandler;
|
||||||
|
|
||||||
private readonly entityPos: vec2 = vec2.clone([6, 6]);
|
private readonly playerMovement: vec2 = vec2.create();
|
||||||
private readonly entityMovement: vec2 = vec2.create();
|
|
||||||
|
|
||||||
private readonly collisionRadius = 7 / 16;
|
private readonly collisionRadius = 7 / 16;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly renderer: Renderer,
|
private readonly renderer: Renderer,
|
||||||
private readonly mapView: SpriteView,
|
private readonly mapView: SpriteView,
|
||||||
private readonly entity: SpriteView,
|
private readonly player: EntityContext,
|
||||||
|
private readonly entities: EntityContext[],
|
||||||
private readonly collision: Collidable[],
|
private readonly collision: Collidable[],
|
||||||
) {
|
) {
|
||||||
this.input = new DirectionHandler();
|
this.input = new DirectionHandler();
|
||||||
this.input.addListener((v) => {
|
this.input.addListener((v) => {
|
||||||
if (vec2.sqrLen(v) > 0)
|
if (vec2.sqrLen(v) > 0)
|
||||||
vec2.normalize(this.entityMovement, v);
|
vec2.normalize(this.playerMovement, v);
|
||||||
else
|
else
|
||||||
vec2.copy(this.entityMovement, [0, 0]);
|
vec2.copy(this.playerMovement, [0, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.requestAnimationFrame(this.render);
|
window.requestAnimationFrame(this.render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTranslation(): null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCollidables(): Collidable[] {
|
||||||
|
return this.collision;
|
||||||
|
}
|
||||||
|
|
||||||
private updateTime(time: number): number {
|
private updateTime(time: number): number {
|
||||||
const diff = this.time !== null ? time - this.time : 0;
|
const diff = this.time !== null ? time - this.time : 0;
|
||||||
this.time = time;
|
this.time = time;
|
||||||
|
@ -95,41 +87,39 @@ export class GameContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStepCollide(out: vec2, dest: vec2): boolean {
|
private updateStepCollide(out: vec2, dest: vec2): boolean {
|
||||||
const move = new Movement(this.entityPos, dest);
|
const move = new Movement(this.player.pos, dest);
|
||||||
|
|
||||||
for (const c of this.collision) {
|
for (const c of [this, ...this.entities]) {
|
||||||
if (!c.collide(out, move, this.collisionRadius))
|
if (collide(c, out, move, this.collisionRadius)) {
|
||||||
continue;
|
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (vec2.squaredDistance(this.entityPos, out) >= vec2.squaredDistance(this.entityPos, dest))
|
return true;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStep(): void {
|
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();
|
const newDest = vec2.create();
|
||||||
|
|
||||||
while (this.updateStepCollide(newDest, dest)) {
|
while (this.updateStepCollide(newDest, dest)) {
|
||||||
if (vec2.equals(newDest, this.entityPos))
|
if (vec2.equals(newDest, this.player.pos))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
vec2.copy(dest, newDest);
|
vec2.copy(dest, newDest);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vec2.copy(this.entityPos, dest);
|
vec2.copy(this.player.pos, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(time: number): void {
|
private update(time: number): void {
|
||||||
const diff = Math.min(this.maxSkip, this.updateTime(time));
|
const diff = Math.min(this.maxSkip, this.updateTime(time));
|
||||||
|
|
||||||
if (vec2.sqrLen(this.entityMovement) === 0) {
|
if (vec2.sqrLen(this.playerMovement) === 0) {
|
||||||
this.renderer.snapToGrid(this.entityPos, this.entityPos);
|
this.renderer.snapToGrid(this.player.pos, this.player.pos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,13 +130,13 @@ export class GameContext {
|
||||||
private render = (time: number) => {
|
private render = (time: number) => {
|
||||||
this.update(Math.round(time / this.tick));
|
this.update(Math.round(time / this.tick));
|
||||||
|
|
||||||
this.renderer.setCenter(this.entityPos);
|
this.renderer.setCenter(this.player.pos);
|
||||||
this.renderer.clear();
|
this.renderer.clear();
|
||||||
|
|
||||||
this.mapView.render();
|
this.mapView.render();
|
||||||
|
|
||||||
this.renderer.setTranslation(this.entityPos);
|
for (const r of [...this.entities, this.player])
|
||||||
this.entity.render();
|
r.render();
|
||||||
|
|
||||||
window.requestAnimationFrame(this.render);
|
window.requestAnimationFrame(this.render);
|
||||||
}
|
}
|
||||||
|
|
6
src/model/data/collision.ts
Normal file
6
src/model/data/collision.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface CollisionPolygon {
|
||||||
|
readonly type: 'polygon';
|
||||||
|
readonly vertices: Array<[number, number]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Collision = CollisionPolygon;
|
19
src/model/data/entity.ts
Normal file
19
src/model/data/entity.ts
Normal file
|
@ -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 || [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,6 @@
|
||||||
export interface CollisionPolygon {
|
import { Collision } from './collision';
|
||||||
readonly type: 'polygon';
|
|
||||||
readonly vertices: Array<[number, number]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Collision = CollisionPolygon;
|
export interface MapDataInput {
|
||||||
|
|
||||||
export interface Input {
|
|
||||||
readonly tiles: string[];
|
readonly tiles: string[];
|
||||||
readonly layers: number[][][];
|
readonly layers: number[][][];
|
||||||
readonly collision: Collision[];
|
readonly collision: Collision[];
|
||||||
|
@ -19,7 +14,7 @@ export class MapData {
|
||||||
public readonly width: number;
|
public readonly width: number;
|
||||||
public readonly height: number;
|
public readonly height: number;
|
||||||
|
|
||||||
constructor(data: Input) {
|
constructor(data: MapDataInput) {
|
||||||
this.tiles = data.tiles;
|
this.tiles = data.tiles;
|
||||||
this.layers = data.layers;
|
this.layers = data.layers;
|
||||||
this.collision = data.collision;
|
this.collision = data.collision;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
import { EntityData } from '../model/data/entity';
|
||||||
import { Renderer } from './renderer/renderer';
|
import { Renderer } from './renderer/renderer';
|
||||||
import { SpriteView, SpriteViewBuilder } from './sprite';
|
import { SpriteView, SpriteViewBuilder } from './sprite';
|
||||||
import { loadImage, mkTexture } from './util/image';
|
import { loadImage, mkTexture } from './util/image';
|
||||||
|
|
||||||
export async function loadSimpleEntity(
|
export async function loadEntity(
|
||||||
r: Renderer,
|
r: Renderer,
|
||||||
sprite: string,
|
data: EntityData,
|
||||||
anchor: [number, number] = [0.5, 0.5],
|
|
||||||
): Promise<SpriteView> {
|
): Promise<SpriteView> {
|
||||||
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 texture = mkTexture(r.getContext(), tile);
|
||||||
|
|
||||||
const [x, y] = anchor;
|
const [x, y] = data.anchor;
|
||||||
|
|
||||||
const builder = new SpriteViewBuilder(r, texture);
|
const builder = new SpriteViewBuilder(r, texture);
|
||||||
builder.addSprite([-x, -y, -x + 1, -y + 1], [0, 0, 1, 1]);
|
builder.addSprite([-x, -y, -x + 1, -y + 1], [0, 0, 1, 1]);
|
||||||
|
|
Reference in a new issue