3주차의 첫 번째 과제인 뱀 게임이다. 문제에서 기본으로 제공되는 코드소스가 있긴 했으나, 그것과 강의내용만으로는 초보 개발자가 이 과제를 이루기가 힘들 것 같다는 생각을 했다.
또 3주차 과제의 해설은 참고한 적이 없고, 기본제공된 코드소스의 일부 역시 임의로 수정했기에 해설과는 다른 점이 있을 수 있다.
(당신이 코딩 뉴비라면)이 글은 맨 위에서부터 차근차근 읽기보다는 필요할 때마다 글의 위 아래를 오가면서 클래스끼리 어떻게 상호작용을 하게 되는지 따라가며 읽는 것을 추천한다.
목 차
Main: Program class
코드를 종합하여 유저와 상호작용을 담당하는 클래스.
게임 시작 전 세팅
// 뱀의 초기 위치와 방향을 설정하고, 그립니다.
Point headPos = new Point(3, 3, '*'); //<1>
Snake snake = new Snake(new Point(headPos), 5, Direction.RIGHT); //<2>
FoodCreator foodCreator = new FoodCreator(snake); //음식 기본: $, 변경가능. //<3>
/*맵 만들기*/ //<4>
Console.WriteLine("//////////////////////////////////////////////////");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("/ /");
Console.WriteLine("//////////////////////////////////////////////////");
Console.WriteLine("게임을 시작하려면 Enter키를 누르세요!"); //<5>
Console.ReadLine();
Console.SetCursorPosition(0, 15); //<6>
Console.WriteLine(" ");
Console.WriteLine(" ");
<1> ~ <3>: 각 클래스 객체의 선언이다. 상세 내용을 보려면 각 클래스를 찾아가서 보는게 좋지만, 여기에서도 간단하게 설명을 한다.
<1>: Point 객체의 선언과 인스턴스 생성이다. 이 선언에서 사용된 생성자는 다음과 같다:
public Point(int _x, int _y, char _sym) //x와 y는 Point의 좌표, sym은 Point(점)을 시각화할 때 사용할 기호.
<2>: Snake 객체의 선언과 인스턴스 생성이다. 이 선언에서 사용된 생성자는 다음과 같다:
//Point는 뱀머리의 위치, int는 뱀의 총 길이, Direction은 뱀이 향할 방향.
public Snake(Point snakePos, int snakeLen, Direction snakeDir)
<3>: FoodCreator 객체의 선언과 인스턴스 생성이다. 이 선언에서 사용된 생성자는 다음과 같다:
//Snake는 게임에서 사용될 뱀 객체를 참조로 받아옴, char은 밥의 모양으로 기본은 '$'
public FoodCreator(Snake snake, char c = '$')
여기서 마지막에 char c = '$' 가 생소할 수 있다. 이건 기본(default) 매개변수이다. 아까 <3>의 선언부에서는 분명 char의 입력이 없었다. 그런 경우에, c에는 임의로 '$' 값이 들어가게 된다. 하지만 만약 new FoodCreator(snake, '!')로 인스턴스를 생성한다면, 이제 c값에는 default값인 '$' 대신 '!'이 들어가게 되는 것이다.

이러한 default 매개변수를 사용하는 이유는 c값의 입력이 있을 때와 없을 때 모두 하나의 생성자로 입력받을 수 있기 때문이다. 주의사항으로는 반드시 가장 오른쪽에 있는 것부터 순서대로 default 매개변수를 지정해주어야 한다는 것이다. 그 이유는, 컴퓨터가 값을 읽어올 때 선언부에서 가장 왼쪽의 인자부터 순서대로 대입을 하기 때문이다. 위의 그림에서 snake = mySnake 처럼 default를 입력한다고 한들, 컴퓨터는 '!'가 앞에 들어가야 할지 뒤에 들어가야 할지 생각하지 않고 반드시 왼쪽부터 순서대로 대입할 것이다.
<4>: snake가 돌아다닐 공간이다. tic-tac-toe와 달리 따로 함수로 빼지 않은 이유는 이번 게임에서는 Console.Clear로 화면 전체를 지우는 것이 아니라 필요한 부분만 골라(Console.SetCursorPosition) ' ' 등의 공백문자로 지워줄 예정이기 때문이다. 따라서 이 맵은 게임 종료시까지 유지될 것이다.
<5>: 프로그램 시작 직후 바로 뱀이 움직이는게 아니라 사용자가 준비할 시간을 벌어준다. tic-tac-toe에서 보였던 잘못된 결과 출력과 같은 방식으로 사용자의 입력(enter)을 받으면 다음 코드로 넘어갈 수 있다.
<6>: 게임시작 멘트(게임을 시작하려면~)을 지우기 위해 커서의 위치를 한 칸 올려준다. 그후 <5>에서 사용자가 실수로 엔터 키가 아닌 다른 문자를 입력한 경우에 대비하여 2개의 줄을 공백으로 지워준다. 공백의 길이보다 사용자의 입력이 길면 뒷부분이 지워지지 않는 문제가 있을 수 있다. 하지만 한 줄만 완벽하게 지워주는 코드를 모르겠어서 우선은 위와 같이 만들었다.
게임 진행
우선 0.1초마다 초기화 될 코드의 전문을 보여주고, 다시 코드를 쪼개서 상세설명한다.
전체 코드 ▼
// 게임 루프: 이 루프는 게임이 끝날 때까지 계속 실행됩니다.
while (true)
{
/*음식 생성*/
if (foodDelay++ % 10 == 0) //음식생성속도.
{
foodCreator.CreateFood();
}
// 방향키 입력 시 방향 변경.
if (Console.KeyAvailable)
{
ConsoleKeyInfo arrowDirection = Console.ReadKey();
switch (arrowDirection.Key)
{
case ConsoleKey.UpArrow:
snake.direction = Direction.UP;
break;
case ConsoleKey.DownArrow:
snake.direction = Direction.DOWN;
break;
case ConsoleKey.LeftArrow:
snake.direction = Direction.LEFT;
break;
case ConsoleKey.RightArrow:
snake.direction = Direction.RIGHT;
break;
default:
break;
}
}
/*뱀의 이동*/
snake.body.Enqueue(new Point(headPos));
headPos.Draw();
snake.Erase();
switch (snake.direction)
{
case Direction.LEFT:
headPos.x -= 1;
break;
case Direction.RIGHT:
headPos.x += 1;
break;
case Direction.UP:
headPos.y -= 1;
break;
case Direction.DOWN:
headPos.y += 1;
break;
default:
break;
}
/*뱀의 충돌처리(음식인지 자기자신인지 벽인지.)*/
if (headPos.x > 0 && headPos. x < 49)
{
if (headPos.y > 0 && headPos.y < 14)
{
if (snake.SnakeConflict(headPos)) //뱀
{
Console.SetCursorPosition(0, 17);
Console.WriteLine("스스로에 부딪힘!");
break;
}
else if (foodCreator.FoodConflict(headPos))
{
snake.length++;
snake.food++;
}
}
else //벽(위, 아래)
{
Console.SetCursorPosition(0, 17);
Console.WriteLine("벽에 부딪힘!");
break;
}
}
else //벽(좌, 우)
{
Console.SetCursorPosition(0, 17);
Console.WriteLine("벽에 부딪힘!");
break;
}
// 뱀의 상태를 출력.
Console.SetCursorPosition(0, 15);
Console.WriteLine($"현재 길이: {snake.length}\t먹은 음식의 수: {snake.food}");
// 게임 속도 조절.
Thread.Sleep(100);
}
#food 생성
food를 최대한 많이 모으는게 이 게임의 목표다. 생성 로직은 foodCreator클래스의 CreateFood 메서드를 통해 할 것이므로 생략하고, Main에서는 어떤 작용을 하는지 설명한다.
/*음식 생성*/
if (foodDelay++ % 10 == 0) //음식생성속도.
{
foodCreator.CreateFood();
}
foodDelay라는 변수가 존재하는 것을 확인할 수 있는데, 이 변수는 코드 시작 시 0으로 선언되고 while문을 한 번 루프할 때마다 이 if문 안에서 1씩 늘어난다. while문이 10번 루프하면 음식이 1번 생성되는 로직으로, if문 안의 % 다음에 오는 10을 늘리면 음식 생성 간격도 함께 늘어나고 줄이면 생성 간격도 함께 줄어들게 된다.
#방향키를 이용한 뱀의 방향 변경
키 입력을 받아 뱀이 다음에 어느 방향으로 갈지를 정해준다.
// 방향키 입력 시 방향 변경.
if (Console.KeyAvailable) //<1>
{
ConsoleKeyInfo arrowDirection = Console.ReadKey(); //<2>
switch (arrowDirection.Key)
{
case ConsoleKey.UpArrow:
snake.direction = Direction.UP; //<3>
break;
case ConsoleKey.DownArrow:
snake.direction = Direction.DOWN;
break;
case ConsoleKey.LeftArrow:
snake.direction = Direction.LEFT;
break;
case ConsoleKey.RightArrow:
snake.direction = Direction.RIGHT;
break;
default: //<4>
break;
}
}
<1>: Console.KeyAvailable은 키 입력이 있는지를 확인하고, 그 결과에 따라 bool형이 반환을 해준다. 만약 키 입력이 있다면 true가 되어 if문 안으로 진입할 수 있다.
<2>: 이제 <1>에서 대체 어떤 키를 입력받았는지 알아야 한다. ConsoleKeyInfo 자료형은 우리가 키보드로 입력한 정보를 저장한다. 이 코드에서는 Console.ReadKey()를 통해 키보드 입력을 받아 arrowDirection에 저장한다.
<3>: <2>에서 받은 입력은 CosoleKey.[입력한 키보드에 맞는 키값]를 통해 어떤 입력을 받은건지 확인할 수 있다. switch문을 통해 방향키가 상, 하, 좌, 우 면 snake의 방향 역시 일치하게 바꿔준다.
이때, Direction.UP 이라는 것을 볼 수 있는데 이건 이 글의 마지막에 따로 설명되어 있다.
<4>: 예외처리를 해준다. 만약 방향키가 아닌 다른 키 입력이 있다면, 아무런 상호작용 없이 무시해버린다.
#방향키를 반영한 뱀의 이동
내가 만든 스네이크 게임에서는 뱀이 매순간 자동으로 움직이고 있다. 내가 할 수 있는건 적절한 방향전환 뿐. 따라서 뱀의 이동은 코드 안에서 구현되어야 한다.
/*뱀의 이동*/
headPos.Draw(); //새로운 뱀머리 콘솔창에 출력.
snake.body.Enqueue(new Point(headPos)); //뱀의 queue +1
snake.Erase(); //지나간 흔적 삭제.
/*다음 뱀 머리 위치(headPos)*/
switch (snake.direction) //Direction에 따라 x, y, 값의 위치를 1씩 변경.
{ //<1>
case Direction.LEFT:
headPos.x -= 1;
break;
case Direction.RIGHT:
headPos.x += 1;
break;
case Direction.UP:
headPos.y -= 1;
break;
case Direction.DOWN:
headPos.y += 1;
break;
default:
break;
}
이 부분은 거의 모든 것이 클래스 내부의 상호작용으로 이루어져 있다. 따라서 맨 위의 3줄은 각 클래스에 상세히 어떤 작용을 하는지 설명이 되어 있으므로, 스크롤을 내리거나 맨 위의 목차 링크를 통해 보고 오길 바란다.
<1>: 스위치 문을 통해 위에서 방향키 입력으로 바꾼 Snake 클래스의 Diection에 따라 실제 headPos의 위치도 바꿔준다.
#뱀의 충돌처리
뱀이 충돌할 수 있는 것에는 3종류가 있다: 맵, 자기 자신, 그리고 food. 각각에 대한 충돌여부는 if문을 통해 감지한다.
/*뱀의 충돌처리(음식인지 자기자신인지 벽인지.)*/
if (headPos.x > 0 && headPos. x < 49) //x축 범위 확인. //<1>
{
if (headPos.y > 0 && headPos.y < 14) //y축 범위 확인. //<2>
{
if (snake.SnakeConflict(headPos)) //스스로의 몸통과 충돌 확인. //<3>
{
Console.SetCursorPosition(0, 17); //커서 위치 변경.
Console.WriteLine("스스로에 부딪힘!"); //게임오버 사유 출력.
break; //while문 탈출.
}
else if (foodCreator.FoodConflict(headPos)) //food와 충돌 확인. //<4>
{
snake.length++; //뱀의 길이 추가.
snake.food++; //뱀이 먹은 음식의 수(==점수) 추가.
} //<5>
}
else //벽(위, 아래)
{
Console.SetCursorPosition(0, 17); //커서 위치 변경.
Console.WriteLine("벽에 부딪힘!"); //게임오버 사유 출력.
break; //while문 탈출.
}
}
else //벽(좌, 우)
{
Console.SetCursorPosition(0, 17); //커서 위치 변경.
Console.WriteLine("벽에 부딪힘!"); //게임오버 사유 출력.
break; //while문 탈출.
}
충돌 감지 로직이 길지만, 다음 순서에 따라 차근차근 따라가면 생각보다 쉽게 이해할 수 있다.
<1>: 첫 if문이다. 뱀의 x축 범위에 따라 왼쪽 혹은 오른쪽의 벽과 충돌했는지를 알 수 있다. 만약 범위를 벗어났을 경우, 맨 밑의 else문을 통해 while문을 탈출한다.
<2>: 두 번째 if문이다. 뱀의 y축 범위에 따라 위쪽 혹은 아래쪽 벽과 충돌했는지를 알 수 있다. 만약 범위를 벗어났을 경우, 맨 밑에서 하나 위의 else문을 통해 while문을 탈출한다.
<3>: 마지막 if문이다. 뱀이 자기 자신과 충돌했는지를 알리는 Snake 클래스의 멤버함수를 통해 충돌여부를 알 수 있다. 바로 밑의 if 문 안에서 while문 탈출이 이루어진다.
<4>: 모든 게임오버 상황이 아닌 경우 food와의 충돌여부를 검사한다. 만약 확인된다면 뱀의 길이를 1 늘리고 밑의 현재 게임 진행현황을 보여주기 위해 만든 snake.food 변수의 크기도 1 늘려준다.
<5>: 마지막 if문의 else문은 그 무엇과도 충돌하지 않았다는 뜻이고, 또 그 경우에는 어떤 이벤트도 일어나지 않기에 생략되었다.
이때, while문을 탈출하는 순간 프로그램 역시 끝나게 된다.
#게임현황판 & 인게임 시간흐름 조정.
위에서 while문의 가장 마지막에 실행될 코드이다.
/*게임 현황 출력.*/
Console.SetCursorPosition(0, 15);
Console.WriteLine($"현재 길이: {snake.length}\t먹은 음식의 수: {snake.food}");
/*게임 속도 조절.*/
Thread.Sleep(100); //<1>
<1>: Thread.Sleep을 통해 즉시 while문이 다시 실행되는 것을 지연시킨다. Sleep()의 안에 들어가는 숫자는 1/1000초로, 100이 들어가면 0.1초가 된다. 이것을 통해 게임의 프레임을 조정하는 것과 같은 효과를 만들 수 있다.
Sub: Point, Snake, FoodCreator class
이 게임에서 쓰인 각 클래스에 대한 설명이다.
- Point클래스: 하나의 점에 관한 속성을 담은 객체.
- Snake클래스: 뱀을 이루는 점들의 집합(Queue) 객체.
- FoodCreator클래스: 맵 곳곳에 랜덤으로 생성될 food들의 집합(List) 객체.
Point Class
포인트 클래스는 단 하나의 점에 대한 정보와 속성을 담고 있다. 이 점은 사용하기에 따라 뱀의 일부가 될 수도 있고, 맵에 흩뿌려질 여러 food 중 하나가 될 수도 있다. 이 글에서 생략되었지만, 내 게임의 레플리카를 만들고 싶다면 꼭 아래의 코드들을 합칠 때 public class Point {} 로 감싸야 한다는 사실을 잊지 마시라.
#프로퍼티와 생성자
Point 클래스는 3개의 Property와 3개의 Constructor(생성자)으로 이루어져 있다.
/* Point 클래스 Property*/ //<1>
public int x { get; set; } //점의 x좌표.
public int y { get; set; } //점의 y좌표.
public char sym { get; set; } //점의 모양.
/* Point 클래스 생성자*/
public Point() { } //생성자1 //<2>
public Point(int _x, int _y, char _sym) //생성자2 //<3>
{
x = _x;
y = _y;
sym = _sym;
}
public Point(Point other) //생성자3 //<4>
{
this.x = other.x;
this.y = other.y;
this.sym = other.sym;
}
<1>: 3개의 property에 관한 설명은 주석만 봐도 충분히 이해할 수 있을거라 생각하고 자세한 설명은 생략한다. Property란 멤버변수를 함수와 유사한 형태로 정의함으로써 클래스의 멤버변수에 간접적으로 접근하도록 하는 것이다. 더 자세한 설명을 원한다면 3주차 강의를 보시라.
<2>: 첫 번째 생성자는 아무런 인자를 받지도 않고, 아무런 작용도 하지 않는다. 그런데 왜 필요할까? FoodCreator에서 List에 새로 만든 Food를 저장할 때 Point를 우선 만들고, 다시 이 Point를 List에 저장해야 하기 때문이다. 그러려면 Point 인스턴스 생성이 필요한데, x값도, y값도, _sym값도 다 FoodCreator에서 저장해줄 예정이기 때문에 굳이 Point 생성자에서 초기화 작업을 할 필요가 없지만 인스턴스 자체만은 필요하다.
쉽게 말하면, 이 깡통 생성자는 붕어빵 틀 같은 역할을 해주고 그 속은 다른 곳에서 채울 예정이라 할 수 있다.
<3>: 두 번째 생성자는 인스턴스 생성 시 x, y, sym을 입력받아 property에 할당해주는 생성자이다. 가장 기본적인 형태로, Main에서 snake의 머리가 되어줄 headPos를 생성할 때 사용되었다.
<4>: 이 친구가 마무리에서 설명할 나를 곤란하게 했던 버그를 해결하는 친구이다. Point의 깊은 복사를 도와준다. 총 3번 쓰였다:
- snake에 최초에 Point 객체를 전달할 때
- while문마다 생기는 snake의 새로운 머리를 EnQueue를 통해 전달할 때
- 새로운 food를 FoodCreator 클래스의 List에 추가할 때
데이터를 전달하는 것 자체는 <3>과 다를게 없지만, 인스턴스 생성 시 Point를 입력받는다는 점이 다르다.
#public void Draw()
점을 Console창에 그려주는 메서드이다. 아주 간단한 메서드이므로 주석만으로 이해가 가능할 것이다.
// 점을 그리는 메서드
public void Draw()
{
Console.SetCursorPosition(x, y); //점을 그릴 위치 선정. (커서의 위치 변경.)
Console.Write(sym); //포인트의 모양 출력.
}
#public void Clear()
뱀의 꼬리를 지울때만 활용되는 지우개 함수이다.(food는 뱀에 닿으면서 자동으로 대체되기 때문: ' $ ' → ' * ')
하지만, 바로 Main함수에서 접근하는 것은 아니고 Snake Class에 있는 Erase함수를 한 번 거치기 때문에 Main함수에서는 Clear을 찾을 수 없다.
// 점을 지우는 메서드
public void Clear()
{
sym = ' '; //sym을 ' '로 바꿔줌으로써 지우는 것과 같은 효과.
Draw(); //바로 위의 Draw() 함수 호출.
}
#public bool IsHit()
다른 Point를 인자로 받아와 현재 객체와 위치가 같은지 여부를 검사한다. 검사 방식은 reutrn문과 같다.
// 두 점이 같은지 비교하는 메서드
public bool IsHit(Point p)
{
return p.x == x && p.y == y; //x와 y가 같을 때만 true 반환.
}
Snake Class
Snake 클래스는 Main함수에서 단 한 번의 인스턴스 생성이 이루어지고, 그 후 FoodCreator에서 Snake가 있는 위치에 food가 랜덤 생성되는 것을 방지하기 위해 한 번의 참조가 이루어졌다. 인게임에서 사용자가 조작할 Snake에 대한 속성들을 담고 있다.
#프로퍼티와 생성자
Snake 클래스에는 5개의 프로퍼티와 1개의 생성자가 있다.
public int length { get; set; } //뱀의 길이
public Direction direction { get; set; } //뱀이 나아가는 방향.
public int food { get; set; } //뱀이 먹은 먹이의 수
public Queue<Point> body { get; set; } //뱀의 몸통을 이루는 각 점의 위치. //<1>
Point position { get; set; } //뱀의 머리 위치 갱신.
/*생성자*/
public Snake(Point snakePos, int snakeLen, Direction snakeDir) //<2>
{
position = snakePos;
length = snakeLen;
direction = snakeDir;
food = 0;
body = new Queue<Point>(); //<3>
body.Enqueue(position); //뱀 머리 Enqueue.
}
<1>: Queue란, 선입선출(FIFO) 구조를 가진 자료 구조이다. 굳이 Queue를 사용하는 이유는 뱀의 꼬리를 자를 때 유용할 거라 생각했기 때문으로, Main에서 EnQueue를 통해 HeadPos가 추가되어 이 Queue에 들어간 Point 객체가 length를 넘어선다면 Dequeue를 통해 가장 먼저 입력되었지만 현재는 꼬리가 되어있을 Point객체를 Queue에서 방출해버린다.
참고:
- Enqueue: 큐에 데이터 삽입.
- Dequeue: 큐에 가장 먼저 삽입된 데이터를 반환한 후 큐에서 삭제.
<2>: Snake의 생성자에는 3개의 인자가 있다.
- Point snakePos
- Main에서 HeadPos를 복사해온다. 이때, Main에서는 new Point(HeadPos)의 방식으로 복사해오기 때문에 깊은 복사를 할 수 있게 된다. 이렇게 받아온 Point 객체는 최초의 머리가 되어 Enqueue를 통해 Queue에 삽입된다.
- int snakeLen
- 뱀의 최초 길이다. Main에서 이 부분을 수정하면 food를 1개도 먹지 않았을 때의 뱀의 길이도 수정된다.
- Direction snakeDir
- 뱀이 향하는 방향이다.
<3>: Queue 인스턴스를 생성해준다.
#public void Erase()
Point 클래스의 Clear 메서드가 바로 여기에서 사용된다. 이 메서드는 Queue로 선언된 body의 Count가 length보다 길어졌을 때, Dequeue를 통해 body의 Count를 length와 같도록 유지하고 이때 Dequeue된 점의 위치에 있는 ' * '( = 뱀의 꼬리)을 Clear 함수를 통해 지워준다.
/*뱀 길이 유지*/
public void Erase()
{
if (body.Count > length) //큐의 길이가 length보다 길 때
{
Point p = body.Dequeue(); //꼬리를 큐에서 방출.
p.Clear(); //꼬리를 콘솔화면에서 지움.
}
}
#public bool SnakeConflict(Point headP)
인자로 입력된 Point 객체에 대해 Snake 객체의 몸과 겹치는 부분이 있는지 검사하고, 만약 있다면 true, 없으면 false값을 반환하는 함수이다.
public bool SnakeConflict(Point point) //<1>
{
foreach(Point bodyPart in body) //큐의 길이만큼 반복. //<2>
{
if (bodyPart.IsHit(point))
{
return true; //겹치는 즉시 true 반환.
}
}
return false; //큐 전체를 순환해도 겹치는 부분이 없을 때.
}
<1>: point로 입력받을 객체에는 2가지 경우의 수가 있다.
- Main의 뱀 머리: 이때 true를 반환하면 뱀이 스스로의 몸통과 부딪혔다고 판단해 게임이 종료된다.
- FoodCreator의 food: 이때 true를 반환하면 뱀이 먹이를 먹은 것으로 간주한다.
<2>: bodyPart는 body Queue에 존재하는 각 Point 객체이다.
FoodCreator class
뱀이 돌아다니면서 먹을 food를 생산해주고 먹은 food는 메모리에서도 지워주는 역할을 한다. 역시나 Main에서 딱 한 번의 instance 생성이 된다.
#프로퍼티와 생성자
1개의 멤버변수, 4개의 프로퍼티, 1개의 생성자가 있다.
Snake refSnake; //참조로 받아올 snake 객체. 수정 금지.
Point food { get; set; } //새롭게 생성된 food 객체.
List<Point> foodList { get; set; } //현재 맵에 존재하는 food 객체 리스트. //<1>
int posX { get; set; } //랜덤으로 생성할 food의 x좌표.
int posY { get; set; } //랜덤으로 생성할 food의 y좌표.
public FoodCreator(Snake snake, char c = '$')
{
food = new Point(); //인스턴스 생성.
foodList = new List<Point>(); //인스턴스 생성.
food.sym = c; //밥의 모양 결정.
refSnake = snake; //Main의 뱀을 참조로 받아와 저장.(다른 멤버함수에서 쓰기 위함.)
}
<1>: Snake와 달리 이 클래스에서는 food 객체들을 순서대로 먹는다는 보장이 없기 때문에 index를 통해 정확한 객체를 삭제하기 편한 List를 사용하게 되었다. Array와의 차이점은 List는 동적으로 선언되기 때문에 만약 1 - 2- 3 순서가 있을 때 2를 삭제한다면 자동으로 1 - 3으로 넘어간다는 점이다.
#public void CreateFood()
Main에서 콘솔창에 food을 출력할 때 사용하는 메서드이다. 아래에 쓰인 FoodLocate()에 관한 상세 내용은 후에 설명할 예정이니 생략한다.
/*food를 생성하는 메서드*/
public void CreateFood()
{
FoodLocate(); //food의 위치를 랜덤으로 설정&할당.
foodList.Add(new Point(food)); //리스트에 새로운 food 객체 추가.
food.Draw(); //콘솔창에 food 출력.
}
#public bool FoodConflict(Point headP)
뱀과 음식의 충돌여부를 감지하는 메서드이다.
/*뱀과 음식의 conflict여부를 감지하는 메서드*/
public bool FoodConflict(Point headP) //<1>
{
for (int i = 0; i < foodList.Count; i++) //맵에 생성된 food의 개수 동안.
{
Point foodP = foodList[i]; //가독성을 위함.
if (foodP.IsHit(headP)) //충돌감지.
{
foodList.RemoveAt(i); //충돌한 food를 리스트에서 삭제.
return true; //부딪혔다고 반환.
}
}
return false; //어떤 food도 뱀의 머리와 같은 위치가 아님.
}
<1>: Point로 현재 뱀의 머리 위치를 받아온다.
#private void FoodLocate()
이번 코드에서 쓰인 유일한 private 메서드이다. food가 생성될 위치를 랜덤으로 정해주는 함수이다.
/*food 위치를 맵에서 뱀과 겹치지 않게 랜덤으로 생성해주는 함수*/
private void FoodLocate()
{
do //<1>
{
posX = new Random().Next(1, 48); //맵 안의 랜덤 x 위치.
food.x = posX; //할당
posY = new Random().Next(1, 13); //맵 안의 랜덤 y 위치.
food.y = posY; //할당
} while (refSnake.SnakeConflict(food)); //단, 뱀과 겹치지 않음.
}
<1>: food의 (x, y)값은 Point 클래스의 빈 생성자에서 설명했던 대로 이곳에서 초기화된다. 최초 1번은 할당할 필요가 있어서 do while문을 사용했다.
그외 : public enum Direction
public enum Direction
{
LEFT,
RIGHT,
UP,
DOWN
}
이 코드는 열거형(enum)이라는 것이다. 관련된 집합을 단일 데이터 타입으로 그룹화할 때 사용된다. 일단은 내가 원하는대로 "새로운 데이터 타입"을 만들 수 있는 기능이라고 이해하면 된다.
내 코드에서는 어떤 class에도 소속되어 있지 않고 같은 namespace만 공유하도록 되어있기 때문에 따로 빼서 정리한다.
마무리
어제 저녁부터 오늘 오전시간의 일부까지 사용해 완성시킨 게임이다. C++와 C#의 차이점 때문에 물을 많이 먹었다. 우선, 이번 앱을 만들면서 버그를 수정하는 데에 약간 시간이 걸렸던 것은 다음과 같다:
- 얕은 복사
- 코드 서순 문제
우선, 함수를 통해 Point 객체를 옮기려고 시도했는데, 내 기억에 C++에서는 함수안에 Point 객체를 그대로 옮겨도 깊은 복사를 통해 복사가 잘 이루어졌다. 하지만, C#은 [함수이름](Point p)의 형태로 넘겼을 때 얕은 복사를 한 상태로 넘겨서 함수안에서 Point의 값을 바꾸면 원본인 p의 값도 함께 변해버리는 문제가 발생했다. 이는 [함수이름](new Point(p))의 형식으로 넘기는 것으로 해결했다. 이 방식을 쓸 때 Point(Point p) 라는 생성자도 새로 만들어주어야 하니 주의. 문제의 발견은 디버깅을 통해 FoodCreator 클래스 안의 List<Point>의 모든 포인트 위치가 동일하며, 새 food가 생기면 모든 포인트의 위치값 역시 변해버리는 것으로 했다.
두 번째 문제는 단순한데도 약간 헤맸던 부분으로, 입력 후에 Console.Clear을 해버리는 바람에 food가 출력되지 않던 현상이다. Clear의 위치를 옮기는 것 만으로 간단히 해결될 문제이지만, 찾는 데에 꽤나 시간을 소모했다. food의 모양을 다른 것으로 잠깐 바꾸니 아주 짧은 프레임동안 food가 생겼다가 없어지는 것을 확인해서 이를 통해 오류를 수정할 수 있었다.
끝으로, 이 게임에 약간의 재미를 더하려면 먹이를 1부터 1씩 증가하도록 생성해서 순서대로 먹지 않으면 게임오버가 되어버리게 하는 것도 재미있을 것이라는 생각이 들었다. 개인 프로젝트도 함께 진행해야 해서 시간관계상 여기에 아이디어만 살짝 남기고 간다.
+원본 코드는 여기처럼 일일이 주석을 달아가며 쓰지는 않았으며, TIL 쓰는 와중에 개선사항이 보여서 일부는 수정하고 일부는 수정하지 않은 부분이 있을 수 있음.
'내일배움캠프 > C#문법종합반' 카테고리의 다른 글
[TIL 24.05.05~06] 5주차 강의-알고리즘 기초, 정렬 알고리즘 (0) | 2024.05.05 |
---|---|
[TIL 24.04.29~05.05] 4주차 강의 필기노트 (0) | 2024.04.29 |
[TIL 24.04.28] 3주차 강의 필기노트 (0) | 2024.04.28 |
[TIL 24.04.23] 2주차 과제2: Tic Tac Toe (0) | 2024.04.23 |
[TIL 24.04.22] 1주차~2주차 강의 필기노트 (0) | 2024.04.22 |