OpenGL/개발 일지

[OpenGL] 게임 엔진 개발 (9) - 플레이어 이동

ciel45 2024. 8. 20. 19:47

참조 : https://www.youtube.com/watch?v=d-kuzyCkjoQ&pp=ygURb3BlbmdsIHBsYWVyIG1vdmU%3D

 

지금까지 플레이어 모델이 항상 A포즈로 서있기만 했는데, 이제 조작을 통해 맵 위로 다닐 수 있도록 할 것이다.

 

우선 Player 클래스를 새로 만들었다.

 

Player.h

#pragma once

#include "glm/glm.hpp"
class Model;
class Terrain;

class Player
{
public:
	Player(Model* model);
	void HandleInput(bool* keys, float deltaTime);
	bool Move(float deltaTime, Terrain* terrain);
	float GetRotY();
	
	Model* GetModel() { return model; }

private:
	void Jump();

	Model* model;

	const float MOVE_SPEED;
	const float TURN_SPEED;
	const float GRAVITY;
	const float JUMP_POWER;

	float currMoveSpeed;
	float currTurnSpeed;
	float upwardSpeed;
	float groundHeight;

	bool isJumping;
};

멤버 변수 중 Model 포인터를 가지고있는 것을 볼 수 있는데,

실제로 지금까지 썼던 모델을 플레이어의 모델로 해서 조작이 가능하도록 할 것이다.

 

우선 생성자로 Model 포인터를 넘겨주면, 그걸 자신의 model로 등록하도록 하였다.

Player::Player(Model* model) : MOVE_SPEED(10.f), TURN_SPEED(200.f), GRAVITY(0.2f), JUMP_POWER(0.05f)
{
	this->model = model;
	groundHeight = 10;
	upwardSpeed = 0;

	isJumping = true;
}

 

그리고 WASD 및 스페이스 바 입력을 받도록 HandleInput 함수를 다음과 같이 짰다.

void Player::HandleInput(bool* keys, float deltaTime)
{
	if (keys[GLFW_KEY_W])
		currMoveSpeed = MOVE_SPEED;
	else if (keys[GLFW_KEY_S])
		currMoveSpeed = -MOVE_SPEED;
	else
		currMoveSpeed = 0;

	if (keys[GLFW_KEY_A])
		currTurnSpeed = TURN_SPEED;
	else if (keys[GLFW_KEY_D])
		currTurnSpeed = -TURN_SPEED;
	else
		currTurnSpeed = 0;

	if (keys[GLFW_KEY_SPACE])
		Jump();
}

 

보시다시피 W/S 키는 currMoveSpeed를 조정하고, A/D 키는 currTurnSpeed를 조정한다.

카메라가 어떻게 회전되어있든 무조건 W를 누르면 앞으로가고, A/D로 좌/우회전을 하는,

약간 고전 바이오하자드 시리즈같은 조작인데, 이게 구현이 편해서 이렇게 했다.

 

 

다음은 핵심 중의 핵심인 Move 함수인데, 먼저 수학적으로 정리를 좀 해보려고 한다.

기본적으로 캐릭터는 +Z 방향을 보고있다는 것을 전제로 한다.

그리고 각도는 반시계 방향을 양의 값으로 가진다.

 

위 좌표계는 OpenGL이 사용하는 좌표계를 그대로 그린 것으로, Y축은 따로 그리지 않았지만 모니터를 뚫고 나오는 방향이 될 것이다.

플레이어는 조작에 따라 Y축을 기준으로 회전될 수 있는데, 이 값을 newRotY라 하자.

회전 후 앞으로 이동한다고 하면, x좌표와 z좌표가 변할 것이다.

 

이때 삼각함수에 의거하여

z좌표의 변화율은 distance * cos(newRotY)가 되고, 

x좌표의 변화율은 distance * sin(newRotY)가 된다.

 

이제 이걸 코드로 적어주면 되는데, OpenGL 코딩 시에는 degree를 radian으로 바꿔주는 것 꼭 잊지 말아야 한다.

 

bool Player::Move(float deltaTime, Terrain* terrain)
{
	// 회전
	GLfloat* currRot = model->GetRotate();

	float rotation = currTurnSpeed * deltaTime;

	float newRotY = currRot[1] + rotation; // new rotY
    // 값을 [-180.f, 180.f] 범위로 clamp
	if(newRotY > 180)
		newRotY -= 360.f;
	if (newRotY < -180.f)
		newRotY += 360.f;

	glm::vec3 newRot(currRot[0], newRotY, currRot[2]);

	model->SetRotate(newRot);

	// 이동
	GLfloat* currPos = model->GetTranslate();
	float distance = currMoveSpeed * deltaTime;

	float dx = distance * sinf(glm::radians(newRotY));
	float dz = distance * cosf(glm::radians(newRotY));

	upwardSpeed -= GRAVITY * deltaTime;

	glm::vec3 newPos(currPos[0]+dx, currPos[1] + upwardSpeed, currPos[2]+dz);

	// terrain collision 처리 파트. 
    // GetHeight 함수는 다음 포스팅에서 다룰 것이다.
	groundHeight = terrain->GetHeight(currPos[0], currPos[2]);
	if (newPos[1] <= groundHeight) // 땅에 닿았다면
	{
		upwardSpeed = 0;
		newPos[1] = groundHeight;
		isJumping = false;
	}

	model->SetTranslate(newPos);

	return currMoveSpeed != 0;
}

 

이렇게 Move함수가 만들어졌고,

플레이어 이동이 가능해졌으므로 플레이어 3인칭 카메라도 만들어보았다.

클래스 이름은 PlayerCamera로 정했다.

 

기존의 Camera 클래스를 FreeCamera로 바꾸고, PlayerCamera와 공통된 부분을 CameraBase 추상클래스로 올려줬다.

그림으로 보면 다음과 같다.

CameraBase 클래스의 구조는 다음과 같다.

#pragma once

#include <GL\glew.h>
#include <GLFW\glfw3.h>

#include <glm\glm.hpp>
#include <glm\gtc\matrix_transform.hpp>

class CameraBase
{
public: 
	CameraBase(glm::vec3 startPosition, GLfloat startPitch = 0.f, GLfloat startYaw = -90.f);

	virtual void Update();
	virtual void KeyControl(bool* keys, GLfloat deltaTime) = 0;
	virtual void MouseControl(GLfloat xChange, GLfloat yChange) = 0;
	virtual void ScrollControl(GLfloat scrollY) = 0;

	bool CanMove() { return canMove; }
	void SetCanMove(bool flag) { canMove = flag; }

	glm::mat4 GetViewMatrix();
	glm::mat4 GetProjectionMatrix(GLfloat width, GLfloat height);
	glm::vec3 GetPosition();


protected:
	glm::vec3 position;
	glm::vec3 front;
	glm::vec3 up;
	glm::vec3 right;
	glm::vec3 worldUp;

	GLfloat yaw;
	GLfloat pitch;

	GLfloat nearClippingPlane;
	GLfloat farClippingPlane;

	bool canMove;
};

순수 가상함수 Update, KeyControl, MouseControl, ScrollControl이 FreeCamera와 PlayerCamera가 각각 다르게 처리할 부분이다.

 

FreeCamera는 https://ciel45.tistory.com/93 여기서 딱히 달라질 게 없으므로,

바로 PlayerCamera를 살펴보자면 다음과 같다.

#pragma once
#include "CameraBase.h"

class Player;

class PlayerCamera : public CameraBase
{
public:
	PlayerCamera(Player* player);

	virtual void Update() override;
	virtual void KeyControl(bool* keys, GLfloat deltaTime) override;
	virtual void MouseControl(GLfloat xChange, GLfloat yChange) override;
	virtual void ScrollControl(GLfloat scrollY) override;

private:
	float CalcHorizontalDistance();
	float CalcVerticalDistance();
	void CalcPosition(float horizontalDistance, float verticalDistance);

	Player* player;
	float distance;
	float angle;
	bool canMove;

	const float HEIGHT_OFFSET;
};

새로운 함수로 CalcHorizontalDistance, CalcVerticalDistance, CalcPosition 3개가 있는데, 

카메라가 Player로부터 일정 distance만큼 떨어져있을때, 그게 수직/수평으로는 얼마만큼의 거리인지 계산 후

플레이어의 이동을 처리했을 때와 비슷하게 위치를 움직여주기 위해서이다.

 

이번에도 수학적으로 조금 정리를 해보자면, 다음과 같다.

먼저 플레이어와 카메라 간의 거리 distance가 있을 때, 이걸 horizontalDistance와 verticalDistance로 분리할 수 있다.

카메라가 조금 아래를 쳐다보므로 pitch가 음수가 될텐데, 우리가 원하는건 양수 거리이므로 pitch에 마이너스를 해줬다.

코드로 옮기면 다음과 같다.

float PlayerCamera::CalcHorizontalDistance()
{
	return distance * cosf(glm::radians(-pitch));
}

float PlayerCamera::CalcVerticalDistance()
{
	return distance * sinf(glm::radians(-pitch));
}

 

verticalDistance는 그대로 y축 성분으로 사용 가능하지만,

horizontalDistance는 또 다시 x축 성분, z축 성분으로 나누어줘야 한다.

좀 복잡한데, 플레이어와 카메라가 모두 회전이 가능하기 때문이다.

플레이어의 y축 기준 회전을 rotY로 두고, 카메라가 플레이어를 기준으로 회전한 각도를 angle이라고 한다면, 

이미지에서 보이다시피 -z방향을 기준으로 카메라가 돌아간 총 각도rotY + angle 이라고 할 수 있고, 이걸 theta라고 하자.

 

그러면 카메라는 플레이어 기준

x축 상으로 horizontalDistance * sin(theta)만큼 떨어져있고,

z축 상으로 horizontalDistance * cos(theta)만큼 떨어져있다는 사실을 알 수 있다.

 

이제 이걸 모두 고려해서 코드로 옮기면 다음과 같다.

void PlayerCamera::Update()
{
	float horizontalDistance = CalcHorizontalDistance();
	float verticalDistance = CalcVerticalDistance();
	CalcPosition(horizontalDistance, verticalDistance);
	yaw = 90.f - (player->GetRotY() + angle);

	CameraBase::Update();
}

void PlayerCamera::CalcPosition(float horizontalDistance, float verticalDistance)
{
	GLfloat* playerPos = player->GetModel()->GetTranslate();
	position.y = playerPos[1] + verticalDistance + HEIGHT_OFFSET;

	float theta = player->GetRotY() + angle;
	float offsetX = horizontalDistance * sinf(glm::radians(theta));
	float offsetZ = horizontalDistance * cosf(glm::radians(theta));

	position.x = playerPos[0] - offsetX;
	position.z = playerPos[2] - offsetZ;
}

Update 함수 안에서 카메라의 yaw 값을 90 - (rotY + angle)로 해주는 이유는,

방금 그림을 조금 더 자세히 보면 카메라가 x축과 이루는 각도는 다음과 같다(녹색 밑줄).

 

저 각도만큼 카메라를 돌려주어야 하는데,

https://ciel45.tistory.com/93  이 포스팅 후반부 그림을 다시 한번 살펴보면

yaw가 0일때 카메라는 x축을 바라보고 있다.

우리가 원하는건 저 그림 속 yaw의 값이 90 - (rotY + angle)임을 알 수 있다.

그래서 yaw를 저렇게 설정해준 것이다.

 

전체 소스코드:

https://github.com/sys010611/YsEngine

 

GitHub - sys010611/YsEngine

Contribute to sys010611/YsEngine development by creating an account on GitHub.

github.com

 

 

실행 결과:

https://www.youtube.com/watch?v=Ro2ALw5RpHs

 

사실 영상은 아직 다루지 않은 terrain collision까지 구현된 결과이다

이에 대해서는 다음 포스팅에서 다룰 것이다.