From 89fafeafd3331cd3275c328781b6106dcc153461 Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Fri, 16 Oct 2020 11:48:52 +0200 Subject: [PATCH] Make physics more fun --- .gitignore | 1 + backend/src/helper/interpolate-angles.ts | 9 + backend/src/main.ts | 6 +- backend/src/map/create-dungeon.ts | 82 ++---- backend/src/objects/character-physical.ts | 245 ++++++++++++------ backend/src/objects/circle-physical.ts | 68 ++--- backend/src/objects/lamp-physical.ts | 6 +- .../{stone-physical.ts => planet-physical.ts} | 61 ++++- backend/src/objects/projectile-physical.ts | 24 +- backend/src/objects/spring.ts | 37 --- .../physics/containers/dynamic-physical.ts | 8 - .../physics/containers/physical-container.ts | 10 +- .../src/physics/containers/static-physical.ts | 7 - .../physics/functions/apply-spring-force.ts | 24 ++ backend/src/physics/functions/evaluate-sdf.ts | 7 + .../physics/functions/force-at-position.ts | 9 + .../functions/get-bounding-box-of-circle.ts | 10 + .../functions/is-circle-intersecting.ts | 8 + .../physics/{ => functions}/move-circle.ts | 46 ++-- .../src/physics/physicals/dynamic-physical.ts | 6 + .../physical-base.ts} | 4 +- backend/src/physics/physicals/physical.ts | 4 + .../physics/physicals/reacts-to-collision.ts | 8 + .../src/physics/physicals/static-physical.ts | 10 + backend/tsconfig.json | 17 ++ frontend/src/index.ts | 6 +- .../commands/generators/keyboard-listener.ts | 2 +- .../commands/generators/touch-listener.ts | 4 +- frontend/src/scripts/game.ts | 10 +- frontend/src/scripts/objects/camera.ts | 2 +- .../objects/{stone-view.ts => planet-view.ts} | 9 +- .../src/scripts/objects/projectile-view.ts | 5 +- frontend/src/scripts/shapes/blob-shape.ts | 34 +-- frontend/src/scripts/shapes/circle.ts | 3 + package.json | 2 +- shared/src/commands/types/move-action.ts | 7 +- shared/src/helper/rotate-minus-90-deg.ts | 3 + shared/src/main.ts | 3 +- .../types/{stone-base.ts => planet-base.ts} | 2 +- shared/src/settings.ts | 33 +-- tsconfig.json | 3 +- 41 files changed, 511 insertions(+), 334 deletions(-) create mode 100644 backend/src/helper/interpolate-angles.ts rename backend/src/objects/{stone-physical.ts => planet-physical.ts} (52%) delete mode 100644 backend/src/objects/spring.ts delete mode 100644 backend/src/physics/containers/dynamic-physical.ts delete mode 100644 backend/src/physics/containers/static-physical.ts create mode 100644 backend/src/physics/functions/apply-spring-force.ts create mode 100644 backend/src/physics/functions/evaluate-sdf.ts create mode 100644 backend/src/physics/functions/force-at-position.ts create mode 100644 backend/src/physics/functions/get-bounding-box-of-circle.ts create mode 100644 backend/src/physics/functions/is-circle-intersecting.ts rename backend/src/physics/{ => functions}/move-circle.ts (50%) create mode 100644 backend/src/physics/physicals/dynamic-physical.ts rename backend/src/physics/{physical.ts => physicals/physical-base.ts} (70%) create mode 100644 backend/src/physics/physicals/physical.ts create mode 100644 backend/src/physics/physicals/reacts-to-collision.ts create mode 100644 backend/src/physics/physicals/static-physical.ts create mode 100644 backend/tsconfig.json rename frontend/src/scripts/objects/{stone-view.ts => planet-view.ts} (66%) create mode 100644 frontend/src/scripts/shapes/circle.ts create mode 100644 shared/src/helper/rotate-minus-90-deg.ts rename shared/src/objects/types/{stone-base.ts => planet-base.ts} (89%) diff --git a/.gitignore b/.gitignore index 713f044..bbc4e21 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ lib node_modules package-lock.json .firebase +lerna-debug.log diff --git a/backend/src/helper/interpolate-angles.ts b/backend/src/helper/interpolate-angles.ts new file mode 100644 index 0000000..13cf942 --- /dev/null +++ b/backend/src/helper/interpolate-angles.ts @@ -0,0 +1,9 @@ +const shortAngleDist = (a0: number, a1: number): number => { + const max = Math.PI * 2; + const da = (a1 - a0) % max; + return ((2 * da) % max) - da; +}; + +export const interpolateAngles = (a0: number, a1: number, t: number) => { + return a0 + shortAngleDist(a0, a1) * t; +}; diff --git a/backend/src/main.ts b/backend/src/main.ts index 9e45d9e..c2adba2 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -20,7 +20,7 @@ glMatrix.setMatrixArrayType(Array); applyArrayPlugins(); -Random.seed = 42; +Random.seed = 44; const objects = new PhysicalContainer(); createDungeon(objects); @@ -94,8 +94,8 @@ const handlePhysics = () => { console.log(players.map((p) => p.latency)); } - objects.stepObjects(delta); - players.forEach((p) => p.step(delta)); + objects.stepObjects(delta / 1000); + players.forEach((p) => p.step(delta / 1000)); const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds(); deltas.push(physicsDelta); diff --git a/backend/src/map/create-dungeon.ts b/backend/src/map/create-dungeon.ts index 989771f..f7475f8 100644 --- a/backend/src/map/create-dungeon.ts +++ b/backend/src/map/create-dungeon.ts @@ -1,36 +1,35 @@ import { vec2, vec3 } from 'gl-matrix'; -import { last, Random, settings } from 'shared'; +import { Random, settings } from 'shared'; import { LampPhysical } from '../objects/lamp-physical'; -import { StonePhysical } from '../objects/stone-physical'; +import { PlanetPhysical } from '../objects/planet-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { Physical } from '../physics/physical'; +import { evaluateSdf } from '../physics/functions/evaluate-sdf'; +import { Physical } from '../physics/physicals/physical'; export const createDungeon = (objectContainer: PhysicalContainer) => { - const width = 100000; - const height = 10000; const objects: Array = []; const lights: Array = []; - let previousEnd = vec2.fromValues(-width / 2, 0); - while (previousEnd.x < width / 2) { - const { stone, end } = createFloorElement(previousEnd); - objects.push(stone); - previousEnd = end; - } - const calculateDistanceField = (target: vec2): number => - objects.reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000); + const worldSize = vec2.length( + vec2.fromValues( + settings.worldTopEdge - settings.worldBottomEdge, + settings.worldRightEdge - settings.worldLeftEdge, + ), + ); + + for (let i = 0; i < worldSize / 400; i++) { + console.log('planet', i); - for (let i = 0; i < 400; i++) { let position: vec2; do { position = vec2.fromValues( - Random.getRandomInRange(-width / 2, width / 2), - Random.getRandomInRange(0, height), + Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge), + Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge), ); } while ( - calculateDistanceField(position) < 800 || - calculateDistanceField(position) > 2000 + evaluateSdf(position, objects) < 800 || + evaluateSdf(position, objects) > 2500 ); objects.push( @@ -38,27 +37,22 @@ export const createDungeon = (objectContainer: PhysicalContainer) => { position, Random.getRandomInRange(300, 800), Random.getRandomInRange(300, 800), - Random.getRandomInRange(10, 40), + Random.getRandomInRange(20, 40), ), ); } - let tryCount = 0; - lightGeneration: for (let i = 0; i < 300; i++) { - console.log(i); + for (let i = 0; i < worldSize / 350; i++) { + console.log('light', i); let position: vec2; do { position = vec2.fromValues( - Random.getRandomInRange(-width / 2, width / 2), - Random.getRandomInRange(-1000, height), + Random.getRandomInRange(settings.worldLeftEdge, settings.worldRightEdge), + Random.getRandomInRange(settings.worldBottomEdge, settings.worldTopEdge), ); - - if (tryCount++ > 1e4) { - break lightGeneration; - } } while ( - calculateDistanceField(position) < 200 || - lights.find((l) => l.distance(position) < 1200) + evaluateSdf(position, objects) < 200 || + lights.find((l) => l.distance(position) < 1800) ); lights.push( new LampPhysical( @@ -84,7 +78,7 @@ const createBlob = ( width: number, height: number, randomness: number, -): StonePhysical => { +): PlanetPhysical => { const vertices = []; for (let i = 0; i < settings.polygonEdgeCount; i++) { @@ -99,29 +93,5 @@ const createBlob = ( ), ); } - return new StonePhysical(vertices); -}; - -const createFloorElement = ( - start: vec2, -): { - stone: StonePhysical; - end: vec2; -} => { - const vertices: Array = [vec2.fromValues(start.x, -10000), start]; - - let previousX = start.x; - let previousY = start.y; - for (let i = 0; i < settings.polygonEdgeCount - 3; i++) { - previousX += Random.getRandomInRange(200, 800); - previousY += Random.getRandomInRange(-100, 100); - vertices.push(vec2.fromValues(previousX, previousY)); - } - - const end = last(vertices)!; - vertices.push(vec2.fromValues(end.x, -10000)); - return { - stone: new StonePhysical(vertices), - end, - }; + return new PlanetPhysical(vertices); }; diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index 484fafe..a7c8711 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -8,20 +8,70 @@ import { clamp, last, GameObject, + Circle, } from 'shared'; -import { DynamicPhysical } from '../physics/conatiners/dynamic-physical'; +import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; import { CirclePhysical } from './circle-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { Spring } from './spring'; import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; import { ProjectilePhysical } from './projectile-physical'; +import { interpolateAngles } from '../helper/interpolate-angles'; +import { forceAtPosition } from '../physics/functions/force-at-position'; +import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; +import { PlanetPhysical } from './planet-physical'; +import { ReactsToCollision } from '../physics/physicals/reacts-to-collision'; @serializesTo(CharacterBase) -export class CharacterPhysical extends CharacterBase implements DynamicPhysical { +export class CharacterPhysical + extends CharacterBase + implements DynamicPhysical, ReactsToCollision { public readonly canCollide = true; public readonly canMove = true; + + private static readonly headRadius = 50; + private static readonly feetRadius = 20; + // offsets are meassured from (0, 0) + private static readonly desiredHeadOffset = vec2.fromValues(0, 65); + private static readonly desiredLeftFootOffset = vec2.fromValues(-20, 0); + private static readonly desiredRightFootOffset = vec2.fromValues(20, 0); + private static readonly centerOfMass = vec2.scale( + vec2.create(), + vec2.add( + vec2.create(), + vec2.add( + vec2.create(), + CharacterPhysical.desiredHeadOffset, + CharacterPhysical.desiredLeftFootOffset, + ), + CharacterPhysical.desiredRightFootOffset, + ), + 1 / 3, + ); + + private static readonly headOffset = vec2.subtract( + vec2.create(), + CharacterPhysical.desiredHeadOffset, + CharacterPhysical.centerOfMass, + ); + private static readonly leftFootOffset = vec2.subtract( + vec2.create(), + CharacterPhysical.desiredLeftFootOffset, + CharacterPhysical.centerOfMass, + ); + private static readonly rightFootOffset = vec2.subtract( + vec2.create(), + CharacterPhysical.desiredRightFootOffset, + CharacterPhysical.centerOfMass, + ); + + public static readonly boundRadius = + (CharacterPhysical.headRadius + CharacterPhysical.feetRadius * 2) * 2; + private isDestroyed = false; - private jumpEnergyLeft = settings.defaultJumpEnergy; + private direction = 0; + private currentPlanet?: PlanetPhysical; + private lastMovementWasRelative = false; + private secondsSinceOnSurface = 1000; public head: CirclePhysical; public leftFoot: CirclePhysical; @@ -29,37 +79,31 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical public bound: CirclePhysical; private movementActions: Array = []; - private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create()); - - public handleMovementAction(c: MoveActionCommand) { - this.movementActions.push(c); - } - - private static readonly headOffset = vec2.fromValues(0, 40); - private static readonly headRadius = 50; - private static readonly feetRadius = 20; - private static readonly leftFootOffset = vec2.fromValues(-20, -35); - private static readonly rightFootOffset = vec2.fromValues(20, -35); + private lastMovementAction: MoveActionCommand = new MoveActionCommand( + vec2.create(), + false, + ); constructor( public readonly colorIndex: number, private readonly container: PhysicalContainer, + startPosition: vec2, ) { super(id(), colorIndex); this.head = new CirclePhysical( - vec2.clone(CharacterPhysical.headOffset), + vec2.add(vec2.create(), startPosition, CharacterPhysical.headOffset), CharacterPhysical.headRadius, this, container, ); this.leftFoot = new CirclePhysical( - vec2.clone(CharacterPhysical.leftFootOffset), + vec2.add(vec2.create(), startPosition, CharacterPhysical.leftFootOffset), CharacterPhysical.feetRadius, this, container, ); this.rightFoot = new CirclePhysical( - vec2.clone(CharacterPhysical.rightFootOffset), + vec2.add(vec2.create(), startPosition, CharacterPhysical.rightFootOffset), CharacterPhysical.feetRadius, this, container, @@ -70,12 +114,16 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical this.bound = new CirclePhysical( vec2.create(), - (CharacterPhysical.headRadius + CharacterPhysical.feetRadius * 2) * 2, + CharacterPhysical.boundRadius, this, container, ); } + public handleMovementAction(c: MoveActionCommand) { + this.movementActions.push(c); + } + public onCollision(other: GameObject) { if (other instanceof ProjectilePhysical) { other.destroy(); @@ -110,7 +158,7 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical ); } - private sumAndResetMovementActions(): vec2 { + private averageAndResetMovementActions(): vec2 { let direction: vec2; if (this.movementActions.length === 0) { direction = vec2.clone(this.lastMovementAction.direction); @@ -122,6 +170,9 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical vec2.scale(direction, direction, 1 / this.movementActions.length); + this.lastMovementWasRelative = + this.movementActions.find((a) => a.isCharacterRelative) !== undefined; + this.lastMovementAction = last(this.movementActions)!; this.movementActions = []; } @@ -129,82 +180,104 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical return direction; } - public step(deltaTimeInMiliseconds: number) { - const deltaTime = deltaTimeInMiliseconds / 1000; - const direction = this.sumAndResetMovementActions(); - const feetAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne; - const isAirborne = feetAirborne && this.head.isAirborne; - this.jumpEnergyLeft += isAirborne ? -deltaTime : deltaTime; - this.jumpEnergyLeft = clamp(this.jumpEnergyLeft, 0, settings.defaultJumpEnergy); + public step(deltaTime: number) { + if ((this.secondsSinceOnSurface += deltaTime) > 1) { + this.currentPlanet = undefined; + } - const xMax = deltaTime * settings.maxAccelerationX; - const yMax = this.jumpEnergyLeft > 0 ? settings.maxAccelerationY : 0; - const movementForce = vec2.multiply( - direction, - direction, - vec2.fromValues(xMax, yMax), + const intersectingWithForcefield = this.container.findIntersecting( + getBoundingBoxOfCircle( + new Circle( + this.center, + CharacterPhysical.boundRadius + settings.maxGravityDistance, + ), + ), ); + const actualGravity = forceAtPosition(this.center, intersectingWithForcefield); - Spring.step( - this.leftFoot, - this.rightFoot, - vec2.distance(CharacterPhysical.leftFootOffset, CharacterPhysical.rightFootOffset), - 300, - deltaTime, - ); + const direction = this.averageAndResetMovementActions(); + const movementForce = vec2.scale(direction, direction, settings.maxAcceleration); + + if (!this.currentPlanet) { + this.applyForce(this.leftFoot, actualGravity, deltaTime); + this.applyForce(this.rightFoot, actualGravity, deltaTime); + + const sumForce = vec2.subtract(vec2.create(), actualGravity, movementForce); + + this.setDirection( + vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce, + deltaTime, + ); + } else { + const leftFootGravity = this.currentPlanet!.getForce(this.leftFoot.center); + const rightFootGravity = this.currentPlanet!.getForce(this.rightFoot.center); + if (movementForce.y > settings.maxAcceleration / 4) { + vec2.scale(leftFootGravity, leftFootGravity, 0.35); + vec2.scale(rightFootGravity, rightFootGravity, 0.35); + } + this.applyForce(this.leftFoot, leftFootGravity, deltaTime); + this.applyForce(this.rightFoot, rightFootGravity, deltaTime); + + if (this.lastMovementWasRelative) { + vec2.rotate(movementForce, movementForce, vec2.create(), this.direction); + } + + const headGravity = this.currentPlanet!.getForce(this.head.center); + + if (vec2.length(headGravity) < vec2.length(actualGravity) / 2) { + this.currentPlanet = undefined; + } + this.setDirection(headGravity, deltaTime); + } - this.applyForce(this.head, movementForce, deltaTime); this.applyForce(this.leftFoot, movementForce, deltaTime); this.applyForce(this.rightFoot, movementForce, deltaTime); - if (feetAirborne) { - this.applyForce(this.head, settings.gravitationalForce, deltaTime); + this.stepBodyPart(this.leftFoot, deltaTime); + this.stepBodyPart(this.rightFoot, deltaTime); + this.keepPosture(); + } + + private setDirection(direction: vec2, deltaTime: number) { + this.direction = interpolateAngles( + this.direction, + Math.atan2(direction.y, direction.x) + Math.PI / 2, + Math.pow(4, deltaTime), + ); + } + + private keepPosture() { + const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); + vec2.add(bodyCenter, bodyCenter, this.rightFoot.center); + vec2.scale(bodyCenter, bodyCenter, 1 / 3); + this.springMove(this.leftFoot, bodyCenter, CharacterPhysical.leftFootOffset); + this.springMove(this.rightFoot, bodyCenter, CharacterPhysical.rightFootOffset); + this.springMove(this.head, bodyCenter, CharacterPhysical.headOffset); + } + + private springMove(object: CirclePhysical, center: vec2, offset: vec2) { + // todo: make time-independent + const springConstant = 0.35; + + const desiredPosition = vec2.add(vec2.create(), center, offset); + vec2.rotate(desiredPosition, desiredPosition, center, this.direction); + const positionDelta = vec2.subtract(desiredPosition, desiredPosition, object.center); + vec2.scale(positionDelta, positionDelta, springConstant); + const hitObject = object.tryMove(positionDelta); + + if (hitObject instanceof PlanetPhysical) { + this.secondsSinceOnSurface = 0; + this.currentPlanet = hitObject; } - this.applyForce(this.leftFoot, settings.gravitationalForce, deltaTime); - this.applyForce(this.rightFoot, settings.gravitationalForce, deltaTime); + } - this.head.step2(deltaTime); - this.leftFoot.step2(deltaTime); - this.rightFoot.step2(deltaTime); + private stepBodyPart(part: CirclePhysical, deltaTime: number) { + const hitObject = part.step2(deltaTime); - let sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); - vec2.add(sumBody, sumBody, this.rightFoot.center); - vec2.scale(sumBody, sumBody, 1 / 3); - - const headPosition = vec2.add(vec2.create(), sumBody, CharacterPhysical.headOffset); - const headDelta = vec2.subtract(headPosition, headPosition, this.head.center); - vec2.scale(headDelta, headDelta, 0.5); - this.head.tryMove(headDelta); - - sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); - vec2.add(sumBody, sumBody, this.rightFoot.center); - vec2.scale(sumBody, sumBody, 1 / 3); - - const leftFootPosition = vec2.add( - vec2.create(), - sumBody, - CharacterPhysical.leftFootOffset, - ); - const leftFootDelta = vec2.subtract( - leftFootPosition, - leftFootPosition, - this.leftFoot.center, - ); - vec2.scale(leftFootDelta, leftFootDelta, 1); - this.leftFoot.tryMove(leftFootDelta); - - const rightFootPosition = vec2.add( - vec2.create(), - sumBody, - CharacterPhysical.rightFootOffset, - ); - const rightFootDelta = vec2.subtract( - rightFootPosition, - rightFootPosition, - this.rightFoot.center, - ); - vec2.scale(rightFootDelta, rightFootDelta, 1); - this.rightFoot.tryMove(rightFootDelta); + if (hitObject instanceof PlanetPhysical) { + this.secondsSinceOnSurface = 0; + this.currentPlanet = hitObject; + } } public applyForce(circle: CirclePhysical, force: vec2, timeInSeconds: number) { diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index 48a019e..daf6191 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,31 +1,28 @@ import { vec2 } from 'gl-matrix'; import { Circle, GameObject, serializesTo, settings } from 'shared'; -import { Physical } from '../physics/physical'; - +import { PhysicalBase } from '../physics/physicals/physical-base'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; -import { moveCircle } from '../physics/move-circle'; +import { moveCircle } from '../physics/functions/move-circle'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { DynamicPhysical } from '../physics/containers/dynamic-physical'; +import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; +import { + ReactsToCollision, + reactsToCollision, +} from '../physics/physicals/reacts-to-collision'; @serializesTo(Circle) -export class CirclePhysical implements Circle, DynamicPhysical { +export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollision { readonly canCollide = true; readonly canMove = true; - private _isAirborne = true; public velocity = vec2.create(); - - public get isAirborne(): boolean { - return this._isAirborne; - } - private _boundingBox: BoundingBox; constructor( private _center: vec2, private _radius: number, - public owner: DynamicPhysical, + public owner: GameObject, private readonly container: PhysicalContainer, private restitution = 0, ) { @@ -47,7 +44,9 @@ export class CirclePhysical implements Circle, DynamicPhysical { } public onCollision(other: GameObject) { - this.owner.onCollision(other); + if (reactsToCollision(this.owner)) { + this.owner.onCollision(other); + } } public get gameObject(): GameObject { @@ -71,11 +70,11 @@ export class CirclePhysical implements Circle, DynamicPhysical { return vec2.distance(target.center, this.center) - this.radius - target.radius; } - public areIntersecting(other: Physical): boolean { + public areIntersecting(other: PhysicalBase): boolean { return other.distance(this.center) < this.radius; } - public isInside(other: Physical): boolean { + public isInside(other: PhysicalBase): boolean { return other.distance(this.center) < -this.radius; } @@ -94,9 +93,9 @@ export class CirclePhysical implements Circle, DynamicPhysical { ); } - public step(deltaTime: number) {} + public step(_: number) {} - public step2(deltaTimeInSeconds: number): boolean { + public step2(deltaTimeInSeconds: number): GameObject | undefined { vec2.scale( this.velocity, this.velocity, @@ -108,7 +107,7 @@ export class CirclePhysical implements Circle, DynamicPhysical { const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); vec2.scale(delta, delta, 1 / stepCount); - let wasHit = false; + let lastHit: GameObject | undefined; for (let i = 0; i < stepCount; i++) { const distance = vec2.scale( @@ -120,7 +119,11 @@ export class CirclePhysical implements Circle, DynamicPhysical { const intersecting = this.container.findIntersecting(this.boundingBox); this.radius -= vec2.length(distance); - const { normal, hitSurface } = moveCircle(this, vec2.clone(distance), intersecting); + const { normal, hitSurface, hitObject } = moveCircle( + this, + vec2.clone(distance), + intersecting, + ); if (hitSurface) { vec2.subtract( @@ -133,40 +136,37 @@ export class CirclePhysical implements Circle, DynamicPhysical { ), ); - wasHit = true; + lastHit = hitObject; } } - this._isAirborne = !wasHit; - return wasHit; + return lastHit; } - public tryMove(delta: vec2) { + public tryMove(delta: vec2): GameObject | undefined { const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); vec2.scale(delta, delta, 1 / stepCount); - let wasHit = false; + let lastHit: GameObject | undefined; for (let i = 0; i < stepCount; i++) { this.radius += vec2.length(delta); const intersecting = this.container.findIntersecting(this.boundingBox); this.radius -= vec2.length(delta); - const { normal, hitSurface } = moveCircle(this, vec2.clone(delta), intersecting); + const { tangent, hitSurface, hitObject } = moveCircle( + this, + vec2.clone(delta), + intersecting, + ); if (hitSurface) { - vec2.subtract( - delta, - delta, - vec2.scale(normal!, normal!, vec2.dot(normal!, delta)), - ); - - wasHit = true; + delta = vec2.scale(delta, tangent!, vec2.length(delta)); + lastHit = hitObject; } } - this._isAirborne = !wasHit; - return wasHit; + return lastHit; } public toArray(): Array { diff --git a/backend/src/objects/lamp-physical.ts b/backend/src/objects/lamp-physical.ts index 2ca2dca..b10a1cb 100644 --- a/backend/src/objects/lamp-physical.ts +++ b/backend/src/objects/lamp-physical.ts @@ -2,7 +2,7 @@ import { vec2, vec3 } from 'gl-matrix'; import { LampBase, settings, id, serializesTo } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; -import { StaticPhysical } from '../physics/containers/static-physical'; +import { StaticPhysical } from '../physics/physicals/static-physical'; @serializesTo(LampBase) export class LampPhysical extends LampBase implements StaticPhysical { @@ -32,6 +32,10 @@ export class LampPhysical extends LampBase implements StaticPhysical { return this; } + public getForce(_: vec2): vec2 { + return vec2.create(); + } + public distance(target: vec2): number { return vec2.distance(this.center, target); } diff --git a/backend/src/objects/stone-physical.ts b/backend/src/objects/planet-physical.ts similarity index 52% rename from backend/src/objects/stone-physical.ts rename to backend/src/objects/planet-physical.ts index 743c3ac..a53b053 100644 --- a/backend/src/objects/stone-physical.ts +++ b/backend/src/objects/planet-physical.ts @@ -1,18 +1,31 @@ import { vec2 } from 'gl-matrix'; -import { clamp01, id, serializesTo, StoneBase } from 'shared'; + +import { + clamp, + clamp01, + id, + rotate90Deg, + rotateMinus90Deg, + serializesTo, + settings, + PlanetBase, +} from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; -import { StaticPhysical } from '../physics/containers/static-physical'; +import { StaticPhysical } from '../physics/physicals/static-physical'; -@serializesTo(StoneBase) -export class StonePhysical extends StoneBase implements StaticPhysical { +@serializesTo(PlanetBase) +export class PlanetPhysical extends PlanetBase implements StaticPhysical { public readonly canCollide = true; public readonly canMove = false; + private center: vec2; private _boundingBox?: ImmutableBoundingBox; constructor(vertices: Array) { super(id(), vertices); + this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); + vec2.scale(this.center, this.center, 1 / vertices.length); } public distance(target: vec2): number { @@ -72,6 +85,46 @@ export class StonePhysical extends StoneBase implements StaticPhysical { return this._boundingBox; } + public getForce(position: vec2): vec2 { + const height = this.distance(position); + + let closestIndex = 0; + this.vertices.forEach((v, i) => { + if ( + vec2.distance(position, v) < vec2.distance(position, this.vertices[closestIndex]) + ) { + closestIndex = i; + } + }); + + const afterClosest = this.vertices[(closestIndex + 1) % this.vertices.length]; + const closest = this.vertices[closestIndex]; + const beforeClosest = this.vertices[ + (closestIndex - 1 + this.vertices.length) % this.vertices.length + ]; + + const diff = vec2.subtract(vec2.create(), position, closest); + const edge1 = vec2.subtract(vec2.create(), afterClosest, closest); + const edge2 = vec2.subtract(vec2.create(), closest, beforeClosest); + const normalizedDiff = vec2.normalize(vec2.create(), diff); + + const currentEdge = + vec2.dot(vec2.normalize(vec2.create(), rotate90Deg(edge1)), normalizedDiff) > + vec2.dot(vec2.normalize(vec2.create(), rotate90Deg(edge2)), normalizedDiff) + ? edge1 + : edge2; + + vec2.normalize(currentEdge, currentEdge); + const normal = rotateMinus90Deg(currentEdge); + + const scale = clamp( + settings.maxGravityQ * ((settings.maxGravityDistance / height) ** 3 - 1), + 0, + settings.maxGravityStrength, + ); + return vec2.scale(normal, normal, scale); + } + public get gameObject(): this { return this; } diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 7ff2824..fb64343 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -2,11 +2,15 @@ import { vec2 } from 'gl-matrix'; import { id, settings, serializesTo, ProjectileBase, GameObject } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; -import { DynamicPhysical } from '../physics/conatiners/dynamic-physical'; +import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; +import { PlanetPhysical } from './planet-physical'; +import { ReactsToCollision } from '../physics/physicals/reacts-to-collision'; @serializesTo(ProjectileBase) -export class ProjectilePhysical extends ProjectileBase implements DynamicPhysical { +export class ProjectilePhysical + extends ProjectileBase + implements DynamicPhysical, ReactsToCollision { public readonly canCollide = true; public readonly canMove = true; private isDestroyed = false; @@ -55,14 +59,16 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica } } - public step(deltaTimeInMiliseconds: number) { - const deltaTime = deltaTimeInMiliseconds / 1000; + public step(deltaTime: number) { + const gravity = vec2.create(); + const intersecting = this.container.findIntersecting(this.boundingBox); + intersecting.forEach((i) => { + if (i instanceof PlanetPhysical) { + vec2.add(gravity, gravity, i.getForce(this.center)); + } + }); - vec2.add( - this.velocity, - this.velocity, - vec2.scale(vec2.create(), settings.gravitationalForce, deltaTime), - ); + vec2.add(this.velocity, this.velocity, vec2.scale(vec2.create(), gravity, deltaTime)); this.object.velocity = this.velocity; this.object.step2(deltaTime); diff --git a/backend/src/objects/spring.ts b/backend/src/objects/spring.ts deleted file mode 100644 index 91f121b..0000000 --- a/backend/src/objects/spring.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Circle } from 'shared'; -import { CirclePhysical } from './circle-physical'; - -export class Spring { - constructor( - private a: CirclePhysical | Circle, - private b: CirclePhysical | Circle, - private distance: number, - private strength: number, - ) {} - - public step(deltaTimeInSeconds: number) { - Spring.step(this.a, this.b, this.distance, this.strength, deltaTimeInSeconds); - } - - public static step( - a: CirclePhysical | Circle, - b: CirclePhysical | Circle, - distance: number, - strength: number, - deltaTimeInSeconds: number, - ) { - const length = vec2.dist(a.center, b.center) - distance; - - const abDirection = vec2.subtract(vec2.create(), b.center, a.center); - vec2.normalize(abDirection, abDirection); - const force = vec2.scale(abDirection, abDirection, strength * length); - if (a instanceof CirclePhysical) { - a.applyForce(force, deltaTimeInSeconds); - } - if (b instanceof CirclePhysical) { - vec2.scale(force, force, -1); - b.applyForce(force, deltaTimeInSeconds); - } - } -} diff --git a/backend/src/physics/containers/dynamic-physical.ts b/backend/src/physics/containers/dynamic-physical.ts deleted file mode 100644 index fd0e066..0000000 --- a/backend/src/physics/containers/dynamic-physical.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { GameObject } from 'shared'; -import { Physical } from '../physical'; - -export interface DynamicPhysical extends Physical { - readonly canMove: true; - step(deltaTimeInMilliseconds: number): void; - onCollision(other: GameObject): void; -} diff --git a/backend/src/physics/containers/physical-container.ts b/backend/src/physics/containers/physical-container.ts index 4d1e6f4..80ad925 100644 --- a/backend/src/physics/containers/physical-container.ts +++ b/backend/src/physics/containers/physical-container.ts @@ -3,9 +3,9 @@ import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; import { BoundingBoxList } from './bounding-box-list'; import { BoundingBoxTree } from './bounding-box-tree'; -import { Physical } from '../physical'; -import { StaticPhysical } from './static-physical'; -import { DynamicPhysical } from './dynamic-physical'; +import { Physical } from '../physicals/physical'; +import { StaticPhysical } from '../physicals/static-physical'; +import { DynamicPhysical } from '../physicals/dynamic-physical'; export class PhysicalContainer { private isTreeInitialized = false; @@ -34,8 +34,8 @@ export class PhysicalContainer { this.dynamicBoundingBoxes.remove(object); } - public stepObjects(deltaTimeInMilliseconds: number) { - this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTimeInMilliseconds)); + public stepObjects(deltaTime: number) { + this.dynamicBoundingBoxes.forEach((o) => o.step(deltaTime)); } public findIntersecting(box: BoundingBoxBase): Array { diff --git a/backend/src/physics/containers/static-physical.ts b/backend/src/physics/containers/static-physical.ts deleted file mode 100644 index 47e8de8..0000000 --- a/backend/src/physics/containers/static-physical.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Physical } from '../physical'; -import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box'; - -export interface StaticPhysical extends Physical { - readonly canMove: false; - readonly boundingBox: ImmutableBoundingBox; -} diff --git a/backend/src/physics/functions/apply-spring-force.ts b/backend/src/physics/functions/apply-spring-force.ts new file mode 100644 index 0000000..e2d3db3 --- /dev/null +++ b/backend/src/physics/functions/apply-spring-force.ts @@ -0,0 +1,24 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from 'shared'; +import { CirclePhysical } from '../../objects/circle-physical'; + +export const applySpringForce = ( + a: CirclePhysical | Circle, + b: CirclePhysical | Circle, + distance: number, + strength: number, + deltaTimeInSeconds: number, +) => { + const length = vec2.dist(a.center, b.center) - distance; + + const abDirection = vec2.subtract(vec2.create(), b.center, a.center); + vec2.normalize(abDirection, abDirection); + const force = vec2.scale(abDirection, abDirection, strength * length); + if (a instanceof CirclePhysical) { + a.applyForce(force, deltaTimeInSeconds); + } + if (b instanceof CirclePhysical) { + vec2.scale(force, force, -1); + b.applyForce(force, deltaTimeInSeconds); + } +}; diff --git a/backend/src/physics/functions/evaluate-sdf.ts b/backend/src/physics/functions/evaluate-sdf.ts new file mode 100644 index 0000000..69d0d6d --- /dev/null +++ b/backend/src/physics/functions/evaluate-sdf.ts @@ -0,0 +1,7 @@ +import { vec2 } from 'gl-matrix'; +import { PhysicalBase } from '../physicals/physical-base'; + +export const evaluateSdf = (target: vec2, objects: Array) => + objects + .filter((i) => i.canCollide) + .reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000); diff --git a/backend/src/physics/functions/force-at-position.ts b/backend/src/physics/functions/force-at-position.ts new file mode 100644 index 0000000..5eef3ab --- /dev/null +++ b/backend/src/physics/functions/force-at-position.ts @@ -0,0 +1,9 @@ +import { vec2 } from 'gl-matrix'; +import { Physical } from '../physicals/physical'; + +export const forceAtPosition = (position: vec2, objects: Array) => + objects.reduce( + (sum: vec2, o: Physical) => + !o.canMove ? vec2.add(sum, sum, o.getForce(position)) : sum, + vec2.create(), + ); diff --git a/backend/src/physics/functions/get-bounding-box-of-circle.ts b/backend/src/physics/functions/get-bounding-box-of-circle.ts new file mode 100644 index 0000000..4677bd2 --- /dev/null +++ b/backend/src/physics/functions/get-bounding-box-of-circle.ts @@ -0,0 +1,10 @@ +import { Circle } from 'shared'; +import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; + +export const getBoundingBoxOfCircle = (circle: Circle): BoundingBoxBase => + new BoundingBoxBase( + circle.center.x - circle.radius, + circle.center.x + circle.radius, + circle.center.y - circle.radius, + circle.center.y + circle.radius, + ); diff --git a/backend/src/physics/functions/is-circle-intersecting.ts b/backend/src/physics/functions/is-circle-intersecting.ts new file mode 100644 index 0000000..3f497fa --- /dev/null +++ b/backend/src/physics/functions/is-circle-intersecting.ts @@ -0,0 +1,8 @@ +import { Circle } from 'shared'; +import { evaluateSdf } from './evaluate-sdf'; +import { PhysicalBase } from '../physicals/physical-base'; + +export const isCircleIntersecting = ( + circle: Circle, + intersectors: Array, +): boolean => evaluateSdf(circle.center, intersectors) < circle.radius; diff --git a/backend/src/physics/move-circle.ts b/backend/src/physics/functions/move-circle.ts similarity index 50% rename from backend/src/physics/move-circle.ts rename to backend/src/physics/functions/move-circle.ts index d7ef36a..9f80bd5 100644 --- a/backend/src/physics/move-circle.ts +++ b/backend/src/physics/functions/move-circle.ts @@ -1,8 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { Circle, rotate90Deg } from 'shared'; -import { CirclePhysical } from '../objects/circle-physical'; -import { DynamicPhysical } from './containers/dynamic-physical'; -import { Physical } from './physical'; +import { Circle, GameObject, rotate90Deg } from 'shared'; +import { CirclePhysical } from '../../objects/circle-physical'; +import { reactsToCollision } from '../physicals/reacts-to-collision'; +import { evaluateSdf } from './evaluate-sdf'; +import { Physical } from '../physicals/physical'; export const moveCircle = ( circle: CirclePhysical, @@ -13,6 +14,7 @@ export const moveCircle = ( hitSurface: boolean; normal?: vec2; tangent?: vec2; + hitObject?: GameObject; } => { const nextCircle = new Circle(vec2.clone(circle.center), circle.radius); vec2.add(nextCircle.center, nextCircle.center, delta); @@ -21,13 +23,7 @@ export const moveCircle = ( (b) => b.gameObject !== circle.gameObject && b.canCollide, ); - const getSdfAtPoint = (point: vec2): number => { - return possibleIntersectors - .filter((i) => i.canCollide) - .reduce((min, i) => (min = Math.min(min, i.distance(point))), 1000); - }; - - const sdfAtCenter = getSdfAtPoint(nextCircle.center); + const sdfAtCenter = evaluateSdf(nextCircle.center, possibleIntersectors); if (sdfAtCenter > nextCircle.radius) { circle.center = vec2.add(circle.center, circle.center, delta); @@ -38,21 +34,26 @@ export const moveCircle = ( }; } - const intersecting = possibleIntersectors - .filter((i) => i.canCollide && i.canMove) - .find((i) => i.distance(nextCircle.center) < circle.radius); + const intersecting = possibleIntersectors.find( + (i) => i.distance(nextCircle.center) <= circle.radius, + )!; - (intersecting?.gameObject as DynamicPhysical)?.onCollision(circle.gameObject); - ((circle.gameObject as unknown) as DynamicPhysical).onCollision( - intersecting?.gameObject, - ); + if (reactsToCollision(intersecting)) { + intersecting.onCollision(circle.gameObject); + } + + if (reactsToCollision(circle)) { + circle.onCollision(intersecting.gameObject); + } const dx = - getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(1, 0))) - - sdfAtCenter; + evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0.01, 0)), [ + intersecting, + ]) - sdfAtCenter; const dy = - getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 1))) - - sdfAtCenter; + evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 0.01)), [ + intersecting, + ]) - sdfAtCenter; const normal = vec2.fromValues(dx, dy); vec2.normalize(normal, normal); const rotatedNormal = rotate90Deg(normal); @@ -60,6 +61,7 @@ export const moveCircle = ( realDelta: delta, hitSurface: true, normal, + hitObject: intersecting?.gameObject, tangent: vec2.dot(rotatedNormal, delta) < 0 ? vec2.scale(rotatedNormal, rotatedNormal, -1) diff --git a/backend/src/physics/physicals/dynamic-physical.ts b/backend/src/physics/physicals/dynamic-physical.ts new file mode 100644 index 0000000..a5e03c1 --- /dev/null +++ b/backend/src/physics/physicals/dynamic-physical.ts @@ -0,0 +1,6 @@ +import { PhysicalBase } from './physical-base'; + +export interface DynamicPhysical extends PhysicalBase { + readonly canMove: true; + step(deltaTimeInMilliseconds: number): void; +} diff --git a/backend/src/physics/physical.ts b/backend/src/physics/physicals/physical-base.ts similarity index 70% rename from backend/src/physics/physical.ts rename to backend/src/physics/physicals/physical-base.ts index 962d699..3f923b6 100644 --- a/backend/src/physics/physical.ts +++ b/backend/src/physics/physicals/physical-base.ts @@ -1,8 +1,8 @@ import { vec2 } from 'gl-matrix'; import { GameObject } from 'shared'; -import { BoundingBoxBase } from './bounding-boxes/bounding-box-base'; +import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; -export interface Physical { +export interface PhysicalBase { readonly canCollide: boolean; readonly canMove: boolean; readonly boundingBox: BoundingBoxBase; diff --git a/backend/src/physics/physicals/physical.ts b/backend/src/physics/physicals/physical.ts new file mode 100644 index 0000000..2e3189b --- /dev/null +++ b/backend/src/physics/physicals/physical.ts @@ -0,0 +1,4 @@ +import { DynamicPhysical } from './dynamic-physical'; +import { StaticPhysical } from './static-physical'; + +export type Physical = StaticPhysical | DynamicPhysical; diff --git a/backend/src/physics/physicals/reacts-to-collision.ts b/backend/src/physics/physicals/reacts-to-collision.ts new file mode 100644 index 0000000..90e880a --- /dev/null +++ b/backend/src/physics/physicals/reacts-to-collision.ts @@ -0,0 +1,8 @@ +import { GameObject } from 'shared'; + +export interface ReactsToCollision { + onCollision(other: GameObject): void; +} + +export const reactsToCollision = (a: any): a is ReactsToCollision => + a && 'onCollision' in a; diff --git a/backend/src/physics/physicals/static-physical.ts b/backend/src/physics/physicals/static-physical.ts new file mode 100644 index 0000000..e222397 --- /dev/null +++ b/backend/src/physics/physicals/static-physical.ts @@ -0,0 +1,10 @@ +import { PhysicalBase } from './physical-base'; +import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box'; +import { vec2 } from 'gl-matrix'; + +export interface StaticPhysical extends PhysicalBase { + readonly canMove: false; + readonly boundingBox: ImmutableBoundingBox; + + getForce(target: vec2): vec2; +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..d477e81 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "target": "es6", + "esModuleInterop": true, + "strict": true, + "experimentalDecorators": true, + "downlevelIteration": true, + "moduleResolution": "node", + "module": "commonjs", + "composite": true, + "lib": ["dom", "es2017"] + }, + "include": ["src/**/*.ts", "src/*.ts"] +} diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 315221d..b5f2bc9 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,17 +1,17 @@ import { glMatrix } from 'gl-matrix'; -import { CharacterBase, LampBase, overrideDeserialization, StoneBase } from 'shared'; +import { CharacterBase, LampBase, overrideDeserialization, PlanetBase } from 'shared'; import { ProjectileBase } from 'shared/src/objects/types/projectile-base'; import { Game } from './scripts/game'; import { CharacterView } from './scripts/objects/character-view'; import { LampView } from './scripts/objects/lamp-view'; import { ProjectileView } from './scripts/objects/projectile-view'; -import { StoneView } from './scripts/objects/stone-view'; +import { PlanetView } from './scripts/objects/planet-view'; import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); overrideDeserialization(CharacterBase, CharacterView); -overrideDeserialization(StoneBase, StoneView); +overrideDeserialization(PlanetBase, PlanetView); overrideDeserialization(LampBase, LampView); overrideDeserialization(ProjectileBase, ProjectileView); diff --git a/frontend/src/scripts/commands/generators/keyboard-listener.ts b/frontend/src/scripts/commands/generators/keyboard-listener.ts index acde088..09f2c70 100644 --- a/frontend/src/scripts/commands/generators/keyboard-listener.ts +++ b/frontend/src/scripts/commands/generators/keyboard-listener.ts @@ -34,7 +34,7 @@ export class KeyboardListener extends CommandGenerator { if (vec2.squaredLength(movement) > 0) { vec2.normalize(movement, movement); } - this.sendCommandToSubcribers(new MoveActionCommand(movement)); + this.sendCommandToSubcribers(new MoveActionCommand(movement, true)); } private normalize(key: string): string { diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts index 0fa4797..7252046 100644 --- a/frontend/src/scripts/commands/generators/touch-listener.ts +++ b/frontend/src/scripts/commands/generators/touch-listener.ts @@ -38,7 +38,7 @@ export class TouchListener extends CommandGenerator { if (vec2.squaredLength(movement) > 0) { vec2.normalize(movement, movement); - this.sendCommandToSubcribers(new MoveActionCommand(movement)); + this.sendCommandToSubcribers(new MoveActionCommand(movement, false)); } this.previousPosition = position; @@ -46,7 +46,7 @@ export class TouchListener extends CommandGenerator { target.addEventListener('touchend', (event: TouchEvent) => { event.preventDefault(); - this.sendCommandToSubcribers(new MoveActionCommand(vec2.create())); + this.sendCommandToSubcribers(new MoveActionCommand(vec2.create(), false)); }); } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 3e26d16..66bda2c 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,6 +1,5 @@ import { vec2 } from 'gl-matrix'; import { - Circle, CircleLight, compile, FilteringOptions, @@ -28,6 +27,7 @@ import { Configuration } from './config/configuration'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { GameObjectContainer } from './objects/game-object-container'; import { BlobShape } from './shapes/blob-shape'; +import { Circle } from './shapes/circle'; import { Polygon } from './shapes/polygon'; export class Game { @@ -74,14 +74,14 @@ export class Game { } private async setupRenderer(): Promise { - const noiseTexture = await renderNoise([256, 256], 2, 1 / 10); + const noiseTexture = await renderNoise([256, 256], 2, 1); this.renderer = await compile( this.canvas, [ { ...Polygon.descriptor, - shaderCombinationSteps: [0, 2, 6, 16, 32], + shaderCombinationSteps: [0, 2, 6, 16], }, { ...BlobShape.descriptor, @@ -89,7 +89,7 @@ export class Game { }, { ...Circle.descriptor, - shaderCombinationSteps: [0, 2, 16, 32], + shaderCombinationSteps: [0, 2, 16], }, { ...CircleLight.descriptor, @@ -115,7 +115,7 @@ export class Game { rgb(1, 1, 1), ...settings.playerColors, ], - enableHighDpiRendering: false, + enableHighDpiRendering: true, lightCutoffDistance: settings.lightCutoffDistance, textures: { noiseTexture: { diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts index 25f88fb..437d2b0 100644 --- a/frontend/src/scripts/objects/camera.ts +++ b/frontend/src/scripts/objects/camera.ts @@ -27,7 +27,7 @@ export class Camera extends GameObject implements ViewObject { renderer.setViewArea(viewArea.topLeft, viewArea.size); renderer.setRuntimeSettings({ - backgroundColor: mixRgb( + ambientLight: mixRgb( settings.backgroundGradient[0], settings.backgroundGradient[1], (this.center.x - settings.worldLeftEdge) / diff --git a/frontend/src/scripts/objects/stone-view.ts b/frontend/src/scripts/objects/planet-view.ts similarity index 66% rename from frontend/src/scripts/objects/stone-view.ts rename to frontend/src/scripts/objects/planet-view.ts index 56d1b32..22f1263 100644 --- a/frontend/src/scripts/objects/stone-view.ts +++ b/frontend/src/scripts/objects/planet-view.ts @@ -1,11 +1,11 @@ import { vec2 } from 'gl-matrix'; import { Drawable, Renderer } from 'sdf-2d'; -import { CommandExecutors, Id, Random, StoneBase } from 'shared'; +import { CommandExecutors, Id, Random, PlanetBase } from 'shared'; import { RenderCommand } from '../commands/types/render'; import { Polygon } from '../shapes/polygon'; import { ViewObject } from './view-object'; -export class StoneView extends StoneBase implements ViewObject { +export class PlanetView extends PlanetBase implements ViewObject { private shape: Drawable; protected commandExecutors: CommandExecutors = { @@ -14,11 +14,12 @@ export class StoneView extends StoneBase implements ViewObject { constructor(id: Id, vertices: Array) { super(id, vertices); - this.shape = new Polygon(vertices, Random.getRandom()); + this.shape = new Polygon(vertices); + (this.shape as any).randomOffset = Random.getRandom(); } public step(deltaTimeInMilliseconds: number): void { - this.shape.randomOffset += deltaTimeInMilliseconds / 5000; + (this.shape as any).randomOffset += deltaTimeInMilliseconds / 4000; } public draw(renderer: Renderer): void { diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts index 02aa249..8968475 100644 --- a/frontend/src/scripts/objects/projectile-view.ts +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -1,10 +1,11 @@ import { vec2 } from 'gl-matrix'; -import { Circle, CircleLight, Renderer } from 'sdf-2d'; +import { CircleLight, Renderer } from 'sdf-2d'; import { Id, ProjectileBase, rgb } from 'shared'; import { ViewObject } from './view-object'; +import { Circle } from '../shapes/circle'; export class ProjectileView extends ProjectileBase implements ViewObject { - private circle: Circle; + private circle: InstanceType; private light: CircleLight; constructor(id: Id, center: vec2, radius: number) { diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts index 0c44f0b..f79f87f 100644 --- a/frontend/src/scripts/shapes/blob-shape.ts +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -11,7 +11,7 @@ export class BlobShape extends Drawable { uniform vec2 rightFootCenters[BLOB_COUNT]; uniform float headRadii[BLOB_COUNT]; uniform float footRadii[BLOB_COUNT]; - uniform float blobColors[BLOB_COUNT]; + uniform int blobColors[BLOB_COUNT]; float blobSmoothMin(float a, float b) { @@ -24,7 +24,7 @@ export class BlobShape extends Drawable { return distance(target, circleCenter) - radius; } - float blobMinDistance(vec2 target, out float colorIndex) { + float blobMinDistance(vec2 target, out vec4 color) { float minDistance = 1000.0; for (int i = 0; i < BLOB_COUNT; i++) { @@ -40,30 +40,24 @@ export class BlobShape extends Drawable { vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2); vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2); - res = max( - res, - -circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target) + float eyeDistance = min( + circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target), + circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target) ); - res = max( - res, - -circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target) - ); - - res = min( - res, - circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) + eyeDistance = max( + eyeDistance, + -circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target) ); - res = min( - res, - circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) + eyeDistance = max( + eyeDistance, + -circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target) ); - minDistance = min(minDistance, res); - - if (res < 0.0) { - colorIndex = blobColors[i]; + if (res < minDistance) { + minDistance = res; + color = eyeDistance < 0.0 ? vec4(1.0) : readFromPalette(blobColors[i]); } } diff --git a/frontend/src/scripts/shapes/circle.ts b/frontend/src/scripts/shapes/circle.ts new file mode 100644 index 0000000..e4f8374 --- /dev/null +++ b/frontend/src/scripts/shapes/circle.ts @@ -0,0 +1,3 @@ +import { CircleFactory } from 'sdf-2d'; + +export const Circle = CircleFactory(2); diff --git a/package.json b/package.json index 71fa50c..d9875f5 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "gl-matrix": "^3.3.0", "lerna": "^3.22.1", "prettier": "^2.0.5", - "sdf-2d": "^0.4.1", + "sdf-2d": "^0.5.2", "socket.io-client": "^2.3.1", "typescript": "^4.0.3", "uuid": "^8.2.0" diff --git a/shared/src/commands/types/move-action.ts b/shared/src/commands/types/move-action.ts index 3e49e4b..94f25b7 100644 --- a/shared/src/commands/types/move-action.ts +++ b/shared/src/commands/types/move-action.ts @@ -4,11 +4,14 @@ import { Command } from '../command'; @serializable export class MoveActionCommand extends Command { - public constructor(public readonly direction: vec2) { + public constructor( + public readonly direction: vec2, + public readonly isCharacterRelative: boolean, + ) { super(); } public toArray(): Array { - return [this.direction]; + return [this.direction, this.isCharacterRelative]; } } diff --git a/shared/src/helper/rotate-minus-90-deg.ts b/shared/src/helper/rotate-minus-90-deg.ts new file mode 100644 index 0000000..5021d64 --- /dev/null +++ b/shared/src/helper/rotate-minus-90-deg.ts @@ -0,0 +1,3 @@ +import { vec2 } from 'gl-matrix'; + +export const rotateMinus90Deg = (vec: vec2): vec2 => vec2.fromValues(vec.y, -vec.x); diff --git a/shared/src/main.ts b/shared/src/main.ts index dcdfc00..9b2d076 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -26,6 +26,7 @@ export * from './helper/mix'; export * from './helper/random'; export * from './helper/id'; export * from './helper/rotate-90-deg'; +export * from './helper/rotate-minus-90-deg'; export * from './objects/game-object'; export * from './transport/serialization/deserialize'; export * from './transport/serialization/serialize'; @@ -34,7 +35,7 @@ export * from './transport/serialization/serializable'; export * from './transport/serialization/override-deserialization'; export * from './objects/types/character-base'; export * from './objects/types/lamp-base'; -export * from './objects/types/stone-base'; +export * from './objects/types/planet-base'; export * from './objects/types/projectile-base'; export * from './settings'; export * from './transport/transport-events'; diff --git a/shared/src/objects/types/stone-base.ts b/shared/src/objects/types/planet-base.ts similarity index 89% rename from shared/src/objects/types/stone-base.ts rename to shared/src/objects/types/planet-base.ts index 8736673..ee62dfd 100644 --- a/shared/src/objects/types/stone-base.ts +++ b/shared/src/objects/types/planet-base.ts @@ -4,7 +4,7 @@ import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; @serializable -export class StoneBase extends GameObject { +export class PlanetBase extends GameObject { constructor(id: Id, public readonly vertices: Array) { super(id); } diff --git a/shared/src/settings.ts b/shared/src/settings.ts index c933924..777265c 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -1,21 +1,25 @@ -import { vec2 } from 'gl-matrix'; import { rgb255 } from './helper/rgb255'; export const settings = { lightCutoffDistance: 600, - physicsMaxStep: 5, - gravitationalForce: vec2.fromValues(0, -3000), - maxVelocityX: 2000, - maxVelocityY: 3650, - polygonEdgeCount: 8, - projectileSpeed: 2000, + physicsMaxStep: 2, + maxVelocityX: 1000, + maxVelocityY: 1000, + polygonEdgeCount: 7, + maxGravityDistance: 700, + maxGravityQ: 180, + maxGravityStrength: 10000, + maxAcceleration: 10000, + projectileSpeed: 4000, projectileMaxBounceCount: 1, - projectileStartOffset: 150, - projectileCreationInterval: 500, + projectileStartOffset: 250, + projectileCreationInterval: 0.1, playerColorIndexOffset: 3, - worldLeftEdge: -5000, - worldRightEdge: 5000, - backgroundGradient: [rgb255(255, 117, 129), rgb255(136, 117, 255)], + worldTopEdge: 10000, + worldRightEdge: 10000, + worldLeftEdge: -10000, + worldBottomEdge: -10000, + backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], playerColors: [ rgb255(107, 48, 188), rgb255(56, 254, 220), @@ -46,11 +50,8 @@ export const settings = { rgb255(114, 73, 30), rgb255(75, 75, 75), ], - maxAccelerationX: 2000000, - maxAccelerationY: 20500, targetPhysicsDeltaTimeInMilliseconds: 20, minPhysicsSleepTime: 4, - velocityAttenuation: 0.1, + velocityAttenuation: 0.25, inViewAreaSize: 1920 * 1080 * 3, - defaultJumpEnergy: 0.25, }; diff --git a/tsconfig.json b/tsconfig.json index 6b26fb4..33b118a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,6 @@ "module": "commonjs", "composite": true, "lib": ["dom", "es2017"] - } + }, + "include": ["**/*.ts"] }