game version overlay, working on actors sync
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Foundation;
|
||||
using RebootKit.Engine.Main;
|
||||
using Unity.Collections;
|
||||
@@ -30,6 +31,10 @@ namespace RebootKit.Engine.Simulation {
|
||||
foreach (Actor actor in m_InSceneActors) {
|
||||
actor.ClientTick(Time.deltaTime);
|
||||
}
|
||||
|
||||
foreach (Actor actor in m_SpawnedActors) {
|
||||
actor.ClientTick(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void OnServerTick(ulong tick) {
|
||||
@@ -39,47 +44,82 @@ namespace RebootKit.Engine.Simulation {
|
||||
|
||||
float dt = 1.0f / RR.TickRate.IndexValue;
|
||||
|
||||
foreach (Actor actor in m_InSceneActors) {
|
||||
actor.ServerTick(dt);
|
||||
TickActorsList(m_InSceneActors, dt);
|
||||
TickActorsList(m_SpawnedActors, dt);
|
||||
}
|
||||
|
||||
void TickActorsList(List<Actor> actors, float deltaTime) {
|
||||
foreach (Actor actor in actors) {
|
||||
actor.ServerTick(deltaTime);
|
||||
|
||||
if (actor.IsDataDirty) {
|
||||
actor.IsDataDirty = false;
|
||||
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (data.IsCreated) {
|
||||
SynchronizeActorStateClientRpc(actor.ActorID, data);
|
||||
SynchronizeActorStateWithClients(actor.ActorID, data);
|
||||
} else {
|
||||
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Actor actor in m_SpawnedActors) {
|
||||
actor.ServerTick(dt);
|
||||
if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) {
|
||||
ActorTransformSyncData syncData = actor.GetTransformSyncData();
|
||||
|
||||
if (actor.IsDataDirty) {
|
||||
actor.IsDataDirty = false;
|
||||
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (data.IsCreated) {
|
||||
SynchronizeActorStateClientRpc(actor.ActorID, data);
|
||||
} else {
|
||||
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
|
||||
foreach ((ulong _, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (state.IsReady) {
|
||||
SynchronizeActorTransformStateRpc(actor.ActorID, syncData, RpcTarget.NotMe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc(Delivery = RpcDelivery.Unreliable)]
|
||||
void SynchronizeActorStateClientRpc(ulong actorID, NativeArray<byte> data) {
|
||||
internal void SynchronizeActorCoreStateWithOther(Actor actor) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only the server can synchronize actor core states.");
|
||||
return;
|
||||
}
|
||||
|
||||
SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe);
|
||||
}
|
||||
|
||||
void SynchronizeActorStateWithClients(ulong actorID, NativeArray<byte> data) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only the server can synchronize actor states with clients.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (state.IsReady) {
|
||||
SynchronizeActorStateRpc(actorID, data, RpcTarget.Single(clientID, RpcTargetUse.Temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
|
||||
void SynchronizeActorStateRpc(ulong actorID, NativeArray<byte> data, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
|
||||
|
||||
DeserializeActorState(actor, data);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
|
||||
void SynchronizeActorTransformStateRpc(ulong actorID, ActorTransformSyncData syncData, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found for transform synchronization.");
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreTransformState(syncData);
|
||||
}
|
||||
|
||||
NativeArray<byte> SerializeActorState(Actor actor) {
|
||||
return DataSerializationUtils.Serialize(actor.Data);
|
||||
}
|
||||
@@ -130,9 +170,9 @@ namespace RebootKit.Engine.Simulation {
|
||||
continue;
|
||||
}
|
||||
|
||||
SynchronizeActorStateForClientRpc(actor.ActorID, data, sendParams);
|
||||
SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams);
|
||||
}
|
||||
|
||||
|
||||
foreach (Actor actor in m_SpawnedActors) {
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (!data.IsCreated) {
|
||||
@@ -140,26 +180,41 @@ namespace RebootKit.Engine.Simulation {
|
||||
continue;
|
||||
}
|
||||
|
||||
ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot();
|
||||
SpawnActorRpc(actor.SourceActorPath,
|
||||
actor.ActorID,
|
||||
actor.transform.position,
|
||||
actor.transform.localRotation,
|
||||
coreStateSnapshot,
|
||||
data,
|
||||
sendParams);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
|
||||
void SynchronizeActorStateForClientRpc(ulong actorID, NativeArray<byte> data, RpcParams rpcParams) {
|
||||
void SynchronizeActorStateForClientRpc(ulong actorID,
|
||||
ActorCoreStateSnapshot coreStateSnapshot,
|
||||
NativeArray<byte> data,
|
||||
RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreCoreState(coreStateSnapshot);
|
||||
DeserializeActorState(actor, data);
|
||||
ClientSynchronizedActorRpc();
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
|
||||
void SynchronizeCoreActorStateRpc(ulong actorID, ActorCoreStateSnapshot snapshot, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found for core state synchronization.");
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreCoreState(snapshot);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
|
||||
void ClientSynchronizedActorRpc(RpcParams rpcParams = default) {
|
||||
ulong clientID = rpcParams.Receive.SenderClientId;
|
||||
@@ -192,8 +247,21 @@ namespace RebootKit.Engine.Simulation {
|
||||
actor.HandleActorCommand(cmd);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Everyone)]
|
||||
internal void SendActorEventToClientsRpc(ActorEvent actorEvent) {
|
||||
internal void SendActorEvent(ActorEvent actorEvent) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only the server can send actor events.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (state.IsReady) {
|
||||
SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void SendActorEventRpc(ActorEvent actorEvent, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorEvent.ActorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorEvent.ActorID} not found for event {actorEvent.EventID}");
|
||||
@@ -221,7 +289,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
m_InSceneActors.Clear();
|
||||
|
||||
foreach (Actor actor in m_SpawnedActors) {
|
||||
if (actor is not null) {
|
||||
if (actor.OrNull() != null) {
|
||||
Destroy(actor.gameObject);
|
||||
}
|
||||
}
|
||||
@@ -250,42 +318,48 @@ namespace RebootKit.Engine.Simulation {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
|
||||
public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
|
||||
if (!IsServer) {
|
||||
s_Logger.Error("Only the server can spawn actors.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!assetReference.RuntimeKeyIsValid()) {
|
||||
s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong actorID = UniqueID.NewULongFromGuid();
|
||||
|
||||
GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion();
|
||||
Actor actor = actorObject.GetComponent<Actor>();
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
|
||||
Destroy(actorObject);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
actor.Manager = this;
|
||||
actor.SourceActorPath = assetReference.AssetGUID;
|
||||
actor.ActorID = UniqueID.NewULongFromGuid();
|
||||
actor.ActorID = actorID;
|
||||
actor.Data = actor.InternalCreateActorData();
|
||||
|
||||
m_SpawnedActors.Add(actor);
|
||||
|
||||
NativeArray<byte> stateData = SerializeActorState(actor);
|
||||
SpawnActorRpc(assetReference.AssetGUID, actor.ActorID, position, rotation, stateData, RpcTarget.NotMe);
|
||||
SpawnActorRpc(assetReference.AssetGUID,
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot(),
|
||||
stateData,
|
||||
RpcTarget.NotMe);
|
||||
return actor;
|
||||
}
|
||||
|
||||
// @NOTE: This RPC is used to spawn actors on clients.
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void SpawnActorRpc(string guid,
|
||||
ulong actorID,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
ActorCoreStateSnapshot coreStateSnapshot,
|
||||
NativeArray<byte> stateData,
|
||||
RpcParams rpcParams) {
|
||||
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
|
||||
@@ -294,7 +368,9 @@ namespace RebootKit.Engine.Simulation {
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion();
|
||||
GameObject actorObject = assetReference
|
||||
.InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation)
|
||||
.WaitForCompletion();
|
||||
if (actorObject == null) {
|
||||
s_Logger.Error($"Failed to instantiate actor with GUID {guid}");
|
||||
return;
|
||||
@@ -312,6 +388,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
actor.ActorID = actorID;
|
||||
actor.Data = actor.InternalCreateActorData();
|
||||
|
||||
actor.RestoreCoreState(coreStateSnapshot);
|
||||
DeserializeActorState(actor, stateData);
|
||||
m_SpawnedActors.Add(actor);
|
||||
}
|
||||
@@ -335,39 +412,5 @@ namespace RebootKit.Engine.Simulation {
|
||||
|
||||
Destroy(actor.gameObject);
|
||||
}
|
||||
|
||||
public bool IsActorHidden(ulong actorID) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return !actor.gameObject.activeSelf;
|
||||
}
|
||||
|
||||
public void SetActorHidden(ulong actorID, bool hidden) {
|
||||
if (!IsServer) {
|
||||
s_Logger.Error("Only the server can set actor visibility.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetActorHiddenRpc(actorID, hidden, RpcTarget.Everyone);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void SetActorHiddenRpc(ulong actorID, bool hidden, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
actor.gameObject.SetActive(false);
|
||||
} else {
|
||||
actor.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RR.NetworkSystemInstance is not null) {
|
||||
if (RR.NetworkSystemInstance != null) {
|
||||
RR.NetworkSystemInstance.Actors.CleanUp();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user