[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 { Vec2, new_v2, vadd, vsub, vnorm, vmul } from "./vector";
import { spr, use_texture, Texture } from "./graphics";
import { has_collision, Level } from "./level";
export interface ActorProps {
id: string;
cx: number;
cy: number;
xr: number;
yr: number;
position: Vec2;
velocity: Vec2;
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 {
cx,
cy,
xr,
yr,
velocity: new_v2(0),
friction: 0.6,
friction: 0.7,
id,
position,
collide_radius,
};
}
@ -30,9 +52,15 @@ export class Actor {
update() {}
update_physics() {
const velocity = vmul(this.props.velocity, this.props.friction);
// Zero if we're smaller than some epsilon.
// IDEAS: Make gameplay logic at 30fps and render updates at 60fps? Does
// this mean we do some "update" in render?
//
// 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) {
velocity.x = 0;
}
@ -40,13 +68,62 @@ export class Actor {
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
// const { w, h } = this.bounds;
this.props.velocity = velocity;
this.props.position = new_position;
const new_position = new_v2((cx + xr) * 16, (cy + yr) * 16);
props.cx = cx;
props.cy = cy;
props.xr = xr;
props.yr = yr;
props.velocity = velocity;
props.position = new_position;
}
draw(_clock: number) {}
@ -55,7 +132,7 @@ export class Actor {
}
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.
sprite: "./bot.png",
animations: [
@ -90,7 +167,7 @@ export class Robo extends Actor {
a.x += 1;
}
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) {

View file

@ -7,52 +7,65 @@ import { load_string } from "./io";
// TODO: Use io-ts? YIKES.
export type Tile = {
export interface Tile {
px: [number, number];
src: [number, number];
f: number;
t: number;
a: number;
};
}
export type TileLayer = {
type: "tile";
export interface TileSet {
id: number;
texture: Texture;
tags: Map<string, number[]>;
}
export interface TileLayer {
type: "tile";
tileset: TileSet;
grid_size: number;
offset: [number, number];
tiles: Tile[];
};
}
export type Entity = {
export interface Entity {
type: string;
id: string;
px: [number, number];
bounds: [number, number];
// TODO: More props here.
};
}
export type EntityLayer = {
export interface EntityLayer {
type: "entity";
entities: Entity[];
};
}
export type Level = {
export interface Level {
world_x: number;
world_y: number;
width: number;
height: number;
cw: number;
ch: number;
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> };
async function load_tileset(def: {
interface LDTKTilesetDef {
uid: number;
relPath: string;
}): Promise<TileSet> {
enumTags: { enumValueId: string; tileIds: number[] }[];
}
async function load_tileset(def: LDTKTilesetDef): Promise<TileSet> {
let relPath = def.relPath as string;
if (relPath.endsWith(".aseprite")) {
// Whoops let's load the export instead?
@ -60,11 +73,16 @@ async function load_tileset(def: {
}
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());
return { id: def.uid, texture };
return { id: def.uid, texture, tags };
}
type TileLayerInstance = {
interface LDTKTileLayerInstance {
__type: "Tiles";
__gridSize: number;
__pxTotalOffsetX: number;
@ -77,11 +95,11 @@ type TileLayerInstance = {
t: number;
a: number;
}[];
};
}
function load_tile_layer_instance(
tile_sets: Map<number, TileSet>,
li: TileLayerInstance
li: LDTKTileLayerInstance
): TileLayer {
const tileset = tile_sets.get(li.__tilesetDefUid);
if (!tileset) {
@ -90,14 +108,14 @@ function load_tile_layer_instance(
return {
type: "tile",
texture: tileset.texture,
tileset,
grid_size: li.__gridSize,
offset: [li.__pxTotalOffsetX, li.__pxTotalOffsetY],
tiles: li.gridTiles,
};
}
type EntityLayerInstance = {
interface LDTKEntityLayerInstance {
__type: "Entities";
entityInstances: {
__identifier: string;
@ -106,72 +124,135 @@ type EntityLayerInstance = {
height: 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";
}
function is_entity_layer_instance(x: LayerInstance): x is EntityLayerInstance {
function is_entity_layer_instance(
x: LDTKLayerInstance
): x is LDTKEntityLayerInstance {
return x.__type == "Entities";
}
function load_level(
tile_sets: Map<number, TileSet>,
def: {
worldX: number;
worldY: number;
pxWid: number;
pxHei: number;
layerInstances: LayerInstance[];
}
): Level {
function is_intgrid_layer_instance(
x: LDTKLayerInstance
): x is LDTKIntGridLayerInstance {
return x.__type == "IntGrid";
}
type LDTKLevel = {
worldX: number;
worldY: number;
pxWid: number;
pxHei: number;
layerInstances: LDTKLayerInstance[];
};
function load_level(tile_sets: Map<number, TileSet>, def: LDTKLevel): Level {
const result: Level = {
world_x: def.worldX,
world_y: def.worldY,
width: def.pxWid,
height: def.pxHei,
cw: 0,
ch: 0,
tile_layers: [],
entity_layers: [],
entities: [],
values: [],
};
for (const li of def.layerInstances) {
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)) {
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;
}
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> {
print("Loading map:", path);
const blob = await load_string(path);
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>();
let loaded_tilesets = await Promise.all(
map.defs.tilesets
.filter((def: any) => def.relPath != null)
.map((def: any) => load_tileset(def))
.filter((def) => def.relPath != null)
.map((def) => load_tileset(def))
);
for (const ts of loaded_tilesets) {
tilesets.set(ts.id, ts);
@ -181,13 +262,24 @@ export async function load_world(path: string): Promise<World> {
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(
level: Level,
offset_x: number = 0,
offset_y: number = 0
) {
for (const layer of level.tile_layers) {
use_texture(layer.texture);
use_texture(layer.tileset.texture);
let [ofx, ofy] = layer.offset;
ofx += offset_x;

View file

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