본문 바로가기

내일배움캠프/[P3-Team.] TextRPG

[P3-Team. TextRPG] Enemy 구현, 로그인 화면 개선

본격적인 게임 구현을 시작했다. 조장님의 베이스 코드를 기반으로 하여 내 스타일을 최대한 그쪽에 맞추려 노력했다.

 

목   차

 


몬스터(Enemy) 구현

몬스터 관련 클래스 & 데이터 구현

Base코드에서는 Enemy클래스가 Unit을 상속받은 상태로 기본 생성자만 있는 상태였다. Item객체를 베이스코드에서 ItemDataManager로 관리하는 것을 보고 나는 우선 EnemyDataManager을 만들었다. 그 후, EnemyDataManager은 싱글톤 처리를 해주었다.

public static EnemyDataManager instance = new EnemyDataManager();

싱글톤은 다음과 같이 간단하게만 구현했다.

 

여기에 3개의 멤버 변수를 만들었다. 

  • MonsterDB: 모든 일반몬스터 정보를 담고 있는 List<Enemy>.
  • SpawnMonsters: Stage에 따른 적절한 몬스터 1~4마리를 담아 전달할 List<Enemy>
  • BossMonster: 마지막 스테이지에만 등장할 Enemy 객체.

 그리고 Init 함수를 통해 MonsterDB에 10개의 Enemy객체를 각기 특색을 담아 만들었다. 1레벨 당 하나의 몬스터로 해서 총 10종류의 일반 몬스터가 있다. 이 일반몬스터들의 회피율과 크리티컬율, 크리티컬 데미지는 일단 고정시켰고, 나머지 스탯(공격력, 방어력, 체력)은 일정하게 증가한다.

 

보스몬스터는 일반몬스터보다 공격력과 방어력이 월등하고, 반드시 혼자 등장하는 만큼 체력을 많이 높게 주었다.

몬스터 랜덤 출현 로직

나는 1~4마리의 몬스터가 랜덤으로 나올 때 각 몬스터의 레벨 합이 곧 스테이지의 난이도가 되었으면 했다.

그래서 이산수학을 공부할 때 배운 알고리즘으로 제한된 합계 분할 문제 해결방법을 참고해 만들었다.

몬스터 레벨 a + b + c + d = N(난이도)가 되어야 하고, a~d는 0 이상 10 이하이며 0이면 몬스터가 출현하지 않았다는 뜻이다. 나는 다음과 같은 알고리즘을 이용했다:

private int[] randomMonsterEncount(int totalLevelLimit, int maxLevel)
{
    List<int[]> levelCombinations = new List<int[]>();
    int[] selectedCombination;
    Random random = new Random();

    maxLevel = Math.Min(totalLevelLimit, maxLevel); 

    for (int i = 0; i <= maxLevel; i++)
    {
        for (int j = 0; i + j <= maxLevel; j++)
        {
            for (int k = 0; i + j + k <= maxLevel; k++)
            {
                int l = totalLevelLimit - (i + j + k);
                if (l >= 0 && l <= maxLevel)
                {
                    levelCombinations.Add(new int[] { i, j, k, l });
                }
            }
        }
    }

    selectedCombination = levelCombinations[random.Next(levelCombinations.Count)];
    return selectedCombination;
}

만약 레벨총합의 value가 크다면 이 알고리즘이 프로그램 실행에 부담이 될 수 있다는 생각이 들지만, 여기서 totalLevelLimit은 최대 20이므로 괜찮을 것이라 생각했다.

 

이 알고리즘은 원래 문제 푸는 방법에서 약간 변형되었는데, (a, b, c, d)의 조합 개수에 약간의 여유가 생긴 대신, 이 여러 조합들 중 하나를 꼽아 반환하게 했다. 따라서 selectedCombination은 단지 정수배열을 반환하고, 실제 SpawnMonsters으로의 대입은 GetSpawnMonsters 함수에서 이루어진다. 여기서는 레벨을 키값으로 하여 적절한 Enemy 객체를 MonsterDB에서 뽑아와 SpawnMonsters List에 Add한다.

 

이렇게 만들어진 함수는 다시 DungeonManager 클래스를 거쳐 실제 스크린이나 다른 스크립트로 정보를 전달할 수 있다.

 

이곳에 MonsterEncount라는 Struct를 만들고, 생성자를 통해 EnemyDataManager.intstance.Init()으로 E.D.M.을 초기화한다. 그 밑에 선언된 DungeonManager 클래스 안의 GetMonsterEncount함수에서 Struct 객체를 선언하고, 여기서 다시 리스트를 불러온다. 

 

이제 스크립트에서 DungeonManager.GetMonsterEncount를 부르면 마침내 spawnMonsters 리스트를 불러올 수 있게 된다.

몬스터 스킬 구현

플레이어 스킬을 만들던 조원과 합의가 덜 된 상태라 데이터 외의 구현 방식은 바뀔 수가 있다.

위와 비슷하게 EnemySkill 클래스는 따로 만들었지만, 이 데이터는 Enemy 객체의 List를 통해 저장되고, 데이터를 입력하는 곳도 위와 같은 E.D.M이다. 일단 일반공격과 비례하게 몬스터 레벨에 따라 점점 스킬의 강함이 올라가게 했다.


로그인 화면 업그레이드

이름 확인 & 불러오기 확인

#입력한 이름이 올바른가?

이미 만들어진 LoginText() 함수를 LoginScreenOn(기본 로그인화면 불러오는 역할) 함수에서 NameCheck(이름 입력 및 확인)함수로 끌고 왔다.

또, While문 2개로 바깥쪽은 이름을 입력받는 UI, 안쪽은 이 이름이 맞는지 확인하도록 했다.

Console.Clear()을 활용해서 바깥쪽 while문은 한번에 지우는 걸로 하고, 안쪽은 일부만 지우는 것이 불가능하므로 SetCursorPosition 함수로 위치를 잡고 그냥 "                       " 이런 식으로 수동 삭제를 했다. 사용자가 고의로 콘솔창에 낙서를 한다면 조금 슬퍼질 테지만...

 

아예 유효하지 않은 입력에 대해서는 "잘못된 입력입니다! 1 또는 2를 다시 입력하세요." 라는 안내 후, Thread로 1초 지연시킴으로써 사용자가 이 출력을 볼 수 있게 한 후 다시 안쪽 while문의 초반부로 돌아간다.

#이 이름에 대한 데이터가 존재하는데, 그 데이터를 불러올 것인가?

이 부분은 위에보다 훨씬 쉬웠다. 위의 방법과 거의 같지만, 이번에는 2번의 입력을 받을 필요가 없으므로 평소처럼 한 번의 while문만 사용해 적절한 타이밍에 탈출하도록만 하면 된다.

만약 사용자의 이름이 데이터베이스에 없거나 존재하는 이름으로 새로운 저장데이터를 만들길 원한다면, 이 이름으로 ClassSelectionScreen으로 보내버리기만 하면 된다.


 

마무리

어제 너무 힘들어서 TIL의 구성만 잡아놓고 상세 내용은 사실 다음날에 보충했다...

더 많은 일을 하고 싶은데 매일 있는 C# 체크리스트 시간때문에 오후 작업시간이 훅 날아가버리는게 항상 아쉽다.