1. 뱀 게임 만들기
최근 진행하고 있는 부트캠프에서 과제로 뱀 게임이 주어졌습니다.
오늘은 뱀 게임을 만들면서 느꼈던 고민들과 구현 내용을 포스팅 하겠습니다.
2. 뱀 게임의 요구사항
1. 뱀은 매 턴마다 자신의 앞으로 이동합니다.
2. 사용자는 방향키를 이용하여 뱀의 이동 방향을 제어할 수 있습니다.
3. 뱀은 맵에 무작위로 생성되는 음식을 먹을 수 있습니다.
4. 뱀이 음식을 먹으면 점수가 올라가고, 길이가 늘어납니다.
5. 뱀이 벽이나 자신의 몸에 부딪히면 게임이 끝나고 'Game Over' 메세지를 출력합니다.
요구사항들을 확인하며 가장 많이 고민 했던 부분들은 아래와 같습니다.
1. 콘솔 환경에서 사용자의 키 입력 받기.
- 콘솔 환경에서 Console.Wirte로 문자열 입력은 받아 봤지만, 방향키 입력을 받아본 적은 없었기 때문에
이 부분에 대해 고민이 있었습니다.
2. 뱀의 이동 구현
-뱀의 길이는 음식을 먹지 않는 이상 일정하기 때문에 어떻게 하면 프레임 마다 일정한 크기로 이동할 수 있을까라는
고민이 있었습니다.
3. 음식을 먹으면 뱀의 길이 늘리기.
-이동과 연계되는 부분인데 뱀의 길이가 늘어났음을 기억하고 이 부분을 뱀의 이동에도 적용해야하기 때문에
이부분에 대한 고민이 있었습니다.
3. 뱀 게임 구현
뱀 게임을 만들면서 구현한 핵심적인 부분만 설명하겠습니다.
- 사용자의 키 입력 받기
Console.keyAvailable을 통해 사용자의 Key입력이 있을 경우에만 방향 전환을 하도록 구현하였습니다.
C#의 구조체인 ConsoleKeyInfo로 inputKey를 만들어 콘솔창에서 방향키의 입력 값을 저장했습니다.
이후, Switch, case로 inputKey의 값에 따라 뱀의 방향을 바꿔주었습니다.
- 뱀의 이동구현
뱀의 이동구현을 만들때 떠오른 방법은 List였습니다.
콘솔 환경속에 존재하는 뱀도 결국엔 좌표의 집합체라고 생각했습니다
콘솔 좌표상으로 y축이 5이고, x축 0부터 시작하는
길이가 4인 뱀은 다르게 표현하면 (0,5),(1,5),(2,5),(3,5) 라는 좌표들의 집합체라고 생각했습니다.
따라서 (0,5)는 뱀의 꼬리, (3,5)는 뱀의 머리, 남은 (1,5), (2,5)는 뱀의 몸통이 될 수 있습니다.
그래서 (0,1)과 같은 2D공간의 좌표를 나타내는 클래스 Pos를 만들었습니다.
이후 이 좌표들의 배열 형태로 저장하기 위해 Snake 클래스에 List<Pos>를 만들어 뱀의 현재 좌표 위치를 저장했습니다.
snakePos는 뱀을 이루고 있는 좌표들의 집합체로 뱀 그 자체라고 볼 수 있습니다.
이후 Draw함수를 만들어 뱀을 그렸습니다.
뱀이 오른쪽을 보고 있다면 뱀의 꼬리 부분이 될 수 있는 snakePost[0]번을 제거하고
x축 방향으로 1칸 앞에 머리를 새로 그리고 뱀을 이루는 snakePos안에 저장했습니다.
이렇게 하면 예를 들어 (1,0), (2,0), (3,0), (4,0)으로 이뤄진 뱀은 프레임마다
꼬리부분이 제거되며 = (2,0), (3,0), (4,0)
새로운 머리가 생깁니다 = (2,0), (3,0), (4,0), (5,0)
List는 앞에 있던 데이터가 제거되면 한칸씩 앞으로 밀리는 특성을 지니고 있기 때문에
(1,0), (2,0), (3,0), (4,0)으로 이루어진 뱀에서 (1,0) 뒤에 존재하던 (2,0)이 새로운 꼬리가 됩니다.
이후에도 이 로직이 반복되기 때문에 프레임이 지나면 뱀은 일정한 길이로 이동하는 것 처럼 보이게 됩니다.
뱀이 음식을 먹어 길이가 늘어나는 경우에도 상황은 같습니다.
(1,0), (2,0), (3,0), (4,0)으로 이루어진 길이가 4인 뱀에서 꼬리와 같은 위치값에 꼬리를 하나 더 넣어줍니다.
(1,0), (1,0), (2,0), (3,0), (4,0) 이후 꼬리는 잘려나가고 새로운 머리가 생깁니다.
(1,0), (2,0), (3,0), (4,0), (5,0) 이렇게 되면 길이가 5인 뱀을 만들 수 있습니다.
뱀이 몸통과 꼬리에 부딪혔을 때도 같은 원리로 구현했습니다.
뱀의 머리는 부딪히는 조건에 포함되면 안되기 때문에 반복문 조건에 snakePos.Count -1을 해줬습니다.
이후 몸통과 꼬리의 좌표가 머리의 좌표가 같다면, true 반환해 충돌을 감지했습니다.
뱀이 벽과 부딪혔을 때 또한 같은 원리로 검출 했습니다.
- 게임 매니저
게임의 점수와 맵, 난이도 등을 관리할 게임 매니저 클래스를 생성하여
안에 맵그리기, 게임 속도UP, 점수 획득을 할 수 있는 메서드를 만들어서 사용했습니다.
싱글톤패턴을 사용해 instance값에 접근만 하면 해당 기능들을 사용할 수 있게 구현했습니다.
4. 시연영상
중간에 왼쪽을 보고있는 상태에서 바로 오른키를 입력하여 죽어버렸습니다..
5. 사용 코드
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.SymbolStore;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Xml.Linq;
class Program
{
// 방향을 표현하는 열거형입니다.
public enum Direction
{
LEFT,
RIGHT,
UP,
DOWN
}
public class Snake
{
//뱀의 머리 위치
public Point p;
//뱀을 구성하는 좌표들
public List<Pos> snakePos = new List<Pos>();
//뱀의 꼬리 좌표
public Pos tailPos = new Pos();
//이전 꼬리 좌표
public Pos pastTailPos = new Pos();
//뱀의 길이
public int length = 0;
//뱀의 이동 방향
public Direction direction;
public Snake() { }
//생성자
public Snake(Point _p, int length, Direction direction)
{
this.p = _p;
this.length = length;
this.direction = direction;
}
//첫 뱀 그리기
public void FirstDraw()
{
for (int i = 0; i < length; i++)
{
p.Draw();
p.x--;
}
p.x += length;
//뱀의 좌표 저장(꼬리부터 머리까지)
snakePos.Add(new Pos(p.x - length + 1, p.y));
snakePos.Add(new Pos(p.x - length + 2, p.y));
snakePos.Add(new Pos(p.x - length + 3, p.y));
snakePos.Add(new Pos(p.x - length + 4, p.y));
}
//뱀 그리기
public void Draw()
{
switch (direction)
{
case Direction.RIGHT:
//커서 위치 정렬
Console.SetCursorPosition(p.x, p.y);
//꼬리 제거
pastTailPos = snakePos[0];
p.TailClear(snakePos[0], length);
snakePos.RemoveAt(0);
//머리 그리기
p.x++;
p.Draw();
//머리 좌표 저장
snakePos.Add(new Pos(p.x, p.y));
break;
case Direction.LEFT:
//커서 위치 정렬
Console.SetCursorPosition(p.x -1, p.y);
//꼬리 제거
pastTailPos = snakePos[0];
p.TailClear(snakePos[0], length);
snakePos.RemoveAt(0);
//머리 그리기
p.x--;
p.Draw();
//머리 좌표 저장
snakePos.Add(new Pos(p.x, p.y));
break;
case Direction.UP:
//커서 위치 정렬
Console.SetCursorPosition(p.x, p.y);
//꼬리 제거
pastTailPos = snakePos[0];
p.TailClear(snakePos[0], length);
snakePos.RemoveAt(0);
//머리 그리기
p.y--;
p.Draw();
//머리 좌표 저장
snakePos.Add(new Pos(p.x, p.y));
break;
case Direction.DOWN:
//커서 위치 정렬
Console.SetCursorPosition(p.x, p.y);
//꼬리 제거
pastTailPos = snakePos[0];
p.TailClear(snakePos[0], length);
snakePos.RemoveAt(0);
//머리 그리기
p.y++;
p.Draw();
//머리 좌표 저장
snakePos.Add(new Pos(p.x, p.y));
break;
}
}
//음식을 먹음
public void EatFood()
{
List<Pos> tempPos = new List<Pos>();
tempPos.Add(pastTailPos);
foreach (Pos pos in snakePos)
{
tempPos.Add(pos);
}
//순서 정렬
snakePos = tempPos;
}
//뱀의 머리가 몸통과 부딪혔는지
public bool IsHit()
{
for(int i = 0; i< snakePos.Count -1; i++)
{
//머리의 좌표가 몸통과 꼬리의 좌표랑 같다면
if (snakePos[i].x == p.x && snakePos[i].y == p.y)
{
return true;
}
}
return false;
}
}
//2D 좌표
public class Pos
{
public int x, y;
public Pos() { }
public Pos(int _x, int _y)
{
x = _x;
y = _y;
}
}
public class Point
{
public int x { get; set; }
public int y { get; set; }
public char sym { get; set; }
// Point 클래스 생성자
public Point(int _x, int _y, char _sym)
{
x = _x;
y = _y;
sym = _sym;
}
// 점을 그리는 메서드
public void Draw()
{
Console.SetCursorPosition(x, y);
Console.Write(sym);
}
// 점을 지우는 메서드
public void Clear()
{
sym = ' ';
Draw();
}
//꼬리 지우기
public void TailClear(Pos _pos, int _length)
{
char clearSym = ' ';
Console.SetCursorPosition(_pos.x, _pos.y);
Console.Write(clearSym);
Console.SetCursorPosition(_pos.x + _length + 1, _pos.y);
}
// 두 점이 같은지 비교하는 메서드
public bool IsHit(Point p)
{
return p.x == x && p.y == y;
}
//음식을 먹었는지 체크
public bool CheckEatFood(Snake _snake)
{
return x == _snake.p.x && y == _snake.p.y;
}
}
public class FoodCreator
{
//음식의 좌표
int x, y;
//음식의 모양
char sym;
public FoodCreator(char _sym)
{
sym = _sym;
}
//음식 생성
public Point CreateFood(int Max_x, int max_y, Snake _snakePos)
{
//음식의 위치와 뱀의 포지션이 같은지
bool isSame = false;
Random rand = new Random();
int x = rand.Next(1, Max_x);
int y = rand.Next(1, max_y);
//음식의 생성 위치가 현재 뱀의 위치와 같은지
for (int i = 0; i < _snakePos.length; i++)
{
//뱀의 포지션 값
Pos pos = _snakePos.snakePos[i];
//동일한 위치 생성시
if (x == pos.x && y == pos.y)
{
isSame = true;
}
}
//음식 생성
if(!isSame)
{
Console.SetCursorPosition(x, y);
Console.Write(sym);
Point foodPos = new Point(x, y, sym);
return foodPos;
}
else
{
//다시 그리기
CreateFood(Max_x , max_y , _snakePos);
Point foodPos = new Point(x, y, sym);
return foodPos;
}
}
}
//게임 관리자
public class GameManager
{
public static GameManager instance = new GameManager();
public int speed = 100;
int maxSpeed = 20;
public int score = 0;
public List<string> map = new List<string>
{
"■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■ ■",
"■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■",
};
//맵그리기
public void DrawMap()
{
for (int i = 0; i < map.Count; i++)
{
Console.WriteLine(map[i]);
}
}
//벽에 부딪혔는지
public bool IsHitWall(Snake _snake)
{
//x축 범위 제한
if(_snake.p.x <= 0 || _snake.p.x >= 59)
{
return true;
}
//y축 범위 제한
if(_snake.p.y <= 0 || _snake.p.y >= 14)
{
return true;
}
return false;
}
//점수 추가
public void AddScore()
{
score++;
PrintScore();
SpeedUp();
}
//게임 속도 UP
public void SpeedUp()
{
if(speed >= maxSpeed)
{
speed -= 5;
}
else
{
speed = maxSpeed;
}
}
//점수 표기하기
public void PrintScore()
{
Console.SetCursorPosition(25, 15);
Console.WriteLine("Score : {0}", score);
}
}
static void Main(string[] args)
{
//맵 그리기
GameManager.instance.DrawMap();
// 뱀의 초기 위치와 방향을 설정하고, 그립니다.
Point p = new Point(4, 5, '*');
Snake snake = new Snake(p, 4, Direction.RIGHT);
snake.FirstDraw();
// 음식의 위치를 무작위로 생성하고, 그립니다.
FoodCreator foodCreator = new FoodCreator('$');
Point food = foodCreator.CreateFood(59, 14, snake);
//점수 표시
GameManager.instance.PrintScore();
//커서 숨기기
Console.CursorVisible = false;
// 게임 루프: 이 루프는 게임이 끝날 때까지 계속 실행됩니다.
while (true)
{
//뱀 그리기
snake.Draw();
//뱀이 음식을 먹었는지
if (food.CheckEatFood(snake))
{
//먹었으면 꼬리 늘리기
snake.EatFood();
//점수 획득
GameManager.instance.AddScore();
//음식 생성
food = foodCreator.CreateFood(59, 14, snake);
}
//벽에 부딪혔는지
if(GameManager.instance.IsHitWall(snake))
{
//게임 오버
break;
}
//머리가 몸통과 부딪혔는지
if(snake.IsHit())
{
//게임 오버
break;
}
// 키 입력이 있는 경우 방향 전환
if (Console.KeyAvailable)
{
ConsoleKeyInfo inputKey = Console.ReadKey(true);
switch (inputKey.Key)
{
case ConsoleKey.LeftArrow:
snake.direction = Direction.LEFT;
break;
case ConsoleKey.RightArrow:
snake.direction = Direction.RIGHT;
break;
case ConsoleKey.UpArrow:
snake.direction = Direction.UP;
break;
case ConsoleKey.DownArrow:
snake.direction = Direction.DOWN;
break;
}
}
// 뱀이 이동하고, 음식을 먹었는지, 벽이나 자신의 몸에 부딪혔는지 등을 확인하고 처리하는 로직을 작성하세요.
// 이동, 음식 먹기, 충돌 처리 등의 로직을 완성하세요.
Thread.Sleep(GameManager.instance.speed); // 게임 속도 조절 (이 값을 변경하면 게임의 속도가 바뀝니다)
// 뱀의 상태를 출력합니다 (예: 현재 길이, 먹은 음식의 수 등)
}
Console.SetCursorPosition(25, 7);
Console.WriteLine("Game Over");
Console.ReadLine();
}
}
6. 후기
제가 구현한 뱀 게임 코드가 주석이 없으면 무엇을 하려고 하는지 알아 보기는 무리가 있는 것 같습니다.
이동 로직 또한 더 간결하고 쉽게 표현하는 방법이 있었을 것 같다는 생각이 드네요.
Class를 사용함에 있어 아직 숙련도도 많이 부족하다고 느껴지는 과제였습니다.
최근 들어 스스로 부족한 점이 많이 보이고 몰랐던 내용들을 많이 알게되는 한 주였던 것 같네요.
항상 겸손한 자세로 배우며 노력하겠습니다.
'C#' 카테고리의 다른 글
Unity Interface (0) | 2024.02.01 |
---|---|
C# BlackJack (1) | 2024.01.08 |
C# virtual, override (1) | 2024.01.05 |
C# Partial (2) | 2024.01.05 |
C# Static (2) | 2024.01.04 |