diff --git a/project.godot b/project.godot index f901748..68bf0a6 100644 --- a/project.godot +++ b/project.godot @@ -77,6 +77,16 @@ move_jump={ , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) ] } +interact_place={ +"deadzone": 0.5, +"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null) + ] +} +interact_break={ +"deadzone": 0.5, +"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null) + ] +} [rendering] diff --git a/scene/Block.tscn b/scene/Block.tscn index 385ac4a..b3af89b 100644 --- a/scene/Block.tscn +++ b/scene/Block.tscn @@ -1,16 +1,16 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=4 format=2] [ext_resource path="res://gfx/block.png" type="Texture" id=1] +[ext_resource path="res://src/Block.cs" type="Script" id=2] - -[sub_resource type="RectangleShape2D" id=3] +[sub_resource type="RectangleShape2D" id=1] extents = Vector2( 8, 8 ) [node name="Block" type="StaticBody2D"] -position = Vector2( 0, 96 ) +script = ExtResource( 2 ) [node name="RectangleShape" type="CollisionShape2D" parent="."] -shape = SubResource( 3 ) +shape = SubResource( 1 ) [node name="Sprite" type="Sprite" parent="."] texture = ExtResource( 1 ) diff --git a/scene/GameScene.tscn b/scene/GameScene.tscn index 9d569b4..4b7866a 100644 --- a/scene/GameScene.tscn +++ b/scene/GameScene.tscn @@ -12,10 +12,11 @@ [ext_resource path="res://gfx/background.png" type="Texture" id=10] [ext_resource path="res://src/Background.cs" type="Script" id=11] -[node name="Game" type="Node"] +[node name="Game" type="Node2D"] pause_mode = 2 script = ExtResource( 3 ) -BlockContainerPath = NodePath("Blocks") +CursorPath = NodePath("CursorLayer/Cursor") +BlockContainerPath = NodePath("World/Blocks") BlockScene = ExtResource( 6 ) [node name="Viewport" type="Node" parent="."] @@ -23,7 +24,7 @@ script = ExtResource( 7 ) [node name="Network" type="Node" parent="."] script = ExtResource( 8 ) -PlayerContainerPath = NodePath("../Players") +PlayerContainerPath = NodePath("../World/Players") OtherPlayerScene = ExtResource( 9 ) [node name="Background" type="TextureRect" parent="."] @@ -39,22 +40,25 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="Players" type="Node" parent="."] +[node name="World" type="Node" parent="."] + +[node name="Players" type="Node" parent="World"] pause_mode = 1 -[node name="LocalPlayer" parent="Players" instance=ExtResource( 5 )] -position = Vector2( 0, -2 ) +[node name="LocalPlayer" parent="World/Players" instance=ExtResource( 5 )] -[node name="Blocks" type="Node" parent="."] +[node name="Blocks" type="Node" parent="World"] [node name="HUD" type="CanvasLayer" parent="."] -[node name="Cursor" type="Node2D" parent="HUD"] -z_index = 1 -script = ExtResource( 2 ) - -[node name="Sprite" type="Sprite" parent="HUD/Cursor"] -texture = ExtResource( 4 ) - [node name="EscapeMenu" parent="HUD" instance=ExtResource( 1 )] visible = false + +[node name="CursorLayer" type="CanvasLayer" parent="."] +layer = 2 +follow_viewport_enable = true + +[node name="Cursor" type="Sprite" parent="CursorLayer"] +z_index = 1000 +texture = ExtResource( 4 ) +script = ExtResource( 2 ) diff --git a/scene/LocalPlayer.tscn b/scene/LocalPlayer.tscn index 4566770..ddcd1d0 100644 --- a/scene/LocalPlayer.tscn +++ b/scene/LocalPlayer.tscn @@ -1,11 +1,15 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=4 format=2] [ext_resource path="res://scene/Player.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/CreativeBuilding.cs" type="Script" id=2] [ext_resource path="res://src/LocalPlayer.cs" type="Script" id=3] [node name="LocalPlayer" instance=ExtResource( 1 )] script = ExtResource( 3 ) -[node name="Camera" type="Camera2D" parent="." index="0"] +[node name="Camera" type="Camera2D" parent="." index="3"] pause_mode = 2 current = true + +[node name="CreativeBuilding" type="Node2D" parent="." index="4"] +script = ExtResource( 2 ) diff --git a/scene/Player.tscn b/scene/Player.tscn index 211dac1..af2b46f 100644 --- a/scene/Player.tscn +++ b/scene/Player.tscn @@ -17,10 +17,6 @@ SpritePath = NodePath("Sprite") [node name="CircleShape" type="CollisionShape2D" parent="."] shape = SubResource( 1 ) -[node name="Sprite" type="Sprite" parent="."] -z_index = -5 -texture = ExtResource( 2 ) - [node name="DisplayName" type="Label" parent="."] modulate = Color( 1, 1, 1, 0.501961 ) anchor_left = 0.5 @@ -36,3 +32,7 @@ valign = 1 __meta__ = { "_edit_use_anchors_": false } + +[node name="Sprite" type="Sprite" parent="."] +z_index = -5 +texture = ExtResource( 2 ) diff --git a/src/Background.cs b/src/Background.cs index 800a0db..bf2d65b 100644 --- a/src/Background.cs +++ b/src/Background.cs @@ -4,11 +4,9 @@ public class Background : TextureRect { public override void _Process(float delta) { - var offset = new Vector2(8, 8); - var tileSize = Texture.GetSize(); - var viewportSize = GetViewport().Size; - var cameraPos = LocalPlayer.Instance.GetNode("Camera").GetCameraPosition(); - RectPosition = ((cameraPos - viewportSize / 2) / tileSize).Floor() * tileSize - offset; - RectSize = ((viewportSize + offset) / tileSize + Vector2.One).Ceil() * tileSize; + var offset = new Vector2(8, 8); + var tileSize = Texture.GetSize(); + RectPosition = (-GetViewportTransform().origin / tileSize).Floor() * tileSize - offset; + RectSize = ((GetViewport().Size + offset) / tileSize + Vector2.One).Ceil() * tileSize; } } diff --git a/src/Block.cs b/src/Block.cs new file mode 100644 index 0000000..55518c9 --- /dev/null +++ b/src/Block.cs @@ -0,0 +1,6 @@ +using Godot; + +public class Block : StaticBody2D +{ + // Empty, but useful to find out whether an object is a "block". +} diff --git a/src/BlockPackets.cs b/src/BlockPackets.cs new file mode 100644 index 0000000..d3df67b --- /dev/null +++ b/src/BlockPackets.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using Godot; + +public static class BlockPackets +{ + public static void Register() + { + Network.API.RegisterS2CPacket(OnSpawnBlockPacket); + Network.API.RegisterS2CPacket(OnSpawnBlocksPacket); + Network.API.RegisterS2CPacket(OnDestroyBlockPacket); + } + + private static void OnSpawnBlockPacket(SpawnBlockPacket packet) + { + // Delete any block previously at this position. + Game.Instance.GetBlockAt(packet.Position)?.QueueFree(); + + var block = Game.Instance.BlockScene.Init(); + block.Position = packet.Position; + block.Modulate = packet.Color; + Game.Instance.BlockContainer.AddChild(block); + } + + private static void OnSpawnBlocksPacket(SpawnBlocksPacket packet) + { + Game.Instance.ClearBlocks(); + foreach (var blockInfo in packet.Blocks) { + var block = Game.Instance.BlockScene.Init(); + block.Position = blockInfo.Position; + block.Modulate = blockInfo.Color; + Game.Instance.BlockContainer.AddChild(block); + } + } + + private static void OnDestroyBlockPacket(DestroyBlockPacket packet) + => Game.Instance.GetBlockAt(packet.Position)?.QueueFree(); +} + +public class SpawnBlockPacket +{ + public Vector2 Position { get; } + public Color Color { get; } + public SpawnBlockPacket(Block block) + { Position = block.Position; Color = block.Modulate; } +} + +public class SpawnBlocksPacket +{ + public List Blocks { get; } + public SpawnBlocksPacket() + => Blocks = Game.Instance.BlockContainer.GetChildren().OfType() + .Select(block => new SpawnBlockPacket(block)).ToList(); +} + +public class DestroyBlockPacket +{ + public Vector2 Position { get; } + public DestroyBlockPacket(Block block) + { Position = block.Position; } +} diff --git a/src/CreativeBuilding.cs b/src/CreativeBuilding.cs new file mode 100644 index 0000000..1cfee84 --- /dev/null +++ b/src/CreativeBuilding.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Godot; + +public class CreativeBuilding : Node2D +{ + private enum BuildMode + { + Placing, + Breaking, + } + + private static readonly Vector2[] _neighborPositions = new Vector2[]{ + Vector2.Left*16, Vector2.Right*16, Vector2.Up*16, Vector2.Down*16 }; + + [Export] public int MaxLength { get; set; } = 6; + + private Texture _blockTex; + private Vector2 _startPos; + private Vector2 _direction; + private int _length; + private bool _canBuild; + + private BuildMode? _currentMode = null; + + private IEnumerable BlockPositions => + Enumerable.Range(0, _length + 1).Select(i => _startPos + _direction * (i * 16)); + + public override void _Ready() + { + _blockTex = GD.Load("res://gfx/block.png"); + } + + public override void _Process(float delta) + { + Update(); + + if (EscapeMenu.Instance.Visible || !Game.Cursor.Visible) + { _currentMode = null; return; } + + switch (_currentMode) { + case null: + if (Input.IsActionJustPressed("interact_place")) + if (_canBuild) _currentMode = BuildMode.Placing; + if (Input.IsActionJustPressed("interact_break")) + _currentMode = BuildMode.Breaking; + break; + case BuildMode.Placing: + if (Input.IsActionJustPressed("interact_break")) _currentMode = null; + else if (!Input.IsActionPressed("interact_place")) { + if (_canBuild) + foreach (var pos in BlockPositions) + PlaceBlock(pos); + _currentMode = null; + } + break; + case BuildMode.Breaking: + if (Input.IsActionJustPressed("interact_place")) _currentMode = null; + else if (!Input.IsActionPressed("interact_break")) { + foreach (var pos in BlockPositions) { + var block = Game.Instance.GetBlockAt(pos); + if (block != null) BreakBlock(block); + } + _currentMode = null; + } + break; + } + + if (_currentMode != null) { + var rad90 = Mathf.Deg2Rad(90.0F); + var angle = Mathf.Round(_startPos.AngleToPoint(Game.Cursor.Position) / rad90) * rad90; + _direction = new Vector2(-Mathf.Cos(angle), -Mathf.Sin(angle)); + _length = Math.Min(MaxLength, Mathf.RoundToInt(_startPos.DistanceTo(Game.Cursor.Position) / 16)); + } else { + _startPos = (Game.Cursor.Position / 16).Round() * 16; + _length = 0; + } + + bool IsBlockAt(Vector2 pos) => Game.Instance.GetBlockAt(pos) != null; + _canBuild = !IsBlockAt(_startPos) && _neighborPositions.Any(pos => IsBlockAt(_startPos + pos)); + } + + private Block PlaceBlock(Vector2 position) + { + if (Game.Instance.GetBlockAt(position) != null) return null; + // FIXME: Test if there is a player in the way. + + var block = Game.Instance.BlockScene.Init(); + block.Position = position; + block.Modulate = Game.LocalPlayer.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F)); + Game.Instance.BlockContainer.AddChild(block); + + if (Network.IsMultiplayerReady) { + if (Network.IsServer) Network.API.SendToEveryone(new SpawnBlockPacket(block)); + else Network.API.SendToServer(new PlaceBlockPacket(position)); + } + + return block; + } + + private void BreakBlock(Block block) + { + // FIXME: Use a different (safer) way to check if a block is one of the default ones. + if (block.Modulate.s < 0.5F) return; + + if (Network.IsMultiplayerReady) { + if (Network.IsServer) Network.API.SendToEveryone(new DestroyBlockPacket(block)); + else Network.API.SendToServer(new BreakBlockPacket(block)); + } + + block.QueueFree(); + } + + public override void _Draw() + { + if (!Game.Cursor.Visible) return; + + var green = Color.FromHsv(1.0F / 3, 1.0F, 1.0F, 0.4F); + var red = Color.FromHsv(0.0F, 1.0F, 1.0F, 0.4F); + var black = new Color(0.0F, 0.0F, 0.0F, 0.65F); + + foreach (var pos in BlockPositions) { + var hasBlock = Game.Instance.GetBlockAt(pos) != null; + var color = (_currentMode != BuildMode.Breaking) + ? ((_canBuild && !hasBlock) ? green : red) + : (hasBlock ? black : red); + DrawTexture(_blockTex, ToLocal(pos - _blockTex.GetSize() / 2), color); + } + } + + + + public static void RegisterPackets() + { + Network.API.RegisterC2SPacket(OnPlaceBlockPacket); + Network.API.RegisterC2SPacket(OnBreakBlockPacket); + } + + private class PlaceBlockPacket + { + public Vector2 Position { get; } + public PlaceBlockPacket(Vector2 position) => Position = position; + } + private static void OnPlaceBlockPacket(Player player, PlaceBlockPacket packet) + { + if (Game.Instance.GetBlockAt(packet.Position) != null) return; + var block = Game.Instance.BlockScene.Init(); + block.Position = packet.Position; + block.Modulate = player.Color.Blend(Color.FromHsv(0.0F, 0.0F, GD.Randf(), 0.2F)); + Game.Instance.BlockContainer.AddChild(block); + + Network.API.SendToEveryone(new SpawnBlockPacket(block)); + } + + private class BreakBlockPacket + { + public Vector2 Position { get; } + public BreakBlockPacket(Block block) => Position = block.Position; + } + private static void OnBreakBlockPacket(Player player, BreakBlockPacket packet) + { + var block = Game.Instance.GetBlockAt(packet.Position); + if (block == null) return; + + if (block.Modulate.s < 0.5F) { + // TODO: Respawn the block the client thought it destroyed? + return; + } + // TODO: Further verify whether player can break a block at this position. + + Network.API.SendToEveryoneExcept(player, new DestroyBlockPacket(block)); + block.QueueFree(); + } +} diff --git a/src/Cursor.cs b/src/Cursor.cs index 701490c..4181010 100644 --- a/src/Cursor.cs +++ b/src/Cursor.cs @@ -17,6 +17,6 @@ public class Cursor : Node2D public override void _Process(float delta) { - Position = GetGlobalMousePosition(); + Position = GetGlobalMousePosition() - GetViewport().CanvasTransform.origin; } } diff --git a/src/EscapeMenuAppearance.cs b/src/EscapeMenuAppearance.cs index df828ec..d78127e 100644 --- a/src/EscapeMenuAppearance.cs +++ b/src/EscapeMenuAppearance.cs @@ -19,7 +19,7 @@ public class EscapeMenuAppearance : CenterContainer ColorSlider.Value = GD.Randf(); var color = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F); - LocalPlayer.Instance.Color = ColorPreview.Modulate = color; + Game.LocalPlayer.Color = ColorPreview.Modulate = color; } @@ -46,7 +46,7 @@ public class EscapeMenuAppearance : CenterContainer private void _on_Appearance_visibility_changed() { if (!IsVisibleInTree()) - Player.ChangeAppearance(LocalPlayer.Instance, + Player.ChangeAppearance(Game.LocalPlayer, DisplayName.Text, ColorPreview.Modulate, Network.IsClient); } diff --git a/src/Game.cs b/src/Game.cs index d499fe7..88dbaf7 100644 --- a/src/Game.cs +++ b/src/Game.cs @@ -1,9 +1,16 @@ +using System.Linq; using Godot; +using Godot.Collections; -public class Game : Node +public class Game : Node2D { public static Game Instance { get; private set; } + public static LocalPlayer LocalPlayer { get; internal set; } + public static Cursor Cursor { get; private set; } + + + [Export] public NodePath CursorPath { get; set; } [Export] public NodePath BlockContainerPath { get; set; } [Export] public PackedScene BlockScene { get; set; } @@ -17,6 +24,7 @@ public class Game : Node public override void _Ready() { + Cursor = GetNode(CursorPath); BlockContainer = GetNode(BlockContainerPath); SpawnDefaultBlocks(); } @@ -30,10 +38,15 @@ public class Game : Node public void SpawnDefaultBlocks() { for (var x = -6; x <= 6; x++) { - var block = BlockScene.Init(); + var block = BlockScene.Init(); block.Position = new Vector2(x * 16, 48); block.Modulate = Color.FromHsv(GD.Randf(), 0.1F, 1.0F); BlockContainer.AddChild(block); } } + + // FIXME: Can only be called during _physics_process?! + public Block GetBlockAt(Vector2 position) + => GetWorld2d().DirectSpaceState.IntersectPoint(position).Cast() + .Select(c => c["collider"]).OfType().FirstOrDefault(); } diff --git a/src/LocalPlayer.cs b/src/LocalPlayer.cs index 1064e07..3e688f6 100644 --- a/src/LocalPlayer.cs +++ b/src/LocalPlayer.cs @@ -1,16 +1,15 @@ using System; using Godot; +// TODO: Implement "low jumps" activated by releasing the jump button early. public class LocalPlayer : Player { - public static LocalPlayer Instance { get; private set; } - public TimeSpan JumpEarlyTime { get; } = TimeSpan.FromSeconds(0.2F); public TimeSpan JumpCoyoteTime { get; } = TimeSpan.FromSeconds(0.2F); - [Export] public float Speed { get; set; } = 120; - [Export] public float JumpSpeed { get; set; } = 180; - [Export] public float Gravity { get; set; } = 400; + [Export] public float MovementSpeed { get; set; } = 160; + [Export] public float JumpVelocity { get; set; } = 240; + [Export] public float Gravity { get; set; } = 480; [Export(PropertyHint.Range, "0,1")] public float Friction { get; set; } = 0.1F; @@ -21,8 +20,8 @@ public class LocalPlayer : Player private DateTime? _jumpPressed = null; private DateTime? _lastOnFloor = null; - public override void _EnterTree() => Instance = this; - public override void _ExitTree() => Instance = null; + public override void _EnterTree() => Game.LocalPlayer = this; + public override void _ExitTree() => Game.LocalPlayer = null; public override void _PhysicsProcess(float delta) { @@ -33,7 +32,7 @@ public class LocalPlayer : Player jumpPressed = Input.IsActionJustPressed("move_jump"); } - Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * Speed, Acceleration) + Velocity.x = (moveDir != 0) ? Mathf.Lerp(Velocity.x, moveDir * MovementSpeed, Acceleration) : Mathf.Lerp(Velocity.x, 0, Friction); Velocity.y += Gravity * delta; Velocity = MoveAndSlide(Velocity, Vector2.Up); @@ -43,7 +42,7 @@ public class LocalPlayer : Player if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) && ((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) { - Velocity.y = -JumpSpeed; + Velocity.y = -JumpVelocity; _jumpPressed = null; _lastOnFloor = null; } diff --git a/src/Network.cs b/src/Network.cs index 406ab23..ed6b725 100644 --- a/src/Network.cs +++ b/src/Network.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Godot; public enum NetworkStatus @@ -63,9 +62,9 @@ public class Network : Node API.RegisterC2SPacket(OnClientAuthPacket); API.RegisterS2CPacket(OnSpawnPlayerPacket); - API.RegisterS2CPacket(OnSpawnBlockPacket); - API.RegisterS2CPacket(OnSpawnBlocksPacket); Player.RegisterPackets(); + CreativeBuilding.RegisterPackets(); + BlockPackets.Register(); } // Let NetworkAPI handle receiving of custom packages. @@ -75,7 +74,7 @@ public class Network : Node public void ResetGame() { - LocalPlayer.Instance.NetworkID = -1; + Game.LocalPlayer.NetworkID = -1; // Clear other players. foreach (var player in _playersById.Values) @@ -106,8 +105,8 @@ public class Network : Node if (error != Error.Ok) return error; GetTree().NetworkPeer = peer; - LocalPlayer.Instance.NetworkID = 1; - _playersById.Add(1, LocalPlayer.Instance); + Game.LocalPlayer.NetworkID = 1; + _playersById.Add(1, Game.LocalPlayer); ChangeStatus(NetworkStatus.ServerRunning); return Error.Ok; @@ -143,10 +142,10 @@ public class Network : Node ChangeStatus(NetworkStatus.Authenticating); var id = GetTree().GetNetworkUniqueId(); - LocalPlayer.Instance.NetworkID = id; - _playersById.Add(id, LocalPlayer.Instance); + Game.LocalPlayer.NetworkID = id; + _playersById.Add(id, Game.LocalPlayer); - API.SendToServer(new ClientAuthPacket(LocalPlayer.Instance)); + API.SendToServer(new ClientAuthPacket(Game.LocalPlayer)); } public void DisconnectFromServer() @@ -214,42 +213,13 @@ public class Network : Node private void OnSpawnPlayerPacket(SpawnPlayerPacket packet) { if (packet.NetworkID == LocalNetworkID) { - var player = LocalPlayer.Instance; + var player = Game.LocalPlayer; player.Position = packet.Position; player.Velocity = Vector2.Zero; ChangeStatus(NetworkStatus.ConnectedToServer); } else SpawnOtherPlayer(packet.NetworkID, packet.Position, packet.DisplayName, packet.Color); } - private struct SpawnBlockPacket - { - public Vector2 Position { get; } - public Color Color { get; } - public SpawnBlockPacket(Node2D block) - { Position = block.Position; Color = block.Modulate; } - } - private void OnSpawnBlockPacket(SpawnBlockPacket packet) - { - var block = Game.Instance.BlockScene.Init(); - block.Position = packet.Position; - block.Modulate = packet.Color; - Game.Instance.BlockContainer.AddChild(block); - } - - private class SpawnBlocksPacket - { - public List Blocks { get; } - public SpawnBlocksPacket() - => Blocks = Game.Instance.BlockContainer.GetChildren().OfType() - .Select(block => new SpawnBlockPacket(block)).ToList(); - } - private void OnSpawnBlocksPacket(SpawnBlocksPacket packet) - { - Game.Instance.ClearBlocks(); - foreach (var block in packet.Blocks) - OnSpawnBlockPacket(block); - } - private void OnPeerConnected(int id) { diff --git a/src/Player.cs b/src/Player.cs index 5bb03ba..177eb9a 100644 --- a/src/Player.cs +++ b/src/Player.cs @@ -91,35 +91,35 @@ public class Player : KinematicBody2D, IInitializer private class PositionChangedPacket { - public int ID { get; set; } - public Vector2 Position { get; set; } + public int ID { get; } + public Vector2 Position { get; } public PositionChangedPacket(Player player) { ID = player.NetworkID; Position = player.Position; } } private class DisplayNameChangedPacket { - public int ID { get; set; } - public string DisplayName { get; set; } + public int ID { get; } + public string DisplayName { get; } public DisplayNameChangedPacket(Player player) { ID = player.NetworkID; DisplayName = player.DisplayName; } } private class ColorChangedPacket { - public int ID { get; set; } - public Color Color { get; set; } + public int ID { get; } + public Color Color { get; } public ColorChangedPacket(Player player) { ID = player.NetworkID; Color = player.Color; } } private class MovePacket { - public Vector2 Position { get; set; } + public Vector2 Position { get; } public MovePacket(Vector2 position) => Position = position; } private class ChangeAppearancePacket { - public string DisplayName { get; set; } - public Color Color { get; set; } + public string DisplayName { get; } + public Color Color { get; } public ChangeAppearancePacket(string displayName, Color color) { DisplayName = displayName; Color = color; } }