holy fuck we actually got somewhere
This commit is contained in:
parent
d64ffb5016
commit
dec7b614d8
@ -1,4 +1,5 @@
|
|||||||
import { Game } from "@engine/Game";
|
import { Game } from "@engine/Game";
|
||||||
|
import { Entity } from "@engine/entities";
|
||||||
import { Grid } from "@engine/structures";
|
import { Grid } from "@engine/structures";
|
||||||
import {
|
import {
|
||||||
WallBounds,
|
WallBounds,
|
||||||
@ -7,67 +8,116 @@ import {
|
|||||||
Physics,
|
Physics,
|
||||||
Input,
|
Input,
|
||||||
Collision,
|
Collision,
|
||||||
MessageQueueProvider,
|
|
||||||
MessagePublisher,
|
|
||||||
NetworkUpdate,
|
NetworkUpdate,
|
||||||
} from "@engine/systems";
|
} from "@engine/systems";
|
||||||
|
import {
|
||||||
|
type MessageQueueProvider,
|
||||||
|
type MessagePublisher,
|
||||||
|
type MessageProcessor,
|
||||||
|
type Message,
|
||||||
|
type EntityAddBody,
|
||||||
|
MessageType,
|
||||||
|
} from "@engine/network";
|
||||||
|
import { stringify, parse } from "@engine/utils";
|
||||||
|
|
||||||
|
class ClientMessageProcessor implements MessageProcessor {
|
||||||
|
private game: Game;
|
||||||
|
|
||||||
|
constructor(game: Game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public process(message: Message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.NEW_ENTITY:
|
||||||
|
const entityAddBody = message.body as unknown as EntityAddBody;
|
||||||
|
this.game.addEntity(
|
||||||
|
Entity.from(entityAddBody.entityName, entityAddBody.args),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ClientSocketMessageQueueProvider implements MessageQueueProvider {
|
class ClientSocketMessageQueueProvider implements MessageQueueProvider {
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private messages: any[];
|
private messages: Message[];
|
||||||
|
|
||||||
constructor(socket: WebSocket) {
|
constructor(socket: WebSocket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
|
||||||
this.socket.addEventListener("message", (e) => {
|
this.socket.addEventListener("message", (e) => {
|
||||||
console.log(e);
|
const message = parse<Message>(e.data);
|
||||||
|
this.messages.push(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getNewMessages() {
|
public getNewMessages() {
|
||||||
return this.messages;
|
return this.messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMessages() {
|
public clearMessages() {
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClientSocketMessagePublisher implements MessagePublisher {
|
class ClientSocketMessagePublisher implements MessagePublisher {
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private messages: any[];
|
private messages: Message[];
|
||||||
|
|
||||||
constructor(socket: WebSocket) {
|
constructor(socket: WebSocket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
|
||||||
this.socket.addEventListener("message", (e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addMessage(_message: any) {}
|
public addMessage(message: Message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
publish() {}
|
public publish() {
|
||||||
|
this.messages.forEach((message: Message) =>
|
||||||
|
this.socket.send(stringify(message)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JumpStorm {
|
export class JumpStorm {
|
||||||
private game: Game;
|
private game: Game;
|
||||||
|
private clientId: string;
|
||||||
|
|
||||||
constructor(ctx: CanvasRenderingContext2D) {
|
constructor(game: Game) {
|
||||||
this.game = new Game();
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
const socket = new WebSocket("ws://localhost:8080");
|
public async init(
|
||||||
setInterval(() => socket.send(JSON.stringify({ x: 1 })), 1_000);
|
ctx: CanvasRenderingContext2D,
|
||||||
const clientSocketMessageQueueProvider =
|
httpMethod: string,
|
||||||
new ClientSocketMessageQueueProvider(socket);
|
wsMethod: string,
|
||||||
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
|
host: string,
|
||||||
socket
|
) {
|
||||||
);
|
await fetch(`${httpMethod}://${host}/assign`)
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
return resp.text();
|
||||||
|
}
|
||||||
|
throw resp;
|
||||||
|
})
|
||||||
|
.then((cookie) => {
|
||||||
|
this.clientId = cookie;
|
||||||
|
});
|
||||||
|
|
||||||
const grid = new Grid();
|
const grid = new Grid();
|
||||||
|
|
||||||
|
const socket = new WebSocket(`${wsMethod}://${host}/game`);
|
||||||
|
const clientSocketMessageQueueProvider =
|
||||||
|
new ClientSocketMessageQueueProvider(socket);
|
||||||
|
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
|
||||||
|
socket,
|
||||||
|
);
|
||||||
|
const clientMessageProcessor = new ClientMessageProcessor(this.game);
|
||||||
[
|
[
|
||||||
this.createInputSystem(),
|
this.createInputSystem(),
|
||||||
new FacingDirection(),
|
new FacingDirection(),
|
||||||
@ -76,7 +126,8 @@ export class JumpStorm {
|
|||||||
new WallBounds(ctx.canvas.width),
|
new WallBounds(ctx.canvas.width),
|
||||||
new NetworkUpdate(
|
new NetworkUpdate(
|
||||||
clientSocketMessageQueueProvider,
|
clientSocketMessageQueueProvider,
|
||||||
clientSocketMessagePublisher
|
clientSocketMessagePublisher,
|
||||||
|
clientMessageProcessor,
|
||||||
),
|
),
|
||||||
new Render(ctx),
|
new Render(ctx),
|
||||||
].forEach((system) => this.game.addSystem(system));
|
].forEach((system) => this.game.addSystem(system));
|
||||||
@ -93,7 +144,7 @@ export class JumpStorm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createInputSystem(): Input {
|
private createInputSystem(): Input {
|
||||||
const inputSystem = new Input();
|
const inputSystem = new Input(this.clientId);
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
window.addEventListener("keydown", (e) => {
|
||||||
if (!e.repeat) {
|
if (!e.repeat) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { loadAssets } from "@engine/config";
|
import { loadAssets } from "@engine/config";
|
||||||
|
import { Game } from "@engine/Game";
|
||||||
import { JumpStorm } from "../JumpStorm";
|
import { JumpStorm } from "../JumpStorm";
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
@ -9,16 +10,18 @@
|
|||||||
export let width: number;
|
export let width: number;
|
||||||
export let height: number;
|
export let height: number;
|
||||||
|
|
||||||
let jumpStorm: JumpStorm;
|
onMount(async () => {
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
ctx = canvas.getContext("2d");
|
ctx = canvas.getContext("2d");
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
loadAssets().then(() => {
|
await loadAssets();
|
||||||
jumpStorm = new JumpStorm(ctx);
|
|
||||||
jumpStorm.play();
|
const game = new Game();
|
||||||
});
|
const jumpStorm = new JumpStorm(game);
|
||||||
|
|
||||||
|
const url = new URL(document.location);
|
||||||
|
await jumpStorm.init(ctx, "http", "ws", url.host + "/api");
|
||||||
|
jumpStorm.play();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": ["@tsconfig/svelte/tsconfig.json", "../tsconfig.engine.json"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
@ -24,8 +24,5 @@
|
|||||||
"src/**/*.js",
|
"src/**/*.js",
|
||||||
"src/**/*.svelte"
|
"src/**/*.svelte"
|
||||||
],
|
],
|
||||||
"paths": {
|
|
||||||
"@engine/*": ["../engine/*"]
|
|
||||||
},
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,16 @@ import { fileURLToPath, URL } from "node:url";
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8080",
|
||||||
|
ws: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cors: true,
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
@ -60,7 +60,7 @@ export class Game {
|
|||||||
return this.systems.get(name);
|
return this.systems.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public doGameLoop = (timeStamp: number) => {
|
public doGameLoop(timeStamp: number) {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -86,5 +86,5 @@ export class Game {
|
|||||||
this.systemOrder.forEach((systemName) => {
|
this.systemOrder.forEach((systemName) => {
|
||||||
this.systems.get(systemName)?.update(dt, this);
|
this.systems.get(systemName)?.update(dt, this);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,15 @@ import { Component, ComponentNames, Velocity } from ".";
|
|||||||
|
|
||||||
export class Control extends Component {
|
export class Control extends Component {
|
||||||
public controlVelocityComponent: Velocity;
|
public controlVelocityComponent: Velocity;
|
||||||
|
public controllableBy: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
controllableBy: string,
|
||||||
controlVelocityComponent: Velocity = new Velocity(),
|
controlVelocityComponent: Velocity = new Velocity(),
|
||||||
controllableBy: string
|
|
||||||
) {
|
) {
|
||||||
super(ComponentNames.Control);
|
super(ComponentNames.Control);
|
||||||
|
|
||||||
|
this.controllableBy = controllableBy;
|
||||||
this.controlVelocityComponent = controlVelocityComponent;
|
this.controlVelocityComponent = controlVelocityComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ export namespace KeyConstants {
|
|||||||
|
|
||||||
// value -> [key] from KeyActions
|
// value -> [key] from KeyActions
|
||||||
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
export const ActionKeys: Map<Action, string[]> = Object.keys(
|
||||||
KeyActions
|
KeyActions,
|
||||||
).reduce((acc: Map<Action, string[]>, key) => {
|
).reduce((acc: Map<Action, string[]>, key) => {
|
||||||
const action = KeyActions[key];
|
const action = KeyActions[key];
|
||||||
|
|
||||||
@ -42,6 +42,4 @@ export namespace Miscellaneous {
|
|||||||
|
|
||||||
export const DEFAULT_GRID_WIDTH = 30;
|
export const DEFAULT_GRID_WIDTH = 30;
|
||||||
export const DEFAULT_GRID_HEIGHT = 30;
|
export const DEFAULT_GRID_HEIGHT = 30;
|
||||||
|
|
||||||
export const SERVER_TICK_RATE = 5 / 100;
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import { EntityNames, Player } from ".";
|
||||||
import type { Component } from "../components";
|
import type { Component } from "../components";
|
||||||
|
|
||||||
export abstract class Entity {
|
export abstract class Entity {
|
||||||
public readonly id: string;
|
public id: string;
|
||||||
public readonly components: Map<string, Component>;
|
public components: Map<string, Component>;
|
||||||
|
public name: string;
|
||||||
|
|
||||||
constructor(id: string = crypto.randomUUID()) {
|
constructor(name: string, id: string = crypto.randomUUID()) {
|
||||||
|
this.name = name;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.components = new Map();
|
this.components = new Map();
|
||||||
}
|
}
|
||||||
@ -27,4 +30,13 @@ export abstract class Entity {
|
|||||||
public hasComponent(name: string): boolean {
|
public hasComponent(name: string): boolean {
|
||||||
return this.components.has(name);
|
return this.components.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static from(entityName: string, args: any): Entity {
|
||||||
|
switch (entityName) {
|
||||||
|
case EntityNames.Player:
|
||||||
|
return new Player(args.playerId);
|
||||||
|
default:
|
||||||
|
throw new Error(".from() Entity type not implemented: " + entityName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
||||||
import { BoundingBox, Sprite } from "../components";
|
import { BoundingBox, Sprite } from "../components";
|
||||||
import { TopCollidable } from "../components/TopCollidable";
|
import { TopCollidable } from "../components/TopCollidable";
|
||||||
import { Entity } from "../entities";
|
import { Entity, EntityNames } from "../entities";
|
||||||
|
|
||||||
export class Floor extends Entity {
|
export class Floor extends Entity {
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
@ -9,7 +9,7 @@ export class Floor extends Entity {
|
|||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor(width: number) {
|
constructor(width: number) {
|
||||||
super();
|
super(EntityNames.Floor);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new Sprite(
|
new Sprite(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Entity } from ".";
|
import { Entity, EntityNames } from ".";
|
||||||
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
|
||||||
import {
|
import {
|
||||||
Jump,
|
Jump,
|
||||||
@ -21,11 +21,11 @@ export class Player extends Entity {
|
|||||||
private static MOI: number = 100;
|
private static MOI: number = 100;
|
||||||
|
|
||||||
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
|
||||||
Sprites.COFFEE
|
Sprites.COFFEE,
|
||||||
) as SpriteSpec;
|
) as SpriteSpec;
|
||||||
|
|
||||||
constructor() {
|
constructor(playerId: string) {
|
||||||
super();
|
super(EntityNames.Player);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new BoundingBox(
|
new BoundingBox(
|
||||||
@ -34,12 +34,12 @@ export class Player extends Entity {
|
|||||||
y: 100,
|
y: 100,
|
||||||
},
|
},
|
||||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||||
0
|
0,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(
|
this.addComponent(
|
||||||
new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 })
|
new Velocity({ dCartesian: { dx: 0, dy: 0 }, dTheta: 0 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new Mass(Player.MASS));
|
this.addComponent(new Mass(Player.MASS));
|
||||||
@ -48,7 +48,7 @@ export class Player extends Entity {
|
|||||||
this.addComponent(new Gravity());
|
this.addComponent(new Gravity());
|
||||||
|
|
||||||
this.addComponent(new Jump());
|
this.addComponent(new Jump());
|
||||||
this.addComponent(new Control());
|
this.addComponent(new Control(playerId));
|
||||||
|
|
||||||
this.addComponent(new Collide());
|
this.addComponent(new Collide());
|
||||||
this.addComponent(new WallBounded());
|
this.addComponent(new WallBounded());
|
||||||
@ -64,8 +64,8 @@ export class Player extends Entity {
|
|||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
|
||||||
Player.spriteSpec.msPerFrame,
|
Player.spriteSpec.msPerFrame,
|
||||||
Player.spriteSpec.frames
|
Player.spriteSpec.frames,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
this.addComponent(new FacingDirection(leftSprite, rightSprite));
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./Entity";
|
export * from "./Entity";
|
||||||
export * from "./Floor";
|
export * from "./Floor";
|
||||||
export * from "./Player";
|
export * from "./Player";
|
||||||
|
export * from "./names";
|
||||||
|
4
engine/entities/names.ts
Normal file
4
engine/entities/names.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export namespace EntityNames {
|
||||||
|
export const Player = "Player";
|
||||||
|
export const Floor = "Floor";
|
||||||
|
}
|
29
engine/network/index.ts
Normal file
29
engine/network/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export enum MessageType {
|
||||||
|
NEW_ENTITY = "NEW_ENTITY",
|
||||||
|
REMOVE_ENTITY = "REMOVE_ENTITY",
|
||||||
|
UPDATE_ENTITY = "UPDATE_ENTITY",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityAddBody = {
|
||||||
|
entityName: string;
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
type: MessageType;
|
||||||
|
body: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MessageQueueProvider {
|
||||||
|
getNewMessages(): Message[];
|
||||||
|
clearMessages(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessagePublisher {
|
||||||
|
addMessage(message: Message): void;
|
||||||
|
publish(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageProcessor {
|
||||||
|
process(message: Message): void;
|
||||||
|
}
|
@ -12,12 +12,14 @@ import { Action } from "../interfaces";
|
|||||||
import { System, SystemNames } from ".";
|
import { System, SystemNames } from ".";
|
||||||
|
|
||||||
export class Input extends System {
|
export class Input extends System {
|
||||||
|
public clientId: string;
|
||||||
private keys: Set<string>;
|
private keys: Set<string>;
|
||||||
private actionTimeStamps: Map<Action, number>;
|
private actionTimeStamps: Map<Action, number>;
|
||||||
|
|
||||||
constructor() {
|
constructor(clientId: string) {
|
||||||
super(SystemNames.Input);
|
super(SystemNames.Input);
|
||||||
|
|
||||||
|
this.clientId = clientId;
|
||||||
this.keys = new Set<string>();
|
this.keys = new Set<string>();
|
||||||
this.actionTimeStamps = new Map<Action, number>();
|
this.actionTimeStamps = new Map<Action, number>();
|
||||||
}
|
}
|
||||||
@ -42,6 +44,7 @@ export class Input extends System {
|
|||||||
const controlComponent = entity.getComponent<Control>(
|
const controlComponent = entity.getComponent<Control>(
|
||||||
ComponentNames.Control,
|
ComponentNames.Control,
|
||||||
);
|
);
|
||||||
|
if (controlComponent.controllableBy != this.clientId) return;
|
||||||
|
|
||||||
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
if (this.hasSomeKey(KeyConstants.ActionKeys.get(Action.MOVE_RIGHT))) {
|
||||||
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
controlComponent.controlVelocityComponent.velocity.dCartesian.dx +=
|
||||||
|
@ -1,43 +1,44 @@
|
|||||||
import { System, SystemNames } from ".";
|
import { System, SystemNames } from ".";
|
||||||
import { Game } from "../Game";
|
import { Game } from "../Game";
|
||||||
import { ComponentNames, NetworkUpdateable } from "../components";
|
import { ComponentNames, NetworkUpdateable } from "../components";
|
||||||
|
import {
|
||||||
export interface MessageQueueProvider {
|
type MessageQueueProvider,
|
||||||
getNewMessages(): any[];
|
type MessagePublisher,
|
||||||
clearMessages(): void;
|
type MessageProcessor,
|
||||||
}
|
} from "../network";
|
||||||
|
|
||||||
export interface MessagePublisher {
|
|
||||||
addMessage(message: any): void;
|
|
||||||
publish(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NetworkUpdate extends System {
|
export class NetworkUpdate extends System {
|
||||||
private queueProvider: MessageQueueProvider;
|
private queueProvider: MessageQueueProvider;
|
||||||
private publisher: MessagePublisher;
|
private publisher: MessagePublisher;
|
||||||
|
private messageProcessor: MessageProcessor;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queueProvider: MessageQueueProvider,
|
queueProvider: MessageQueueProvider,
|
||||||
publisher: MessagePublisher
|
publisher: MessagePublisher,
|
||||||
|
messageProcessor: MessageProcessor,
|
||||||
) {
|
) {
|
||||||
super(SystemNames.NetworkUpdate);
|
super(SystemNames.NetworkUpdate);
|
||||||
|
|
||||||
this.queueProvider = queueProvider;
|
this.queueProvider = queueProvider;
|
||||||
this.publisher = publisher;
|
this.publisher = publisher;
|
||||||
|
this.messageProcessor = messageProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(_dt: number, game: Game) {
|
public update(_dt: number, game: Game) {
|
||||||
const messages = this.queueProvider.getNewMessages();
|
this.queueProvider
|
||||||
if (messages.length) console.log(messages);
|
.getNewMessages()
|
||||||
|
.forEach((message) => this.messageProcessor.process(message));
|
||||||
this.queueProvider.clearMessages();
|
this.queueProvider.clearMessages();
|
||||||
|
|
||||||
game.forEachEntityWithComponent(
|
game.forEachEntityWithComponent(
|
||||||
ComponentNames.NetworkUpdateable,
|
ComponentNames.NetworkUpdateable,
|
||||||
(entity) => {
|
(entity) => {
|
||||||
const networkUpdateComponent = entity.getComponent<NetworkUpdateable>(
|
const networkUpdateComponent = entity.getComponent<NetworkUpdateable>(
|
||||||
ComponentNames.NetworkUpdateable
|
ComponentNames.NetworkUpdateable,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.publisher.publish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
engine/utils/coding.ts
Normal file
27
engine/utils/coding.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const replacer = (_key: any, value: any) => {
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return {
|
||||||
|
dataType: "Map",
|
||||||
|
value: Array.from(value.entries()),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reviver = (_key: any, value: any) => {
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
if (value.dataType === "Map") {
|
||||||
|
return new Map(value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringify = (obj: any) => {
|
||||||
|
return JSON.stringify(obj, replacer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse = <T>(str: string) => {
|
||||||
|
return JSON.parse(str, reviver) as unknown as T;
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./rotateVector";
|
export * from "./rotateVector";
|
||||||
export * from "./dotProduct";
|
export * from "./dotProduct";
|
||||||
export * from "./clamp";
|
export * from "./clamp";
|
||||||
|
export * from "./coding";
|
||||||
|
@ -1,109 +1,179 @@
|
|||||||
import { Game } from "@engine/Game";
|
import { Game } from "@engine/Game";
|
||||||
import { Floor, Player } from "@engine/entities";
|
import { EntityNames, Player } from "@engine/entities";
|
||||||
|
import { WallBounds, Physics, Collision, NetworkUpdate } from "@engine/systems";
|
||||||
import {
|
import {
|
||||||
WallBounds,
|
type MessageQueueProvider,
|
||||||
Physics,
|
type MessagePublisher,
|
||||||
Collision,
|
MessageType,
|
||||||
NetworkUpdate,
|
type MessageProcessor,
|
||||||
MessageQueueProvider,
|
type Message,
|
||||||
MessagePublisher,
|
} from "@engine/network";
|
||||||
} from "@engine/systems";
|
import { stringify, parse } from "@engine/utils";
|
||||||
import { Grid } from "@engine/structures";
|
import { Grid } from "@engine/structures";
|
||||||
import { Miscellaneous } from "@engine/config";
|
import { Miscellaneous } from "@engine/config";
|
||||||
import { Server } from "bun";
|
import { Server } from "bun";
|
||||||
|
|
||||||
|
const SERVER_PORT = 8080;
|
||||||
|
const SERVER_TICK_RATE = (1 / 100) * 1000;
|
||||||
|
const GAME_TOPIC = "game";
|
||||||
|
|
||||||
|
type SessionData = { sessionId: string };
|
||||||
|
|
||||||
|
interface ServerMessage extends Message {
|
||||||
|
sessionData: SessionData;
|
||||||
|
}
|
||||||
|
|
||||||
class ServerSocketMessageReceiver implements MessageQueueProvider {
|
class ServerSocketMessageReceiver implements MessageQueueProvider {
|
||||||
private messages: any[];
|
private messages: ServerMessage[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addMessage(message: any) {
|
public addMessage(message: ServerMessage) {
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNewMessages() {
|
public getNewMessages() {
|
||||||
return this.messages;
|
return this.messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMessages() {
|
public clearMessages() {
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerSocketMessagePublisher implements MessagePublisher {
|
class ServerMessageProcessor implements MessageProcessor {
|
||||||
private server: Server;
|
constructor() {}
|
||||||
private messages: any[];
|
|
||||||
|
public process(_message: ServerMessage) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerSocketMessagePublisher implements MessagePublisher {
|
||||||
|
private server?: Server;
|
||||||
|
private messages: Message[];
|
||||||
|
|
||||||
|
constructor(server?: Server) {
|
||||||
|
if (server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(server: Server) {
|
|
||||||
this.server = server;
|
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addMessage(_message: any) {}
|
public setServer(server: Server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
publish() {}
|
public addMessage(message: Message) {
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public publish() {
|
||||||
|
this.messages.forEach(
|
||||||
|
(message) => this.server?.publish(GAME_TOPIC, stringify(message)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const game = new Game();
|
const game = new Game();
|
||||||
|
|
||||||
const messageReceiver = new ServerSocketMessageReceiver();
|
const messageReceiver = new ServerSocketMessageReceiver();
|
||||||
|
const messagePublisher = new ServerSocketMessagePublisher();
|
||||||
|
const messageProcessor = new ServerMessageProcessor();
|
||||||
|
const sessionControllableEntities: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
const server = Bun.serve<{ sessionId: string }>({
|
const server = Bun.serve<SessionData>({
|
||||||
port: 8080,
|
port: SERVER_PORT,
|
||||||
fetch: async (req, server): Promise<string> => {
|
fetch: async (req, server): Promise<Response> => {
|
||||||
const sessionId = crypto.randomUUID();
|
const url = new URL(req.url);
|
||||||
|
|
||||||
server.upgrade(req, {
|
const headers = new Headers();
|
||||||
headers: {
|
headers.set("Access-Control-Allow-Origin", "*");
|
||||||
"Set-Cookie": `SessionId=${sessionId}`,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
sessionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return sessionId;
|
if (url.pathname == "/assign") {
|
||||||
|
const sessionId = crypto.randomUUID();
|
||||||
|
headers.set("Set-Cookie", `SessionId=${sessionId};`);
|
||||||
|
|
||||||
|
return new Response(sessionId, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = req.headers.get("cookie");
|
||||||
|
if (!cookie) {
|
||||||
|
return new Response("No session", { headers, status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = cookie.split(";").at(0)!.split("SessionId=").at(1);
|
||||||
|
|
||||||
|
if (url.pathname == "/game") {
|
||||||
|
headers.set(
|
||||||
|
"Set-Cookie",
|
||||||
|
`SessionId=${sessionId}; HttpOnly; SameSite=Strict;`,
|
||||||
|
);
|
||||||
|
server.upgrade(req, {
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response("upgraded", { headers });
|
||||||
|
}
|
||||||
|
if (url.pathname == "/me") {
|
||||||
|
return new Response(sessionId, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Not found", { headers, status: 404 });
|
||||||
},
|
},
|
||||||
websocket: {
|
websocket: {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
const { sessionId } = ws.data;
|
const { sessionId } = ws.data;
|
||||||
|
|
||||||
if (sessionControllableEntities.has(sessionId)) {
|
if (sessionControllableEntities.has(sessionId)) {
|
||||||
|
// no need to add player
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = new Player();
|
const player = new Player(sessionId);
|
||||||
game.addEntity(player);
|
game.addEntity(player);
|
||||||
|
|
||||||
sessionControllableEntities.set(sessionId, new Set(player.id));
|
sessionControllableEntities.set(sessionId, new Set(player.id));
|
||||||
|
|
||||||
|
messagePublisher.addMessage({
|
||||||
|
type: MessageType.NEW_ENTITY,
|
||||||
|
body: {
|
||||||
|
entityName: EntityNames.Player,
|
||||||
|
args: { playerId: sessionId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.subscribe(GAME_TOPIC);
|
||||||
},
|
},
|
||||||
message(ws, message) {
|
message(ws, message) {
|
||||||
console.log(JSON.parse(message));
|
if (typeof message == "string") {
|
||||||
messageReceiver.addMessage(message);
|
const receivedMessage = parse<ServerMessage>(message);
|
||||||
|
receivedMessage.sessionData = ws.data;
|
||||||
|
|
||||||
|
messageReceiver.addMessage(receivedMessage);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
close(ws) {},
|
close(_ws) {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const messagePublisher = new ServerSocketMessagePublisher(server);
|
messagePublisher.setServer(server);
|
||||||
|
|
||||||
[
|
[
|
||||||
new Physics(),
|
new Physics(),
|
||||||
new Collision(new Grid()),
|
new Collision(new Grid()),
|
||||||
new WallBounds(Miscellaneous.WIDTH),
|
new WallBounds(Miscellaneous.WIDTH),
|
||||||
new NetworkUpdate(messageReceiver, messagePublisher),
|
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
|
||||||
].forEach((system) => game.addSystem(system));
|
].forEach((system) => game.addSystem(system));
|
||||||
|
|
||||||
[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity));
|
|
||||||
|
|
||||||
game.start();
|
game.start();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
game.doGameLoop(performance.now());
|
game.doGameLoop(performance.now());
|
||||||
}, Miscellaneous.SERVER_TICK_RATE);
|
}, SERVER_TICK_RATE);
|
||||||
|
|
||||||
const sessionControllableEntities: Map<string, Set<string>> = new Map();
|
|
||||||
|
|
||||||
console.log(`Listening on ${server.hostname}:${server.port}`);
|
console.log(`Listening on ${server.hostname}:${server.port}`);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../tsconfig.engine.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// add Bun type definitions
|
// add Bun type definitions
|
||||||
"types": ["bun-types"],
|
"types": ["bun-types"],
|
||||||
@ -21,18 +22,6 @@
|
|||||||
// best practices
|
// best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true
|
||||||
|
|
||||||
// engine path
|
|
||||||
"paths": {
|
|
||||||
"@engine/*": ["../engine/*"],
|
|
||||||
"@engine/components": ["../engine/components"],
|
|
||||||
"@engine/config": ["../engine/config"],
|
|
||||||
"@engine/entities": ["../engine/entities"],
|
|
||||||
"@engine/interfaces": ["../engine/interfaces"],
|
|
||||||
"@engine/structures": ["../engine/structures"],
|
|
||||||
"@engine/systems": ["../engine/systems"],
|
|
||||||
"@engine/utils": ["../engine/utils"],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
tsconfig.engine.json
Normal file
15
tsconfig.engine.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@engine/*": ["./engine/*"],
|
||||||
|
"@engine/components": ["./engine/components"],
|
||||||
|
"@engine/config": ["./engine/config"],
|
||||||
|
"@engine/entities": ["./engine/entities"],
|
||||||
|
"@engine/interfaces": ["./engine/interfaces"],
|
||||||
|
"@engine/structures": ["./engine/structures"],
|
||||||
|
"@engine/systems": ["./engine/systems"],
|
||||||
|
"@engine/utils": ["./engine/utils"],
|
||||||
|
"@engine/network": ["./engine/network"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user