* 아주 기본적인 최적화 방법 중 하나인 오브젝트 풀링을 사용할 수 있다.
* ECS에서 생성과 파괴의 방법을 다룰 수 있다.
* 기존 방법 : Queue나 Stack 중 하나 그리고 List를 활용한다.
* ECS 방법 : EntityCommandBuffer 또는 EntityManager를 활용한다.
1.1 준비
// 생성 오브젝트 프리팹
public GameObject prefab;
// 최대 개수
public int createdCount;
// 자동 관리가 시작되는 개수.
public int managedCount;
// 꺼내기 전, 오브젝트를 담아둔 큐 -> 새로 꺼낼 때 활용.
public Queue<GameObject> prefabQueue;
// 꺼낸 후, 오브젝트를 관리하는 리스트 -> 다시 넣을 때 활용.
public List<GameObject> prefabList;
1.2 생성
private void Start()
{
CreatePool();
}
private void CreatePool()
{
prefabQueue = new Queue<GameObject>();
prefabList = new List<GameObject>();
for(int i = 0; i < createdCount; i ++)
{
// 생성
var item = Instantiate(prefab);
// 필요한 설정
...
item.name = $"Recycle_{i}";
item.transform.localscale = Vector3.one;
...
// 끄고 큐에 넣으면 준비 끝
item.SetActive(false);
prefabQueue.Enqueue(item);
}
}
1.3 대여 및 반납
/// 대여
public GameObject Dequeue()
{
// 자동 관리 시점이 되면 동작 한다.
if(prefabList.Count >= managedCount)
{
// 가장 먼저 만들었던 오래된 녀석을 가져온다.
var readyMadeItem = prefabList.First();
// 해당 오브젝트를 끄고 넣는다.
readyMadeItem.SetActive(false);
prefabQueue.Enqueue(readyMadeItem);
}
// 새로 쓸 오브젝트를, 꺼낸 후 리스트에 집어넣는다. ( * 여기서 켜지 않는다. )
var item = prefabQueue.Dequeue();
prefabList.Add(item);
reutrun item;
}
--------------- 이하 외부 스크립트 ------------------
public class Gun : MonoBehaviour
{
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space)
{
// 필요할 때 꺼내서
var item = PoolManager.Instance.Dequeue();
// 꺼낸 오브젝트의 트랜스폼 값을 초기화 해준 뒤
item.transform.localscale = Vector3.one;
item.transform.localPosition = Vector3.zero;
...
// 여기서 오브젝트를 활성화 한다.
item.SetActive(true);
}
}
}
/// 반납
public void Enqueue(GameObject go)
{
// 끄고 큐에 먼저 집어넣는다.
go.SetActive(false);
prefabQueue.Enqueue(go);
// 마지막에 리스트에서 삭제한다.
prefabList.Remove(go);
}
----------------- 이하 외부 스크립트 ------------------
public class Bullet : MonoBehaviour
{
[SerializedField] private float lifeTime;
[SerializedField] private float maxLifeTime;
[SerializedField] private float bulletSpeed;
private void OnEnable()
{
lifeTime = maxLifeTime;
}
// 움직이는 부분
private void Update()
{
// 반납 부분
if(lifeTime <= 0)
PoolManager.Instance.Enqueue(gameObject);
lifeTime -= Time.deltaTime;
transform.Translate(transform.foward * Time.deltaTime * bulletSpeed);
}
}
2.1 준비
// 1. 스폰 대상 태그, 태그는 비워 두어도 됨.
public struct BallTag : IComponentData { }
------ 이하 별도의 하나의 스크립트 (예시 기준 : BallTagAuthoring.cs) 에 쓰기 -------
public class BallTagAuthoring : MonoBehaviour { }
public class BallTagBaker : Bake<BallTagAuthoring>
{
AddComponent(new BallTag());
}
// 2. 스폰 정보
public struct SpawnerProperties : IComponentData
{
public Entity Prefab;
public int CreatedCount;
}
------------------- 이하 별도의 하나의 스크립트(예시 기준 : SpawnerPropertiesAuthoring.cs)에 쓰기 -----------------------------
public class SpawnerPropertiesAuthoring : MonoBehaviour
{
public GameObject prefab;
public int createdCount;
}
public class SpawnerPropertiesBaker : Baker<SpawnerPropertiesAuthoring>
{
public override void Bake(SpawnerPropertiesAuthoring authoring)
{
AddComponent(new SpanwerProperties()
{
Prefab = GetEntity(authoring.prefab),
CreatedCount = authoring.createdCount
});
}
}
2.2 생성
/// 반드시 partial struct 두개를 모두 써야 함.
[BurstCompile]
public partial struct SpanwerSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
}
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// state.EntitiyManaer.CreateQuery를 사용할 경우 에러 발생 !
// 생성할 볼 들을 관리할 수 있는 쿼리 작성
var query = SystemAPI.QueryBuilder().WithAll<BallTag>().Build();
// 생성 버퍼, EntitiyManager.Instantiate와의 차이는 메인스레드에서 작동 함.
var ecb = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
var spawnData = SystemAPI.GetSingleton<BallSpawnProperties>();
if (query.CalculateEntityCount() < spawnData.MaxCount)
{
var entitiy = ecb.Instantiate(spawnData.Prefab);
// 기존 오브젝트에 설정된 SpeedAuthoring이 있고, 값이 설정 되었다 하더라도 덮어 쓸 수 있음.
// ecb.SetComponent(entity, new Speed(){ Value = 5 });
}
}
}
2.3 사용 및 반납
/// 사용
/// 다양한 사용 방법이 있지만, 그냥 적당히 움직이다가 사라지는 방식을 적음.
/// 이동 시스템이 작성 되었다고 가정함, 이전 포스팅을 기준으로 작성하였기 때문에 여기서는 별도로 작성하지 않겠음.
/// 이하 이동 관련 Aspect class
public readonly partial struct BallAspect : IAspect
{
// 파괴하기 위해 작성
private readonly Entity _entity;
// 볼의 속도, 활동 시간 등을 적어 둔 종합 프로퍼티들
private readonly RefRW<BallProperties> _properties;
// 외부에서 접근하기 위한 엔티티 프로퍼티
public Entity Entitiy => _entitiy;
// 실제 움직이는 로직
public void Move(float deltaTime)
{
.. 구현되어 있음..
}
// 활동 시간 업데이트 로직
public float UpdateLifeTime(float deltaTime)
{
return _properties.ValueRW.lifeTime -= deltaTime;
}
}
/// 이하 이동 관련 ISystem
public partial struct BallMovingSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
}
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 시간 변화량
var deltaTime = SystemAPI.Time.deltaTime;
// EntityCommandBuffer 설정 - 엔티티 파괴용.
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
// 볼이 움직임 JobSystem 구현 부분
var ballMovingHandle = new BallMovingJob
{
deltaTime = deltaTime,
ecb = ecb
}.Schedule(state.Dependency);
}
}
public struct BallMovingJob : IJobEntity
{
public float deltaTime;
public EntityCommandBuffer ecb;
public void Excute(BallAspect ball)
{
// 움직이는 로직 실행
if(ball.Entity != Entity.Null)
ball.Move(delta);
// 활동시간 체크, 시간 만료되면 파괴
if(ball.UpdateLifeTime(detaTime) <= 0)
ecb.DestroyEntity(ball.Entity);
}
}
위의 스크립트와 다른 내용입니다. 하지만 파란색 총알이 3초뒤에 사라지는 부분은 똑같습니다.