holy fuck we actually got somewhere

This commit is contained in:
Elizabeth Hunt 2023-08-23 19:44:59 -06:00
parent d64ffb5016
commit dec7b614d8
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
20 changed files with 346 additions and 133 deletions

View File

@ -1,4 +1,5 @@
import { Game } from "@engine/Game";
import { Entity } from "@engine/entities";
import { Grid } from "@engine/structures";
import {
WallBounds,
@ -7,67 +8,116 @@ import {
Physics,
Input,
Collision,
MessageQueueProvider,
MessagePublisher,
NetworkUpdate,
} 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 {
private socket: WebSocket;
private messages: any[];
private messages: Message[];
constructor(socket: WebSocket) {
this.socket = socket;
this.messages = [];
this.socket.addEventListener("message", (e) => {
console.log(e);
const message = parse<Message>(e.data);
this.messages.push(message);
});
}
getNewMessages() {
public getNewMessages() {
return this.messages;
}
clearMessages() {
public clearMessages() {
this.messages = [];
}
}
class ClientSocketMessagePublisher implements MessagePublisher {
private socket: WebSocket;
private messages: any[];
private messages: Message[];
constructor(socket: WebSocket) {
this.socket = socket;
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 {
private game: Game;
private clientId: string;
constructor(ctx: CanvasRenderingContext2D) {
this.game = new Game();
constructor(game: Game) {
this.game = game;
}
const socket = new WebSocket("ws://localhost:8080");
setInterval(() => socket.send(JSON.stringify({ x: 1 })), 1_000);
const clientSocketMessageQueueProvider =
new ClientSocketMessageQueueProvider(socket);
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
socket
);
public async init(
ctx: CanvasRenderingContext2D,
httpMethod: string,
wsMethod: string,
host: string,
) {
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 socket = new WebSocket(`${wsMethod}://${host}/game`);
const clientSocketMessageQueueProvider =
new ClientSocketMessageQueueProvider(socket);
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
socket,
);
const clientMessageProcessor = new ClientMessageProcessor(this.game);
[
this.createInputSystem(),
new FacingDirection(),
@ -76,7 +126,8 @@ export class JumpStorm {
new WallBounds(ctx.canvas.width),
new NetworkUpdate(
clientSocketMessageQueueProvider,
clientSocketMessagePublisher
clientSocketMessagePublisher,
clientMessageProcessor,
),
new Render(ctx),
].forEach((system) => this.game.addSystem(system));
@ -93,7 +144,7 @@ export class JumpStorm {
}
private createInputSystem(): Input {
const inputSystem = new Input();
const inputSystem = new Input(this.clientId);
window.addEventListener("keydown", (e) => {
if (!e.repeat) {

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { loadAssets } from "@engine/config";
import { Game } from "@engine/Game";
import { JumpStorm } from "../JumpStorm";
let canvas: HTMLCanvasElement;
@ -9,17 +10,19 @@
export let width: number;
export let height: number;
let jumpStorm: JumpStorm;
onMount(() => {
onMount(async () => {
ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
loadAssets().then(() => {
jumpStorm = new JumpStorm(ctx);
await loadAssets();
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>
<canvas bind:this={canvas} {width} {height} />

View File

@ -1,5 +1,5 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"extends": ["@tsconfig/svelte/tsconfig.json", "../tsconfig.engine.json"],
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
@ -24,8 +24,5 @@
"src/**/*.js",
"src/**/*.svelte"
],
"paths": {
"@engine/*": ["../engine/*"]
},
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -4,6 +4,16 @@ import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
ws: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
cors: true,
plugins: [svelte()],
resolve: {
alias: {

View File

@ -60,7 +60,7 @@ export class Game {
return this.systems.get(name);
}
public doGameLoop = (timeStamp: number) => {
public doGameLoop(timeStamp: number) {
if (!this.running) {
return;
}
@ -86,5 +86,5 @@ export class Game {
this.systemOrder.forEach((systemName) => {
this.systems.get(systemName)?.update(dt, this);
});
};
}
}

View File

@ -2,13 +2,15 @@ import { Component, ComponentNames, Velocity } from ".";
export class Control extends Component {
public controlVelocityComponent: Velocity;
public controllableBy: string;
constructor(
controllableBy: string,
controlVelocityComponent: Velocity = new Velocity(),
controllableBy: string
) {
super(ComponentNames.Control);
this.controllableBy = controllableBy;
this.controlVelocityComponent = controlVelocityComponent;
}
}

View File

@ -14,7 +14,7 @@ export namespace KeyConstants {
// value -> [key] from KeyActions
export const ActionKeys: Map<Action, string[]> = Object.keys(
KeyActions
KeyActions,
).reduce((acc: Map<Action, string[]>, key) => {
const action = KeyActions[key];
@ -42,6 +42,4 @@ export namespace Miscellaneous {
export const DEFAULT_GRID_WIDTH = 30;
export const DEFAULT_GRID_HEIGHT = 30;
export const SERVER_TICK_RATE = 5 / 100;
}

View File

@ -1,10 +1,13 @@
import { EntityNames, Player } from ".";
import type { Component } from "../components";
export abstract class Entity {
public readonly id: string;
public readonly components: Map<string, Component>;
public id: string;
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.components = new Map();
}
@ -27,4 +30,13 @@ export abstract class Entity {
public hasComponent(name: string): boolean {
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);
}
}
}

View File

@ -1,7 +1,7 @@
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
import { BoundingBox, Sprite } from "../components";
import { TopCollidable } from "../components/TopCollidable";
import { Entity } from "../entities";
import { Entity, EntityNames } from "../entities";
export class Floor extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
@ -9,7 +9,7 @@ export class Floor extends Entity {
) as SpriteSpec;
constructor(width: number) {
super();
super(EntityNames.Floor);
this.addComponent(
new Sprite(

View File

@ -1,4 +1,4 @@
import { Entity } from ".";
import { Entity, EntityNames } from ".";
import { IMAGES, SPRITE_SPECS, Sprites, type SpriteSpec } from "../config";
import {
Jump,
@ -21,11 +21,11 @@ export class Player extends Entity {
private static MOI: number = 100;
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
Sprites.COFFEE
Sprites.COFFEE,
) as SpriteSpec;
constructor() {
super();
constructor(playerId: string) {
super(EntityNames.Player);
this.addComponent(
new BoundingBox(
@ -34,12 +34,12 @@ export class Player extends Entity {
y: 100,
},
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
0
)
0,
),
);
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));
@ -48,7 +48,7 @@ export class Player extends Entity {
this.addComponent(new Gravity());
this.addComponent(new Jump());
this.addComponent(new Control());
this.addComponent(new Control(playerId));
this.addComponent(new Collide());
this.addComponent(new WallBounded());
@ -64,8 +64,8 @@ export class Player extends Entity {
{ x: 0, y: 0 },
{ width: Player.spriteSpec.width, height: Player.spriteSpec.height },
Player.spriteSpec.msPerFrame,
Player.spriteSpec.frames
)
Player.spriteSpec.frames,
),
);
this.addComponent(new FacingDirection(leftSprite, rightSprite));

View File

@ -1,3 +1,4 @@
export * from "./Entity";
export * from "./Floor";
export * from "./Player";
export * from "./names";

4
engine/entities/names.ts Normal file
View File

@ -0,0 +1,4 @@
export namespace EntityNames {
export const Player = "Player";
export const Floor = "Floor";
}

29
engine/network/index.ts Normal file
View 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;
}

View File

@ -12,12 +12,14 @@ import { Action } from "../interfaces";
import { System, SystemNames } from ".";
export class Input extends System {
public clientId: string;
private keys: Set<string>;
private actionTimeStamps: Map<Action, number>;
constructor() {
constructor(clientId: string) {
super(SystemNames.Input);
this.clientId = clientId;
this.keys = new Set<string>();
this.actionTimeStamps = new Map<Action, number>();
}
@ -42,6 +44,7 @@ export class Input extends System {
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 +=

View File

@ -1,43 +1,44 @@
import { System, SystemNames } from ".";
import { Game } from "../Game";
import { ComponentNames, NetworkUpdateable } from "../components";
export interface MessageQueueProvider {
getNewMessages(): any[];
clearMessages(): void;
}
export interface MessagePublisher {
addMessage(message: any): void;
publish(): void;
}
import {
type MessageQueueProvider,
type MessagePublisher,
type MessageProcessor,
} from "../network";
export class NetworkUpdate extends System {
private queueProvider: MessageQueueProvider;
private publisher: MessagePublisher;
private messageProcessor: MessageProcessor;
constructor(
queueProvider: MessageQueueProvider,
publisher: MessagePublisher
publisher: MessagePublisher,
messageProcessor: MessageProcessor,
) {
super(SystemNames.NetworkUpdate);
this.queueProvider = queueProvider;
this.publisher = publisher;
this.messageProcessor = messageProcessor;
}
public update(_dt: number, game: Game) {
const messages = this.queueProvider.getNewMessages();
if (messages.length) console.log(messages);
this.queueProvider
.getNewMessages()
.forEach((message) => this.messageProcessor.process(message));
this.queueProvider.clearMessages();
game.forEachEntityWithComponent(
ComponentNames.NetworkUpdateable,
(entity) => {
const networkUpdateComponent = entity.getComponent<NetworkUpdateable>(
ComponentNames.NetworkUpdateable
ComponentNames.NetworkUpdateable,
);
}
},
);
this.publisher.publish();
}
}

27
engine/utils/coding.ts Normal file
View 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;
};

View File

@ -1,3 +1,4 @@
export * from "./rotateVector";
export * from "./dotProduct";
export * from "./clamp";
export * from "./coding";

View File

@ -1,109 +1,179 @@
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 {
WallBounds,
Physics,
Collision,
NetworkUpdate,
MessageQueueProvider,
MessagePublisher,
} from "@engine/systems";
type MessageQueueProvider,
type MessagePublisher,
MessageType,
type MessageProcessor,
type Message,
} from "@engine/network";
import { stringify, parse } from "@engine/utils";
import { Grid } from "@engine/structures";
import { Miscellaneous } from "@engine/config";
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 {
private messages: any[];
private messages: ServerMessage[];
constructor() {
this.messages = [];
}
addMessage(message: any) {
public addMessage(message: ServerMessage) {
this.messages.push(message);
}
getNewMessages() {
public getNewMessages() {
return this.messages;
}
clearMessages() {
public clearMessages() {
this.messages = [];
}
}
class ServerSocketMessagePublisher implements MessagePublisher {
private server: Server;
private messages: any[];
class ServerMessageProcessor implements MessageProcessor {
constructor() {}
constructor(server: Server) {
public process(_message: ServerMessage) {}
}
class ServerSocketMessagePublisher implements MessagePublisher {
private server?: Server;
private messages: Message[];
constructor(server?: Server) {
if (server) {
this.server = server;
}
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 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 }>({
port: 8080,
fetch: async (req, server): Promise<string> => {
const server = Bun.serve<SessionData>({
port: SERVER_PORT,
fetch: async (req, server): Promise<Response> => {
const url = new URL(req.url);
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
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: {
"Set-Cookie": `SessionId=${sessionId}`,
},
headers,
data: {
sessionId,
},
});
return sessionId;
return new Response("upgraded", { headers });
}
if (url.pathname == "/me") {
return new Response(sessionId, { headers });
}
return new Response("Not found", { headers, status: 404 });
},
websocket: {
open(ws) {
const { sessionId } = ws.data;
if (sessionControllableEntities.has(sessionId)) {
// no need to add player
return;
}
const player = new Player();
const player = new Player(sessionId);
game.addEntity(player);
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) {
console.log(JSON.parse(message));
messageReceiver.addMessage(message);
if (typeof message == "string") {
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 Collision(new Grid()),
new WallBounds(Miscellaneous.WIDTH),
new NetworkUpdate(messageReceiver, messagePublisher),
new NetworkUpdate(messageReceiver, messagePublisher, messageProcessor),
].forEach((system) => game.addSystem(system));
[new Floor(160), new Player()].forEach((entity) => game.addEntity(entity));
game.start();
setInterval(() => {
game.doGameLoop(performance.now());
}, Miscellaneous.SERVER_TICK_RATE);
const sessionControllableEntities: Map<string, Set<string>> = new Map();
}, SERVER_TICK_RATE);
console.log(`Listening on ${server.hostname}:${server.port}`);

View File

@ -1,4 +1,5 @@
{
"extends": "../tsconfig.engine.json",
"compilerOptions": {
// add Bun type definitions
"types": ["bun-types"],
@ -21,18 +22,6 @@
// best practices
"strict": true,
"forceConsistentCasingInFileNames": 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"],
}
"skipLibCheck": true
}
}

15
tsconfig.engine.json Normal file
View 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"]
}
}
}