Improve physics

This commit is contained in:
schmelczerandras 2020-10-06 09:26:56 +02:00
parent 8b87b68dae
commit ec0b700313
14 changed files with 181 additions and 71 deletions

View 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;
}
}

View file

@ -2,12 +2,19 @@ import ioserver, { Socket } from 'socket.io';
import express from 'express'; import express from 'express';
import { Server } from 'http'; import { Server } from 'http';
import cors from 'cors'; import cors from 'cors';
import { applyArrayPlugins, Random, TransportEvents, deserializeCommand } from 'shared'; import {
applyArrayPlugins,
Random,
TransportEvents,
deserializeCommand,
StepCommand,
} from 'shared';
import './index.html'; import './index.html';
import { Player } from './players/player'; import { Player } from './players/player';
import { PhysicalContainer } from './physics/containers/physical-container'; import { PhysicalContainer } from './physics/containers/physical-container';
import { createDungeon } from './map/create-dungeon'; import { createDungeon } from './map/create-dungeon';
import { glMatrix } from 'gl-matrix'; import { glMatrix } from 'gl-matrix';
import { DeltaTimeCalculator } from './helper/delta-time-calculator';
glMatrix.setMatrixArrayType(Array); glMatrix.setMatrixArrayType(Array);
@ -26,6 +33,8 @@ objects.initialize();
const app = express(); const app = express();
const deltaTimeCalculator = new DeltaTimeCalculator();
app.use( app.use(
cors({ cors({
origin: (origin, callback) => { origin: (origin, callback) => {
@ -50,7 +59,6 @@ app.get('/', function (req, res) {
io.on('connection', (socket: SocketIO.Socket) => { io.on('connection', (socket: SocketIO.Socket) => {
socket.on(TransportEvents.PlayerJoining, () => { socket.on(TransportEvents.PlayerJoining, () => {
const player = new Player(objects, socket); const player = new Player(objects, socket);
socket.on(TransportEvents.PlayerToServer, (text: string) => { socket.on(TransportEvents.PlayerToServer, (text: string) => {
const command = deserializeCommand(JSON.parse(text)); const command = deserializeCommand(JSON.parse(text));
player.sendCommand(command); player.sendCommand(command);
@ -71,3 +79,14 @@ io.on('connection', (socket: SocketIO.Socket) => {
server.listen(port, () => { server.listen(port, () => {
console.log(`server started at http://localhost:${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();

View file

@ -1,27 +1,64 @@
import { vec2 } from 'gl-matrix'; 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 { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box';
import { CirclePhysical } from './circle-physical'; import { CirclePhysical } from './circle-physical';
import { Physical } from '../physics/physical'; import { Physical } from '../physics/physical';
import { PhysicalContainer } from '../physics/containers/physical-container';
export class CharacterPhysical extends CharacterBase implements Physical { export class CharacterPhysical extends CharacterBase implements Physical {
public readonly canCollide = true; public readonly canCollide = true;
public readonly isInverted = false; public readonly isInverted = false;
public readonly canMove = true; 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 headOffset = vec2.fromValues(0, 40);
private static readonly leftFootOffset = vec2.fromValues(-20, -10); private static readonly leftFootOffset = vec2.fromValues(-20, -10);
private static readonly rightFootOffset = vec2.fromValues(20, -10); private static readonly rightFootOffset = vec2.fromValues(20, -10);
constructor() { constructor(container: PhysicalContainer) {
super( super(
id(), id(),
new CirclePhysical(vec2.clone(CharacterPhysical.headOffset), 50, null), new CirclePhysical(vec2.clone(CharacterPhysical.headOffset), 50, null, container),
new CirclePhysical(vec2.clone(CharacterPhysical.leftFootOffset), 20, null), new CirclePhysical(
new CirclePhysical(vec2.clone(CharacterPhysical.rightFootOffset), 20, null) 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; private _boundingBox?: ImmutableBoundingBox;
@ -42,16 +79,51 @@ export class CharacterPhysical extends CharacterBase implements Physical {
public distance(_: vec2): number { public distance(_: vec2): number {
return 0; return 0;
} }
/*
public step() { private sumAndResetMovementActions(): vec2 {
const movementForce = vec2.fromValues(right - left, up - down); 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(movementForce, deltaTime);
this.head.applyForce(settings.gravitationalForce, 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 leftFootPositon = vec2.add(
const rightFootPositon = vec2.add(vec2.create(), bodyCenter, this.rightFootOffset); 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 leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon);
const rightFootDelta = vec2.sub( const rightFootDelta = vec2.sub(
@ -80,13 +152,13 @@ export class CharacterPhysical extends CharacterBase implements Physical {
this.leftFoot.step(deltaTime); this.leftFoot.step(deltaTime);
this.rightFoot.step(deltaTime); this.rightFoot.step(deltaTime);
this.flashlight.center = vec2.add( /*this.flashlight.center = vec2.add(
vec2.create(), vec2.create(),
this.head.center, this.head.center,
vec2.fromValues(0, 0) vec2.fromValues(0, 0)
); );*/
} }
*/
public toJSON(): any { public toJSON(): any {
const { type, id, head, leftFoot, rightFoot } = this; const { type, id, head, leftFoot, rightFoot } = this;
return [type, id, head, leftFoot, rightFoot]; return [type, id, head, leftFoot, rightFoot];

View file

@ -1,9 +1,18 @@
import { vec2 } from 'gl-matrix'; 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 { Physical } from '../physics/physical';
import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; 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 { export class CirclePhysical implements Circle, Physical {
readonly isInverted = false; readonly isInverted = false;
@ -18,9 +27,18 @@ export class CirclePhysical implements Circle, Physical {
return this._isAirborne; return this._isAirborne;
} }
protected commandExecutors: CommandExecutors = {
[StepCommand.type]: this.step.bind(this),
};
private _boundingBox: BoundingBox; 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._boundingBox = new BoundingBox(null);
this.recalculateBoundingBox(); this.recalculateBoundingBox();
} }
@ -59,11 +77,11 @@ export class CirclePhysical implements Circle, Physical {
return vec2.distance(target.center, this.center) - this.radius - target.radius; 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; return other.distance(this.center) < this.radius;
} }
public isInside(other: CirclePhysical): boolean { public isInside(other: Physical): boolean {
return other.distance(this.center) < -this.radius; return other.distance(this.center) < -this.radius;
} }
@ -105,12 +123,13 @@ export class CirclePhysical implements Circle, Physical {
this.velocity = vec2.create(); this.velocity = vec2.create();
} }
/*public step(timeInMilliseconds: number): boolean { public step(timeInMilliseconds: number): boolean {
vec2.scale( vec2.scale(
this.velocity, this.velocity,
this.velocity, this.velocity,
Math.pow(settings.velocityAttenuation, timeInMilliseconds) Math.pow(settings.velocityAttenuation, timeInMilliseconds)
); );
const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds); const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds);
const distanceLength = vec2.length(distance); const distanceLength = vec2.length(distance);
const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep); const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep);
@ -119,22 +138,21 @@ export class CirclePhysical implements Circle, Physical {
let wasHit = false; let wasHit = false;
for (let i = 0; i < stepCount; i++) { for (let i = 0; i < stepCount; i++) {
const { normal, tangent, hitSurface } = this.physics.tryMovingDynamicCircle( const { normal, tangent, hitSurface } = moveCircle(
this, this,
distance distance,
this.container.findIntersecting(this.boundingBox)
); );
if (hitSurface) { if (hitSurface) {
vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity)); vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity));
wasHit = true; wasHit = true;
} }
this._isAirborne = !( this._isAirborne = hitSurface;
hitSurface && vec2.dot(normal, settings.gravitationalForce) < 0
);
} }
return wasHit; return wasHit;
}*/ }
public toJSON(): any { public toJSON(): any {
const { center, radius } = this; const { center, radius } = this;

View file

@ -71,7 +71,7 @@ export class PhysicalContainer {
private createGroupForCommand(commandType: string) { private createGroupForCommand(commandType: string) {
const objectsReactingToCommand = []; const objectsReactingToCommand = [];
console.log(commandType);
this.objects.forEach((o, _) => { this.objects.forEach((o, _) => {
if (o.reactsToCommand(commandType)) { if (o.reactsToCommand(commandType)) {
objectsReactingToCommand.push(o); objectsReactingToCommand.push(o);

View file

@ -1,12 +1,12 @@
/*import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { rotate90Deg } from 'shared'; import { rotate90Deg, settings } from 'shared';
import { CirclePhysics } from './bounds/circle-physics'; import { CirclePhysical } from '../objects/circle-physical';
import { PhysicalGameObject } from './physical-game-object'; import { Physical } from './physical';
export const moveCircle = ( export const moveCircle = (
circle: CirclePhysics, circle: CirclePhysical,
delta: vec2, delta: vec2,
possibleIntersectors: Array<PhysicalGameObject> possibleIntersectors: Array<Physical>
): { ): {
realDelta: vec2; realDelta: vec2;
hitSurface: boolean; hitSurface: boolean;
@ -16,7 +16,7 @@ export const moveCircle = (
circle.center = vec2.add(circle.center, circle.center, delta); circle.center = vec2.add(circle.center, circle.center, delta);
const intersecting = possibleIntersectors.filter( 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) { if (intersecting.length === 0) {
@ -34,7 +34,7 @@ export const moveCircle = (
closest: intersecting closest: intersecting
.map((i) => ({ .map((i) => ({
inverted: i.isInverted, inverted: i.isInverted,
distance: i.owner.distance(point), distance: i.distance(point),
})) }))
.sort((a, b) => a.distance - b.distance)[0], .sort((a, b) => a.distance - b.distance)[0],
})) }))
@ -79,4 +79,3 @@ export const moveCircle = (
tangent: rotate90Deg(approxNormal), tangent: rotate90Deg(approxNormal),
}; };
}; };
*/

View file

@ -21,7 +21,7 @@ import { jsonSerialize } from '../serialize';
export class Player extends CommandReceiver { export class Player extends CommandReceiver {
public isActive = true; public isActive = true;
private character: CharacterPhysical = new CharacterPhysical(); private character: CharacterPhysical;
private objectsPreviouslyInViewArea: Array<Physical> = []; private objectsPreviouslyInViewArea: Array<Physical> = [];
private objectsInViewArea: Array<Physical> = []; private objectsInViewArea: Array<Physical> = [];
@ -30,12 +30,8 @@ export class Player extends CommandReceiver {
[SetViewAreaActionCommand.type]: this.setViewArea.bind(this), [SetViewAreaActionCommand.type]: this.setViewArea.bind(this),
[MoveActionCommand.type]: (c: MoveActionCommand) => { [MoveActionCommand.type]: (c: MoveActionCommand) => {
vec2.normalize(c.delta, c.delta); vec2.normalize(c.delta, c.delta);
vec2.scale(c.delta, c.delta, 40); vec2.scale(c.delta, c.delta, 15);
this.character.head.center = vec2.add( this.character.sendCommand(c);
this.character.head.center,
this.character.head.center,
c.delta
);
}, },
}; };
@ -46,6 +42,7 @@ export class Player extends CommandReceiver {
private readonly socket: SocketIO.Socket private readonly socket: SocketIO.Socket
) { ) {
super(); super();
this.character = new CharacterPhysical(objects);
this.objectsPreviouslyInViewArea.push(this.character); this.objectsPreviouslyInViewArea.push(this.character);
this.objectsInViewArea.push(this.character); this.objectsInViewArea.push(this.character);
@ -82,7 +79,9 @@ export class Player extends CommandReceiver {
this.socket.emit( this.socket.emit(
TransportEvents.ServerToPlayer, TransportEvents.ServerToPlayer,
jsonSerialize( 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, TransportEvents.ServerToPlayer,
jsonSerialize( jsonSerialize(
new CreateObjectsCommand( 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, TransportEvents.ServerToPlayer,
jsonSerialize( jsonSerialize(
new UpdateObjectsCommand( new UpdateObjectsCommand(
jsonSerialize(this.objectsInViewArea.filter((o) => o.canMove)) jsonSerialize([
...new Set(
this.objectsInViewArea.filter((p) => p.canMove).map((p) => p.gameObject)
),
])
) )
) )
); );

View file

@ -15,6 +15,7 @@ import {
prettyPrint, prettyPrint,
settings, settings,
SetViewAreaActionCommand, SetViewAreaActionCommand,
StepCommand,
TransportEvents, TransportEvents,
} from 'shared'; } from 'shared';
import io from 'socket.io-client'; 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 { TouchListener } from './commands/generators/touch-listener';
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
import { RenderCommand } from './commands/types/render'; import { RenderCommand } from './commands/types/render';
import { StepCommand } from './commands/types/step';
import { Configuration } from './config/configuration'; import { Configuration } from './config/configuration';
import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { DeltaTimeCalculator } from './helper/delta-time-calculator';
import { rgb } from './helper/rgb'; import { rgb } from './helper/rgb';
@ -142,7 +142,7 @@ export class Game {
} }
private gameLoop(time: DOMHighResTimeStamp) { private gameLoop(time: DOMHighResTimeStamp) {
const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time); const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time);
this.keyboardListener.generateCommands(); this.keyboardListener.generateCommands();
if (this.gameObjects.camera) { if (this.gameObjects.camera) {

View file

@ -5,7 +5,9 @@ export class DeltaTimeCalculator {
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
} }
public getNextDeltaTime(currentTime: DOMHighResTimeStamp): DOMHighResTimeStamp { public getNextDeltaTimeInMilliseconds(
currentTime: DOMHighResTimeStamp
): DOMHighResTimeStamp {
if (this.previousTime === null) { if (this.previousTime === null) {
this.previousTime = currentTime; this.previousTime = currentTime;
} }

View file

@ -7,9 +7,9 @@ import {
DeleteObjectsCommand, DeleteObjectsCommand,
GameObject, GameObject,
Id, Id,
StepCommand,
UpdateObjectsCommand, UpdateObjectsCommand,
} from 'shared'; } from 'shared';
import { StepCommand } from '../commands/types/step';
import { deserialize, deserializeJsonArray } from '../transport/deserialize'; import { deserialize, deserializeJsonArray } from '../transport/deserialize';
import { Camera } from './camera'; import { Camera } from './camera';
import { CharacterView } from './character-view'; import { CharacterView } from './character-view';

View file

@ -1,16 +0,0 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

View file

@ -1,4 +1,4 @@
import { Command } from 'shared'; import { Command } from '../command';
export class StepCommand extends Command { export class StepCommand extends Command {
public static readonly type = 'StepCommand'; public static readonly type = 'StepCommand';

View file

@ -3,6 +3,7 @@ export * from './commands/types/create-objects';
export * from './commands/types/create-player'; export * from './commands/types/create-player';
export * from './commands/types/delete-objects'; export * from './commands/types/delete-objects';
export * from './commands/types/update-objects'; export * from './commands/types/update-objects';
export * from './commands/types/step';
export * from './commands/types/ternary-action'; export * from './commands/types/ternary-action';
export * from './commands/types/move-action'; export * from './commands/types/move-action';
export * from './commands/types/set-view-area-action'; export * from './commands/types/set-view-area-action';

View file

@ -5,9 +5,9 @@ export const settings = {
hitDetectionCirclePointCount: 32, hitDetectionCirclePointCount: 32,
hitDetectionMaxOverlap: 0.01, hitDetectionMaxOverlap: 0.01,
physicsMaxStep: 5, physicsMaxStep: 5,
gravitationalForce: vec2.fromValues(0, -0.015), gravitationalForce: vec2.fromValues(0, -0.005),
maxVelocityX: 1.5, maxVelocityX: 1.5,
maxVelocityY: 8, maxVelocityY: 20,
velocityAttenuation: 0.99, velocityAttenuation: 0.98,
inViewAreaSize: 1920 * 1080 * 3, inViewAreaSize: 1920 * 1080 * 3,
}; };