This commit is contained in:
2025-08-31 20:46:27 +02:00
parent 2d06552025
commit 104259f79b
11 changed files with 310 additions and 137 deletions

View File

@@ -1,24 +1,53 @@
using UnityEngine;
using System;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace RebootKit.Engine.Animations {
[Serializable]
public class AnimationClipNode : IReAnimatorNode {
[field: SerializeField] public string Name { get; private set; }
AnimationClipPlayable m_Playable;
bool m_PlayOnce;
Action m_Callback;
public AnimationClip Clip;
public void Tick(float deltaTime) {
if (m_Playable.GetPlayState() == PlayState.Paused) {
return;
}
if (m_PlayOnce) {
if (m_Playable.GetTime() >= m_Playable.GetAnimationClip().length) {
m_Playable.Pause();
m_Callback?.Invoke();
m_Callback = null;
m_PlayOnce = false;
}
}
}
public IPlayable Build(PlayableGraph graph) {
AnimationClipPlayable playable = AnimationClipPlayable.Create(graph, Clip);
return playable;
m_Playable = AnimationClipPlayable.Create(graph, Clip);
return m_Playable;
}
public bool TryFindChild(string name, out IReAnimatorNode node) {
node = null;
return false;
}
public void PlayOnceWithCallback(Action callback) {
m_Playable.SetTime(0.0);
m_Playable.Play();
m_Callback = callback;
m_PlayOnce = true;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace RebootKit.Engine.Animations {
[Serializable]
public class LayerMixerNode : IReAnimatorNode {
[Serializable]
struct LayerState {
[SerializeReference] public IReAnimatorNode node;
public AvatarMask mask;
public bool isAdditive;
[Range(0.0f, 1.0f)] public float targetWeight;
[NonSerialized] public float CurrentWeight;
}
[field: SerializeField] public string Name { get; private set; }
AnimationLayerMixerPlayable m_LayerMixer;
[SerializeField] float m_TransitionSpeed = 5.0f;
[SerializeField] LayerState[] m_Layers;
public void Tick(float deltaTime) {
for (int i = 0; i < m_Layers.Length; ++i){
if (m_TransitionSpeed > 0.0f) {
m_Layers[i].CurrentWeight = Mathf.MoveTowards(m_Layers[i].CurrentWeight,
m_Layers[i].targetWeight,
m_TransitionSpeed * deltaTime);
} else {
m_Layers[i].CurrentWeight = m_Layers[i].targetWeight;
}
m_LayerMixer.SetInputWeight(i, m_Layers[i].CurrentWeight);
m_Layers[i].node.Tick(deltaTime);
}
}
public IPlayable Build(PlayableGraph graph) {
m_LayerMixer = AnimationLayerMixerPlayable.Create(graph, m_Layers.Length);
for (int i = 0; i < m_Layers.Length; ++i) {
IPlayable playable = m_Layers[i].node.Build(graph);
if (playable is AnimationMixerPlayable mixerPlayable) {
m_LayerMixer.ConnectInput(i, mixerPlayable, 0, m_Layers[i].targetWeight);
} else if (playable is AnimationClipPlayable clipPlayable) {
m_LayerMixer.ConnectInput(i, clipPlayable, 0, m_Layers[i].targetWeight);
} else if (playable is AnimationLayerMixerPlayable layerMixerPlayable) {
m_LayerMixer.ConnectInput(i, layerMixerPlayable, 0, m_Layers[i].targetWeight);
}
m_LayerMixer.SetLayerAdditive((uint)i, m_Layers[i].isAdditive);
if (m_Layers[i].mask != null) {
m_LayerMixer.SetLayerMaskFromAvatarMask((uint)i, m_Layers[i].mask);
}
}
return m_LayerMixer;
}
public bool TryFindChild(string name, out IReAnimatorNode node) {
for (int i = 0; i < m_Layers.Length; i++) {
if (m_Layers[i].node.Name.Equals(name, StringComparison.Ordinal)) {
node = m_Layers[i].node;
return true;
}
if (m_Layers[i].node.TryFindChild(name, out IReAnimatorNode childNode)) {
node = childNode;
return true;
}
}
node = null;
return false;
}
public void SetLayerWeight(int index, float weight) {
m_Layers[index].targetWeight = weight;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 977149bceb2944b1b405035704d40f52
timeCreated: 1754706607

View File

@@ -47,6 +47,8 @@ namespace RebootKit.Engine.Animations {
m_Mixer.ConnectInput(i, mixerPlayable, 0, m_Inputs[i].targetWeight);
} else if (playable is AnimationClipPlayable clipPlayable) {
m_Mixer.ConnectInput(i, clipPlayable, 0, m_Inputs[i].targetWeight);
} else if (playable is AnimationLayerMixerPlayable layerMixerPlayable) {
m_Mixer.ConnectInput(i, layerMixerPlayable, 0, m_Inputs[i].targetWeight);
}
}

View File

@@ -18,54 +18,26 @@ namespace RebootKit.Engine.Animations {
bool TryFindChild(string name, out IReAnimatorNode node);
}
[Serializable]
class ReAnimatorLayer {
public string name;
public AvatarMask mask;
public bool isAdditive;
[Range(0.0f, 1.0f)] public float weight = 1.0f;
[SerializeReference] public IReAnimatorNode root;
}
[DefaultExecutionOrder(-100)]
public class ReAnimator : MonoBehaviour {
static readonly Logger s_Logger = new Logger(nameof(ReAnimator));
[SerializeField] Animator m_Animator;
[SerializeField] ReAnimatorLayer[] m_Layers;
[SerializeField] LayerMixerNode m_Root;
PlayableGraph m_Graph;
AnimationPlayableOutput m_Output;
AnimationLayerMixerPlayable m_LayerMixer;
void Awake() {
Assert.IsNotNull(m_Animator, "Animator is not set");
Assert.IsTrue(m_Layers.Length > 0, "Atleast one layer must be defined!");
m_Animator.runtimeAnimatorController = null;
m_Graph = PlayableGraph.Create(name);
m_Output = AnimationPlayableOutput.Create(m_Graph, "Animation Output", m_Animator);
m_LayerMixer = AnimationLayerMixerPlayable.Create(m_Graph, m_Layers.Length);
m_Output.SetSourcePlayable(m_LayerMixer, 0);
for (int i = 0; i < m_Layers.Length; ++i) {
IPlayable playable = m_Layers[i].root.Build(m_Graph);
if (playable is AnimationMixerPlayable mixerPlayable) {
m_LayerMixer.ConnectInput(i, mixerPlayable, 0, m_Layers[i].weight);
} else if (playable is AnimationClipPlayable clipPlayable) {
m_LayerMixer.ConnectInput(i, clipPlayable, 0, m_Layers[i].weight);
}
m_LayerMixer.SetLayerAdditive((uint)i, m_Layers[i].isAdditive);
if (m_Layers[i].mask != null) {
m_LayerMixer.SetLayerMaskFromAvatarMask((uint)i, m_Layers[i].mask);
}
if (m_Root.Build(m_Graph) is AnimationLayerMixerPlayable layerMixer) {
m_Output.SetSourcePlayable(layerMixer, 0);
}
m_Graph.Play();
@@ -78,25 +50,16 @@ namespace RebootKit.Engine.Animations {
}
void Update() {
for (int i = 0; i < m_Layers.Length; i++) {
m_Layers[i].root.Tick(Time.deltaTime);
}
m_Root.Tick(Time.deltaTime);
}
public void SetLayerWeight(int layer, float weight) {
m_Layers[layer].weight = weight;
m_LayerMixer.SetInputWeight(layer, weight);
m_Root.SetLayerWeight(layer, weight);
}
public IReAnimatorNode FindNode(string nodeName) {
foreach (ReAnimatorLayer layer in m_Layers) {
if (layer.root.Name.Equals(nodeName, StringComparison.Ordinal)) {
return layer.root;
}
if (layer.root.TryFindChild(nodeName, out IReAnimatorNode childNode)) {
return childNode;
}
if (m_Root.TryFindChild(nodeName, out IReAnimatorNode node)) {
return node;
}
s_Logger.Error($"Couldn't find node with name: {nodeName}");

View File

@@ -224,6 +224,15 @@ namespace RebootKit.Engine.Main {
return Network.Actors.SpawnActor(assetReference, position, rotation);
}
public static void DestroyActor(Actor actor) {
if (actor == null) {
s_Logger.Error("Cannot destroy actor. Actor is null.");
return;
}
Network.Actors.DestroyActor(actor);
}
public static Actor FindSpawnedActor(ushort actorID) {
if (Network is null) {
s_Logger.Error("NetworkSystemInstance is not initialized. Cannot find actor.");
@@ -252,6 +261,16 @@ namespace RebootKit.Engine.Main {
Network.SendPossessedActor(clientID, actorID);
}
public static IEnumerable<Actor> Actors() {
foreach (Actor actor in Network.Actors.InSceneActors) {
yield return actor;
}
foreach (Actor actor in Network.Actors.SpawnedActors) {
yield return actor;
}
}
//
// @MARK: Service API
// Services seems to be useless in the current architecture. Consider removing this API in the future.

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8f2313df373f454f9f49020342ed07a8
timeCreated: 1754968660

View File

@@ -0,0 +1,22 @@
Shader "Unlit/StencilMask" {
Properties {
[IntRange] _StencilID("Stencil ID", Range(0, 255)) = 0
}
SubShader {
Tags {
"RenderType"="Opaque"
"Queue"="Geometry-1"
"RenderPipeline" = "UniversalPipeline"
}
Pass {
Blend Zero One
ZWrite Off
Stencil {
Ref [_StencilID]
Comp Always
Pass Replace
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20ca2eacae4c45dd891af0222af0295a
timeCreated: 1754968671

View File

@@ -10,6 +10,7 @@ using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Simulation {
@@ -233,16 +234,17 @@ namespace RebootKit.Engine.Simulation {
}
}
[DeclareFoldoutGroup("Actor")]
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, TriInspector.ReadOnly] public ulong ActorStaticID { get; internal set; }
[field: SerializeField, TriInspector.ReadOnly] public ushort ActorID { get; internal set; }
[field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public string SourceActorPath { get; internal set; } = "";
[field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ulong ActorStaticID { get; internal set; }
[field: SerializeField, TriInspector.ReadOnly, Group("Actor")] public ushort ActorID { get; internal set; }
[NonSerialized] internal IActorData Data;
[SerializeField] string m_ActorName = "";
[SerializeField, Group("Actor")] string m_ActorName = "";
public string ActorName {
get {
@@ -252,20 +254,20 @@ namespace RebootKit.Engine.Simulation {
internal bool IsLocalOnly;
[SerializeField] internal Rigidbody actorRigidbody;
[SerializeField, Group("Actor")] internal Rigidbody actorRigidbody;
[InfoBox("If empty, will use GetComponentsInChildren<Collider>() to find colliders.")]
[SerializeField] Collider[] m_OverrideActorColliders;
[SerializeField, Group("Actor")] Collider[] m_OverrideActorColliders;
[SerializeField] bool m_SetKinematicOnMount = true;
[SerializeField] bool m_DisableCollidersOnMount = true;
[SerializeField, Group("Actor")] bool m_SetKinematicOnMount = true;
[SerializeField, Group("Actor")] bool m_DisableCollidersOnMount = true;
internal ActorFlags Flags = ActorFlags.None;
// @NOTE: Sync won't happen if actor is mounted to another actor.
[SerializeField] internal bool syncTransform = true;
[SerializeField] internal bool syncPosition = true;
[SerializeField] internal bool syncRotation = true;
[SerializeField] internal bool syncScale = false;
[SerializeField, Group("Actor")] internal bool syncTransform = true;
[SerializeField, Group("Actor")] internal bool syncPosition = true;
[SerializeField, Group("Actor")] internal bool syncRotation = true;
[SerializeField, Group("Actor")] internal bool syncScale = false;
class ActorClientState {
public ulong LastSyncTick;
@@ -286,7 +288,7 @@ namespace RebootKit.Engine.Simulation {
public Quaternion localRotation;
}
[SerializeField] AttachmentSocket[] m_AttachmentSockets;
[SerializeField, Group("Actor")] AttachmentSocket[] m_AttachmentSockets;
// @NOTE: Master actor is the actor that this actor is attached to, if any.
internal Actor MasterActor;
@@ -296,6 +298,8 @@ namespace RebootKit.Engine.Simulation {
public bool IsDataDirty { get; protected internal set; }
internal ActorsManager Manager;
internal AsyncOperationHandle<GameObject> AssetHandle;
internal DateTime LastCoreStateSyncTime = DateTime.MinValue;
//

View File

@@ -9,6 +9,7 @@ using Unity.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;
using Logger = RebootKit.Engine.Foundation.Logger;
using Object = UnityEngine.Object;
@@ -21,14 +22,15 @@ namespace RebootKit.Engine.Simulation {
readonly NetworkSystem m_Network;
readonly List<Actor> m_InSceneActors = new List<Actor>();
readonly List<Actor> m_SpawnedActors = new List<Actor>();
internal readonly List<Actor> InSceneActors = new List<Actor>();
internal readonly List<Actor> SpawnedActors = new List<Actor>();
// @NOTE: 0 is reserved for no actor so we should start assigning IDs from 1.
ushort m_ActorIDCounter;
List<ushort> m_ActorIDFreeList = new List<ushort>(ushort.MaxValue);
public ushort InSceneActorsCount { get { return (ushort) m_InSceneActors.Count; } }
public ushort SpawnedActorsCount { get { return (ushort) m_SpawnedActors.Count; } }
public ushort InSceneActorsCount { get { return (ushort) InSceneActors.Count; } }
public ushort SpawnedActorsCount { get { return (ushort) SpawnedActors.Count; } }
public int TotalActorsCount { get { return InSceneActorsCount + SpawnedActorsCount; } }
public ActorsManager(NetworkSystem networkSystem) {
@@ -41,11 +43,11 @@ namespace RebootKit.Engine.Simulation {
// @MARK: Update
//
public void Tick(float deltaTime) {
foreach (Actor actor in m_InSceneActors) {
foreach (Actor actor in InSceneActors) {
actor.OnClientTick(deltaTime);
}
foreach (Actor actor in m_SpawnedActors) {
foreach (Actor actor in SpawnedActors) {
actor.OnClientTick(deltaTime);
}
}
@@ -58,8 +60,8 @@ namespace RebootKit.Engine.Simulation {
return;
}
TickActorsList(m_InSceneActors, dt);
TickActorsList(m_SpawnedActors, dt);
TickActorsList(InSceneActors, dt);
TickActorsList(SpawnedActors, dt);
}
void TickActorsList(List<Actor> actors, float deltaTime) {
@@ -137,19 +139,14 @@ namespace RebootKit.Engine.Simulation {
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);
Actor actor = InstantiateActor(assetReference.AssetGUID,
actorID,
position,
rotation,
null,
default);
actor.IsLocalOnly = false;
SpawnedActors.Add(actor);
foreach (NetworkClientState client in m_Network.Clients.Values) {
if (client.IsServer) {
@@ -166,6 +163,18 @@ namespace RebootKit.Engine.Simulation {
}
bool TryGenerateNextActorID(out ushort actorID) {
if (m_ActorIDFreeList.Count > 0) {
actorID = m_ActorIDFreeList[0];
m_ActorIDFreeList.RemoveAtSwapBack(0);
return true;
}
if (m_ActorIDCounter >= ushort.MaxValue) {
s_Logger.Error("Reached maximum number of actor ids which is: " + ushort.MaxValue);
actorID = 0;
return false;
}
m_ActorIDCounter += 1;
actorID = m_ActorIDCounter;
return true;
@@ -178,8 +187,9 @@ namespace RebootKit.Engine.Simulation {
}
m_ActorIDCounter = 0;
m_ActorIDFreeList.Clear();
foreach (Actor actor in m_InSceneActors) {
foreach (Actor actor in InSceneActors) {
if (!TryGenerateNextActorID(out ushort actorID)) {
s_Logger.Error("Failed to generate actor ID. Probably reached the limit of 65535 actors.");
return;
@@ -189,33 +199,30 @@ namespace RebootKit.Engine.Simulation {
}
}
public Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
return null;
public void DestroyActor(Actor actor) {
if (actor.IsLocalOnly) {
Object.Destroy(actor.gameObject);
return;
}
if (!TryGenerateNextActorID(out ushort actorID)) {
s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID.");
return null;
if (!RR.IsServer()) {
s_Logger.Error("Only the server can destroy non-local only actors");
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.");
Object.Destroy(actorObject);
return null;
if (InSceneActors.Contains(actor)) {
s_Logger.Error("InScene actors cannot be destroyed!");
return;
}
actor.Manager = this;
actor.IsLocalOnly = true;
actor.SourceActorPath = assetReference.AssetGUID;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
// m_SpawnedActors.Add(actor);
for (int i = SpawnedActors.Count - 1; i >= 0; --i) {
if (SpawnedActors[i] == actor) {
m_ActorIDFreeList.Add(actor.ActorID);
return;
}
}
return actor;
s_Logger.Error($"Failed to destroy an actor(name: {actor.ActorName}, id: {actor.ActorID})");
}
//
@@ -228,11 +235,11 @@ namespace RebootKit.Engine.Simulation {
actor.Manager = this;
m_InSceneActors.Add(actor);
InSceneActors.Add(actor);
}
Actor FindInSceneActorWithStaticID(ulong staticID) {
foreach (Actor actor in m_InSceneActors) {
foreach (Actor actor in InSceneActors) {
if (actor.ActorStaticID == staticID) {
return actor;
}
@@ -242,13 +249,13 @@ namespace RebootKit.Engine.Simulation {
}
public Actor FindActorByID(ushort actorID) {
foreach (Actor actor in m_InSceneActors) {
foreach (Actor actor in InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
foreach (Actor actor in m_SpawnedActors) {
foreach (Actor actor in SpawnedActors) {
if (actor.ActorID == actorID) {
return actor;
}
@@ -258,15 +265,40 @@ namespace RebootKit.Engine.Simulation {
}
public void CleanUp() {
m_InSceneActors.Clear();
InSceneActors.Clear();
foreach (Actor actor in m_SpawnedActors) {
foreach (Actor actor in SpawnedActors) {
if (actor.OrNull() != null) {
Object.Destroy(actor.gameObject);
}
}
m_SpawnedActors.Clear();
SpawnedActors.Clear();
}
public Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
return null;
}
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(assetReference, position, rotation);
GameObject actorObject = handle.WaitForCompletion();
Actor actor = actorObject.GetComponent<Actor>();
if (actor == null) {
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
handle.Release();
return null;
}
actor.Manager = this;
actor.AssetHandle = handle;
actor.IsLocalOnly = true;
actor.SourceActorPath = assetReference.AssetGUID;
actor.ActorID = 0;
actor.Data = actor.InternalCreateActorData();
return actor;
}
///
@@ -355,50 +387,55 @@ namespace RebootKit.Engine.Simulation {
return;
}
SpawnLocalActor(assetGUID.ToString(),
InstantiateActor(assetGUID.ToString(),
coreState.ActorID,
coreState.Position,
coreState.Rotation,
coreState,
stateData);
}
void SpawnLocalActor(string guid,
ActorCoreStateSnapshot coreStateSnapshot,
Actor InstantiateActor(string guid,
ushort actorID,
Vector3 position,
Quaternion rotation,
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;
return null;
}
GameObject actorObject = assetReference
.InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation)
.WaitForCompletion();
if (actorObject == null) {
s_Logger.Error($"Failed to instantiate actor with GUID {guid}");
return;
}
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(assetReference, position, rotation);
GameObject actorObject = handle.WaitForCompletion();
Actor actor = actorObject.GetComponent<Actor>();
if (actor is null) {
if (actor == null) {
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
Object.Destroy(actorObject);
return;
Addressables.ReleaseInstance(handle);
return null;
}
actor.Manager = this;
actor.AssetHandle = handle;
actor.SourceActorPath = guid;
actor.ActorID = coreStateSnapshot.ActorID;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
if (!RR.IsServer()) {
actor.InitializeOnClient();
}
actor.RestoreCoreState(coreStateSnapshot);
if (coreStateSnapshot.HasValue) {
actor.RestoreCoreState(coreStateSnapshot.Value);
}
if (stateData.Length > 0) {
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
}
m_SpawnedActors.Add(actor);
return actor;
}
//
@@ -427,7 +464,7 @@ namespace RebootKit.Engine.Simulation {
internal void WriteInSceneActorsStates(NetworkBufferWriter writer) {
writer.Write((ushort) InSceneActorsCount);
foreach (Actor actor in m_InSceneActors) {
foreach (Actor actor in InSceneActors) {
writer.Write(actor.ActorStaticID);
writer.Write(actor.ActorID);
actor.GetCoreStateSnapshot().Serialize(writer);
@@ -468,7 +505,7 @@ namespace RebootKit.Engine.Simulation {
s_Logger.Info("Actor id set to " + actor.ActorID);
}
foreach (Actor inSceneActor in m_InSceneActors) {
foreach (Actor inSceneActor in InSceneActors) {
s_Logger.Info($"InSceneActor: StaticID={inSceneActor.ActorStaticID}, ID={inSceneActor.ActorID}, Path={inSceneActor.SourceActorPath}");
}
@@ -481,7 +518,7 @@ namespace RebootKit.Engine.Simulation {
return;
}
foreach (Actor actor in m_SpawnedActors) {
foreach (Actor actor in SpawnedActors) {
if (actor.OrNull() == null) {
continue;
}