working on actors

This commit is contained in:
2025-07-03 05:52:25 +02:00
parent dca9f8883a
commit c39b87ed44
16 changed files with 975 additions and 165 deletions

View File

@@ -1,5 +1,8 @@
using RebootKit.Engine.Main;
using System.Text;
using RebootKit.Engine.Main;
using RebootKit.Engine.UI;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.UIElements;
@@ -10,6 +13,7 @@ namespace RebootKit.Engine.Services.Development {
VisualElement m_RootElement;
Label m_FPSLabel;
Label m_NetworkStatsLabel;
void Update() {
if (m_RootElement == null) {
@@ -18,6 +22,17 @@ namespace RebootKit.Engine.Services.Development {
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";
NetworkManager nm = NetworkManager.Singleton;
StringBuilder sb = new StringBuilder();
sb.Append("Network: ");
sb.Append($"IsServer: {nm.IsServer}");
sb.Append($" | IsClient: {nm.IsClient}");
sb.Append($" | IsHost: {nm.IsHost}");
m_NetworkStatsLabel.text = sb.ToString();
}
public override VisualElement Build() {
@@ -26,6 +41,8 @@ namespace RebootKit.Engine.Services.Development {
CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}");
m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}");
m_NetworkStatsLabel = CreateLabel("Network Stats");
return m_RootElement;
}

View File

@@ -0,0 +1,12 @@
using System;
namespace RebootKit.Engine.Foundation {
public static class UniqueID {
public static ulong NewULongFromGuid() {
Guid guid = Guid.NewGuid();
byte[] bytes = guid.ToByteArray();
ulong id = BitConverter.ToUInt64(bytes, 0);
return id;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e7c51400f234e5986c5d2779ca00fb3
timeCreated: 1751491055

View File

@@ -10,40 +10,25 @@ namespace RebootKit.Engine.Main {
public abstract class Game : NetworkBehaviour {
static readonly Logger s_GameLogger = new Logger(nameof(Game));
protected NetworkVariable<FixedString128Bytes> m_CurrentWorldID =
new NetworkVariable<FixedString128Bytes>(new FixedString128Bytes(""));
// Server only callbacks
protected virtual void OnPlayerBecameReady(ulong clientID) {
}
// Event callbacks
public virtual void OnWorldLoaded() {
}
public virtual void OnWorldUnload() {
}
public virtual void OnChatMessage(string message) {
protected virtual void OnChatMessage(string message) {
s_GameLogger.Info($"Chat: {message}");
}
// Network
// NGO callbacks
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
RR.GameInstance = this;
m_CurrentWorldID.OnValueChanged += OnCurrentWorldIDChanged;
LoadWorld(m_CurrentWorldID.Value.Value);
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
m_CurrentWorldID.OnValueChanged -= OnCurrentWorldIDChanged;
RR.GameInstance = null;
}
[ServerRpc]
public void SetCurrentWorldServerRpc(string worldID) {
m_CurrentWorldID.Value = new FixedString128Bytes(worldID);
}
// Chat
[Rpc(SendTo.Server)]
@@ -55,24 +40,14 @@ namespace RebootKit.Engine.Main {
void PrintChatMessageClientRpc(string message) {
OnChatMessage(message);
}
void OnCurrentWorldIDChanged(FixedString128Bytes previousValue, FixedString128Bytes newValue) {
string worldID = newValue.Value;
LoadWorld(worldID);
}
void LoadWorld(string worldID) {
if (string.IsNullOrEmpty(worldID)) {
RR.World.Unload();
} else {
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is not null) {
RR.CloseMainMenu();
RR.World.LoadAsync(worldConfigAsset.Config, Application.exitCancellationToken).Forget();
} else {
s_GameLogger.Error($"World config asset for '{worldID}' not found.");
}
internal void PlayerBecameReady(ulong clientID) {
if (!IsServer) {
s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server.");
return;
}
OnPlayerBecameReady(clientID);
}
}
}

View File

@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Services.Simulation;
using RebootKit.Engine.Simulation;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Main {
class NetworkClientState {
public ulong ClientID;
public bool IsWorldLoaded;
public bool AreActorsSynced;
public bool IsReadyForActorsSync;
public int ActorsSyncPacketsLeft;
public bool IsReady;
}
public class NetworkSystem : NetworkBehaviour {
static readonly Logger s_Logger = new Logger(nameof(NetworkSystem));
[field: SerializeField] public ActorsManager Actors { get; private set; }
readonly Dictionary<ulong, NetworkClientState> m_Clients = new Dictionary<ulong, NetworkClientState>();
FixedString512Bytes m_WorldID = new FixedString512Bytes("");
bool m_IsChangingWorld = false;
void Awake() {
RR.NetworkSystemInstance = this;
}
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect;
}
void OnClientConnected(ulong clientID) {
if (!IsServer) {
return;
}
s_Logger.Info($"OnClientConnected: {clientID}");
NetworkClientState newClientState = new NetworkClientState {
ClientID = clientID,
IsWorldLoaded = false,
AreActorsSynced = false,
IsReadyForActorsSync = false,
IsReady = false
};
m_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));
}
}
void OnClientDisconnect(ulong clientID) {
if (!IsServer) {
return;
}
s_Logger.Info($"OnClientDisconnect: {clientID}");
m_Clients.Remove(clientID);
}
internal NetworkClientState GetClientState(ulong clientID) {
if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) {
return clientState;
}
s_Logger.Error($"Client state for {clientID} not found.");
return null;
}
public void SetCurrentWorld(string worldID) {
if (!IsServer) {
s_Logger.Error("Only server can set the current world.");
return;
}
if (m_IsChangingWorld) {
s_Logger.Error($"Already changing world to '{m_WorldID}'. Please wait until the current world change is complete.");
return;
}
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found.");
return;
}
m_WorldID = worldID;
foreach (KeyValuePair<ulong, NetworkClientState> kv in m_Clients) {
kv.Value.IsWorldLoaded = false;
kv.Value.AreActorsSynced = false;
kv.Value.IsReadyForActorsSync = false;
kv.Value.IsReady = false;
}
ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget();
}
async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) {
s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}");
m_IsChangingWorld = true;
RR.World.Unload();
RR.CloseMainMenu();
await RR.World.LoadAsync(asset.Config, cancellationToken);
m_IsChangingWorld = false;
NetworkClientState localClientState = GetClientState(NetworkManager.Singleton.LocalClientId);
localClientState.IsReady = true;
RR.GameInstance.PlayerBecameReady(localClientState.ClientID);
ClientLoadWorldRpc(asset.name, RpcTarget.NotMe);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void ClientLoadWorldRpc(string worldID, RpcParams rpcParams) {
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"World config asset for '{worldID}' not found.");
RR.Disconnect();
return;
}
ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget();
}
async UniTask ClientLoadWorldAsync(string worldID, CancellationToken cancellationToken) {
s_Logger.Info($"ClientLoadWorldAsync: {worldID}");
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"World config asset for '{worldID}' not found.");
return;
}
RR.World.Unload();
RR.CloseMainMenu();
await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken);
m_WorldID = worldID;
ClientLoadedWorldRpc(worldID);
}
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
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}'.");
NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!");
return;
}
if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) {
clientState.IsWorldLoaded = true;
clientState.IsReadyForActorsSync = false;
Actors.SynchronizeActorsForClient(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.");
return;
}
clientState.IsReady = true;
RR.GameInstance.PlayerBecameReady(clientID);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f967d37c17e4704b80849c305a53be9
timeCreated: 1751411566

View File

@@ -27,7 +27,7 @@ namespace RebootKit.Engine.Main {
[ConfigVar("con.write_log", 1, "Enables writing game log to console output")]
static ConfigVar s_writeLogToConsole;
[ConfigVar("sv.tick_rate", 60, "Server tick rate in Hz")]
[ConfigVar("sv.tick_rate", 24, "Server tick rate in Hz")]
public static ConfigVar TickRate;
internal static EngineConfigAsset EngineConfig;
@@ -37,6 +37,9 @@ namespace RebootKit.Engine.Main {
static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle;
static NetworkSystem s_networkSystemPrefab;
internal static NetworkSystem NetworkSystemInstance;
internal static ConsoleService Console { get; private set; }
public static InputService Input { get; private set; }
public static WorldService World { get; private set; }
@@ -60,7 +63,7 @@ namespace RebootKit.Engine.Main {
s_Logger.Info("Initializing");
s_servicesBag = new DisposableBag();
s_disposableBag = new DisposableBag();
s_Logger.Info("Registering core services");
Console = CreateService(EngineConfig.coreServices.consoleService);
Input = CreateService(EngineConfig.coreServices.inputService);
@@ -68,11 +71,14 @@ namespace RebootKit.Engine.Main {
await InitializeAssetsAsync(cancellationToken);
await SteamManager.InitializeAsync(cancellationToken);
// await SteamManager.InitializeAsync(cancellationToken);
}
// @NOTE: This method is called after the main scene is loaded.
internal static async UniTask RunAsync(CancellationToken cancellationToken) {
s_networkSystemPrefab =
Resources.Load<NetworkSystem>(RConsts.k_CoreNetworkGameSystemsResourcesPath);
NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent;
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
NetworkManager.Singleton.OnServerStopped += OnServerStopped;
@@ -111,7 +117,7 @@ namespace RebootKit.Engine.Main {
NetworkManager.Singleton.OnServerStopped -= OnServerStopped;
}
SteamManager.Shutdown();
// SteamManager.Shutdown();
s_servicesBag.Dispose();
s_disposableBag.Dispose();
@@ -138,7 +144,7 @@ namespace RebootKit.Engine.Main {
WorldConfigAsset worldConfig =
s_WorldConfigsAssets.Find(asset => asset.Config.name.Equals(name, StringComparison.Ordinal));
if (!worldConfig) {
if (worldConfig is null) {
throw new KeyNotFoundException($"World config '{name}' not found");
}
@@ -187,7 +193,29 @@ namespace RebootKit.Engine.Main {
return;
}
GameInstance.SetCurrentWorldServerRpc(worldID);
NetworkSystemInstance.SetCurrentWorld(worldID);
}
public static void SpawnActor(AssetReferenceGameObject assetReference,
Vector3 position,
Quaternion rotation) {
if (!IsServer()) {
s_Logger.Error("Cannot spawn actor. Not a server instance.");
return;
}
if (NetworkSystemInstance is null) {
s_Logger.Error("NetworkSystemInstance is not initialized. Cannot spawn actor.");
return;
}
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error("Asset reference is not valid. Cannot spawn actor.");
return;
}
s_Logger.Info($"Spawning actor from asset reference: {assetReference.RuntimeKey}");
NetworkSystemInstance.Actors.SpawnActor(assetReference, position, rotation);
}
// Service API
@@ -343,8 +371,6 @@ namespace RebootKit.Engine.Main {
TickCount++;
}
World.Tick(deltaTime);
}
static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) {
@@ -356,6 +382,9 @@ namespace RebootKit.Engine.Main {
GameInstance = Object.Instantiate(EngineConfig.gamePrefab);
GameInstance.NetworkObject.Spawn();
NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab);
NetworkSystemInstance.NetworkObject.Spawn();
}
static void OnServerStopped(bool obj) {
@@ -363,10 +392,13 @@ namespace RebootKit.Engine.Main {
if (GameInstance is not null) {
GameInstance.NetworkObject.Despawn();
Object.Destroy(GameInstance.gameObject);
GameInstance = null;
}
if (NetworkSystemInstance is not null) {
NetworkSystemInstance.NetworkObject.Despawn();
NetworkSystemInstance = null;
}
GameInstance = null;
}
// Console Commands

View File

@@ -8,6 +8,8 @@
public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName;
public const string k_EngineConfigAssetPath = "Assets/TheGame/" + k_EngineConfigAssetName + ".asset";
public const string k_CoreNetworkGameSystemsResourcesPath = "RebootKit/core_network_game_systems";
internal const string k_AddComponentMenu = "Reboot Reality/";
internal const string k_CreateAssetMenu = "Reboot Reality/";

View File

@@ -1,48 +1,247 @@
using Unity.Collections;
using System;
using NUnit.Framework;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Netcode;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Simulation {
public interface ISerializableEntity {
void Serialize(FastBufferWriter writer);
void Deserialize(FastBufferReader reader);
// @NOTE: -1 means use the default size and have hope it will fit.
int MinimumSizeInBytes() { return -1; }
}
public interface IActorData : ISerializableEntity { }
public class NoActorData : IActorData {
public void Serialize(FastBufferWriter writer) { }
public void Deserialize(FastBufferReader reader) { }
}
public static class DataSerializationUtils {
public const int k_DefaultMessageSize = 256;
public static NativeArray<byte> Serialize<TEntity>(TEntity entity,
Allocator allocator = Allocator.Temp)
where TEntity : ISerializableEntity {
int size = entity.MinimumSizeInBytes();
if (size < 0) {
size = k_DefaultMessageSize;
}
using FastBufferWriter writer = new FastBufferWriter(size, allocator);
if (writer.TryBeginWrite(size)) {
entity.Serialize(writer);
int length = writer.Length;
NativeArray<byte> data = new NativeArray<byte>(length, allocator);
unsafe {
void* dst = data.GetUnsafePtr();
void* src = writer.GetUnsafePtr();
Buffer.MemoryCopy(src, dst, length, length);
}
return data;
}
return default;
}
public static void Deserialize<TEntity>(NativeArray<byte> data, ref TEntity entity)
where TEntity : ISerializableEntity {
using FastBufferReader reader = new FastBufferReader(data, Allocator.Temp);
if (reader.TryBeginRead(data.Length)) {
entity.Deserialize(reader);
}
}
}
public struct ActorCommand : INetworkSerializable {
public ulong ActorID;
public ulong ClientID;
public ushort CommandID;
public NativeArray<byte> Data;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref ActorID);
serializer.SerializeValue(ref ClientID);
serializer.SerializeValue(ref CommandID);
if (serializer.IsWriter) {
bool hasData = Data.IsCreated;
serializer.SerializeValue(ref hasData);
if (hasData) {
serializer.SerializeValue(ref Data, Allocator.Temp);
}
} else if (serializer.IsReader) {
bool hasData = false;
serializer.SerializeValue(ref hasData);
if (hasData) {
serializer.SerializeValue(ref Data, Allocator.Temp);
}
}
}
}
public struct ActorEvent : INetworkSerializable {
public ulong ActorID;
public ulong ClientID;
public ushort EventID;
public NativeArray<byte> Data;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref ActorID);
serializer.SerializeValue(ref ClientID);
serializer.SerializeValue(ref EventID);
if (serializer.IsWriter) {
bool hasData = Data.IsCreated;
serializer.SerializeValue(ref hasData);
if (hasData) {
serializer.SerializeValue(ref Data, Allocator.Temp);
}
} else if (serializer.IsReader) {
bool hasData = false;
serializer.SerializeValue(ref hasData);
if (hasData) {
serializer.SerializeValue(ref Data, Allocator.Temp);
}
}
}
}
public abstract class Actor : MonoBehaviour {
bool m_IsPlaying = false;
public bool IsPlaying {
get {
return m_IsPlaying;
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; }
[NonSerialized] internal IActorData Data;
public bool IsDataDirty { get; protected internal set; }
internal ActorsManager Manager;
internal IActorData InternalCreateActorData() {
return CreateActorData();
}
internal void HandleActorCommand(ActorCommand actorCommand) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})");
return;
}
set {
if (m_IsPlaying == value) {
return;
}
m_IsPlaying = value;
if (m_IsPlaying) {
OnBeginPlay();
} else {
OnEndPlay();
}
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(actorCommand);
}
public virtual void OnSpawned() {
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);
}
public virtual void OnDespawned() {
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);
}
public virtual void OnBeginPlay() {
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);
}
public virtual void OnEndPlay() {
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);
}
public virtual void Tick(float deltaTime) {
}
public virtual void SerializeNetworkState(ref DataStreamWriter writer) {
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}");
}
public virtual void DeserializeNetworkState(ref DataStreamReader reader) {
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();
}
}
}
}

View File

@@ -0,0 +1,338 @@
using System.Collections.Generic;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.AddressableAssets;
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.
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 override void OnNetworkSpawn() {
base.OnNetworkSpawn();
RR.ServerTick += OnServerTick;
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
RR.ServerTick -= OnServerTick;
}
void Update() {
foreach (Actor actor in m_InSceneActors) {
actor.ClientTick(Time.deltaTime);
}
}
void OnServerTick(ulong tick) {
if (!IsServer) {
return;
}
float dt = 1.0f / RR.TickRate.IndexValue;
foreach (Actor actor in m_InSceneActors) {
actor.ServerTick(dt);
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 (Actor actor in m_SpawnedActors) {
actor.ServerTick(dt);
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}");
}
}
}
}
[ClientRpc(Delivery = RpcDelivery.Unreliable)]
void SynchronizeActorStateClientRpc(ulong actorID, NativeArray<byte> data) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
return;
}
DeserializeActorState(actor, data);
}
NativeArray<byte> SerializeActorState(Actor actor) {
return DataSerializationUtils.Serialize(actor.Data);
}
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, 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;
}
SpawnActorRpc(actor.SourceActorPath,
actor.ActorID,
actor.transform.position,
actor.transform.localRotation,
data,
sendParams);
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void SynchronizeActorStateForClientRpc(ulong actorID, NativeArray<byte> data, RpcParams rpcParams) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
return;
}
DeserializeActorState(actor, data);
ClientSynchronizedActorRpc();
}
[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);
}
[Rpc(SendTo.Everyone)]
internal void SendActorEventToClientsRpc(ActorEvent actorEvent) {
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 is not 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;
}
public void SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!IsServer) {
s_Logger.Error("Only the server can spawn actors.");
return;
}
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
return;
}
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;
}
actor.SourceActorPath = assetReference.AssetGUID;
actor.ActorID = UniqueID.NewULongFromGuid();
actor.Data = actor.InternalCreateActorData();
m_SpawnedActors.Add(actor);
NativeArray<byte> stateData = SerializeActorState(actor);
SpawnActorRpc(assetReference.AssetGUID, actor.ActorID, position, rotation, stateData, RpcTarget.NotMe);
}
// @NOTE: This RPC is used to spawn actors on clients.
[Rpc(SendTo.SpecifiedInParams)]
void SpawnActorRpc(string guid,
ulong actorID,
Vector3 position,
Quaternion rotation,
NativeArray<byte> stateData,
RpcParams rpcParams) {
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
return;
}
GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion();
if (actorObject == null) {
s_Logger.Error($"Failed to instantiate actor with GUID {guid}");
return;
}
Actor actor = actorObject.GetComponent<Actor>();
if (actor is null) {
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
Destroy(actorObject);
return;
}
actor.SourceActorPath = guid;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
DeserializeActorState(actor, stateData);
m_SpawnedActors.Add(actor);
}
public void KillActor(Actor actor) {
if (!IsServer) {
s_Logger.Error("Only the server can kill actors.");
return;
}
if (actor is null) {
s_Logger.Error("Trying to kill a null actor.");
return;
}
if (!m_SpawnedActors.Remove(actor)) {
s_Logger.Error($"Trying to kill an actor that is not registered: {actor.name}. " +
"Remember you can only kill actors spawned that are dynamically created");
return;
}
Destroy(actor.gameObject);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: af0c5ff8ac4945eca2a9cab0a88268f4
timeCreated: 1751411111

View File

@@ -4,4 +4,8 @@ namespace RebootKit.Engine.Simulation.Sensors {
public interface ISensor {
GameObject Sense();
}
public interface ISensor<out T> where T : class {
T Sense();
}
}

View File

@@ -4,6 +4,7 @@ using Cysharp.Threading.Tasks;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Services.Simulation;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
@@ -27,14 +28,19 @@ namespace RebootKit.Engine.Simulation {
WorldState m_WorldState = WorldState.Unloaded;
WorldConfig m_Config;
AsyncOperationHandle<SceneInstance> m_SceneInstance;
enum ActorOrigin {
InScene,
Prefab
}
struct ActorData {
public ActorOrigin Origin;
public Actor Actor;
public readonly bool ManagedByAddressables;
public ActorData(Actor actor, bool managedByAddressables) {
public ActorData(ActorOrigin origin, Actor actor) {
Origin = origin;
Actor = actor;
ManagedByAddressables = managedByAddressables;
}
}
@@ -68,7 +74,9 @@ namespace RebootKit.Engine.Simulation {
await m_SceneInstance.Result.ActivateAsync();
SceneManager.SetActiveScene(m_SceneInstance.Result.Scene);
// await UniTask.WaitWhile(() => RR.CoreNetworkGameSystemsInstance is null, cancellationToken: cancellationToken);
foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) {
if (root.TryGetComponent(out IWorldContext worldContext)) {
Assert.IsNull(Context,
@@ -77,15 +85,11 @@ namespace RebootKit.Engine.Simulation {
}
foreach (Actor actor in root.GetComponentsInChildren<Actor>()) {
m_Actors.Add(new ActorData(actor, false));
RR.NetworkSystemInstance.Actors.RegisterInSceneActor(actor);
}
}
m_WorldState = WorldState.Loaded;
if (RR.GameInstance is not null) {
RR.GameInstance.OnWorldLoaded();
}
}
public void Unload() {
@@ -93,12 +97,10 @@ namespace RebootKit.Engine.Simulation {
return;
}
if (RR.GameInstance is not null) {
RR.GameInstance.OnWorldUnload();
if (RR.NetworkSystemInstance is not null) {
RR.NetworkSystemInstance.Actors.CleanUp();
}
KillAllActors();
if (m_SceneInstance.IsValid()) {
m_SceneInstance.Release();
m_SceneInstance = default;
@@ -107,79 +109,5 @@ namespace RebootKit.Engine.Simulation {
m_WorldState = WorldState.Unloaded;
Context = null;
}
public async UniTask<TActor> SpawnActor<TActor>(AssetReferenceT<GameObject> asset,
CancellationToken cancellationToken) where TActor : Actor {
if (m_WorldState != WorldState.Loaded) {
s_Logger.Error("World is not loaded. Cannot spawn actor.");
return null;
}
GameObject gameObject = await Addressables.InstantiateAsync(asset);
if (cancellationToken.IsCancellationRequested) {
asset.ReleaseInstance(gameObject);
return null;
}
if (gameObject.TryGetComponent(out TActor actor)) {
actor.OnSpawned();
actor.IsPlaying = true;
m_Actors.Add(new ActorData(actor, true));
return actor;
}
asset.ReleaseInstance(gameObject);
return null;
}
public void KillActor(Actor actor) {
ActorData actorData = default;
bool found = false;
for (int i = m_Actors.Count - 1; i >= 0; i--) {
if (m_Actors[i].Actor == actor) {
found = true;
actorData = m_Actors[i];
m_Actors.RemoveAt(i);
break;
}
}
Assert.IsTrue(found, $"Actor {actor.name} not found in the world actors list.");
actor.IsPlaying = false;
actor.OnDespawned();
if (actorData.ManagedByAddressables) {
Addressables.ReleaseInstance(actor.gameObject);
}
}
public void KillAllActors() {
foreach (ActorData actorData in m_Actors) {
actorData.Actor.IsPlaying = false;
actorData.Actor.OnDespawned();
if (actorData.ManagedByAddressables) {
Addressables.ReleaseInstance(actorData.Actor.gameObject);
} else {
UnityEngine.Object.Destroy(actorData.Actor.gameObject);
}
}
m_Actors.Clear();
}
public void Tick(float deltaTime) {
if (m_WorldState != WorldState.Loaded) {
return;
}
foreach (ActorData actorData in m_Actors) {
Actor actor = actorData.Actor;
if (actor.IsPlaying) {
actor.Tick(deltaTime);
}
}
}
}
}

View File

@@ -0,0 +1,87 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1321683558189709310
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1878935056881269035}
- component: {fileID: 258784011466397156}
- component: {fileID: 8437866359923088342}
- component: {fileID: 2149791309811179493}
m_Layer: 0
m_Name: core_network_game_systems
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1878935056881269035
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1321683558189709310}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &258784011466397156
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1321683558189709310}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier:
GlobalObjectIdHash: 2588764075
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 0
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 0
SyncOwnerTransformWhenParented: 0
AllowOwnerToParent: 0
--- !u!114 &8437866359923088342
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1321683558189709310}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1f967d37c17e4704b80849c305a53be9, type: 3}
m_Name:
m_EditorClassIdentifier:
<Actors>k__BackingField: {fileID: 2149791309811179493}
--- !u!114 &2149791309811179493
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1321683558189709310}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: af0c5ff8ac4945eca2a9cab0a88268f4, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2cc631d24ab41194ebdeffff7faf62a5
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -421,7 +421,7 @@ MonoBehaviour:
NetworkManagerExpanded: 0
NetworkConfig:
ProtocolVersion: 0
NetworkTransport: {fileID: 1456272201}
NetworkTransport: {fileID: 1456272198}
PlayerPrefab: {fileID: 0}
Prefabs:
NetworkPrefabsLists:
@@ -434,7 +434,7 @@ MonoBehaviour:
TimeResyncInterval: 30
EnsureNetworkVariableLengthSafety: 0
EnableSceneManagement: 0
ForceSamePrefabs: 1
ForceSamePrefabs: 0
RecycleNetworkIds: 1
NetworkIdRecycleDelay: 120
RpcHashSize: 0