오늘은 미니 프로젝트의 전반적인 작업을 정리하고, 어제까지 진행한 내역과 팀 단위 협업 과정을 점검하는 시간을 가졌다. 어제까지는 각 팀원들이 개별적으로 맡은 기능을 구현하고 있었고, 오늘은 팀장 주도로 각자의 파트를 검수하고 하나의 프로젝트로 병합하는 작업을 진행했다. 이를 통해 기능 간 충돌을 사전에 확인하고, 전체적인 흐름이 자연스럽게 연결되는지 테스트할 수 있었다.
나는 카드 애니메이션 구현을 중심으로 작업을 진행했다. 카드 클릭 시 Y축 회전 애니메이션을 통해 앞면이 자연스럽게 보이도록 설정하였고, 두 장의 카드가 열렸을 때 일치 여부를 판단하여 맞지 않으면 다시 뒤집히는 애니메이션이 재생되도록 구성하였다. 카드가 일치했을 경우에는 제거 처리까지 연계되도록 애니메이터 트리거를 제어했다.
Tile.cs가 붙어 있고, Collider2D가 적용되어 클릭 판정이 가능하다.Resources/Towers/ 또는 Prefabs/Tile/ 등의 구조로 정리되었다.public bool SpendGold(int amount)
{
if (gold >= amount)
{
gold -= amount;
return true;
}
return false;
}
Tile 스크립트를 통해 상태 관리가 가능하다.Dictionary<Vector2Int, Tile>)로 저장되어 좌표 기반 참조가 편리하다.if (tile != null && !tile.isOccupied && tile.isBuildable)
{
if (GameManager.Instance.SpendGold(towerCost))
{
SpawnRandomTower(tile.transform.position);
tile.isOccupied = true;
}
}
Start() 대신 SetupPathTiles() 함수를 따로 만들고,// TileManager.cs 내부
if (PathManager.Instance != null)
{
PathManager.Instance.SetupPathTiles();
}
for (int x = xStart; x <= xEnd; x++)
{
ApplyPathToTile(new Vector2Int(x, y));
}
isBuildable = false가 자동 설정된다.Enemy 오브젝트가 뒤틀린 경로로 이동하는 게 확인됐다.
GetLineBetween()함수가 항상 좌표를 정방향으로만 채우기 때문에 실제 pathPoints에는 (0,9), (0,8), (0,7), (0,6) 같은 식으로 역방향으로 좌표가 들어가야 하는데 정방향으로 들어가게 됐다.pathPoints에 들어가는 순서가 일치하지 않아 경로가 꼬이는 문제가 발생했다.Enemy가 경로를 따라 이동하지 않고, 오히려 반복되거나 엉뚱한 순서로 타일을 밟는 현상이 발생했다.Enemy가 정확히 우리가 입력한 좌표 순서대로, 그리고 그 사이의 모든 타일을 순서대로 따라가도록 해야 했다.GetLineBetween 함수 구현이 필요했다.PathManager.cs)pathTileCoords에는 플레이어가 입력한 좌표만 저장한다. 예: (0,9), (0,6), (9,6)...SetupPathTiles()에서는 GetLineBetween()을 이용해 좌표 사이의 모든 타일을 계산하고 pathPoints에 순서대로 Transform을 추가한다.Enemy는 이 pathPoints를 따라 정확히 순서대로 이동하며, 모든 중간 타일까지 밟게 된다.isBuildable=false 처리가 되어 타워 설치가 불가능해진다.private List<Vector2Int> GetLineBetween(Vector2Int a, Vector2Int b)
{
List<Vector2Int> list = new List<Vector2Int>();
if (a.x == b.x) // 세로 이동
{
int dir;
if (a.y < b.y) dir = 1;
else dir = -1;
for (int y = a.y; y != b.y + dir; y += dir)
{
list.Add(new Vector2Int(a.x, y));
}
}
else if (a.y == b.y) // 가로 이동
{
int dir;
if (a.x < b.x) dir = 1;
else dir = -1;
for (int x = a.x; x != b.x + dir; x += dir)
{
list.Add(new Vector2Int(x, a.y));
}
}
return list;
}

EnemySpawner.cs를 붙여줬다.PathManager.pathPoints를 참조하여 Enemy가 정확히 경로를 따라 움직이도록 구성EnemySpawner.cs의 전체 구조이다.public class EnemySpawner : MonoBehaviour
{
public GameObject normalEnemyPrefab;
public GameObject bossEnemyPrefab;
public float spawnInterval = 0.5f;
public int enemiesPerWave = 30;
private int currentWave = 1;
private bool isSpawning = false;
void Start()
{
StartCoroutine(StartWave());
}
IEnumerator StartWave()
{
isSpawning = true;
for (int i = 0; i < enemiesPerWave; i++)
{
SpawnEnemy(false);
yield return new WaitForSeconds(spawnInterval);
}
if (currentWave % 10 == 0)
{
SpawnEnemy(true); // 10 웨이브마다 보스 출현
}
isSpawning = false;
GameManager.Instance.AddGold(400); // 웨이브 보상 지급
yield return new WaitForSeconds(10f);
currentWave++;
StartCoroutine(StartWave());
}
void SpawnEnemy(bool isBoss)
{
GameObject prefab = isBoss ? bossEnemyPrefab : normalEnemyPrefab;
if (PathManager.Instance.pathPoints.Count > 0)
{
Vector3 spawnPos = PathManager.Instance.pathPoints[0].position;
Instantiate(prefab, spawnPos, Quaternion.identity);
}
}
}
Coroutine이란?
IEnumerator로 작성된 함수를 StartCoroutine()으로 호출하면, 해당 함수는 여러 프레임에 걸쳐 비동기적으로 실행된다.
yield return을 사용해 딜레이, 조건 대기, 프레임 건너뛰기 등을 쉽게 구현할 수 있다.
예를 들어, yield return new WaitForSeconds(0.5f);는 0.5초 뒤에 다음 줄을 실행한다.
EnemySpawner에서는 다음과 같이 사용되었다.:
- StartCoroutine(StartWave()) → 게임 시작 시 첫 웨이브를 실행한다.
- yield return new WaitForSeconds(spawnInterval) → 각 적을 0.5초 간격으로 생성한다.
- yield return new WaitForSeconds(5f) → 웨이브 끝나고 5초 대기 후 다음 웨이브를 실행한다.
- 코루틴을 사용하면 Update()에 모든 처리를 몰아넣지 않아도 되며, 시간 제어가 직관적이고 깔끔해진다.
public void AddGold(int amount)
{
gold += amount;
}
GameManager.Instance.AddGold() 호출PathManager가 생성한 pathPoints를 순서대로 따라 이동한다.EnemySpawner.cs를 수정해주었다. yield return StartCoroutine(WaitUntilAllEnemiesDead());
.
.
.
IEnumerator WaitUntilAllEnemiesDead()
{
while (GameObject.FindGameObjectsWithTag("Enemy").Length > 0)
{
yield return null; // 다음 프레임까지 대기
}
}
EnemyTag가 있는 오브젝트가 없어질 때까지 대기하도록 설정했다.Coroutine은 Unity에서 시간 기반 처리(딜레이, 조건 대기)에 가장 널리 사용되는 방법이다.yield return new WaitForSeconds(x)로 특정 시간만큼 기다릴 수 있으며,yield return null은 한 프레임 쉬기, yield return 조건은 특정 조건을 만족할 때까지 기다리는 데 쓰인다.오늘도 쉽지 않은 실습이었다. 초반에 Tile 생성 및 Tower 소환 스크립트를 작성할 때만 해도 쉽게 느껴졌는데, 경로 지정할 때 의도한대로 오브젝트가 움직이지 않아서 굉장히 헤매었다.
하지만, 오브젝트의 AI 등을 설정할 때, 경로 지정은 아주 많이 쓰이므로, 지금 헤맨 게 오히려 다행이라는 생각이 든다.
또, 코루틴을 새로 배웠는데, 실제 게임 제작에 굉장히 자주 쓰일 것 같다. 항상 Invoke()를 썼었는데, 이걸 쓰지 않아도 간편하게 조건, 시간 대기를 설정할 수 있다는 점이 효율적이라고 생각한다.
내일 오전에 팀 회의를 마치고, 내일은 Tower 오브젝트의 공격 설정, UI 제작, 레벨 디자인을 해볼 생각이다. 특히 레벨 디자인은 지금까지 다루지 않았던 영역이라 새로운 영역인 만큼 더 시간을 들일 필요가 있을 것 같다.