multiplayer refactor

This commit is contained in:
2025-07-21 09:04:43 +02:00
parent 1054061d91
commit a0a0f6303d
29 changed files with 2186 additions and 603 deletions

View File

@@ -1,7 +1,11 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using NUnit.Framework;
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Network;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
@@ -27,12 +31,10 @@ namespace RebootKit.Engine.Simulation {
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
RR.ServerTick += OnServerTick;
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
RR.ServerTick -= OnServerTick;
}
//
@@ -51,13 +53,11 @@ namespace RebootKit.Engine.Simulation {
//
// @MARK: Server-side logic
//
void OnServerTick(ulong tick) {
public void ServerTick(float dt) {
if (!IsServer) {
return;
}
float dt = 1.0f / RR.TickRate.IndexValue;
TickActorsList(m_InSceneActors, dt);
TickActorsList(m_SpawnedActors, dt);
}
@@ -69,78 +69,28 @@ namespace RebootKit.Engine.Simulation {
if (actor.IsDataDirty) {
actor.IsDataDirty = false;
NativeArray<byte> data = SerializeActorState(actor);
if (data.IsCreated) {
SendActorStateToClients(actor.ActorID, data);
} else {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
if (actor.Data.GetMaxBytes() > 0) {
RR.NetworkSystemInstance.WriteActorState(NetworkPacketTarget.AllClients(), actor.ActorID, actor.Data);
}
}
if (actor.IsCoreStateDirty) {
actor.IsCoreStateDirty = false;
RR.NetworkSystemInstance.WriteActorCoreState(NetworkPacketTarget.AllClients(),
actor.ActorID,
actor.GetCoreStateSnapshot());
}
if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) {
ActorTransformSyncData syncData = actor.GetTransformSyncData();
foreach ((ulong _, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (state.IsReady) {
SynchronizeActorTransformStateRpc(actor.ActorID, syncData, RpcTarget.NotMe);
}
}
RR.NetworkSystemInstance.WriteActorTransformState(NetworkPacketTarget.AllClients(),
actor.ActorID,
syncData);
}
}
}
internal void SynchronizeActorCoreStateWithOther(Actor actor) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can synchronize actor core states.");
return;
}
SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe);
}
void SendActorStateToClients(ulong actorID, NativeArray<byte> data) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can synchronize actor states with clients.");
return;
}
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (state.IsReady) {
SynchronizeActorStateRpc(actorID, data, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
void SynchronizeActorStateRpc(ulong actorID, NativeArray<byte> data, RpcParams rpcParams) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
return;
}
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
DeserializeActorState(actor, data);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
void SynchronizeActorTransformStateRpc(ulong actorID, ActorTransformSyncData syncData, RpcParams rpcParams) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {actorID} not found for transform synchronization.");
return;
}
actor.RestoreTransformState(syncData);
}
NativeArray<byte> SerializeActorState(Actor actor) {
return DataSerializationUtils.Serialize(actor.Data);
}
void DeserializeActorState(Actor actor, NativeArray<byte> data) {
DataSerializationUtils.Deserialize(data, ref actor.Data);
}
//
// @MARK: Server API
//
@@ -172,73 +122,14 @@ namespace RebootKit.Engine.Simulation {
m_SpawnedActors.Add(actor);
NativeArray<byte> stateData = SerializeActorState(actor);
SpawnActorRpc(assetReference.AssetGUID,
actor.ActorID,
actor.GetCoreStateSnapshot(),
stateData,
RpcTarget.NotMe);
RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.AllClients(),
assetReference.AssetGUID,
actor.ActorID,
actor.GetCoreStateSnapshot(),
actor.Data);
return actor;
}
// @NOTE: This RPC is used to spawn actors on clients.
[Rpc(SendTo.SpecifiedInParams)]
void SpawnActorRpc(string guid,
ulong actorID,
ActorCoreStateSnapshot coreStateSnapshot,
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(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.");
Destroy(actorObject);
return;
}
actor.Manager = this;
actor.SourceActorPath = guid;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
actor.RestoreCoreState(coreStateSnapshot);
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);
}
public void CleanUp() {
if (IsServer) {
CleanUpRpc();
@@ -259,7 +150,7 @@ namespace RebootKit.Engine.Simulation {
void CleanUpRpc() {
CleanUp();
}
//
// @MARK: Common API
//
@@ -325,60 +216,27 @@ namespace RebootKit.Engine.Simulation {
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
RR.NetworkSystemInstance.UpdateClientState(clientState);
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
s_Logger.Info($"Starting actor synchronization for client {clientID}.\n" +
$"InScene Actors to sync: {m_InSceneActors.Count}\n" +
$"Actors to spawn: {m_SpawnedActors.Count}");
foreach (Actor actor in m_InSceneActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams);
RR.NetworkSystemInstance.WriteActorSynchronize(NetworkPacketTarget.Single(clientID),
actor.ActorID,
actor.GetCoreStateSnapshot(),
actor.Data);
}
foreach (Actor actor in m_SpawnedActors) {
NativeArray<byte> data = SerializeActorState(actor);
if (!data.IsCreated) {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
continue;
}
ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot();
SpawnActorRpc(actor.SourceActorPath,
actor.ActorID,
coreStateSnapshot,
data,
sendParams);
s_Logger.Info("Spawning actor for client synchronization: " + actor.SourceActorPath);
RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.Single(clientID),
actor.SourceActorPath,
actor.ActorID,
actor.GetCoreStateSnapshot(),
actor.Data);
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void SynchronizeActorStateForClientRpc(ulong actorID,
ActorCoreStateSnapshot coreStateSnapshot,
NativeArray<byte> data,
RpcParams rpcParams) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
return;
}
actor.RestoreCoreState(coreStateSnapshot);
DeserializeActorState(actor, data);
ClientSynchronizedActorRpc();
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void SynchronizeCoreActorStateRpc(ulong actorID, ActorCoreStateSnapshot snapshot, RpcParams rpcParams) {
Actor actor = FindActorByID(actorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {actorID} not found for core state synchronization.");
return;
}
actor.RestoreCoreState(snapshot);
}
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
void ClientSynchronizedActorRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
@@ -389,6 +247,7 @@ namespace RebootKit.Engine.Simulation {
}
clientState.ActorsSyncPacketsLeft--;
s_Logger.Info($"Synchronized actor for client {clientID}. Packets left: {clientState.ActorsSyncPacketsLeft}");
RR.NetworkSystemInstance.UpdateClientState(clientState);
if (clientState.ActorsSyncPacketsLeft == 0) {
@@ -396,6 +255,129 @@ namespace RebootKit.Engine.Simulation {
}
}
///
/// @MARK: Network Data Handling
///
internal void OnReceivedEntity(NetworkDataHeader header, NativeArray<byte> data) {
if (header.Type == NetworkDataType.ActorCoreState) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for core state update.");
return;
}
using NetworkBufferReader reader = new NetworkBufferReader(data);
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
coreState.Deserialize(reader);
actor.RestoreCoreState(coreState);
} else if (header.Type == NetworkDataType.ActorTransformSync) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for transform state update.");
return;
}
using NetworkBufferReader reader = new NetworkBufferReader(data);
ActorTransformSyncData transformSyncData = new ActorTransformSyncData();
transformSyncData.Deserialize(reader);
actor.RestoreTransformState(transformSyncData);
} else if (header.Type == NetworkDataType.ActorState) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for state update.");
return;
}
DataSerializationUtils.Deserialize(data, ref actor.Data);
} else if (header.Type == NetworkDataType.ActorEvent) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for event handling.");
return;
}
throw new NotImplementedException();
} else if (header.Type == NetworkDataType.ActorCommand) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for command handling.");
return;
}
throw new NotImplementedException();
} else if (header.Type == NetworkDataType.SynchronizeActor) {
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for synchronization.");
return;
}
using NetworkBufferReader reader = new NetworkBufferReader(data);
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
coreState.Deserialize(reader);
reader.Read(out ushort actorDataSize);
reader.Read(out NativeArray<byte> stateData, actorDataSize);
actor.RestoreCoreState(coreState);
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
ClientSynchronizedActorRpc();
} else if (header.Type == NetworkDataType.SpawnActor) {
using NetworkBufferReader reader = new NetworkBufferReader(data);
reader.Read(out FixedString64Bytes value);
string guid = value.ToString();
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
coreState.Deserialize(reader);
reader.Read(out ushort actorDataSize);
reader.Read(out NativeArray<byte> stateData, actorDataSize);
SpawnLocalActor(guid,
header.ActorID,
coreState,
stateData);
}
}
void SpawnLocalActor(string guid,
ulong actorID,
ActorCoreStateSnapshot coreStateSnapshot,
NativeArray<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.");
Destroy(actorObject);
return;
}
actor.Manager = this;
actor.SourceActorPath = guid;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
actor.RestoreCoreState(coreStateSnapshot);
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
m_SpawnedActors.Add(actor);
}
//
// @MARK: Actor Commands and Events
//
@@ -422,6 +404,10 @@ namespace RebootKit.Engine.Simulation {
}
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (NetworkManager.Singleton.LocalClientId == clientID) {
continue;
}
if (state.IsReady) {
SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}