aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-08-24 19:11:26 -0700
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2024-08-24 19:16:22 -0700
commita0a2068b66204d7d06eb3c3b4aa61b28469121f6 (patch)
treee9706970af1ce8b4d12df9c0a3f64fa17ca74b97
parentb144ad5df76e458297bd6dc030cad4af0f344901 (diff)
downloadkennel.hatecomputers.club-a0a2068b66204d7d06eb3c3b4aa61b28469121f6.tar.gz
kennel.hatecomputers.club-a0a2068b66204d7d06eb3c3b4aa61b28469121f6.zip
improve event parsing and begin sending events from the client
-rw-r--r--kennel/engine/components/component.py2
-rw-r--r--kennel/engine/components/controllable.py2
-rw-r--r--kennel/engine/components/position.py2
-rw-r--r--kennel/engine/entities/entity.py2
-rw-r--r--kennel/engine/systems/network.py104
-rw-r--r--kennel/kennel.py50
-rw-r--r--kennel/main.py42
-rw-r--r--static/src/component.ts8
-rw-r--r--static/src/entity.ts11
-rw-r--r--static/src/event.ts28
-rw-r--r--static/src/main.ts47
-rw-r--r--static/src/mouse_controller.ts54
-rw-r--r--static/src/vector.ts10
-rw-r--r--static/tsconfig.json4
14 files changed, 284 insertions, 82 deletions
diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py
index c8ef2d0..da55138 100644
--- a/kennel/engine/components/component.py
+++ b/kennel/engine/components/component.py
@@ -12,5 +12,5 @@ class Component:
self.component_type = component_type
@abstractmethod
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
pass
diff --git a/kennel/engine/components/controllable.py b/kennel/engine/components/controllable.py
index 25ec835..a9173af 100644
--- a/kennel/engine/components/controllable.py
+++ b/kennel/engine/components/controllable.py
@@ -9,6 +9,6 @@ class Controllable(Component):
def __repr__(self) -> str:
return f"Controllable(by={self.by})"
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
# don't serialize who owns this
return {}
diff --git a/kennel/engine/components/position.py b/kennel/engine/components/position.py
index 2a4da4a..5c5b372 100644
--- a/kennel/engine/components/position.py
+++ b/kennel/engine/components/position.py
@@ -10,5 +10,5 @@ class Position(Component):
def __repr__(self) -> str:
return f"Position(x={self.x}, y={self.y})"
- def dict(self) -> dict:
+ def to_dict(self) -> dict:
return {"x": self.x, "y": self.y}
diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py
index 6623512..1d8c34d 100644
--- a/kennel/engine/entities/entity.py
+++ b/kennel/engine/entities/entity.py
@@ -27,7 +27,7 @@ class Entity:
return {
"entity_type": self.entity_type,
"id": self.id,
- "components": {k: v.dict() for k, v in self.components.items()},
+ "components": {k: v.to_dict() for k, v in self.components.items()},
}
diff --git a/kennel/engine/systems/network.py b/kennel/engine/systems/network.py
index c9696c8..8dbf4cf 100644
--- a/kennel/engine/systems/network.py
+++ b/kennel/engine/systems/network.py
@@ -1,15 +1,17 @@
from enum import Enum
-from kennel.engine.entities.entity import EntityManager
+from kennel.engine.entities.entity import Entity, EntityType, EntityManager
from .system import System, SystemType
from abc import abstractmethod
+from typing import Optional, List
import asyncio
class EventType(str, Enum):
INITIAL_STATE = "INITIAL_STATE"
+ SET_CONTROLLABLE = "SET_CONTROLLABLE"
ENTITY_BORN = "ENTITY_BORN"
- ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
ENTITY_DEATH = "ENTITY_DEATH"
+ ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
class Event:
@@ -17,26 +19,83 @@ class Event:
self.event_type = event_type
self.data = data
- def __str__(self):
+ def __str__(self) -> str:
return f"Event({self.event_type}, {self.data})"
- def dict(self):
+ def to_dict(self) -> dict:
return {"event_type": self.event_type, "data": self.data}
@staticmethod
- def from_dict(data: dict):
+ def from_dict(data: dict) -> Optional["Event"]:
+ if "event_type" not in data or "data" not in data:
+ return
+ event_type = data["event_type"]
+ if event_type == EventType.INITIAL_STATE:
+ return InitialStateEvent(
+ data["data"]["world"]["width"],
+ data["data"]["world"]["height"],
+ data["data"]["entities"],
+ )
+ if event_type == EventType.SET_CONTROLLABLE:
+ return SetControllableEvent(data["data"]["id"], data["data"]["client_id"])
+ if event_type == EventType.ENTITY_BORN:
+ return EntityBornEvent(data["data"])
+ if event_type == EventType.ENTITY_POSITION_UPDATE:
+ return EntityPositionUpdateEvent(
+ data["data"]["id"], data["data"]["position"]
+ )
+ if event_type == EventType.ENTITY_DEATH:
+ return EntityDeathEvent(data["data"]["id"])
+
+ logger.warn(f"Unknown event type: {data['event_type']}")
return Event(EventType(data["event_type"]), data["data"])
-class Publishable:
- @abstractmethod
- async def publish(self, event: Event):
- pass
+class InitialStateEvent(Event):
+ def __init__(self, world_width: int, world_height: int, entities: List[Entity]):
+ self.world_width = world_width
+ self.world_height = world_height
+ self.entities = entities
+ super().__init__(
+ EventType.INITIAL_STATE,
+ {
+ "world": {"width": world_width, "height": world_height},
+ "entities": [entity.to_dict() for entity in entities],
+ },
+ )
+
+
+class SetControllableEvent(Event):
+ def __init__(self, entity_id: str, client_id: str):
+ self.entity_id = entity_id
+ self.client_id = client_id
+ super().__init__(
+ EventType.SET_CONTROLLABLE, {"id": entity_id, "client_id": client_id}
+ )
-class ClientEventTransformer:
+class EntityBornEvent(Event):
+ def __init__(self, entity: Entity):
+ self.entity = entity
+ super().__init__(EventType.ENTITY_BORN, {"entity": entity.to_dict()})
+
+
+class EntityPositionUpdateEvent(Event):
+ def __init__(self, entity_id: str, position: dict):
+ super().__init__(
+ EventType.ENTITY_POSITION_UPDATE,
+ {"id": entity_id, "position": position},
+ )
+
+
+class EntityDeathEvent(Event):
+ def __init__(self, entity_id: str):
+ super().__init__(EventType.ENTITY_DEATH, {"id": entity_id})
+
+
+class Publishable:
@abstractmethod
- def apply(self, event: Event) -> Event:
+ async def publish(self, event: Event):
pass
@@ -47,37 +106,32 @@ class EventProcessor:
class NetworkSystem(System):
- def __init__(
- self,
- event_processor: EventProcessor,
- client_event_transformer: ClientEventTransformer,
- ):
+ def __init__(self, event_processor: EventProcessor):
super().__init__(SystemType.NETWORK)
self.event_processor = event_processor
- self.client_event_transformer = client_event_transformer
self.events = []
+ self.client_events = []
self.clients = {}
async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
- for event in self.events:
+ if len(self.events) + len(self.client_events) == 0:
+ return
+ for event in self.events + self.client_events:
self.event_processor.accept(entity_manager, event)
- client_events = [
- self.client_event_transformer.apply(entity_manager, event)
- for event in self.events
- ]
+ client_sendable = self.events + [event for event, _ in self.client_events]
await asyncio.gather(
*[
client.publish(event)
for client in self.clients.values()
- for event in client_events
+ for event in client_sendable
]
)
self.events = []
+ self.client_events = []
def client_event(self, client_id: str, event: Event) -> None:
- event.data["client_id"] = client_id
- self.events.append(event)
+ self.client_events.append((event, client_id))
def add_event(self, event: Event) -> None:
self.events.append(event)
diff --git a/kennel/kennel.py b/kennel/kennel.py
index 61847a5..bb0438c 100644
--- a/kennel/kennel.py
+++ b/kennel/kennel.py
@@ -6,7 +6,6 @@ from kennel.engine.systems.system import SystemManager
from kennel.engine.systems.network import (
NetworkSystem,
EventProcessor,
- ClientEventTransformer,
Event,
EventType,
)
@@ -21,28 +20,33 @@ system_manager = SystemManager()
class KennelEventProcessor(EventProcessor):
- def accept(self, entity_manager: EntityManager, event: Event) -> None:
+ def accept(
+ self, entity_manager: EntityManager, event: type[Event | tuple[Event, str]]
+ ) -> None:
+ if isinstance(event, tuple):
+ client_event, client_id = event
+ self._process_client_event(entity_manager, client_event, client_id)
+ return
+
+ def _process_client_event(
+ self, entity_manager: EntityManager, event: Event, client_id: str
+ ) -> None:
if event.event_type == EventType.ENTITY_POSITION_UPDATE:
- self._process_entity_position_update(entity_manager, event)
+ self._process_entity_position_update(entity_manager, event, client_id)
def _process_entity_position_update(
- self, entity_manager: EntityManager, event: Event
+ self, entity_manager: EntityManager, event: Event, client_id: str
) -> None:
- entity = entity_manager.get_entity(event.data["entity_id"])
+ entity = entity_manager.get_entity(event.data["id"])
if entity is None:
- logger.error(f"Entity {event.data['entity_id']} does not exist")
- return
- if event.data["client_id"] is None:
- logger.error("Client ID is required for position updates")
+ 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 != event.data["client_id"]:
- logger.error(
- f"Entity {entity} is not controllable by client {event.data['client_id']}"
- )
+ if 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:
@@ -51,30 +55,14 @@ class KennelEventProcessor(EventProcessor):
entity.add_component(position)
-class KennelClientEventTransformer(ClientEventTransformer):
- def apply(self, entity_manager: EntityManager, event: Event) -> Event:
- if event.event_type == EventType.ENTITY_BORN:
- id = event.data["id"]
- entity = entity_manager.get_entity(id)
- if entity is None:
- logger.error(f"Entity {id} does not exist")
- return event
- for component_type in entity.components:
- component = entity.get_component(component_type)
- if component is not None:
- event.data[component_type] = component.dict()
-
- return event
-
-
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
system_manager.add_system(
- NetworkSystem(EventProcessor(), KennelClientEventTransformer()),
+ NetworkSystem(KennelEventProcessor()),
)
kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)
def create_session_controllable_entities(session: str) -> List[Entity]:
- laser = Laser(uuid.uuid4().hex, session)
+ laser = Laser(uuid.uuid4().hex[:10], session)
return [laser]
diff --git a/kennel/main.py b/kennel/main.py
index 6f661cb..fc0fd0e 100644
--- a/kennel/main.py
+++ b/kennel/main.py
@@ -11,7 +11,14 @@ from fastapi import (
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from kennel.engine.systems.system import SystemType
-from kennel.engine.systems.network import Event, Publishable, EventType
+from kennel.engine.systems.network import (
+ Event,
+ Publishable,
+ EventType,
+ InitialStateEvent,
+ SetControllableEvent,
+ EntityBornEvent,
+)
from typing import Annotated, Optional
from kennel.kennel import (
kennel,
@@ -76,7 +83,7 @@ class WebSocketClient(Publishable):
self.websocket = websocket
async def publish(self, event: Event):
- await self.websocket.send_json(event.dict())
+ await self.websocket.send_json([event.event_type, event.data])
@app.websocket("/ws")
@@ -85,19 +92,16 @@ async def websocket_endpoint(
session: Annotated[str, Depends(get_cookie_or_token)],
):
await websocket.accept()
+ client = WebSocketClient(websocket)
logger.info(f"Websocket connection established for session {session}")
- await websocket.send_json(
- {
- "event_type": EventType.INITIAL_STATE,
- "data": {
- "world": {"width": config.WORLD_WIDTH, "height": config.WORLD_HEIGHT},
- "entities": kennel.entity_manager.to_dict(),
- },
- }
+ initial_state = InitialStateEvent(
+ config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict()
)
+ await client.publish(initial_state)
session_entities = create_session_controllable_entities(session)
+ logger.info(f"Creating {len(session_entities)} entities for session {session}")
try:
network_system = system_manager.get_system(SystemType.NETWORK)
if network_system is None:
@@ -106,14 +110,26 @@ async def websocket_endpoint(
for entity in session_entities:
logger.info(f"Adding entity {entity.id} for session {session}")
entity_manager.add_entity(entity)
- network_system.add_event(Event(EventType.ENTITY_BORN, {"id": entity.id}))
+ network_system.add_event(EntityBornEvent(entity))
+
+ set_controllable_event = SetControllableEvent(entity.id, session)
+ await client.publish(set_controllable_event)
network_system.add_client(session, WebSocketClient(websocket))
while True:
message = await websocket.receive_json()
- network_system.client_event(session, Event.from_dict(message))
+ if type(message) is not list:
+ message = [message]
+ events = [Event.from_dict(event) for event in message]
+ if not all([event is not None for event in events]):
+ logger.info(f"Invalid events in: {message}"[:100])
+ continue
+ for event in events:
+ network_system.client_event(session, event)
+ except WebSocketDisconnect as e:
+ logger.info(f"Websocket connection closed by client: {session}")
except Exception as e:
- logger.error(f"WebSocket exception {e}")
+ logger.error("Exception occurred", exc_info=e)
finally:
logger.info("Websocket connection closed")
for entity in session_entities:
diff --git a/static/src/component.ts b/static/src/component.ts
new file mode 100644
index 0000000..d338cad
--- /dev/null
+++ b/static/src/component.ts
@@ -0,0 +1,8 @@
+export enum ComponentType {
+ POSITION = "POSITION",
+ RENDERABLE = "RENDERABLE",
+}
+
+export interface Component {
+ name: string;
+}
diff --git a/static/src/entity.ts b/static/src/entity.ts
new file mode 100644
index 0000000..811d05a
--- /dev/null
+++ b/static/src/entity.ts
@@ -0,0 +1,11 @@
+import type { Component } from "./component";
+
+export enum EntityType {
+ LASER = "LASER",
+}
+
+export interface Entity {
+ entity_type: EntityType;
+ id: string;
+ components: Record<string, Component>;
+}
diff --git a/static/src/event.ts b/static/src/event.ts
new file mode 100644
index 0000000..cd50122
--- /dev/null
+++ b/static/src/event.ts
@@ -0,0 +1,28 @@
+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 8d12587..fc04b72 100644
--- a/static/src/main.ts
+++ b/static/src/main.ts
@@ -1,18 +1,51 @@
import $ from "jquery";
+import { Vec2 } from "./vector";
+import { EventType, type SetControllableEvent, type Event } from "./event";
+import { MouseController } from "./mouse_controller";
$(document).ready(async () => {
- await fetch("/assign", {
+ const session_id = await fetch("/assign", {
credentials: "include",
+ })
+ .then((res) => res.json())
+ .then(({ session }) => session);
+
+ const controllable_entities = new Set<string>();
+ 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);
+ $(document).on("mousemove", (event) => {
+ mouse_controller.move(event.clientX, event.clientY);
});
+ mouse_controller.start();
const ws = new WebSocket("/ws");
- ws.onopen = () => {
- console.log("connected");
- };
+ await new Promise<void>((resolve) => {
+ ws.onopen = () => {
+ console.log("connected");
+ resolve();
+ };
+ });
ws.onmessage = ({ data }) => {
- console.log(JSON.parse(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 = (e) => {
- console.log("disconnected", e);
+ ws.onclose = () => {
+ controllable_entities.clear();
};
});
diff --git a/static/src/mouse_controller.ts b/static/src/mouse_controller.ts
new file mode 100644
index 0000000..b8849b4
--- /dev/null
+++ b/static/src/mouse_controller.ts
@@ -0,0 +1,54 @@
+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;
+
+ constructor(private readonly callback: (new_movement: Vec2) => void) {}
+
+ public start() {
+ if (this.interval_id !== null) {
+ return;
+ }
+ this.interval_id = setInterval(() => {
+ this.publish_movement();
+ }, this.debounce_ms);
+ }
+
+ public stop() {
+ if (this.interval_id === null) {
+ return;
+ }
+ clearInterval(this.interval_id);
+ this.interval_id = null;
+ }
+
+ 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
+ ) {
+ return;
+ }
+ this.publish_movement();
+ }
+
+ private publish_movement() {
+ if (
+ Date.now() - this.last_event_time < this.debounce_ms ||
+ this.movement_queue.length === 0
+ ) {
+ return;
+ }
+
+ this.last_event_time = Date.now();
+ this.callback(this.movement_queue.at(-1)!);
+ this.movement_queue = [];
+ }
+}
diff --git a/static/src/vector.ts b/static/src/vector.ts
new file mode 100644
index 0000000..e7be9fa
--- /dev/null
+++ b/static/src/vector.ts
@@ -0,0 +1,10 @@
+export class Vec2 {
+ constructor(
+ private readonly x: number,
+ private readonly y: number,
+ ) {}
+
+ public distance_to(that: Vec2): number {
+ return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2);
+ }
+}
diff --git a/static/tsconfig.json b/static/tsconfig.json
index 0511b9f..8f3e483 100644
--- a/static/tsconfig.json
+++ b/static/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */