개요
유저가 직접 조작하는 유닛 배치 필드 구현 회고, 그리고 소개하는 글이다
내용
유닛 배치 필드 소개

플레이어가 소환한 유닛이 배치되고, 유닛 배치 변경 및 플레이어 입력을 받아 특정 동작을 수행할 수 있는 영역이다
이 글을 통해 소개하고 싶은 부분은 UnitPlacementNode와 UnitGroup을 이용하여 어떻게 내부 상태를 관리하는지이다
UnitPlacementNode

UnitPlacementNode는 유닛이 배치되는 단일 노드로, 각 사이드에 18개씩 존재하여 유닛 배치를 관리하는 역할을 한다
유닛의 배치 상태를 관리하고, 유닛 추가 및 삭제, 그리고 위치 조정을 처리한다. 또한 내부의 UnitGroup 클래스를 통해 유닛의 등급, 타입, 개수 등을 관리하며, 배치 가능 여부를 판단할 수 있도록 했다
UnitPlacementNode - UnitGroup 클래스를 통해 구현된 구조의 핵심은 노드와 관련된 동작을 추상화하고 유닛과 관련된 동작 및 데이터는 UnitGroup에 의존하게 설계하여 책임을 명확하게 분리한 것이다
public sealed class UnitPlacementNode : MonoBehaviourBase
{
public UnitGroup UnitGroup = new UnitGroup();
public bool CanAcceptUnit(SUnitSpawnRequestData requestData)
{
if (UnitGroup.IsFull())
return false;
if (UnitGroup.IsEmpty())
return true;
return UnitGroup.UnitType == requestData.UnitType &&
UnitGroup.UnitGrade == requestData.UnitGrade;
}
public void AddUnit(UnitRoot unit)
{
UnitGroup.AddUnit(unit);
UnitGroup.UpdateUnitInfo();
RearrangeUnitPositions();
}
public void SubUnit()
{
UnitGroup.SubUnit();
UnitGroup.UpdateUnitInfo();
RearrangeUnitPositions();
}
private void RearrangeUnitPositions()
{
UnitGroup.SetPositions(GetUnitPositions());
}
public async UniTask SwapWith(UnitPlacementNode targetNode)
{
// 유닛 그룹 교환
(UnitGroup, targetNode.UnitGroup) = (targetNode.UnitGroup, UnitGroup);
// 이동할 목표 위치 설정
Transform[] myTargetPositions = GetUnitPositions();
Transform[] targetNodePositions = targetNode.GetUnitPositions();
// 유닛 이동 (모두 이동 완료될 때까지 대기)
await UniTask.WhenAll(
UnitGroup.MoveToTargetNode(myTargetPositions),
targetNode.UnitGroup.MoveToTargetNode(targetNodePositions)
);
UnitGroup.UpdateUnitInfo();
targetNode.UnitGroup.UpdateUnitInfo();
RearrangeUnitPositions();
targetNode.RearrangeUnitPositions();
}
// ...
}
UnitGroup
UnitGroup은 하나의 배치 노드에 속한 유닛들의 집합과 정보를 관리하는 클래스다
각 UnitPlacementNode 내부에서 유닛 배치 상태를 유지하며 그룹에 대한 유닛 추가, 삭제, 이동과 같은 동작을 수행한다
public class UnitGroup
{
// ...
public void AddUnit(UnitRoot unit)
{
AssertHelper.NotEqualsValue(typeof(UnitGroup), UnitCount, MAX_UNIT_COUNT);
_placedUnits.Add(unit);
UpdateUnitInfo();
}
public void SubUnit()
{
if (IsEmpty()) return;
int lastIndex = _placedUnits.Count - 1;
UnitRoot unit = _placedUnits[lastIndex];
AssertHelper.NotNull(typeof(UnitGroup), unit);
unit.ReleaseObject();
_placedUnits.RemoveAt(lastIndex); // 마지막 유닛 제거
if (!IsEmpty())
{
UpdateUnitInfo(); // 남아 있는 유닛 기준으로 UnitType, UnitGrade 갱신
return;
}
Clear();
}
public void UpdateUnitInfo()
{
if (IsEmpty())
{
Clear();
return;
}
UnitGrade = _placedUnits[0].grade;
UnitType = _placedUnits[0].type;
}
private void Clear()
{
_placedUnits.Clear(); // 리스트 비우기
UnitGrade = EUnitGrade.None;
UnitType = EUnitType.None;
}
public bool IsFull() => UnitCount >= MAX_UNIT_COUNT || UnitGrade == EUnitGrade.Mythic;
public bool IsEmpty() => _placedUnits.Count == 0;
public void SetPositions(params Transform[] targetPositions)
{
for (int i = 0; i < UnitCount; ++i)
{
_placedUnits[i].transform.position = targetPositions[i].position;
}
}
public async UniTask MoveToTargetNode(params Transform[] targetPositions)
{
for (int i = 0; i < MAX_UNIT_COUNT; ++i)
{
_moveTasks[i] = default;
}
for (int i = 0; i < UnitCount; ++i)
{
_moveTasks[i] = _placedUnits[i].MoveController.MoveToTarget(targetPositions[i]);
}
await UniTask.WhenAll(_moveTasks);
}
public float GetAttackRange()
{
UnitMetaData data = RootManager.Ins.DataManager.UnitResources.GetResource(UnitGrade, UnitType);
AssertHelper.NotNull(typeof(UnitGroup), data);
return data.AttackRange;
}
}
마무리
이번 유닛 배치 시스템 구현에서는 UnitPlacementNode와 UnitGroup을 분리하여 책임을 명확하게 구분하는 구조를 설계했다
- UnitPlacementNode는 배치 위치 및 노드 간 유닛 이동을 담당하고,
- UnitGroup은 배치된 유닛의 속성을 관리하고 상태를 갱신하는 역할을 수행한다.
만약 배치 필드에서 추가적으로 유저가 스킬을 사용한다라는 기능을 구현해야 한다면 확장만 하면 된다. 즉 개방-폐쇄 원칙(OCP) 또한 잘 지켜지고 있다
'프로젝트 > Lucky Defense (InGame Clone)' 카테고리의 다른 글
운빨존많겜 인게임 클론 프로젝트 소개 (0) | 2025.03.14 |
---|---|
AI Player 동작 구현 (0) | 2025.03.14 |
에너미와 유닛 재활용을 위한 풀링 시스템 (0) | 2025.03.14 |
에너미와 유닛 프리팹 구조 (0) | 2025.03.14 |
UniRx와 MVP 패턴을 이용한 UI 시스템 (0) | 2025.03.14 |