a bit more progress
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
parent
a0a2068b66
commit
d35ab6cc85
|
@ -8,6 +8,8 @@ from kennel.engine.systems.network import (
|
||||||
EventProcessor,
|
EventProcessor,
|
||||||
Event,
|
Event,
|
||||||
EventType,
|
EventType,
|
||||||
|
Entity,
|
||||||
|
EntityPositionUpdateEvent,
|
||||||
)
|
)
|
||||||
from kennel.engine.systems.world import WorldSystem
|
from kennel.engine.systems.world import WorldSystem
|
||||||
from kennel.config import config
|
from kennel.config import config
|
||||||
|
@ -35,24 +37,28 @@ class KennelEventProcessor(EventProcessor):
|
||||||
self._process_entity_position_update(entity_manager, event, client_id)
|
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, client_id: str
|
self,
|
||||||
|
entity_manager: EntityManager,
|
||||||
|
event: EntityPositionUpdateEvent,
|
||||||
|
client_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
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")
|
||||||
return
|
return
|
||||||
controllable = entity.get_component(ComponentType.CONTROLLABLE)
|
controllable = entity.get_component(ComponentType.CONTROLLABLE)
|
||||||
if controllable is None:
|
if controllable is None or controllable.by != client_id:
|
||||||
logger.error(f"Entity {entity} is not controllable")
|
|
||||||
return
|
|
||||||
if controllable.by != client_id:
|
|
||||||
logger.error(f"Entity {entity} is not controllable by client {client_id}")
|
logger.error(f"Entity {entity} is not controllable by client {client_id}")
|
||||||
return
|
return
|
||||||
position = entity.get_component(ComponentType.POSITION)
|
position = entity.get_component(ComponentType.POSITION)
|
||||||
if position is None:
|
if position is None:
|
||||||
logger.error(f"Entity {entity} has no position")
|
logger.error(f"Entity {entity} has no position")
|
||||||
return
|
return
|
||||||
|
position.x = event.data["position"]["x"]
|
||||||
|
position.y = event.data["position"]["y"]
|
||||||
|
|
||||||
entity.add_component(position)
|
entity.add_component(position)
|
||||||
|
entity_manager.add_entity(entity)
|
||||||
|
|
||||||
|
|
||||||
system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
|
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 $ from "jquery";
|
||||||
import { Vec2 } from "./vector";
|
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 { 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", {
|
const session_id = await fetch("/assign", {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(({ session }) => session);
|
.then(({ session }) => session);
|
||||||
|
|
||||||
const controllable_entities = new Set<string>();
|
const ws = new WebSocket("/ws");
|
||||||
const control_callback = (movement: Vec2) => {
|
await new Promise<void>((resolve) => {
|
||||||
for (const id of controllable_entities) {
|
ws.onopen = () => resolve();
|
||||||
const message = JSON.stringify({
|
|
||||||
event_type: EventType.ENTITY_POSITION_UPDATE,
|
|
||||||
data: { id, position: movement },
|
|
||||||
});
|
});
|
||||||
ws.send(message);
|
|
||||||
}
|
const queue: EventQueue = new WebSocketEventQueue(ws);
|
||||||
};
|
|
||||||
const mouse_controller = new MouseController(control_callback);
|
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) => {
|
$(document).on("mousemove", (event) => {
|
||||||
mouse_controller.move(event.clientX, event.clientY);
|
mouse_controller.move(event.clientX, event.clientY);
|
||||||
});
|
});
|
||||||
mouse_controller.start();
|
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";
|
import { Vec2 } from "./vector";
|
||||||
|
|
||||||
export class MouseController {
|
export class MouseController {
|
||||||
private readonly debounce_ms = 400;
|
|
||||||
private readonly movement_threshold = 40;
|
|
||||||
private last_event_time = Date.now();
|
private last_event_time = Date.now();
|
||||||
private movement_queue: Vec2[] = [];
|
private last_movement: Vec2 | undefined;
|
||||||
private interval_id: number | null = null;
|
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() {
|
public start() {
|
||||||
if (this.interval_id !== null) {
|
if (typeof this.interval_id !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.interval_id = setInterval(() => {
|
this.interval_id = setInterval(
|
||||||
this.publish_movement();
|
() => this.publish_movement(),
|
||||||
}, this.debounce_ms);
|
this.debounce_ms,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
|
@ -23,32 +25,30 @@ export class MouseController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearInterval(this.interval_id);
|
clearInterval(this.interval_id);
|
||||||
this.interval_id = null;
|
delete this.interval_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public move(x: number, y: number) {
|
public move(x: number, y: number) {
|
||||||
const new_movement = new Vec2(x, y);
|
const new_movement = new Vec2(x, y);
|
||||||
const last_movement = this.movement_queue.at(-1);
|
|
||||||
this.movement_queue.push(new_movement);
|
|
||||||
if (
|
if (
|
||||||
typeof last_movement === "undefined" ||
|
typeof this.last_movement !== "undefined" &&
|
||||||
new_movement.distance_to(last_movement) < this.movement_threshold
|
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() {
|
private publish_movement() {
|
||||||
if (
|
if (
|
||||||
Date.now() - this.last_event_time < this.debounce_ms ||
|
typeof this.last_movement === "undefined" ||
|
||||||
this.movement_queue.length === 0
|
Date.now() - this.last_event_time < this.debounce_ms
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.last_event_time = Date.now();
|
this.last_event_time = Date.now();
|
||||||
this.callback(this.movement_queue.at(-1)!);
|
this.publisher(this.last_movement.copy());
|
||||||
this.movement_queue = [];
|
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 {
|
public distance_to(that: Vec2): number {
|
||||||
return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2);
|
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