20241212 / Unity_6차 14주차 목요일
<Kingdom : New Lands> 에서는 플레이어가 건물을 지을 때 돈을 지불하여 건설 예약을 걸어 두면
일을 하지 않는 작업자가 알아서 와서 건물을 짓는다.
하나의 건물에는 최대 2명의 작업자가 와서 건설할 수 있으며,
한번에 2명을 부르는 것이 아니라 한명을 먼저 부르고 일정 시간이 지나면 한 명을 더 부른다.
당연하게도 1명이라도 작업자가 붙은 건물은
1명도 붙지 않은 건물보다 작업자 호출의 우선순위가 밀린다.
이상을 종합해 봤을 때, 작업자의 건설 명령을 컨트롤하는 스케쥴러가 필요하다는 결론을 내렸다.
설계 핵심
- BuildingController의 건설 요청 관리:
- BuildingController에서 발생한 건설 요청 이벤트를 처리.
- 요청된 Building을 리스트에 추가.
- 건설 작업 우선순위를 관리하며, Worker NPC를 할당.
- NPCController의 Worker NPC 할당:
- 할당되지 않은 Worker NPC 중 가장 가까운 NPC를 선택.
- 한 건물에 최대 2명의 Worker NPC를 5초 간격으로 배정.
- Worker NPC가 작업을 시작하면, 관련 정보를 업데이트.
- Building 작업 진행:
- 작업 시작 시 Building.WorkStart(int workerCount) 호출.
- 건설 완료 이벤트 처리:
- 건설 요청 리스트에서 해당 Building 제거.
- 작업 중이던 Worker NPC의 작업 종료 처리 (BuildEnd 호출).
결과 위와 같은 와이어 프레임이 나왔다.
모든 NPC는 NPC Controller가 생성하며
모든 Building은 Building Controller에서 정보를 가지고 있으므로,
Build Event Controller는 스케쥴링만 하고 그때그때 필요한 이벤트를 호출해 주면 된다.
public class BuildEventController
{
private class BuildOrder
{
public BuildableArchitecture Building;
public List<AllyNPC> Workers; // 해당 빌딩 건설에 투입된 일꾼
public float LastAssignWorkerTime; // 가장 최근에 일꾼이 투입된 시간
public BuildOrder(BuildableArchitecture building)
{
Building = building;
Workers = new List<AllyNPC>();
LastAssignWorkerTime = float.MinValue;
}
}
Dictionary<BuildableArchitecture, BuildOrder> buildingToOrders = new(); // building 에서 접근
Dictionary<AllyNPC, BuildOrder> workerToOrders = new(); // worker 에서 접근
LinkedList<BuildableArchitecture> building_0Worker = new(); // 작업자가 없는 building list(빠른 우선순위 검색)
LinkedList<BuildableArchitecture> building_1Worker = new(); // 작업자가 1명 있는 building list(빠른 우선순위 검색)
List<AllyNPC> freeWorkers = new();
private const float assignSecondWorkerDelay = 5f;
private const float updateTick = 0.1f;
private float updateTime;
public void RegistBuildOrder(BuildableArchitecture building)
{
buildingToOrders.Add(building, new BuildOrder(building));
building_0Worker.AddLast(building);
}
public void AddWorker(AllyNPC worker)
{
freeWorkers.Add(worker);
}
public void UpdateController()
{
updateTime -= Time.deltaTime;
if (updateTime < 0)
{
updateTime = updateTick;
return;
}
AssignWorker();
}
private void AssignWorker()
{
if (freeWorkers.Count == 0) return;
foreach (BuildableArchitecture building in building_0Worker)
{
// TODO : 위험지역은 건설 불가하도록 조건 추가
BuildOrder buildOrder = buildingToOrders[building];
AllyNPC worker = PopClosestWorker(building.transform.position.x);
// 작업 구현(건설 영역은 임시로 임의로 지정)
worker.AI.SetState_Work(building.transform.position.x - 2f, building.transform.position.x + 2f);
buildOrder.Workers.Add(worker);
buildOrder.LastAssignWorkerTime = Time.time;
workerToOrders.Add(worker, buildOrder);
building_0Worker.RemoveFirst();
building_1Worker.AddLast(building);
if (freeWorkers.Count == 0) return;
}
foreach (BuildableArchitecture building in building_1Worker)
{
// TODO : 위험지역은 건설 불가하도록 조건 추가
BuildOrder buildOrder = buildingToOrders[building];
// 첫 작업자가 배정된 뒤 일정 시간이 지나야 다음 작업자를 받을 수 있음
if (buildOrder.LastAssignWorkerTime - Time.time < assignSecondWorkerDelay) continue;
AllyNPC worker = PopClosestWorker(building.transform.position.x);
// 작업 구현(건설 영역은 임시로 임의로 지정)
worker.AI.SetState_Work(building.transform.position.x - 2f, building.transform.position.x + 2f);
buildOrder.Workers.Add(worker);
workerToOrders.Add(worker, buildOrder);
building_1Worker.RemoveFirst();
if (freeWorkers.Count == 0) return;
}
}
private AllyNPC PopClosestWorker(float position)
{
if (freeWorkers.Count == 0) return null;
int closestIndex = 0;
float closestDistance = Mathf.Abs(freeWorkers[0].transform.position.x - position);
for (int i = 1; i < freeWorkers.Count; i++)
{
float distance = Mathf.Abs(freeWorkers[i].transform.position.x - position);
if (distance < closestDistance)
{
closestIndex = i;
closestDistance = distance;
}
}
AllyNPC closestWorker = freeWorkers[closestIndex];
freeWorkers.RemoveAt(closestIndex);
return closestWorker;
}
public void WorkStart(AllyNPC worker)
{
workerToOrders[worker].Building.AddBuilder();
}
public void WorkerDead(AllyNPC worker)
{
BuildOrder buildOrder = workerToOrders[worker];
buildOrder.Workers.Remove(worker);
int workerCount = buildOrder.Workers.Count;
if (workerCount == 0)
{
building_1Worker.Remove(buildOrder.Building);
building_0Worker.AddLast(buildOrder.Building);
}
else if (workerCount == 1)
{
building_1Worker.AddLast(buildOrder.Building);
}
buildOrder.Building.RemoveBuilder();
workerToOrders.Remove(worker);
}
public void BuildComplete(BuildableArchitecture building)
{
BuildOrder buildOrder = buildingToOrders[building];
int workerCount = buildOrder.Workers.Count;
if (workerCount == 0)
{
building_0Worker.Remove(building);
}
else if (workerCount == 1)
{
building_1Worker.Remove(building);
}
foreach (AllyNPC worker in buildOrder.Workers)
{
workerToOrders.Remove(worker);
freeWorkers.Add(worker);
// 작업 완료
worker.AI.EndState_Work();
}
buildingToOrders.Remove(building);
}
}
추후 낮밤이나 확장 영역에 따라 작업자를 배정하는 조건이 추가될 예정이다.