diff options
Diffstat (limited to 'engine')
-rw-r--r-- | engine/components/BoundingBox.ts | 4 | ||||
-rw-r--r-- | engine/components/Control.ts | 2 | ||||
-rw-r--r-- | engine/components/NetworkUpdateable.ts | 8 | ||||
-rw-r--r-- | engine/config/constants.ts | 10 | ||||
-rw-r--r-- | engine/entities/Entity.ts | 32 | ||||
-rw-r--r-- | engine/entities/Floor.ts | 24 | ||||
-rw-r--r-- | engine/entities/Player.ts | 44 | ||||
-rw-r--r-- | engine/network/index.ts | 12 | ||||
-rw-r--r-- | engine/systems/Input.ts | 144 | ||||
-rw-r--r-- | engine/systems/NetworkUpdate.ts | 40 | ||||
-rw-r--r-- | engine/systems/Physics.ts | 2 |
11 files changed, 229 insertions, 93 deletions
diff --git a/engine/components/BoundingBox.ts b/engine/components/BoundingBox.ts index dbe083e..921feb9 100644 --- a/engine/components/BoundingBox.ts +++ b/engine/components/BoundingBox.ts @@ -15,7 +15,6 @@ export class BoundingBox extends Component { this.rotation = rotation ?? 0; } - // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem public isCollidingWith(box: BoundingBox): boolean { if (this.rotation == 0 && box.rotation == 0) { const thisTopLeft = this.getTopLeft(); @@ -36,6 +35,7 @@ export class BoundingBox extends Component { return true; } + // https://en.wikipedia.org/wiki/Hyperplane_separation_theorem const boxes = [this.getVertices(), box.getVertices()]; for (const poly of boxes) { for (let i = 0; i < poly.length; i++) { @@ -89,6 +89,8 @@ export class BoundingBox extends Component { let rads = this.getRotationInPiOfUnitCircle(); const { width, height } = this.dimension; + if (rads == 0) return this.dimension; + if (rads <= Math.PI / 2) { return { width: Math.abs(height * Math.sin(rads) + width * Math.cos(rads)), diff --git a/engine/components/Control.ts b/engine/components/Control.ts index beec82c..d3987d7 100644 --- a/engine/components/Control.ts +++ b/engine/components/Control.ts @@ -3,6 +3,7 @@ import { Component, ComponentNames, Velocity } from '.'; export class Control extends Component { public controlVelocityComponent: Velocity; public controllableBy: string; + public isControllable: boolean; // computed each update in the input system constructor( controllableBy: string, @@ -12,5 +13,6 @@ export class Control extends Component { this.controllableBy = controllableBy; this.controlVelocityComponent = controlVelocityComponent; + this.isControllable = false; } } diff --git a/engine/components/NetworkUpdateable.ts b/engine/components/NetworkUpdateable.ts index 7fb6d18..014270c 100644 --- a/engine/components/NetworkUpdateable.ts +++ b/engine/components/NetworkUpdateable.ts @@ -1,13 +1,7 @@ import { Component, ComponentNames } from '.'; export class NetworkUpdateable extends Component { - public isPublish: boolean; - public isSubscribe: boolean; - - constructor(isPublish: boolean, isSubscribe: boolean) { + constructor() { super(ComponentNames.NetworkUpdateable); - - this.isPublish = isPublish; - this.isSubscribe = isSubscribe; } } diff --git a/engine/config/constants.ts b/engine/config/constants.ts index 45b0301..dc98ad0 100644 --- a/engine/config/constants.ts +++ b/engine/config/constants.ts @@ -3,13 +3,13 @@ import { Action } from '../interfaces'; export namespace KeyConstants { export const KeyActions: Record<string, Action> = { a: Action.MOVE_LEFT, - ArrowLeft: Action.MOVE_LEFT, + arrowleft: Action.MOVE_LEFT, d: Action.MOVE_RIGHT, - ArrowRight: Action.MOVE_RIGHT, + arrowright: Action.MOVE_RIGHT, w: Action.JUMP, - ArrowUp: Action.JUMP, + arrowup: Action.JUMP, ' ': Action.JUMP }; @@ -18,7 +18,7 @@ export namespace KeyConstants { export const ActionKeys: Map<Action, string[]> = Object.keys( KeyActions ).reduce((acc: Map<Action, string[]>, key) => { - const action = KeyActions[key]; + const action = KeyActions[key.toLowerCase()]; if (acc.has(action)) { acc.get(action)!.push(key); @@ -33,7 +33,7 @@ export namespace KeyConstants { export namespace PhysicsConstants { export const MAX_JUMP_TIME_MS = 150; export const GRAVITY = 0.0075; - export const PLAYER_MOVE_VEL = 1; + export const PLAYER_MOVE_VEL = 0.8; export const PLAYER_JUMP_ACC = -0.008; export const PLAYER_JUMP_INITIAL_VEL = -1; } diff --git a/engine/entities/Entity.ts b/engine/entities/Entity.ts index b016fc0..63fb370 100644 --- a/engine/entities/Entity.ts +++ b/engine/entities/Entity.ts @@ -1,12 +1,15 @@ -import { EntityNames, Player } from '.'; -import type { Component } from '../components'; +import { EntityNames, Floor, Player } from '.'; +import { type Component } from '../components'; + +const randomId = () => + (performance.now() + Math.random() * 10_000_000).toString(); export abstract class Entity { public id: string; public components: Map<string, Component>; public name: string; - constructor(name: string, id: string = crypto.randomUUID()) { + constructor(name: string, id: string = randomId()) { this.name = name; this.id = id; this.components = new Map(); @@ -31,14 +34,29 @@ export abstract class Entity { return this.components.has(name); } - static from(entityName: string, args: any): Entity { + public static from(entityName: string, id: string, args: any): Entity { + let entity: Entity; + switch (entityName) { case EntityNames.Player: - const player = new Player(args.playerId); - player.id = args.id; - return player; + const player = new Player(); + player.setFrom(args); + entity = player; + break; + case EntityNames.Floor: + const floor = new Floor(args.floorWidth); + floor.setFrom(args); + entity = floor; + break; default: throw new Error('.from() Entity type not implemented: ' + entityName); } + + entity.id = id; + return entity; } + + public abstract setFrom(args: Record<string, any>): void; + + public abstract serialize(): Record<string, any>; } diff --git a/engine/entities/Floor.ts b/engine/entities/Floor.ts index 6f9b13b..b4f48e5 100644 --- a/engine/entities/Floor.ts +++ b/engine/entities/Floor.ts @@ -1,5 +1,5 @@ import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from '../config'; -import { BoundingBox, Sprite } from '../components'; +import { BoundingBox, ComponentNames, Sprite } from '../components'; import { TopCollidable } from '../components/TopCollidable'; import { Entity, EntityNames } from '../entities'; @@ -8,9 +8,13 @@ export class Floor extends Entity { Sprites.FLOOR ) as SpriteSpec; + private width: number; + constructor(width: number) { super(EntityNames.Floor); + this.width = width; + this.addComponent( new Sprite( IMAGES.get((Floor.spriteSpec?.states?.get(width) as SpriteSpec).sheet), @@ -23,4 +27,22 @@ export class Floor extends Entity { this.addComponent(new TopCollidable()); } + + public serialize() { + return { + floorWidth: this.width, + boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox) + }; + } + + public setFrom(args: any) { + const { boundingBox } = args; + this.addComponent( + new BoundingBox( + boundingBox.center, + boundingBox.dimension, + boundingBox.rotation + ) + ); + } } diff --git a/engine/entities/Player.ts b/engine/entities/Player.ts index 947fbd6..4d91c6f 100644 --- a/engine/entities/Player.ts +++ b/engine/entities/Player.ts @@ -10,9 +10,10 @@ import { WallBounded, Forces, Collide, - Control, Mass, - Moment + Moment, + ComponentNames, + Control } from '../components'; import { Direction } from '../interfaces'; @@ -24,14 +25,14 @@ export class Player extends Entity { Sprites.COFFEE ) as SpriteSpec; - constructor(playerId: string) { + constructor() { super(EntityNames.Player); this.addComponent( new BoundingBox( { - x: 300, - y: 100 + x: 0, + y: 0 }, { width: Player.spriteSpec.width, height: Player.spriteSpec.height }, 0 @@ -48,7 +49,6 @@ export class Player extends Entity { this.addComponent(new Gravity()); this.addComponent(new Jump()); - this.addComponent(new Control(playerId)); this.addComponent(new Collide()); this.addComponent(new WallBounded()); @@ -69,6 +69,36 @@ export class Player extends Entity { ); this.addComponent(new FacingDirection(leftSprite, rightSprite)); - this.addComponent(leftSprite); // face Left by default + this.addComponent(leftSprite); // face left by default + } + + public serialize(): Record<string, any> { + return { + control: this.getComponent<Control>(ComponentNames.Control), + boundingBox: this.getComponent<BoundingBox>(ComponentNames.BoundingBox), + velocity: this.getComponent<Velocity>(ComponentNames.Velocity), + forces: this.getComponent<Forces>(ComponentNames.Forces) + }; + } + + public setFrom(args: Record<string, any>) { + const { control, velocity, forces, boundingBox } = args; + + let center = boundingBox.center; + + const myCenter = this.getComponent<BoundingBox>( + ComponentNames.BoundingBox + ).center; + const distance = Math.sqrt( + Math.pow(center.y - myCenter.y, 2) + Math.pow(center.x - myCenter.x, 2) + ); + if (distance < 30) center = myCenter; + + [ + Object.assign(new Control(control.controllableBy), control), + new Velocity(velocity.velocity), + new Forces(forces.forces), + new BoundingBox(center, boundingBox.dimension, boundingBox.rotation) + ].forEach((component) => this.addComponent(component)); } } diff --git a/engine/network/index.ts b/engine/network/index.ts index 1bf95fb..5dc7ece 100644 --- a/engine/network/index.ts +++ b/engine/network/index.ts @@ -1,12 +1,20 @@ export enum MessageType { NEW_ENTITIES = 'NEW_ENTITIES', REMOVE_ENTITIES = 'REMOVE_ENTITIES', - UPDATE_ENTITY = 'UPDATE_ENTITY' + UPDATE_ENTITIES = 'UPDATE_ENTITIES', + NEW_INPUT = 'NEW_INPUT', + REMOVE_INPUT = 'REMOVE_INPUT' } export type EntityAddBody = { entityName: string; - args: any; + id: string; + args: Record<string, any>; +}; + +export type EntityUpdateBody = { + id: string; + args: Record<string, any>; }; export type Message = { diff --git a/engine/systems/Input.ts b/engine/systems/Input.ts index 8a68905..9afd1ab 100644 --- a/engine/systems/Input.ts +++ b/engine/systems/Input.ts @@ -10,26 +10,111 @@ import { Game } from '../Game'; import { KeyConstants, PhysicsConstants } from '../config'; import { Action } from '../interfaces'; import { System, SystemNames } from '.'; +import { MessagePublisher, MessageType } from '../network'; +import { Entity } from '../entities'; export class Input extends System { public clientId: string; + private keys: Set<string>; private actionTimeStamps: Map<Action, number>; + private messagePublisher?: MessagePublisher; - constructor(clientId: string) { + constructor(clientId: string, messagePublisher?: MessagePublisher) { super(SystemNames.Input); this.clientId = clientId; - this.keys = new Set<string>(); - this.actionTimeStamps = new Map<Action, number>(); + this.keys = new Set(); + this.actionTimeStamps = new Map(); + + this.messagePublisher = messagePublisher; } public keyPressed(key: string) { this.keys.add(key); + + if (this.messagePublisher) { + this.messagePublisher.addMessage({ + type: MessageType.NEW_INPUT, + body: key + }); + } } public keyReleased(key: string) { this.keys.delete(key); + + if (this.messagePublisher) { + this.messagePublisher.addMessage({ + type: MessageType.REMOVE_INPUT, + body: key + }); + } + } + + public update(_dt: number, game: Game) { + game.forEachEntityWithComponent(ComponentNames.Control, (entity) => + this.handleInput(entity) + ); + } + + public handleInput(entity: Entity) { + const controlComponent = entity.getComponent<Control>( + ComponentNames.Control + ); + controlComponent.isControllable = + controlComponent.controllableBy === this.clientId; + + if (!controlComponent.isControllable) return; + + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += + PhysicsConstants.PLAYER_MOVE_VEL; + } + + if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { + controlComponent.controlVelocityComponent.velocity.dCartesian.dx += + -PhysicsConstants.PLAYER_MOVE_VEL; + } + + if ( + entity.hasComponent(ComponentNames.Jump) && + this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP)) + ) { + this.performJump(entity); + } + } + + private performJump(entity: Entity) { + const velocity = entity.getComponent<Velocity>( + ComponentNames.Velocity + ).velocity; + const jump = entity.getComponent<Jump>(ComponentNames.Jump); + + if (jump.canJump) { + this.actionTimeStamps.set(Action.JUMP, performance.now()); + + velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL; + jump.canJump = false; + } + + if ( + performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) < + PhysicsConstants.MAX_JUMP_TIME_MS + ) { + const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; + + const jumpForce = { + fCartesian: { + fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, + fx: 0 + }, + torque: 0 + }; + entity + .getComponent<Forces>(ComponentNames.Forces) + ?.forces.push(jumpForce); + } } private hasSomeKey(keys?: string[]): boolean { @@ -38,57 +123,4 @@ export class Input extends System { } return false; } - - public update(_dt: number, game: Game) { - game.forEachEntityWithComponent(ComponentNames.Control, (entity) => { - const controlComponent = entity.getComponent<Control>( - ComponentNames.Control - ); - if (controlComponent.controllableBy != this.clientId) return; - - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) { - controlComponent.controlVelocityComponent.velocity.dCartesian.dx += - PhysicsConstants.PLAYER_MOVE_VEL; - } - - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_LEFT))) { - controlComponent.controlVelocityComponent.velocity.dCartesian.dx += - -PhysicsConstants.PLAYER_MOVE_VEL; - } - - if (entity.hasComponent(ComponentNames.Jump)) { - const velocity = entity.getComponent<Velocity>( - ComponentNames.Velocity - ).velocity; - const jump = entity.getComponent<Jump>(ComponentNames.Jump); - - if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.JUMP))) { - if (jump.canJump) { - this.actionTimeStamps.set(Action.JUMP, performance.now()); - - velocity.dCartesian.dy += PhysicsConstants.PLAYER_JUMP_INITIAL_VEL; - jump.canJump = false; - } - - if ( - performance.now() - (this.actionTimeStamps.get(Action.JUMP) || 0) < - PhysicsConstants.MAX_JUMP_TIME_MS - ) { - const mass = entity.getComponent<Mass>(ComponentNames.Mass).mass; - - const jumpForce = { - fCartesian: { - fy: mass * PhysicsConstants.PLAYER_JUMP_ACC, - fx: 0 - }, - torque: 0 - }; - entity - .getComponent<Forces>(ComponentNames.Forces) - ?.forces.push(jumpForce); - } - } - } - }); - } } diff --git a/engine/systems/NetworkUpdate.ts b/engine/systems/NetworkUpdate.ts index bcfb71e..6d13574 100644 --- a/engine/systems/NetworkUpdate.ts +++ b/engine/systems/NetworkUpdate.ts @@ -1,10 +1,12 @@ import { System, SystemNames } from '.'; import { Game } from '../Game'; -import { ComponentNames, NetworkUpdateable } from '../components'; +import { ComponentNames } from '../components'; import { type MessageQueueProvider, type MessagePublisher, - type MessageProcessor + type MessageProcessor, + MessageType, + EntityUpdateBody } from '../network'; export class NetworkUpdate extends System { @@ -12,6 +14,8 @@ export class NetworkUpdate extends System { private publisher: MessagePublisher; private messageProcessor: MessageProcessor; + private entityUpdateTimers: Map<string, number>; + constructor( queueProvider: MessageQueueProvider, publisher: MessagePublisher, @@ -22,23 +26,47 @@ export class NetworkUpdate extends System { this.queueProvider = queueProvider; this.publisher = publisher; this.messageProcessor = messageProcessor; + + this.entityUpdateTimers = new Map(); } - public update(_dt: number, game: Game) { + public update(dt: number, game: Game) { + // 1. process new messages this.queueProvider .getNewMessages() .forEach((message) => this.messageProcessor.process(message)); this.queueProvider.clearMessages(); + // 2. send entity updates + const updateMessages: EntityUpdateBody[] = []; game.forEachEntityWithComponent( ComponentNames.NetworkUpdateable, (entity) => { - const networkUpdateComponent = entity.getComponent<NetworkUpdateable>( - ComponentNames.NetworkUpdateable - ); + let timer = this.entityUpdateTimers.get(entity.id) ?? dt; + timer -= dt; + this.entityUpdateTimers.set(entity.id, timer); + + if (timer > 0) return; + this.entityUpdateTimers.set(entity.id, this.getNextUpdateTimeMs()); + + if (entity.hasComponent(ComponentNames.NetworkUpdateable)) { + updateMessages.push({ + id: entity.id, + args: entity.serialize() + }); + } } ); + this.publisher.addMessage({ + type: MessageType.UPDATE_ENTITIES, + body: updateMessages + }); + // 3. publish changes this.publisher.publish(); } + + private getNextUpdateTimeMs() { + return Math.random() * 70 + 50; + } } diff --git a/engine/systems/Physics.ts b/engine/systems/Physics.ts index 35afb3f..b5df459 100644 --- a/engine/systems/Physics.ts +++ b/engine/systems/Physics.ts @@ -99,7 +99,7 @@ export class Physics extends System { : boundingBox.rotation) % 360; // clear the control velocity - if (control) { + if (control && control.isControllable) { control.controlVelocityComponent = new Velocity(); } }); |