본문 바로가기
-----ETC-----/C++로 배우는 게임 엔진 개발

[C++로 배우는 게임 엔진 개발] Day 24: 카메라 시스템 구현

by cogito21_cpp 2024. 8. 1.
반응형

카메라 시스템 구현

오늘은 3D 씬을 탐색하고 조작할 수 있는 카메라 시스템을 구현하는 방법을 학습하겠습니다. 카메라 시스템은 게임 엔진에서 중요한 역할을 하며, 사용자가 3D 세계를 탐색하고 상호작용할 수 있도록 합니다.

1. 카메라 클래스 설계

카메라의 위치, 방향 및 프로젝션을 관리하는 Camera 클래스를 설계합니다.

 

헤더 파일 작성

include/Camera.h 파일을 생성하고 다음과 같이 작성합니다.

#ifndef CAMERA_H
#define CAMERA_H

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

enum Camera_Movement {
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

const float YAW = -90.0f;
const float PITCH = 0.0f;
const float SPEED = 2.5f;
const float SENSITIVITY = 0.1f;
const float ZOOM = 45.0f;

class Camera {
public:
    Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), 
           glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), 
           float yaw = YAW, float pitch = PITCH);

    glm::mat4 GetViewMatrix();
    void ProcessKeyboard(Camera_Movement direction, float deltaTime);
    void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true);
    void ProcessMouseScroll(float yoffset);

    float Zoom;

private:
    void updateCameraVectors();

    glm::vec3 Position;
    glm::vec3 Front;
    glm::vec3 Up;
    glm::vec3 Right;
    glm::vec3 WorldUp;
    float Yaw;
    float Pitch;
    float MovementSpeed;
    float MouseSensitivity;
};

#endif // CAMERA_H

 

소스 파일 작성

src/Camera.cpp 파일을 생성하고 다음과 같이 작성합니다.

#include "Camera.h"

Camera::Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch)
    : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM) {
    Position = position;
    WorldUp = up;
    Yaw = yaw;
    Pitch = pitch;
    updateCameraVectors();
}

glm::mat4 Camera::GetViewMatrix() {
    return glm::lookAt(Position, Position + Front, Up);
}

void Camera::ProcessKeyboard(Camera_Movement direction, float deltaTime) {
    float velocity = MovementSpeed * deltaTime;
    if (direction == FORWARD)
        Position += Front * velocity;
    if (direction == BACKWARD)
        Position -= Front * velocity;
    if (direction == LEFT)
        Position -= Right * velocity;
    if (direction == RIGHT)
        Position += Right * velocity;
}

void Camera::ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch) {
    xoffset *= MouseSensitivity;
    yoffset *= MouseSensitivity;

    Yaw += xoffset;
    Pitch += yoffset;

    if (constrainPitch) {
        if (Pitch > 89.0f)
            Pitch = 89.0f;
        if (Pitch < -89.0f)
            Pitch = -89.0f;
    }

    updateCameraVectors();
}

void Camera::ProcessMouseScroll(float yoffset) {
    if (Zoom >= 1.0f && Zoom <= 45.0f)
        Zoom -= yoffset;
    if (Zoom <= 1.0f)
        Zoom = 1.0f;
    if (Zoom >= 45.0f)
        Zoom = 45.0f;
}

void Camera::updateCameraVectors() {
    glm::vec3 front;
    front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
    front.y = sin(glm::radians(Pitch));
    front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
    Front = glm::normalize(front);
    Right = glm::normalize(glm::cross(Front, WorldUp));
    Up = glm::normalize(glm::cross(Right, Front));
}

2. 게임 엔진에 카메라 시스템 추가

이제 게임 엔진에 카메라 시스템을 통합하고, 사용자 입력을 처리하여 카메라를 조작할 수 있도록 합니다.

 

헤더 파일 수정

include/GameEngine.h 파일을 수정하여 Camera 클래스를 포함합니다.

#ifndef GAMEENGINE_H
#define GAMEENGINE_H

#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <GL/glew.h>
#include "ParticleSystem.h"
#include "UIManager.h"
#include "LuaManager.h"
#include "NetworkManager.h"
#include "TextureManager.h"
#include "Graphics3D.h"
#include "Model.h"
#include "Camera.h"
#include "AudioManager.h"

class GameEngine {
public:
    GameEngine();
    ~GameEngine();
    bool Initialize(const char* title, int width, int height);
    void Run();
    void Shutdown();

private:
    SDL_Window* window;
    SDL_GLContext glContext;
    SDL_Renderer* renderer;
    GLuint shaderProgram;
    ParticleSystem* particleSystem;
    UIManager* uiManager;
    LuaManager* luaManager;
    NetworkManager* networkManager;
    Graphics3D* graphics3D;
    Model* model;
    Camera* camera;
    bool isRunning;
    Uint32 frameStart;
    int frameTime;

    bool InitializeOpenGL();
    GLuint LoadShader(const std::string& vertexPath, const std::string& fragmentPath);
    void HandleEvents();
    void Update();
    void Render();
};

#endif // GAMEENGINE_H

 

소스 파일 수정

src/GameEngine.cpp 파일을 다음과 같이 수정합니다.

#include "GameEngine.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

const int FPS = 60;
const int FRAME_DELAY = 1000 / FPS;

float lastX = 400, lastY = 300;
bool firstMouse = true;

GameEngine::GameEngine()
    : window(nullptr), glContext(nullptr), renderer(nullptr), shaderProgram(0), 
      particleSystem(nullptr), uiManager(nullptr), luaManager(nullptr), networkManager(nullptr),
      graphics3D(nullptr), model(nullptr), camera(nullptr), isRunning(false), frameStart(0), frameTime(0) {}

GameEngine::~GameEngine() {
    Shutdown();
}

bool GameEngine::Initialize(const char* title, int width, int height) {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl;
        return false;
    }

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
    if (window == nullptr) {
        std::cerr << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
        return false;
    }

    glContext = SDL_GL_CreateContext(window);
    if (glContext == nullptr) {
        std::cerr << "SDL_GL_CreateContext Error: " << SDL_GetError() << std::endl;
        return false;
    }

    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (renderer == nullptr) {
        std::cerr << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
        return false;
    }

    if (glewInit() != GLEW_OK) {
        std::cerr << "GLEW Initialization failed!" << std::endl;
        return false;
    }

    shaderProgram = LoadShader("shaders/vertex_shader_3d.glsl", "shaders/fragment_shader_3d.glsl");
    if (shaderProgram == 0) {
        return false;
    }

    particleSystem = new ParticleSystem(500);

    uiManager = new UIManager(renderer);
    if (!uiManager->Initialize()) {
        return false;
    }

    uiManager->RenderText("Hello, Game Engine!", 10, 10, 24, { 255, 255, 255, 255 });

    luaManager = new LuaManager();
    if (!luaManager->Initialize()) {
        return false;
    }

    luaManager->LoadScript("path/to/your/script.lua");

    if (!TextureManager::Instance().Load("path/to/your/texture.png", "example", renderer)) {
        return false;
    }

    networkManager = new NetworkManager();
    if (!networkManager->Initialize()) {
        return false;
    }

    graphics3D

 = new Graphics3D();
    if (!graphics3D->Initialize()) {
        return false;
    }

    model = new Model("path/to/your/model.obj");
    camera = new Camera(glm::vec3(0.0f, 0.0f, 3.0f));

    isRunning = true;
    return true;
}

GLuint GameEngine::LoadShader(const std::string& vertexPath, const std::string& fragmentPath) {
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;

    vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

    try {
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;

        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();

        vShaderFile.close();
        fShaderFile.close();

        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch (std::ifstream::failure& e) {
        std::cerr << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
        return 0;
    }

    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();

    GLuint vertexShader, fragmentShader;
    GLint success;
    GLchar infoLog[512];

    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vShaderCode, nullptr);
    glCompileShader(vertexShader);

    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        std::cerr << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
        return 0;
    }

    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fShaderCode, nullptr);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        std::cerr << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
        return 0;
    }

    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);

    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(program, 512, nullptr, infoLog);
        std::cerr << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
        return 0;
    }

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return program;
}

void GameEngine::Run() {
    while (isRunning) {
        frameStart = SDL_GetTicks();

        HandleEvents();
        Update();
        Render();

        frameTime = SDL_GetTicks() - frameStart;
        if (FRAME_DELAY > frameTime) {
            SDL_Delay(FRAME_DELAY - frameTime);
        }
    }
}

void GameEngine::HandleEvents() {
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_QUIT) {
            isRunning = false;
        }
        if (event.type == SDL_KEYDOWN) {
            switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    isRunning = false;
                    break;
                case SDLK_w:
                    camera->ProcessKeyboard(FORWARD, frameTime / 1000.0f);
                    break;
                case SDLK_s:
                    camera->ProcessKeyboard(BACKWARD, frameTime / 1000.0f);
                    break;
                case SDLK_a:
                    camera->ProcessKeyboard(LEFT, frameTime / 1000.0f);
                    break;
                case SDLK_d:
                    camera->ProcessKeyboard(RIGHT, frameTime / 1000.0f);
                    break;
                default:
                    break;
            }
        }
        if (event.type == SDL_MOUSEMOTION) {
            float xpos = static_cast<float>(event.motion.x);
            float ypos = static_cast<float>(event.motion.y);

            if (firstMouse) {
                lastX = xpos;
                lastY = ypos;
                firstMouse = false;
            }

            float xoffset = xpos - lastX;
            float yoffset = lastY - ypos;

            lastX = xpos;
            lastY = ypos;

            camera->ProcessMouseMovement(xoffset, yoffset);
        }
        if (event.type == SDL_MOUSEWHEEL) {
            camera->ProcessMouseScroll(static_cast<float>(event.wheel.y));
        }
    }
}

void GameEngine::Update() {
    float deltaTime = frameTime / 1000.0f;
    particleSystem->Update(deltaTime);

    luaManager->ExecuteFunction("update", deltaTime);

    networkManager->Service();
}

void GameEngine::Render() {
    // 화면을 검은색으로 지우기
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(shaderProgram);

    // 카메라 뷰 및 프로젝션 설정
    glm::mat4 view = camera->GetViewMatrix();
    glm::mat4 projection = glm::perspective(glm::radians(camera->Zoom), (float)800 / (float)600, 0.1f, 100.0f);

    GLuint viewLoc = glGetUniformLocation(shaderProgram, "view");
    GLuint projLoc = glGetUniformLocation(shaderProgram, "projection");
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

    // 3D 모델 렌더링
    model->Render(shaderProgram);

    // SDL 렌더러 사용하여 텍스처 렌더링
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);
    TextureManager::Instance().Draw("example", 100, 100, 64, 64, renderer);
    uiManager->Render();
    SDL_RenderPresent(renderer);

    SDL_GL_SwapWindow(window);
}

void GameEngine::Shutdown() {
    if (particleSystem) {
        delete particleSystem;
        particleSystem = nullptr;
    }
    if (uiManager) {
        uiManager->Shutdown();
        delete uiManager;
        uiManager = nullptr;
    }
    if (luaManager) {
        luaManager->Shutdown();
        delete luaManager;
        luaManager = nullptr;
    }
    if (networkManager) {
        networkManager->Shutdown();
        delete networkManager;
        networkManager = nullptr;
    }
    if (graphics3D) {
        graphics3D->Shutdown();
        delete graphics3D;
        graphics3D = nullptr;
    }
    if (model) {
        delete model;
        model = nullptr;
    }
    if (camera) {
        delete camera;
        camera = nullptr;
    }
    TextureManager::Instance().ClearTextureMap();
    if (shaderProgram) {
        glDeleteProgram(shaderProgram);
        shaderProgram = 0;
    }
    if (renderer) {
        SDL_DestroyRenderer(renderer);
        renderer = nullptr;
    }
    if (glContext) {
        SDL_GL_DeleteContext(glContext);
        glContext = nullptr;
    }
    if (window) {
        SDL_DestroyWindow(window);
        window = nullptr;
    }
    SDL_Quit();
}

3. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  3. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, 카메라를 사용하여 3D 씬을 탐색할 수 있습니다.

마무리

오늘은 카메라 시스템을 구현하고, 게임 엔진에서 카메라를 사용하여 3D 씬을 탐색하는 방법을 학습했습니다. 이를 통해 사용자가 3D 세계를 탐색하고 상호작용할 수 있는 기능을 추가할 수 있었습니다. 다음 단계에서는 3D 물리 엔진을 다루어 게임 오브젝트 간의 물리적 상호작용을 구현하는 방법을 배워보겠습니다.

질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요.

Day 25 예고

다음 날은 "3D 물리 엔진 기초 (Bullet)"에 대해 다룰 것입니다. 게임 오브젝트 간의 물리적 상

호작용을 구현하고, 이를 통해 현실적인 게임 플레이를 제공하는 방법을 배워보겠습니다.

반응형