Keep track of game object by UniqueID

. Add ObjectHolder which does just that
- ObjectHolder is accessible though Game.Objects
- UniqueID replaces "sync ID"
- Move DeSerializer classes to IO folder
main
copygirl 5 years ago
parent e62d7f5ee6
commit 7861e524ab
  1. 4
      src/EscapeMenuMultiplayer.cs
  2. 14
      src/IO/DeSerializer.Impl.cs
  3. 0
      src/IO/DeSerializer.Interfaces.cs
  4. 2
      src/IO/DeSerializerRegistry.cs
  5. 7
      src/IO/Save.cs
  6. 32
      src/Network/Sync.cs
  7. 15
      src/Network/SyncClient.cs
  8. 19
      src/Network/SyncServer.cs
  9. 49
      src/Objects/ObjectHolder.cs
  10. 1
      src/Scenes/Game.cs
  11. 2
      src/Scenes/Server.cs

@ -135,12 +135,15 @@ public class EscapeMenuMultiplayer : Container
if (IntegratedServer != null) { if (IntegratedServer != null) {
IntegratedServer.Server.Stop(); IntegratedServer.Server.Stop();
// TODO: Have a single method to "reset" the state?
IntegratedServer.Server.Objects.Clear();
IntegratedServer.Server.Sync.Clear(); IntegratedServer.Server.Sync.Clear();
IntegratedServer.GetParent().RemoveChild(IntegratedServer); IntegratedServer.GetParent().RemoveChild(IntegratedServer);
IntegratedServer.QueueFree(); IntegratedServer.QueueFree();
IntegratedServer = null; IntegratedServer = null;
client.Disconnect(); client.Disconnect();
client.Objects.Clear();
client.Sync.Clear(); client.Sync.Clear();
} }
@ -156,6 +159,7 @@ public class EscapeMenuMultiplayer : Container
client.Connect(address, port); client.Connect(address, port);
} else { } else {
client.Disconnect(); client.Disconnect();
client.Objects.Clear();
client.Sync.Clear(); client.Sync.Clear();
} }
} }

@ -205,26 +205,26 @@ public class DictionaryDeSerializerGenerator
} }
} }
public class SyncedObjectDeSerializerGenerator public class NodeDeSerializerGenerator
: IDeSerializerGenerator : IDeSerializerGenerator
{ {
public IDeSerializer GenerateFor(Type type) public IDeSerializer GenerateFor(Type type)
{ {
if (!typeof(Node).IsAssignableFrom(type) || (type.GetCustomAttribute<SyncObjectAttribute>() == null)) return null; if (!typeof(Node).IsAssignableFrom(type)) return null;
var deSerializerType = typeof(SyncedObjectDeSerializer<>).MakeGenericType(type); var deSerializerType = typeof(NodeDeSerializer<>).MakeGenericType(type);
return (IDeSerializer)Activator.CreateInstance(deSerializerType); return (IDeSerializer)Activator.CreateInstance(deSerializerType);
} }
private class SyncedObjectDeSerializer<TObj> private class NodeDeSerializer<TObj>
: DeSerializer<TObj> : DeSerializer<TObj>
where TObj : Node where TObj : Node
{ {
public override void Serialize(Game game, BinaryWriter writer, TObj value) public override void Serialize(Game game, BinaryWriter writer, TObj value)
=> writer.Write(game.Sync.GetStatusOrThrow(value).SyncID); => writer.Write(game.Objects.GetSyncID(value).Value);
public override TObj Deserialize(Game game, BinaryReader reader) public override TObj Deserialize(Game game, BinaryReader reader)
{ {
var id = reader.ReadUInt32(); var id = new UniqueID(reader.ReadUInt32());
var value = (TObj)game.Sync.GetStatusOrThrow(id).Object; var value = (TObj)game.Objects.GetNodeByID(id);
if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}"); if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}");
return value; return value;
} }

@ -38,7 +38,7 @@ public static class DeSerializerRegistry
RegisterGenerator(new ArrayDeSerializerGenerator()); RegisterGenerator(new ArrayDeSerializerGenerator());
RegisterGenerator(new CollectionDeSerializerGenerator()); RegisterGenerator(new CollectionDeSerializerGenerator());
RegisterGenerator(new DictionaryDeSerializerGenerator()); RegisterGenerator(new DictionaryDeSerializerGenerator());
RegisterGenerator(new SyncedObjectDeSerializerGenerator()); RegisterGenerator(new NodeDeSerializerGenerator());
} }
public static void Register<T>(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize) public static void Register<T>(Action<BinaryWriter, T> serialize, Func<BinaryReader, T> deserialize)

@ -0,0 +1,7 @@
using System;
[AttributeUsage(AttributeTargets.Property)]
public class SaveAttribute : Attribute
{
}

@ -9,17 +9,17 @@ using Godot;
public class Sync public class Sync
{ {
protected Game Game { get; } protected Game Game { get; }
protected Dictionary<uint, SyncStatus> StatusBySyncID { get; } = new Dictionary<uint, SyncStatus>(); protected Dictionary<UniqueID, SyncStatus> StatusByID { get; } = new Dictionary<UniqueID, SyncStatus>();
protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>(); protected Dictionary<Node, SyncStatus> StatusByObject { get; } = new Dictionary<Node, SyncStatus>();
static Sync() => DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer()); static Sync() => DeSerializerRegistry.Register(new SyncPacketObjectDeSerializer());
public Sync(Game game) => Game = game; public Sync(Game game) => Game = game;
public SyncStatus GetStatusOrNull(uint syncID) public SyncStatus GetStatusOrNull(UniqueID id)
=> StatusBySyncID.TryGetValue(syncID, out var value) ? value : null; => StatusByID.TryGetValue(id, out var value) ? value : null;
public SyncStatus GetStatusOrThrow(uint syncID) public SyncStatus GetStatusOrThrow(UniqueID id)
=> GetStatusOrNull(syncID) ?? throw new Exception( => GetStatusOrNull(id) ?? throw new Exception(
$"No {nameof(SyncStatus)} found for ID {syncID}"); $"No {nameof(SyncStatus)} found for ID {id}");
public SyncStatus GetStatusOrNull(Node obj) public SyncStatus GetStatusOrNull(Node obj)
{ {
@ -39,23 +39,23 @@ public class Sync
node.QueueFree(); node.QueueFree();
} }
StatusByID.Clear();
StatusByObject.Clear(); StatusByObject.Clear();
StatusBySyncID.Clear();
} }
} }
public class SyncStatus public class SyncStatus
{ {
public uint SyncID { get; } public UniqueID ID { get; }
public Node Object { get; } public Node Object { get; }
public SyncObjectInfo Info { get; } public SyncObjectInfo Info { get; }
public int DirtyProperties { get; set; } public int DirtyProperties { get; set; }
public SyncMode Mode { get; set; } public SyncMode Mode { get; set; }
public SyncStatus(uint syncID, Node obj, SyncObjectInfo info) public SyncStatus(UniqueID id, Node obj, SyncObjectInfo info)
{ SyncID = syncID; Object = obj; Info = info; } { ID = id; Object = obj; Info = info; }
} }
public enum SyncMode public enum SyncMode
@ -73,11 +73,11 @@ public class SyncPacket
public class Object public class Object
{ {
public ushort InfoID { get; } public ushort InfoID { get; }
public uint SyncID { get; } public UniqueID ID { get; }
public SyncMode Mode { get; } public SyncMode Mode { get; }
public List<(byte, object)> Values { get; } public List<(byte, object)> Values { get; }
public Object(ushort infoID, uint syncID, SyncMode mode, List<(byte, object)> values) public Object(ushort infoID, UniqueID id, SyncMode mode, List<(byte, object)> values)
{ InfoID = infoID; SyncID = syncID; Mode = mode; Values = values; } { InfoID = infoID; ID = id; Mode = mode; Values = values; }
} }
} }
@ -87,7 +87,7 @@ internal class SyncPacketObjectDeSerializer
public override void Serialize(Game game, BinaryWriter writer, SyncPacket.Object value) public override void Serialize(Game game, BinaryWriter writer, SyncPacket.Object value)
{ {
writer.Write(value.InfoID); writer.Write(value.InfoID);
writer.Write(value.SyncID); writer.Write(value.ID.Value);
writer.Write((byte)value.Mode); writer.Write((byte)value.Mode);
writer.Write((byte)value.Values.Count); writer.Write((byte)value.Values.Count);
@ -103,7 +103,7 @@ internal class SyncPacketObjectDeSerializer
public override SyncPacket.Object Deserialize(Game game, BinaryReader reader) public override SyncPacket.Object Deserialize(Game game, BinaryReader reader)
{ {
var infoID = reader.ReadUInt16(); var infoID = reader.ReadUInt16();
var syncID = reader.ReadUInt32(); var id = new UniqueID(reader.ReadUInt32());
var mode = (SyncMode)reader.ReadByte(); var mode = (SyncMode)reader.ReadByte();
var count = reader.ReadByte(); var count = reader.ReadByte();
@ -124,6 +124,6 @@ internal class SyncPacketObjectDeSerializer
values.Add((propID, deSerializer.Deserialize(game, reader))); values.Add((propID, deSerializer.Deserialize(game, reader)));
} }
return new SyncPacket.Object(infoID, syncID, mode, values); return new SyncPacket.Object(infoID, id, mode, values);
} }
} }

@ -14,25 +14,28 @@ public class SyncClient : Sync
{ {
foreach (var packetObj in packet.Changes) { foreach (var packetObj in packet.Changes) {
var info = SyncRegistry.Get(packetObj.InfoID); var info = SyncRegistry.Get(packetObj.InfoID);
var status = GetStatusOrNull(packetObj.SyncID); var status = GetStatusOrNull(packetObj.ID);
if (status == null) { if (status == null) {
if (packetObj.Mode != SyncMode.Spawn) throw new Exception( if (packetObj.Mode != SyncMode.Spawn) throw new Exception(
$"Unknown synced object {info.Name} (ID {packetObj.SyncID})"); $"Unknown synced object {info.Name} (ID {packetObj.ID})");
var obj = info.Scene.Init<Node>(); var obj = info.Scene.Init<Node>();
status = new SyncStatus(packetObj.SyncID, obj, info); Client.Objects.Add(packetObj.ID, obj);
StatusBySyncID.Add(status.SyncID, status);
status = new SyncStatus(packetObj.ID, obj, info);
StatusByID.Add(status.ID, status);
StatusByObject.Add(status.Object, status); StatusByObject.Add(status.Object, status);
Client.GetNode("World").AddChild(obj); Client.GetNode("World").AddChild(obj);
} else { } else {
if (packetObj.Mode == SyncMode.Spawn) throw new Exception( if (packetObj.Mode == SyncMode.Spawn) throw new Exception(
$"Spawning object {info.Name} with ID {packetObj.SyncID}, but it already exists"); $"Spawning object {info.Name} with ID {packetObj.ID}, but it already exists");
if (info != status.Info) throw new Exception( if (info != status.Info) throw new Exception(
$"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})"); $"Info of synced object being modified doesn't match ({info.Name} != {status.Info.Name})");
if (packetObj.Mode == SyncMode.Destroy) { if (packetObj.Mode == SyncMode.Destroy) {
StatusBySyncID.Remove(status.SyncID); StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object); StatusByObject.Remove(status.Object);
status.Object.GetParent().RemoveChild(status.Object); status.Object.GetParent().RemoveChild(status.Object);

@ -6,7 +6,6 @@ using Godot;
public class SyncServer : Sync public class SyncServer : Sync
{ {
private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>(); private static readonly HashSet<SyncStatus> _dirtyObjects = new HashSet<SyncStatus>();
private static uint _syncIDCounter = 1;
protected Server Server => (Server)Game; protected Server Server => (Server)Game;
@ -16,12 +15,15 @@ public class SyncServer : Sync
public T Spawn<T>() public T Spawn<T>()
where T : Node where T : Node
{ {
var info = SyncRegistry.Get<T>(); var info = SyncRegistry.Get<T>();
var obj = info.Scene.Init<T>(); var obj = info.Scene.Init<T>();
var status = new SyncStatus(_syncIDCounter++, obj, info){ Mode = SyncMode.Spawn }; var id = Server.Objects.Add(obj);
StatusBySyncID.Add(status.SyncID, status);
var status = new SyncStatus(id, obj, info){ Mode = SyncMode.Spawn };
StatusByID.Add(status.ID, status);
StatusByObject.Add(status.Object, status); StatusByObject.Add(status.Object, status);
_dirtyObjects.Add(status); _dirtyObjects.Add(status);
Server.GetNode("World").AddChild(obj); Server.GetNode("World").AddChild(obj);
return obj; return obj;
@ -33,7 +35,7 @@ public class SyncServer : Sync
var status = GetStatusOrThrow(obj); var status = GetStatusOrThrow(obj);
status.Mode = SyncMode.Destroy; status.Mode = SyncMode.Destroy;
StatusBySyncID.Remove(status.SyncID); StatusByID.Remove(status.ID);
StatusByObject.Remove(status.Object); StatusByObject.Remove(status.Object);
_dirtyObjects.Add(status); _dirtyObjects.Add(status);
@ -63,7 +65,7 @@ public class SyncServer : Sync
foreach (var prop in status.Info.PropertiesByID) foreach (var prop in status.Info.PropertiesByID)
if ((status.DirtyProperties & (1 << prop.ID)) != 0) if ((status.DirtyProperties & (1 << prop.ID)) != 0)
values.Add((prop.ID, prop.Getter(status.Object))); values.Add((prop.ID, prop.Getter(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, status.Mode, values)); packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, status.Mode, values));
// If the object has been newly spawned, now is the time to remove the "Spawn" flag. // If the object has been newly spawned, now is the time to remove the "Spawn" flag.
if (status.Mode == SyncMode.Spawn) status.Mode = SyncMode.Default; if (status.Mode == SyncMode.Spawn) status.Mode = SyncMode.Default;
} }
@ -80,7 +82,7 @@ public class SyncServer : Sync
var values = new List<(byte, object)>(); var values = new List<(byte, object)>();
foreach (var prop in status.Info.PropertiesByID) foreach (var prop in status.Info.PropertiesByID)
values.Add((prop.ID, prop.Getter(status.Object))); values.Add((prop.ID, prop.Getter(status.Object)));
packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.SyncID, SyncMode.Spawn, values)); packet.Changes.Add(new SyncPacket.Object(status.Info.ID, status.ID, SyncMode.Spawn, values));
} }
NetworkPackets.Send(server, new []{ networkID }, packet); NetworkPackets.Send(server, new []{ networkID }, packet);
} }
@ -89,6 +91,5 @@ public class SyncServer : Sync
{ {
base.Clear(); base.Clear();
_dirtyObjects.Clear(); _dirtyObjects.Clear();
_syncIDCounter = 1;
} }
} }

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using Godot;
public class ObjectHolder
{
private readonly Dictionary<UniqueID, Node> _nodeByID = new Dictionary<UniqueID, Node>();
private readonly Dictionary<Node, UniqueID> _idByNode = new Dictionary<Node, UniqueID>();
private uint _newIDCounter = 1;
public UniqueID Add(Node obj)
{
UniqueID id;
// Keep going until we find an unused UniqueID.
while (_nodeByID.TryGetValue(id = new UniqueID(_newIDCounter++), out _)) { }
Add(id, obj);
return id;
}
public void Add(UniqueID id, Node obj)
{
_nodeByID.Add(id, obj);
_idByNode.Add(obj, id);
}
public UniqueID GetSyncID(Node obj)
=> _idByNode.TryGetValue(obj, out var value) ? value : throw new Exception(
$"The specified object '{obj}' does not have a UniqueID");
public Node GetNodeByID(UniqueID id)
=> _nodeByID.TryGetValue(id, out var value) ? value : throw new Exception(
$"No object associated with {id}");
public void Clear()
{
_nodeByID.Clear();
_idByNode.Clear();
}
}
public readonly struct UniqueID : IEquatable<UniqueID>
{
public uint Value { get; }
public UniqueID(uint value) => Value = value;
public override bool Equals(object obj) => (obj is UniqueID other) && Equals(other);
public bool Equals(UniqueID other) => Value == other.Value;
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => $"{nameof(UniqueID)}({Value})";
public static bool operator ==(UniqueID left, UniqueID right) => left.Equals(right);
public static bool operator !=(UniqueID left, UniqueID right) => !left.Equals(right);
}

@ -4,6 +4,7 @@ using Godot.Collections;
public abstract class Game : Node2D public abstract class Game : Node2D
{ {
public ObjectHolder Objects { get; } = new ObjectHolder();
public Sync Sync { get; protected set; } public Sync Sync { get; protected set; }
// Using _EnterTree to make sure this code runs before any other. // Using _EnterTree to make sure this code runs before any other.

@ -155,7 +155,7 @@ public readonly struct NetworkID : IEquatable<NetworkID>
public override bool Equals(object obj) => (obj is NetworkID other) && Equals(other); public override bool Equals(object obj) => (obj is NetworkID other) && Equals(other);
public bool Equals(NetworkID other) => Value == other.Value; public bool Equals(NetworkID other) => Value == other.Value;
public override int GetHashCode() => Value.GetHashCode(); public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => $"NetworkID({Value})"; public override string ToString() => $"{nameof(NetworkID)}({Value})";
public static bool operator ==(NetworkID left, NetworkID right) => left.Equals(right); public static bool operator ==(NetworkID left, NetworkID right) => left.Equals(right);
public static bool operator !=(NetworkID left, NetworkID right) => !left.Equals(right); public static bool operator !=(NetworkID left, NetworkID right) => !left.Equals(right);
} }

Loading…
Cancel
Save