469 lines
16 KiB
C#
469 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using RebootKit.Engine.Extensions;
|
|
using RebootKit.Engine.Foundation;
|
|
using RebootKit.Engine.Main;
|
|
using RebootKit.Engine.Network;
|
|
using Unity.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.AddressableAssets;
|
|
using UnityEngine.Assertions;
|
|
using Logger = RebootKit.Engine.Foundation.Logger;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace RebootKit.Engine.Simulation {
|
|
// @TODO:
|
|
// - Actors States might be packed into chunks to reduce the number of RPCs sent.
|
|
// - Release addressables when they are no longer needed.
|
|
public class ActorsManager : IDisposable {
|
|
static readonly Logger s_Logger = new Logger(nameof(ActorsManager));
|
|
|
|
readonly NetworkSystem m_Network;
|
|
|
|
readonly List<Actor> m_InSceneActors = new List<Actor>();
|
|
readonly List<Actor> m_SpawnedActors = new List<Actor>();
|
|
|
|
ushort m_ActorIDCounter = 0;
|
|
|
|
public ushort InSceneActorsCount { get { return (ushort) m_InSceneActors.Count; } }
|
|
public ushort SpawnedActorsCount { get { return (ushort) m_SpawnedActors.Count; } }
|
|
public int TotalActorsCount { get { return InSceneActorsCount + SpawnedActorsCount; } }
|
|
|
|
public ActorsManager(NetworkSystem networkSystem) {
|
|
m_Network = networkSystem;
|
|
}
|
|
|
|
public void Dispose() { }
|
|
|
|
//
|
|
// @MARK: Update
|
|
//
|
|
public void Tick(float deltaTime) {
|
|
foreach (Actor actor in m_InSceneActors) {
|
|
actor.OnClientTick(deltaTime);
|
|
}
|
|
|
|
foreach (Actor actor in m_SpawnedActors) {
|
|
actor.OnClientTick(deltaTime);
|
|
}
|
|
}
|
|
|
|
//
|
|
// @MARK: Server-side logic
|
|
//
|
|
public void ServerTick(float dt) {
|
|
if (!RR.IsServer()) {
|
|
return;
|
|
}
|
|
|
|
TickActorsList(m_InSceneActors, dt);
|
|
TickActorsList(m_SpawnedActors, dt);
|
|
}
|
|
|
|
void TickActorsList(List<Actor> actors, float deltaTime) {
|
|
foreach (Actor actor in actors) {
|
|
actor.OnServerTick(deltaTime);
|
|
|
|
if (actor.IsDataDirty) {
|
|
actor.IsDataDirty = false;
|
|
|
|
if (actor.Data.GetMaxBytes() > 0) {
|
|
foreach (NetworkClientState client in m_Network.Clients.Values) {
|
|
if (client.IsServer) {
|
|
continue;
|
|
}
|
|
|
|
m_Network.WriteActorState(client.ClientID, actor.ActorID, actor.Data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (actor.IsCoreStateDirty) {
|
|
actor.IsCoreStateDirty = false;
|
|
|
|
foreach (NetworkClientState client in m_Network.Clients.Values) {
|
|
if (client.IsServer) {
|
|
continue;
|
|
}
|
|
|
|
m_Network.WriteActorCoreState(client.ClientID, actor.GetCoreStateSnapshot());
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (NetworkClientState client in m_Network.Clients.Values) {
|
|
if (client.IsServer) {
|
|
continue;
|
|
}
|
|
|
|
foreach (Actor actor in actors.OrderBy(t => t.GetLastSyncTick(client.ClientID))) {
|
|
if (!actor.syncTransform || actor.MasterActor != null) {
|
|
continue;
|
|
}
|
|
|
|
ActorTransformSyncData syncData = actor.GetTransformSyncDataForClient(client.ClientID);
|
|
if (syncData.SyncMode == ActorTransformSyncMode.None) {
|
|
continue;
|
|
}
|
|
|
|
if (m_Network.WriteActorTransformState(client.ClientID, syncData)) {
|
|
actor.UpdateClientState(client.ClientID, RR.Network.TickCount);
|
|
} else {
|
|
// @NOTE: We ran out of space in the packet
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @MARK: Server API
|
|
//
|
|
public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Error("Only the server can spawn actors.");
|
|
return null;
|
|
}
|
|
|
|
if (!assetReference.RuntimeKeyIsValid()) {
|
|
s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
|
|
return null;
|
|
}
|
|
|
|
if (!TryGenerateNextActorID(out ushort actorID)) {
|
|
s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID.");
|
|
return null;
|
|
}
|
|
|
|
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.");
|
|
Object.Destroy(actorObject);
|
|
return null;
|
|
}
|
|
|
|
actor.Manager = this;
|
|
actor.SourceActorPath = assetReference.AssetGUID;
|
|
actor.ActorID = actorID;
|
|
actor.Data = actor.InternalCreateActorData();
|
|
m_SpawnedActors.Add(actor);
|
|
|
|
foreach (NetworkClientState client in m_Network.Clients.Values) {
|
|
if (client.IsServer) {
|
|
continue;
|
|
}
|
|
|
|
m_Network.SendSpawnActor(client.ClientID,
|
|
assetReference.AssetGUID,
|
|
actor.GetCoreStateSnapshot(),
|
|
actor.Data);
|
|
}
|
|
|
|
return actor;
|
|
}
|
|
|
|
bool TryGenerateNextActorID(out ushort actorID) {
|
|
m_ActorIDCounter += 1;
|
|
actorID = m_ActorIDCounter;
|
|
return true;
|
|
}
|
|
|
|
internal void AssignActorsIDs() {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Info("Only the server can assign actors IDs.");
|
|
return;
|
|
}
|
|
|
|
m_ActorIDCounter = 0;
|
|
|
|
foreach (Actor actor in m_InSceneActors) {
|
|
if (!TryGenerateNextActorID(out ushort actorID)) {
|
|
s_Logger.Error("Failed to generate actor ID. Probably reached the limit of 65535 actors.");
|
|
return;
|
|
}
|
|
|
|
actor.ActorID = actorID;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @MARK: Common API
|
|
//
|
|
public void RegisterInSceneActor(Actor actor) {
|
|
if (actor.Data == null) {
|
|
actor.Data = actor.InternalCreateActorData();
|
|
}
|
|
|
|
actor.Manager = this;
|
|
|
|
m_InSceneActors.Add(actor);
|
|
}
|
|
|
|
Actor FindInSceneActorWithStaticID(ulong staticID) {
|
|
foreach (Actor actor in m_InSceneActors) {
|
|
if (actor.ActorStaticID == staticID) {
|
|
return actor;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Actor FindActorByID(ushort 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 CleanUp() {
|
|
m_InSceneActors.Clear();
|
|
|
|
foreach (Actor actor in m_SpawnedActors) {
|
|
if (actor.OrNull() != null) {
|
|
Object.Destroy(actor.gameObject);
|
|
}
|
|
}
|
|
|
|
m_SpawnedActors.Clear();
|
|
}
|
|
|
|
///
|
|
/// @MARK: Network Data Handling
|
|
///
|
|
internal void ProcessActorEvent(in ActorEvent actorEvent) {
|
|
Actor actor = FindActorByID(actorEvent.ActorID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {actorEvent.ActorID} for event handling.");
|
|
return;
|
|
}
|
|
|
|
actor.HandleActorEvent(actorEvent);
|
|
}
|
|
|
|
internal void ProcessActorCommand(ulong senderID, in ActorCommand actorCommand) {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Error("Received ActorCommand on client, but this should only be handled on the server.");
|
|
return;
|
|
}
|
|
|
|
Actor actor = FindActorByID(actorCommand.ActorID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {actorCommand.ActorID} for command handling.");
|
|
return;
|
|
}
|
|
|
|
actor.HandleActorCommand(senderID, actorCommand);
|
|
}
|
|
|
|
internal void ProcessActorCoreState(in ActorCoreStateSnapshot coreState) {
|
|
if (RR.IsServer()) {
|
|
s_Logger.Error("Received ActorCoreState on server, but this should only be handled on the client.");
|
|
return;
|
|
}
|
|
|
|
Actor actor = FindActorByID(coreState.ActorID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {coreState.ActorID} for core state update.");
|
|
return;
|
|
}
|
|
|
|
actor.RestoreCoreState(coreState);
|
|
}
|
|
|
|
internal void ProcessActorState(ushort actorID, NativeSlice<byte> stateData) {
|
|
if (RR.IsServer()) {
|
|
s_Logger.Error("Received ActorState on server, but this should only be handled on the client.");
|
|
return;
|
|
}
|
|
|
|
Actor actor = FindActorByID(actorID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {actorID} for state update.");
|
|
return;
|
|
}
|
|
|
|
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
|
|
}
|
|
|
|
internal void ProcessActorTransformState(in ActorTransformSyncData transformData) {
|
|
if (RR.IsServer()) {
|
|
s_Logger.Error("Received ActorTransformSync on server, but this should only be handled on the client.");
|
|
return;
|
|
}
|
|
|
|
Actor actor = FindActorByID(transformData.ActorID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {transformData.ActorID} for transform state update.");
|
|
return;
|
|
}
|
|
|
|
actor.RestoreTransformState(transformData);
|
|
}
|
|
|
|
internal void ProcessSpawnActor(in FixedString64Bytes assetGUID,
|
|
in ActorCoreStateSnapshot coreState,
|
|
in NativeSlice<byte> stateData) {
|
|
if (RR.IsServer()) {
|
|
s_Logger.Error("Received SpawnActor on server, but this should only be handled on the client.");
|
|
return;
|
|
}
|
|
|
|
if (assetGUID.IsEmpty) {
|
|
s_Logger.Error("Received SpawnActor with empty asset GUID, this should not happen.");
|
|
return;
|
|
}
|
|
|
|
SpawnLocalActor(assetGUID.ToString(),
|
|
coreState,
|
|
stateData);
|
|
}
|
|
|
|
void SpawnLocalActor(string guid,
|
|
ActorCoreStateSnapshot coreStateSnapshot,
|
|
NativeSlice<byte> stateData) {
|
|
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
|
|
if (!assetReference.RuntimeKeyIsValid()) {
|
|
s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
|
|
return;
|
|
}
|
|
|
|
GameObject actorObject = assetReference
|
|
.InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.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.");
|
|
Object.Destroy(actorObject);
|
|
return;
|
|
}
|
|
|
|
actor.Manager = this;
|
|
actor.SourceActorPath = guid;
|
|
actor.ActorID = coreStateSnapshot.ActorID;
|
|
actor.Data = actor.InternalCreateActorData();
|
|
|
|
if (!RR.IsServer()) {
|
|
actor.InitializeOnClient();
|
|
}
|
|
|
|
actor.RestoreCoreState(coreStateSnapshot);
|
|
if (stateData.Length > 0) {
|
|
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
|
|
}
|
|
|
|
m_SpawnedActors.Add(actor);
|
|
}
|
|
|
|
//
|
|
// @MARK: Actor Commands and Events
|
|
//
|
|
internal void SendActorEvent(ActorEvent actorEvent) {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Error("Only the server can send actor events.");
|
|
return;
|
|
}
|
|
|
|
foreach (ulong clientID in m_Network.Clients.Keys) {
|
|
m_Network.WriteActorEvent(clientID, actorEvent);
|
|
}
|
|
}
|
|
|
|
internal void SendActorCommand(ActorCommand actorCommand) {
|
|
m_Network.WriteActorCommand(actorCommand);
|
|
}
|
|
|
|
///
|
|
/// @MARK: Synchronization Helpers
|
|
///
|
|
|
|
// @TODO: We might want to sending this in chunks because it probably wont fit into a single packet in real game.
|
|
internal void WriteInSceneActorsStates(NetworkBufferWriter writer) {
|
|
writer.Write((ushort) InSceneActorsCount);
|
|
|
|
foreach (Actor actor in m_InSceneActors) {
|
|
writer.Write(actor.ActorStaticID);
|
|
writer.Write(actor.ActorID);
|
|
actor.GetCoreStateSnapshot().Serialize(writer);
|
|
|
|
writer.Write((byte) actor.Data.GetMaxBytes());
|
|
actor.Data.Serialize(writer);
|
|
}
|
|
}
|
|
|
|
internal bool ReadInSceneActorsStates(NetworkBufferReader reader) {
|
|
reader.Read(out ushort count);
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
reader.Read(out ulong actorStaticID);
|
|
reader.Read(out ushort actorID);
|
|
|
|
s_Logger.Info($"Reading actor with StaticID {actorStaticID} and ID {actorID} during synchronization.");
|
|
|
|
Actor actor = FindInSceneActorWithStaticID(actorStaticID);
|
|
if (actor == null) {
|
|
s_Logger.Error($"Failed to find actor with ID {actorID} during synchronization.");
|
|
return false;
|
|
}
|
|
|
|
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
|
|
coreState.Deserialize(reader);
|
|
|
|
reader.Read(out byte actorDataSize);
|
|
reader.Read(out NativeSlice<byte> stateData, actorDataSize);
|
|
|
|
actor.RestoreCoreState(coreState);
|
|
if (stateData.Length > 0) {
|
|
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
|
|
}
|
|
|
|
s_Logger.Info($"Assigning StaticID {actorStaticID} and ID {actorID}");
|
|
actor.ActorID = actorID;
|
|
s_Logger.Info("Actor id set to " + actor.ActorID);
|
|
}
|
|
|
|
foreach (Actor inSceneActor in m_InSceneActors) {
|
|
s_Logger.Info($"InSceneActor: StaticID={inSceneActor.ActorStaticID}, ID={inSceneActor.ActorID}, Path={inSceneActor.SourceActorPath}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void SpawnDynamicActorsForClient(ulong clientID) {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Error("Only the server can spawn dynamic actors for clients.");
|
|
return;
|
|
}
|
|
|
|
foreach (Actor actor in m_SpawnedActors) {
|
|
if (actor.OrNull() == null) {
|
|
continue;
|
|
}
|
|
|
|
ActorCoreStateSnapshot coreState = actor.GetCoreStateSnapshot();
|
|
m_Network.SendSpawnActor(clientID,
|
|
actor.SourceActorPath,
|
|
coreState,
|
|
actor.Data);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
} |