From d08bcc10c089e4d7e92100bd64793ab2dcf489db Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sat, 28 Sep 2024 09:37:08 +0100 Subject: [PATCH] Perfected movement controller. --- spec/unit/movementContoller.spec.ts | 28 +++++++++--- src/movementController.ts | 67 +++++++++++++---------------- src/world.ts | 25 +++-------- 3 files changed, 57 insertions(+), 63 deletions(-) diff --git a/spec/unit/movementContoller.spec.ts b/spec/unit/movementContoller.spec.ts index a55577e..4fa9576 100644 --- a/spec/unit/movementContoller.spec.ts +++ b/spec/unit/movementContoller.spec.ts @@ -25,6 +25,7 @@ async function visualisePhysics(world: GameWorld, fileOut: string) { const cls = buffers.colors; for (let i = 0; i < vtx.length / 4; i += 1) { + // Pad the canvas so we can see it. const vtxA = 250 + Math.round(vtx[i * 4] * 50); const vtxB = 250 + Math.round(vtx[i * 4 + 1] * 50); const vtxC = 250 + Math.round(vtx[i * 4 + 2] * 50); @@ -63,6 +64,15 @@ function constructTestEnv(): { world: GameWorld, player: RapierPhysicsObject, ti ColliderDesc.cuboid(3, 0.25), RigidBodyDesc.fixed().setTranslation(1, 2).lockRotations() ); + const playerEnt = { + priority: 0, + body: player.body, + destroyed: false, + destroy() { + + }, + } satisfies IPhysicalEntity; + world.addBody(playerEnt, player.collider); return { world, player, @@ -72,7 +82,7 @@ function constructTestEnv(): { world: GameWorld, player: RapierPhysicsObject, ti do { world.step(); step++; - } while(player.body.isMoving() && step <= MaxSteps); + } while(world.areEntitiesMoving() && step <= MaxSteps); expect(step).toBeLessThan(MaxSteps); return player.body.translation(); } @@ -80,7 +90,7 @@ function constructTestEnv(): { world: GameWorld, player: RapierPhysicsObject, ti } function createBlock(world: GameWorld, x: number,y: number, width = 1, height = 1) { - const body = world.createRigidBodyCollider(ColliderDesc.cuboid(width, height), RigidBodyDesc.fixed().setUserData({'test':'block'}).setTranslation( + const body = world.createRigidBodyCollider(ColliderDesc.cuboid(width, height), RigidBodyDesc.fixed().setTranslation( x, y )); const ent = { @@ -93,6 +103,8 @@ function createBlock(world: GameWorld, x: number,y: number, width = 1, height = }, } satisfies IPhysicalEntity&{collider: Collider}; world.addBody(ent, ent.collider); + // REQUIRED: So the collider has time to be registered. + world.step(); return ent; } @@ -116,7 +128,9 @@ describe('calculateMovement', () => { const testName = currentTestName?.replaceAll(/\s/g, '_') ?? "unknown"; const filename = `./test-out/${testName}.webp`; // Show debug shape. - env.world.createRigidBodyCollider(new ColliderDesc(debugData.shape), RigidBodyDesc.fixed().setTranslation(debugData.rayCoodinate.worldX, debugData.rayCoodinate.worldY)); + if (debugData) { + env.world.createRigidBodyCollider(new ColliderDesc(debugData.shape), RigidBodyDesc.fixed().setTranslation(debugData.rayCoodinate.worldX, debugData.rayCoodinate.worldY)); + } await visualisePhysics(env.world, filename); const fullPath = resolve(filename); if (assertionCalls !== numPassingAsserts) { @@ -134,7 +148,7 @@ describe('calculateMovement', () => { test('should be able to move left when there are no obstacles', () => { const move = calculateMovement(env.player, new Vector2(-1, 0), maxStep, env.world); env.player.body.setTranslation(move, false); - const {x, y} =env.waitUntilStopped(); + const {x, y} = env.waitUntilStopped(); expect(x).toBeCloseTo(0); expect(y).toBeCloseTo(1, 0); }); @@ -147,7 +161,7 @@ describe('calculateMovement', () => { expect(y).toBeCloseTo(1, 0); }); - test.only('should not be able to move if an obstacle is in the way', () => { + test('should not be able to move if an obstacle is in the way', () => { createBlock(env.world, 0, 1.25, 0.5, 0.5); const originalTranslation = env.player.body.translation(); const move = calculateMovement(env.player, new Vector2(-0.5, 0), maxStep, env.world); @@ -157,7 +171,7 @@ describe('calculateMovement', () => { expect(originalTranslation.y-y).toBeCloseTo(0, 1); }); - test('should be able to step over obstacles', () => { + test('should be able to step on top of obstacles', () => { createBlock(env.world, 0.5, 1.5, 0.25, 0.25); env.waitUntilStopped(); const move = calculateMovement(env.player, new Vector2(-0.5, 0), maxStep, env.world); @@ -183,7 +197,7 @@ describe('calculateMovement', () => { expect(y).toBeCloseTo(0, 0.5); }); - test('should not be able to enter cave-like entrances', () => { + test.only('should not be able to enter cave-like entrances', () => { createBlock(env.world, 0.5, 1.5, 0.25, 0.25); createBlock(env.world, 0.5, 0.5, 0.25, 0.25); const { y: originalY } = env.waitUntilStopped(); diff --git a/src/movementController.ts b/src/movementController.ts index a74c0f7..822a21d 100644 --- a/src/movementController.ts +++ b/src/movementController.ts @@ -17,52 +17,47 @@ export function calculateMovement(physObject: RapierPhysicsObject, movement: Vec const {y: objHalfHeight, x: objHalfWidth } = (physObject.collider.shape as Cuboid).halfExtents; // Get the extremity. const rayCoodinate = new Coordinate( - move.x - 0.5, - move.y - 0.25, + move.x, + // Increase the bounds to the steppy position. + move.y - maxSteppy.value, ); - // Always move to whatever y delta is lower ( as in higher ) - let topYDelta = move.y; - - - const initialCollisionShape = new Cuboid(objHalfWidth, objHalfHeight); + console.log(rayCoodinate.worldX, rayCoodinate.worldY); + // Increase by steppy amount. + const initialCollisionShape = new Cuboid(objHalfWidth, objHalfHeight - maxSteppy.value); debugData = { rayCoodinate, shape: initialCollisionShape }; - // TODO: Render this shape! - const collides = world.checkCollisionShape(new Coordinate(currentTranslation.x, currentTranslation.y), rayCoodinate.toWorldVector(), initialCollisionShape, physObject.collider); + const collides = world.checkCollisionShape(rayCoodinate, initialCollisionShape, physObject.collider); let canTravel = collides.length === 0; - for (const c of collides) { - const shape = c.collider.shape; - const bodyT = c.collider.translation(); - if (currentTranslation.y - bodyT.y < maxSteppy.value) { - // TODO: Support more types. - const halfHeight = shape.type === ShapeType.Cuboid ? (shape as Cuboid).halfExtents.y : (shape as Ball).radius; + // Pop the highest collider + const highestCollider = collides.sort((a,b) => a.collider.translation().y-b.collider.translation().y)[0]; - // Step - const potentialX = bodyT.x; - const potentialY = bodyT.y - halfHeight - objHalfHeight; - const yDelta = move.y-bodyT.y; - if (yDelta < topYDelta) { - continue; - } - - // Check step is safe - // if (world.checkCollisionShape(new Coordinate(potentialX, potentialY), physObject.collider.shape, physObject.collider).length){ - // continue; - // } - move.y = potentialY; - move.x = potentialX; - topYDelta = yDelta; - canTravel = true; - console.log('canTravel'); - } + // No collisions, go go go! + if (!highestCollider) { + return move; } - if (!canTravel) { + const shape = highestCollider.collider.shape; + const bodyT = highestCollider.collider.translation(); + if (currentTranslation.y - bodyT.y > maxSteppy.value) { + console.log('too big to steppy'); return currentTranslation; } + // TODO: Support more types. + const halfHeight = shape.type === ShapeType.Cuboid ? (shape as Cuboid).halfExtents.y : (shape as Ball).radius; - console.log("numColliders", collides.length); - + // Step + const potentialX = bodyT.x; + // Crop a bit off the top to avoid colliding with it. + const potentialY = bodyT.y - halfHeight - objHalfHeight - 0.01; + + // Check step is safe + debugData = { rayCoodinate: new Coordinate(potentialX, potentialY), shape: physObject.collider.shape } + if (world.checkCollisionShape(new Coordinate(potentialX, potentialY), physObject.collider.shape, physObject.collider).length){ + console.log('step is not safe'); + return currentTranslation; + } + move.y = potentialY; + move.x = potentialX; return move; } \ No newline at end of file diff --git a/src/world.ts b/src/world.ts index 2b67ae0..029f2a6 100644 --- a/src/world.ts +++ b/src/world.ts @@ -2,7 +2,6 @@ import { IGameEntity, IPhysicalEntity } from "./entities/entity"; import { Ticker, UPDATE_PRIORITY } from "pixi.js"; import { Ball, Collider, ColliderDesc, EventQueue, QueryFilterFlags, RigidBody, RigidBodyDesc, Shape, Vector2, World } from "@dimforge/rapier2d-compat"; import { Coordinate, MetersValue } from "./utils/coodinate"; -import { sub } from "./utils"; /** * Utility class holding the matterjs composite and entity map. @@ -152,36 +151,22 @@ export class GameWorld { return found; } - public checkCollisionShape(start: Coordinate, dir: Vector2, shape: Shape, ownCollier: Collider): {collider: Collider, entity: IPhysicalEntity}[] { + public checkCollisionShape(position: Coordinate, shape: Shape, ownCollier: Collider): {collider: Collider, entity: IPhysicalEntity}[] { // Ensure a unique set of results. const results = new Array<{collider: Collider, entity: IPhysicalEntity}>(); - // console.log(position.worldX, position.worldY, shape, results, this.rapierWorld.colliders.forEach(c => - // console.log("=>", c.translation(), c.shape) - // )); - this.rapierWorld.castShape( - start.toWorldVector(), + this.rapierWorld.intersectionsWithShape( + new Vector2(position.worldX, position.worldY), 0, - dir, shape, - 1, - 1, - false, - undefined, - undefined, - undefined, - undefined, (collider) => { + console.log("Got collier", collider.handle, ownCollier.handle); if (collider.handle !== ownCollier.handle) { - console.log('BARK2!'); const entity = this.bodyEntityMap.get(collider.handle); if (entity) { results.push({entity, collider}); } - console.log('nono', entity); - } else { - console.log('collided with self'); } - return false; + return true; }, ); return [...results];