랩실에서 진행 중인 게임 개발 프로젝트에서 계속 생성되고 삭제되어야 하는 NPC를 구현하기 위해 Instantiate(오브젝트 생성)와 Destroy(오브젝트 파괴)를 사용하였지만, 이 두 함수는 상당히 비용을 크게 먹는다. Instantiate은 메모리를 새로 할당하고 리소스를 로드하는 등의 초기화 과정이 필요하고, Destroy는 파괴 이후에 발생하는 가비지 컬렉팅으로 인한 프레임 드랍이 발생할 수 있다. 그래서 오브젝트 풀링(Object Pooling)이라는 방법을 사용했다. 자주 사용하는 오브젝트를 미리 생성해 놓고 이 것을 사용할 때마다 새로 생성 삭제하는 것이 아닌 사용할 때는 오브젝트 풀한테 빌려서 사용하고 삭제할 때는 오브젝트 풀한테 돌려줌으로써 단순하게 오브젝트를 활성화와 비활성화만 하는 개념이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPoolingManager : MonoBehaviour
{
// 싱글톤 인스턴스
public static ObjectPoolingManager Instance;
// 최대 외래 환자와 의사 수
public int maxOfOutpatient;
public int maxOfDoctor;
// 비활성화된 외래 환자와 의사 오브젝트를 저장하는 큐
public Queue<GameObject> outpatientQueue = new Queue<GameObject>();
public Queue<GameObject> doctorQueue = new Queue<GameObject>();
void Awake()
{
// 싱글톤 패턴 구현
Instance = this;
// 의사와 외래 환자 초기화
DoctorInitialize();
OutpatientInitialize();
}
private void OutpatientInitialize()
{
// 외래 환자 프리팹 로드
GameObject[] OutpatientPrefabs = Resources.LoadAll<GameObject>("Prefabs/RealPatient");
for (int i = 0; i < maxOfOutpatient; i++)
{
// 프리팹 리스트에서 랜덤으로 하나 선택하여 생성
GameObject newOutPatient = Instantiate(OutpatientPrefabs[Random.Range(0, OutpatientPrefabs.Length)]);
outpatientQueue.Enqueue(newOutPatient);
newOutPatient.SetActive(false);
}
}
private void DoctorInitialize()
{
// 의사 프리팹 로드
GameObject[] DoctorPrefabs = Resources.LoadAll<GameObject>("Prefabs/Doctor");
for (int i = 0; i < maxOfDoctor; i++)
{
// 프리팹 리스트에서 랜덤으로 하나 선택하여 생성
GameObject newDoctor = Instantiate(DoctorPrefabs[Random.Range(0, DoctorPrefabs.Length)]);
newDoctor.name = "Doctor " + i;
DoctorController doctorController = newDoctor.GetComponent<DoctorController>();
// 의사 사무실 할당
DoctorOffice spawnArea = GameObject.Find("DoctorWaypoints").transform.Find("Ward (" + i / 5 + ")").Find("Doctor'sOffice (" + i + ")").GetComponent<DoctorOffice>();
spawnArea.doctor = newDoctor;
doctorController.waypoints.Add(spawnArea);
// 외래 환자 대기 구역 할당
GameObject parentObject = GameObject.Find("OutPatientWaypoints");
DoctorOffice waypointrange = parentObject.transform.Find("Doctor'sOffice (" + i + ")").GetComponent<DoctorOffice>();
waypointrange.doctor = newDoctor;
doctorController.waypoints.Add(waypointrange);
newDoctor.SetActive(false);
}
}
// 외래 환자 비활성화 및 초기화
public void DeactivateOutpatient(GameObject outpatient)
{
outpatient.GetComponent<Person>().status = InfectionState.Normal;
OutpatientController outpatientController = outpatient.GetComponent<OutpatientController>();
outpatientController.waypoints.Clear();
outpatientController.isWaiting = false;
outpatientController.waypointIndex = 0;
outpatientController.signal = false;
outpatientQueue.Enqueue(outpatient);
outpatient.SetActive(false);
}
// 외래 환자 활성화 및 위치 설정
public GameObject ActivateOutpatient(Vector3 position)
{
GameObject newOutpatient = outpatientQueue.Dequeue();
newOutpatient.transform.position = position;
newOutpatient.SetActive(true);
return newOutpatient;
}
// 의사 비활성화 및 초기화
public void DeactivateDoctor(GameObject doctor)
{
DoctorController doctorController = doctor.GetComponent<DoctorController>();
doctorController.patientCount = 0;
doctorController.isWaiting = false;
doctor.SetActive(false);
}
// 의사 활성화 및 위치 설정
public GameObject ActivateDoctor(GameObject newDoctor)
{
DoctorController doctorController = newDoctor.GetComponent<DoctorController>();
newDoctor.transform.position = doctorController.waypoints[0].GetRandomPointInRange();
doctorController.age++; // 의사의 나이 증가 (아사 현상 방지)
newDoctor.SetActive(true);
return newDoctor;
}
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OutpatientCreator : MonoBehaviour
{
public static int numberOfOutpatient = 0;
public List<Waypoint> spawnAreas = new List<Waypoint>();
public float infectionRate = 0.03f;
public float spawnDelay = 0.1f; // 대기 시간을 설정 가능하게 만듦
private bool isWaiting = false;
// Start는 처음 프레임이 업데이트되기 전에 호출됩니다.
void Start()
{
GameObject go = GameObject.Find("Gateways");
if (go != null)
{
for (int i = 0; i < go.transform.childCount; i++)
{
Waypoint waypointRange = go.transform.GetChild(i).GetComponent<Waypoint>();
if (waypointRange != null)
{
spawnAreas.Add(waypointRange);
}
else
{
Debug.LogError("Gateways 자식 오브젝트에 Waypoint 컴포넌트가 없습니다.");
}
}
}
else
{
Debug.LogError("Gateways 게임 오브젝트를 찾을 수 없습니다.");
}
}
// Update는 매 프레임마다 호출됩니다.
void Update()
{
if (!isWaiting && numberOfOutpatient < ObjectPoolingManager.Instance.maxOfOutpatient)
{
StartCoroutine(SpawnOutpatient());
}
}
IEnumerator SpawnOutpatient()
{
isWaiting = true;
Vector3 spawnPosition = spawnAreas[Random.Range(0, spawnAreas.Count)].GetRandomPointInRange();
GameObject newOutpatient = ObjectPoolingManager.Instance.ActivateOutpatient(spawnPosition);
if (newOutpatient != null)
{
Person newOutPatientPerson = newOutpatient.GetComponent<Person>();
if (newOutPatientPerson != null)
{
if (Random.value < infectionRate)
{
if (StageManager.Instance.stage == 1)
{
newOutPatientPerson.status = InfectionState.Stage1;
}
else if (StageManager.Instance.stage == 2)
{
newOutPatientPerson.status = InfectionState.Stage2;
}
}
newOutPatientPerson.role = Role.Outpatient;
numberOfOutpatient++;
}
else
{
Debug.LogError("새 외래 환자에 Person 컴포넌트가 없습니다.");
}
}
else
{
Debug.LogError("새 외래 환자를 활성화하는 데 실패했습니다.");
}
yield return new WaitForSeconds(spawnDelay);
isWaiting = false;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using System.Linq;
public class OutpatientController : MonoBehaviour
{
// 컴포넌트 참조
private Animator animator;
private NavMeshAgent agent;
// 웨이포인트 관련 변수
public List<Waypoint> waypoints = new List<Waypoint>();
public int waypointIndex = 0;
// 상태 플래그
public bool isWaiting = false;
public bool isWaitingForDoctor = false;
public bool signal = false;
// 씬 오브젝트 참조
GameObject parentObject;
GameObject gatewayObject;
int randomWard;
private void Awake()
{
// 컴포넌트 초기화
animator = GetComponent<Animator>();
agent = GetComponent<NavMeshAgent>();
agent.avoidancePriority = Random.Range(0, 100);
// 씬 오브젝트 찾기
parentObject = GameObject.Find("OutPatientWaypoints");
gatewayObject = GameObject.Find("Gateways");
randomWard = Random.Range(0, 6);
}
private void OnEnable()
{
// 첫 번째 웨이포인트 추가
AddWaypoint(parentObject.transform, $"CounterWaypoint ({randomWard})");
}
private void Update()
{
// 애니메이션 업데이트
UpdateAnimation();
// 대기 중이면 이동 처리하지 않음
if (isWaiting || isWaitingForDoctor)
{
return;
}
// 목적지에 도착했는지 확인
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
if (waypointIndex == 4)
{
// 모든 웨이포인트를 방문했으면 비활성화
ObjectPoolingManager.Instance.DeactivateOutpatient(gameObject);
OutpatientCreator.numberOfOutpatient--;
return;
}
else
{
// 다음 웨이포인트로 이동
StartCoroutine(MoveToNextWaypointAfterWait());
}
}
}
// 애니메이션 업데이트 메서드
private void UpdateAnimation()
{
if (!agent.isOnNavMesh)
{
animator.SetFloat("MoveSpeed", 0);
animator.SetBool("Grounded", false);
return;
}
float moveSpeed = agent.remainingDistance > agent.stoppingDistance ? agent.velocity.magnitude / agent.speed : 0;
animator.SetFloat("MoveSpeed", moveSpeed);
animator.SetBool("Grounded", !agent.isOnOffMeshLink && agent.isOnNavMesh);
}
// 다음 웨이포인트로 이동하는 코루틴
private IEnumerator MoveToNextWaypointAfterWait()
{
isWaiting = true;
yield return new WaitForSeconds(1.5f);
isWaiting = false;
// 현재 웨이포인트 인덱스에 따라 다음 웨이포인트 추가
AddNextWaypoint();
if (waypointIndex < waypoints.Count)
{
if (waypoints[waypointIndex] is DoctorOffice doctorOffice)
{
StartCoroutine(WaitForDoctorOffice(doctorOffice));
}
else if (waypointIndex > 0 && waypoints[waypointIndex - 1] is DoctorOffice doc)
{
doc.is_empty = true;
}
if (!isWaitingForDoctor)
{
agent.SetDestination(waypoints[waypointIndex++].GetRandomPointInRange());
StartCoroutine(UpdateMovementAnimation());
}
}
}
// 다음 웨이포인트 추가 메서드
private void AddNextWaypoint()
{
switch (waypointIndex)
{
case 0:
AddWaypoint(parentObject.transform, $"CounterWaypoint ({randomWard})");
break;
case 1:
AddWaypoint(parentObject.transform, $"SofaWaypoint ({randomWard})");
break;
case 2:
if (waypoints.Count < 3)
{
AddWaypoint(parentObject.transform, $"Doctor'sOffice ({randomWard * 5})");
}
break;
case 3:
AddWaypoint(gatewayObject.transform, $"Gateway ({Random.Range(0, 2)})");
break;
}
}
// 의사 사무실 대기 코루틴
private IEnumerator WaitForDoctorOffice(DoctorOffice doctorOffice)
{
isWaitingForDoctor = true;
while (!signal)
{
yield return new WaitForSeconds(1);
}
doctorOffice.is_empty = false;
isWaitingForDoctor = false;
}
// 웨이포인트 추가 메서드
private void AddWaypoint(Transform parent, string childName)
{
Transform waypointTransform = parent.Find(childName);
if (waypointTransform != null)
{
Waypoint comp = waypointTransform.gameObject.GetComponent<Waypoint>();
if (comp is DoctorOffice)
{
// 의사 사무실 선택 로직
SelectDoctorOffice(parent, childName);
}
else
{
if (!waypoints.Contains(comp))
{
waypoints.Add(comp);
Debug.Log($"Added waypoint: {childName}");
}
}
}
else
{
Debug.LogWarning($"Can't find waypoint: {childName}");
}
}
// 의사 사무실 선택 메서드
private void SelectDoctorOffice(Transform parent, string childName)
{
// 의사 사무실 번호 추출
string strNum = childName.Substring(childName.Length - 3, 2);
int num = 0;
if (!int.TryParse(strNum, out num))
{
num = childName[childName.Length - 2] - '0';
}
// 가능한 의사 사무실 목록 생성
Dictionary<DoctorOffice, int> countDic = new Dictionary<DoctorOffice, int>();
for (int i = num / 5 * 5; i < num + 5; i++)
{
string objectName = "Doctor'sOffice (" + i + ")";
DoctorOffice doctorOffice = parent.Find(objectName).GetComponent<DoctorOffice>();
GameObject searchedDoctor = doctorOffice.doctor;
if (!searchedDoctor.activeInHierarchy)
{
continue;
}
int patientCount = searchedDoctor.GetComponent<DoctorController>().patientCount + doctorOffice.waitingQueue.Count;
if (patientCount < DoctorController.patientMaxCount)
{
if (!countDic.ContainsKey(doctorOffice))
{
countDic.Add(doctorOffice, patientCount);
}
}
}
// 최적의 의사 사무실 선택
if (countDic.Count > 0)
{
DoctorOffice searchedOffice = countDic
.OrderBy(kvp => kvp.Key.doctor.GetComponent<DoctorController>().age)
.ThenBy(kvp => kvp.Value)
.FirstOrDefault().Key;
searchedOffice.waitingQueue.Enqueue(this);
waypoints.Add(searchedOffice);
countDic.Clear();
}
else
{
Debug.LogError("의사를 찾을 수 없습니다.");
}
}
// 이동 애니메이션 업데이트 코루틴
private IEnumerator UpdateMovementAnimation()
{
while (true)
{
animator.SetFloat("MoveSpeed", agent.velocity.magnitude / agent.speed);
yield return null;
}
}
}
오브젝트가 생성될 때 모든 웨이포인트를 추가하고 진료 대기 큐에 넣으니 비활성화 되었다가 다시 활성화 되었을 때 진료 대기 큐에 들어가지 않는 현상이 발생하여 해결하였다. 그리고 의사가 5명 중 3명 씩 돌아가면서 근무를 하는데, 의사가 존재하는 진료실만 방문해야하기 때문에 각 웨이포인트에 도착했을 때 다음 웨이포인트를 추가해주는 방향으로 로직을 수정했다.
환자를 받은 수가 가장 적은 의사에 배정해주었지만 교체 직 후 카운트가 0이라 계속 그 진료실로만 배정되는 현상(아사 현상)이 발생하여 의사들마다 age 변수를 두어 교대가 더 많이 된 의사에게는 덜 배정되게 구현하였다.
의사는 1개의 병동 당 5명이 있는데 이 중 3명씩 돌아가면서 근무를 한다. 이를 구현하기 위해 슬라이딩 윈도우 방식으로 3명씩 의사가 활성화와 비활성화 되게 코드를 짰다.
처음엔 10초에 한 번씩 교대를 하게 구현하였지만 로직 상 교대 시간 타이밍을 맞추기 쉽지 않을 것 같아서 의사가 환자를 받은 횟수를 Count하여 10회 받으면 교대하는 방식으로 구현하였다.
그러기 위해 각 진료실 웨이포인트에 의사 오브젝트 정보를 저장해주었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class DoctorController : MonoBehaviour
{
private Animator animator;
private NavMeshAgent agent;
public List<Waypoint> waypoints = new List<Waypoint>();
public bool isWaiting = false;
public int patientCount = 0;
public static int patientMaxCount = 10;
public int age = 0;
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
agent = GetComponent<NavMeshAgent>();
Person newDoctorPerson = gameObject.GetComponent<Person>();
newDoctorPerson.role = Role.Doctor;
}
// Update is called once per frame
void Update()
{
// 애니메이션
if (!agent.isOnNavMesh)
{
if (animator.GetFloat("MoveSpeed") != 0)
animator.SetFloat("MoveSpeed", 0);
if (animator.GetBool("Grounded"))
animator.SetBool("Grounded", false);
return;
}
if (agent.remainingDistance > agent.stoppingDistance)
{
if (animator.GetFloat("MoveSpeed") != agent.velocity.magnitude / agent.speed)
animator.SetFloat("MoveSpeed", agent.velocity.magnitude / agent.speed);
}
else
{
if (animator.GetFloat("MoveSpeed") != 0)
{
animator.SetFloat("MoveSpeed", 0);
}
}
if (animator.GetBool("Grounded") != (!agent.isOnOffMeshLink && agent.isOnNavMesh))
animator.SetBool("Grounded", !agent.isOnOffMeshLink && agent.isOnNavMesh);
if (patientCount >= patientMaxCount && waypoints[1] is DoctorOffice doctorOffice)
{
if (doctorOffice.waitingQueue.Count == 0 && doctorOffice.is_empty)
{
DoctorCreator.Instance.ChangeDoctor(gameObject);
return;
}
}
if (isWaiting)
{
return;
}
StartCoroutine(MoveToNextWaypointAfterWait());
}
private IEnumerator MoveToNextWaypointAfterWait()
{
isWaiting = true;
yield return new WaitForSeconds(Random.Range(1.0f, 2.0f));
isWaiting = false;
if (waypoints[1] is DoctorOffice doctorOffice)
{
if (doctorOffice.is_empty)
{
agent.SetDestination(waypoints[0].GetRandomPointInRange());
}
else
{
agent.SetDestination(waypoints[1].GetRandomPointInRange());
}
}
StartCoroutine(UpdateMovementAnimation());
}
private IEnumerator UpdateMovementAnimation()
{
while (true)
{
animator.SetFloat("MoveSpeed", agent.velocity.magnitude / agent.speed);
yield return null;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoctorOffice : Waypoint
{
public Queue<OutpatientController> waitingQueue = new Queue<OutpatientController>();
public bool is_empty = true;
public GameObject doctor;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (is_empty && waitingQueue.Count > 0)
{
OutpatientController next = waitingQueue.Peek();
if (next.isWaitingForDoctor)
{
next = waitingQueue.Dequeue();
doctor.GetComponent<DoctorController>().patientCount++;
is_empty = false;
next.signal = true;
}
}
}
public void enter(OutpatientController outpatient)
{
waitingQueue.Enqueue(outpatient);
}
public void exit()
{
is_empty = true;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoctorCreator : MonoBehaviour
{
public static DoctorCreator Instance;
public int numberOfDoctor = 0;
private List<GameObject> rootObjects = new List<GameObject>();
private List<int> doctorCount = new List<int> { 3, 3, 3, 3, 3, 3 };
// Start is called before the first frame update
void Start()
{
Instance = this;
// 루트 오브젝트를 캐싱합니다.
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
rootObjects.AddRange(scene.GetRootGameObjects());
// 처음 0부터 2번째 Doctor를 활성화합니다.
for (int i = 0; i < 6; i++)
{
for (int j = 0; j < 3; j++)
{
foreach (var rootObject in rootObjects)
{
if (rootObject.name == "Doctor " + (i * 5 + j))
{
ObjectPoolingManager.Instance.ActivateDoctor(rootObject);
}
}
}
}
}
private void Update()
{
}
public void ChangeDoctor(GameObject endDoctor)
{
string name = endDoctor.name;
string strNum = name.Substring(name.Length - 2, 2);
int num = 0;
if (!int.TryParse(strNum, out num))
{
num = name[name.Length - 1] - '0';
}
while (true)
{
int ward = num / 5;
foreach (var rootObject in rootObjects)
{
if (rootObject.name == "Doctor " + (ward * 5 + (doctorCount[ward] % 5)) && !rootObject.activeInHierarchy)
{
ObjectPoolingManager.Instance.DeactivateDoctor(endDoctor);
ObjectPoolingManager.Instance.ActivateDoctor(rootObject);
doctorCount[ward]++;
return;
}
}
doctorCount[ward]++;
}
}
}
환자들끼리 충돌하여 엉키는 상황이 아니면 계속 진료실에 들어가는 것을 볼 수 있다.