[game] Collision detection

This commit is contained in:
John Doty 2023-08-23 19:55:34 -07:00
parent 2388acaa94
commit 38f5f95827
6 changed files with 511 additions and 242 deletions

View file

@ -2,20 +2,42 @@ import { load_texture } from "./assets";
import { btn, Button } from "./input"; import { btn, Button } from "./input";
import { Vec2, new_v2, vadd, vsub, vnorm, vmul } from "./vector"; import { Vec2, new_v2, vadd, vsub, vnorm, vmul } from "./vector";
import { spr, use_texture, Texture } from "./graphics"; import { spr, use_texture, Texture } from "./graphics";
import { has_collision, Level } from "./level";
export interface ActorProps { export interface ActorProps {
id: string;
cx: number;
cy: number;
xr: number;
yr: number;
position: Vec2; position: Vec2;
velocity: Vec2; velocity: Vec2;
friction: number; friction: number;
id: string; collide_radius: number;
} }
export function new_actor_props(id: string, position: Vec2): ActorProps { export function new_actor_props(
id: string,
position: Vec2,
collide_radius: number
): ActorProps {
const cx = Math.trunc(position.x / 16);
const cy = Math.trunc(position.y / 16);
const xr = (position.x - cx * 16) / 16;
const yr = (position.y - cy * 16) / 16;
return { return {
cx,
cy,
xr,
yr,
velocity: new_v2(0), velocity: new_v2(0),
friction: 0.6, friction: 0.7,
id, id,
position, position,
collide_radius,
}; };
} }
@ -30,9 +52,15 @@ export class Actor {
update() {} update() {}
update_physics() { // IDEAS: Make gameplay logic at 30fps and render updates at 60fps? Does
const velocity = vmul(this.props.velocity, this.props.friction); // this mean we do some "update" in render?
// Zero if we're smaller than some epsilon. //
// TODO: Update physics in a better kind of way rather than an object call.
update_physics(level: Level | undefined) {
// This is very nice: https://deepnight.net/tutorial/a-simple-platformer-engine-part-1-basics/
// Apply friction to velocity and zero if we're close enough to zero.
const props = this.props;
const velocity = vmul(props.velocity, props.friction);
if (Math.abs(velocity.x) < 0.01) { if (Math.abs(velocity.x) < 0.01) {
velocity.x = 0; velocity.x = 0;
} }
@ -40,13 +68,62 @@ export class Actor {
velocity.y = 0; velocity.y = 0;
} }
const new_position = vadd(this.props.position, velocity); let cx = props.cx;
let cy = props.cy;
// Adjust xr with velocity, check for collisions, etc.
let xr = props.xr + velocity.x;
do {
// TODO: Cap velocity to 1 tile/frame? Then we wouldn't need this loop...
if (xr >= 0.7 && has_collision(level, cx + 1, cy)) {
xr = 0.7;
velocity.x = 0;
}
if (xr <= 0.3 && has_collision(level, cx - 1, cy)) {
xr = 0.3;
velocity.x = 0;
}
if (xr > 1) {
cx += 1;
xr -= 1;
}
if (xr < 0) {
cx -= 1;
xr += 1;
}
} while (xr > 1 || xr < 0);
let yr = props.yr + velocity.y;
do {
if (yr >= 0.4 && has_collision(level, cx, cy + 1)) {
yr = 0.4;
velocity.y = 0;
}
if (yr <= 0.1 && has_collision(level, cx, cy - 1)) {
yr = 0.1;
velocity.y = 0;
}
if (yr > 1) {
cy += 1;
yr -= 1;
}
if (yr < 0) {
cy -= 1;
yr += 1;
}
} while (yr > 1 || yr < 0);
// TODO: Collision detection // TODO: Collision detection
// const { w, h } = this.bounds; // const { w, h } = this.bounds;
this.props.velocity = velocity; const new_position = new_v2((cx + xr) * 16, (cy + yr) * 16);
this.props.position = new_position;
props.cx = cx;
props.cy = cy;
props.xr = xr;
props.yr = yr;
props.velocity = velocity;
props.position = new_position;
} }
draw(_clock: number) {} draw(_clock: number) {}
@ -55,7 +132,7 @@ export class Actor {
} }
const robo_info = { const robo_info = {
anchor: new_v2(16, 24), // Distance from upper-left of sprite. anchor: new_v2(16, 16), // Distance from upper-left of sprite.
bounds: new_v2(32), // Width/height of sprite. bounds: new_v2(32), // Width/height of sprite.
sprite: "./bot.png", sprite: "./bot.png",
animations: [ animations: [
@ -90,7 +167,7 @@ export class Robo extends Actor {
a.x += 1; a.x += 1;
} }
vnorm(a); vnorm(a);
this.props.velocity = vadd(this.props.velocity, vmul(a, 1.5)); this.props.velocity = vadd(this.props.velocity, vmul(a, 0.06));
} }
draw(clock: number) { draw(clock: number) {

View file

@ -7,52 +7,65 @@ import { load_string } from "./io";
// TODO: Use io-ts? YIKES. // TODO: Use io-ts? YIKES.
export type Tile = { export interface Tile {
px: [number, number]; px: [number, number];
src: [number, number]; src: [number, number];
f: number; f: number;
t: number; t: number;
a: number; a: number;
}; }
export type TileLayer = { export interface TileSet {
type: "tile"; id: number;
texture: Texture; texture: Texture;
tags: Map<string, number[]>;
}
export interface TileLayer {
type: "tile";
tileset: TileSet;
grid_size: number; grid_size: number;
offset: [number, number]; offset: [number, number];
tiles: Tile[]; tiles: Tile[];
}; }
export type Entity = { export interface Entity {
type: string; type: string;
id: string; id: string;
px: [number, number]; px: [number, number];
bounds: [number, number]; bounds: [number, number];
// TODO: More props here. // TODO: More props here.
}; }
export type EntityLayer = { export interface EntityLayer {
type: "entity"; type: "entity";
entities: Entity[]; entities: Entity[];
}; }
export type Level = { export interface Level {
world_x: number; world_x: number;
world_y: number; world_y: number;
width: number; width: number;
height: number; height: number;
cw: number;
ch: number;
tile_layers: TileLayer[]; tile_layers: TileLayer[];
entity_layers: EntityLayer[]; entities: Entity[];
}; values: number[];
}
export type TileSet = { id: number; texture: Texture }; export interface World {
levels: Level[];
tilesets: Map<number, TileSet>;
}
export type World = { levels: Level[]; tilesets: Map<number, TileSet> }; interface LDTKTilesetDef {
async function load_tileset(def: {
uid: number; uid: number;
relPath: string; relPath: string;
}): Promise<TileSet> { enumTags: { enumValueId: string; tileIds: number[] }[];
}
async function load_tileset(def: LDTKTilesetDef): Promise<TileSet> {
let relPath = def.relPath as string; let relPath = def.relPath as string;
if (relPath.endsWith(".aseprite")) { if (relPath.endsWith(".aseprite")) {
// Whoops let's load the export instead? // Whoops let's load the export instead?
@ -60,11 +73,16 @@ async function load_tileset(def: {
} }
let texture = await load_texture(relPath); let texture = await load_texture(relPath);
let tags = new Map();
for (const et of def.enumTags) {
tags.set(et.enumValueId, et.tileIds);
}
print("Loaded tileset", def.uid, "from", relPath, "as ID", texture.id()); print("Loaded tileset", def.uid, "from", relPath, "as ID", texture.id());
return { id: def.uid, texture }; return { id: def.uid, texture, tags };
} }
type TileLayerInstance = { interface LDTKTileLayerInstance {
__type: "Tiles"; __type: "Tiles";
__gridSize: number; __gridSize: number;
__pxTotalOffsetX: number; __pxTotalOffsetX: number;
@ -77,11 +95,11 @@ type TileLayerInstance = {
t: number; t: number;
a: number; a: number;
}[]; }[];
}; }
function load_tile_layer_instance( function load_tile_layer_instance(
tile_sets: Map<number, TileSet>, tile_sets: Map<number, TileSet>,
li: TileLayerInstance li: LDTKTileLayerInstance
): TileLayer { ): TileLayer {
const tileset = tile_sets.get(li.__tilesetDefUid); const tileset = tile_sets.get(li.__tilesetDefUid);
if (!tileset) { if (!tileset) {
@ -90,14 +108,14 @@ function load_tile_layer_instance(
return { return {
type: "tile", type: "tile",
texture: tileset.texture, tileset,
grid_size: li.__gridSize, grid_size: li.__gridSize,
offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY], offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
tiles: li.gridTiles, tiles: li.gridTiles,
}; };
} }
type EntityLayerInstance = { interface LDTKEntityLayerInstance {
__type: "Entities"; __type: "Entities";
entityInstances: { entityInstances: {
__identifier: string; __identifier: string;
@ -106,72 +124,135 @@ type EntityLayerInstance = {
height: number; height: number;
px: [number, number]; px: [number, number];
}[]; }[];
};
function load_entity_layer_instance(li: EntityLayerInstance): EntityLayer {
return {
type: "entity",
entities: li.entityInstances.map((ei) => {
return {
type: ei.__identifier,
id: ei.iid,
px: ei.px,
bounds: [ei.width, ei.height],
};
}),
};
} }
type LayerInstance = TileLayerInstance | EntityLayerInstance; function load_entity_layer_instance(li: LDTKEntityLayerInstance): Entity[] {
return li.entityInstances.map((ei) => {
return {
type: ei.__identifier,
id: ei.iid,
px: ei.px,
bounds: [ei.width, ei.height],
};
});
}
function is_tile_layer_instance(x: LayerInstance): x is TileLayerInstance { interface LDTKIntGridLayerInstance {
__type: "IntGrid";
__cWid: number;
__cHei: number;
intGridCsv: number[];
}
type LDTKLayerInstance =
| LDTKTileLayerInstance
| LDTKEntityLayerInstance
| LDTKIntGridLayerInstance;
function is_tile_layer_instance(
x: LDTKLayerInstance
): x is LDTKTileLayerInstance {
return x.__type == "Tiles"; return x.__type == "Tiles";
} }
function is_entity_layer_instance(x: LayerInstance): x is EntityLayerInstance { function is_entity_layer_instance(
x: LDTKLayerInstance
): x is LDTKEntityLayerInstance {
return x.__type == "Entities"; return x.__type == "Entities";
} }
function load_level( function is_intgrid_layer_instance(
tile_sets: Map<number, TileSet>, x: LDTKLayerInstance
def: { ): x is LDTKIntGridLayerInstance {
worldX: number; return x.__type == "IntGrid";
worldY: number; }
pxWid: number;
pxHei: number; type LDTKLevel = {
layerInstances: LayerInstance[]; worldX: number;
} worldY: number;
): Level { pxWid: number;
pxHei: number;
layerInstances: LDTKLayerInstance[];
};
function load_level(tile_sets: Map<number, TileSet>, def: LDTKLevel): Level {
const result: Level = { const result: Level = {
world_x: def.worldX, world_x: def.worldX,
world_y: def.worldY, world_y: def.worldY,
width: def.pxWid, width: def.pxWid,
height: def.pxHei, height: def.pxHei,
cw: 0,
ch: 0,
tile_layers: [], tile_layers: [],
entity_layers: [], entities: [],
values: [],
}; };
for (const li of def.layerInstances) { for (const li of def.layerInstances) {
if (is_tile_layer_instance(li)) { if (is_tile_layer_instance(li)) {
result.tile_layers.push(load_tile_layer_instance(tile_sets, li)); const tli = load_tile_layer_instance(tile_sets, li);
result.tile_layers.push(tli);
} else if (is_entity_layer_instance(li)) { } else if (is_entity_layer_instance(li)) {
result.entity_layers.push(load_entity_layer_instance(li)); // TODO: Why would I support multiple entity layers?
result.entities = load_entity_layer_instance(li);
} else if (is_intgrid_layer_instance(li)) {
result.cw = li.__cWid;
result.ch = li.__cHei;
result.values = li.intGridCsv; // ?
} else {
print("WARNING: Unknown layer type");
} }
} }
return result; return result;
} }
interface LDTKMap {
__header__: {
fileType: "LDtk Project JSON";
app: "LDtk";
doc: "https://ldtk.io/json";
schema: "https://ldtk.io/files/JSON_SCHEMA.json";
appAuthor: "Sebastien 'deepnight' Benard";
appVersion: "1.3.3";
url: "https://ldtk.io";
};
defs: {
tilesets: LDTKTilesetDef[];
};
levels: LDTKLevel[];
}
function is_ldtk_map(map: unknown): map is LDTKMap {
if (
map instanceof Object &&
"__header__" in map &&
map.__header__ instanceof Object
) {
const header = map.__header__;
if ("fileType" in header && header.fileType == "LDtk Project JSON") {
return true;
}
}
return false;
}
export async function load_world(path: string): Promise<World> { export async function load_world(path: string): Promise<World> {
print("Loading map:", path); print("Loading map:", path);
const blob = await load_string(path); const blob = await load_string(path);
const map = JSON.parse(blob); const map = JSON.parse(blob);
if (!is_ldtk_map(map)) {
throw new Error("Map does not appear to be an LDTK level");
}
const tilesets = new Map<number, TileSet>(); const tilesets = new Map<number, TileSet>();
let loaded_tilesets = await Promise.all( let loaded_tilesets = await Promise.all(
map.defs.tilesets map.defs.tilesets
.filter((def: any) => def.relPath != null) .filter((def) => def.relPath != null)
.map((def: any) => load_tileset(def)) .map((def) => load_tileset(def))
); );
for (const ts of loaded_tilesets) { for (const ts of loaded_tilesets) {
tilesets.set(ts.id, ts); tilesets.set(ts.id, ts);
@ -181,13 +262,24 @@ export async function load_world(path: string): Promise<World> {
return { levels, tilesets }; return { levels, tilesets };
} }
export function has_collision(
level: Level | undefined,
cx: number,
cy: number
): boolean {
if (!level) return true;
if (cx < 0 || cx >= level.cw) return false;
if (cy < 0 || cy >= level.ch) return false;
return level.values[cy * level.cw + cx] == 1; // TODO: MAGIC NUMBER?
}
export function draw_level( export function draw_level(
level: Level, level: Level,
offset_x: number = 0, offset_x: number = 0,
offset_y: number = 0 offset_y: number = 0
) { ) {
for (const layer of level.tile_layers) { for (const layer of level.tile_layers) {
use_texture(layer.texture); use_texture(layer.tileset.texture);
let [ofx, ofy] = layer.offset; let [ofx, ofy] = layer.offset;
ofx += offset_x; ofx += offset_x;

View file

@ -36,16 +36,14 @@ function load_assets() {
// TODO: SPAWN ACTORS BASED ON LEVEL. // TODO: SPAWN ACTORS BASED ON LEVEL.
actors.length = 0; actors.length = 0;
for (const entity_layer of level.entity_layers) { for (const entity of level.entities) {
for (const entity of entity_layer.entities) { if (is_actor_type(entity.type)) {
if (is_actor_type(entity.type)) { const [x, y] = entity.px;
const [x, y] = entity.px; const [w, _] = entity.bounds;
const [w, h] = entity.bounds; const props = new_actor_props(entity.id, new_v2(x, y), w);
const props = new_actor_props(entity.id, new_v2(x, y), new_v2(w, h)); actors.push(spawn_actor(entity.type, props));
actors.push(spawn_actor(entity.type, props)); } else {
} else { print("WARNING: Ignoring entity of type", entity.type);
print("WARNING: Ignoring entity of type", entity.type);
}
} }
} }
}); });
@ -91,7 +89,7 @@ export function update() {
} }
for (const actor of actors) { for (const actor of actors) {
actor.update_physics(); actor.update_physics(level);
} }
// TODO: Bonks // TODO: Bonks

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 703 B

Before After
Before After