텍스트 Rpg - C#

C#으로 만드는 텍스트 Rpg (4)

게임만드는학생 2025. 2. 3. 17:23

미션4를 해결했다. 

간단하게 공격과 죽었을 때의 이벤트가 발생하게끔 구현했다. 

문법의 개념과 사용법 자체는 어렵지 않은 것 같다. 

어떻게 설계에 사용되는지 익히면 될 것 같다. 

여기선 간단하게 문장출력으로 구현했다.

유니티에서는 여러가지 로직이 추가될 것 같다. 

 

미션 4: 델리게이트와 이벤트

  • 목표: 델리게이트와 이벤트를 활용하여 게임의 상태를 관리한다.
  • 미션:
    1. 체력이 0 이하가 되면 "게임 오버" 이벤트를 발생시키는 델리게이트와 이벤트를 만들어라.
    2. 플레이어가 공격할 때 "공격 이벤트"를 발생시키고, 콘솔에 공격 메시지를 출력해라.
    3. 몬스터가 죽으면 "몬스터 처치 이벤트"를 발생시켜라.

 

처음엔 delegate를 직접 커스텀해서 event를 각 player와 monster에 만들었는데, 

이걸 IGameInterface 즉, player와 monster가 공통적으로 상속하는 저 인터페이스에 구현하는게 낫겠다고 판단했다.

하지만 delegate는 클래스나 네임스페이스 내에서만 선언이 가능하고 인터페이스에서는 불가능하다는 것을 알았다. 

그래서 c# 에서 내장돼있는 delegate인 Action을 사용하여 해결했다. 

 

public interface IGameCharacter
{
    public int Hp { get; set; }
    public int AttackPower { get; set; }
    public string Name { get; set; }

     //delegate void OnDead();
     event Action OnDeadEvent;

     //delegate void OnAttack();
     event Action OnAttackEvent;
}

이렇게 action 을 사용하여 구현하였다. 

유니티에서 복잡한 입출력이 필요하다면 그때, 커스텀하거나 하면 될 것 같다. 

 

 public class Player : IGameCharacter
 {
     public int MaxHp { get; set; } = 100;
     int _hp;
     int _exp = 0;

     //public delegate void OnDead();
     public event Action OnDeadEvent;

     //public delegate void OnAttack();
     public event Action OnAttackEvent;

플레이어에 이렇게 구현했고

 

public int Hp { get { return _hp; }
    set {
        if (value <= 0) { _hp = 0; OnDeadEvent?.Invoke(); }
        else if (value > MaxHp) { _hp = MaxHp; }
        else { _hp = value; } 
    } 
}

public Player() { _hp = MaxHp;OnDeadEvent += Dead; OnAttackEvent += PlayAttack; }

public void PlayAttack()
{
    Console.WriteLine("플레이어가 공격모션을 실행합니다. in Player");
}

public void Attack(Monster monster)
{
    OnAttackEvent?.Invoke();
    monster.TakeDamage(AttackPower);

}

이렇게 생성자에서 이벤트에 내가 가지고 있는 함수를 등록했고, hp의 set프로퍼티에서 0일때 deadevent를 호출하게 했다. 

attack도 마찬가지로 attack함수 내에서 invoke를 실행했다. '

 

몬스터도 마찬가지이다. 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TextRpgCS
{
    public class Monster : IGameCharacter
    {
        static int CountId = 1;

        int _hp = 30;

        public int Id { get; } = CountId;
        public string Name { get; set; }
        public int Level { get; set; } = 1;
        public int Exp { get; set; } = 10;
        public int Hp { get { return _hp; } set { if (value <= 0) { _hp = 0; OnDeadEvent?.Invoke(); } else { _hp = value; } } }

        //public delegate void OnDead();
        public event Action OnDeadEvent;

        //public delegate void OnAttack();
        public event Action OnAttackEvent;

        public int AttackPower { get; set; } = 10;
        
        public Monster()
        {
            Level = 1*CountId;
            Exp = 10*CountId;
            Hp = 30*CountId;
            AttackPower = 10*CountId/2;
            this.Name = $"몬스터{CountId++}";

            OnDeadEvent += Dead;
            OnAttackEvent += PlayAttack;
        }

        public void PlayAttack()
        {
            Console.WriteLine($"{Name}몬스터가 공격을 시도합니다.");
        }

        public void Attack(Player player)
        {
            OnAttackEvent?.Invoke();
            player.TakeDamage(AttackPower);
        }

        public void TakeDamage(int damage)
        {
            Hp -= damage;
            Console.WriteLine($"{Name}에게 데미지{damage}를 입혔습니다. {Name}의 체력 : {Hp}");
        }

        void Dead()
        {
            Console.WriteLine($"{CountId}번 몬스터 사망!");
        }
    }
}

 

 

마지막은 GameManager에서도 구독하여 외부에서 구독한 것을 구현해봤다.

 

public void Init()
{
    Player = new Player();
    monsters = new List<Monster>();
    for(int i = 0; i < MonsterCount; i++) 
    {
        monsters.Add(new Monster());
        monsters[i].OnDeadEvent += BroadcastMonsterDead;
    }

    _boss = new Boss();
    _boss.OnDeadEvent += BroadcastMonsterDead;
    // 이벤트 등록
    Player.OnDeadEvent += GameOver;
    Player.OnAttackEvent += BroadcastPlayerAttack;

}


void GameOver()
{
    Console.WriteLine("플레이어가 사망하여 게임이 종료되었습니다. in GameManager");
}

void BroadcastPlayerAttack()
{
    Console.WriteLine("GameManager : 플레이어가 공격을 시도합니다!");
}

void BroadcastMonsterDead()
{
    Console.WriteLine("몬스터가 사망합니다!");
}

3개의 테스트 구독용 함수를 만들어서 구독을 구현했다.

 

추가로 gameManager에 있던 전투관련함수를 BattleManager를 만들어서 분리했다. 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TextRpgCS
{
    internal class BattleManager
    {
        public static BattleManager Instance = new BattleManager();


        public void Init()
        {

        }

        public ResultBattle StartBattle(Player player, Monster monster)
        {
            Console.WriteLine($"{monster.Name}이(가) 당신을 공격합니다!\n");
            int choice;
            while (player.Hp > 0 && monster.Hp > 0)
            {
                Console.WriteLine(UtilTextManager.ChoiceMenuInBattle);
                choice = int.Parse(Console.ReadLine());
                if (choice == 3)
                {
                    if (monster is Boss)
                        Console.WriteLine(UtilTextManager.RetreatBoss);
                    else
                    {
                        Console.WriteLine(UtilTextManager.ExitDungeon);
                        return ResultBattle.RetreatPlayer;
                    }
                }
                else if (choice == 2)
                {
                    // 인벤토리 보여주기
                    ItemManager.Instance.PrintInventory();
                }
                else
                {
                    Console.WriteLine($"용사{player.Name}가 {monster.Name}을 공격!");
                    player.Attack(monster);
                    if (monster.Hp <= 0) return ResultBattle.MonsterDie;
                    monster.Attack(player);
                }
            }
            return ResultBattle.PlayerDie;
        }
    }
}

 

마지막으로 테스트다. 이것은 chatgpt가 작성한 유닛테스트 코드이다.

msTest프로젝트를 추가하여 미션4에 대한 테스트를 진행했다. 

 

테스트의 장점은 내가 캐치못한 부분을 혼자 이렇게 돌아가겠지하고코드를 일일이 따라가는 것보다 훨씬 빠르게 문제를 캐치할 수 있다는 점이다. 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TextRpgCS
{
    internal class BattleManager
    {
        public static BattleManager Instance = new BattleManager();


        public void Init()
        {

        }

        public ResultBattle StartBattle(Player player, Monster monster)
        {
            Console.WriteLine($"{monster.Name}이(가) 당신을 공격합니다!\n");
            int choice;
            while (player.Hp > 0 && monster.Hp > 0)
            {
                Console.WriteLine(UtilTextManager.ChoiceMenuInBattle);
                choice = int.Parse(Console.ReadLine());
                if (choice == 3)
                {
                    if (monster is Boss)
                        Console.WriteLine(UtilTextManager.RetreatBoss);
                    else
                    {
                        Console.WriteLine(UtilTextManager.ExitDungeon);
                        return ResultBattle.RetreatPlayer;
                    }
                }
                else if (choice == 2)
                {
                    // 인벤토리 보여주기
                    ItemManager.Instance.PrintInventory();
                }
                else
                {
                    Console.WriteLine($"용사{player.Name}가 {monster.Name}을 공격!");
                    player.Attack(monster);
                    if (monster.Hp <= 0) return ResultBattle.MonsterDie;
                    monster.Attack(player);
                }
            }
            return ResultBattle.PlayerDie;
        }
    }
}

테스트 성공했다.