game version overlay, working on actors sync

This commit is contained in:
2025-07-16 23:03:48 +02:00
parent 0da6f275c0
commit 4ec3dedd42
31 changed files with 1826 additions and 680 deletions

View File

@@ -1,7 +1,9 @@
using System;
using NUnit.Framework;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using TriInspector;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Netcode;
@@ -122,48 +124,514 @@ namespace RebootKit.Engine.Simulation {
}
}
[Flags]
enum ActorPhysicsFlags : byte {
None = 0,
IsKinematic = 1 << 0,
DisableColliders = 1 << 1,
}
struct ActorCoreStateSnapshot : INetworkSerializable {
public DateTime Timestamp;
// @NOTE: Position, Rotation, and Scale are in local space.
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public bool IsHidden;
public ActorPhysicsFlags Flags;
public ulong MasterActorID;
public FixedString32Bytes MasterSocketName;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref Timestamp);
serializer.SerializeValue(ref Position);
serializer.SerializeValue(ref Rotation);
serializer.SerializeValue(ref Scale);
serializer.SerializeValue(ref IsHidden);
serializer.SerializeValue(ref Flags);
serializer.SerializeValue(ref MasterActorID);
serializer.SerializeValue(ref MasterSocketName);
}
}
///
/// Represents the synchronization mode for actor transforms (and rigidbody).
/// @TODO: Might be a good idea to keep client-side actors rigidbody as kinematic and simulate physics only on the server.
/// IMPORTANT:
/// - Position, Rotation, and Scale are in local space.
/// - Velocity and AngularVelocity are only used if UsingRigidbody is set.
/// - When Actor is mounted to another actor, sync won't happen.
///
[Flags]
public enum ActorTransformSyncMode : byte {
None = 0,
Position = 1 << 0,
Rotation = 1 << 1,
Scale = 1 << 2,
UsingRigidbody = 1 << 3, // @NOTE: If this is set, Position and Rotation will be synced using Rigidbody's position and rotation.
Velocity = 1 << 4, // @NOTE: Velocity is only used if UsingRigidbody is set.
AngularVelocity = 1 << 5 // @NOTE: AngularVelocity is only used if UsingRigidbody is set.
}
public struct ActorTransformSyncData : INetworkSerializable {
public ActorTransformSyncMode SyncMode;
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public Vector3 Velocity;
public Vector3 AngularVelocity;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref SyncMode);
if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
serializer.SerializeValue(ref Position);
}
if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
serializer.SerializeValue(ref Rotation);
}
if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
serializer.SerializeValue(ref Scale);
}
if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) {
serializer.SerializeValue(ref Velocity);
}
if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
serializer.SerializeValue(ref AngularVelocity);
}
}
}
public abstract class Actor : MonoBehaviour {
static readonly Logger s_ActorLogger = new Logger(nameof(Actor));
[field: SerializeField, TriInspector.ReadOnly] public string SourceActorPath { get; internal set; } = "";
[field: SerializeField, ReadOnly] public ulong ActorID { get; internal set; }
[field: SerializeField, Unity.Collections.ReadOnly] public ulong ActorID { get; internal set; }
[NonSerialized] internal IActorData Data;
[SerializeField] string m_ActorName = "";
public string ActorName {
get {
return m_ActorName;
}
}
[SerializeField] internal Rigidbody actorRigidbody;
[InfoBox("If empty, will use GetComponentsInChildren<Collider>() to find colliders.")]
[SerializeField] Collider[] m_OverrideActorColliders;
[SerializeField] bool m_SetKinematicOnMount = true;
[SerializeField] bool m_DisableCollidersOnMount = true;
internal ActorPhysicsFlags PhysicsFlagsBeforeMount = ActorPhysicsFlags.None;
internal ActorPhysicsFlags PhysicsFlags = ActorPhysicsFlags.None;
// @NOTE: Sync won't happen if actor is mounted to another actor.
[SerializeField] internal ActorTransformSyncMode transformSyncMode = ActorTransformSyncMode.None;
[Serializable]
public struct AttachmentSocket {
[MaxLength(32)]
public string socketName;
public Transform root;
public Vector3 localPosition;
public Quaternion localRotation;
}
[SerializeField] AttachmentSocket[] m_AttachmentSockets;
// @NOTE: Master actor is the actor that this actor is attached to, if any.
internal Actor MasterActor;
internal FixedString32Bytes MasterSocketName;
public bool IsDataDirty { get; protected internal set; }
internal ActorsManager Manager;
public bool IsHidden() {
return !gameObject.activeSelf;
}
internal DateTime LastCoreStateSyncTime = DateTime.MinValue;
// @MARK: Callbacks to override in derived classes
protected abstract IActorData CreateActorData();
// Override this method to implement server-side logic
public virtual void ServerTick(float deltaTime) { }
// Override this method to implement client-side logic
public virtual void ClientTick(float deltaTime) { }
// @NOTE: Server-side method to handle actor commands
protected virtual void OnActorCommandServer(ActorCommand actorCommand) { }
// @NOTE: Client-side method to handle actor events
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;
}
Manager.SetActorHidden(ActorID, hidden);
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);
Manager.SynchronizeActorCoreStateWithOther(this);
}
public void MountTo(Actor actor, string slotName) {
if (!RR.IsServer()) {
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);
PhysicsFlagsBeforeMount = PhysicsFlags;
if (m_SetKinematicOnMount) {
PhysicsFlags |= ActorPhysicsFlags.IsKinematic;
}
if (m_DisableCollidersOnMount) {
PhysicsFlags |= ActorPhysicsFlags.DisableColliders;
}
UpdateLocalPhysicsState(PhysicsFlags);
UpdateMountedTransform();
Manager.SynchronizeActorCoreStateWithOther(this);
}
}
public void UnMount() {
if (!RR.IsServer()) {
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();
PhysicsFlags = PhysicsFlagsBeforeMount;
UpdateLocalPhysicsState(PhysicsFlags);
Manager.SynchronizeActorCoreStateWithOther(this);
}
public void SetCollidersEnabled(bool enableColliders) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can enable/disable colliders. Actor: {name} (ID: {ActorID})");
return;
}
if (actorRigidbody is null) {
s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) has no Rigidbody to set colliders on.");
return;
}
if (enableColliders) {
PhysicsFlags &= ~ActorPhysicsFlags.DisableColliders;
} else {
PhysicsFlags |= ActorPhysicsFlags.DisableColliders;
}
UpdateLocalCollidersState(enableColliders);
Manager.SynchronizeActorCoreStateWithOther(this);
}
public void SetKinematic(bool isKinematic) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can set kinematic state. Actor: {name} (ID: {ActorID})");
return;
}
if (actorRigidbody is null) {
s_ActorLogger.Error($"Actor {name} (ID: {ActorID}) has no Rigidbody to set kinematic state on.");
return;
}
if (isKinematic) {
PhysicsFlags |= ActorPhysicsFlags.IsKinematic;
} else {
PhysicsFlags &= ~ActorPhysicsFlags.IsKinematic;
}
actorRigidbody.isKinematic = isKinematic;
Manager.SynchronizeActorCoreStateWithOther(this);
}
// @MARK: Common API
public bool IsHidden() {
return !gameObject.activeSelf;
}
protected void SendActorCommand<TCmdData>(ushort commandID, ref TCmdData commandData)
where TCmdData : struct, ISerializableEntity {
NativeArray<byte> data = DataSerializationUtils.Serialize(commandData);
SendActorCommand(commandID, data);
}
protected void SendActorCommand(ushort commandID, NativeArray<byte> 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,
ClientID = NetworkManager.Singleton.LocalClientId,
CommandID = commandID,
Data = data
};
Manager.SendActorCommandToServerRpc(command);
}
protected void SendActorEvent<TEventData>(ushort eventID, ref TEventData eventData)
where TEventData : struct, ISerializableEntity {
NativeArray<byte> data = DataSerializationUtils.Serialize(eventData);
SendActorEvent(eventID, data);
}
protected void SendActorEvent(ushort eventID, NativeArray<byte> data = default) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})");
return;
}
if (Manager is null) {
s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})");
return;
}
ActorEvent actorEvent = new ActorEvent {
ActorID = ActorID,
ClientID = NetworkManager.Singleton.LocalClientId,
EventID = eventID,
Data = data
};
Manager.SendActorEvent(actorEvent);
}
protected T DataAs<T>() 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) {
foreach (AttachmentSocket attachmentSocket in m_AttachmentSockets) {
if (attachmentSocket.socketName == socketName) {
socket = attachmentSocket;
return true;
}
}
socket = default;
return false;
}
// @MARK: Internal API
internal ActorCoreStateSnapshot GetCoreStateSnapshot() {
ActorCoreStateSnapshot snapshot = new ActorCoreStateSnapshot();
snapshot.Timestamp = DateTime.UtcNow;
snapshot.Position = transform.localPosition;
snapshot.Rotation = transform.localRotation;
snapshot.Scale = transform.localScale;
snapshot.IsHidden = !gameObject.activeSelf;
snapshot.Flags = PhysicsFlags;
snapshot.MasterActorID = MasterActor != null ? MasterActor.ActorID : 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;
PhysicsFlags = 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.IsHidden) {
gameObject.SetActive(false);
} else {
gameObject.SetActive(true);
}
UpdateLocalPhysicsState(PhysicsFlags);
}
internal ActorTransformSyncData GetTransformSyncData() {
ActorTransformSyncData data = new ActorTransformSyncData {
SyncMode = transformSyncMode
};
bool useRigidbody = (data.SyncMode & ActorTransformSyncMode.UsingRigidbody) != 0;
if (useRigidbody && actorRigidbody == null) {
s_ActorLogger
.Error($"Actor {name} (ID: {ActorID.ToString()}) has no Rigidbody to sync transform. Ignoring transform sync.");
data.SyncMode = ActorTransformSyncMode.None;
return data;
}
if ((data.SyncMode & ActorTransformSyncMode.Position) != 0) {
if (useRigidbody) {
data.Position = actorRigidbody.position;
} else {
data.Position = transform.localPosition;
}
}
if ((data.SyncMode & ActorTransformSyncMode.Rotation) != 0) {
if (useRigidbody) {
data.Rotation = actorRigidbody.rotation;
} else {
data.Rotation = transform.localRotation;
}
}
if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) {
data.Scale = transform.localScale;
}
if (useRigidbody && actorRigidbody != null) {
if ((data.SyncMode & ActorTransformSyncMode.Velocity) != 0) {
data.Velocity = actorRigidbody.linearVelocity;
}
if ((data.SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
data.AngularVelocity = actorRigidbody.angularVelocity;
}
}
return data;
}
internal void RestoreTransformState(ActorTransformSyncData data) {
bool useRigidbody = (data.SyncMode & ActorTransformSyncMode.UsingRigidbody) != 0;
if (useRigidbody && actorRigidbody == null) {
s_ActorLogger
.Error($"Actor {name} (ID: {ActorID.ToString()}) has no Rigidbody to restore transform state. Ignoring transform sync.");
return;
}
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 = data.Rotation;
} else {
transform.localRotation = data.Rotation;
}
}
if ((data.SyncMode & ActorTransformSyncMode.Scale) != 0) {
transform.localScale = data.Scale;
}
if (useRigidbody && (data.SyncMode & ActorTransformSyncMode.Velocity) != 0) {
actorRigidbody.linearVelocity = data.Velocity;
}
if (useRigidbody && (data.SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
actorRigidbody.angularVelocity = data.AngularVelocity;
}
}
void UpdateLocalPhysicsState(ActorPhysicsFlags flags) {
if (actorRigidbody == null) {
return;
}
if ((flags & ActorPhysicsFlags.IsKinematic) != 0) {
actorRigidbody.isKinematic = true;
} else {
actorRigidbody.isKinematic = false;
}
bool enableColliders = (flags & ActorPhysicsFlags.DisableColliders) == 0;
UpdateLocalCollidersState(enableColliders);
}
void UpdateLocalCollidersState(bool enable) {
Collider[] colliders = m_OverrideActorColliders.Length > 0 ? m_OverrideActorColliders
: GetComponentsInChildren<Collider>();
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() {
@@ -205,71 +673,8 @@ namespace RebootKit.Engine.Simulation {
OnActorEventClient(actorEvent);
}
protected abstract IActorData CreateActorData();
// Override this method to implement server-side logic
public virtual void ServerTick(float deltaTime) { }
// Override this method to implement client-side logic
public virtual void ClientTick(float deltaTime) { }
// @NOTE: Server-side method to handle actor commands
protected virtual void OnActorCommandServer(ActorCommand actorCommand) { }
// @NOTE: Client-side method to handle actor events
protected virtual void OnActorEventClient(ActorEvent actorEvent) { }
protected void SendActorCommand<TCmdData>(ushort commandID, ref TCmdData commandData)
where TCmdData : struct, ISerializableEntity {
NativeArray<byte> data = DataSerializationUtils.Serialize(commandData);
SendActorCommand(commandID, data);
}
protected void SendActorCommand(ushort commandID, NativeArray<byte> 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,
ClientID = NetworkManager.Singleton.LocalClientId,
CommandID = commandID,
Data = data
};
Manager.SendActorCommandToServerRpc(command);
}
protected void SendActorEvent(ushort eventID, NativeArray<byte> data = default) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})");
return;
}
if (Manager is null) {
s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})");
return;
}
ActorEvent actorEvent = new ActorEvent {
ActorID = ActorID,
ClientID = NetworkManager.Singleton.LocalClientId,
EventID = eventID,
Data = data
};
Manager.SendActorEventToClientsRpc(actorEvent);
}
protected T DataAs<T>() where T : IActorData {
if (Data is T data) {
return data;
}
throw new System.InvalidCastException($"Actor data is not of type {typeof(T).Name}");
}
// @MARK: Unity lifecycle methods
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();