눈팅하는 게임개발자 블로그

Snake Game 본문

Project/OpenGL Snake Game

Snake Game

Palamore 2020. 10. 4. 23:47

github.com/Palamore/OpenGL-Snake-Game

 

Palamore/OpenGL-Snake-Game

Snake Game with OpenGL Graphics Lib. Contribute to Palamore/OpenGL-Snake-Game development by creating an account on GitHub.

github.com

 

OpenGL 튜토리얼을 적당히 끝내고 만들어본 스네이크 게임.

 

LearnOpenGL 사이트에서 배운 그대로 glfw, glad, stb_image 등의 라이브러리를 사용했다.

또한 Camera, Shader도 따로 모듈화하여 작성한 상태로 사용.

 

전역변수, 함수, Main함수 순으로 작성.

전역변수

const unsigned int SCREEN_WIDTH = 800;
const unsigned int SCREEN_HEIGHT = 600;
const float FRAME_RATE = 1.0f / 60.0f;

float deltaTime = 0.0f;
float lastFrame = 0.0f;

Camera camera(glm::vec3(0.0f, 6.0f, 3.0f), glm::vec3(0.0f, 1.0f, 0.0f), -90.0f, -60.0f);

Snake snake;

윈도우 창의 width, height를 지정하고 초당 60프레임을 구현하기 위한 FRAME_RATE 변수를 선언.

deltaTime과 lastFrame은 게임 루프에서 현실시간 경과를 측정하기 위한 변수.

 

Camera 모듈은 Camera.h로 따로 만들어뒀다.

첫 번째 인자로 카메라의 위치를, 두 번째 인자로 카메라의 윗방향 축을,

세 번째 인자로 euler angle의 yaw 값을, 네 번째 인자로 pitch값을 받는다.

맵의 위쪽에서 대각선 아래 방향으로 맵 전체를 비스듬히 내려다보는 위치와 각도이다.

 

마지막으로 플레이어가 조작하는 snake에 대한 정보를 담는 Snake 객체를 전역변수로 선언.

snake 객체는 현재 몸체의 길이, snake가 바라보고 있는 방향을 멤버변수로 갖는다.

 

함수

void FrameBufferSizeCallBack(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void ProcessInput(GLFWwindow* window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
	float cameraSpeed = 0.5f * deltaTime;
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera.ProcessKeyboard(FORWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera.ProcessKeyboard(BACKWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera.ProcessKeyboard(LEFT, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera.ProcessKeyboard(RIGHT, deltaTime);

	if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
		snake.SetDirection(SnakeDirection::UP);
	if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
		snake.SetDirection(SnakeDirection::DOWN);
	if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS)
		snake.SetDirection(SnakeDirection::LEFT);
	if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS)
		snake.SetDirection(SnakeDirection::RIGHT);
				
}

FrameBufferSizeCallBack은 전혀 건들지 않았다.

GLFW에서 window에 등록하는 콜백함수인듯.

 

processInput함수에는 W, A, S, D 키로 카메라를 움직일 수 있고

플레이어가 방향키로 snake가 움직이는 방향을 설정하도록 했다.

void MakeTexture(unsigned int& tex, const char* resource)
{
	glGenTextures(1, &tex);
	glBindTexture(GL_TEXTURE_2D, tex);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	int width, height, nrChannels;
	unsigned char* data = stbi_load(resource, &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "FAILED TO LOAD TEXTURE" << std::endl;
	}
	stbi_image_free(data);
}

텍스처 세팅을 위한 함수 MakeTextrue.

텍스처 ID와 resource의 Path를 인자로 받아서 해당 텍스처 ID에 대한 설정을 해준다.

 

glm::mat4 SetSnakeBodyPos(glm::vec3 tilePos[], int x, int y)
{
	glm::mat4 posMatrix = glm::mat4(1.0f);
	posMatrix = glm::translate(posMatrix, tilePos[x] + glm::vec3(0.0f, 0.0f, 1.0f * y));
	return posMatrix;
}

11 x 11짜리 맵 위에 Box가 위치할 수 있도록 model matrix를 세팅해주는 SetSnakeBodyPos함수.

x, y축으로 0~10 인덱스 범위 내에서 설정한다.

카메라가 y축 상의위에서 내려다보는 형태로

실제 맵은 x, z평면이므로 y값을 실제 적용할 때에는 z축으로 적용해줘야 한다.

 

Pos GenerateSnakeBody(std::vector<Pos>& snakePos)
{
	Pos _pos;
	bool genFlag = true;
	while (1)
	{
		srand(static_cast<unsigned int>(time(NULL)));
		int xPos = rand() % 11;
		int yPos = rand() % 11;
		for (size_t i = 0; i < snakePos.size(); i++)
		{
			if (snakePos[i].x == xPos && snakePos[i].y == yPos)
			{
				genFlag = false;
				break;
			}
		}
		if (!genFlag)
		{
			genFlag = true;
		}
		else
		{
			_pos.x = xPos;
			_pos.y = yPos;
			return _pos;
		}
	}
}

게임에서 플레이어가 snake를 조작해 먹는 추가 몸통을 생성하는 GenerateSnakeBody 함수.

snakePos 벡터변수를 인자로 받아 snake의 몸체가 없는 곳의 x, y좌표(Pos)값이 나올 때까지 반복하여

값이 나왔다면 해당 값을 반환해준다.

 

glm::vec3 SetPosToVec(Pos _pos)
{
	return glm::vec3(static_cast<float>(_pos.x), 0.0f, static_cast<float>(_pos.y));
}

좌표값 Pos를 행렬연산에 사용하기 위해 Vec3 값으로 변환해주는 SetPosToVec 함수.

 

bool CheckHeadCollision(glm::vec3 _pos, glm::vec3 headPos, SnakeDirection sd)
{
	float minX = _pos.x - 0.5f, maxX = _pos.x + 0.5f;
	float minZ = _pos.z - 0.5f, maxZ = _pos.z + 0.5f;
	float headZ = headPos.z, headX = headPos.x;
	switch (sd)
	{
	case SnakeDirection::UP:
		headZ -= 0.5f;
		break;
	case SnakeDirection::DOWN:
		headZ += 0.5f;
		break;
	case SnakeDirection::LEFT:
		headX -= 0.5f;
		break;
	case SnakeDirection::RIGHT:
		headX += 0.5f;
		break;
	default: break;
	}
	if (headZ > minZ &&
		headZ < maxZ &&
		headX > minX &&
		headX < maxX)
		return true;
	return false;
}

snake 게임에서의 충돌체크는 머리부분만 하면 된다. 머리 부분의 충돌을 체크하는 CheckHeadCollision함수.

현재 머리부분의 중심값과 충돌체크를 할 대상(플레이어의 snake 몸체 또는 추가 몸체)의 중심값,

현재 snake가 향하고 있는 방향(sd)을 인자로 받아 각 꼭지점(x, z축, y축은 의미가 없다.)을 기준으로 충돌체크를 한다.

방향에 따라 머리부분에서 충돌체크를 할 점을 설정한다.

 

void GameOver(GLFWwindow* window)
{
	std::cout << "GAME OVER!" << std::endl;
	glfwSetWindowShouldClose(window, true);
}

GameOver 함수.

CLI 창에 GAME OVER 문자열을 띄워주고 GLFW 윈도우 창을 닫는다.

 

 

Main 

glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

glfw 이니셜라이징, 버전 선언과 OpenGL의 Profile, Core 기능을 사용할 것을 명시해준다.

 

GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Snake Game", NULL, NULL);
if (window == NULL)
{
	std::cout << "FAILED TO CREATE GLFW WINDOW" << std::endl;
	glfwTerminate();
	return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, FrameBufferSizeCallBack);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
	std::cout << "FAILED TO INITIALIZE GLAD" << std::endl;
	return -1;
}
glEnable(GL_DEPTH_TEST);

GLFW window를 만들어주고 현재 context를 해당 window로 설정. FrameBufferSize 콜백함수 등록.

glad 이니셜라이징과 마지막으로 Z-Buffer를 사용하기 위해 GL_DEPTH_TEST를 활성화해준다.

 

Shader shader("SnakeGameVertexShader.vs", "SnakeGameFragmentShader.fs");

/* SnakeGameVertexShader.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	gl_Position = projection * view * model * vec4(aPos, 1.0f);
	TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
*/
/* SnakeGameFragmentShader.fs
#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;
uniform sampler2D texture3;
uniform sampler2D texture4;
uniform int texFlag;

void main()
{
	if(texFlag == 1)
	FragColor = texture(texture1, TexCoord);
	else if(texFlag == 2)
	FragColor = texture(texture2, TexCoord);
	else if(texFlag == 3)
	FragColor = texture(texture3, TexCoord);
	else if(texFlag == 4)
	FragColor = texture(texture4, TexCoord);
}
*/

따로 만들어둔 Shader 모듈로 shader 객체를 생성.

생성자 매개변수로 vertex shader 코드와 fragment shader 코드가 담겨있는 파일의 path를 받는다.

Vertex, Fragment Shader에는 특별할 것 없이 주어진 텍스처를 매핑하는 코드만 있다.

 

	float vertices[] =
	{
		-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
		-0.5f, -0.5f,  0.5f, 0.0f, 1.0f,
		-0.5f, -0.5f,  0.5f, 0.0f, 1.0f,
		 0.5f, -0.5f,  0.5f, 1.0f, 1.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f
	};

	float snakeVertices[] =
	{
		-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
		-0.5f, -0.5f,  0.5f, 0.0f, 1.0f,
		-0.5f, -0.5f,  0.5f, 0.0f, 1.0f,
		 0.5f, -0.5f,  0.5f, 1.0f, 1.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,

		 0.5f, -0.5f,  0.5f, 0.0f, 0.0f,
		 0.5f,  0.5f,  0.5f, 0.0f, 1.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
		 0.5f,  0.5f, -0.5f, 1.0f, 1.0f,
		 0.5f,  0.5f,  0.5f, 0.0f, 1.0f,

		-0.5f, -0.5f,  0.5f, 0.0f, 0.0f,
		 0.5f, -0.5f,  0.5f, 1.0f, 0.0f,
		-0.5f,  0.5f,  0.5f, 0.0f, 1.0f,
		-0.5f,  0.5f,  0.5f, 0.0f, 1.0f,
		 0.5f,  0.5f,  0.5f, 1.0f, 1.0f,
		 0.5f, -0.5f,  0.5f, 1.0f, 0.0f,

		-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
		-0.5f, -0.5f,  0.5f, 1.0f, 0.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		-0.5f,  0.5f,  0.5f, 1.0f, 1.0f,
		-0.5f, -0.5f,  0.5f, 1.0f, 0.0f,

		-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		 0.5f,  0.5f, -0.5f, 1.0f, 1.0f,
		 0.5f, -0.5f, -0.5f, 1.0f, 0.0f,

		-0.5f,  0.5f,  0.5f, 0.0f, 0.0f,
		 0.5f,  0.5f,  0.5f, 1.0f, 0.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		-0.5f,  0.5f, -0.5f, 0.0f, 1.0f,
		 0.5f,  0.5f, -0.5f, 1.0f, 1.0f,
		 0.5f,  0.5f,  0.5f, 1.0f, 0.0f
	};

	glm::vec3 tilePositions[] = {
		glm::vec3(-5.0f, -5.0f, -10.0f),
		glm::vec3(-4.0f, -5.0f, -10.0f),
		glm::vec3(-3.0f, -5.0f, -10.0f),
		glm::vec3(-2.0f, -5.0f, -10.0f),
		glm::vec3(-1.0f, -5.0f, -10.0f),
		glm::vec3( 0.0f, -5.0f, -10.0f),
		glm::vec3( 1.0f, -5.0f, -10.0f),
		glm::vec3( 2.0f, -5.0f, -10.0f),
		glm::vec3( 3.0f, -5.0f, -10.0f),
		glm::vec3( 4.0f, -5.0f, -10.0f),
		glm::vec3( 5.0f, -5.0f, -10.0f)
	};

버텍스 정보는 기본적으로 3개의 위치 정보와 2개의 texCoord 정보이다.

vertices는 맵의 버텍스 정보(사각형)

snakeVertices는 snake의 몸체가 되는 정육각형의 버텍스 정보.

tilePosition은 맵의 가장자리 왼쪽 부분 맵의 위치정보이다.

이를 11번 반복해서 model matrix에 사용하여 11 x 11짜리 맵을 만들어낸다.

 

	unsigned int VBO, VAO, snakeVBO, snakeVAO;
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);

	glBindVertexArray(VAO);

	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

	//-----------------------------------

	glGenVertexArrays(1, &snakeVAO);
	glGenBuffers(1, &snakeVBO);

	glBindVertexArray(snakeVAO);
	glBindBuffer(GL_ARRAY_BUFFER, snakeVBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(snakeVertices), snakeVertices, GL_STATIC_DRAW);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

VBO, VAO는 맵 타일(사각형)에 대한 vertex 속성을.

snakeVBO, snakeVAO는 snake의 몸체에 대한 vertex 속성을 설정한다.

 

	stbi_set_flip_vertically_on_load(true);
	unsigned int boxTex1, boxTex2, snakeTex, addBodyTex;
	MakeTexture(boxTex1, "resources/tile1.jpg");
	MakeTexture(boxTex2, "resources/tile2.jpg");
	MakeTexture(snakeTex, "resources/brownBox.jpg");
	MakeTexture(addBodyTex, "resources/YellowBox.jpg");

	shader.use();
	shader.setInt("texture1", 0);
	shader.setInt("texture2", 1);
	shader.setInt("texture3", 2);
	shader.setInt("texture4", 3);
	shader.setInt("texFlag", 2);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, boxTex1);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, boxTex2);
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, snakeTex);
	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, addBodyTex);

stbi로더가 위아래가 뒤집혀서 로드된다는 특징이 있다고 한다.

이를 다시 뒤집어주기 위해서 stbi_set_flip_vertically_on_load 함수를 먼저 실행해준다.

boxTex1은 밝은 부분의 맵 타일, boxTex2는 어두운 부분의 맵 타일의 텍스처 ID가 되고.

snakeTex는 플레이어가 조작하는 snake의 몸체 부분의 텍스처 ID,

addBodyTex는 추가 몸체의 텍스처 ID가 된다.

미리 선언해둔 MakeTexture 함수로 해당 텍스처들을 로드하고.

shader객체를 통해 uniform Sampler2D 변수들을 세팅해둔다.

마지막으로 각 텍스처들을 위한 텍스처 유닛을 활성화하고 텍스처들을 바인드하여 텍스처 유닛에 저장.

 

	// Snake Initialization
	std::vector<Pos> snakeBodyContainer;
	snakeBodyContainer.reserve(50);
	SnakeDirection snakeDirection = SnakeDirection::UP;
	Pos pos = { 5, 5 };
	Pos genPos = { 6, 6 };
	Pos nextPos = { 5, 4 }, lastPos = pos, curPos = pos;
	lastPos.y += 1;
	float frame = 0.0f, oneSec = 0.0f;
	bool getBoxFlag = false;
	snakeBodyContainer.push_back(lastPos);

vector 컨테이너 변수 snakeBodyContainer는 snake 몸체의 위치(Pos)를 저장하고. 

snakeBodyContainer 변수는 현재 snake가 향하는 방향을 저장한다.

Pos 변수들은 snake의 현재 위치, 다음 향할 위치, 마지막(꼬리) 위치, 추가 몸체가 선언될 위치를 담는다.

deltaTime을 이용해 1초에 60번 실행될 루프를 만들기 위한 변수 frame.

deltaTime을 이용해 1초에 2번 실행될 루프를 만들기 위한 변수 oneSec.

getBoxFlag 변수는 플레이어가 추가 몸체를 획득했을 때를 위한 플래그 변수.

기본적으로 하나의 몸체를 snakeBodyContainer에 넣어주고 게임루프가 시작된다.

 

게임 루프

	while (!glfwWindowShouldClose(window))
    {
    ...
    }

우선 glfwWindowShouldClose 플래그가 true가 될때까지 계속 루프한다.

 

		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;
		frame += deltaTime;
		oneSec += deltaTime;

		ProcessInput(window);
        
	        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

실제 시간 경과를 위한 deltaTime 연산. glfwGetTime()함수를 사용한다.

유저로부터 입력을 받는 ProcessInput함수 호출.

배경 색을 설정해주고 모든 프레임마다 Color 버퍼와 Z-버퍼를 비워주기 위한 glClear함수 호출.

 

		shader.use();

		glm::mat4 view = glm::mat4(1.0f);
		view = camera.GetViewMatrix();
		glm::mat4 projection = glm::mat4(1.0f);
		projection = glm::perspective(glm::radians(camera.Zoom), (float)SCREEN_WIDTH / (float)SCREEN_HEIGHT, 0.1f, 100.0f);

		int viewLoc = glGetUniformLocation(shader.ID, "view");
		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
		int projLoc = glGetUniformLocation(shader.ID, "projection");
		glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

shader 객체에 view 행렬과 projection 행렬 값을 uniform 변수로 전달해준다.

 

		int tileCnt = 0;
		glBindVertexArray(VAO);
		for (size_t i = 0; i < 11; i++)
		{
			for (size_t j = 0; j < 11; j++)
			{
				if (tileCnt % 2 == 0)
					shader.setInt("texFlag", 1);
				else
					shader.setInt("texFlag", 2);
				tileCnt++;

				glm::mat4 model = glm::mat4(1.0f);
				model = glm::translate(model, tilePositions[j] + glm::vec3(0.0f, 0.0f, 1.0f * (float)i));

				shader.setMat4("model", model);

				glDrawArrays(GL_TRIANGLES, 0, 6);
			}
		}

11 x 11짜리 맵을 먼저 그려준다. 하나의 Vertex Array를 그릴 때마다 shader의 texFlag Uniform 변수의 값을 변경시켜

번갈아가면서 다른 텍스처가 나오도록 했다.

 

		glm::vec3 nextVec = SetPosToVec(nextPos);
		glm::vec3 curVec = SetPosToVec(curPos);
		glm::vec3 lastVec = SetPosToVec(lastPos);
		glm::vec3 tailVec;
		if (!snakeBodyContainer.empty())
		{
			tailVec = SetPosToVec(snakeBodyContainer[0]);
		}
		else
		{
			tailVec = curVec;
		}

움직이는 머리 부분, 꼬리 부분의 연산을 위한 vec3변수들을 미리 선언해둔다.

 

		glBindVertexArray(snakeVAO);
		shader.setInt("texFlag", 3);
		for (size_t i = 0; i < snakeBodyContainer.size(); i++)
		{
			glm::mat4 model = SetSnakeBodyPos(tilePositions, snakeBodyContainer[i].x, snakeBodyContainer[i].y);
			shader.setMat4("model", model);
			glDrawArrays(GL_TRIANGLES, 0, 36);
		}

이제부터 정육면체들을 그려줄 것이므로 snakeVAO를 바인드. texFlag도 snake의 몸체 텍스처 인덱스인 3으로 설정.

우선 snakeBodyContainer안의 좌표를 이용해서 snake의 움직이지 않는 몸체들을 그려준다.

 

		if (frame >= FRAME_RATE)
		{
      	 	 ...
    	 	}

이후는 1초에 60번 실행되는 게임루프.

 

			float t = oneSec;
			if (t >= 0.5f) t = 0.5f;
			frame -= FRAME_RATE;
			glm::vec3 headingVec = curVec + (nextVec - curVec) * t * 2.0f;

			glm::mat4 snakeHead = SetSnakeBodyPos(tilePositions, curPos.x, curPos.y);
			snakeHead = glm::translate(snakeHead, (nextVec - curVec) * t * 2.0f);
			shader.setMat4("model", snakeHead);
			glDrawArrays(GL_TRIANGLES, 0, 36);
			glm::mat4 snakeTail = SetSnakeBodyPos(tilePositions, lastPos.x, lastPos.y);
			snakeTail = glm::translate(snakeTail, (tailVec - lastVec) * t * 2.0f);
			shader.setMat4("model", snakeTail);
			glDrawArrays(GL_TRIANGLES, 0, 36);

움직이는 머리 부분과 꼬리 부분의 model 행렬에 추가적인 translate 연산을 해준다.

목표 위치에 현재 위치값을 뺀 후 시간경과에 따라 이동시켜준다. (0.5초에 1칸을 이동)

 

			if (nextPos.x < 0 || nextPos.x > 10 || nextPos.y < 0 || nextPos.y > 10)
			{
				GameOver(window);
			}
			// 꼬리 충돌체크
			if (CheckHeadCollision(lastVec + ((tailVec - lastVec) * t * 2.0f), headingVec, snakeDirection))
			{
				GameOver(window);
			}
			// 몸통 충돌체크
			for (size_t i = 0; i < snakeBodyContainer.size(); i++)
			{
				glm::vec3 bodyVec = SetPosToVec(snakeBodyContainer[i]);
				if (CheckHeadCollision(bodyVec, headingVec, snakeDirection))
				{
					GameOver(window);
				}
			}
			// 추가 몸통 충돌체크 
			glm::vec3 genVec = SetPosToVec(genPos);
			if (CheckHeadCollision(genVec, headingVec, snakeDirection))
			{
				std::cout << (int)snakeDirection << std::endl;
				getBoxFlag = true;
				genPos = GenerateSnakeBody(snakeBodyContainer);
			}

snake가 맵밖으로 나갔는지, 다른 오브젝트들과 충돌하는지에 대한 체크.

꼬리, 몸체와 충돌하면 게임오버, 추가 몸통과 충돌하면 새로운 몸통을 맵에 생성하고 getBoxFlaog를 true로 바꿔준다.

 

if (oneSec >= 0.5f)
			{
				oneSec -= 0.5f;

				if (snakeBodyContainer.empty())
					lastPos = curPos;
				else
					lastPos = snakeBodyContainer[0];
				curPos = nextPos;

				snakeDirection = snake.GetDirection();

				switch (snakeDirection)
				{
				case SnakeDirection::UP:
					nextPos.y -= 1;
					break;
				case SnakeDirection::DOWN:
					nextPos.y += 1;
					break;
				case SnakeDirection::LEFT:
					nextPos.x -= 1;
					break;
				case SnakeDirection::RIGHT:
					nextPos.x += 1;
					break;
				}

				snakeBodyContainer.push_back(curPos);
				if (!getBoxFlag)
				{
					snakeBodyContainer.erase(snakeBodyContainer.begin());
				}
				else
				{
					getBoxFlag = false;
				}
			}

이후 1초마다 2번 실행되는 게임로직.

0.5초마다 한칸씩 이동하므로 한칸 이동 후에 필요한 연산들을 수행한다.

현재 nextPos(도착한 좌표)를 curPos에 대입해주고

snake가 현재 향하는 방향을 받아와서 다음으로 향할 칸의 좌표를 nextPos에 지정해준다.

이후 snakeBodyContainer에 현재 도착한 좌표값을 넣어주고 기존 꼬리 부분을 삭제한다.

getBoxFlag함수가 true일 경우에는(추가 몸체를 획득한 경우)

꼬리 부분을 삭제하지 않는다.(결과적으로 몸체가 하나 늘어난다.)

 

		shader.setInt("texFlag", 4);
		glm::mat4 model = SetSnakeBodyPos(tilePositions, genPos.x, genPos.y);
		shader.setMat4("model", model);
		glDrawArrays(GL_TRIANGLES, 0, 36);

텍스처 인덱스를 4로 바꿔주고(추가 몸체용 텍스처)

추가 몸체를 맵상에 그려준다.

여기까지가 모든 게임루프 로직.

 

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteVertexArrays(1, &snakeVAO);
	glDeleteBuffers(1, &snakeVBO);


	glfwTerminate();
	return 0;

main함수의 끝부분, VAO와 VBO들을 지워주고

프로그램 종료.