WIP: ECS / Network System #1
|
@ -12,5 +12,5 @@ class Component:
|
||||||
self.component_type = component_type
|
self.component_type = component_type
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -9,6 +9,6 @@ class Controllable(Component):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Controllable(by={self.by})"
|
return f"Controllable(by={self.by})"
|
||||||
|
|
||||||
def dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
# don't serialize who owns this
|
# don't serialize who owns this
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -10,5 +10,5 @@ class Position(Component):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Position(x={self.x}, y={self.y})"
|
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}
|
return {"x": self.x, "y": self.y}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Entity:
|
||||||
return {
|
return {
|
||||||
"entity_type": self.entity_type,
|
"entity_type": self.entity_type,
|
||||||
"id": self.id,
|
"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()},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
from enum import Enum
|
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 .system import System, SystemType
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from typing import Optional, List
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
class EventType(str, Enum):
|
class EventType(str, Enum):
|
||||||
INITIAL_STATE = "INITIAL_STATE"
|
INITIAL_STATE = "INITIAL_STATE"
|
||||||
|
SET_CONTROLLABLE = "SET_CONTROLLABLE"
|
||||||
ENTITY_BORN = "ENTITY_BORN"
|
ENTITY_BORN = "ENTITY_BORN"
|
||||||
ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
|
|
||||||
ENTITY_DEATH = "ENTITY_DEATH"
|
ENTITY_DEATH = "ENTITY_DEATH"
|
||||||
|
ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE"
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
class Event:
|
||||||
|
@ -17,29 +19,86 @@ class Event:
|
||||||
self.event_type = event_type
|
self.event_type = event_type
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"Event({self.event_type}, {self.data})"
|
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}
|
return {"event_type": self.event_type, "data": self.data}
|
||||||
|
|
||||||
@staticmethod
|
@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"])
|
return Event(EventType(data["event_type"]), data["data"])
|
||||||
|
|
||||||
|
|
||||||
|
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 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:
|
class Publishable:
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def publish(self, event: Event):
|
async def publish(self, event: Event):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ClientEventTransformer:
|
|
||||||
@abstractmethod
|
|
||||||
def apply(self, event: Event) -> Event:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EventProcessor:
|
class EventProcessor:
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def accept(self, entity_manager: EntityManager, event: Event) -> None:
|
def accept(self, entity_manager: EntityManager, event: Event) -> None:
|
||||||
|
@ -47,37 +106,32 @@ class EventProcessor:
|
||||||
|
|
||||||
|
|
||||||
class NetworkSystem(System):
|
class NetworkSystem(System):
|
||||||
def __init__(
|
def __init__(self, event_processor: EventProcessor):
|
||||||
self,
|
|
||||||
event_processor: EventProcessor,
|
|
||||||
client_event_transformer: ClientEventTransformer,
|
|
||||||
):
|
|
||||||
super().__init__(SystemType.NETWORK)
|
super().__init__(SystemType.NETWORK)
|
||||||
self.event_processor = event_processor
|
self.event_processor = event_processor
|
||||||
self.client_event_transformer = client_event_transformer
|
|
||||||
|
|
||||||
self.events = []
|
self.events = []
|
||||||
|
self.client_events = []
|
||||||
self.clients = {}
|
self.clients = {}
|
||||||
|
|
||||||
async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
|
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)
|
self.event_processor.accept(entity_manager, event)
|
||||||
client_events = [
|
client_sendable = self.events + [event for event, _ in self.client_events]
|
||||||
self.client_event_transformer.apply(entity_manager, event)
|
|
||||||
for event in self.events
|
|
||||||
]
|
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
client.publish(event)
|
client.publish(event)
|
||||||
for client in self.clients.values()
|
for client in self.clients.values()
|
||||||
for event in client_events
|
for event in client_sendable
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.events = []
|
self.events = []
|
||||||
|
self.client_events = []
|
||||||
|
|
||||||
def client_event(self, client_id: str, event: Event) -> None:
|
def client_event(self, client_id: str, event: Event) -> None:
|
||||||
event.data["client_id"] = client_id
|
self.client_events.append((event, client_id))
|
||||||
self.events.append(event)
|
|
||||||
|
|
||||||
def add_event(self, event: Event) -> None:
|
def add_event(self, event: Event) -> None:
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
|
|
|
@ -6,7 +6,6 @@ from kennel.engine.systems.system import SystemManager
|
||||||
from kennel.engine.systems.network import (
|
from kennel.engine.systems.network import (
|
||||||
NetworkSystem,
|
NetworkSystem,
|
||||||
EventProcessor,
|
EventProcessor,
|
||||||
ClientEventTransformer,
|
|
||||||
Event,
|
Event,
|
||||||
EventType,
|
EventType,
|
||||||
)
|
)
|
||||||
|
@ -21,28 +20,33 @@ system_manager = SystemManager()
|
||||||
|
|
||||||
|
|
||||||
class KennelEventProcessor(EventProcessor):
|
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:
|
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(
|
def _process_entity_position_update(
|
||||||
self, entity_manager: EntityManager, event: Event
|
self, entity_manager: EntityManager, event: Event, client_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
entity = entity_manager.get_entity(event.data["entity_id"])
|
entity = entity_manager.get_entity(event.data["id"])
|
||||||
if entity is None:
|
if entity is None:
|
||||||
logger.error(f"Entity {event.data['entity_id']} does not exist")
|
logger.error(f"Entity(id={event.data['id']}) does not exist")
|
||||||
return
|
|
||||||
if event.data["client_id"] is None:
|
|
||||||
logger.error("Client ID is required for position updates")
|
|
||||||
return
|
return
|
||||||
controllable = entity.get_component(ComponentType.CONTROLLABLE)
|
controllable = entity.get_component(ComponentType.CONTROLLABLE)
|
||||||
if controllable is None:
|
if controllable is None:
|
||||||
logger.error(f"Entity {entity} is not controllable")
|
logger.error(f"Entity {entity} is not controllable")
|
||||||
return
|
return
|
||||||
if controllable.by != event.data["client_id"]:
|
if controllable.by != client_id:
|
||||||
logger.error(
|
logger.error(f"Entity {entity} is not controllable by client {client_id}")
|
||||||
f"Entity {entity} is not controllable by client {event.data['client_id']}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
position = entity.get_component(ComponentType.POSITION)
|
position = entity.get_component(ComponentType.POSITION)
|
||||||
if position is None:
|
if position is None:
|
||||||
|
@ -51,30 +55,14 @@ class KennelEventProcessor(EventProcessor):
|
||||||
entity.add_component(position)
|
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(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
|
||||||
system_manager.add_system(
|
system_manager.add_system(
|
||||||
NetworkSystem(EventProcessor(), KennelClientEventTransformer()),
|
NetworkSystem(KennelEventProcessor()),
|
||||||
)
|
)
|
||||||
|
|
||||||
kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)
|
kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP)
|
||||||
|
|
||||||
|
|
||||||
def create_session_controllable_entities(session: str) -> List[Entity]:
|
def create_session_controllable_entities(session: str) -> List[Entity]:
|
||||||
laser = Laser(uuid.uuid4().hex, session)
|
laser = Laser(uuid.uuid4().hex[:10], session)
|
||||||
return [laser]
|
return [laser]
|
||||||
|
|
|
@ -11,7 +11,14 @@ from fastapi import (
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from kennel.engine.systems.system import SystemType
|
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 typing import Annotated, Optional
|
||||||
from kennel.kennel import (
|
from kennel.kennel import (
|
||||||
kennel,
|
kennel,
|
||||||
|
@ -76,7 +83,7 @@ class WebSocketClient(Publishable):
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
async def publish(self, event: Event):
|
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")
|
@app.websocket("/ws")
|
||||||
|
@ -85,19 +92,16 @@ async def websocket_endpoint(
|
||||||
session: Annotated[str, Depends(get_cookie_or_token)],
|
session: Annotated[str, Depends(get_cookie_or_token)],
|
||||||
):
|
):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
client = WebSocketClient(websocket)
|
||||||
logger.info(f"Websocket connection established for session {session}")
|
logger.info(f"Websocket connection established for session {session}")
|
||||||
|
|
||||||
await websocket.send_json(
|
initial_state = InitialStateEvent(
|
||||||
{
|
config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict()
|
||||||
"event_type": EventType.INITIAL_STATE,
|
|
||||||
"data": {
|
|
||||||
"world": {"width": config.WORLD_WIDTH, "height": config.WORLD_HEIGHT},
|
|
||||||
"entities": kennel.entity_manager.to_dict(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
await client.publish(initial_state)
|
||||||
|
|
||||||
session_entities = create_session_controllable_entities(session)
|
session_entities = create_session_controllable_entities(session)
|
||||||
|
logger.info(f"Creating {len(session_entities)} entities for session {session}")
|
||||||
try:
|
try:
|
||||||
network_system = system_manager.get_system(SystemType.NETWORK)
|
network_system = system_manager.get_system(SystemType.NETWORK)
|
||||||
if network_system is None:
|
if network_system is None:
|
||||||
|
@ -106,14 +110,26 @@ async def websocket_endpoint(
|
||||||
for entity in session_entities:
|
for entity in session_entities:
|
||||||
logger.info(f"Adding entity {entity.id} for session {session}")
|
logger.info(f"Adding entity {entity.id} for session {session}")
|
||||||
entity_manager.add_entity(entity)
|
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))
|
network_system.add_client(session, WebSocketClient(websocket))
|
||||||
while True:
|
while True:
|
||||||
message = await websocket.receive_json()
|
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:
|
except Exception as e:
|
||||||
logger.error(f"WebSocket exception {e}")
|
logger.error("Exception occurred", exc_info=e)
|
||||||
finally:
|
finally:
|
||||||
logger.info("Websocket connection closed")
|
logger.info("Websocket connection closed")
|
||||||
for entity in session_entities:
|
for entity in session_entities:
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export enum ComponentType {
|
||||||
|
POSITION = "POSITION",
|
||||||
|
RENDERABLE = "RENDERABLE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,18 +1,51 @@
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
|
import { Vec2 } from "./vector";
|
||||||
|
import { EventType, type SetControllableEvent, type Event } from "./event";
|
||||||
|
import { MouseController } from "./mouse_controller";
|
||||||
|
|
||||||
$(document).ready(async () => {
|
$(document).ready(async () => {
|
||||||
await fetch("/assign", {
|
const session_id = await fetch("/assign", {
|
||||||
credentials: "include",
|
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");
|
const ws = new WebSocket("/ws");
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log("connected");
|
console.log("connected");
|
||||||
};
|
resolve();
|
||||||
ws.onmessage = ({ data }) => {
|
};
|
||||||
console.log(JSON.parse(data));
|
});
|
||||||
};
|
ws.onmessage = ({ data }) => {
|
||||||
ws.onclose = (e) => {
|
const [event_type, event_data] = JSON.parse(data);
|
||||||
console.log("disconnected", e);
|
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();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|
Loading…
Reference in New Issue