From c9eae3adeb434477ce079b9e30b88930efb76fd9 Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Tue, 27 Oct 2020 10:32:37 +0100 Subject: [PATCH] Improve movement --- backend/src/objects/circle-physical.ts | 90 +++------ backend/src/objects/planet-physical.ts | 38 +--- .../src/objects/player-character-physical.ts | 185 +++++++++--------- backend/src/objects/projectile-physical.ts | 15 +- backend/src/physics/functions/move-circle.ts | 107 ++++++---- backend/src/players/npc.ts | 2 +- backend/src/players/player-base.ts | 2 +- backend/tsconfig.json | 1 - frontend/src/index.html | 9 - frontend/src/index.ts | 4 - .../commands/generators/keyboard-listener.ts | 4 +- .../generators/touch-joystick-listener.ts | 19 +- frontend/src/scripts/join-form-handler.ts | 3 + frontend/src/scripts/options-handler.ts | 2 - shared/src/commands/types/move-action.ts | 7 +- shared/src/settings.ts | 18 +- 16 files changed, 227 insertions(+), 279 deletions(-) diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index bdadbeb..f92f91c 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -10,6 +10,7 @@ import { ReactsToCollision, reactsToCollision, } from '../physics/physicals/reacts-to-collision'; +import { Physical } from '../physics/physicals/physical'; @serializesTo(Circle) export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollision { @@ -18,6 +19,7 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio public velocity = vec2.create(); private _boundingBox: BoundingBox; + public lastNormal = vec2.fromValues(1, 0); constructor( private _center: vec2, @@ -95,79 +97,47 @@ export class CirclePhysical implements Circle, DynamicPhysical, ReactsToCollisio public step(_: number) {} - public step2(deltaTimeInSeconds: number): GameObject | undefined { - vec2.scale( - this.velocity, - this.velocity, - Math.pow(settings.velocityAttenuation, deltaTimeInSeconds), - ); - - const delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); + public step2( + deltaTimeInSeconds: number, + additionalCollider?: Physical, + ): { hitObject: GameObject | undefined; velocity: vec2 } { + let delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); this.radius += vec2.length(delta); - const intersecting = this.container.findIntersecting(this.boundingBox); + const intersecting = this.container + .findIntersecting(this.boundingBox) + .filter((b) => b.gameObject !== this.gameObject && b.canCollide); this.radius -= vec2.length(delta); - const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); - vec2.scale(delta, delta, 1 / stepCount); + if (additionalCollider) { + intersecting.push(additionalCollider); + } - let lastHit: GameObject | undefined; + let { normal, hitSurface, hitObject } = moveCircle(this, delta, intersecting); - for (let i = 0; i < stepCount; i++) { - const distance = vec2.scale( - vec2.create(), + if (hitSurface) { + vec2.copy(this.lastNormal, normal!); + + vec2.subtract( this.velocity, - deltaTimeInSeconds / stepCount, + this.velocity, + vec2.scale( + normal!, + normal!, + (1 + this.restitution) * vec2.dot(normal!, this.velocity), + ), ); - const { normal, hitSurface, hitObject } = moveCircle( - this, - vec2.clone(distance), - intersecting, - ); - - if (hitSurface) { - vec2.subtract( - this.velocity, - this.velocity, - vec2.scale( - normal!, - normal!, - (1 + this.restitution) * vec2.dot(normal!, this.velocity), - ), - ); - - lastHit = hitObject; + if (vec2.length(this.velocity) > 50) { + delta = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); + moveCircle(this, delta, intersecting); } } - return lastHit; - } + const lastVelocity = vec2.clone(this.velocity); + vec2.zero(this.velocity); - public tryMove(delta: vec2): GameObject | undefined { - this.radius += vec2.length(delta); - const intersecting = this.container.findIntersecting(this.boundingBox); - this.radius -= vec2.length(delta); - - const stepCount = Math.ceil(vec2.length(delta) / settings.physicsMaxStep); - vec2.scale(delta, delta, 1 / stepCount); - - let lastHit: GameObject | undefined; - - for (let i = 0; i < stepCount; i++) { - const { tangent, hitSurface, hitObject } = moveCircle( - this, - vec2.clone(delta), - intersecting, - ); - - if (hitSurface) { - delta = vec2.scale(delta, tangent!, vec2.length(delta)); - lastHit = hitObject; - } - } - - return lastHit; + return { hitObject, velocity: lastVelocity }; } public toArray(): Array { diff --git a/backend/src/objects/planet-physical.ts b/backend/src/objects/planet-physical.ts index 3027af5..b882f66 100644 --- a/backend/src/objects/planet-physical.ts +++ b/backend/src/objects/planet-physical.ts @@ -154,43 +154,15 @@ export class PlanetPhysical } 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 diff = vec2.subtract(vec2.create(), this.center, position); + const dist = Math.max(0, vec2.length(diff) - 600); + vec2.normalize(diff, diff); const scale = clamp( - settings.maxGravityQ * ((settings.maxGravityDistance / height) ** 3 - 1), + settings.maxGravityQ * ((settings.maxGravityDistance / dist) ** 1.5 - 1), 0, settings.maxGravityStrength, ); - return vec2.scale(normal, normal, scale); + return vec2.scale(diff, diff, scale); } public get gameObject(): this { diff --git a/backend/src/objects/player-character-physical.ts b/backend/src/objects/player-character-physical.ts index 44b7494..54d7d7b 100644 --- a/backend/src/objects/player-character-physical.ts +++ b/backend/src/objects/player-character-physical.ts @@ -70,10 +70,13 @@ export class PlayerCharacterPhysical public static readonly boundRadius = (PlayerCharacterPhysical.headRadius + PlayerCharacterPhysical.feetRadius * 2) * 2; + private timeSinceDying = 0; private isDestroyed = false; + private timeSinceBorn = 0; + private hasJustBorn = true; + private direction = 0; private currentPlanet?: PlanetPhysical; - private lastMovementWasRelative = false; private secondsSinceOnSurface = 1000; public head: CirclePhysical; @@ -81,15 +84,8 @@ export class PlayerCharacterPhysical public rightFoot: CirclePhysical; public bound: CirclePhysical; - public get isAlive(): boolean { - return !this.isDestroyed; - } - private movementActions: Array = []; - private lastMovementAction: MoveActionCommand = new MoveActionCommand( - vec2.create(), - false, - ); + private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create()); constructor( name: string, @@ -131,6 +127,10 @@ export class PlayerCharacterPhysical ); } + public get isAlive(): boolean { + return !this.isDestroyed; + } + public handleMovementAction(c: MoveActionCommand) { this.movementActions.push(c); } @@ -150,7 +150,7 @@ export class PlayerCharacterPhysical this.health -= other.strength; this.remoteCall('setHealth', this.health); if (this.health <= 0) { - this.destroy(); + this.kill(); other.originator.addKill(); } } @@ -217,9 +217,6 @@ export class PlayerCharacterPhysical vec2.scale(direction, direction, 1 / this.movementActions.length); - this.lastMovementWasRelative = - this.movementActions.find((a) => a.isCharacterRelative) !== undefined; - this.lastMovementAction = last(this.movementActions)!; this.movementActions = []; } @@ -227,8 +224,35 @@ export class PlayerCharacterPhysical return vec2.normalize(direction, direction); } + private animateScaling(q: number) { + this.remoteCall( + 'updateCircles', + new Circle(this.head.center, PlayerCharacterPhysical.headRadius * q), + new Circle(this.leftFoot.center, PlayerCharacterPhysical.feetRadius * q), + new Circle(this.rightFoot.center, PlayerCharacterPhysical.feetRadius * q), + ); + } + public step(deltaTime: number) { - if ((this.secondsSinceOnSurface += deltaTime) > 1) { + if (this.isDestroyed) { + if ((this.timeSinceDying += deltaTime) > settings.spawnDespawnTime) { + this.destroy(); + } else { + this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime); + } + return; + } + + if (this.hasJustBorn) { + if ((this.timeSinceBorn += deltaTime) > settings.spawnDespawnTime) { + this.hasJustBorn = false; + } else { + this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime); + } + return; + } + + if ((this.secondsSinceOnSurface += deltaTime) > 0.5) { this.currentPlanet = undefined; } @@ -247,34 +271,24 @@ export class PlayerCharacterPhysical ), ), ); - const feetCenter = vec2.add( - vec2.create(), - this.leftFoot.center, - this.rightFoot.center, - ); - vec2.scale(feetCenter, feetCenter, 0.5); - const leftFootGravity = forceAtPosition( - this.leftFoot.center, - intersectingWithForcefield, - ); - const rightFootGravity = forceAtPosition( - this.rightFoot.center, - intersectingWithForcefield, - ); const direction = this.averageAndResetMovementActions(); const movementForce = vec2.scale(direction, direction, settings.maxAcceleration); if (!this.currentPlanet) { + const leftFootGravity = forceAtPosition( + this.leftFoot.center, + intersectingWithForcefield, + ); + const rightFootGravity = forceAtPosition( + this.rightFoot.center, + intersectingWithForcefield, + ); + this.applyForce(this.leftFoot, leftFootGravity, deltaTime); this.applyForce(this.rightFoot, rightFootGravity, deltaTime); - const sumForce = vec2.subtract( - vec2.create(), - // the next line is intentional - vec2.add(vec2.create(), leftFootGravity, rightFootGravity), - movementForce, - ); + const sumForce = vec2.subtract(vec2.create(), leftFootGravity, movementForce); this.setDirection( vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce, @@ -284,32 +298,41 @@ export class PlayerCharacterPhysical const leftFootGravity = this.currentPlanet!.getForce(this.leftFoot.center); const rightFootGravity = this.currentPlanet!.getForce(this.rightFoot.center); - if (this.lastMovementWasRelative) { - vec2.rotate(movementForce, movementForce, vec2.create(), this.direction); + vec2.add(leftFootGravity, leftFootGravity, rightFootGravity); + const gravity = vec2.scale(leftFootGravity, leftFootGravity, 0.5); + + if (vec2.dot(movementForce, gravity) < -vec2.length(movementForce) * 0.8) { + vec2.scale(gravity, gravity, 0.35); } - if (vec2.dot(movementForce, leftFootGravity) < -vec2.length(movementForce) * 0.8) { - vec2.scale(leftFootGravity, leftFootGravity, 0.35); - vec2.scale(rightFootGravity, rightFootGravity, 0.35); - } - this.applyForce(this.leftFoot, leftFootGravity, deltaTime); - this.applyForce(this.rightFoot, rightFootGravity, deltaTime); + const scaledLeftFootGravity = vec2.scale( + vec2.create(), + this.leftFoot.lastNormal, + vec2.dot(this.leftFoot.lastNormal, gravity), + ); + this.applyForce(this.leftFoot, scaledLeftFootGravity, deltaTime); - const headGravity = this.currentPlanet!.getForce(this.head.center); + const scaledRightFootGravity = vec2.scale( + vec2.create(), + this.rightFoot.lastNormal, + vec2.dot(this.rightFoot.lastNormal, gravity), + ); - if (vec2.length(headGravity) < vec2.length(leftFootGravity) / 2) { + this.applyForce(this.rightFoot, scaledRightFootGravity, deltaTime); + + if (vec2.length(gravity) <= 100) { this.currentPlanet = undefined; } - this.setDirection(headGravity, deltaTime); + this.setDirection(gravity, deltaTime); } this.applyForce(this.leftFoot, movementForce, deltaTime); this.applyForce(this.rightFoot, movementForce, deltaTime); + this.keepPosture(deltaTime); this.stepBodyPart(this.leftFoot, deltaTime); this.stepBodyPart(this.rightFoot, deltaTime); - - this.keepPosture(deltaTime); + this.stepBodyPart(this.head, deltaTime); this.remoteCall('updateCircles', this.head, this.leftFoot, this.rightFoot); } @@ -323,45 +346,29 @@ export class PlayerCharacterPhysical } private keepPosture(deltaTime: number) { - const center = this.center; + let center = this.center; this.springMove( this.leftFoot, center, PlayerCharacterPhysical.leftFootOffset, deltaTime, + 15000, ); this.springMove( this.rightFoot, center, PlayerCharacterPhysical.rightFootOffset, deltaTime, + 15000, ); - /* - const feetDelta = vec2.subtract( - vec2.create(), - this.leftFoot.center, - this.rightFoot.center, - ); - const desiredDistance = vec2.dist( - PlayerCharacterPhysical.desiredLeftFootOffset, - PlayerCharacterPhysical.desiredRightFootOffset, - ); - const actualDistance = vec2.length(feetDelta); - const delta = vec2.normalize(feetDelta, feetDelta); - vec2.scale(delta, delta, Math.min(actualDistance / 2, deltaTime * 200)); - let hitObject = this.rightFoot.tryMove(delta); - if (hitObject instanceof PlanetPhysical) { - this.secondsSinceOnSurface = 0; - this.currentPlanet = hitObject; - } - vec2.scale(delta, delta, -1); - hitObject = this.leftFoot.tryMove(delta); - if (hitObject instanceof PlanetPhysical) { - this.secondsSinceOnSurface = 0; - this.currentPlanet = hitObject; - }*/ - this.springMove(this.head, center, PlayerCharacterPhysical.headOffset, deltaTime); + this.springMove( + this.head, + center, + PlayerCharacterPhysical.headOffset, + deltaTime, + 25000, + ); } private springMove( @@ -369,28 +376,25 @@ export class PlayerCharacterPhysical center: vec2, offset: vec2, deltaTime: number, + strength: number, ) { const desiredPosition = vec2.add(vec2.create(), center, offset); vec2.rotate(desiredPosition, desiredPosition, center, this.direction); - const positionDelta = vec2.subtract(desiredPosition, desiredPosition, object.center); + const positionDelta = vec2.subtract(vec2.create(), desiredPosition, object.center); + const positionDeltaDirection = vec2.normalize(vec2.create(), positionDelta); const positionDeltaLength = vec2.length(positionDelta); vec2.scale( positionDelta, positionDeltaDirection, - Math.min(positionDeltaLength, (positionDeltaLength / 50) * deltaTime * 800), + positionDeltaLength * deltaTime * strength, ); - const hitObject = object.tryMove(positionDelta); - if (hitObject instanceof PlanetPhysical) { - this.secondsSinceOnSurface = 0; - this.currentPlanet = hitObject; - } + object.applyForce(positionDelta, deltaTime); } private stepBodyPart(part: CirclePhysical, deltaTime: number) { - const hitObject = part.step2(deltaTime); - + const { hitObject } = part.step2(deltaTime); if (hitObject instanceof PlanetPhysical) { this.secondsSinceOnSurface = 0; this.currentPlanet = hitObject; @@ -411,13 +415,14 @@ export class PlayerCharacterPhysical ); } - public destroy() { - if (!this.isDestroyed) { - this.isDestroyed = true; - this.container.removeObject(this); - this.container.removeObject(this.head); - this.container.removeObject(this.leftFoot); - this.container.removeObject(this.rightFoot); - } + public kill() { + this.isDestroyed = true; + } + + private destroy() { + this.container.removeObject(this); + this.container.removeObject(this.head); + this.container.removeObject(this.leftFoot); + this.container.removeObject(this.rightFoot); } } diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 18f421b..6787fee 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -114,10 +114,19 @@ export class ProjectilePhysical } }); - vec2.add(this.velocity, this.velocity, vec2.scale(vec2.create(), gravity, deltaTime)); + vec2.add( + this.velocity, + this.velocity, + vec2.scale( + vec2.create(), + gravity, + deltaTime * settings.gravityScalingForProjectiles, + ), + ); - this.object.velocity = this.velocity; - this.object.step2(deltaTime); + vec2.copy(this.object.velocity, this.velocity); + const { velocity } = this.object.step2(deltaTime); + vec2.copy(this.velocity, velocity); this.remoteCall('setCenter', this.center); } diff --git a/backend/src/physics/functions/move-circle.ts b/backend/src/physics/functions/move-circle.ts index 4500985..8b436f7 100644 --- a/backend/src/physics/functions/move-circle.ts +++ b/backend/src/physics/functions/move-circle.ts @@ -17,59 +17,82 @@ export const moveCircle = ( tangent?: vec2; hitObject?: GameObject; } => { - const nextCircle = new Circle(vec2.clone(circle.center), circle.radius); - vec2.add(nextCircle.center, nextCircle.center, delta); + const direction = vec2.normalize(vec2.create(), delta); + const deltaLength = vec2.length(delta); + let travelled = 0; + let rayEnd = vec2.create(); + let prevMinDistance = 0; + while (travelled < deltaLength) { + travelled += prevMinDistance; + vec2.add( + rayEnd, + circle.center, + vec2.scale(vec2.create(), direction, Math.min(travelled, deltaLength)), + ); - possibleIntersectors = possibleIntersectors.filter( - (b) => b.gameObject !== circle.gameObject && b.canCollide, - ); + const minDistance = evaluateSdf(rayEnd, possibleIntersectors); - const sdfAtCenter = evaluateSdf(nextCircle.center, possibleIntersectors); + if (minDistance < circle.radius) { + const intersecting = possibleIntersectors.find( + (i) => i.distance(rayEnd) <= circle.radius, + )!; - if (sdfAtCenter > nextCircle.radius) { - circle.center = vec2.add(circle.center, circle.center, delta); + if (ignoreCollision) { + circle.center = vec2.add(circle.center, circle.center, delta); + } else { + if (reactsToCollision(intersecting)) { + intersecting.onCollision(circle.gameObject); + } - return { - realDelta: delta, - hitSurface: false, - }; - } + if (reactsToCollision(circle)) { + circle.onCollision(intersecting.gameObject); + } + } - const intersecting = possibleIntersectors.find( - (i) => i.distance(nextCircle.center) <= circle.radius, - )!; + vec2.add( + rayEnd, + circle.center, + vec2.scale(vec2.create(), direction, travelled - prevMinDistance), + ); - if (ignoreCollision) { - circle.center = vec2.add(circle.center, circle.center, delta); - } else { - if (reactsToCollision(intersecting)) { - intersecting.onCollision(circle.gameObject); + vec2.copy(circle.center, rayEnd); + + const dx = + evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0.01, 0)), [ + intersecting, + ]) - + evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(-0.01, 0)), [ + intersecting, + ]); + const dy = + evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0, 0.01)), [ + intersecting, + ]) - + evaluateSdf(vec2.add(vec2.create(), rayEnd, vec2.fromValues(0, -0.01)), [ + intersecting, + ]); + const normal = vec2.fromValues(dx, dy); + vec2.normalize(normal, normal); + const rotatedNormal = rotate90Deg(normal); + return { + realDelta: delta, + hitSurface: true, + normal, + hitObject: intersecting?.gameObject, + tangent: + vec2.dot(rotatedNormal, delta) < 0 + ? vec2.scale(rotatedNormal, rotatedNormal, -1) + : rotatedNormal, + }; } - if (reactsToCollision(circle)) { - circle.onCollision(intersecting.gameObject); - } + prevMinDistance = minDistance; } - const dx = - evaluateSdf(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0.01, 0)), [ - intersecting, - ]) - sdfAtCenter; - const dy = - 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); + vec2.add(circle.center, circle.center, delta); + return { realDelta: delta, - hitSurface: true, - normal, - hitObject: intersecting?.gameObject, - tangent: - vec2.dot(rotatedNormal, delta) < 0 - ? vec2.scale(rotatedNormal, rotatedNormal, -1) - : rotatedNormal, + hitSurface: false, }; }; diff --git a/backend/src/players/npc.ts b/backend/src/players/npc.ts index 53b58ce..5151f9c 100644 --- a/backend/src/players/npc.ts +++ b/backend/src/players/npc.ts @@ -196,7 +196,7 @@ export class NPC extends PlayerBase { this.timeSinceLastFindShootTarget = 0; } - this.character?.handleMovementAction(new MoveActionCommand(this.direction, false)); + this.character?.handleMovementAction(new MoveActionCommand(this.direction)); } protected createCharacter() { diff --git a/backend/src/players/player-base.ts b/backend/src/players/player-base.ts index 258545b..10b9450 100644 --- a/backend/src/players/player-base.ts +++ b/backend/src/players/player-base.ts @@ -69,6 +69,6 @@ export abstract class PlayerBase extends CommandReceiver { } public destroy() { - this.character?.destroy(); + this.character?.kill(); } } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 20c6965..c5b0dad 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -9,7 +9,6 @@ "downlevelIteration": true, "moduleResolution": "node", "module": "commonjs", - "composite": true, "lib": ["dom", "es2017"], "typeRoots": ["./types", "./node_modules/@types"] }, diff --git a/frontend/src/index.html b/frontend/src/index.html index 16829d2..ace21fc 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -63,15 +63,6 @@
- -