Unity/Study

Update 사용 없이 Coroutine으로 게임 진행 구현

홍삼맛 2024. 6. 14. 19:15

기술 소개

유니티를 Monobehaviour 클래스에 생명주기(Life Cycle) 중에 가장 많이 쓰이는 Update 클래스가 존재한다.
Update 클래스는 프레임 당 한번 호출되는 클래스로 프레임 업데이트를 위한 주요 작업 함수이다.
해당 클래스를 잘 제어하지 못하고 Update 클래스에 의존만 한다면 프레임 드롭같이 최적화 문제가 발생할 수도 있다.
그래서 P.P.R 프로젝트에서 전반적인 게임 플레이를 Update 사용 없이 Coroutine으로 진행 루틴을 만들려고 한다.


기술 설명

YieldCache.cs : 코루틴 최적화

/// Boxing 발생하지 않게 해주며, 의도치 않게 가비지가 생성되는 것을 방지
class FloatComparer : IEqualityComparer<float>
{
    bool IEqualityComparer<float>.Equals(float x, float y)
    {
        return x == y;
    }
    int IEqualityComparer<float>.GetHashCode(float obj)
    {
        return obj.GetHashCode();
    }
}

private static readonly Dictionary<float, WaitForSeconds> _timeInterval = new();

// 코루틴 Yield WaitForSeconds 최적화
public static WaitForSeconds WaitForSeconds(float seconds)
{
    if (!_timeInterval.TryGetValue(seconds, out WaitForSeconds waitForSeconds))
        _timeInterval.Add(seconds, waitForSeconds = new WaitForSeconds(seconds));
    return waitForSeconds;
}
  • 코루틴을 사용할 때 new 키워드 사용으로 GC 비용이 발생
  • 게임 제작 특성상, 코루틴 만으로 진행 루틴을 만들어야 하니 많은 new 사용으로 GC 부하가 예상됨
  • 그래서 코루틴을 미리 캐싱하여 불필요한 인스턴스 생성을 방지한다
  • 참고 자료

BattleSystem.cs : 전투 시작

// 코루틴 Start로 턴 제어
private IEnumerator Start()
{
    // 전투 세팅
    this.gameBoard.SetBoard();

    yield return YieldCache.WaitForSeconds(1.0f);

    // 전투 시작 (반복)
    while (true)
    {
        // 플레이어 행동 턴 
        yield return StartCoroutine(PlayerTurn());
        
        // 적 행동 턴
        yield return StartCoroutine(EnemyTurn());
    }
}
  • Start 클래스를 열거자 IEnumrator로 선언하여 코루틴을 사용할 준비를 한다
  • 게임 시작 전 전투 세팅을 한번 호출하고 1초의 딜레이 후 전투 시작
  • yield return StartCoroutine을 통해 다른 코루틴이 끝날 때까지 대기하여 플레이어 턴이 끝나고 적 턴이 진행하도록 한다

BattleSystem.cs : 전투 진행

// 플레이어 턴
private IEnumerator PlayerTurn()
{
    if (this.battlePlayer.OnStart)
    {
        yield return StartCoroutine(this.battlePlayer.PlayerOnStart());
    }
    else
    {
        this.battlePlayer.PlayerOnInit();
    }

    yield return StartCoroutine(BattleNotice.Instance.UpdateNotice("Player Turn"));

    // 적 스킬 생성
    foreach (var battleEnemy in this.BattleEnemys)
    {
        battleEnemy.EnemySkillInstance(this.gameBoard);
    }

    // 플레이어 행동 반복
    // 최소한 2의 행동을 소모해야 작동 => 행동력이 1 이하일 경우 턴 종료
    while (this.battlePlayer.CurrentACT > 1)
    {
        // 플레이어 card 선택
        yield return this.gameBoard.WaitForSelection();
        
        // 최종 선택된 cards 소멸 
        yield return this.gameBoard.DespawnSelection();
        
        // 빈 곳으로 cards 이동 
        yield return this.gameBoard.WaitForMovement();
        
        // 소멸 된 cards 수 만큼 재생성
        yield return this.gameBoard.RespawnCards();
        
        // 행동 끝난 후 적 상태 체크
        EnemyStateCheck();
    }
}


// 적 턴
public IEnumerator EnemyTurn()
{
    // 적 행동 초기화
    foreach (var battleEnemy in this.BattleEnemys)
    {
        battleEnemy.Init();
    }

    yield return StartCoroutine(BattleNotice.Instance.UpdateNotice("Enemy Turn"));

    // 적 스킬 사용
    foreach (var battleEnemy in this.BattleEnemys)
    {
        yield return StartCoroutine(battleEnemy.EnemyUseSkill());
    }
}
  • PlayerTurn안의 while 반복문을 통해 코루틴을 순서대로 불러서 행동 루틴을 구현하였다
  • EnemyTurn도 적의 상태에 따라 스킬이 사용되도록 AI 패턴을 호출한다

구현 결과

전투 루틴