From f2c1886cee38737e4b895167ec765219af5aaf1a Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Wed, 16 Nov 2022 01:19:20 +0100 Subject: [PATCH] added frontend event system --- Mine2d/backend/Publisher.cs | 31 ++++- Mine2d/backend/RemoteBackend.cs | 9 +- Mine2d/core/extensions/TypeExtensions.cs | 6 +- Mine2d/engine/system/EventPriority.cs | 12 +- Mine2d/engine/system/EventType.cs | 12 ++ .../system/annotations/EventListener.cs | 15 +++ Mine2d/frontend/EventPublisher.cs | 108 ++++++++++++++++ Mine2d/frontend/EventService.cs | 45 +++++++ Mine2d/frontend/Frontend.cs | 118 +----------------- Mine2d/frontend/events/Exit.cs | 22 ++++ Mine2d/frontend/events/Fullscreen.cs | 23 ++++ Mine2d/frontend/events/PlayerBreakInput.cs | 50 ++++++++ Mine2d/frontend/events/PlayerInput.cs | 86 +++++++++++++ Mine2d/frontend/events/Resize.cs | 20 +++ 14 files changed, 425 insertions(+), 132 deletions(-) create mode 100644 Mine2d/engine/system/EventType.cs create mode 100644 Mine2d/engine/system/annotations/EventListener.cs create mode 100644 Mine2d/frontend/EventPublisher.cs create mode 100644 Mine2d/frontend/EventService.cs create mode 100644 Mine2d/frontend/events/Exit.cs create mode 100644 Mine2d/frontend/events/Fullscreen.cs create mode 100644 Mine2d/frontend/events/PlayerBreakInput.cs create mode 100644 Mine2d/frontend/events/PlayerInput.cs create mode 100644 Mine2d/frontend/events/Resize.cs diff --git a/Mine2d/backend/Publisher.cs b/Mine2d/backend/Publisher.cs index beb520a..058108a 100644 --- a/Mine2d/backend/Publisher.cs +++ b/Mine2d/backend/Publisher.cs @@ -9,6 +9,8 @@ public class Publisher { private readonly Dictionary> subscribers = new(); + private readonly HashSet clientOnlySubscriptions = new(); + private readonly HashSet serverSubscriptions = new(); private readonly InteractorKind kind; public Publisher(InteractorKind kind) @@ -24,20 +26,28 @@ public class Publisher .GetTypesSafe(); foreach (var type in types) { - var attrs = type.GetCustomAttributes(typeof(InteractorAttribute), false); - if (attrs.Length == 0) + var classAttrs = type.GetCustomAttributes(typeof(InteractorAttribute), false); + if (classAttrs.Length == 0) { continue; } var methods = type.GetMethods(); foreach (var method in methods) { - var attrs2 = method.GetCustomAttributes(typeof(InteractionAttribute), false); - if (attrs2.Length == 0) + var methodAttrs = method.GetCustomAttributes(typeof(InteractionAttribute), false); + if (methodAttrs.Length == 0) { continue; } - var attr = (InteractionAttribute)attrs2[0]; + var attr = (InteractionAttribute)methodAttrs[0]; + if (attr.Kind is InteractorKind.Server or InteractorKind.Hybrid) + { + this.serverSubscriptions.Add(attr.Type); + } + if (attr.Kind is InteractorKind.Client) + { + this.clientOnlySubscriptions.Add(attr.Type); + } if (attr.Kind != this.kind && this.kind != InteractorKind.Hybrid) { continue; @@ -51,6 +61,7 @@ public class Publisher this.Subscribe(attr.Type, del); } } + this.clientOnlySubscriptions.ExceptWith(this.serverSubscriptions); } private void Subscribe(string type, Delegate callback) @@ -100,4 +111,14 @@ public class Publisher } } } + + public bool IsClientOnlyPacket(string type) + { + return this.clientOnlySubscriptions.Contains(type); + } + + public bool IsServerPacket(string type) + { + return this.serverSubscriptions.Contains(type); + } } diff --git a/Mine2d/backend/RemoteBackend.cs b/Mine2d/backend/RemoteBackend.cs index 98e40fc..fb5513a 100644 --- a/Mine2d/backend/RemoteBackend.cs +++ b/Mine2d/backend/RemoteBackend.cs @@ -1,5 +1,6 @@ using System.Text; using mine2d.backend.data; +using mine2d.engine; using mine2d.engine.system.annotations; using mine2d.state; using Newtonsoft.Json; @@ -7,7 +8,7 @@ using WatsonTcp; namespace mine2d.backend; -class RemoteBackend : IBackend +public class RemoteBackend : IBackend { private WatsonTcpClient client; private Publisher publisher; @@ -27,6 +28,10 @@ class RemoteBackend : IBackend public void ProcessPacket(ValueType packet) { this.publisher.Publish(packet); + if (this.publisher.IsClientOnlyPacket(PacketUtils.GetType(packet))) + { + return; + } var json = JsonConvert.SerializeObject(packet); var bytes = Encoding.UTF8.GetBytes(json); this.client.Send(bytes); @@ -53,4 +58,4 @@ class RemoteBackend : IBackend }; this.client.Connect(); } -} \ No newline at end of file +} diff --git a/Mine2d/core/extensions/TypeExtensions.cs b/Mine2d/core/extensions/TypeExtensions.cs index 0139bc3..8491fd7 100644 --- a/Mine2d/core/extensions/TypeExtensions.cs +++ b/Mine2d/core/extensions/TypeExtensions.cs @@ -16,8 +16,8 @@ public static class TypeExtensions { var missingTypes = e.Types .Where(t => typeArguments.Contains(t) && t != null); - - throw new Exception($"Failed to make generic type {type} with arguments {string.Join(", ", missingTypes)}", e); + + throw new ArgumentException($"Failed to make generic type {type} with arguments {string.Join(", ", missingTypes)}", e); } } -} \ No newline at end of file +} diff --git a/Mine2d/engine/system/EventPriority.cs b/Mine2d/engine/system/EventPriority.cs index 8ca89ce..e33be9f 100644 --- a/Mine2d/engine/system/EventPriority.cs +++ b/Mine2d/engine/system/EventPriority.cs @@ -2,10 +2,10 @@ namespace mine2d.engine.system; public enum EventPriority { - Lowest = 0, - Low = 1, - Normal = 2, - High = 3, - Highest = 4, - Important = 5, + Lowest = -2, + Low = -1, + Normal = 0, + High = 1, + Highest = 2, + Important = 3, } diff --git a/Mine2d/engine/system/EventType.cs b/Mine2d/engine/system/EventType.cs new file mode 100644 index 0000000..95546f7 --- /dev/null +++ b/Mine2d/engine/system/EventType.cs @@ -0,0 +1,12 @@ +namespace Mine2d.engine.system; + +public enum EventType +{ + Quit, + MouseMotion, + MouseButtonDown, + MouseButtonUp, + KeyDown, + KeyUp, + WindowResize, +} diff --git a/Mine2d/engine/system/annotations/EventListener.cs b/Mine2d/engine/system/annotations/EventListener.cs new file mode 100644 index 0000000..e44dffb --- /dev/null +++ b/Mine2d/engine/system/annotations/EventListener.cs @@ -0,0 +1,15 @@ +using mine2d.engine.system; + +namespace Mine2d.engine.system.annotations; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class EventListenerAttribute : Attribute +{ + public EventType Type { get; } + public EventPriority Priority { get; set; } + + public EventListenerAttribute(EventType type) + { + this.Type = type; + } +} diff --git a/Mine2d/frontend/EventPublisher.cs b/Mine2d/frontend/EventPublisher.cs new file mode 100644 index 0000000..21adba2 --- /dev/null +++ b/Mine2d/frontend/EventPublisher.cs @@ -0,0 +1,108 @@ +using mine2d.core.extensions; +using mine2d.engine.system; +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend; + +internal struct EventAction +{ + public EventPriority priority; + public Delegate del; +} + +public class EventPublisher +{ + private readonly Dictionary> eventListeners = new(); + + public EventPublisher() + { + this.Scan(); + } + + public void Scan() + { + var types = this.GetType().Assembly + .GetTypesSafe() + .Where(t => t.Namespace != null && t.Namespace.StartsWith("Mine2d.frontend.events", StringComparison.Ordinal)); + foreach (var type in types) + { + var methods = type.GetMethods() + .Where(m => m.GetCustomAttributes(typeof(EventListenerAttribute), false).Length > 0); + foreach (var method in methods) + { + var attributes = method.GetCustomAttributes(typeof(EventListenerAttribute), false); + foreach (var eventType in from EventListenerAttribute attribute in attributes + let eventType = attribute.Type + select eventType) + { + if (!this.eventListeners.ContainsKey(eventType)) + { + this.eventListeners.Add(eventType, new List()); + } + + var del = method.GetParameters().Length == 0 ? + Delegate.CreateDelegate(typeof(Action), method) : + Delegate.CreateDelegate( + typeof(Action<>).MakeGenericTypeSafely(method.GetParameters()[0].ParameterType), + method + ); + this.eventListeners[eventType].Add(new EventAction + { + priority = ((EventListenerAttribute)attributes[0]).Priority, + del = del + }); + } + } + } + + foreach (var (_, value) in this.eventListeners) + { + value.Sort((a, b) => a.priority.CompareTo(b.priority)); + } + } + + public void Dump() + { + foreach (var eventType in this.eventListeners.Keys) + { + Console.WriteLine(eventType); + foreach (var action in this.eventListeners[eventType]) + { + Console.WriteLine(action.del.Method.Name); + } + } + } + + public void Publish(EventType eventType, SDL_Event e) + { + if (this.eventListeners.ContainsKey(eventType)) + { + try + { + foreach (var action in this.eventListeners[eventType]) + { + if (action.del.Method.GetParameters().Length == 0) + { + action.del.DynamicInvoke(); + } + else + { + action.del.DynamicInvoke(e); + } + } + } + catch (CancelEventException) + { + + } + } + } +} + +public class CancelEventException : Exception +{ + public CancelEventException(string message) : base(message) + { + } +} diff --git a/Mine2d/frontend/EventService.cs b/Mine2d/frontend/EventService.cs new file mode 100644 index 0000000..073ed5a --- /dev/null +++ b/Mine2d/frontend/EventService.cs @@ -0,0 +1,45 @@ +using mine2d; +using mine2d.backend.data; +using Mine2d.engine.system; + +namespace Mine2d.frontend; + +public class EventService +{ + private static readonly EventPublisher eventPublisher = new(); + + public static void PollEvents() + { + while (SDL_PollEvent(out var e) != 0) + { + if (e.type == SDL_EventType.SDL_QUIT) + { + eventPublisher.Publish(EventType.Quit, e); + } + if (e.type == SDL_EventType.SDL_WINDOWEVENT && e.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED) + { + eventPublisher.Publish(EventType.WindowResize, e); + } + if (e.type == SDL_EventType.SDL_MOUSEMOTION) + { + eventPublisher.Publish(EventType.MouseMotion, e); + } + if (e.type == SDL_EventType.SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) + { + eventPublisher.Publish(EventType.MouseButtonDown, e); + } + if (e.type == SDL_EventType.SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) + { + eventPublisher.Publish(EventType.MouseButtonUp, e); + } + if (e.type == SDL_EventType.SDL_KEYDOWN) + { + eventPublisher.Publish(EventType.KeyDown, e); + } + if (e.type == SDL_EventType.SDL_KEYUP) + { + eventPublisher.Publish(EventType.KeyUp, e); + } + } + } +} diff --git a/Mine2d/frontend/Frontend.cs b/Mine2d/frontend/Frontend.cs index 2d2b07e..ecedb47 100644 --- a/Mine2d/frontend/Frontend.cs +++ b/Mine2d/frontend/Frontend.cs @@ -1,6 +1,7 @@ using mine2d.backend.data; using mine2d.core; using mine2d.frontend.renderer; +using Mine2d.frontend; namespace mine2d.frontend; @@ -23,122 +24,7 @@ public class Frontend : IFrontend public void Process() { var ctx = Context.Get(); - while (SDL_PollEvent(out var e) != 0) - { - if (e.type == SDL_EventType.SDL_QUIT) - { - Environment.Exit(0); - } - if (e.type == SDL_EventType.SDL_WINDOWEVENT) - { - if (e.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED) - { - ctx.FrontendGameState.WindowWidth = e.window.data1; - ctx.FrontendGameState.WindowHeight = e.window.data2; - Console.WriteLine($"Window resized to {e.window.data1}x{e.window.data2}"); - var player = ctx.GameState.Players.Find( - p => p.Id == ctx.FrontendGameState.PlayerGuid - ); - ctx.FrontendGameState.Camera.CenterOn(player.Position); - } - } - if (e.type == SDL_EventType.SDL_MOUSEMOTION) - { - var mousePos = new Vector2(e.motion.x, e.motion.y); - ctx.FrontendGameState.MousePosition = mousePos; - if (ctx.GameState.Players.Find(player => player.Id == ctx.FrontendGameState.PlayerGuid)?.Mining != Vector2.Zero) - { - var amp = ctx.FrontendGameState.MousePosition / ctx.FrontendGameState.Settings.GameScale + ctx.FrontendGameState.Camera.Position; - ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, amp)); - } - } - if (e.type == SDL_EventType.SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) - { - var amp = ctx.FrontendGameState.MousePosition / ctx.FrontendGameState.Settings.GameScale + ctx.FrontendGameState.Camera.Position; - ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, amp)); - } - if (e.type == SDL_EventType.SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) - { - ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, Vector2.Zero)); - } - if (e.type == SDL_EventType.SDL_KEYDOWN && e.key.repeat == 0) - { - var movementInput = ctx.FrontendGameState.MovementInput; - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_F11) - { - _ = SDL_SetWindowFullscreen( - ctx.Window.GetRaw(), - ctx.FrontendGameState.Settings.Fullscreen ? 0 : (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP - ); - ctx.FrontendGameState.Settings.Fullscreen = !ctx.FrontendGameState.Settings.Fullscreen; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) - { - movementInput.X -= 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) - { - movementInput.X += 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) - { - movementInput.Y -= 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) - { - movementInput.Y += 1; - } - ctx.FrontendGameState.MovementInput = movementInput; - } - if (e.type == SDL_EventType.SDL_KEYUP && e.key.repeat == 0) - { - var movementInput = ctx.FrontendGameState.MovementInput; - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) - { - movementInput.X += 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) - { - movementInput.X -= 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) - { - movementInput.Y += 1; - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) - { - movementInput.Y -= 1; - } - ctx.FrontendGameState.MovementInput = movementInput; - } - if ( - e.key.keysym.scancode - is SDL_Scancode.SDL_SCANCODE_A - or SDL_Scancode.SDL_SCANCODE_D - or SDL_Scancode.SDL_SCANCODE_W - or SDL_Scancode.SDL_SCANCODE_S - && e.key.repeat == 0 - && e.type is SDL_EventType.SDL_KEYDOWN or SDL_EventType.SDL_KEYUP - ) - { - if (e.key.repeat == 1) - { - continue; - } - - var movement = ctx.FrontendGameState.MovementInput; - if (movement.Length() > 0) - { - movement = Vector2.Normalize(movement); - } - - ctx.Backend.ProcessPacket(new MovePacket(ctx.FrontendGameState.PlayerName, movement)); - } - if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_ESCAPE) - { - Environment.Exit(0); - } - } + EventService.PollEvents(); ctx.Renderer.Clear(); var scale = ctx.FrontendGameState.Settings.GameScale; diff --git a/Mine2d/frontend/events/Exit.cs b/Mine2d/frontend/events/Exit.cs new file mode 100644 index 0000000..9b24108 --- /dev/null +++ b/Mine2d/frontend/events/Exit.cs @@ -0,0 +1,22 @@ +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend.events; + +public class Exit +{ + [EventListener(EventType.Quit)] + public static void onExit() + { + Environment.Exit(0); + } + + [EventListener(EventType.KeyDown)] + public static void onKeyDown(SDL_Event e) + { + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_ESCAPE) + { + Environment.Exit(0); + } + } +} diff --git a/Mine2d/frontend/events/Fullscreen.cs b/Mine2d/frontend/events/Fullscreen.cs new file mode 100644 index 0000000..7ad002f --- /dev/null +++ b/Mine2d/frontend/events/Fullscreen.cs @@ -0,0 +1,23 @@ +using mine2d; +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend.events; + +public class Fullscreen +{ + [EventListener(EventType.KeyDown)] + public static void onToggleFullscreen(SDL_Event e) + { + if (e.key.keysym.scancode != SDL_Scancode.SDL_SCANCODE_F11) + { + return; + } + + var ctx = Context.Get(); + _ = SDL_SetWindowFullscreen( + ctx.Window.GetRaw(), + ctx.FrontendGameState.Settings.Fullscreen ? 0 : (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP); + ctx.FrontendGameState.Settings.Fullscreen = !ctx.FrontendGameState.Settings.Fullscreen; + } +} diff --git a/Mine2d/frontend/events/PlayerBreakInput.cs b/Mine2d/frontend/events/PlayerBreakInput.cs new file mode 100644 index 0000000..c75886e --- /dev/null +++ b/Mine2d/frontend/events/PlayerBreakInput.cs @@ -0,0 +1,50 @@ +using mine2d; +using mine2d.backend.data; +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend.events; + +public class PlayerBreakInput +{ + [EventListener(EventType.MouseMotion)] + public static void onBreak(SDL_Event e) + { + var ctx = Context.Get(); + var mousePos = new Vector2(e.motion.x, e.motion.y); + ctx.FrontendGameState.MousePosition = mousePos; + if (ctx.GameState.Players.Find(player => player.Id == ctx.FrontendGameState.PlayerGuid)?.Mining + != Vector2.Zero) + { + var amp = ctx.FrontendGameState.MousePosition + / ctx.FrontendGameState.Settings.GameScale + + ctx.FrontendGameState.Camera.Position; + ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, amp)); + } + } + + [EventListener(EventType.MouseButtonDown)] + public static void onMouseDown(SDL_Event e) + { + if (e.button.button != SDL_BUTTON_LEFT) + { + return; + } + + var ctx = Context.Get(); + var amp = ctx.FrontendGameState.MousePosition / ctx.FrontendGameState.Settings.GameScale + ctx.FrontendGameState.Camera.Position; + ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, amp)); + } + + [EventListener(EventType.MouseButtonUp)] + public static void onMouseUp(SDL_Event e) + { + if (e.button.button != SDL_BUTTON_LEFT) + { + return; + } + + var ctx = Context.Get(); + ctx.Backend.ProcessPacket(new BreakPacket(ctx.FrontendGameState.PlayerGuid, Vector2.Zero)); + } +} diff --git a/Mine2d/frontend/events/PlayerInput.cs b/Mine2d/frontend/events/PlayerInput.cs new file mode 100644 index 0000000..5c9078f --- /dev/null +++ b/Mine2d/frontend/events/PlayerInput.cs @@ -0,0 +1,86 @@ +using mine2d; +using mine2d.backend.data; +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend.events; + +public class PlayerInput +{ + [EventListener(EventType.KeyDown)] + public static void move(SDL_Event e) + { + var ctx = Context.Get(); + if (!isMovementKey(e.key.keysym.scancode) || e.key.repeat == 1) + { + return; + } + var movementInput = ctx.FrontendGameState.MovementInput; + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) + { + movementInput.X -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) + { + movementInput.X += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) + { + movementInput.Y -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) + { + movementInput.Y += 1; + } + ctx.FrontendGameState.MovementInput = movementInput; + sendMovement(); + } + + [EventListener(EventType.KeyUp)] + public static void stopMove(SDL_Event e) + { + var ctx = Context.Get(); + if (!isMovementKey(e.key.keysym.scancode) || e.key.repeat == 1) + { + return; + } + var movementInput = ctx.FrontendGameState.MovementInput; + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_A) + { + movementInput.X += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_D) + { + movementInput.X -= 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_W) + { + movementInput.Y += 1; + } + if (e.key.keysym.scancode == SDL_Scancode.SDL_SCANCODE_S) + { + movementInput.Y -= 1; + } + ctx.FrontendGameState.MovementInput = movementInput; + sendMovement(); + } + + private static bool isMovementKey(SDL_Scancode scancode) + { + return scancode is SDL_Scancode.SDL_SCANCODE_A + or SDL_Scancode.SDL_SCANCODE_D + or SDL_Scancode.SDL_SCANCODE_W + or SDL_Scancode.SDL_SCANCODE_S; + } + + private static void sendMovement() + { + var ctx = Context.Get(); + var movement = ctx.FrontendGameState.MovementInput; + if (movement.Length() > 0) + { + movement = Vector2.Normalize(movement); + } + ctx.Backend.ProcessPacket(new MovePacket(ctx.FrontendGameState.PlayerName, movement)); + } +} diff --git a/Mine2d/frontend/events/Resize.cs b/Mine2d/frontend/events/Resize.cs new file mode 100644 index 0000000..a71ac43 --- /dev/null +++ b/Mine2d/frontend/events/Resize.cs @@ -0,0 +1,20 @@ +using mine2d; +using Mine2d.engine.system; +using Mine2d.engine.system.annotations; + +namespace Mine2d.frontend.events; + +public class Resize +{ + [EventListener(EventType.WindowResize)] + public static void onResize(SDL_Event e) + { + var ctx = Context.Get(); + ctx.FrontendGameState.WindowWidth = e.window.data1; + ctx.FrontendGameState.WindowHeight = e.window.data2; + var player = ctx.GameState.Players.Find( + p => p.Id == ctx.FrontendGameState.PlayerGuid + ); + ctx.FrontendGameState.Camera.CenterOn(player.Position); + } +}