티스토리 뷰
학원에서 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} !");
}
...
데미지는 위키에서 구한 계산식을 사용했다.
(데미지 = (((((((레벨 × 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일이라는 짧은 시간 동안 잠 줄여가며 만들었는데
아쉬운건 아쉬운대로, 이 기회에 부족했던 부분을 깨닫고 채울 수 있는 좋은 기회였다.
'게임 개발 > 게임 개발 프로젝트' 카테고리의 다른 글
유니티 2D 게임 프로젝트 (feat. 날려날려) (7) | 2024.06.24 |
---|---|
학원 수업 ) 길 찾기 알고리즘 (1) | 2024.06.11 |
C# 콘솔로 7포커... (feat. 버블정렬, ) (0) | 2024.05.20 |
#003 뱀 게임 (0) | 2024.05.01 |
#002 지뢰찾기 만들기 (0) | 2024.04.30 |