Replicated

[Fortress Craft] ** Network Object Pooling (Photon Fusion2, Shared Mode) ** 본문

유니티 엔진/Fortress Craft

[Fortress Craft] ** Network Object Pooling (Photon Fusion2, Shared Mode) **

라구넹 2025. 2. 5. 21:35

Unit과 Arrow 등 많은 오브젝트에 오브젝트 풀링이 적용되어야 함

유니티 프로파일러 써보면 이거 없으면 오브젝트 만들 때마다 부하가 확 심해지는 걸 확인 가능

 

일단 네트워크 오브젝트는 일반적인 오브젝트 풀링으로 안된다

그 이유는 unactive해도 동기화가 안되고 그냥 자기 꺼에서만 꺼지고, 다른 클라이언트의 화면에는 그냥 있기 때문이다

즉, SetActive 등의 행위가 RPC로 행해져야 한다

 

https://doc.photonengine.com/fusion/current/manual/advanced/network-object-provider

 

Fusion 2 NetworkObjectProvider | Photon Engine

When Spawned is called, the INetworkObjectProvider that was passed in StartGame() StartArgs handles getting/creating the GameObject that will be attac

doc.photonengine.com

이거 보고 만드려고 했는데

 

공식 문서가 굉장히 불친절하다

정작 중요한 내부를 비워버리니 써먹기가 힘들다

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;

namespace Agit.FortressCraft
{
	public class NetworkObjectPoolManager : NetworkObjectProviderDefault
	{
		public static NetworkObjectPoolManager Instance { get; set; }

		private Dictionary<NetworkObjectTypeId, Stack<NetworkObject>> _poolTable = new();

		private void Awake()
		{
			Instance = this;
		}

		public void AddPoolTable(NetworkObjectTypeId id)
		{
			if (!_poolTable.ContainsKey(id))
			{
				_poolTable.Add(id, new Stack<NetworkObject>());
			}
		}

		protected override NetworkObject InstantiatePrefab(NetworkRunner runner, NetworkObject prefab)
		{
			NetworkObject instance = null;

			instance = GetFromObjetPool(runner, prefab);


			if (instance == null)
			{
				instance = runner.Spawn(prefab, (Vector2)transform.position, Quaternion.identity);
			}

			return instance;
		}

		private NetworkObject GetFromObjetPool(NetworkRunner runner, NetworkObject prefab)
		{
			NetworkObject instance = null;

			if (_poolTable.TryGetValue(prefab.NetworkTypeId, out var pool) == true)
			{
				if (pool.TryPop(out var pooledObject) == true)
				{
					instance = pooledObject;
					//Debug.Log("Pop! - " + instance.name);
				}
			}

			if (instance == null)
			{
				instance = GetNewInstance(runner, prefab);
			}

			return instance;
		}

		private NetworkObject GetNewInstance(NetworkRunner runner, NetworkObject prefab)
		{
			NetworkObject instance = runner.Spawn(prefab, (Vector2)transform.position, Quaternion.identity);

			if (_poolTable.TryGetValue(prefab.NetworkTypeId, out var stack) == false)
			{
				stack = new Stack<NetworkObject>();
				_poolTable.Add(prefab.NetworkTypeId, stack);
			}

			return instance;
		}

		protected override void DestroyPrefabInstance(NetworkRunner runner, NetworkPrefabId prefabId, NetworkObject instance)
		{
			if (_poolTable.TryGetValue(prefabId, out var stack) == true)
			{
				Debug.Log("DestroyPrefabInstance - Pooling");
				instance.transform.SetParent(null);
				//instance.gameObject.SetActive(false);
				if (instance.TryGetComponent<NormalUnitRigidBodyMovement>(out NormalUnitRigidBodyMovement normal))
				{
                    normal.RPCSetUnactive();
					//normal.gameObject.SetActive(false);
                }
				else if (instance.TryGetComponent<Arrow>(out Arrow arrow))
                {
					Debug.Log("Arrow unactive");
                    arrow.RPCSetUnactive();
                }
				else if( instance.TryGetComponent<ArcherArrow>( out ArcherArrow archerArrow ) )
                {
					archerArrow.RPCSetUnactive(archerArrow);
                }
				stack.Push(instance);
			}
			else
			{
				//Debug.Log("DestroyPrefabInstance - Destroy");
				Destroy(instance.gameObject);
			}
		}
    }
}

어찌저찌 코드를 여기저기서 뒤져가며 찾아서 내부를 만들었다

내부 poolTable은 딕셔너리로, Key로 NetworkObjectTypeId를 가지고, Value는 네트워크 오브젝트의 스택이다

NetworkObjectTypeId는 각각 네트워크 오브젝트마다 가지는 PrefabId인데, 이걸 얻으려면 Spawn해야 얻을 수 있다

 

보면 RPCSetUnactive를 하려고 else if 연결해둔게 보이는데,

지금 생각하면 interface 만들어서 해결하는게 훨씬 코드가 간결할 거라 본다

리팩토링 필요

 

NetworkObject temp = Runner.Spawn(UnitPrefab, (Vector2)transform.position, Quaternion.identity);
id = temp.NetworkTypeId.AsPrefabId;
Destroy(temp.gameObject);
poolManager.AddPoolTable(id);

Spawner의 Spawned() 함수 일부인데, 여기서 임시로 유닛을 스폰하고 Id를 얻어서 풀 테이블에 추가한다

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;
using Fusion.Sockets;
using System;
using UnityEngine.UI;

namespace Agit.FortressCraft
{
    public class NormalUnitFire : MonoBehaviour
    {
        [SerializeField] private NetworkObject arrow;
        public Transform TargetTranform { get; set; }
        public string TargetUnit { get; set; }
        public string SecondTargetUnit { get; set; }
        private NormalUnitRigidBodyMovement normalUnit;
        public NetworkObjectPoolManager poolManager;
        
        public NetworkPrefabId id;

        private void Awake()
        {
            poolManager = NetworkObjectPoolManager.Instance;
            normalUnit = GetComponent<NormalUnitRigidBodyMovement>();
        }

        public void Fire(NetworkRunner runner)
        {
            if (normalUnit.Spawner == null) return;
            Debug.Log("Fire");
            id = normalUnit.Spawner.arrowId;
            poolManager.AddPoolTable(id);
            TargetUnit = normalUnit.TargetUnit;

            if (TargetUnit.CompareTo("Unit_" + normalUnit.OwnType) == 0)
            {
                TargetUnit = SecondTargetUnit;
            }

            NetworkPrefabAcquireContext context = new NetworkPrefabAcquireContext(id);
            NetworkObject networkObject;
            poolManager.AcquirePrefabInstance(runner, context, out networkObject);
            Debug.Log(networkObject);
            //networkObject.transform.position = transform.position;

            Arrow arrow = networkObject.GetComponent<Arrow>();
            arrow.TargetTransform = TargetTranform;
            arrow.ID = id;
            arrow.Normal = normalUnit;
            RPCSetActive(arrow, transform.position);
            arrow.RPCSetActive();

            arrow.ReserveRelease();

            AttackCollider attackCollider = networkObject.GetComponentInChildren<AttackCollider>();
            attackCollider.TargetUnit = TargetUnit;
            attackCollider.OwnType = normalUnit.OwnType;
            attackCollider.Damage = normalUnit.Damage;
        }

        [Rpc(RpcSources.All, RpcTargets.All)]
        public void RPCSetActive(Arrow arrow, Vector3 pos)
        {
            arrow.gameObject.GetComponent<Rigidbody2D>().transform.position = pos;
        }
    }
}

그걸 유닛이 받아와서 Fire()에서 이용한다

 

그리고 NetworkPrefabAcquireContext를 만들어준다

NetworkPrefabAcquireContext가 자료가 정말 없어서 찾기 힘들었는데, 이게 위에서 찾은 Id를 넣어주는 구조체이다

이걸 AcquirePrefabInstance에 넘기면 풀 테이블에서 오브젝트를 꺼내온다

 

* SetActive의 경우 풀 매니저에서 해주지 않는다. Active하기 전에 미리 설정해야 하는 것들이 존재하기 때문이다. UnActive는 풀 매니저가 해준다

 

ReserveRelease는 일정 시간이 지난 뒤 풀링되도록 하는 함수로 만들었다

 

using UnityEngine;
using Fusion;
using NetworkRigidbody2D = Fusion.Addons.Physics.NetworkRigidbody2D;
using static UnityEngine.EventSystems.PointerEventData;

namespace Agit.FortressCraft
{
    public class Arrow : NetworkBehaviour
    {
        public Transform TargetTransform { get; set; }
        private NetworkRigidbody2D _rb;
        [SerializeField] private float arrowSpeed = 5.0f;
        public NetworkPrefabId ID { get; set; }
        public NormalUnitRigidBodyMovement Normal { get; set; }
        public bool Fired { get; set; }
        private TickTimer destroyTimer;

        public override void Spawned()
        {
            _rb = GetComponent<NetworkRigidbody2D>();
        }

        public override void FixedUpdateNetwork()
        {
            if (TargetTransform == null) return;
            if( destroyTimer.Expired(Runner) )
            {
                if( gameObject.activeSelf == true )
                {
                    Release();
                }
            }

            if (Mathf.Abs(TargetTransform.position.x - transform.position.x) < 0.05f &&
                Mathf.Abs(TargetTransform.position.y - transform.position.y) < 0.05f)
            {
                _rb.Rigidbody.velocity = Vector2.zero;
                return;
            }

            Vector3 movDir = TargetTransform.position - transform.position;
            Vector3 movDirNormalized = movDir.normalized;
            _rb.Rigidbody.velocity = movDirNormalized * arrowSpeed;

            float angle = Mathf.Atan2(movDir.y, movDir.x) * Mathf.Rad2Deg;
            Quaternion targetRotation = Quaternion.AngleAxis(angle, Vector3.forward);
            transform.rotation = targetRotation;
        }

        public void ReserveRelease()
        {
            destroyTimer = TickTimer.CreateFromSeconds(Runner, 1.3f);
            //Invoke("DestroySelf", 1.3f);
        }

        public void Release()
        {
            destroyTimer = TickTimer.None;
            NetworkObjectReleaseContext context = new NetworkObjectReleaseContext(Object, ID, false, false);
            NetworkObjectPoolManager.Instance.ReleaseInstance(Runner, context);
        }

        [Rpc(RpcSources.All, RpcTargets.All)]
        public void RPCSetActive()
        {
            gameObject.SetActive(true);
        }

        [Rpc(RpcSources.All, RpcTargets.All)]
        public void RPCSetUnactive()
        {
            gameObject.SetActive(false);
        }
    }
}

Arrow 클래스이다

public void Release()
{
    destroyTimer = TickTimer.None;
    NetworkObjectReleaseContext context = new NetworkObjectReleaseContext(Object, ID, false, false);
    NetworkObjectPoolManager.Instance.ReleaseInstance(Runner, context);
}

여기서 Release를 보면 풀 매니저로 릴리즈하는 걸 확인 가능하다

 

작동 확인 가능