왜.... 오셨나요?

부끄러워요

유니티개발 TIL

3주차 3일 TIL_TextRPG심화(상태패턴)

와피했는데 2025. 7. 16. 21:09

들어가기 앞서

바로 이전글에서 RPG의 탐험 요소인 몬스터와 전투 시스템은 설계하고 제작하게 되었다 그과정중 던전클래스 자체를 역할을 좀더 추가하여 던전클래스에서 상태를 변경하여 턴제전투를 할수있는 구조로 말그대로 다시 뜯어고첬다

그래서 이번엔 이전에 연장선으로 몬스터쪽 상태일때 하는 행동과 승리/패배 조건을 구현하도록 하겠다


상태패턴(StatePattern)


상태 패턴(State Pattern)은 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 상태를 조건문으로 검사해서 행위를 달리하는 것이 아닌, 상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴을 말한다.

예시로 아주 유명한 게임 포켓몬스터를 예를 들수있다 그리고 본인은 이미 상태패턴을 사용한적이 있다

해당글을 확인하면 알수있다

https://passingwolf.tistory.com/32

 

[내일배움캠프] 1주차 5일 TIL - 간단한 기능 모작(포켓몬스터-02), TIL정리

-간단한 게임기능 모작 (포켓몬스터 전투시스템)-진행도 : 100% BattleSystem 스크립트 기능 구현: 1. 상태패턴 적용2. 배틀시 오브젝트 스폰3. 배틀시 각 오브젝트 Unit에 저장된 데이터 적용4. 플레이어

passingwolf.tistory.com

포켓로그 전투장면


 

플레이어 공격턴 구현 단계

 

<플레이어공격턴 기능>

● 화면에서 죽은 몬스터 별도 표시

죽은 몬스터 공격할수없게 조건식

몬스터 전멸확인 기능 추가

플레이어공격 다이어그램

 

  화면랜더 함수의 몬스터 목록 갱신부분

for (int i = 0; i < currentMonsters.Count; i++)
{
    currentMonsters[i].PrintMonster(i + 1, ConsoleColor.DarkCyan, ConsoleColor.DarkGray);
}

 


 

  공격 함수와 죽은 몬스터 확인 함수

 void PlayerAttack(int index)
 {
     // 중독상태 같은 상태이상 공격이 있을 경우는 플레이어 공격전에 DeadCheck를 먼저 실행해야함
     myPlayer.Attack(currentMonsters[index]);
     MonstersDeadCheck();
 }

 void MonstersDeadCheck()
 {
     // 다음 몬스터턴 행동에 사용될 살아있는 몬스터들 큐에 추가
     foreach (var monster in currentMonsters)
     {
         if (monster.Stat.CurrentHp > 0)               //  살아있는 몬스터 필터링
         {
             monsterQueue.Enqueue(monster);
         }
     }

     // 몬스터들 전부 죽었는지  확인
     foreach (var monster in currentMonsters)
     {
         if (monster.Stat.CurrentHp == 0)
             deadCount++;

         if (deadCount == currentMonsters.Count)
         {
             isWin = true; // 모든 몬스터를 처치했을 때 승리 상태로 변경
             GameManager.Instance.currentState = DungeonState.EndBattle;
             Info("모든 몬스터를 처치했습니다.");
             Thread.Sleep(800);
         }
         else
         {
             GameManager.Instance.currentState = DungeonState.EnemyTurn;
             Thread.Sleep(800);
         }
     }
 }
● 죽은 몬스터 확인을 for문과 foreach문을 하나의 반복문만 쓰는 대신 두가지 다 쓴이유
몬스터가 이미 리스트에 등록된 상태에서 다시금 인덱스들의 확인을 위해 반복문을 사용한다면 정해진 크기에 자주 쓰이는 foreach문으로 통일했겠지만 화면갱신할때 앞에 list.Count에 마다 번호가 보여저야 하기 때문에 비교적 구현이 쉽고 단순한 for문으로 구현함
이후엔 별도 표기가 없고 Count 크기 전체를 훑어봐야 할경우는 foreach문으로 구현함


몬스터턴 구현 단계


< 몬스터 공격 조건 >

● 몬스터가 죽은 상태면 공격하면 안됨

공격한후 다음 몬스터가 공격할 차례에 플레이어가 죽었다면 전투종료

플레이어가 살아있다면 다시 플레이어턴으로우선 몬스터에 요구하는 기능은 다음과 같다

 

  몬스터 공격함수

void EnemyAttackMove(int index)
{
    if (index < 0 || index > currentMonsters.Count)
    {
        Print("\ninfo : 잘못 입력 하셨습니다.");
        Thread.Sleep(300);
        return;
    }

    if( index == 0)
    {            
        if (monsterQueue.Count > 0)
        {
            var nextMonster = monsterQueue.Dequeue(); // 큐에서 몬스터를 하나씩 꺼내서 공격
            int idx = currentMonsters.IndexOf(nextMonster); //list.IndexOf : 리스트속 <T> 객체의 인덱스를 반환
            EnemyAttack(idx);
        }

        else
        {
            // 큐가 비어있다면 플레이어 턴으로 전환
            isDF = false; // 방어 상태 해제
            GameManager.Instance.currentState = DungeonState.PlayerTrun;
            Print("\ninfo : 몬스터의 공격이 끝났습니다");
            Thread.Sleep(800);
        }
    }
}
● 일반 큐(Queue)를 활용한 이유
다수의 몬스터의 행동이 순서대로 첫번째 몬스터가 먼저 행동하고 그다음 그다음으로 넘어가는 순서여서 선입선출(First In, First Out)하면 떠오르는 자료구조인 Queue를 사용해서 살아있는 몬스터만 담아서 플레이어의 입력 다음 순서대로 꺼내어 행동하게 구현함

 

=> Queue의 사용 예시 

주로 게임에서 SRPG전투 조작을 생각하면 Queue자료구조를 이해하기 쉬워진다

나아가 Command Pattern(커맨드 패턴)도 쓰임에 따라서 자료구조를 쓰는데 Queue를 여기에 사용할 수도 있다.

파이어 앰블렘: 풍화설월

 

● Queue에 살아있는 몬스터를 for말고 foreach을 사용한 이유
미리 전투중 추가가 완료된 list여서 확실한 index만 담고있기 때문에 안정하고 간결하게 순회하기 위해 foreach를 사용

 


전투 결과 구현 단계


<전투종료 조건>

● 몬스터 전부가 죽음

플레이어가 죽음

 

  전투결과로 넘어가는 조건들

// 플레이어 공격상태에서 조건
if (deadCount == currentMonsters.Count)
{
    isWin = true; // 모든 몬스터를 처치했을 때 승리 상태로 변경
    GameManager.Instance.currentState = DungeonState.EndBattle;
    Info("모든 몬스터를 처치했습니다.");
    Thread.Sleep(800);
}

// 몬스터턴에서 조건
if (myPlayer.Stat.IsDead)
{
    isDF= false; // 방어 상태 해제
    isWin = false; // 플레이어가 죽었을 때 패배 상태로 변경
    GameManager.Instance.currentState = DungeonState.EndBattle;
    Thread.Sleep(800);
}

 

  결국 둘중 한쪽은 체력==0 이 되어야 끝난다.


  전투결과 화면출력 함수

 void WinRender()
 {
     Print("◎Battle!! - Result◎", ConsoleColor.DarkYellow);
     Print($"\nVictory!\n", ConsoleColor.Green);

     Print($"던전에서 몬스터 {currentMonsters.Count}마리를 잡았습니다.\n");
     Print($"Lv.{myPlayer.Stat.Level} | {myPlayer.Name}");
     Print($"HP.{dungeonHP} -> {myPlayer.Stat.CurrentHp}\n");

     Print(0, "다음", ConsoleColor.DarkCyan);

     Print("\n원하시는 행동을 입력해주세요");
     Console.Write(">>");
 }

 void LoseRender()
 {
     Print("◎Battle!! - Result◎", ConsoleColor.DarkYellow);
     Print($"\nYou Lose!\n", ConsoleColor.Green);

     Print($"Lv.{myPlayer.Stat.Level} | {myPlayer.Name}\n");
     Print($"HP.{dungeonHP} -> {myPlayer.Stat.CurrentHp}\n");
     
     Print(0, "다음", ConsoleColor.DarkCyan);
     Print("\n원하시는 행동을 입력해주세요");
     Console.Write(">>");
 }

 

  최대체력에서 전투종료시 체력을 보여줌


  전투결과에 따른 수치 변화 로직

if (isWin)
{
    GameManager.Instance.currentState = DungeonState.Idle;
    currentMonsters.Clear(); // 몬스터 목록 초기화
    dungeonHP = 0;
    Print("\ninfo : 전투를 종료합니다.");
    Thread.Sleep(300);
}
else
{
    int loseHP = (int)(dungeonHP * 0.5f); // 플레이어가 죽었을 때 체력 감소
    myPlayer.Stat.CurrentHp += loseHP;
    dungeonHP = 0;
    Print("\ninfo : 쓰러진 당신은 던전마법으로 마을로 돌아갑니다.");
    Print($"패배 패널티로 체력이 {myPlayer.Stat.CurrentHp}/{myPlayer.Stat.MaxHp}이 됩니다.");
    GameManager.Instance.SwitchScene(GameState.TownScene); // 마을로 돌아가기
    walkCount = 0;// 이동 횟수 초기화
    Thread.Sleep(1000);
    return;
}

 

  승리시 던전으로 다시 돌아가지만 패배할경우 체력감소 패널티와 마을로 돌아가게됨