C# 문법 종합반 강의 1, 2, 3주차를 들었다.
각 주차마다 Console에서 실행되는 게임을 만드는 과제가 있는데, 2주차 과제까진 할만 했는데 3주차 과제부터는 조금 어려웠다.
3주차 과제는 지렁이 게임과 블랙잭을 구현해야했는데, 구현 과정에서 어려웠던 점과 어떻게 해결했는지를 정리해보려한다.
키보드로 지렁이가 진행하는 방향을 바꿔가며 먹이를 먹고 지렁이를 키워나가는 유명한 게임이다.
정답 코드도 같이 동봉되어 있었는데, 이거를 보고 따라 치는 것 보다는, 코드를 보지 않고 먼저 빌드를 돌려서 게임이 어떻게 돌아가는지 확인을 해봤다. 그 결과 알게된 사항은 다음과 같다.
과제에서 정답 코드 말고도 기본으로 제공해주는 코드가 있었다. 이 코드에 살을 붙여서 구현된 결과물을 제출하는 방식. 제공된 코드의 기능은 다음과 같았다.
Point
클래스Point
끼리 접촉했는지 검사하는 메서드Point
를 그리거나 지워주는 메서드while
문과 Thread
를 이용한 게임루프위의 상황에서 과제를 완수하기 위해 내가 구현해야 하는 기능을 정리해보면 다음과 같다.
FoodCreator
클래스Snake
클래스Point
클래스를 이용해야한다.가장 먼저 Snake
클래스를 건드렸다. 일단 지렁이가 움직여야 테스트가 가능하기 때문이다.
public class Snake
{
private List<Point> body;
public int Length
{
get
{
return body.Count;
}
}
public Point Head
{
get
{
return body.Last();
}
}
public Point Tail
{
get
{
return body.First();
}
}
public Direction dir;
...
지렁이의 몸은 Point
들로 이루어져있다. 따라서 List<Point>
변수를 하나 만들어줬다. 그리고 지렁이의 길이와 머리, 꼬리를 받아오기 위한 프로퍼티들도 만들어줬다. 이제 지렁이의 움직임을 구현할 Move()
메서드를 작성하면 된다. 여기서 문제가 생겼다.
지렁이가 움직일 때, 모든 Point
들을 다같이 이동시키려 하면 처리할 양이 많아지고, 몸이 꺾이는 부분에서의 처리가 어려워진다. 따라서 나는 다음과 같은 방법으로 구현하려고 했다.
지렁이의 꼬리를 떼어내서, 머리에 붙인다.
이 방법으로 구현하면, 몸이 꺾이는 부분에서의 처리도 생각하지 않아도 되고, 지렁이의 길이가 몇이던간에 처리할 양도 변하지 않는다.
따라서, 다음과 같이 Snake.Move()
메서드를 작성했다.
public void Move()
{
Tail.Clear();
var newHead = Tail;
body.RemoveAt(0);
switch (dir)
{
case Direction.DOWN:
newHead.y = Head.y++;
break;
case Direction.UP:
newHead.y = Head.y--;
break;
case Direction.LEFT:
newHead.x = Head.x -= 2;
break;
case Direction.RIGHT:
newHead.x = Head.x += 2;
break;
}
body.Add(newHead);
Head.Draw();
}
Point.Clear()
는 화면상에서 현재 Point
를 지워주는 메서드이다. 이 메서드를 이용해 꼬리를 화면에서 지워주고, body
에서 꺼낸다음, 진행방향을 계산한 좌표로 바꿔서 새로운 머리로 넣어주고, 화면에 그려준다.
실행해보니 이동은 하지않고 꼬리부터 하나씩 사라졌다! 꼬리를 떼서 머리에 붙이는 것에는 문제가 없는 것 같은데 말이다.
문제의 원인은 Point.Clear()
메서드에 있었다.
public void Clear()
{
sym = ' ';
Draw();
}
sym
은 이 Point
가 어떤 문자로 그려질지를 저장한 변수이다. 그렇다. 이 메서드는 sym
을 공백으로 바꾸고, 화면에 공백을 그려내는 것으로 화면에서 Point
를 그려내고 있었다. 이 메서드를 호출하고 나면 이 Point
의 sym
에는 공백이 들어가 있는 것이다!!
나는 꼬리를 화면에서 지우는 과정에서 이 메서드를 불러놓고 다시 머리로 넣어줄 때 sym
값을 다시 바꿔주지 않았으니 결과적으로 꼬리가 하나씩 공백으로 그려지면서 결국 투명지렁이가 되는 것이었다.
C#에서 class는 reference type인걸 망각한 실수인 셈.
문제의 원인을 찾았으니 이제 해결해야한다. 다음과 같은 방법으로 해결하기로 했다.
Point.Clear()
)sym
이 공백값이 아닌 새로운 Point
) public void Move()
{
Tail.Clear();
body.Remove(Tail);
AddBody();
Head.Draw();
}
알맞는 진행방향에 새로운 Point
를 머리로 넣어주는 Addbody()
를 이용했다.
public void AddBody()
{
Point p = new Point(Head.x, Head.y, '*');
switch (dir)
{
case Direction.DOWN:
p.y++;
break;
case Direction.UP:
p.y--;
break;
case Direction.LEFT:
p.x -= 2;
break;
case Direction.RIGHT:
p.x += 2;
break;
body.Add(p);
}
C#에선 class
가 Reference Type
인걸 망각한 실수라고 했는데, Reference Type
과 Value Type
을 다시 정리해보자.
int
, float
, long
, double
, bool
, char
등 일반적인 자료형struct
, enum
등으로 정의한 자료형Array
string
, List
, int[]
등과 같은 배열 자체Array
그 자체를 말함Value Type
일수도, Reference Type
일수도 있음class
로 정의한 자료형Value Type은 a = b
와 같은 대입을 실행할 떄, 값의 복사가 일어난다.
메모리 상에 서로 다른 a
의 주소에 b
의 주소에 있는 값을 복사해서 할당하는 방식이다.
따라서, 대입 이후에 a
의 값을 바꾸거나 해도, b
의 값이 바뀌거나 하는 일은 없다.
Reference Type은 a = b
와 같은 대입을 실행할 떄, a
의 값에는 b
의 인스턴스의 주소값을 할당한다.
따라서, 대입 이후에 a
의 멤버변수를 조작하거나 한다면, a
가 가리키는 인스턴스의 멤버변수를 조작하는 것이므로, 같은 인스턴스를 가리키는 b
의 인스턴스 값도 따라서 바뀐다!
C++의 포인터를 생각하면 되겠다. 대신에 C#의 Reference Type은 포인터와 달리 *나 &와 같이 어려운 개념을 알아서 해주는 것이라고 이해하면 될 것 같다.