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

[C++로 배우는 게임 엔진 개발] Day 19: 스크립팅 시스템 기초 (Lua)

by cogito21_cpp 2024. 8. 1.
반응형

스크립팅 시스템 기초 (Lua)

오늘은 Lua를 사용하여 스크립팅 시스템을 구현하고, 게임 로직을 스크립트로 작성하는 방법을 학습하겠습니다. 스크립팅 시스템을 통해 게임 로직을 유연하게 변경하고, 확장할 수 있습니다.

1. Lua 설치 및 설정

먼저 Lua를 설치하고 프로젝트에 설정합니다.

 

Lua 다운로드 및 설치

  1. Lua 공식 웹사이트에서 Lua 바이너리 파일을 다운로드합니다.
  2. 다운로드한 파일을 프로젝트 디렉토리로 이동합니다.

CMakeLists.txt 수정

CMakeLists.txt 파일에 Lua 라이브러리를 추가합니다.

cmake_minimum_required(VERSION 3.10)

# 프로젝트 이름과 버전 설정
project(GameEngine VERSION 1.0)

# C++ 표준 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 헤더 파일 디렉토리 포함
include_directories(include)

# SDL2 라이브러리 설정
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})

# SDL2_image 라이브러리 설정
find_package(SDL2_image REQUIRED)
include_directories(${SDL2_IMAGE_INCLUDE_DIRS})

# SDL2_ttf 라이브러리 설정
find_package(SDL2_ttf REQUIRED)
include_directories(${SDL2_TTF_INCLUDE_DIRS})

# Lua 라이브러리 설정
set(LUA_INCLUDE_DIR path/to/lua/include)
set(LUA_LIBRARY path/to/lua/liblua.a)
include_directories(${LUA_INCLUDE_DIR})
link_directories(${LUA_LIBRARY})

# 소스 파일 설정
file(GLOB SOURCES "src/*.cpp")

# 실행 파일 생성
add_executable(GameEngine ${SOURCES})

# SDL2 및 SDL2_image, SDL2_ttf, Lua 라이브러리 링크
target_link_libraries(GameEngine ${SDL2_LIBRARIES} ${SDL2_IMAGE_LIBRARIES} ${SDL2_TTF_LIBRARIES} ${LUA_LIBRARY})

2. Lua 통합 클래스 설계

Lua 스크립트를 관리하기 위한 LuaManager 클래스를 설계합니다.

 

헤더 파일 작성

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

#ifndef LUAMANAGER_H
#define LUAMANAGER_H

extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
#include <string>

class LuaManager {
public:
    LuaManager();
    ~LuaManager();

    bool Initialize();
    void Shutdown();

    bool LoadScript(const std::string& scriptPath);
    bool ExecuteFunction(const std::string& functionName, int arg);

private:
    lua_State* L;
};

#endif // LUAMANAGER_H

 

소스 파일 작성

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

#include "LuaManager.h"
#include <iostream>

LuaManager::LuaManager() : L(nullptr) {}

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

bool LuaManager::Initialize() {
    L = luaL_newstate();
    if (L == nullptr) {
        std::cerr << "Failed to create Lua state" << std::endl;
        return false;
    }

    luaL_openlibs(L);
    return true;
}

void LuaManager::Shutdown() {
    if (L) {
        lua_close(L);
        L = nullptr;
    }
}

bool LuaManager::LoadScript(const std::string& scriptPath) {
    if (luaL_loadfile(L, scriptPath.c_str()) || lua_pcall(L, 0, 0, 0)) {
        std::cerr << "Failed to load Lua script: " << lua_tostring(L, -1) << std::endl;
        return false;
    }
    return true;
}

bool LuaManager::ExecuteFunction(const std::string& functionName, int arg) {
    lua_getglobal(L, functionName.c_str());
    if (!lua_isfunction(L, -1)) {
        std::cerr << "Function " << functionName << " not found" << std::endl;
        return false;
    }

    lua_pushnumber(L, arg);
    if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
        std::cerr << "Failed to execute function " << functionName << ": " << lua_tostring(L, -1) << std::endl;
        return false;
    }
    return true;
}

3. 게임 엔진에 Lua 시스템 추가

이제 게임 엔진에 Lua 시스템을 통합하고, 스크립트를 로드하고 실행하도록 수정합니다.

 

헤더 파일 수정

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

#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 "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;
    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;

GameEngine::GameEngine()
    : window(nullptr), glContext(nullptr), renderer(nullptr), shaderProgram(0), 
      particleSystem(nullptr), uiManager(nullptr), luaManager(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.glsl", "shaders/fragment_shader.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");

    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;
                default:
                    break;
            }
        }
    }
}

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

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

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 = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -3.0f));
    glm::mat4 projection = glm::perspective(glm::radians(45.0f), (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));

    particleSystem->Render();

    // SDL 렌더러 사용하여 UI 렌더링
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(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 (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();
}

4. Lua 스크립트 작성

Lua 스크립트를 작성하여 게임 로직을 정의합니다.

 

Lua 스크립트 작성

script.lua 파일을 생성하고 다음과 같이 작성합니다.

function update(deltaTime)
    print("Update called with deltaTime: " .. deltaTime)
    -- 게임 로직을 여기에 추가합니다.
end

5. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. Lua 라이브러리 설치:
    • Lua 라이브러리를 프로젝트에 설정합니다.
  3. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  4. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, Lua 스크립트가 실행되어 출력 결과를 확인할 수 있습니다.

마무리

오늘은 Lua를 사용하여 스크립팅 시스템을 구현하고, 게임 로직을 스크립트로 작성하는 방법을 학습했습니다. 이를 통해 게임 로직을 유연하게 변경하고 확장할 수 있었습니다. 다음 단계에서는 리소스 관리 시스템을 구현하여 게임 리소스를 효율적으로 관리하는 방법을 배워보겠습니다.

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

Day 20 예고

다음 날은 "리소스 관리 시스템 구현"에 대해 다룰 것입니다. 게임 리소스를 효율적으로 관리하고, 메모리 사용을 최적화하는 방법을 배워보겠습니다.

반응형