working on actors
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
using RebootKit.Engine.Main;
|
using System.Text;
|
||||||
|
using RebootKit.Engine.Main;
|
||||||
using RebootKit.Engine.UI;
|
using RebootKit.Engine.UI;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using Unity.Netcode.Transports.UTP;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@ namespace RebootKit.Engine.Services.Development {
|
|||||||
VisualElement m_RootElement;
|
VisualElement m_RootElement;
|
||||||
|
|
||||||
Label m_FPSLabel;
|
Label m_FPSLabel;
|
||||||
|
Label m_NetworkStatsLabel;
|
||||||
|
|
||||||
void Update() {
|
void Update() {
|
||||||
if (m_RootElement == null) {
|
if (m_RootElement == null) {
|
||||||
@@ -18,6 +22,17 @@ namespace RebootKit.Engine.Services.Development {
|
|||||||
|
|
||||||
Resolution resolution = Screen.currentResolution;
|
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_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() {
|
public override VisualElement Build() {
|
||||||
@@ -26,6 +41,8 @@ namespace RebootKit.Engine.Services.Development {
|
|||||||
CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}");
|
CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}");
|
||||||
m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}");
|
m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}");
|
||||||
|
|
||||||
|
m_NetworkStatsLabel = CreateLabel("Network Stats");
|
||||||
|
|
||||||
return m_RootElement;
|
return m_RootElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
Runtime/Engine/Code/Foundation/UniqueID.cs
Normal file
12
Runtime/Engine/Code/Foundation/UniqueID.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Runtime/Engine/Code/Foundation/UniqueID.cs.meta
Normal file
3
Runtime/Engine/Code/Foundation/UniqueID.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7e7c51400f234e5986c5d2779ca00fb3
|
||||||
|
timeCreated: 1751491055
|
||||||
@@ -10,41 +10,26 @@ namespace RebootKit.Engine.Main {
|
|||||||
public abstract class Game : NetworkBehaviour {
|
public abstract class Game : NetworkBehaviour {
|
||||||
static readonly Logger s_GameLogger = new Logger(nameof(Game));
|
static readonly Logger s_GameLogger = new Logger(nameof(Game));
|
||||||
|
|
||||||
protected NetworkVariable<FixedString128Bytes> m_CurrentWorldID =
|
// Server only callbacks
|
||||||
new NetworkVariable<FixedString128Bytes>(new FixedString128Bytes(""));
|
protected virtual void OnPlayerBecameReady(ulong clientID) {
|
||||||
|
}
|
||||||
|
|
||||||
// Event callbacks
|
// Event callbacks
|
||||||
public virtual void OnWorldLoaded() {
|
protected virtual void OnChatMessage(string message) {
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void OnWorldUnload() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void OnChatMessage(string message) {
|
|
||||||
s_GameLogger.Info($"Chat: {message}");
|
s_GameLogger.Info($"Chat: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network
|
// NGO callbacks
|
||||||
public override void OnNetworkSpawn() {
|
public override void OnNetworkSpawn() {
|
||||||
base.OnNetworkSpawn();
|
base.OnNetworkSpawn();
|
||||||
RR.GameInstance = this;
|
RR.GameInstance = this;
|
||||||
|
|
||||||
m_CurrentWorldID.OnValueChanged += OnCurrentWorldIDChanged;
|
|
||||||
LoadWorld(m_CurrentWorldID.Value.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn() {
|
public override void OnNetworkDespawn() {
|
||||||
base.OnNetworkDespawn();
|
base.OnNetworkDespawn();
|
||||||
|
|
||||||
m_CurrentWorldID.OnValueChanged -= OnCurrentWorldIDChanged;
|
|
||||||
RR.GameInstance = null;
|
RR.GameInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServerRpc]
|
|
||||||
public void SetCurrentWorldServerRpc(string worldID) {
|
|
||||||
m_CurrentWorldID.Value = new FixedString128Bytes(worldID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
[Rpc(SendTo.Server)]
|
[Rpc(SendTo.Server)]
|
||||||
public void SendChatMessageRpc(string message) {
|
public void SendChatMessageRpc(string message) {
|
||||||
@@ -56,23 +41,13 @@ namespace RebootKit.Engine.Main {
|
|||||||
OnChatMessage(message);
|
OnChatMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnCurrentWorldIDChanged(FixedString128Bytes previousValue, FixedString128Bytes newValue) {
|
internal void PlayerBecameReady(ulong clientID) {
|
||||||
string worldID = newValue.Value;
|
if (!IsServer) {
|
||||||
LoadWorld(worldID);
|
s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadWorld(string worldID) {
|
OnPlayerBecameReady(clientID);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
200
Runtime/Engine/Code/Main/NetworkSystem.cs
Normal file
200
Runtime/Engine/Code/Main/NetworkSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Runtime/Engine/Code/Main/NetworkSystem.cs.meta
Normal file
3
Runtime/Engine/Code/Main/NetworkSystem.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1f967d37c17e4704b80849c305a53be9
|
||||||
|
timeCreated: 1751411566
|
||||||
@@ -27,7 +27,7 @@ namespace RebootKit.Engine.Main {
|
|||||||
[ConfigVar("con.write_log", 1, "Enables writing game log to console output")]
|
[ConfigVar("con.write_log", 1, "Enables writing game log to console output")]
|
||||||
static ConfigVar s_writeLogToConsole;
|
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;
|
public static ConfigVar TickRate;
|
||||||
|
|
||||||
internal static EngineConfigAsset EngineConfig;
|
internal static EngineConfigAsset EngineConfig;
|
||||||
@@ -37,6 +37,9 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle;
|
static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle;
|
||||||
|
|
||||||
|
static NetworkSystem s_networkSystemPrefab;
|
||||||
|
internal static NetworkSystem NetworkSystemInstance;
|
||||||
|
|
||||||
internal static ConsoleService Console { get; private set; }
|
internal static ConsoleService Console { get; private set; }
|
||||||
public static InputService Input { get; private set; }
|
public static InputService Input { get; private set; }
|
||||||
public static WorldService World { get; private set; }
|
public static WorldService World { get; private set; }
|
||||||
@@ -68,11 +71,14 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
await InitializeAssetsAsync(cancellationToken);
|
await InitializeAssetsAsync(cancellationToken);
|
||||||
|
|
||||||
await SteamManager.InitializeAsync(cancellationToken);
|
// await SteamManager.InitializeAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @NOTE: This method is called after the main scene is loaded.
|
// @NOTE: This method is called after the main scene is loaded.
|
||||||
internal static async UniTask RunAsync(CancellationToken cancellationToken) {
|
internal static async UniTask RunAsync(CancellationToken cancellationToken) {
|
||||||
|
s_networkSystemPrefab =
|
||||||
|
Resources.Load<NetworkSystem>(RConsts.k_CoreNetworkGameSystemsResourcesPath);
|
||||||
|
|
||||||
NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent;
|
NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent;
|
||||||
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
|
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
|
||||||
NetworkManager.Singleton.OnServerStopped += OnServerStopped;
|
NetworkManager.Singleton.OnServerStopped += OnServerStopped;
|
||||||
@@ -111,7 +117,7 @@ namespace RebootKit.Engine.Main {
|
|||||||
NetworkManager.Singleton.OnServerStopped -= OnServerStopped;
|
NetworkManager.Singleton.OnServerStopped -= OnServerStopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
SteamManager.Shutdown();
|
// SteamManager.Shutdown();
|
||||||
|
|
||||||
s_servicesBag.Dispose();
|
s_servicesBag.Dispose();
|
||||||
s_disposableBag.Dispose();
|
s_disposableBag.Dispose();
|
||||||
@@ -138,7 +144,7 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
WorldConfigAsset worldConfig =
|
WorldConfigAsset worldConfig =
|
||||||
s_WorldConfigsAssets.Find(asset => asset.Config.name.Equals(name, StringComparison.Ordinal));
|
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");
|
throw new KeyNotFoundException($"World config '{name}' not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +193,29 @@ namespace RebootKit.Engine.Main {
|
|||||||
return;
|
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
|
// Service API
|
||||||
@@ -343,8 +371,6 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
TickCount++;
|
TickCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
World.Tick(deltaTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) {
|
static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) {
|
||||||
@@ -356,6 +382,9 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
GameInstance = Object.Instantiate(EngineConfig.gamePrefab);
|
GameInstance = Object.Instantiate(EngineConfig.gamePrefab);
|
||||||
GameInstance.NetworkObject.Spawn();
|
GameInstance.NetworkObject.Spawn();
|
||||||
|
|
||||||
|
NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab);
|
||||||
|
NetworkSystemInstance.NetworkObject.Spawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnServerStopped(bool obj) {
|
static void OnServerStopped(bool obj) {
|
||||||
@@ -363,10 +392,13 @@ namespace RebootKit.Engine.Main {
|
|||||||
|
|
||||||
if (GameInstance is not null) {
|
if (GameInstance is not null) {
|
||||||
GameInstance.NetworkObject.Despawn();
|
GameInstance.NetworkObject.Despawn();
|
||||||
Object.Destroy(GameInstance.gameObject);
|
GameInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameInstance = null;
|
if (NetworkSystemInstance is not null) {
|
||||||
|
NetworkSystemInstance.NetworkObject.Despawn();
|
||||||
|
NetworkSystemInstance = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Console Commands
|
// Console Commands
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName;
|
public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName;
|
||||||
public const string k_EngineConfigAssetPath = "Assets/TheGame/" + k_EngineConfigAssetName + ".asset";
|
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_AddComponentMenu = "Reboot Reality/";
|
||||||
|
|
||||||
internal const string k_CreateAssetMenu = "Reboot Reality/";
|
internal const string k_CreateAssetMenu = "Reboot Reality/";
|
||||||
|
|||||||
@@ -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 UnityEngine;
|
||||||
|
using Logger = RebootKit.Engine.Foundation.Logger;
|
||||||
|
|
||||||
namespace RebootKit.Engine.Simulation {
|
namespace RebootKit.Engine.Simulation {
|
||||||
public abstract class Actor : MonoBehaviour {
|
public interface ISerializableEntity {
|
||||||
bool m_IsPlaying = false;
|
void Serialize(FastBufferWriter writer);
|
||||||
public bool IsPlaying {
|
void Deserialize(FastBufferReader reader);
|
||||||
get {
|
|
||||||
return m_IsPlaying;
|
// @NOTE: -1 means use the default size and have hope it will fit.
|
||||||
|
int MinimumSizeInBytes() { return -1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
set {
|
public interface IActorData : ISerializableEntity { }
|
||||||
if (m_IsPlaying == value) {
|
|
||||||
|
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 {
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_IsPlaying = value;
|
if (Manager is null) {
|
||||||
|
s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})");
|
||||||
if (m_IsPlaying) {
|
return;
|
||||||
OnBeginPlay();
|
|
||||||
} else {
|
|
||||||
OnEndPlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnSpawned() {
|
if (actorCommand.ActorID != ActorID) {
|
||||||
|
s_ActorLogger
|
||||||
|
.Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnDespawned() {
|
OnActorCommandServer(actorCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnBeginPlay() {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void OnEndPlay() {
|
if (actorEvent.ActorID != ActorID) {
|
||||||
|
s_ActorLogger
|
||||||
|
.Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Tick(float deltaTime) {
|
OnActorEventClient(actorEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void SerializeNetworkState(ref DataStreamWriter writer) {
|
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 DeserializeNetworkState(ref DataStreamReader reader) {
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnValidate() {
|
||||||
|
if (ActorID == 0) {
|
||||||
|
ActorID = UniqueID.NewULongFromGuid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
338
Runtime/Engine/Code/Simulation/ActorsManager.cs
Normal file
338
Runtime/Engine/Code/Simulation/ActorsManager.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Runtime/Engine/Code/Simulation/ActorsManager.cs.meta
Normal file
3
Runtime/Engine/Code/Simulation/ActorsManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: af0c5ff8ac4945eca2a9cab0a88268f4
|
||||||
|
timeCreated: 1751411111
|
||||||
@@ -4,4 +4,8 @@ namespace RebootKit.Engine.Simulation.Sensors {
|
|||||||
public interface ISensor {
|
public interface ISensor {
|
||||||
GameObject Sense();
|
GameObject Sense();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ISensor<out T> where T : class {
|
||||||
|
T Sense();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using Cysharp.Threading.Tasks;
|
|||||||
using RebootKit.Engine.Foundation;
|
using RebootKit.Engine.Foundation;
|
||||||
using RebootKit.Engine.Main;
|
using RebootKit.Engine.Main;
|
||||||
using RebootKit.Engine.Services.Simulation;
|
using RebootKit.Engine.Services.Simulation;
|
||||||
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.AddressableAssets;
|
using UnityEngine.AddressableAssets;
|
||||||
using UnityEngine.Assertions;
|
using UnityEngine.Assertions;
|
||||||
@@ -28,13 +29,18 @@ namespace RebootKit.Engine.Simulation {
|
|||||||
WorldConfig m_Config;
|
WorldConfig m_Config;
|
||||||
AsyncOperationHandle<SceneInstance> m_SceneInstance;
|
AsyncOperationHandle<SceneInstance> m_SceneInstance;
|
||||||
|
|
||||||
struct ActorData {
|
enum ActorOrigin {
|
||||||
public Actor Actor;
|
InScene,
|
||||||
public readonly bool ManagedByAddressables;
|
Prefab
|
||||||
|
}
|
||||||
|
|
||||||
public ActorData(Actor actor, bool managedByAddressables) {
|
struct ActorData {
|
||||||
|
public ActorOrigin Origin;
|
||||||
|
public Actor Actor;
|
||||||
|
|
||||||
|
public ActorData(ActorOrigin origin, Actor actor) {
|
||||||
|
Origin = origin;
|
||||||
Actor = actor;
|
Actor = actor;
|
||||||
ManagedByAddressables = managedByAddressables;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +75,8 @@ namespace RebootKit.Engine.Simulation {
|
|||||||
await m_SceneInstance.Result.ActivateAsync();
|
await m_SceneInstance.Result.ActivateAsync();
|
||||||
SceneManager.SetActiveScene(m_SceneInstance.Result.Scene);
|
SceneManager.SetActiveScene(m_SceneInstance.Result.Scene);
|
||||||
|
|
||||||
|
// await UniTask.WaitWhile(() => RR.CoreNetworkGameSystemsInstance is null, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) {
|
foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) {
|
||||||
if (root.TryGetComponent(out IWorldContext worldContext)) {
|
if (root.TryGetComponent(out IWorldContext worldContext)) {
|
||||||
Assert.IsNull(Context,
|
Assert.IsNull(Context,
|
||||||
@@ -77,15 +85,11 @@ namespace RebootKit.Engine.Simulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (Actor actor in root.GetComponentsInChildren<Actor>()) {
|
foreach (Actor actor in root.GetComponentsInChildren<Actor>()) {
|
||||||
m_Actors.Add(new ActorData(actor, false));
|
RR.NetworkSystemInstance.Actors.RegisterInSceneActor(actor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_WorldState = WorldState.Loaded;
|
m_WorldState = WorldState.Loaded;
|
||||||
|
|
||||||
if (RR.GameInstance is not null) {
|
|
||||||
RR.GameInstance.OnWorldLoaded();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Unload() {
|
public void Unload() {
|
||||||
@@ -93,12 +97,10 @@ namespace RebootKit.Engine.Simulation {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RR.GameInstance is not null) {
|
if (RR.NetworkSystemInstance is not null) {
|
||||||
RR.GameInstance.OnWorldUnload();
|
RR.NetworkSystemInstance.Actors.CleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
KillAllActors();
|
|
||||||
|
|
||||||
if (m_SceneInstance.IsValid()) {
|
if (m_SceneInstance.IsValid()) {
|
||||||
m_SceneInstance.Release();
|
m_SceneInstance.Release();
|
||||||
m_SceneInstance = default;
|
m_SceneInstance = default;
|
||||||
@@ -107,79 +109,5 @@ namespace RebootKit.Engine.Simulation {
|
|||||||
m_WorldState = WorldState.Unloaded;
|
m_WorldState = WorldState.Unloaded;
|
||||||
Context = null;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2cc631d24ab41194ebdeffff7faf62a5
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -421,7 +421,7 @@ MonoBehaviour:
|
|||||||
NetworkManagerExpanded: 0
|
NetworkManagerExpanded: 0
|
||||||
NetworkConfig:
|
NetworkConfig:
|
||||||
ProtocolVersion: 0
|
ProtocolVersion: 0
|
||||||
NetworkTransport: {fileID: 1456272201}
|
NetworkTransport: {fileID: 1456272198}
|
||||||
PlayerPrefab: {fileID: 0}
|
PlayerPrefab: {fileID: 0}
|
||||||
Prefabs:
|
Prefabs:
|
||||||
NetworkPrefabsLists:
|
NetworkPrefabsLists:
|
||||||
@@ -434,7 +434,7 @@ MonoBehaviour:
|
|||||||
TimeResyncInterval: 30
|
TimeResyncInterval: 30
|
||||||
EnsureNetworkVariableLengthSafety: 0
|
EnsureNetworkVariableLengthSafety: 0
|
||||||
EnableSceneManagement: 0
|
EnableSceneManagement: 0
|
||||||
ForceSamePrefabs: 1
|
ForceSamePrefabs: 0
|
||||||
RecycleNetworkIds: 1
|
RecycleNetworkIds: 1
|
||||||
NetworkIdRecycleDelay: 120
|
NetworkIdRecycleDelay: 120
|
||||||
RpcHashSize: 0
|
RpcHashSize: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user