티스토리 뷰

 

학원에서 C# 언어 공부를 마치며 개인 프로젝트를 만들게 되었다.

콘솔 창에서 플레이 가능한 TEXT RPG를 만드는 것이었다.

이전 과제 때 제출했던 콘솔용 포켓몬스터 게임을 더 다듬어 보기로 했다.

 

규정상 학원 로고를 삽입했습니다.

 

 

 


 

똑같은 프로젝트를 다시 진행하면서 특별히 추가된 것은 스프라이트 이미지이다.

 

콘솔 환경이므로 아스키 아트를 사용하였다.

 

 

무료 변환 사이트의 도움으로 30여 종의 텍스트 이미지를 만들고 파일을 불러와 그대로 출력했다.

 

        //콘솔 창에서 이미지를 출력할 위치와 파일명을 매개변수로 사용한다.
        public static void DrawImage(int cursorPosX, int cursorPosY, string fileName)
        {
        //기본적으로 실행파일 위치에 resources 폴더를 만들어 사용한다.
            string filePath = "resources/sprites/" + fileName + ".txt";
            string[] lines = File.ReadAllLines(filePath);
			
            //for문을 사용해서 열 단위로 출력한다.
            for (int i = 0; i < lines.Length; i++)
            {
            	//콘솔 커서의 행 위치는 변하지 않지만 열 위치는 i 값을 따라 증가한다. 
                Console.SetCursorPosition(cursorPosX, cursorPosY + i);
                Console.WriteLine(lines[i]);
            }
        }

 

몇 줄 안되는 코드로 이미지를 출력할 수 있었다.

 

이미지를 준비했으면 실제 포켓몬 데이터를 준비한다.

	Pokemon Venusaur = new Pokemon("이상해꽃", "이상해꽃은", "이상해꽃을", "이상해꽃이", new int[6] { 80, 82, 83, 100, 100, 80 }, PkTypes.풀, PkTypes.독);
        Pokemon Charizard = new Pokemon("리자몽", "리자몽은", "리자몽을", "리자몽이", new int[6] { 78, 84, 78, 109, 85, 100 }, PkTypes.불꽃, PkTypes.비행);
        Pokemon Blastoise = new Pokemon("거북왕", "거북왕은", "거북왕을", "거북왕이", new int[6] { 79, 83, 100, 85, 105, 78 }, PkTypes.물, PkTypes.없음);
        Pokemon Pidgeot = new Pokemon("피죤투", "피죤투는", "피죤투를", "피죤투가", new int[6] { 83, 80, 75, 70, 70, 101 }, PkTypes.노말, PkTypes.비행);
        Pokemon Arbok = new Pokemon("아보크", "아보크는", "아보크를", "아보크가", new int[6] { 60, 95, 69, 65, 79, 80 }, PkTypes.독, PkTypes.없음);
        Pokemon Pikachu = new Pokemon("피카츄", "피카츄는", "피카츄를", "피카츄가", new int[6] { 35, 55, 40, 50, 50, 90 }, PkTypes.전기, PkTypes.없음);
        Pokemon Golbat = new Pokemon("골뱃", "골뱃은", "골뱃을", "골뱃이", new int[6] { 75, 80, 70, 65, 75, 90 }, PkTypes.독, PkTypes.비행);
        Pokemon Vileplume = new Pokemon("라플레시아", "라플레시아는", "라플레시아를", "라플레시아가", new int[6] { 75, 80, 85, 110, 90, 50 }, PkTypes.풀, PkTypes.독);
        Pokemon Arcanine = new Pokemon("윈디", "윈디는", "윈디를", "윈디가", new int[6] { 90, 110, 80, 100, 80, 95 }, PkTypes.불꽃, PkTypes.없음);
        Pokemon Alakazam = new Pokemon("후딘", "후딘은", "후딘을", "후딘이", new int[6] { 55, 50, 45, 135, 95, 120 }, PkTypes.에스퍼, PkTypes.없음);
        Pokemon Machamp = new Pokemon("괴력몬", "괴력몬은", "괴력몬을", "괴력몬이", new int[6] { 90, 130, 80, 65, 85, 55 }, PkTypes.격투, PkTypes.없음);
        Pokemon Golem = new Pokemon("딱구리", "딱구리는", "딱구리를", "딱구리가", new int[6] { 80, 120, 130, 55, 65, 45 }, PkTypes.바위, PkTypes.땅);
        Pokemon Slowbro = new Pokemon("야도란", "야도란은", "야도란을", "야도란이", new int[6] { 95, 75, 110, 100, 80, 30 }, PkTypes.물, PkTypes.에스퍼);
        Pokemon Dewgong = new Pokemon("쥬레곤", "쥬레곤은", "쥬레곤을", "쥬레곤이", new int[6] { 90, 70, 80, 70, 95, 70 }, PkTypes.물, PkTypes.얼음);
        Pokemon Cloyster = new Pokemon("파르셀", "파르셀은", "파르셀을", "파르셀이", new int[6] { 50, 95, 180, 85, 45, 70 }, PkTypes.물, PkTypes.얼음);
        Pokemon Haunter = new Pokemon("고우스트", "고우스트는", "고우스트를", "고우스트가", new int[6] { 45, 50, 45, 115, 55, 95 }, PkTypes.고스트, PkTypes.독);
        Pokemon Gengar = new Pokemon("팬텀", "팬텀은", "팬텀을", "팬텀이", new int[6] { 60, 65, 60, 130, 75, 110 }, PkTypes.고스트, PkTypes.독);
        Pokemon Onix = new Pokemon("롱스톤", "롱스톤은", "롱스톤을", "롱스톤아", new int[6] { 35, 45, 160, 30, 45, 70 }, PkTypes.바위, PkTypes.땅);
        Pokemon Exeggutor = new Pokemon("나시", "나시는", "나시를", "나시가", new int[6] { 95, 95, 85, 125, 75, 55 }, PkTypes.풀, PkTypes.에스퍼);
        Pokemon Hitmonlee = new Pokemon("시라소몬", "시라소몬은", "시라소몬을", "시라소몬이", new int[6] { 50, 120, 53, 35, 110, 87 }, PkTypes.격투, PkTypes.없음);
        Pokemon Hitmonchan = new Pokemon("홍수몬", "홍수몬은", "홍수몬을", "홍수몬이", new int[6] { 50, 105, 79, 35, 110, 76 }, PkTypes.격투, PkTypes.없음);
        Pokemon Jynx = new Pokemon("루주라", "루주라는", "루주라를", "루주라가", new int[6] { 65, 50, 35, 115, 95, 95 }, PkTypes.얼음, PkTypes.에스퍼);
        Pokemon Gyarados = new Pokemon("갸라도스", "갸라도스는", "갸라도스를", "갸라도스가", new int[6] { 95, 125, 79, 60, 100, 81 }, PkTypes.물, PkTypes.비행);
        Pokemon Lapras = new Pokemon("라프라스", "라프라스는", "라프라스를", "라프라스가", new int[6] { 130, 85, 80, 85, 95, 60 }, PkTypes.물, PkTypes.얼음);
        Pokemon Jolteon = new Pokemon("쥬피썬더", "쥬피썬더는", "쥬피썬더를", "쥬피썬더가", new int[6] { 65, 65, 60, 110, 95, 130 }, PkTypes.전기, PkTypes.없음);
        Pokemon Aerodactyl = new Pokemon("프테라", "프테라는", "프테라를", "프테라가", new int[6] { 80, 105, 65, 60, 75, 130 }, PkTypes.바위, PkTypes.비행);
        Pokemon Snorlax = new Pokemon("잠만보", "잠만보는", "잠만보를", "잠만보가", new int[6] { 160, 110, 65, 65, 110, 30 }, PkTypes.노말, PkTypes.없음);
        Pokemon Dragonair = new Pokemon("신뇽", "신뇽은", "신뇽을", "신뇽이", new int[6] { 61, 84, 65, 70, 70, 70 }, PkTypes.드래곤, PkTypes.없음);
        Pokemon Dragonite = new Pokemon("망나뇽", "망나뇽은", "망나뇽을", "망나뇽이", new int[6] { 91, 134, 95, 100, 100, 80 }, PkTypes.드래곤, PkTypes.비행);

 

포켓몬 클래스에는

이름, 레벨, 성별, 성격, 배열로 저장된 6가지 종족값, 개체값, 노력치, 실제 능력치,

최대 4가지의 기술을 저장할 Skill[4] 등이 멤버 변수로 선언 되어 있다. 

생성자를 사용할 때는 포켓몬 고유의 데이터를 입력하게 되어있다.

이름, 종족값, 타입이다.

 

타입은 열거형으로 만들었다.

 

enum PkTypes
    {
        없음 = 0,
        노말,
        불꽃,
        물,
        풀,
        전기,
        얼음,
        격투,
        독,
        땅,
        비행,
        에스퍼,
        벌레,
        바위,
        고스트,
        드래곤,
        악,
        강철,
        페어리
    };

 

요소에 한글 사용이 가능한데, 이는 나중에 ToString()을 사용할 필요가 없어 꽤 유용하다.

 

이것을 int 형으로 캐스팅하여 사용할 수도 있는데,

 

  private readonly float[,] typeAdvantage = new float[19, 19]
       {
           //0      1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18  
            {1,     1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1}, // None
                                                                                                       
        	{1,     1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1, .5f,   0,   1,   1, .5f,   1}, //노말 1
        	{1,     1, .5f, .5f,   2,   1,   2,   1,   1,   1,   1,   1,   2, .5f,   1, .5f,   1,   2,   1}, // 불꽃 2
        	{1,     1,   2, .5f, .5f,   1,   1,   1,   1,   2,   1,   1,   1,   2,   1, .5f,   1,   1,   1}, // 물 3
        	{1,     1, .5f,   2, .5f,   1,   1,   1, .5f,   2, .5f,   1, .5f,   2,   1, .5f,   1, .5f,   1}, // 풀 4
        	{1,     1,   1,   2, .5f, .5f,   1,   1,   1,   0,   2,   1,   1,   1,   1, .5f,   1,   1,   1}, // 전기 5
        	{1,     1, .5f, .5f,   2,   1, .5f,   1,   1,   2,   2,   1,   1,   1,   1,   2,   1, .5f,   1}, // 얼음 6
        	{1,     2,   1,   1,   1,   1,   2,   1, .5f,   1, .5f, .5f, .5f,   2,   0,   1,   2,   2, .5f}, // 격투 7
        	{1,     1,   1,   1,   2,   1,   1,   1, .5f, .5f,   1,   1,   1, .5f, .5f,   1,   1,   0,   2}, // 독 8
        	{1,     1,   2,   1, .5f,   2,   1,   1,   2,   1,   0,   1, .5f,   2,   1,   1,   1,   2,   1}, // 땅 9
        	{1,     1,   1,   1,   2, .5f,   1,   2,   1,   1,   1,   1,   2, .5f,   1,   1,   1, .5f,   1}, // 비행 10
        	{1,     1,   1,   1,   1,   1,   1,   2,   2,   1,   1, .5f,   1,   1,   1,   1,   0, .5f,   1}, // 에스퍼 11
        	{1,     1, .5f,   1,   2,   1,   1, .5f, .5f,   1, .5f,   2,   1,   1, .5f,   1,   2, .5f, .5f}, // 벌레 12
        	{1,     1,  2,    1,   1,   1,   2, .5f,   1, .5f,   2,   1,   2,   1,   1,   1,   1, .5f,   1}, // 바위 13
        	{1,     0,   1,   1,   1,   1,   1,   1,   1,   1,   1,   2,   1,   1,   2,   1, .5f,   1,   1}, // 고스트 14
        	{1,     1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   2,   1, .5f,   0}, // 드래곤 15
        	{1,     1,   1,   1,   1,   1,   1, .5f,   1,   1,   1,   2,   1,   1,   2,   1, .5f,   1, .5f}, // 악 16
        	{1,     1, .5f, .5f,   1, .5f,   2,   1,   1,   1,   1,   1,   1,   2,   1,   1,   1, .5f,   2}, // 강철 17
        	{1,     1, .5f,   1,   1,   1,   1,   2, .5f,   1,   1,   1,   1,   1,   1,   2,   2, .5f,   1}, // 페어리 18
       };
       
        public float TypeAdvantage(int atkType, int hitType)
        {
            return typeAdvantage[atkType, hitType];
        }

 

이차원 배열을 사용하여 typeAdvantage[ 공격하는 타입, 방어하는 타입] 으로 기술의 위력 배수를 구할 수 있다.

 

가위 바위 보 같은 상성도 비슷하게 구현할 수 있을 것이다. ( 패배 -1 / 무승부 0 / 승리 1)

 

이 부분은 Skill 클래스에 정리해두었는데,

 

Skill 클래스의 구성은 아래와 같다.

 

 

// 기술을 사용한 포켓몬, 기술을 당한 포켓몬, (out) 상대에게 입힌 데미지
void CaculateDemage(Pokemon attacker, Pokemon target, out int trueDamage)
{
	데미지 계산을 하고 상대의 체력을 감소시킨다.
    out 키워드로 입힌 데미지를 받아 반동 데미지나 체력 흡수에 사용합니다.
}

// 기술을 당한 포켓몬, 변경될 스탯, 스탯이 변경될 확률, 스탯 변동치
void SetRank(Pokemon target, int statNum, int percent, int setSize)
{
	공격자나 피격자의 능력치를 변동시키는 부가효과를 가진 기술에 이 메서드를 추가합니다.
    난수와 매개변수의 확률을 비교하여 부가효과 발동 유무를 결정합니다.
    만약 반드시 상대의 능력치를 떨어트리거나 자신의 능력치를 올리는 기술은 100을 입력합니다.
    스탯은 
}

// 기술을 사용할 포켓몬, 상대에게 입힌 데미지, 돌려받을 데미지 비율
void KnockBack(Pokemon target, int trueDamage, float percent)
{
	CaculateDemage 에서 얻은 trueDamage 값을 사용자에게 반동 데미지로 되돌려 준다.
    이 값이 + 라면 체력 흡수 효과처럼 쓸 수도 있다.
}

// 기술을 사용할 포켓몬, 기술을 당한 포켓몬
bool IsHit(Pokemon attacker, Pokemon target)
{
	사용자의 명중률과 피격자의 회피율을 계산하여 난수와 비교합니다.
    난수보다 크거나 작은 경우에 따라 기술의 성공과 실패를 판단합니다.
}

// 기술을 사용하는 포켓몬, 기술을 받은 포켓몬
virtual int ChoicePoint(Pokemon attacker, Pokemon target)
{
	자신과 상대의 정보를 토대로 이 기술이 얼마나 효과적인지를 점수로 계산합니다.
    NPC는 점수가 가장 높은 기술을 사용하게 됩니다.
}


// 기술위 메서드들을 이 가상 함수에 조합하여 사용합니다.
public virtual void Effect(Pokemon attacker, Pokemon target)
{
	GameManager.FlowChat($"{attacker.NickName}의 {name} !");
}

...

 

 

 

데미지는 위키에서 구한 계산식을 사용했다.

 

데미지 | 포켓몬 위키 | Fandom

 

데미지

데미지는 배틀에서 기술로 포켓몬의 HP를 줄이는 수치이다. (데미지 = (위력 × 공격 × (레벨 × [[급소]] × 2 ÷ 5 + 2 ) ÷ 방어 ÷ 50 + 2 ) × [[자속 보정]] × 타입상성1 × 타입상성2 × 랜덤수/255) (데미지

pokemon.fandom.com

 

 

(데미지 = (((((((레벨 × 2 ÷ 5) + 2) × 위력 × 특수공격 ÷ 50) ÷ 특수방어) × Mod1) + 2) × [[급소]] × Mod2 ×  랜덤수 ÷ 100) × 자속보정 × 타입상성1 × 타입상성2 × Mod3)

 

 

Mod 는 지닌 도구나 사용한 날씨와 필드 기술, 상태이상, 아이템 등의 사용 유무에 따라 추가되는 수치이다.

 

일단 기술이 특수 기술이라면 공격자의 특수 공격 수치와 피격자의 특수 방어 수치를 적용할 것이다.

 

    enum AttackType
    {
        변화 = 0,
        물리 = 1,
        특수 = 2
    }

 

이것은 열거형으로 기술의 공격 타입을 선언한 것이다.

 

        /// <summary>
        /// 현재 레벨의 스탯
        /// 0.HP 1.공격 2.방어 3.특공 4.특방 5.스피드
        /// </summary>
        public int[] initialStats = new int[6];

 

이것은 포켓몬의 능력치이다. 인덱스 값에 따라 필요한 스탯을 사용할 수 있다.

 

그렇다면 물리, 특수 공격 계산은 이렇게 정리할 수 있다.

            float attackerPoint = 1; // 기술을 사용한 포켓몬의 공격 실능
            float targetPoint = 1; // 기술을 당한 포켓몬의 방어 실능

            //공격하는 쪽에서 사용할 공격 스탯 (공격 1 / 특수 공격 3)
            int attackStat = (int)Math.Pow(2, (int)attackType) - 1;
            //방어하는 쪽에서 사용할 방어 스탯 (방어 2 / 특수 방어 4)
            int defenseStat = (int)Math.Pow(2, (int)attackType);

            attackerPoint = attacker.initialStats[attackStat] * (attacker.statRank[attackStat] > 0 ? 1 + attacker.statRank[attackStat] * 0.5f : 2f / (2 - attacker.statRank[attackStat]));
            targetPoint = target.initialStats[defenseStat] * (target.statRank[defenseStat] > 0 ? 1 + target.statRank[defenseStat] * 0.5f : 2f / (2 - target.statRank[defenseStat]));

 

여기서 포켓몬의 능력치 변화도 계산 해야하는데

 

다른 게임에서 말하는 버프와 디버프가 바로 그것이다.

 

최소 -6 에서 +6 랭크까지 증감하며 그 수치에 따라

 

능력치가 25% 에서 400% 까지 변동된다.

 

 

그러니 공격 형태에 따라 적용할 랭크 변동 값도 달라질 것이다.

 

전투 중 능력치 증감에 대한 수치도 int[6] 형으로 선언하였으므로

 

attacker.statRank[attackStat] 와 같이 사용했다.

 

여기서 인덱서의 필요성을 느꼈다.

 

랭크 변화는 최소 -6, 최대 +6 이므로 범위를 초과하는 값이 저장되면 안되기 때문에...

 

using System;

class BoundedArray
{
    private int[] array = new int[6];

    // 인덱서 프로퍼티
    public int this[int index]
    {
        get
        {
            if (index < 0 || index >= array.Length)
                throw new IndexOutOfRangeException("인덱스가 배열 범위를 벗어났습니다.");
            return array[index];
        }
        set
        {
            if (index < 0 || index >= array.Length)
                throw new IndexOutOfRangeException("인덱스가 배열 범위를 벗어났습니다.");
            if (value < -6 || value > 6)
                throw new ArgumentOutOfRangeException("값은 -6에서 6 사이여야 합니다.");
            array[index] = value;
        }
    }

    // 배열 출력 메서드
    public void PrintArray()
    {
        for (int i = 0; i < array.Length; i++)
        {
            Console.Write(array[i] + " ");
        }
        Console.WriteLine();
    }
}

 

 

 


 

 

 

 

프로젝트를 하면서 수업 중에 배운 것들을 다양하게 적용해보려고 시도해보았다.

상태 패턴을 응용하여 포켓몬의 상태이상 시스템을 구현할 수 있었고

팩토리 패턴을 참고하여 트레이너나 포켓몬의 인스턴스를 간단하게 만들 수 있었다.

 

그리고 시스템 구현보다 어렵게 느낀 것은 시스템 기획이었다...   

 

 

처음 만들어보는 플로우 차트인데 

기획이 엉망이니 전투 시스템을 구현하는 부분에서 큰 난항을 겪었다...

어찌어찌 완성했다해도 if 문과 switch 문이 적재적소에 쓰였는지 생각해보면 자신이 없다... 

 

교육과정 중 첫 프로젝트였다. 3일이라는 짧은 시간 동안 잠 줄여가며 만들었는데

아쉬운건 아쉬운대로, 이 기회에 부족했던 부분을 깨닫고 채울 수 있는 좋은 기회였다.  

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함