Improve physics
This commit is contained in:
parent
8b87b68dae
commit
ec0b700313
14 changed files with 181 additions and 71 deletions
12
backend/src/helper/delta-time-calculator.ts
Normal file
12
backend/src/helper/delta-time-calculator.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<MoveActionCommand> = [];
|
||||
|
||||
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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<PhysicalGameObject>
|
||||
possibleIntersectors: Array<Physical>
|
||||
): {
|
||||
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),
|
||||
};
|
||||
};
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<Physical> = [];
|
||||
private objectsInViewArea: Array<Physical> = [];
|
||||
|
|
@ -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)
|
||||
),
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"hosting": {
|
||||
"public": "dist",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Command } from 'shared';
|
||||
import { Command } from '../command';
|
||||
|
||||
export class StepCommand extends Command {
|
||||
public static readonly type = 'StepCommand';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue