using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using TriInspector; using Unity.Collections; using Unity.Mathematics; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.ResourceManagement.AsyncOperations; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Simulation { public interface IActorData : ISerializableEntity { } public class NoActorData : IActorData { public void Serialize(NetworkBufferWriter writer) { } public void Deserialize(NetworkBufferReader reader) { } public int GetMaxBytes() { return 0; } } public struct ActorCommand : ISerializableEntity { public ushort ActorID; public byte CommandID; public NativeArray Data; public int GetMaxBytes() { return sizeof(ushort) + // ActorID sizeof(byte) + // CommandID sizeof(byte) + // Data length sizeof(byte) * Data.Length; // Data } public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); writer.Write(CommandID); writer.Write((byte) Data.Length); if (Data.IsCreated) { writer.Write(Data); } } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); reader.Read(out CommandID); reader.Read(out byte dataLength); if (dataLength > 0) { reader.Read(out Data, dataLength, Allocator.Temp); } } } public struct ActorEvent : ISerializableEntity { public ushort ActorID; public byte EventID; public NativeArray Data; public int GetMaxBytes() { return sizeof(ushort) + // ActorID sizeof(byte) + // EventID sizeof(byte) + // Data length sizeof(byte) * Data.Length; // Data } public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); writer.Write(EventID); Assert.IsTrue(Data.Length < byte.MaxValue, "Data of ActorEvent is too large to fit in a byte."); writer.Write((byte) Data.Length); if (Data.IsCreated) { writer.Write(Data); } } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); reader.Read(out EventID); reader.Read(out byte dataLength); if (dataLength > 0) { reader.Read(out Data, dataLength, Allocator.Temp); } } } [Flags] enum ActorFlags : byte { None = 0, Hidden = 1 << 0, DisableColliders = 1 << 1, } struct ActorCoreStateSnapshot : ISerializableEntity { public ushort ActorID; public DateTime Timestamp; // @NOTE: Position, Rotation, and Scale are in local space. public Vector3 Position; public Quaternion Rotation; public Vector3 Scale; public ActorFlags Flags; public ushort MasterActorID; public FixedString32Bytes MasterSocketName; public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); writer.Write(Timestamp.Ticks); writer.Write(Position); writer.Write(Rotation); writer.Write(Scale); writer.Write((byte) Flags); writer.Write(MasterActorID); writer.Write(MasterSocketName); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); reader.Read(out long ticks); Timestamp = new DateTime(ticks, DateTimeKind.Utc); reader.Read(out Position); reader.Read(out Rotation); reader.Read(out Scale); reader.Read(out byte flagsByte); Flags = (ActorFlags) flagsByte; reader.Read(out MasterActorID); reader.Read(out MasterSocketName); } public int GetMaxBytes() { return sizeof(ushort) + // ActorID sizeof(long) + // Timestamp sizeof(float) * 3 + // Position sizeof(float) * 4 + // Rotation (Quaternion) sizeof(float) * 3 + // Scale sizeof(byte) + // Flags sizeof(ushort) + // MasterActorID sizeof(byte) * 32; // MasterSocketName } } [Flags] public enum ActorTransformSyncMode : byte { None = 0, Position = 1 << 0, Rotation = 1 << 1, Scale = 1 << 2 } public struct ActorTransformSyncData : ISerializableEntity { public ushort ActorID; public ActorTransformSyncMode SyncMode; public Vector3 Position; public Vector3 Rotation; public Vector3 Scale; public void Serialize(NetworkBufferWriter writer) { writer.Write(ActorID); writer.Write((byte) SyncMode); if ((SyncMode & ActorTransformSyncMode.Position) != 0) { writer.Write(Position); } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { Rotation.x = Mathf.Repeat(Rotation.x, 360.0f); Rotation.y = Mathf.Repeat(Rotation.y, 360.0f); Rotation.z = Mathf.Repeat(Rotation.z, 360.0f); ushort rotX = QuantizationUtility.FloatToUShort(Rotation.x, 0.0f, 360.0f); ushort rotY = QuantizationUtility.FloatToUShort(Rotation.y, 0.0f, 360.0f); ushort rotZ = QuantizationUtility.FloatToUShort(Rotation.z, 0.0f, 360.0f); writer.Write(rotX); writer.Write(rotY); writer.Write(rotZ); } if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { writer.Write(Scale); } } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ActorID); reader.Read(out byte syncModeByte); SyncMode = (ActorTransformSyncMode) syncModeByte; if ((SyncMode & ActorTransformSyncMode.Position) != 0) { reader.Read(out Position); } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { reader.Read(out ushort rotX); reader.Read(out ushort rotY); reader.Read(out ushort rotZ); Rotation.x = QuantizationUtility.UShortToFloat(rotX, 0.0f, 360.0f); Rotation.y = QuantizationUtility.UShortToFloat(rotY, 0.0f, 360.0f); Rotation.z = QuantizationUtility.UShortToFloat(rotZ, 0.0f, 360.0f); } if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { reader.Read(out Vector3 scale); } } public int GetMaxBytes() { int size = sizeof(byte); // SyncMode if ((SyncMode & ActorTransformSyncMode.Position) != 0) { size += sizeof(float) * 3; // Vector3 } if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) { size += sizeof(ushort) * 3; // Vector3 - Euler angles } if ((SyncMode & ActorTransformSyncMode.Scale) != 0) { size += sizeof(float) * 3; // Vector3 } return size; } } [DeclareFoldoutGroup("Actor")] public abstract class Actor : MonoBehaviour { static readonly Logger s_ActorLogger = new Logger(nameof(Actor)); [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public string SourceActorPath { get; internal set; } = ""; [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ulong ActorStaticID { get; internal set; } [field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ushort ActorID { get; internal set; } [NonSerialized] internal IActorData Data; [SerializeField, Group("Actor")] string m_ActorName = ""; public string ActorName { get { return m_ActorName; } } internal bool IsLocalOnly; [SerializeField, Group("Actor")] internal Rigidbody actorRigidbody; [InfoBox("If empty, will use GetComponentsInChildren() to find colliders.")] [SerializeField, Group("Actor")] Collider[] m_OverrideActorColliders; [SerializeField, Group("Actor")] bool m_SetKinematicOnMount = true; [SerializeField, Group("Actor")] bool m_DisableCollidersOnMount = true; internal ActorFlags Flags = ActorFlags.None; // @NOTE: Sync won't happen if actor is mounted to another actor. [SerializeField, Group("Actor")] internal bool syncTransform = true; [SerializeField, Group("Actor")] internal bool syncPosition = true; [SerializeField, Group("Actor")] internal bool syncRotation = true; [SerializeField, Group("Actor")] internal bool syncScale = false; class ActorClientState { public ulong LastSyncTick; public Vector3 Position; public Quaternion Rotation; public Vector3 Scale; } readonly Dictionary m_ClientsStates = new Dictionary(); [Serializable] public struct AttachmentSocket { [MaxLength(32)] public string socketName; public Transform root; public Vector3 localPosition; public Quaternion localRotation; } [SerializeField, Group("Actor")] AttachmentSocket[] m_AttachmentSockets; // @NOTE: Master actor is the actor that this actor is attached to, if any. internal Actor MasterActor; internal FixedString32Bytes MasterSocketName; internal bool IsCoreStateDirty; public bool IsDataDirty { get; protected internal set; } internal ActorsManager Manager; internal AsyncOperationHandle AssetHandle; internal DateTime LastCoreStateSyncTime = DateTime.MinValue; // // @MARK: Unity callbacks // void OnValidate() { if (ActorStaticID == 0) { ActorStaticID = UniqueID.NewULongFromGuid(); } } // // @MARK: Callbacks to override in derived classes // protected abstract IActorData CreateActorData(); // @MARK: Server side public virtual void OnServerTick(float deltaTime) { } protected virtual void OnActorCommandServer(ulong senderID, ActorCommand actorCommand) { } // Override this method to implement client-side logic public virtual void OnClientTick(float deltaTime) { } protected virtual void OnActorEventClient(ActorEvent actorEvent) { } // // @MARK: Server API // public void SetHidden(bool hidden) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can set actor visibility. Actor: {name} (ID: {ActorID})"); return; } bool shouldBeActive = !hidden; if (gameObject.activeSelf == shouldBeActive) { s_ActorLogger.Warning($"Actor {name} (ID: {ActorID}) is already in the desired visibility state: {shouldBeActive.ToString()}"); return; } gameObject.SetActive(shouldBeActive); if (hidden) { Flags |= ActorFlags.Hidden; } else { Flags &= ~ActorFlags.Hidden; } IsCoreStateDirty = true; } public void MountTo(Actor actor, string slotName) { if (!RR.IsServer() && !IsLocalOnly) { s_ActorLogger.Error($"Only the server can mount actors. Actor: {name} (ID: {ActorID})"); return; } if (actor.TryGetAttachmentSocket(slotName, out AttachmentSocket _)) { MasterActor = actor; MasterSocketName = new FixedString32Bytes(slotName); if (m_DisableCollidersOnMount) { Flags |= ActorFlags.DisableColliders; UpdateLocalCollidersState(false); } if (m_SetKinematicOnMount) { actorRigidbody.isKinematic = true; } UpdateMountedTransform(); IsCoreStateDirty = true; } } public void UnMount() { if (!RR.IsServer() && !IsLocalOnly) { s_ActorLogger.Error($"Only the server can unmount actors. Actor: {name} (ID: {ActorID})"); return; } if (MasterActor == null) { s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) is not mounted to any actor."); return; } MasterActor = null; MasterSocketName = default; UpdateMountedTransform(); if (m_DisableCollidersOnMount) { UpdateLocalCollidersState(true); Flags &= ~ActorFlags.DisableColliders; } if (m_SetKinematicOnMount) { actorRigidbody.isKinematic = false; } IsCoreStateDirty = true; } // // @MARK: Common API // public bool IsHidden() { return !gameObject.activeSelf; } protected void SendActorCommand(byte commandID, ref TCmdData commandData) where TCmdData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(commandData); SendActorCommand(commandID, data); } protected void SendActorCommand(byte commandID, NativeArray data = default) { if (Manager is null) { s_ActorLogger.Error($"Cannot send command because Manager is null for actor {name} (ID: {ActorID})"); return; } ActorCommand command = new ActorCommand { ActorID = ActorID, CommandID = commandID, Data = data }; Manager.SendActorCommand(command); } protected void SendActorEvent(byte eventID, ref TEventData eventData) where TEventData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(eventData); SendActorEvent(eventID, data); } protected void SendActorEvent(byte eventID, NativeArray data = default) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})"); return; } if (Manager == null) { s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})"); return; } ActorEvent actorEvent = new ActorEvent { ActorID = ActorID, EventID = eventID, Data = data }; Manager.SendActorEvent(actorEvent); } protected T DataAs() where T : IActorData { if (Data is T data) { return data; } throw new InvalidCastException($"Actor data is not of type {typeof(T).Name}"); } bool TryGetAttachmentSocket(string socketName, out AttachmentSocket socket) { s_ActorLogger.Info("Getting attachment socket: " + socketName); foreach (AttachmentSocket attachmentSocket in m_AttachmentSockets) { if (attachmentSocket.socketName.Equals(socketName, StringComparison.Ordinal)) { socket = attachmentSocket; return true; } } socket = default; return false; } // // @MARK: Internal // internal void InitializeOnClient() { if (actorRigidbody != null) { actorRigidbody.isKinematic = true; } } internal ActorCoreStateSnapshot GetCoreStateSnapshot() { ActorCoreStateSnapshot snapshot = new ActorCoreStateSnapshot(); snapshot.ActorID = ActorID; snapshot.Timestamp = DateTime.UtcNow; snapshot.Position = transform.localPosition; snapshot.Rotation = transform.localRotation; snapshot.Scale = transform.localScale; snapshot.Flags = Flags; snapshot.MasterActorID = MasterActor != null ? MasterActor.ActorID : (ushort)0; if (snapshot.MasterActorID != 0) { snapshot.MasterSocketName = MasterSocketName; } else { snapshot.MasterSocketName = default; } return snapshot; } internal void RestoreCoreState(ActorCoreStateSnapshot snapshot) { if (snapshot.Timestamp < LastCoreStateSyncTime) { s_ActorLogger.Warning($"Received an outdated core state snapshot for actor {name} (ID: {ActorID}). " + $"Current time: {DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)}, Snapshot time: {snapshot.Timestamp}"); return; } LastCoreStateSyncTime = snapshot.Timestamp; Flags = snapshot.Flags; if (snapshot.MasterActorID != 0) { MasterActor = RR.FindSpawnedActor(snapshot.MasterActorID); MasterSocketName = snapshot.MasterSocketName; UpdateMountedTransform(); } else { MasterActor = null; MasterSocketName = default; UpdateMountedTransform(); transform.localPosition = snapshot.Position; transform.localRotation = snapshot.Rotation; transform.localScale = snapshot.Scale; } if ((snapshot.Flags & ActorFlags.Hidden) != 0) { gameObject.SetActive(false); } else { gameObject.SetActive(true); } bool enableColliders = (Flags & ActorFlags.DisableColliders) == 0; UpdateLocalCollidersState(enableColliders); } ActorClientState GetActorClientState(ulong clientID) { if (m_ClientsStates.TryGetValue(clientID, out ActorClientState clientState)) { return clientState; } clientState = new ActorClientState { LastSyncTick = 0, Position = transform.localPosition, Rotation = transform.localRotation, Scale = transform.localScale }; m_ClientsStates[clientID] = clientState; return clientState; } internal ulong GetLastSyncTick(ulong clientID) { ActorClientState actorClientState = GetActorClientState(clientID); return actorClientState.LastSyncTick; } internal void UpdateClientState(ulong clientID, ulong serverTick) { ActorClientState actorClientState = GetActorClientState(clientID); actorClientState.LastSyncTick = serverTick; actorClientState.Position = transform.localPosition; actorClientState.Rotation = transform.localRotation; actorClientState.Scale = transform.localScale; } internal ActorTransformSyncData GetTransformSyncDataForClient(ulong clientID) { ActorTransformSyncData data = new ActorTransformSyncData { ActorID = ActorID, SyncMode = ActorTransformSyncMode.None }; if (!syncTransform || MasterActor != null) { return data; } data.Position = transform.localPosition; data.Rotation = transform.localRotation.eulerAngles; data.Scale = transform.localScale; ActorClientState actorClientState = GetActorClientState(clientID); if (syncPosition && math.distancesq(actorClientState.Position, transform.localPosition) > 0.01f) { data.SyncMode |= ActorTransformSyncMode.Position; } if (syncRotation && Quaternion.Angle(actorClientState.Rotation, transform.localRotation) > 0.01f) { data.SyncMode |= ActorTransformSyncMode.Rotation; } if (syncScale && math.distancesq(actorClientState.Scale, transform.localScale) > 0.01f) { data.SyncMode |= ActorTransformSyncMode.Scale; } return data; } internal void RestoreTransformState(ActorTransformSyncData data) { bool useRigidbody = actorRigidbody != null; if ((data.SyncMode & ActorTransformSyncMode.Position) != 0) { if (useRigidbody) { actorRigidbody.position = data.Position; } else { transform.localPosition = data.Position; } } if ((data.SyncMode & ActorTransformSyncMode.Rotation) != 0) { if (useRigidbody) { actorRigidbody.rotation = Quaternion.Euler(data.Rotation); } else { transform.localRotation = Quaternion.Euler(data.Rotation); } } if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) { transform.localScale = data.Scale; } } void UpdateLocalCollidersState(bool enable) { Collider[] colliders = m_OverrideActorColliders.Length > 0 ? m_OverrideActorColliders : GetComponentsInChildren(); foreach (Collider col in colliders) { col.enabled = enable; } } void UpdateMountedTransform() { if (MasterActor == null) { transform.SetParent(null); return; } if (MasterActor.TryGetAttachmentSocket(MasterSocketName.Value, out AttachmentSocket socket)) { transform.SetParent(socket.root); transform.localPosition = socket.localPosition; transform.localRotation = socket.localRotation; } else { s_ActorLogger .Error($"Failed to update mounted transform: Socket {MasterSocketName} not found on {MasterActor.name}"); } } internal IActorData InternalCreateActorData() { return CreateActorData(); } internal void HandleActorCommand(ulong senderID, ActorCommand actorCommand) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})"); return; } if (Manager is null) { s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})"); return; } if (actorCommand.ActorID != ActorID) { s_ActorLogger .Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}"); return; } OnActorCommandServer(senderID, actorCommand); } internal void HandleActorEvent(ActorEvent actorEvent) { if (Manager is null) { s_ActorLogger.Error($"Cannot handle event because Manager is null for actor {name} (ID: {ActorID})"); return; } if (actorEvent.ActorID != ActorID) { s_ActorLogger .Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}"); return; } OnActorEventClient(actorEvent); } } }