WIP: ECS / Network System #1
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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<string>;
|
||||
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<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);
|
||||
const ws = new WebSocket("/ws");
|
||||
await new Promise<void>((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<void>((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();
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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<void>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue