Skip to content

Commit

Permalink
Basic rapier support with terrain and grenade
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Sep 22, 2024
1 parent 65d1a4b commit a237d97
Show file tree
Hide file tree
Showing 16 changed files with 418 additions and 379 deletions.
52 changes: 23 additions & 29 deletions src/entities/bitmapTerrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { UPDATE_PRIORITY, Container, Graphics, Rectangle, Texture, Sprite } from
import { IMatterEntity } from "./entity";
import { generateQuadTreeFromTerrain, imageDataToTerrainBoundaries } from "../terrain";
import Flags from "../flags";
import { GameWorld, RapierPhysicsObject } from "../world";
import { Collider, ColliderDesc, Cuboid, RigidBodyDesc, Vector2 } from "@dimforge/rapier2d";
import { GameWorld, PIXELS_PER_METER, RapierPhysicsObject } from "../world";
import { ActiveEvents, Collider, ColliderDesc, Cuboid, RigidBody, RigidBodyDesc, Vector2 } from "@dimforge/rapier2d";

Check warning on line 6 in src/entities/bitmapTerrain.ts

View workflow job for this annotation

GitHub Actions / lint

'ActiveEvents' is defined but never used

Check warning on line 6 in src/entities/bitmapTerrain.ts

View workflow job for this annotation

GitHub Actions / lint

'Cuboid' is defined but never used

export type OnDamage = () => void;
export class BitmapTerrain implements IMatterEntity {
Expand Down Expand Up @@ -76,6 +76,7 @@ export class BitmapTerrain implements IMatterEntity {

calculateBoundaryVectors(boundaryX = 0, boundaryY = 0, boundaryWidth = this.canvas.width, boundaryHeight = this.canvas.height) {
console.time('Generating terrain');
console.log({boundaryX, boundaryY, boundaryWidth, boundaryHeight});
const context = this.canvas.getContext('2d');
if (!context) {
throw Error('Failed to get render context of canvas');
Expand All @@ -84,9 +85,10 @@ export class BitmapTerrain implements IMatterEntity {
// Remove everything within the boundaries
const removableBodies = this.parts.filter(
(b) => {
const tr = b.body.translation();
let tr = b.body.translation();
tr = { x: tr.x * PIXELS_PER_METER, y: tr.y * PIXELS_PER_METER };
return (tr.x >= boundaryX && tr.x <= boundaryX + boundaryWidth) &&
(tr.y >= boundaryY && tr.y <= boundaryY + boundaryHeight)
(tr.y >= boundaryY && tr.y <= boundaryY + boundaryHeight)}
)

console.log("Removing", removableBodies.length, "bodies");
Expand All @@ -98,7 +100,8 @@ export class BitmapTerrain implements IMatterEntity {
damageFn?.();
}
}
this.parts = this.parts.filter(b => !removableBodies.some(rB => b.id === rB.id));
// TODO: Fix this.
this.parts = this.parts.filter(b => !removableBodies.some(rB => b.body.handle === rB.body.handle));
const imgData = context.getImageData(boundaryX, boundaryY, boundaryWidth, boundaryHeight);
const { boundaries, boundingBox } = imageDataToTerrainBoundaries(boundaryX, boundaryY, imgData);
this.bounds = boundingBox;
Expand All @@ -113,8 +116,9 @@ export class BitmapTerrain implements IMatterEntity {
const newParts: RapierPhysicsObject[] = [];
for (const quad of quadtreeRects) {
const body = this.gameWorld.createRigidBodyCollider(
ColliderDesc.cuboid(quad.width/2, quad.height/2),
RigidBodyDesc.fixed().setTranslation(quad.x + this.sprite.x, quad.y + this.sprite.y)
ColliderDesc.cuboid(quad.width/(PIXELS_PER_METER*2), quad.height/(PIXELS_PER_METER*2)),
RigidBodyDesc.fixed().setTranslation(
(quad.x + this.sprite.x)/PIXELS_PER_METER, (quad.y + this.sprite.y)/PIXELS_PER_METER)
)
newParts.push(body);
}
Expand All @@ -124,17 +128,18 @@ export class BitmapTerrain implements IMatterEntity {
console.timeEnd("Generating terrain");
}

onDamage(point: Vector, radius: number) {
onDamage(point: Vector2, radius: number) {
const context = this.canvas.getContext('2d');
if (!context) {
throw Error('Failed to get context');
}

radius = radius * PIXELS_PER_METER;
console.log('onDamage', point, radius);

// Optmise this check!
const imageX = point.x - this.sprite.x;
const imageY = point.y - this.sprite.y;
const imageX = (point.x*PIXELS_PER_METER) - this.sprite.x;
const imageY = (point.y*PIXELS_PER_METER) - this.sprite.y;
const snapshotX = (imageX-radius) - 30;
const snapshotY = (imageY-radius) - 30;
const snapshotWidth = (radius*3);
Expand Down Expand Up @@ -187,21 +192,10 @@ export class BitmapTerrain implements IMatterEntity {
return;
}
this.gfx.clear();
for (const rect of this.parts) {
let color = 0xFFBD01;
if (this.nearestTerrainPositionBodies.has(rect)) {
color = 0x00AA00;
} else if (!rect.collider.isEnabled()) {
color = 0x0000FF;
}
const position = rect.body.translation();
const shape = rect.collider.shape as Cuboid;
this.gfx.rect(position.x, position.y, shape.halfExtents.x * 2, shape.halfExtents.y * 2).stroke({ width: 1, color });
}
this.gfx.rect(this.nearestTerrainPositionPoint.x, this.nearestTerrainPositionPoint.y, 1, 1).stroke({width: 5, color: 0xFF0000});
}

public getNearestTerrainPosition(point: Vector2, width: number, maxHeightDiff: number, xDirection = 0): {point: Vector, fell: false}|{fell: true, point: null} {
public getNearestTerrainPosition(point: Vector2, width: number, maxHeightDiff: number, xDirection = 0): {point: Vector2, fell: false}|{fell: true, point: null} {
// This needs a rethink, we really want to have it so that the character's "platform" is visualised
// by this algorithm. We want to figure out if we can move left or right, and if not if we're going to fall.

Expand All @@ -210,20 +204,20 @@ export class BitmapTerrain implements IMatterEntity {

// First filter for all the points within the range of the point.
const filteredPoints = this.parts.filter((p) => {
return p.position.x < point.x + width + xDirection &&
p.position.x > point.x - width - xDirection &&
p.position.y > point.y - maxHeightDiff
return p.body.translation().x < point.x + width + xDirection &&
p.body.translation().x > point.x - width - xDirection &&
p.body.translation().y > point.y - maxHeightDiff
});

// This needs to answer the following as quickly as possible:

// Can we go to the next x point without falling?
let closestTerrainPoint: Vector|undefined;
let closestTerrainPoint: Vector2|undefined;

const rejectedPoints: Body[] = [];
const rejectedPoints: RigidBody[] = [];

for (const terrain of filteredPoints) {
const terrainPoint = terrain.position;
const terrainPoint = terrain.body.translation();
const distY = Math.abs(terrainPoint.y - point.y);
if (xDirection < 0 && terrainPoint.x - point.x > xDirection) {
// If moving left, -3
Expand All @@ -234,7 +228,7 @@ export class BitmapTerrain implements IMatterEntity {
continue;
}
if (distY > maxHeightDiff) {
rejectedPoints.push(terrain);
rejectedPoints.push(terrain.body);
continue;
}
const distX = Math.abs(terrainPoint.x - (point.x + xDirection));
Expand Down
6 changes: 3 additions & 3 deletions src/entities/entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Vector2 } from "@dimforge/rapier2d";
import { UPDATE_PRIORITY } from "pixi.js";
import { ShapeContact, Vector2 } from "@dimforge/rapier2d";

Check warning on line 1 in src/entities/entity.ts

View workflow job for this annotation

GitHub Actions / lint

'ShapeContact' is defined but never used
import { Point, UPDATE_PRIORITY } from "pixi.js";

Check warning on line 2 in src/entities/entity.ts

View workflow job for this annotation

GitHub Actions / lint

'Point' is defined but never used

/**
* Base entity which all game objects implement
Expand All @@ -26,5 +26,5 @@ export interface IMatterEntity extends IGameEntity {
* @param contactPoint
* @returns True if the collision should stop being processed
*/
onCollision?(other: IMatterEntity, contactPoint: Vector2): boolean;
onCollision?(other: IMatterEntity, contactPoint: Vector2|null): boolean;
}
2 changes: 1 addition & 1 deletion src/entities/phys/bazookaShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class BazookaShell extends TimedExplosive {

static async create(parent: Container, gameWorld: GameWorld, position: {x: number, y: number}, initialAngle: number, initialForce: number, wind: number) {
const ent = new BazookaShell(position, initialAngle, gameWorld, initialForce, wind);
gameWorld.addBody(ent, ent.body);
gameWorld.addBody(ent, ent.body.collider);
parent.addChild(ent.sprite);
parent.addChild(ent.wireframe.renderable);
console.log("New zooka", ent.body);
Expand Down
57 changes: 27 additions & 30 deletions src/entities/phys/grenade.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import { Container, Sprite, Text, Texture, Ticker } from 'pixi.js';
import grenadePaths from "../../assets/grenade.svg";
import { Bodies, Vector } from "matter-js";
import { TimedExplosive } from "./timedExplosive";
import { IMatterEntity } from '../entity';
import { BitmapTerrain } from '../bitmapTerrain';
import { IMediaInstance, Sound } from '@pixi/sound';
import { loadSvg } from '../../loadSvg';
import { GameWorld } from '../../world';
import { GameWorld, PIXELS_PER_METER } from '../../world';
import { ActiveEvents, ColliderDesc, RigidBodyDesc, ShapeContact, Vector2 } from '@dimforge/rapier2d';

Check warning on line 7 in src/entities/phys/grenade.ts

View workflow job for this annotation

GitHub Actions / lint

'ShapeContact' is defined but never used
import { magnitude } from '../../utils';
/**
* Standard grenade projectile.
*/
export class Grenade extends TimedExplosive {
private static readonly FRICTION = 0.99;
private static readonly RESITITUTION = 0;
private static readonly DENSITY = 35;
private static bodyVertices = loadSvg(grenadePaths, 50, 1, 1, Vector.create(0.5, 0.5));
private static readonly FRICTION = 0.15;
public static texture: Texture;
public static bounceSoundsLight: Sound;
public static boundSoundHeavy: Sound;

static async create(parent: Container, world: GameWorld, position: {x: number, y: number}, initialForce: { x: number, y: number}) {
const ent = new Grenade(position, await Grenade.bodyVertices, initialForce, world);
const ent = new Grenade(position, initialForce, world);
parent.addChild(ent.sprite, ent.wireframe.renderable);
return ent;
}
Expand All @@ -33,25 +28,25 @@ export class Grenade extends TimedExplosive {
}
public bounceSoundPlayback?: IMediaInstance;

private constructor(position: { x: number, y: number }, bodyVerticies: Vector[][], initialForce: { x: number, y: number}, world: GameWorld) {
private constructor(position: { x: number, y: number }, initialForce: { x: number, y: number}, world: GameWorld) {
const sprite = new Sprite(Grenade.texture);
sprite.scale.set(0.5, 0.5);
sprite.anchor.set(0.5, 0.5);
const body = Bodies.rectangle(sprite.x, sprite.y, sprite.width, sprite.height, {
position,
sleepThreshold: 60*(5+2),
friction: Grenade.FRICTION,
restitution: Grenade.RESITITUTION,
density: Grenade.DENSITY,
isSleeping: false,
isStatic: false,
label: "Grenade",
});
console.log("Created grenade body", body.id);
sprite.scale.set(0.5);
sprite.anchor.set(0.5);
const body = world.createRigidBodyCollider(
ColliderDesc.roundCuboid(
0.05,
0.05, 0.50).setActiveEvents(ActiveEvents.COLLISION_EVENTS),
RigidBodyDesc
.dynamic()
.setTranslation(position.x/PIXELS_PER_METER, position.y/PIXELS_PER_METER)
// .setLinvel(initialForce.x, initialForce.y)
// .setLinearDamping(Grenade.FRICTION)
);
console.log("Created grenade body", body.collider.handle);
super(sprite, body, world, {
explosionRadius: 60,
explosionRadius: 3, // in meters
explodeOnContact: false,
timerSecs: 1,
timerSecs: 5,
});
//Body.applyForce(body, Vector.create(body.position.x - 20, body.position.y), initialForce);
this.timerText = new Text(this.timerTextValue, {
Expand All @@ -69,14 +64,16 @@ export class Grenade extends TimedExplosive {
return;
}

this.wireframe.setDebugText(`velocity: ${Math.round(magnitude(this.body.body.linvel())*1000)/1000}`)

// Body.applyForce(this.body, Vector.create(this.body.position.x - 5, this.body.position.y - 5), Vector.create(this.initialForce.x, this.initialForce.y));
if (!this.timerText.destroyed) {
this.timerText.rotation = -this.body.angle;
this.timerText.rotation = -this.body.body.rotation();
this.timerText.text = this.timerTextValue;
}
}

onCollision(otherEnt: IMatterEntity, contactPoint: Vector) {
onCollision(otherEnt: IMatterEntity, contactPoint: Vector2) {
if (super.onCollision(otherEnt, contactPoint)) {
this.timerText.destroy();
return true;
Expand All @@ -86,13 +83,13 @@ export class Grenade extends TimedExplosive {
return false;
}

const velocity = Vector.magnitude(this.body.velocity);
const velocity = magnitude(this.body.body.linvel());

// TODO: can these interrupt?
if (!this.bounceSoundPlayback?.progress || this.bounceSoundPlayback.progress === 1 && this.timer > 0) {
// TODO: Hacks
Promise.resolve(
(velocity >= 4 ? Grenade.boundSoundHeavy : Grenade.bounceSoundsLight).play()
(velocity >= 8 ? Grenade.boundSoundHeavy : Grenade.bounceSoundsLight).play()
).then((instance) =>{
this.bounceSoundPlayback = instance;
})
Expand Down
25 changes: 14 additions & 11 deletions src/entities/phys/physicsEntity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Body, Vector } from "matter-js";
import { UPDATE_PRIORITY, Sprite } from "pixi.js";
import { IMatterEntity } from "../entity";
import { Water } from "../water";
import { BodyWireframe } from "../../mixins/bodyWireframe.";
import globalFlags from "../../flags";
import { IMediaInstance, Sound } from "@pixi/sound";
import { GameWorld } from "../../world";
import { GameWorld, PIXELS_PER_METER, RapierPhysicsObject } from "../../world";
import { ShapeContact, Vector2 } from "@dimforge/rapier2d";

Check warning on line 8 in src/entities/phys/physicsEntity.ts

View workflow job for this annotation

GitHub Actions / lint

'ShapeContact' is defined but never used

/**
* Any object that is physically present in the world i.e. a worm.
Expand All @@ -24,7 +24,7 @@ export abstract class PhysicsEntity implements IMatterEntity {
return this.sprite.destroyed;
}

constructor(public readonly sprite: Sprite, protected body: Body, protected gameWorld: GameWorld) {
constructor(public readonly sprite: Sprite, protected body: RapierPhysicsObject, protected gameWorld: GameWorld) {
this.wireframe = new BodyWireframe(this.body, globalFlags.DebugView);
globalFlags.on('toggleDebugView', (on) => {
this.wireframe.enabled = on;
Expand All @@ -39,22 +39,23 @@ export abstract class PhysicsEntity implements IMatterEntity {
}

update(dt: number): void {
this.sprite.position = this.body.position;
this.sprite.rotation = this.body.angle;

const pos = this.body.body.translation();
const rotation = this.body.body.rotation();
this.sprite.updateTransform({x: pos.x * PIXELS_PER_METER, y: pos.y * PIXELS_PER_METER, rotation });

this.wireframe.update();

// Sinking.
if (this.isSinking) {
this.body.position.y += 1 * dt;
if (this.body.position.y > this.sinkingY) {
this.body.body.setTranslation({x: pos.x, y: pos.y + (0.05 * dt)}, false);
if (pos.y > this.sinkingY) {
this.destroy();
}
}

}

onCollision(otherEnt: IMatterEntity, contactPoint: Vector) {
onCollision(otherEnt: IMatterEntity, contactPoint: Vector2) {
if (otherEnt instanceof Water) {
console.log('hit water');

Expand All @@ -64,10 +65,12 @@ export abstract class PhysicsEntity implements IMatterEntity {
this.splashSoundPlayback = instance;
})
}
const contactY = contactPoint.y;
// Time to sink
this.isSinking = true;
this.sinkingY = contactPoint.y + 200;
Body.setStatic(this.body, true);
this.sinkingY = contactY + 200;
// Set static.
this.body.body.setEnabled(false);
return true;
}
return false;
Expand Down
20 changes: 10 additions & 10 deletions src/entities/phys/timedExplosive.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Body, Vector, Bodies } from "matter-js";
import { UPDATE_PRIORITY, Ticker, Sprite } from "pixi.js";
import { UPDATE_PRIORITY, Ticker, Sprite, Point } from "pixi.js";
import { BitmapTerrain } from "../bitmapTerrain";
import { IMatterEntity } from "../entity";
import { PhysicsEntity } from "./physicsEntity";
import { Explosion } from "../explosion";
import { GameWorld } from "../../world";
import { GameWorld, PIXELS_PER_METER, RapierPhysicsObject } from "../../world";
import { ShapeContact, Vector2 } from "@dimforge/rapier2d";

Check warning on line 7 in src/entities/phys/timedExplosive.ts

View workflow job for this annotation

GitHub Actions / lint

'ShapeContact' is defined but never used

interface Opts {
explosionRadius: number,
Expand All @@ -22,9 +22,9 @@ export abstract class TimedExplosive extends PhysicsEntity implements IMatterEnt

priority: UPDATE_PRIORITY = UPDATE_PRIORITY.NORMAL;

constructor(sprite: Sprite, body: Body, gameWorld: GameWorld, public readonly opts: Opts) {
constructor(sprite: Sprite, body: RapierPhysicsObject, gameWorld: GameWorld, public readonly opts: Opts) {
super(sprite, body, gameWorld);
this.gameWorld.addBody(this, body);
this.gameWorld.addBody(this, body.collider);
this.timer = Ticker.targetFPMS * opts.timerSecs * 1000;
}

Expand All @@ -36,12 +36,12 @@ export abstract class TimedExplosive extends PhysicsEntity implements IMatterEnt
}

onExplode() {
const point = this.body.position;
const point = this.body.body.translation();
const radius = this.opts.explosionRadius;
// Detect if anything is around us.
const hitOtherEntity = this.gameWorld.checkCollision(Bodies.circle(point.x, point.y, radius), this);
const hitOtherEntity = this.gameWorld.checkCollision(point, radius, this.body.collider);
console.log("onExplode", hitOtherEntity);
this.gameWorld.addEntity(Explosion.create(this.gameWorld.viewport, point, radius, 15, 35));
this.gameWorld.addEntity(Explosion.create(this.gameWorld.viewport, new Point(point.x*PIXELS_PER_METER, point.y*PIXELS_PER_METER), radius, 15, 35));
// Find contact point with any terrain
if (hitOtherEntity) {
this.onCollision(hitOtherEntity, point);
Expand All @@ -59,11 +59,11 @@ export abstract class TimedExplosive extends PhysicsEntity implements IMatterEnt
}
}

onCollision(otherEnt: IMatterEntity, contactPoint: Vector) {
onCollision(otherEnt: IMatterEntity, contactPoint: Vector2) {
if (super.onCollision(otherEnt, contactPoint)) {
if (this.isSinking) {
this.timer = 0;
this.body.angle = 0.15;
this.body.body.setRotation(0.15, false);
}
return true;
}
Expand Down
Loading

0 comments on commit a237d97

Please sign in to comment.