WIP: ECS / Network System #1
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
from kennel.app import logger
|
from kennel.app import logger
|
||||||
from kennel.engine.entities.entity import Entity, EntityManager
|
from kennel.engine.entities.entity import Entity, EntityManager
|
||||||
|
@ -55,7 +55,9 @@ class Event:
|
||||||
|
|
||||||
|
|
||||||
class InitialStateEvent(Event):
|
class InitialStateEvent(Event):
|
||||||
def __init__(self, world_width: int, world_height: int, entities: List[Entity]):
|
def __init__(
|
||||||
|
self, world_width: int, world_height: int, entities: Dict[str, Entity]
|
||||||
|
):
|
||||||
self.world_width = world_width
|
self.world_width = world_width
|
||||||
self.world_height = world_height
|
self.world_height = world_height
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
|
@ -63,7 +65,7 @@ class InitialStateEvent(Event):
|
||||||
EventType.INITIAL_STATE,
|
EventType.INITIAL_STATE,
|
||||||
{
|
{
|
||||||
"world": {"width": world_width, "height": world_height},
|
"world": {"width": world_width, "height": world_height},
|
||||||
"entities": [entity.to_dict() for entity in entities],
|
"entities": entities,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,51 +100,66 @@ class EntityDeathEvent(Event):
|
||||||
|
|
||||||
class Publishable:
|
class Publishable:
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def publish(self, event: Event):
|
async def publish(self, event: List[Event]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EventProcessor:
|
class UpstreamEventProcessor:
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def accept(
|
def accept(
|
||||||
self, entity_manager: EntityManager, event: Event | tuple[Event, str]
|
self, entity_manager: EntityManager, client_event: tuple[Event, str]
|
||||||
) -> None:
|
) -> Optional[Event]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NetworkSystem(System):
|
class NetworkSystem(System):
|
||||||
def __init__(self, event_processor: EventProcessor):
|
event_processor: UpstreamEventProcessor
|
||||||
super().__init__(SystemType.NETWORK)
|
sever_events: List[Event]
|
||||||
self.event_processor = event_processor
|
client_upstream_events: List[tuple[Event, str]]
|
||||||
|
clients: Dict[str, tuple[Publishable, List[Event]]]
|
||||||
|
|
||||||
|
def __init__(self, upstream_event_processor: UpstreamEventProcessor):
|
||||||
|
super().__init__(SystemType.NETWORK)
|
||||||
|
self.upstream_event_processor = upstream_event_processor
|
||||||
|
|
||||||
|
self.server_events = [] # events to propogate to the entire network
|
||||||
|
self.client_upstream_events = [] # events that come from the clients
|
||||||
|
|
||||||
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:
|
||||||
if len(self.events) + len(self.client_events) == 0:
|
for event in self.client_upstream_events:
|
||||||
return
|
produced_event = self.upstream_event_processor.accept(entity_manager, event)
|
||||||
for event in self.events + self.client_events:
|
if produced_event is not None:
|
||||||
self.event_processor.accept(entity_manager, event)
|
self.server_events.append(produced_event)
|
||||||
client_sendable = self.events + [event for event, _ in self.client_events]
|
promises = [
|
||||||
await asyncio.gather(
|
client.publish(self.server_events + events)
|
||||||
*[
|
for client, events in self.clients.values()
|
||||||
client.publish(event)
|
if len(self.server_events + events) > 0
|
||||||
for client in self.clients.values()
|
|
||||||
for event in client_sendable
|
|
||||||
]
|
]
|
||||||
)
|
await asyncio.gather(*promises)
|
||||||
self.events = []
|
|
||||||
self.client_events = []
|
|
||||||
|
|
||||||
def client_event(self, client_id: str, event: Event) -> None:
|
self.server_events = []
|
||||||
self.client_events.append((event, client_id))
|
self.client_upstream_events = []
|
||||||
|
for client_id in self.clients.keys():
|
||||||
|
(client, _) = self.clients[client_id]
|
||||||
|
self.clients[client_id] = (client, [])
|
||||||
|
|
||||||
def add_event(self, event: Event) -> None:
|
def server_global_event(self, event: Event) -> None:
|
||||||
self.events.append(event)
|
self.server_events.append(event)
|
||||||
|
|
||||||
|
def client_upstream_event(self, client_id: str, event: Event) -> None:
|
||||||
|
self.client_upstream_events.append((event, client_id))
|
||||||
|
|
||||||
|
def client_downstream_event(self, client_id: str, event: Event) -> None:
|
||||||
|
if client_id not in self.clients:
|
||||||
|
logger.info(f"client {client_id} not found")
|
||||||
|
return
|
||||||
|
(client, events) = self.clients[client_id]
|
||||||
|
self.clients[client_id] = (client, events + [event])
|
||||||
|
|
||||||
def add_client(self, client_id: str, client: Publishable) -> None:
|
def add_client(self, client_id: str, client: Publishable) -> None:
|
||||||
self.clients[client_id] = client
|
self.clients[client_id] = (client, [])
|
||||||
|
|
||||||
def remove_client(self, client_id: str) -> None:
|
def remove_client(self, client_id: str) -> None:
|
||||||
del self.clients[client_id]
|
del self.clients[client_id]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
from kennel.config import config
|
from kennel.config import config
|
||||||
from kennel.engine.components.component import ComponentType
|
from kennel.engine.components.component import ComponentType
|
||||||
|
@ -9,7 +9,7 @@ from kennel.engine.game import Game
|
||||||
from kennel.engine.systems.network import (
|
from kennel.engine.systems.network import (
|
||||||
EntityPositionUpdateEvent,
|
EntityPositionUpdateEvent,
|
||||||
Event,
|
Event,
|
||||||
EventProcessor,
|
UpstreamEventProcessor,
|
||||||
EventType,
|
EventType,
|
||||||
NetworkSystem,
|
NetworkSystem,
|
||||||
)
|
)
|
||||||
|
@ -22,27 +22,22 @@ entity_manager = EntityManager()
|
||||||
system_manager = SystemManager()
|
system_manager = SystemManager()
|
||||||
|
|
||||||
|
|
||||||
class KennelEventProcessor(EventProcessor):
|
class KennelEventProcessor(UpstreamEventProcessor):
|
||||||
def accept(
|
def accept(
|
||||||
self, entity_manager: EntityManager, event: Event | tuple[Event, str]
|
self, entity_manager: EntityManager, event: tuple[Event, str]
|
||||||
) -> None:
|
) -> Optional[Event]:
|
||||||
if isinstance(event, tuple):
|
|
||||||
client_event, client_id = event
|
client_event, client_id = event
|
||||||
self._process_client_event(entity_manager, client_event, client_id)
|
if client_event.event_type == EventType.ENTITY_POSITION_UPDATE:
|
||||||
return
|
return self._process_entity_position_update(
|
||||||
|
entity_manager, client_event, client_id
|
||||||
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, client_id)
|
|
||||||
|
|
||||||
def _process_entity_position_update(
|
def _process_entity_position_update(
|
||||||
self,
|
self,
|
||||||
entity_manager: EntityManager,
|
entity_manager: EntityManager,
|
||||||
event: EntityPositionUpdateEvent,
|
event: EntityPositionUpdateEvent,
|
||||||
client_id: str,
|
client_id: str,
|
||||||
) -> None:
|
) -> Optional[Event]:
|
||||||
entity = entity_manager.get_entity(event.data["id"])
|
entity = entity_manager.get_entity(event.data["id"])
|
||||||
if entity is None:
|
if entity is None:
|
||||||
logger.error(f"Entity(id={event.data['id']}) does not exist")
|
logger.error(f"Entity(id={event.data['id']}) does not exist")
|
||||||
|
@ -60,6 +55,7 @@ class KennelEventProcessor(EventProcessor):
|
||||||
|
|
||||||
entity.add_component(position)
|
entity.add_component(position)
|
||||||
entity_manager.add_entity(entity)
|
entity_manager.add_entity(entity)
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
|
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional, List
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
Cookie,
|
Cookie,
|
||||||
|
@ -82,8 +82,8 @@ class WebSocketClient(Publishable):
|
||||||
def __init__(self, websocket: WebSocket):
|
def __init__(self, websocket: WebSocket):
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
async def publish(self, event: Event):
|
async def publish(self, events: List[Event]):
|
||||||
await self.websocket.send_json([event.event_type, event.data])
|
await self.websocket.send_json([event.to_dict() for event in events])
|
||||||
|
|
||||||
|
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
|
@ -95,27 +95,30 @@ async def websocket_endpoint(
|
||||||
client = WebSocketClient(websocket)
|
client = WebSocketClient(websocket)
|
||||||
logger.info(f"Websocket connection established for session {session}")
|
logger.info(f"Websocket connection established for session {session}")
|
||||||
|
|
||||||
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)
|
session_entities = create_session_controllable_entities(session)
|
||||||
logger.info(f"Creating {len(session_entities)} entities for session {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:
|
||||||
raise "Network system not found"
|
raise "Network system not found"
|
||||||
|
network_system.add_client(session, WebSocketClient(websocket))
|
||||||
|
|
||||||
|
network_system.client_downstream_event(
|
||||||
|
session,
|
||||||
|
InitialStateEvent(
|
||||||
|
config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
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(EntityBornEvent(entity))
|
|
||||||
|
|
||||||
set_controllable_event = SetControllableEvent(entity.id, session)
|
network_system.server_global_event(EntityBornEvent(entity))
|
||||||
await client.publish(set_controllable_event)
|
network_system.client_downstream_event(
|
||||||
|
session, SetControllableEvent(entity.id, session)
|
||||||
|
)
|
||||||
|
|
||||||
network_system.add_client(session, WebSocketClient(websocket))
|
|
||||||
while True:
|
while True:
|
||||||
message = await websocket.receive_json()
|
message = await websocket.receive_json()
|
||||||
if not isinstance(message, list):
|
if not isinstance(message, list):
|
||||||
|
@ -125,20 +128,20 @@ async def websocket_endpoint(
|
||||||
logger.info(f"Invalid events in: {message}"[:100])
|
logger.info(f"Invalid events in: {message}"[:100])
|
||||||
continue
|
continue
|
||||||
for event in events:
|
for event in events:
|
||||||
network_system.client_event(session, event)
|
network_system.client_upstream_event(session, event)
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
logger.info(f"Websocket connection closed by client: {session}")
|
logger.info(f"Websocket connection closed by client: {session}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Exception occurred", exc_info=e)
|
logger.error("Exception occurred", exc_info=e)
|
||||||
finally:
|
finally:
|
||||||
logger.info("Websocket connection closed")
|
network_system.remove_client(session)
|
||||||
for entity in session_entities:
|
for entity in session_entities:
|
||||||
logger.info(f"Removing entity {entity.id} for session {session}")
|
logger.info(f"Removing entity {entity.id} for session {session}")
|
||||||
entity_manager.remove_entity(entity.id)
|
entity_manager.remove_entity(entity.id)
|
||||||
network_system.add_event(Event(EventType.ENTITY_DEATH, {"id": entity.id}))
|
network_system.add_event(Event(EventType.ENTITY_DEATH, {"id": entity.id}))
|
||||||
|
|
||||||
network_system.remove_client(session)
|
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
|
logger.info("Websocket connection closed")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/healthcheck")
|
@app.get("/healthcheck")
|
||||||
|
|
|
@ -65,9 +65,11 @@ class KennelClient {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (events.length > 0) {
|
||||||
console.log(events, dt);
|
console.log(events, dt);
|
||||||
|
}
|
||||||
this.event_queue.clear();
|
this.event_queue.clear();
|
||||||
|
this.event_publisher.publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private process_set_controllable_event(event: SetControllableEvent) {
|
private process_set_controllable_event(event: SetControllableEvent) {
|
||||||
|
|
|
@ -7,7 +7,6 @@ export class MouseController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly publisher: (new_movement: Vec2) => void | Promise<void>,
|
private readonly publisher: (new_movement: Vec2) => void | Promise<void>,
|
||||||
private readonly debounce_ms = 200,
|
private readonly debounce_ms = 200,
|
||||||
private readonly l2_norm_threshold = 40,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
|
@ -29,20 +28,14 @@ export class MouseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public move(x: number, y: number) {
|
public move(x: number, y: number) {
|
||||||
const new_movement = new Vec2(x, y);
|
this.last_movement = new Vec2(x, y);
|
||||||
if (
|
|
||||||
typeof this.last_movement !== "undefined" &&
|
|
||||||
new_movement.distance_to(this.last_movement) >= this.l2_norm_threshold
|
|
||||||
) {
|
|
||||||
this.publish_movement();
|
this.publish_movement();
|
||||||
}
|
}
|
||||||
this.last_movement = new_movement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private publish_movement() {
|
private publish_movement() {
|
||||||
if (
|
if (
|
||||||
typeof this.last_movement === "undefined" ||
|
Date.now() - this.last_event_time < this.debounce_ms ||
|
||||||
Date.now() - this.last_event_time < this.debounce_ms
|
typeof this.last_movement === "undefined"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,8 +66,7 @@ export class WebSocketEventQueue implements EventQueue {
|
||||||
|
|
||||||
private listen_to(websocket: WebSocket) {
|
private listen_to(websocket: WebSocket) {
|
||||||
websocket.onmessage = ({ data }) => {
|
websocket.onmessage = ({ data }) => {
|
||||||
const [event_type, event_data] = JSON.parse(data);
|
this.queue = this.queue.concat(JSON.parse(data));
|
||||||
this.queue.push({ event_type, data: event_data } as Event);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +83,9 @@ export class WebsocketEventPublisher implements EventPublisher {
|
||||||
}
|
}
|
||||||
|
|
||||||
public publish() {
|
public publish() {
|
||||||
|
if (this.queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.websocket.send(JSON.stringify(this.queue));
|
this.websocket.send(JSON.stringify(this.queue));
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue