diff --git a/backend/src/helper/delta-time-calculator.ts b/backend/src/helper/delta-time-calculator.ts new file mode 100644 index 0000000..ebf75f5 --- /dev/null +++ b/backend/src/helper/delta-time-calculator.ts @@ -0,0 +1,12 @@ +export class DeltaTimeCalculator { + private previousTime: [number, number] = process.hrtime(); + + public getNextDeltaTimeInMilliseconds(): number { + const deltaTime = process.hrtime(this.previousTime); + this.previousTime = process.hrtime(); + + const [seconds, nanoSeconds] = deltaTime; + + return seconds * 1000 + nanoSeconds / 1000 / 1000; + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts index dbe0f6c..1e90611 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,12 +2,19 @@ import ioserver, { Socket } from 'socket.io'; import express from 'express'; import { Server } from 'http'; import cors from 'cors'; -import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared'; +import { + applyArrayPlugins, + Random, + TransportEvents, + deserializeCommand, + StepCommand, +} from 'shared'; import './index.html'; import { Player } from './players/player'; import { PhysicalContainer } from './physics/containers/physical-container'; import { createDungeon } from './map/create-dungeon'; import { glMatrix } from 'gl-matrix'; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; glMatrix.setMatrixArrayType(Array); @@ -26,6 +33,8 @@ objects.initialize(); const app = express(); +const deltaTimeCalculator = new DeltaTimeCalculator(); + app.use( cors({ origin: (origin, callback) => { @@ -50,7 +59,6 @@ app.get('/', function (req, res) { io.on('connection', (socket: SocketIO.Socket) => { socket.on(TransportEvents.PlayerJoining, () => { const player = new Player(objects, socket); - socket.on(TransportEvents.PlayerToServer, (text: string) => { const command = deserializeCommand(JSON.parse(text)); player.sendCommand(command); @@ -71,3 +79,14 @@ io.on('connection', (socket: SocketIO.Socket) => { server.listen(port, () => { console.log(`server started at http://localhost:${port}`); }); + +const handlePhysics = () => { + const delta = deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); + const step = new StepCommand(delta); + + objects.sendCommand(step); + + setTimeout(handlePhysics, 5); +}; + +handlePhysics(); diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index c5ca448..3c3204d 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -1,27 +1,64 @@ import { vec2 } from 'gl-matrix'; -import { id, CharacterBase } from 'shared'; +import { + id, + CharacterBase, + StepCommand, + settings, + CommandExecutors, + MoveActionCommand, +} from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; import { Physical } from '../physics/physical'; +import { PhysicalContainer } from '../physics/containers/physical-container'; export class CharacterPhysical extends CharacterBase implements Physical { public readonly canCollide = true; public readonly isInverted = false; public readonly canMove = true; + public head: CirclePhysical; + public leftFoot: CirclePhysical; + public rightFoot: CirclePhysical; + + private movementActions: Array = []; + + protected commandExecutors: CommandExecutors = { + [StepCommand.type]: this.step.bind(this), + [MoveActionCommand.type]: (c: MoveActionCommand) => this.movementActions.push(c), + }; + private static readonly headOffset = vec2.fromValues(0, 40); private static readonly leftFootOffset = vec2.fromValues(-20, -10); private static readonly rightFootOffset = vec2.fromValues(20, -10); - constructor() { + constructor(container: PhysicalContainer) { super( id(), - new CirclePhysical(vec2.clone(CharacterPhysical.headOffset), 50, null), - new CirclePhysical(vec2.clone(CharacterPhysical.leftFootOffset), 20, null), - new CirclePhysical(vec2.clone(CharacterPhysical.rightFootOffset), 20, null) + new CirclePhysical(vec2.clone(CharacterPhysical.headOffset), 50, null, container), + new CirclePhysical( + vec2.clone(CharacterPhysical.leftFootOffset), + 20, + null, + container + ), + new CirclePhysical( + vec2.clone(CharacterPhysical.rightFootOffset), + 20, + null, + container + ) ); + + this.head.owner = this; + this.leftFoot.owner = this; + this.rightFoot.owner = this; + + container.addObject(this.head); + container.addObject(this.leftFoot); + container.addObject(this.rightFoot); } private _boundingBox?: ImmutableBoundingBox; @@ -42,16 +79,51 @@ export class CharacterPhysical extends CharacterBase implements Physical { public distance(_: vec2): number { return 0; } - /* - public step() { - const movementForce = vec2.fromValues(right - left, up - down); + + private sumAndResetMovementActions(): vec2 { + if (this.movementActions.length === 0) { + return vec2.create(); + } + + const movementForce = this.movementActions.reduce( + (sum, current) => vec2.add(sum, sum, current.delta), + vec2.create() + ); + + vec2.scale(movementForce, movementForce, 1 / this.movementActions.length); + + this.movementActions = []; + + return movementForce; + } + + public step(c: StepCommand) { + const deltaTime = c.deltaTimeInMiliseconds; + + const movementForce = this.sumAndResetMovementActions(); + if (!this.leftFoot.isAirborne || !this.rightFoot.isAirborne) { + movementForce.y = 0; + } + this.head.applyForce(movementForce, deltaTime); this.head.applyForce(settings.gravitationalForce, deltaTime); - const bodyCenter = vec2.sub(vec2.create(), this.head.center, this.headOffset); + const bodyCenter = vec2.sub( + vec2.create(), + this.head.center, + CharacterPhysical.headOffset + ); - const leftFootPositon = vec2.add(vec2.create(), bodyCenter, this.leftFootOffset); - const rightFootPositon = vec2.add(vec2.create(), bodyCenter, this.rightFootOffset); + const leftFootPositon = vec2.add( + vec2.create(), + bodyCenter, + CharacterPhysical.leftFootOffset + ); + const rightFootPositon = vec2.add( + vec2.create(), + bodyCenter, + CharacterPhysical.rightFootOffset + ); const leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon); const rightFootDelta = vec2.sub( @@ -80,13 +152,13 @@ export class CharacterPhysical extends CharacterBase implements Physical { this.leftFoot.step(deltaTime); this.rightFoot.step(deltaTime); - this.flashlight.center = vec2.add( + /*this.flashlight.center = vec2.add( vec2.create(), this.head.center, vec2.fromValues(0, 0) - ); + );*/ } -*/ + public toJSON(): any { const { type, id, head, leftFoot, rightFoot } = this; return [type, id, head, leftFoot, rightFoot]; diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index 32e3f86..45df3df 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,9 +1,18 @@ import { vec2 } from 'gl-matrix'; -import { Circle, clamp, GameObject, settings } from 'shared'; +import { + Circle, + clamp, + CommandExecutors, + GameObject, + settings, + StepCommand, +} from 'shared'; import { Physical } from '../physics/physical'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; +import { moveCircle } from '../physics/move-circle'; +import { PhysicalContainer } from '../physics/containers/physical-container'; export class CirclePhysical implements Circle, Physical { readonly isInverted = false; @@ -18,9 +27,18 @@ export class CirclePhysical implements Circle, Physical { return this._isAirborne; } + protected commandExecutors: CommandExecutors = { + [StepCommand.type]: this.step.bind(this), + }; + private _boundingBox: BoundingBox; - constructor(private _center: vec2, private _radius: number, private owner: GameObject) { + constructor( + private _center: vec2, + private _radius: number, + public owner: GameObject, + private readonly container: PhysicalContainer + ) { this._boundingBox = new BoundingBox(null); this.recalculateBoundingBox(); } @@ -59,11 +77,11 @@ export class CirclePhysical implements Circle, Physical { return vec2.distance(target.center, this.center) - this.radius - target.radius; } - public areIntersecting(other: CirclePhysical): boolean { + public areIntersecting(other: Physical): boolean { return other.distance(this.center) < this.radius; } - public isInside(other: CirclePhysical): boolean { + public isInside(other: Physical): boolean { return other.distance(this.center) < -this.radius; } @@ -105,12 +123,13 @@ export class CirclePhysical implements Circle, Physical { this.velocity = vec2.create(); } - /*public step(timeInMilliseconds: number): boolean { + public step(timeInMilliseconds: number): boolean { vec2.scale( this.velocity, this.velocity, Math.pow(settings.velocityAttenuation, timeInMilliseconds) ); + const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds); const distanceLength = vec2.length(distance); const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep); @@ -119,22 +138,21 @@ export class CirclePhysical implements Circle, Physical { let wasHit = false; for (let i = 0; i < stepCount; i++) { - const { normal, tangent, hitSurface } = this.physics.tryMovingDynamicCircle( + const { normal, tangent, hitSurface } = moveCircle( this, - distance + distance, + this.container.findIntersecting(this.boundingBox) ); if (hitSurface) { vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity)); wasHit = true; } - this._isAirborne = !( - hitSurface && vec2.dot(normal, settings.gravitationalForce) < 0 - ); + this._isAirborne = hitSurface; } return wasHit; - }*/ + } public toJSON(): any { const { center, radius } = this; diff --git a/backend/src/physics/containers/physical-container.ts b/backend/src/physics/containers/physical-container.ts index 2ce9ba4..61b7a56 100644 --- a/backend/src/physics/containers/physical-container.ts +++ b/backend/src/physics/containers/physical-container.ts @@ -71,7 +71,7 @@ export class PhysicalContainer { private createGroupForCommand(commandType: string) { const objectsReactingToCommand = []; - + console.log(commandType); this.objects.forEach((o, _) => { if (o.reactsToCommand(commandType)) { objectsReactingToCommand.push(o); diff --git a/backend/src/physics/move-circle.ts b/backend/src/physics/move-circle.ts index dc6aa83..d152c57 100644 --- a/backend/src/physics/move-circle.ts +++ b/backend/src/physics/move-circle.ts @@ -1,12 +1,12 @@ -/*import { vec2 } from 'gl-matrix'; -import { rotate90Deg } from 'shared'; -import { CirclePhysics } from './bounds/circle-physics'; -import { PhysicalGameObject } from './physical-game-object'; +import { vec2 } from 'gl-matrix'; +import { rotate90Deg, settings } from 'shared'; +import { CirclePhysical } from '../objects/circle-physical'; +import { Physical } from './physical'; export const moveCircle = ( - circle: CirclePhysics, + circle: CirclePhysical, delta: vec2, - possibleIntersectors: Array + possibleIntersectors: Array ): { realDelta: vec2; hitSurface: boolean; @@ -16,7 +16,7 @@ export const moveCircle = ( circle.center = vec2.add(circle.center, circle.center, delta); const intersecting = possibleIntersectors.filter( - (b) => b !== circle && circle.areIntersecting(b) && b.canCollide + (b) => b.gameObject !== circle.gameObject && circle.areIntersecting(b) && b.canCollide ); if (intersecting.length === 0) { @@ -34,7 +34,7 @@ export const moveCircle = ( closest: intersecting .map((i) => ({ inverted: i.isInverted, - distance: i.owner.distance(point), + distance: i.distance(point), })) .sort((a, b) => a.distance - b.distance)[0], })) @@ -79,4 +79,3 @@ export const moveCircle = ( tangent: rotate90Deg(approxNormal), }; }; -*/ diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 0517b65..a9f5b49 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -21,7 +21,7 @@ import { jsonSerialize } from '../serialize'; export class Player extends CommandReceiver { public isActive = true; - private character: CharacterPhysical = new CharacterPhysical(); + private character: CharacterPhysical; private objectsPreviouslyInViewArea: Array = []; private objectsInViewArea: Array = []; @@ -30,12 +30,8 @@ export class Player extends CommandReceiver { [SetViewAreaActionCommand.type]: this.setViewArea.bind(this), [MoveActionCommand.type]: (c: MoveActionCommand) => { vec2.normalize(c.delta, c.delta); - vec2.scale(c.delta, c.delta, 40); - this.character.head.center = vec2.add( - this.character.head.center, - this.character.head.center, - c.delta - ); + vec2.scale(c.delta, c.delta, 15); + this.character.sendCommand(c); }, }; @@ -46,6 +42,7 @@ export class Player extends CommandReceiver { private readonly socket: SocketIO.Socket ) { super(); + this.character = new CharacterPhysical(objects); this.objectsPreviouslyInViewArea.push(this.character); this.objectsInViewArea.push(this.character); @@ -82,7 +79,9 @@ export class Player extends CommandReceiver { this.socket.emit( TransportEvents.ServerToPlayer, jsonSerialize( - new DeleteObjectsCommand(noLongerIntersecting.map((p) => p.gameObject.id)) + new DeleteObjectsCommand([ + ...new Set(noLongerIntersecting.map((p) => p.gameObject.id)), + ]) ) ); } @@ -92,7 +91,7 @@ export class Player extends CommandReceiver { TransportEvents.ServerToPlayer, jsonSerialize( new CreateObjectsCommand( - jsonSerialize(newlyIntersecting.map((p) => p.gameObject)) + jsonSerialize([...new Set(newlyIntersecting.map((p) => p.gameObject))]) ) ) ); @@ -102,7 +101,11 @@ export class Player extends CommandReceiver { TransportEvents.ServerToPlayer, jsonSerialize( new UpdateObjectsCommand( - jsonSerialize(this.objectsInViewArea.filter((o) => o.canMove)) + jsonSerialize([ + ...new Set( + this.objectsInViewArea.filter((p) => p.canMove).map((p) => p.gameObject) + ), + ]) ) ) ); diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 50483a1..4865042 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -15,6 +15,7 @@ import { prettyPrint, settings, SetViewAreaActionCommand, + StepCommand, TransportEvents, } from 'shared'; import io from 'socket.io-client'; @@ -23,7 +24,6 @@ import { MouseListener } from './commands/generators/mouse-listener'; import { TouchListener } from './commands/generators/touch-listener'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; import { RenderCommand } from './commands/types/render'; -import { StepCommand } from './commands/types/step'; import { Configuration } from './config/configuration'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { rgb } from './helper/rgb'; @@ -142,7 +142,7 @@ export class Game { } private gameLoop(time: DOMHighResTimeStamp) { - const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time); + const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); this.keyboardListener.generateCommands(); if (this.gameObjects.camera) { diff --git a/frontend/src/scripts/helper/delta-time-calculator.ts b/frontend/src/scripts/helper/delta-time-calculator.ts index fb7686d..f616553 100644 --- a/frontend/src/scripts/helper/delta-time-calculator.ts +++ b/frontend/src/scripts/helper/delta-time-calculator.ts @@ -5,7 +5,9 @@ export class DeltaTimeCalculator { document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); } - public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp { + public getNextDeltaTimeInMilliseconds( + currentTime: DOMHighResTimeStamp + ): DOMHighResTimeStamp { if (this.previousTime === null) { this.previousTime = currentTime; } diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index 99459f7..20680b2 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -7,9 +7,9 @@ import { DeleteObjectsCommand, GameObject, Id, + StepCommand, UpdateObjectsCommand, } from 'shared'; -import { StepCommand } from '../commands/types/step'; import { deserialize, deserializeJsonArray } from '../transport/deserialize'; import { Camera } from './camera'; import { CharacterView } from './character-view'; diff --git a/shared/firebase.json b/shared/firebase.json deleted file mode 100644 index 2c33c29..0000000 --- a/shared/firebase.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hosting": { - "public": "dist", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } -} diff --git a/frontend/src/scripts/commands/types/step.ts b/shared/src/commands/types/step.ts similarity index 83% rename from frontend/src/scripts/commands/types/step.ts rename to shared/src/commands/types/step.ts index f10f9fb..ba62a0e 100644 --- a/frontend/src/scripts/commands/types/step.ts +++ b/shared/src/commands/types/step.ts @@ -1,4 +1,4 @@ -import { Command } from 'shared'; +import { Command } from '../command'; export class StepCommand extends Command { public static readonly type = 'StepCommand'; diff --git a/shared/src/main.ts b/shared/src/main.ts index b1ce9ae..4480f84 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -3,6 +3,7 @@ export * from './commands/types/create-objects'; export * from './commands/types/create-player'; export * from './commands/types/delete-objects'; export * from './commands/types/update-objects'; +export * from './commands/types/step'; export * from './commands/types/ternary-action'; export * from './commands/types/move-action'; export * from './commands/types/set-view-area-action'; diff --git a/shared/src/settings.ts b/shared/src/settings.ts index e3c10a0..8628145 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -5,9 +5,9 @@ export const settings = { hitDetectionCirclePointCount: 32, hitDetectionMaxOverlap: 0.01, physicsMaxStep: 5, - gravitationalForce: vec2.fromValues(0, -0.015), + gravitationalForce: vec2.fromValues(0, -0.005), maxVelocityX: 1.5, - maxVelocityY: 8, - velocityAttenuation: 0.99, + maxVelocityY: 20, + velocityAttenuation: 0.98, inViewAreaSize: 1920 * 1080 * 3, };