diff --git a/kennel/kennel.py b/kennel/kennel.py index bb0438c..a9ab4b8 100644 --- a/kennel/kennel.py +++ b/kennel/kennel.py @@ -8,6 +8,8 @@ from kennel.engine.systems.network import ( EventProcessor, Event, EventType, + Entity, + EntityPositionUpdateEvent, ) from kennel.engine.systems.world import WorldSystem from kennel.config import config @@ -35,24 +37,28 @@ class KennelEventProcessor(EventProcessor): self._process_entity_position_update(entity_manager, event, client_id) def _process_entity_position_update( - self, entity_manager: EntityManager, event: Event, client_id: str + self, + entity_manager: EntityManager, + event: EntityPositionUpdateEvent, + client_id: str, ) -> None: entity = entity_manager.get_entity(event.data["id"]) if entity is None: logger.error(f"Entity(id={event.data['id']}) does not exist") return controllable = entity.get_component(ComponentType.CONTROLLABLE) - if controllable is None: - logger.error(f"Entity {entity} is not controllable") - return - if controllable.by != client_id: + if controllable is None or controllable.by != client_id: logger.error(f"Entity {entity} is not controllable by client {client_id}") return position = entity.get_component(ComponentType.POSITION) if position is None: logger.error(f"Entity {entity} has no position") return + position.x = event.data["position"]["x"] + position.y = event.data["position"]["y"] + entity.add_component(position) + entity_manager.add_entity(entity) system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) diff --git a/static/src/event.ts b/static/src/event.ts deleted file mode 100644 index cd50122..0000000 --- a/static/src/event.ts +++ /dev/null @@ -1,28 +0,0 @@ -export enum EventType { - INITIAL_STATE = "INITIAL_STATE", - SET_CONTROLLABLE = "SET_CONTROLLABLE", - ENTITY_BORN = "ENTITY_BORN", - ENTITY_DEATH = "ENTITY_DEATH", - ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE", -} - -export interface Event { - event_type: EventType; - data: any; -} - -export interface InitialStateEvent extends Event { - event_type: EventType.INITIAL_STATE; - data: { - world: { width: number; height: number }; - entities: any[]; - }; -} - -export interface SetControllableEvent extends Event { - event_type: EventType.SET_CONTROLLABLE; - data: { - id: string; - client_id: string; - }; -} diff --git a/static/src/main.ts b/static/src/main.ts index fc04b72..eb98ba8 100644 --- a/static/src/main.ts +++ b/static/src/main.ts @@ -1,51 +1,93 @@ import $ from "jquery"; import { Vec2 } from "./vector"; -import { EventType, type SetControllableEvent, type Event } from "./event"; +import { + EventType, + type SetControllableEvent, + type Event, + type EventPublisher, +} from "./event"; import { MouseController } from "./mouse_controller"; +import { + EntityPositionUpdateEvent, + EventProcessor, + EventQueue, + WebSocketEventQueue, +} from "./network"; -$(document).ready(async () => { +class KennelClient { + private running: boolean; + private last_update: number; + + private controllable_entities: Set; + private mouse_controller: MouseController; + + constructor( + private readonly event_queue: EventQueue, + private readonly event_publisher: EventPublisher, + ) { + this.last_update = 0; + this.running = false; + + this.mouse_controller = new MouseController((position: Vec2) => + this.on_mouse_move(position), + ); + } + + public start() { + this.running = true; + this.last_update = performance.now(); + this.mouse_controller.start(); + + const loop = (timestamp: number) => { + const dt = timestamp - this.last_update; + this.propogate_state_after(dt); + requestAnimationFrame(loop); // tail call recursion! /s + }; + requestAnimationFrame(loop); + } + + public close() { + this.running = false; + this.mouse_controller.stop(); + this.controllable_entities.clear(); + } + + private propogate_state_after(dt: number) { + // TODO: interpolate cats and lasers and stuff + } + + private on_mouse_move(position: Vec2) { + for (const id of this.controllable_entities) { + const event: EntityPositionUpdateEvent = { + event_type: EventType.ENTITY_POSITION_UPDATE, + data: { id, position: { x: position.x, y: position.y } }, + }; + this.event_publisher.send(event); + } + } +} + +$(async () => { const session_id = await fetch("/assign", { credentials: "include", }) .then((res) => res.json()) .then(({ session }) => session); - const controllable_entities = new Set(); - const control_callback = (movement: Vec2) => { - for (const id of controllable_entities) { - const message = JSON.stringify({ - event_type: EventType.ENTITY_POSITION_UPDATE, - data: { id, position: movement }, - }); - ws.send(message); - } - }; - const mouse_controller = new MouseController(control_callback); + const ws = new WebSocket("/ws"); + await new Promise((resolve) => { + ws.onopen = () => resolve(); + }); + + const queue: EventQueue = new WebSocketEventQueue(ws); + + const kennel_client = new KennelClient(session_id, null); + + ws.onclose = () => kennel_client.close(); + + const mouse_controller = new MouseController(on_mouse_move); $(document).on("mousemove", (event) => { mouse_controller.move(event.clientX, event.clientY); }); mouse_controller.start(); - - const ws = new WebSocket("/ws"); - await new Promise((resolve) => { - ws.onopen = () => { - console.log("connected"); - resolve(); - }; - }); - ws.onmessage = ({ data }) => { - const [event_type, event_data] = JSON.parse(data); - const message = { event_type, data: event_data } as Event; - - console.log("Received message", message); - if (message.event_type === EventType.SET_CONTROLLABLE) { - const event = message as SetControllableEvent; - if (event.data.client_id === session_id) { - controllable_entities.add(event.data.id); - } - } - }; - ws.onclose = () => { - controllable_entities.clear(); - }; }); diff --git a/static/src/mouse_controller.ts b/static/src/mouse_controller.ts index b8849b4..c7ae304 100644 --- a/static/src/mouse_controller.ts +++ b/static/src/mouse_controller.ts @@ -1,21 +1,23 @@ import { Vec2 } from "./vector"; - export class MouseController { - private readonly debounce_ms = 400; - private readonly movement_threshold = 40; private last_event_time = Date.now(); - private movement_queue: Vec2[] = []; - private interval_id: number | null = null; + private last_movement: Vec2 | undefined; + private interval_id: number | undefined; - constructor(private readonly callback: (new_movement: Vec2) => void) {} + constructor( + private readonly publisher: (new_movement: Vec2) => void | Promise, + private readonly debounce_ms = 200, + private readonly l2_norm_threshold = 40, + ) {} public start() { - if (this.interval_id !== null) { + if (typeof this.interval_id !== "undefined") { return; } - this.interval_id = setInterval(() => { - this.publish_movement(); - }, this.debounce_ms); + this.interval_id = setInterval( + () => this.publish_movement(), + this.debounce_ms, + ); } public stop() { @@ -23,32 +25,30 @@ export class MouseController { return; } clearInterval(this.interval_id); - this.interval_id = null; + delete this.interval_id; } public move(x: number, y: number) { const new_movement = new Vec2(x, y); - const last_movement = this.movement_queue.at(-1); - this.movement_queue.push(new_movement); if ( - typeof last_movement === "undefined" || - new_movement.distance_to(last_movement) < this.movement_threshold + typeof this.last_movement !== "undefined" && + new_movement.distance_to(this.last_movement) >= this.l2_norm_threshold ) { - return; + this.publish_movement(); } - this.publish_movement(); + this.last_movement = new_movement; } private publish_movement() { if ( - Date.now() - this.last_event_time < this.debounce_ms || - this.movement_queue.length === 0 + typeof this.last_movement === "undefined" || + Date.now() - this.last_event_time < this.debounce_ms ) { return; } this.last_event_time = Date.now(); - this.callback(this.movement_queue.at(-1)!); - this.movement_queue = []; + this.publisher(this.last_movement.copy()); + delete this.last_movement; } } diff --git a/static/src/network.ts b/static/src/network.ts new file mode 100644 index 0000000..0d4e216 --- /dev/null +++ b/static/src/network.ts @@ -0,0 +1,73 @@ +export enum EventType { + INITIAL_STATE = "INITIAL_STATE", + SET_CONTROLLABLE = "SET_CONTROLLABLE", + ENTITY_BORN = "ENTITY_BORN", + ENTITY_DEATH = "ENTITY_DEATH", + ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE", +} + +export interface Event { + event_type: EventType; + data: any; +} + +export interface EntityPositionUpdateEvent extends Event { + event_type: EventType.ENTITY_POSITION_UPDATE; + data: { + id: string; + position: { + x: number; + y: number; + }; + }; +} + +export interface InitialStateEvent extends Event { + event_type: EventType.INITIAL_STATE; + data: { + world: { width: number; height: number }; + entities: any[]; + }; +} + +export interface SetControllableEvent extends Event { + event_type: EventType.SET_CONTROLLABLE; + data: { + id: string; + client_id: string; + }; +} + +export interface EventQueue { + peek(): Event[]; + clear(): void; +} + +export interface EventPublisher { + add(event: Event): void; + publish(): void; +} + +export class WebSocketEventQueue implements EventQueue { + private queue: Event[]; + + constructor(websocket: WebSocket) { + this.queue = []; + this.listen_to(websocket); + } + + public peek() { + return this.queue; + } + + public clear() { + this.queue = []; + } + + private listen_to(websocket: WebSocket) { + websocket.onmessage = ({ data }) => { + const [event_type, event_data] = JSON.parse(data); + this.queue.push({ event_type, data: event_data } as Event); + }; + } +} diff --git a/static/src/vector.ts b/static/src/vector.ts index e7be9fa..96bd721 100644 --- a/static/src/vector.ts +++ b/static/src/vector.ts @@ -7,4 +7,8 @@ export class Vec2 { public distance_to(that: Vec2): number { return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2); } + + public copy(): Vec2 { + return new Vec2(this.x, this.y); + } }