diff --git a/backend/src/helper/delta-time-calculator.ts b/backend/src/helper/delta-time-calculator.ts index ebf75f5..d52526c 100644 --- a/backend/src/helper/delta-time-calculator.ts +++ b/backend/src/helper/delta-time-calculator.ts @@ -9,4 +9,12 @@ export class DeltaTimeCalculator { return seconds * 1000 + nanoSeconds / 1000 / 1000; } + + public getDeltaTimeInMilliseconds(): number { + const deltaTime = process.hrtime(this.previousTime); + + const [seconds, nanoSeconds] = deltaTime; + + return seconds * 1000 + nanoSeconds / 1000 / 1000; + } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 3b9f43c..0d1c62c 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -8,6 +8,7 @@ import { TransportEvents, deserialize, StepCommand, + settings, } from 'shared'; import './index.html'; import { Player } from './players/player'; @@ -30,8 +31,9 @@ createDungeon(objects); objects.initialize(); -const app = express(); +let players: Array = []; +const app = express(); const deltaTimeCalculator = new DeltaTimeCalculator(); app.use( @@ -60,6 +62,7 @@ app.get('/', function (req, res) { io.on('connection', (socket: SocketIO.Socket) => { socket.on(TransportEvents.PlayerJoining, () => { const player = new Player(objects, socket); + players.push(player); socket.on(TransportEvents.PlayerToServer, (json: string) => { const command = deserialize(json); player.sendCommand(command); @@ -67,14 +70,13 @@ io.on('connection', (socket: SocketIO.Socket) => { socket.on('disconnect', () => { player.destroy(); + players = players.filter((p) => p !== player); }); }); socket.on('join', (room_name: string) => { socket.join(room_name); }); - - //socket.on('disconnect', () => {}); }); server.listen(port, () => { @@ -86,8 +88,15 @@ const handlePhysics = () => { const step = new StepCommand(delta); objects.sendCommand(step); + players.forEach((p) => p.sendCommand(step)); - setTimeout(handlePhysics, 5); + const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds(); + const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; + if (sleepTime >= settings.minPhysicsSleepTime) { + setTimeout(handlePhysics, sleepTime); + } else { + setImmediate(handlePhysics); + } }; handlePhysics(); diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index 1be5b9e..b6a1742 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -7,10 +7,9 @@ import { CommandExecutors, MoveActionCommand, serializesTo, + clamp, } 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'; @@ -21,6 +20,8 @@ export class CharacterPhysical extends CharacterBase implements Physical { public readonly isInverted = false; public readonly canMove = true; + private jumpEnergyLeft = settings.defaultJumpEnergy; + public head: CirclePhysical; public leftFoot: CirclePhysical; public rightFoot: CirclePhysical; @@ -37,27 +38,25 @@ export class CharacterPhysical extends CharacterBase implements Physical { private static readonly rightFootOffset = vec2.fromValues(20, -10); constructor(private readonly container: PhysicalContainer) { - super( - id(), - 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, - ), + super(id(), undefined, undefined, undefined); + this.head = new CirclePhysical( + vec2.clone(CharacterPhysical.headOffset), + 50, + this, + container, + ); + this.leftFoot = new CirclePhysical( + vec2.clone(CharacterPhysical.leftFootOffset), + 20, + this, + container, + ); + this.rightFoot = new CirclePhysical( + vec2.clone(CharacterPhysical.rightFootOffset), + 20, + this, + 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); @@ -77,9 +76,14 @@ export class CharacterPhysical extends CharacterBase implements Physical { return this; } - // todo - public distance(_: vec2): number { - return 0; + public distance(target: vec2): number { + return ( + Math.min( + this.head.distance(target), + this.leftFoot.distance(target), + this.rightFoot.distance(target), + ) - 20 + ); } private sumAndResetMovementActions(): vec2 { @@ -102,15 +106,32 @@ export class CharacterPhysical extends CharacterBase implements Physical { public step(c: StepCommand) { const deltaTime = c.deltaTimeInMiliseconds; + this.head.applyForce(settings.gravitationalForce, deltaTime); + this.leftFoot.applyForce(settings.gravitationalForce, deltaTime); + this.rightFoot.applyForce(settings.gravitationalForce, deltaTime); + const movementForce = this.sumAndResetMovementActions(); - if (!this.leftFoot.isAirborne || !this.rightFoot.isAirborne) { - movementForce.y = 0; + + const isAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne; + if (isAirborne) { + this.jumpEnergyLeft -= deltaTime; + } else { + this.jumpEnergyLeft = settings.defaultJumpEnergy; } + const xMax = deltaTime * settings.maxAccelerationX; + const yMax = this.jumpEnergyLeft > 0 ? deltaTime * settings.maxAccelerationY : 0; + + vec2.set( + movementForce, + clamp(movementForce.x, -xMax, xMax), + clamp(movementForce.y, -yMax, yMax), + ); this.head.applyForce(movementForce, deltaTime); - this.head.applyForce(settings.gravitationalForce, deltaTime); + this.leftFoot.applyForce(movementForce, deltaTime); + this.rightFoot.applyForce(movementForce, deltaTime); - const bodyCenter = vec2.sub( + const bodyCenter = vec2.subtract( vec2.create(), this.head.center, CharacterPhysical.headOffset, @@ -127,38 +148,35 @@ export class CharacterPhysical extends CharacterBase implements Physical { CharacterPhysical.rightFootOffset, ); - const leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon); - const rightFootDelta = vec2.sub( + const leftFootDelta = vec2.subtract( + vec2.create(), + this.leftFoot.center, + leftFootPositon, + ); + + const rightFootDelta = vec2.subtract( vec2.create(), this.rightFoot.center, rightFootPositon, ); - vec2.scale(leftFootDelta, leftFootDelta, 0.0006); - vec2.scale(rightFootDelta, rightFootDelta, 0.0006); + if (vec2.squaredLength(leftFootDelta) > 0) { + vec2.scale(leftFootDelta, leftFootDelta, 0.001); + this.head.applyForce(leftFootDelta, deltaTime); + vec2.scale(leftFootDelta, leftFootDelta, -4); + this.leftFoot.applyForce(leftFootDelta, deltaTime); + } - this.head.applyForce(leftFootDelta, deltaTime); - this.head.applyForce(rightFootDelta, deltaTime); - - vec2.scale(leftFootDelta, leftFootDelta, -0.5); - vec2.scale(rightFootDelta, rightFootDelta, -0.5); - - this.leftFoot.applyForce(movementForce, deltaTime); - this.rightFoot.applyForce(movementForce, deltaTime); - this.leftFoot.applyForce(leftFootDelta, deltaTime); - this.rightFoot.applyForce(rightFootDelta, deltaTime); - this.leftFoot.applyForce(settings.gravitationalForce, deltaTime); - this.rightFoot.applyForce(settings.gravitationalForce, deltaTime); + if (vec2.squaredLength(rightFootDelta) > 0) { + vec2.scale(rightFootDelta, rightFootDelta, 0.001); + this.head.applyForce(rightFootDelta, deltaTime); + vec2.scale(rightFootDelta, rightFootDelta, -4); + this.rightFoot.applyForce(rightFootDelta, deltaTime); + } this.head.step(deltaTime); this.leftFoot.step(deltaTime); this.rightFoot.step(deltaTime); - - /*this.flashlight.center = vec2.add( - vec2.create(), - this.head.center, - vec2.fromValues(0, 0) - );*/ } public destroy() { diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index ba1b4be..27a733a 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,13 +1,5 @@ import { vec2 } from 'gl-matrix'; -import { - Circle, - clamp, - CommandExecutors, - GameObject, - serializesTo, - settings, - StepCommand, -} from 'shared'; +import { Circle, clamp, GameObject, serializesTo, settings } from 'shared'; import { Physical } from '../physics/physical'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; @@ -22,17 +14,12 @@ export class CirclePhysical implements Circle, Physical { readonly canMove = true; private _isAirborne = true; - private velocity = vec2.create(); public get isAirborne(): boolean { return this._isAirborne; } - protected commandExecutors: CommandExecutors = { - [StepCommand.type]: this.step.bind(this), - }; - private _boundingBox: BoundingBox; constructor( @@ -117,7 +104,7 @@ export class CirclePhysical implements Circle, Physical { vec2.set( this.velocity, clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), - clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY), + this.velocity.y, ); } @@ -140,7 +127,7 @@ export class CirclePhysical implements Circle, Physical { let wasHit = false; for (let i = 0; i < stepCount; i++) { - const { tangent, hitSurface } = moveCircle( + const { normal, tangent, hitSurface } = moveCircle( this, distance, this.container.findIntersecting(this.boundingBox), @@ -149,10 +136,9 @@ export class CirclePhysical implements Circle, Physical { vec2.scale(this.velocity, tangent!, vec2.dot(tangent!, this.velocity)); wasHit = true; } - - this._isAirborne = hitSurface; } + this._isAirborne = !wasHit; return wasHit; } diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 9d3c16b..3cc6ad0 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -1,4 +1,3 @@ -import { vec2 } from 'gl-matrix'; import { CommandExecutors, CommandReceiver, @@ -10,6 +9,7 @@ import { SetViewAreaActionCommand, TransportEvents, UpdateObjectsCommand, + StepCommand, } from 'shared'; import { CharacterPhysical } from '../objects/character-physical'; @@ -18,18 +18,15 @@ import { PhysicalContainer } from '../physics/containers/physical-container'; import { Physical } from '../physics/physical'; export class Player extends CommandReceiver { - public isActive = true; - private character: CharacterPhysical; private objectsPreviouslyInViewArea: Array = []; private objectsInViewArea: Array = []; protected commandExecutors: CommandExecutors = { + [StepCommand.type]: this.sendObjects.bind(this), [SetViewAreaActionCommand.type]: this.setViewArea.bind(this), [MoveActionCommand.type]: (c: MoveActionCommand) => { - vec2.normalize(c.delta, c.delta); - vec2.scale(c.delta, c.delta, 15); this.character.sendCommand(c); }, }; @@ -54,7 +51,7 @@ export class Player extends CommandReceiver { } public setViewArea(c: SetViewAreaActionCommand) { - const viewArea = new BoundingBox(null); + const viewArea = new BoundingBox(); viewArea.topLeft = c.viewArea.topLeft; viewArea.size = c.viewArea.size; @@ -103,15 +100,9 @@ export class Player extends CommandReceiver { ]), ), ); - - if (this.isActive) { - //setImmediate(this.sendObjects.bind(this)); - setTimeout(this.sendObjects.bind(this), 5); - } } public destroy() { - this.isActive = false; this.character.destroy(); this.objects.removeObject(this.character); } diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..9b6f578 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "target": "es2015", + "esModuleInterop": true, + "strict": true, + "experimentalDecorators": true, + "downlevelIteration": true, + "moduleResolution": "Node", + "module": "es6", + "composite": true, + "lib": ["dom", "es2017"] + } +} diff --git a/frontend/src/scripts/commands/generators/keyboard-listener.ts b/frontend/src/scripts/commands/generators/keyboard-listener.ts index a3008a3..0968aaa 100644 --- a/frontend/src/scripts/commands/generators/keyboard-listener.ts +++ b/frontend/src/scripts/commands/generators/keyboard-listener.ts @@ -4,7 +4,7 @@ import { CommandGenerator, MoveActionCommand } from 'shared'; export class KeyboardListener extends CommandGenerator { private keysDown: Set = new Set(); - constructor(target: Element, private moveScale: number) { + constructor(target: HTMLElement) { super(); target.addEventListener('keydown', (event: KeyboardEvent) => { @@ -31,7 +31,6 @@ export class KeyboardListener extends CommandGenerator { const movement = vec2.fromValues(right - left, up - down); if (vec2.squaredLength(movement) > 0) { vec2.normalize(movement, movement); - vec2.scale(movement, movement, this.moveScale); this.sendCommandToSubcribers(new MoveActionCommand(movement)); } } diff --git a/frontend/src/scripts/commands/generators/mouse-listener.ts b/frontend/src/scripts/commands/generators/mouse-listener.ts index 9b11688..ca0cdd7 100644 --- a/frontend/src/scripts/commands/generators/mouse-listener.ts +++ b/frontend/src/scripts/commands/generators/mouse-listener.ts @@ -7,7 +7,7 @@ import { } from 'shared'; export class MouseListener extends CommandGenerator { - constructor(target: Element) { + constructor(target: HTMLElement) { super(); target.addEventListener('mousemove', (event: MouseEvent) => { diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts index 86f5da2..6aa6db5 100644 --- a/frontend/src/scripts/commands/generators/touch-listener.ts +++ b/frontend/src/scripts/commands/generators/touch-listener.ts @@ -1,14 +1,17 @@ import { vec2 } from 'gl-matrix'; import { CommandGenerator, - MoveActionCommand, PrimaryActionCommand, SecondaryActionCommand, TernaryActionCommand, + MoveActionCommand, } from 'shared'; export class TouchListener extends CommandGenerator { private previousPosition = vec2.create(); + private currentPosition = vec2.create(); + + private previousDeltas: Array = []; constructor(target: HTMLElement) { super(); @@ -19,6 +22,7 @@ export class TouchListener extends CommandGenerator { const touchCount = event.touches.length; const position = this.positionFromEvent(event); this.previousPosition = position; + this.currentPosition = position; if (touchCount == 1) { this.sendCommandToSubcribers(new PrimaryActionCommand(position)); @@ -33,20 +37,29 @@ export class TouchListener extends CommandGenerator { event.preventDefault(); const position = this.positionFromEvent(event); - - this.sendCommandToSubcribers( - new MoveActionCommand( - vec2.subtract(vec2.create(), position, this.previousPosition), - ), + this.previousDeltas.push( + vec2.subtract(vec2.create(), position, this.previousPosition), ); - - this.previousPosition = position; + this.currentPosition = position; }); } + public generateCommands() { + const movement = vec2.subtract( + vec2.create(), + this.currentPosition, + this.previousPosition, + ); + this.previousPosition = this.currentPosition; + if (vec2.squaredLength(movement) > 0) { + vec2.normalize(movement, movement); + this.sendCommandToSubcribers(new MoveActionCommand(movement)); + } + } + private positionFromEvent(event: TouchEvent): vec2 { - const center = Array.prototype.reduce.call( - event.touches, + const touches = Array.prototype.slice.call(event.touches); + const center = touches.reduce( (center: vec2, touch: Touch) => vec2.add(center, center, vec2.fromValues(-touch.clientX, touch.clientY)), vec2.create(), diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 6bf7919..e26a759 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -13,7 +13,6 @@ import { import { broadcastCommands, deserialize, - prettyPrint, serialize, settings, SetViewAreaActionCommand, @@ -41,6 +40,7 @@ export class Game { private deltaTimeCalculator = new DeltaTimeCalculator(); private overlay: HTMLElement = document.querySelector('#overlay'); private keyboardListener: KeyboardListener; + private touchListener: TouchListener; private async setupCommunication(): Promise { await Configuration.initialize(); @@ -62,14 +62,11 @@ export class Game { this.socket.emit(TransportEvents.PlayerJoining, null); - this.keyboardListener = new KeyboardListener(document.body, 100); + this.keyboardListener = new KeyboardListener(document.body); + this.touchListener = new TouchListener(this.canvas); broadcastCommands( - [ - this.keyboardListener, - new MouseListener(this.canvas), - new TouchListener(this.canvas), - ], + [this.keyboardListener, new MouseListener(this.canvas), this.touchListener], [this.gameObjects, new CommandReceiverSocket(this.socket)], ); } @@ -82,15 +79,15 @@ export class Game { [ { ...InvertedTunnel.descriptor, - shaderCombinationSteps: [0, 2, 4, 8, 16], + shaderCombinationSteps: [0, 2, 6, 16], }, { ...BlobShape.descriptor, - shaderCombinationSteps: [0, 1, 2, 3, 4, 5, 8], + shaderCombinationSteps: [0, 1, 2, 8], }, { ...Circle.descriptor, - shaderCombinationSteps: [0, 2, 4, 8, 16], + shaderCombinationSteps: [0, 2, 16], }, { ...CircleLight.descriptor, @@ -146,6 +143,7 @@ export class Game { private gameLoop(time: DOMHighResTimeStamp) { const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); this.keyboardListener.generateCommands(); + this.touchListener.generateCommands(); if (this.gameObjects.camera) { // todo: Should only send aspect ratio @@ -171,7 +169,7 @@ export class Game { this.renderer.renderDrawables(); - this.overlay.innerText = prettyPrint(this.renderer.insights); + //this.overlay.innerText = prettyPrint(this.renderer.insights); requestAnimationFrame(this.gameLoop.bind(this)); } } diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts index 17dbdbf..fb5a3fa 100644 --- a/frontend/src/scripts/shapes/blob-shape.ts +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -98,15 +98,11 @@ export class BlobShape extends Drawable { } public minDistance(target: vec2): number { - // todo - return 0; - /*return ( - Math.min( - this.head.distance(target), - this.leftFoot.distance(target), - this.rightFoot.distance(target) - ) / 2 - );*/ + return Math.min( + this.head.distance(target), + this.leftFoot.distance(target), + this.rightFoot.distance(target), + ); } protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts index eb1a731..a0e6f84 100644 --- a/shared/src/objects/types/character-base.ts +++ b/shared/src/objects/types/character-base.ts @@ -7,9 +7,9 @@ import { GameObject } from '../game-object'; export class CharacterBase extends GameObject { constructor( id: Id, - public head: Circle, - public leftFoot: Circle, - public rightFoot: Circle, + public head?: Circle, + public leftFoot?: Circle, + public rightFoot?: Circle, ) { super(id); } diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 8628145..4e11342 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -5,9 +5,13 @@ export const settings = { hitDetectionCirclePointCount: 32, hitDetectionMaxOverlap: 0.01, physicsMaxStep: 5, - gravitationalForce: vec2.fromValues(0, -0.005), - maxVelocityX: 1.5, - maxVelocityY: 20, - velocityAttenuation: 0.98, + gravitationalForce: vec2.fromValues(0, -0.0055), + maxVelocityX: 1, + maxAccelerationX: 0.005, + maxAccelerationY: 0.01, + targetPhysicsDeltaTimeInMilliseconds: 10, + minPhysicsSleepTime: 4, + velocityAttenuation: 0.99, inViewAreaSize: 1920 * 1080 * 3, + defaultJumpEnergy: 600, };