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

[C++로 배우는 게임 엔진 개발] Day 17: 입자 시스템 구현

by cogito21_cpp 2024. 8. 1.
반응형

입자 시스템 구현

오늘은 OpenGL과 GLSL을 사용하여 입자 시스템을 구현하고, 다양한 시각 효과를 만드는 방법을 학습하겠습니다. 입자 시스템은 폭발, 연기, 불꽃, 눈, 비 등 다양한 시각 효과를 표현하는 데 사용됩니다.

1. 입자 시스템의 개요

입자 시스템은 작은 입자들로 구성되어 특정한 규칙에 따라 움직이는 시스템입니다. 각 입자는 위치, 속도, 색상 등의 속성을 가지며, 시간에 따라 변화합니다.

2. 입자 클래스 설계

먼저 입자를 관리하기 위한 Particle 클래스를 설계합니다.

 

헤더 파일 작성

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

#ifndef PARTICLE_H
#define PARTICLE_H

#include <glm/glm.hpp>

struct Particle {
    glm::vec3 position;
    glm::vec3 velocity;
    glm::vec4 color;
    float life;

    Particle()
        : position(0.0f), velocity(0.0f), color(1.0f), life(0.0f) {}
};

#endif // PARTICLE_H

3. 입자 시스템 클래스 설계

입자들을 관리하고 업데이트하는 ParticleSystem 클래스를 설계합니다.

 

헤더 파일 작성

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

#ifndef PARTICLESYSTEM_H
#define PARTICLESYSTEM_H

#include <vector>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include "Particle.h"

class ParticleSystem {
public:
    ParticleSystem(unsigned int maxParticles);

    void Update(float deltaTime);
    void Render();

private:
    std::vector<Particle> particles;
    unsigned int maxParticles;
    unsigned int lastUsedParticle;

    GLuint VAO, VBO;

    unsigned int FirstUnusedParticle();
    void RespawnParticle(Particle& particle);

    void Initialize();
};

#endif // PARTICLESYSTEM_H

 

소스 파일 작성

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

#include "ParticleSystem.h"
#include <iostream>
#include <algorithm>

ParticleSystem::ParticleSystem(unsigned int maxParticles)
    : maxParticles(maxParticles), lastUsedParticle(0) {
    particles.resize(maxParticles);
    Initialize();
}

void ParticleSystem::Initialize() {
    float particleQuad[] = {
        -0.05f, -0.05f, 0.0f,
         0.05f, -0.05f, 0.0f,
        -0.05f,  0.05f, 0.0f,
         0.05f,  0.05f, 0.0f,
    };

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(particleQuad), particleQuad, GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

unsigned int ParticleSystem::FirstUnusedParticle() {
    for (unsigned int i = lastUsedParticle; i < maxParticles; ++i) {
        if (particles[i].life <= 0.0f) {
            lastUsedParticle = i;
            return i;
        }
    }
    for (unsigned int i = 0; i < lastUsedParticle; ++i) {
        if (particles[i].life <= 0.0f) {
            lastUsedParticle = i;
            return i;
        }
    }
    lastUsedParticle = 0;
    return 0;
}

void ParticleSystem::RespawnParticle(Particle& particle) {
    float random = ((rand() % 100) - 50) / 10.0f;
    float rColor = 0.5 + ((rand() % 100) / 100.0f);
    particle.position = glm::vec3(random, random, 0.0f);
    particle.color = glm::vec4(rColor, rColor, rColor, 1.0f);
    particle.life = 1.0f;
    particle.velocity = glm::vec3(0.0f, 0.1f, 0.0f);
}

void ParticleSystem::Update(float deltaTime) {
    for (unsigned int i = 0; i < maxParticles; ++i) {
        Particle& p = particles[i];
        p.life -= deltaTime;
        if (p.life > 0.0f) {
            p.position -= p.velocity * deltaTime;
            p.color.a -= deltaTime * 2.5f;
        }
    }
    for (unsigned int i = 0; i < 2; ++i) {
        int unusedParticle = FirstUnusedParticle();
        RespawnParticle(particles[unusedParticle]);
    }
}

void ParticleSystem::Render() {
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glBindVertexArray(VAO);
    for (Particle particle : particles) {
        if (particle.life > 0.0f) {
            glColor4f(particle.color.r, particle.color.g, particle.color.b, particle.color.a);
            glTranslatef(particle.position.x, particle.position.y, particle.position.z);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glTranslatef(-particle.position.x, -particle.position.y, -particle.position.z);
        }
    }
    glBindVertexArray(0);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

4. 게임 엔진에 입자 시스템 추가

이제 게임 엔진에 입자 시스템을 통합하고, 입자 효과를 렌더링하도록 수정합니다.

 

헤더 파일 수정

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

#ifndef GAMEENGINE_H
#define GAMEENGINE_H

#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include "ParticleSystem.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;
    GLuint shaderProgram;
    ParticleSystem* particleSystem;
    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), shaderProgram(0), particleSystem(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;
    }

    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);

    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);
}

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_GL_SwapWindow(window);
}

void GameEngine::Shutdown() {
    if (particleSystem) {
        delete particleSystem;
        particleSystem = nullptr;
    }
    if (shaderProgram) {
        glDeleteProgram(shaderProgram);
        shaderProgram = 0;
    }
    if (glContext) {
        SDL_GL_DeleteContext(glContext);
        glContext = nullptr;
    }
    if (window) {
        SDL_DestroyWindow(window);
        window = nullptr;
    }
    SDL_Quit();
}

5. 프로젝트 빌드 및 실행

  1. Visual Studio에서 CMake 프로젝트 열기:
    • Visual Studio를 실행하고, File -> Open -> CMake...를 선택합니다.
    • GameEngine 디렉토리를 선택하여 프로젝트를 엽니다.
  2. 프로젝트 빌드:
    • Visual Studio 상단의 Build 메뉴에서 Build All을 선택하여 프로젝트를 빌드합니다.
  3. 프로젝트 실행:
    • Debug 메뉴에서 Start Without Debugging을 선택하여 프로그램을 실행합니다.
    • 윈도우 창이 생성되고, 입자 시스템이 화면에 렌더링됩니다.

마무리

오늘은 OpenGL과 GLSL을 사용하여 입자 시스템을 구현하고, 다양한 시각 효과를 만드는 방법을 학습했습니다. 입자 시스템을 통해 게임 내에서 생동감 있는 환경을 만들 수 있었습니다. 다음 단계에서는 간단한 UI 시스템을 구현하여 게임 내에서 사용자 인터페이스를 만드는 방법을 배워보겠습니다.

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

Day 18 예고

다음 날은 "간단한 UI 시스템 구현"에 대해 다룰 것입니다. 게임 내에서 사용자 인터페이스를 만들고, 다양한 UI 요소를 추가하는 방법을 배워보겠습니다.

반응형