[C++ 기초] 2차원 배열, 콘솔게임

라멘커비·2023년 12월 22일
0

CPP 입문

목록 보기
9/25
post-thumbnail

2차원 배열

1차원 배열 방식으로도 초기화가 된다. 그렇다고 메모리에 2차원으로 들어가 있는 것은 아니다.

char Arr[3][3] = {'0','1','2','3','4','5','6','7','8'};

좀 더 명시적인 초기화 방법.

char Arr[3][4] = { {'0','1','2'},{'3','4','5'},{'6','7','8'}};
char Arr2[3][4] = { "123", "345", "678"};

각각의 자료형은 다음과 같다.

Arr       : char [3][4]
Arr[0]    : char [4]
Arr[0][0] : char
  • 문자열의 끝을 의미하는 0이 들어갈 자리가 없을 때
    char Arr[3][4] = { {'0','1','2', '2'},{'3','4','5'},{'6','7','8'}};
    printf_s(Arr[0]);

Arr[0][3]의 자리를 비워두지 않고 값을 넣었더니 Arr[1]까지 읽고 0을 만나 출력이 끝난 모습이다.

Arr[0][3], Arr[1][3], Arr[2][3](각 행의 끝자리)도 채우면 메모리 속에서 0을 만날 때까지 출력되어 의도하지 않은 값들이 출력된다.

  • 배열의 크기를 변수로 넣기

    • 변수를 넣을 수 없다. 값이 변할 수 있기 때문이다.
      exe파일에 적혀있다. (ex : main함수는 100바이트 사용한다.) 한 번 컴파일이 되고 나면 절대로 바뀔 수 없다.

    • const를 붙인 변수는 상수처럼 읽히기 때문에 가능하다.

메모리

char Arr[3][4] 배열의 주소를 보면 char형의 크기인 1바이트씩 연속으로 있는 것을 알 수 있다.

과제

  1. 플레이어 상하좌우 움직이기
  2. 맵의 테두리는 벽을 쳐서 못가게 만들기
  3. 총알을 만들어서 어떤 키를 누르면 어딘가에 총알이 생기게 만들기

코드

  • 내 코드
// ConsoleGame.cpp : 이 파일에는 'main' 함수가 포함됩니다. 거기서 프로그램 실행이 시작되고 종료됩니다.
//

#include <iostream>
#include <conio.h>

class int2
{
	// 보통 이런 수학적 클래스는
	// public으로 맴버변수를 두는 편입니다.
public:
	int X = 0;
	int Y = 0;
};

// 이런걸 정의하는것을 좋아하지는 않습니다.
// typedef int2 Pos;

// 전역변수는 영역과 관련없이
// 이름이 위에 있다면 그것을 사용할 수 있다.
const int ScreenX = 20;
const int ScreenY = 10;
const int ScreenXHalf = ScreenX / 2;
const int ScreenYHalf = ScreenY / 2;

class Bullet {
public:
	void SetBullet(const int2& _Pos) {
		Pos = _Pos;
	}
	int2 GetBulletPos() {
		return Pos;
	}
private:
	int2 Pos = { 0,0 };
};


class ConsoleScreen
{
public:
	// 생성자를 만든다는 것은
	// 내가 만든 생성자 형식으로만 생성해라.
	ConsoleScreen(char _BaseChar)
	{
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]

		BaseCh = _BaseChar;

		for (int y = 0; y < ScreenY; y++)
		{
			for (int x = 0; x < ScreenX - 1; x++)
			{
				Arr[y][x] = BaseCh;
			}
		}
	}

	void PrintScreen()
	{
		for (int y = 0; y < ScreenY; y++)
		{
			char* Ptr = Arr[y];
			printf_s(Ptr);
			printf_s("\n");
		}
	}

	void ClearScreen()
	{
		system("cls");
		for (int y = 0; y < ScreenY; y++)
		{
			for (int x = 0; x < ScreenX - 1; x++)
			{
				if ((y == 0 || y == ScreenY - 1) || (x == 0 || x == ScreenX - 2)) {
					Arr[y][x] = '#';
				}
				else {
					Arr[y][x] = BaseCh;
				}
			}
		}
	}

	void CreateBullet(const int2& _Bullet) {
		if (_Bullet.X != 0 && _Bullet.Y != 0) {
			Arr[_Bullet.Y - 1][_Bullet.X] = '|';
		}
	}

	void SetPixel(const int2& _Position, char _Char)
	{
		SetPixel(_Position.X, _Position.Y, _Char);
	}

	void SetPixel(int _X, int _Y, char _Char)
	{
		Arr[_Y][_X] = _Char;
	}

protected:

private:
	char Arr[ScreenY][ScreenX] = { 0, };
	char BaseCh = ' ';
	//int2 Bullet = { 0, 0 };

	//ScreenY가 5일 때
	//0 [0][0][0][0][0][0]
	//1 [0][0][0][0][0][0]
	//2 [0][0][0][0][0][0]
	//3 [0][0][0][0][0][0]
	//4 [0][0][0][0][0][0]
};

class Player
{
public:
	Player()
	{
	}

	Player(const int2& _StartPos, char _RenderChar)
		: Pos(_StartPos), RenderChar(_RenderChar)
	{
	}

	// inline은 컴파일러가 함수를 삭제하고
	// 그 위치에 치환시켜 버린다.
	inline int2 GetPos()
	{
		return Pos;
	}

	inline char GetRenderChar()
	{
		return RenderChar;
	}

	void ClearScreen()
	{
		system("cls");
	}


	void Update(char ch)
	{
		// getch의 리턴값을 확인해서
		// 상하좌우로 움직이게 하세요.
		
		//char ch = _getch();
		//ClearScreen();
		switch (ch) {
		case 'w':
		case 'W':
			if (Pos.Y - 1 > 0) {
				Pos.Y -= 1;
			}
			break;
		case 's':
		case 'S':
			if (Pos.Y + 1 < ScreenY - 1) {
				Pos.Y += 1;
			}
			break;
		case 'a':
		case 'A':
			if (Pos.X - 1 > 0) {
				Pos.X -= 1;
			}
			break;
		case 'd':
		case 'D':
			if (Pos.X + 1 < ScreenX - 2) { // X는 문자열의 끝에 0이 둘 자리가 있어야해서 하나 더 작음
				Pos.X += 1;
			}
			break;
		default:
			break;
		}
		// 입력값이 w면 위로 y - 1
		// 입력값이 s면 아래로 y + 1
		// a면 왼쪽으로 x - 1
		// d면 오른쪽으로 x + 1

	}

private:
	int2 Pos = { 0, 0 };
	char RenderChar = '@';
};

int main()
{
	ConsoleScreen NewScreen = ConsoleScreen('*');
	Player NewPlayer = Player({ ScreenXHalf, ScreenYHalf }, '@');
	Bullet NewBullet = Bullet();

	while (true)
	{
		NewScreen.ClearScreen();
		NewScreen.SetPixel(NewPlayer.GetPos(), NewPlayer.GetRenderChar());
		NewScreen.CreateBullet(NewBullet.GetBulletPos());
		NewScreen.PrintScreen();
;		char ch = _getch();
		if (ch == ';') {
			NewBullet.SetBullet(NewPlayer.GetPos());
		}
		else {
			NewPlayer.Update(ch);
		}
	}
}
  • 선생님 코드
    선생님은 벽을 ConsoleScreen 클래스에 포함시키지 않고 맵과 관련된 클래스를 추가 생성하심.
    총알이 생기는 방식을 IsFire라는 플래그를 두어서 조절(?)하심.
    모든 이동은 const int2로 연산자 오버로딩하심. (직관적)
// ConsoleGame.cpp : 이 파일에는 'main' 함수가 포함됩니다. 거기서 프로그램 실행이 시작되고 종료됩니다.
//

#include <iostream>
#include <conio.h>

class int2
{
	// 보통 이런 수학적 클래스는
	// public으로 맴버변수를 두는 편입니다.
public:
	int X = 0;
	int Y = 0;

	// 디폴트 대입연산자.
	void operator=(const int2& _Other) 
	{
		X = _Other.X;
		Y = _Other.Y;
	}

	int2 operator+(const int2& _Other)
	{
		return { X + _Other.X, Y + _Other.Y };
	}

	void operator+=(const int2& _Other)
	{
		X += _Other.X;
		Y += _Other.Y;
	}
};

const int2 Left = {-1, 0};
const int2 Right = { 1, 0 };
const int2 Up = { 0, -1 };
const int2 Down = { 0, 1 };

// 전역변수는 영역과 관련없이
// 이름이 위에 있다면 그것을 사용할 수 있다.
const int ScreenX = 20;
const int ScreenY = 12;
const int ScreenXHalf = ScreenX / 2;
const int ScreenYHalf = ScreenY / 2;

class ConsoleScreen
{
public:
	// 생성자를 만든다는것은
	// 내가 만든 생성자 형식으로만 생성해라.
	ConsoleScreen(char _BaseChar)
	{
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]
		// [*][*][*][*][*][0]

		BaseCh = _BaseChar;

	}

	void PrintScreen() 
	{
		for (int y = 0; y < ScreenY; y++)
		{
			char* Ptr = Arr[y];
			printf_s(Ptr);
			printf_s("\n");
		}
	}

	void ClearScreen()
	{
		system("cls");

		for (int y = 0; y < ScreenY; y++)
		{
			for (int x = 0; x < ScreenX - 1; x++)
			{
				Arr[y][x] = BaseCh;
			}
		}
	}

	void SetPixel(const int2& _Position, char _Char) 
	{
		SetPixel(_Position.X, _Position.Y, _Char);
	}

	void SetPixel(int _X, int _Y, char _Char)
	{
		Arr[_Y][_X] = _Char;
	}

protected:

private:
	char Arr[ScreenY][ScreenX] = { 0, };
	char BaseCh = ' ';
	// [0][0][0][0][0][0]
	// [0][0][0][0][0][0]
	// [0][0][0][0][0][0]
	// [0][0][0][0][0][0]
	// [0][0][0][0][0][0]
};

class Galaga
{
public:
	void GalagaWallDraw(ConsoleScreen& _Screen)
	{
		// [#][#][#][#][#][#]
		// [0][0][0][0][0][0]
		// [0][0][0][0][0][0]
		// [0][0][0][0][0][0]
		// [0][0][0][0][0][0]

		{
			int2 Start0 = { 0, 0 };
			int2 Start1 = { 0, ScreenY -1 };
			for (int i = 0; i < ScreenX - 1; i++)
			{
				_Screen.SetPixel(Start0, '#');
				_Screen.SetPixel(Start1, '#');
				Start0 += Right;
				Start1 += Right;
			}
		}

		{
			int2 Start0 = { 0, 0 };
			int2 Start1 = { ScreenX - 2, 0 };
			for (int i = 0; i < ScreenY; i++)
			{
				_Screen.SetPixel(Start0, '#');
				_Screen.SetPixel(Start1, '#');
				Start0 += Down;
				Start1 += Down;
			}
		}

	}

};

class Player
{
public:
	Player()
	{
	}

	Player(const int2& _StartPos, char _RenderChar)
		: Pos(_StartPos), RenderChar(_RenderChar)
	{
	}

	// inline은 컴파일러가 함수를 삭제하고
	// 그 위치에 치환시켜 버린다.
	inline int2 GetPos()
	{
		return Pos;
	}

	inline char GetRenderChar()
	{
		return RenderChar;
	}

	void Update() 
	{
		// Pos.X += 1;
		// getch의 리턴값을 확인해서

		int Value = _getch();

		switch (Value)
		{
		case 'a':
		case 'A':
		{
			if ((Pos + Left).X != 0)
			{
				Pos += Left;
			}
			break;
		}
		case 'd':
		case 'D':
		{
			if ((Pos + Right).X != (ScreenX - 2))
			{
				Pos += Right;
			}
			break;
		}
		case 'w':
		case 'W':
		{
			if ((Pos + Up).Y != 0)
			{
				Pos += Up;
			}
			break;
		}
		case 's':
		case 'S':
		{
			if ((Pos + Down).Y != (ScreenY - 1))
			{
				Pos += Down;
			}
			break;
		}
		case 'q':
		case 'Q':
		{
			if (nullptr != IsFire)
			{
				*IsFire = true;
			}
			// IsFire = true;
		}
		default:
			break;
		}

		int a = 0;
	}

	void SetBulletFire(bool* _IsFire)
	{
		if (nullptr == _IsFire)
		{
			int a = 0;
			return;
		}

		IsFire = _IsFire;
	}

private:
	int2 Pos = {0, 0};
	char RenderChar = '@';
	bool* IsFire = nullptr;
};

class Bullet 
{
public:
	Bullet(const int2& _StartPos, char _RenderChar)
		: Pos(_StartPos), RenderChar(_RenderChar)
	{
	}

	bool& GetIsFireRef() 
	{
		return IsFire;
	}

	inline int2 GetPos()
	{
		return Pos;
	}

	inline char GetRenderChar()
	{
		return RenderChar;
	}

private:
	bool IsFire = false;
	int2 Pos = { 0, 0 };
	char RenderChar = '@';
};

// 함수의 실행 메모리는 고정되어 있어야 한다.
// => exe파일에 적혀있다. main함수는 100바이트 사용한다.
// 한번 컴파일이 되고나면 절대로 바뀔수 없다
int main()
{
	// 과제 1
	// 화면 외곽을 벽으로 채워라
	// 플레이어가 벽 바깥으로 못나가게 만들어라.
	// [#][#][#][#][#][0]
	// [#][*][*][*][#][0]
	// [#][*][@][*][#][0]
	// [#][*][*][*][#][0]
	// [#][#][#][#][#][0]

	// 과제 2
	// 플레이어가 키를 누르면
	// [#][#][#][#][#][0]
	// [#][I][*][*][#][0]
	// [#][*][@][*][#][0]
	// [#][*][*][*][#][0]
	// [#][#][#][#][#][0]


	// 변수가 들어갈수 없어요.
	ConsoleScreen NewScreen = ConsoleScreen('*');
	Galaga NewGalaga = Galaga();

	Player NewPlayer = Player({ ScreenXHalf, ScreenYHalf }, '@');
	Bullet NewBullet = Bullet({0,0}, '^');

	bool& Ref = NewBullet.GetIsFireRef();
	NewPlayer.SetBulletFire(&Ref);

	while (true)
	{
		NewScreen.ClearScreen();
		NewGalaga.GalagaWallDraw(NewScreen);

		int2 Index = NewPlayer.GetPos();
		char Ch = NewPlayer.GetRenderChar();

		NewScreen.SetPixel(Index, Ch);

		if (true == NewBullet.GetIsFireRef())
		{
			NewScreen.SetPixel(NewBullet.GetPos(), NewBullet.GetRenderChar());
		}

		NewScreen.PrintScreen();
		NewPlayer.Update();
	}
}

실행화면

  • wsad로 이동

  • ;로 총알 생성

  • 플레이어@ 모습

  • 플레이어 이동 후 총알|이 생긴 모습

profile
일단 시작해보자

0개의 댓글

관련 채용 정보