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, | ||||
|     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