This commit is contained in:
2025-07-17 06:36:37 +02:00
parent 4ec3dedd42
commit 1054061d91
52 changed files with 804 additions and 704 deletions

View File

@@ -74,8 +74,8 @@ namespace RebootKit.Engine.Console {
ConfigVar.StateChanged -= OnCVarStateChanged;
m_LogFileWriter.Dispose();
m_LogFileStream.Dispose();
m_LogFileWriter?.Dispose();
m_LogFileStream?.Dispose();
m_LogFileStream = null;
m_LogFileWriter = null;

View File

@@ -1,55 +1,122 @@
using System.Text;
using RebootKit.Engine.Main;
using RebootKit.Engine.Simulation;
using RebootKit.Engine.UI;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UIElements;
namespace RebootKit.Engine.Development {
public class DebugOverlayView : UIDocumentView {
const string k_DebugLabelClassName = "rr__debug-label";
const string k_DebugLabelClassName = "rr__debug-overlay-label";
string m_HeaderText;
VisualElement m_RootElement;
Label m_FPSLabel;
Label m_NetworkStatsLabel;
Label m_Label;
readonly StringBuilder m_StringBuilder = new StringBuilder();
//
// @MARK: Unity callbacks
//
void Update() {
if (m_RootElement == null) {
return;
}
Resolution resolution = Screen.currentResolution;
m_FPSLabel.text = $"fps: {Mathf.RoundToInt(1f / Time.deltaTime)} | dt: {Time.deltaTime:F4}ms | runtime: {Time.time:F4}s | resolution: {resolution.width}x{resolution.height}@{resolution.refreshRateRatio}Hz";
m_StringBuilder.AppendLine(m_HeaderText);
AppendFPSInfo();
AppendNetworkStateInfo();
AppendActorsStateInfo();
NetworkManager nm = NetworkManager.Singleton;
StringBuilder sb = new StringBuilder();
sb.Append("Network: ");
sb.Append($"IsServer: {nm.IsServer.ToString()}");
sb.Append($" | IsClient: {nm.IsClient.ToString()}");
sb.Append($" | IsHost: {nm.IsHost.ToString()}");
m_NetworkStatsLabel.text = sb.ToString();
m_Label.text = m_StringBuilder.ToString();
m_StringBuilder.Clear();
}
void AppendFPSInfo() {
Resolution resolution = Screen.currentResolution;
m_StringBuilder.Append("fps: ");
m_StringBuilder.Append(Mathf.RoundToInt(1f / Time.deltaTime));
m_StringBuilder.Append(" | dt: ");
m_StringBuilder.Append(Time.deltaTime.ToString("F4"));
m_StringBuilder.Append("ms | runtime: ");
m_StringBuilder.Append(Time.time.ToString("F4"));
m_StringBuilder.Append("s | resolution: ");
m_StringBuilder.Append(resolution.width);
m_StringBuilder.Append("x");
m_StringBuilder.Append(resolution.height);
m_StringBuilder.Append("@");
m_StringBuilder.Append(resolution.refreshRateRatio);
m_StringBuilder.AppendLine();
}
void AppendNetworkStateInfo() {
NetworkSystem network = RR.NetworkSystemInstance;
if (network == null) {
m_StringBuilder.AppendLine("NetworkSystem not initialized");
return;
}
m_StringBuilder.Append($"IsServer: {RR.IsServer().ToString()}");
m_StringBuilder.Append($" | IsClient: {RR.IsClient().ToString()}");
m_StringBuilder.Append($" | WorldID: {network.WorldID.ToString()}");
m_StringBuilder.Append($" | Clients: {network.Clients.Count.ToString()}");
m_StringBuilder.Append($" | ReadyClientsCount: {network.GetReadyClientsCount().ToString()}");
m_StringBuilder.AppendLine();
if (network.TryGetClientState(network.LocalClientID, out NetworkClientState clientState)) {
m_StringBuilder.Append($"LocalClientID: {clientState.ClientID.ToString()}");
m_StringBuilder.Append($" | SyncState: {clientState.SyncState.ToString()}");
m_StringBuilder.Append($" | ActorsSyncPacketsLeft: {clientState.ActorsSyncPacketsLeft.ToString()}");
m_StringBuilder.Append($" | IsReady: {clientState.IsReady.ToString()}");
} else {
m_StringBuilder.Append("ClientState not found for LocalClientID: ");
m_StringBuilder.Append(network.LocalClientID.ToString());
}
m_StringBuilder.AppendLine();
}
void AppendActorsStateInfo() {
NetworkSystem network = RR.NetworkSystemInstance;
if (network == null) {
return;
}
ActorsManager actorsManager = network.Actors;
if (actorsManager == null) {
m_StringBuilder.Append("ActorsManager not initialized");
return;
}
m_StringBuilder.Append("InScene Actors Count: ");
m_StringBuilder.Append(actorsManager.InSceneActorsCount.ToString());
m_StringBuilder.Append(" | Dynamic Actors Count: ");
m_StringBuilder.Append(actorsManager.SpawnedActorsCount.ToString());
m_StringBuilder.Append(" | Total Actors Count: ");
m_StringBuilder.Append(actorsManager.TotalActorsCount.ToString());
m_StringBuilder.AppendLine();
}
//
// @MARK: UIDocumentView
//
public override VisualElement Build() {
if (m_HeaderText == null) {
m_HeaderText =
$"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}";
}
m_RootElement = new VisualElement();
CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}");
m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate.ToString()}");
m_NetworkStatsLabel = CreateLabel("Network Stats");
m_Label = (Label)LabelBuilder.New("").Build();
m_Label.AddToClassList(k_DebugLabelClassName);
m_RootElement.Add(m_Label);
return m_RootElement;
}
Label CreateLabel(string text) {
Label label = (Label)LabelBuilder.New(text).Build();
label.AddToClassList(k_DebugLabelClassName);
m_RootElement.Add(label);
return label;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using RebootKit.Engine.Foundation;
using Unity.Multiplayer.Tools.NetStatsMonitor;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -18,7 +19,7 @@ namespace RebootKit.Engine.Development {
public class DevToolsService : ServiceMonoBehaviour {
[SerializeField] DebugOverlayView m_DebugOverlayView;
[SerializeField] GameVersionOverlay m_GameVersionOverlay;
[SerializeField] GameObject m_NetworkStatsOverlay;
[SerializeField] RuntimeNetStatsMonitor m_NetworkStatsOverlay;
IDisposable m_CVarChangedListener;
@@ -41,6 +42,10 @@ namespace RebootKit.Engine.Development {
if (InputSystem.GetDevice<Keyboard>().f3Key.wasReleasedThisFrame) {
DebugCVars.OverlayMode.Set(DebugCVars.OverlayMode.IndexValue == 1 ? 0 : 1);
}
if (InputSystem.GetDevice<Keyboard>().f4Key.wasReleasedThisFrame) {
DebugCVars.ShowNetworkStats.Set(DebugCVars.ShowNetworkStats.IndexValue == 1 ? 0 : 1);
}
}
void OnOverlayModeChanged(int mode) {
@@ -57,7 +62,7 @@ namespace RebootKit.Engine.Development {
} else if (cvar == DebugCVars.ShowGameVersion) {
m_GameVersionOverlay.gameObject.SetActive(cvar.IndexValue > 0);
} else if (cvar == DebugCVars.ShowNetworkStats) {
m_NetworkStatsOverlay.SetActive(cvar.IndexValue > 0);
m_NetworkStatsOverlay.Visible = cvar.IndexValue > 0;
}
}
}

View File

@@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ZLinq;
namespace RebootKit.Engine.Foundation {
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method)]
public class InjectAttribute : Attribute {
}
public class DIContext {
const BindingFlags k_fieldsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
const BindingFlags k_methodsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
static readonly Logger s_logger = new Logger(nameof(DIContext));
readonly Dictionary<Type, object> m_BindingsMaps = new Dictionary<Type, object>();
readonly List<IFieldInjector> m_FieldInjectors = new List<IFieldInjector>();
public DIContext() {
Bind(this);
AddInjector(new InjectAttributeFieldInjector());
}
public void AddInjector(IFieldInjector injector) {
m_FieldInjectors.Add(injector);
}
public void Bind(Type type, object obj) {
if (!m_BindingsMaps.TryAdd(type, obj)) {
s_logger.Error($"Cannot bind to '{type}', slot is already occupied");
}
}
public void Bind<TBind>(TBind obj) {
Bind(typeof(TBind), obj);
}
public object Resolve(Type type) {
if (m_BindingsMaps.TryGetValue(type, out object obj)) return obj;
s_logger.Error($"Couldn't resolve `{type}`");
return null;
}
public T Resolve<T>() {
return (T) Resolve(typeof(T));
}
// @brief creates new instance of an object and injects dependencies
public T Create<T>() {
T instance = Activator.CreateInstance<T>();
Inject(instance);
return instance;
}
public T Create<T>(params object[] args) {
T instance = (T) Activator.CreateInstance(typeof(T), args);
Inject(instance);
return instance;
}
public void Inject(object target) {
Type type = target.GetType();
foreach (FieldInfo field in type.GetFields(k_fieldsBindingFlags)) {
InjectField(field, target);
}
foreach (MethodInfo method in type.GetMethods(k_methodsBindingFlags)) {
if (!Attribute.IsDefined(method, typeof(InjectAttribute))) {
continue;
}
Type[] paramsTypes = method.GetParameters()
.AsValueEnumerable()
.Select(t => t.ParameterType)
.ToArray();
object[] instances = new object[paramsTypes.Length];
for (int i = 0; i < paramsTypes.Length; ++i) {
instances[i] = Resolve(paramsTypes[i]);
if (instances[i] == null) {
s_logger.Error($"Failed to resolve method parameter of type `{paramsTypes[i]}`");
}
}
method.Invoke(target, instances);
}
}
bool InjectField(FieldInfo field, object target) {
for (int i = m_FieldInjectors.Count - 1; i >= 0; i--) {
if (m_FieldInjectors[i].Inject(field, target, this)) {
return true;
}
}
return false;
}
public interface IFieldInjector {
bool Inject(FieldInfo field, object target, DIContext context);
}
class InjectAttributeFieldInjector : IFieldInjector {
public bool Inject(FieldInfo field, object target, DIContext context) {
if (!Attribute.IsDefined(field, typeof(InjectAttribute))) return false;
object instance = context.Resolve(field.FieldType);
if (instance == null) {
s_logger.Error($"Cannot resolve `{field.FieldType}`");
return false;
}
field.SetValue(target, instance);
return true;
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cee4133e4a594126868703cc035663cd
timeCreated: 1742001112

View File

@@ -5,10 +5,6 @@ namespace RebootKit.Engine.Foundation {
TProd Create();
}
public interface IFactoryDI<out TProd> {
TProd Create(DIContext context);
}
public abstract class FactoryAsset<TProd> : ScriptableObject, IFactory<TProd> where TProd : class {
public abstract TProd Create();
}

View File

@@ -1,45 +0,0 @@
using UnityEngine;
using UnityEngine.Assertions;
namespace RebootKit.Engine.Foundation {
public interface IDependencyInstaller {
void Install(DIContext context);
}
public abstract class SceneDependencyInstaller : MonoBehaviour, IDependencyInstaller {
public abstract void Install(DIContext context);
}
[DefaultExecutionOrder(-1000)]
public class SceneContext : MonoBehaviour {
static readonly Logger s_logger = new Logger(nameof(SceneContext));
[SerializeField] SceneDependencyInstaller[] m_Installers;
DIContext m_DIContext;
void Awake() {
m_DIContext = new DIContext();
s_logger.Info("Installing scene dependency installers");
foreach (SceneDependencyInstaller installer in m_Installers) {
installer.Install(m_DIContext);
}
foreach (GameObject root in gameObject.scene.GetRootGameObjects()) {
m_DIContext.InjectGameObject(root);
}
}
}
public static class DIContextGameObjectEx {
public static void InjectGameObject(this DIContext context, GameObject gameObject, bool injectChildren = true) {
Assert.IsNotNull(gameObject);
Component[] components = injectChildren ? gameObject.GetComponentsInChildren<Component>() : gameObject.GetComponents<Component>();
foreach (Component component in components) {
context.Inject(component);
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 8dd28652b58c4d689ab3f2f9354d7589
timeCreated: 1742006992

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using RebootKit.Engine.Simulation;
using Unity.Collections;
using Unity.Netcode;
@@ -8,14 +11,31 @@ using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Main {
class NetworkClientState {
enum NetworkClientSyncState {
NotReady,
LoadingWorld,
PreparingForActorsSync,
SyncingActors,
Ready
}
struct NetworkClientState : INetworkSerializable {
public ulong ClientID;
public bool IsWorldLoaded;
public bool AreActorsSynced;
public bool IsReadyForActorsSync;
public NetworkClientSyncState SyncState;
public int ActorsSyncPacketsLeft;
public bool IsReady;
public bool IsReady {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get {
return SyncState == NetworkClientSyncState.Ready;
}
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref ClientID);
serializer.SerializeValue(ref SyncState);
serializer.SerializeValue(ref ActorsSyncPacketsLeft);
}
}
public class NetworkSystem : NetworkBehaviour {
@@ -25,13 +45,26 @@ namespace RebootKit.Engine.Main {
internal readonly Dictionary<ulong, NetworkClientState> Clients = new Dictionary<ulong, NetworkClientState>();
FixedString512Bytes m_WorldID = new FixedString512Bytes("");
public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes("");
bool m_IsChangingWorld = false;
public ulong LocalClientID {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get {
return NetworkManager.Singleton.LocalClientId;
}
}
//
// @MARK: Unity callbacks
//
void Awake() {
RR.NetworkSystemInstance = this;
}
//
// @MARK: NetworkBehaviour callbacks
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
@@ -55,35 +88,42 @@ namespace RebootKit.Engine.Main {
NetworkClientState newClientState = new NetworkClientState {
ClientID = clientID,
IsWorldLoaded = false,
AreActorsSynced = false,
IsReadyForActorsSync = false,
IsReady = false
SyncState = NetworkClientSyncState.NotReady
};
Clients.Add(clientID, newClientState);
if (!m_WorldID.IsEmpty) {
s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{m_WorldID}'");
ClientLoadWorldRpc(m_WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp));
if (clientID != NetworkManager.Singleton.LocalClientId) {
foreach (NetworkClientState state in Clients.Values) {
UpdateClientStateRpc(state, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
if (!WorldID.IsEmpty) {
s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'");
ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
void OnClientDisconnect(ulong clientID) {
if (!IsServer) {
return;
}
s_Logger.Info($"OnClientDisconnect: {clientID}");
Clients.Remove(clientID);
}
internal NetworkClientState GetClientState(ulong clientID) {
if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) {
return clientState;
//
// @MARK: Server API
//
public void KickClient(ulong clientID, string reason = "Kicked by server") {
if (!IsServer) {
s_Logger.Error("Only server can kick clients.");
return;
}
s_Logger.Error($"Client state for {clientID} not found.");
return null;
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(clientID, out NetworkClient client)) {
NetworkManager.Singleton.DisconnectClient(clientID, reason);
s_Logger.Info($"Kicked client {clientID}: {reason}");
} else {
s_Logger.Error($"Client {clientID} not found.");
}
}
public void SetCurrentWorld(string worldID) {
@@ -93,7 +133,7 @@ namespace RebootKit.Engine.Main {
}
if (m_IsChangingWorld) {
s_Logger.Error($"Already changing world to '{m_WorldID}'. Please wait until the current world change is complete.");
s_Logger.Error($"Already changing world to '{WorldID}'. Please wait until the current world change is complete.");
return;
}
@@ -103,13 +143,12 @@ namespace RebootKit.Engine.Main {
return;
}
m_WorldID = worldID;
foreach (KeyValuePair<ulong, NetworkClientState> kv in Clients) {
kv.Value.IsWorldLoaded = false;
kv.Value.AreActorsSynced = false;
kv.Value.IsReadyForActorsSync = false;
kv.Value.IsReady = false;
WorldID = worldID;
foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) {
NetworkClientState state = clientState;
state.SyncState = NetworkClientSyncState.LoadingWorld;
UpdateClientState(state);
}
ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget();
@@ -127,8 +166,14 @@ namespace RebootKit.Engine.Main {
m_IsChangingWorld = false;
NetworkClientState localClientState = GetClientState(NetworkManager.Singleton.LocalClientId);
localClientState.IsReady = true;
if (!TryGetClientState(NetworkManager.Singleton.LocalClientId, out NetworkClientState localClientState)) {
s_Logger.Error($"Local client state not found for client ID {NetworkManager.Singleton.LocalClientId}.");
RR.Disconnect();
return;
}
localClientState.SyncState = NetworkClientSyncState.Ready;
UpdateClientState(localClientState);
RR.GameInstance.PlayerBecameReady(localClientState.ClientID);
@@ -161,7 +206,7 @@ namespace RebootKit.Engine.Main {
await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken);
m_WorldID = worldID;
WorldID = worldID;
ClientLoadedWorldRpc(worldID);
}
@@ -169,30 +214,60 @@ namespace RebootKit.Engine.Main {
void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
if (!m_WorldID.Equals(worldID)) {
s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{m_WorldID}'.");
if (!WorldID.Equals(worldID)) {
s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{WorldID}'.");
NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!");
return;
}
if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) {
clientState.IsWorldLoaded = true;
clientState.IsReadyForActorsSync = false;
Actors.SynchronizeActorsForClient(clientID);
Actors.InitializeActorsForClient(clientID);
} else {
NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!");
}
}
internal void ClientSynchronizedActors(ulong clientID) {
NetworkClientState clientState = GetClientState(clientID);
if (clientState is null) {
s_Logger.Error($"Client state for {clientID} not found.");
//
// @MARK: Internal
//
internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) {
return Clients.TryGetValue(clientID, out clientState);
}
internal void UpdateClientState(NetworkClientState clientState) {
if (!IsServer) {
s_Logger.Error("UpdateClientState can only be called on the server.");
return;
}
Clients[clientState.ClientID] = clientState;
UpdateClientStateRpc(clientState, RpcTarget.NotServer);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void UpdateClientStateRpc(NetworkClientState newState, RpcParams rpcParams) {
Clients[newState.ClientID] = newState;
}
clientState.IsReady = true;
RR.GameInstance.PlayerBecameReady(clientID);
internal void ClientSynchronizedActors(ulong clientID) {
if (TryGetClientState(clientID, out NetworkClientState state)) {
state.SyncState = NetworkClientSyncState.Ready;
UpdateClientState(state);
RR.GameInstance.PlayerBecameReady(clientID);
} else {
s_Logger.Error($"Client state for {clientID} not found.");
}
}
internal int GetReadyClientsCount() {
int count = 0;
foreach (NetworkClientState clientState in Clients.Values) {
if (clientState.IsReady) {
count++;
}
}
return count;
}
}
}

View File

@@ -1,15 +0,0 @@
using RebootKit.Engine.Foundation;
using UnityEngine;
namespace RebootKit.Engine {
public class MainSceneInstaller : SceneDependencyInstaller {
public override void Install(DIContext context) {
foreach (GameObject rootGameObject in gameObject.scene.GetRootGameObjects()) {
IService[] services = rootGameObject.GetComponentsInParent<IService>();
foreach (IService service in services) {
context.Bind(service.GetType(), service);
}
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 4238ea1a17e342e583cdd929103a22c6
timeCreated: 1742007242

View File

@@ -259,25 +259,35 @@ namespace RebootKit.Engine.Simulation {
public bool IsDataDirty { get; protected internal set; }
internal ActorsManager Manager;
internal DateTime LastCoreStateSyncTime = DateTime.MinValue;
//
// @MARK: Unity callbacks
//
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();
}
}
//
// @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
// @MARK: Server side
public virtual void OnServerTick(float deltaTime) { }
protected virtual void OnActorCommandServer(ActorCommand actorCommand) { }
// @NOTE: Client-side method to handle actor events
// Override this method to implement client-side logic
public virtual void OnClientTick(float deltaTime) { }
protected virtual void OnActorEventClient(ActorEvent actorEvent) { }
protected virtual void OnClientFinishedInitialSync() { }
//
// @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})");
@@ -383,7 +393,9 @@ namespace RebootKit.Engine.Simulation {
Manager.SynchronizeActorCoreStateWithOther(this);
}
//
// @MARK: Common API
//
public bool IsHidden() {
return !gameObject.activeSelf;
}
@@ -457,7 +469,9 @@ namespace RebootKit.Engine.Simulation {
return false;
}
// @MARK: Internal API
//
// @MARK: Internal
//
internal ActorCoreStateSnapshot GetCoreStateSnapshot() {
ActorCoreStateSnapshot snapshot = new ActorCoreStateSnapshot();
snapshot.Timestamp = DateTime.UtcNow;
@@ -637,7 +651,11 @@ namespace RebootKit.Engine.Simulation {
internal IActorData InternalCreateActorData() {
return CreateActorData();
}
internal void InitialSyncFinished() {
OnClientFinishedInitialSync();
}
internal void HandleActorCommand(ActorCommand actorCommand) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})");
@@ -672,13 +690,5 @@ namespace RebootKit.Engine.Simulation {
OnActorEventClient(actorEvent);
}
// @MARK: Unity lifecycle methods
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();
}
}
}
}

View File

@@ -11,12 +11,20 @@ using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Simulation {
// @TODO:
// - Actors States might be packed into chunks to reduce the number of RPCs sent.
// - Release addressables when they are no longer needed.
public class ActorsManager : NetworkBehaviour {
static readonly Logger s_Logger = new Logger(nameof(ActorsManager));
readonly List<Actor> m_InSceneActors = new List<Actor>();
readonly List<Actor> m_SpawnedActors = new List<Actor>();
public int InSceneActorsCount { get { return m_InSceneActors.Count; } }
public int SpawnedActorsCount { get { return m_SpawnedActors.Count; } }
public int TotalActorsCount { get { return m_InSceneActors.Count + m_SpawnedActors.Count; } }
//
// @MARK: NetworkBehaviour callbacks
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
RR.ServerTick += OnServerTick;
@@ -27,16 +35,22 @@ namespace RebootKit.Engine.Simulation {
RR.ServerTick -= OnServerTick;
}
//
// @MARK: Unity callbacks
//
void Update() {
foreach (Actor actor in m_InSceneActors) {
actor.ClientTick(Time.deltaTime);
actor.OnClientTick(Time.deltaTime);
}
foreach (Actor actor in m_SpawnedActors) {
actor.ClientTick(Time.deltaTime);
actor.OnClientTick(Time.deltaTime);
}
}
//
// @MARK: Server-side logic
//
void OnServerTick(ulong tick) {
if (!IsServer) {
return;
@@ -50,14 +64,14 @@ namespace RebootKit.Engine.Simulation {
void TickActorsList(List<Actor> actors, float deltaTime) {
foreach (Actor actor in actors) {
actor.ServerTick(deltaTime);
actor.OnServerTick(deltaTime);
if (actor.IsDataDirty) {
actor.IsDataDirty = false;
NativeArray<byte> data = SerializeActorState(actor);
if (data.IsCreated) {
SynchronizeActorStateWithClients(actor.ActorID, data);
SendActorStateToClients(actor.ActorID, data);
} else {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
}
@@ -84,7 +98,7 @@ namespace RebootKit.Engine.Simulation {
SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe);
}
void SynchronizeActorStateWithClients(ulong actorID, NativeArray<byte> data) {
void SendActorStateToClients(ulong actorID, NativeArray<byte> data) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can synchronize actor states with clients.");
return;
@@ -103,9 +117,8 @@ namespace RebootKit.Engine.Simulation {
if (actor is null) {
return;
}
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
DeserializeActorState(actor, data);
}
@@ -127,197 +140,10 @@ namespace RebootKit.Engine.Simulation {
void DeserializeActorState(Actor actor, NativeArray<byte> data) {
DataSerializationUtils.Deserialize(data, ref actor.Data);
}
internal void SynchronizeActorsForClient(ulong clientID) {
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot synchronize actors.");
return;
}
PrepareClientForActorsSyncRpc(RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void PrepareClientForActorsSyncRpc(RpcParams rpcParams) {
foreach (Actor spawnedActor in m_SpawnedActors) {
Destroy(spawnedActor.gameObject);
}
m_SpawnedActors.Clear();
ClientIsReadyForActorsSyncRpc();
}
[Rpc(SendTo.Server)]
void ClientIsReadyForActorsSyncRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as ready for actors sync.");
return;
}
clientState.IsReadyForActorsSync = true;
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
foreach (Actor actor in m_InSceneActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams);
}
foreach (Actor actor in m_SpawnedActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot();
SpawnActorRpc(actor.SourceActorPath,
actor.ActorID,
coreStateSnapshot,
data,
sendParams);
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
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;
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as synchronized.");
return;
}
clientState.ActorsSyncPacketsLeft--;
if (clientState.ActorsSyncPacketsLeft == 0) {
RR.NetworkSystemInstance.ClientSynchronizedActors(clientID);
}
}
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
internal void SendActorCommandToServerRpc(ActorCommand cmd) {
if (!IsServer) {
s_Logger.Error("Only the server can handle actor events.");
return;
}
Actor actor = FindActorByID(cmd.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {cmd.ActorID} not found for command {cmd.CommandID}");
return;
}
actor.HandleActorCommand(cmd);
}
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}");
return;
}
actor.HandleActorEvent(actorEvent);
}
public void RegisterInSceneActor(Actor actor) {
if (actor.Data == null) {
actor.Data = actor.InternalCreateActorData();
}
actor.Manager = this;
m_InSceneActors.Add(actor);
}
public void CleanUp() {
if (IsServer) {
CleanUpRpc();
}
m_InSceneActors.Clear();
foreach (Actor actor in m_SpawnedActors) {
if (actor.OrNull() != null) {
Destroy(actor.gameObject);
}
}
m_SpawnedActors.Clear();
}
[Rpc(SendTo.NotMe)]
void CleanUpRpc() {
CleanUp();
}
public Actor FindActorByID(ulong actorID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
foreach (Actor actor in m_SpawnedActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
return null;
}
//
// @MARK: Server API
//
public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!IsServer) {
s_Logger.Error("Only the server can spawn actors.");
@@ -412,5 +238,205 @@ namespace RebootKit.Engine.Simulation {
Destroy(actor.gameObject);
}
public void CleanUp() {
if (IsServer) {
CleanUpRpc();
}
m_InSceneActors.Clear();
foreach (Actor actor in m_SpawnedActors) {
if (actor.OrNull() != null) {
Destroy(actor.gameObject);
}
}
m_SpawnedActors.Clear();
}
[Rpc(SendTo.NotMe)]
void CleanUpRpc() {
CleanUp();
}
//
// @MARK: Common API
//
public void RegisterInSceneActor(Actor actor) {
if (actor.Data == null) {
actor.Data = actor.InternalCreateActorData();
}
actor.Manager = this;
m_InSceneActors.Add(actor);
}
public Actor FindActorByID(ulong actorID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
foreach (Actor actor in m_SpawnedActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
return null;
}
//
// @MARK: Initial synchronization
//
internal void InitializeActorsForClient(ulong clientID) {
if (RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
clientState.SyncState = NetworkClientSyncState.PreparingForActorsSync;
RR.NetworkSystemInstance.UpdateClientState(clientState);
PrepareClientForActorsSyncRpc(RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
} else {
s_Logger.Error($"Client state for {clientID} not found. Cannot synchronize actors.");
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void PrepareClientForActorsSyncRpc(RpcParams rpcParams) {
foreach (Actor spawnedActor in m_SpawnedActors) {
Destroy(spawnedActor.gameObject);
}
m_SpawnedActors.Clear();
ClientIsReadyForActorsSyncRpc();
}
[Rpc(SendTo.Server)]
void ClientIsReadyForActorsSyncRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as ready for actors sync.");
return;
}
clientState.SyncState = NetworkClientSyncState.SyncingActors;
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
RR.NetworkSystemInstance.UpdateClientState(clientState);
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
foreach (Actor actor in m_InSceneActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams);
}
foreach (Actor actor in m_SpawnedActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot();
SpawnActorRpc(actor.SourceActorPath,
actor.ActorID,
coreStateSnapshot,
data,
sendParams);
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
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;
if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as synchronized.");
return;
}
clientState.ActorsSyncPacketsLeft--;
RR.NetworkSystemInstance.UpdateClientState(clientState);
if (clientState.ActorsSyncPacketsLeft == 0) {
RR.NetworkSystemInstance.ClientSynchronizedActors(clientID);
}
}
//
// @MARK: Actor Commands and Events
//
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
internal void SendActorCommandToServerRpc(ActorCommand cmd) {
if (!IsServer) {
s_Logger.Error("Only the server can handle actor events.");
return;
}
Actor actor = FindActorByID(cmd.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {cmd.ActorID} not found for command {cmd.CommandID}");
return;
}
actor.HandleActorCommand(cmd);
}
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}");
return;
}
actor.HandleActorEvent(actorEvent);
}
}
}