본문 바로가기
-----ETC-----/C++ 게임 개발 시리즈

[C++ 게임 개발 시리즈] Day 28: 네트워크 동기화와 지연 처리

by cogito21_cpp 2024. 8. 1.
반응형

네트워크 동기화와 지연 처리

멀티플레이어 게임에서 네트워크 지연(Latency)은 플레이어 경험에 큰 영향을 미칩니다. 오늘은 네트워크 동기화와 지연 처리 기법을 학습하여 네트워크 지연을 최소화하고, 플레이어 간의 동기화를 유지하는 방법을 살펴보겠습니다.

네트워크 지연과 동기화 문제

네트워크 지연은 패킷이 서버와 클라이언트 간에 전송되는 데 걸리는 시간입니다. 지연 시간이 길어지면 플레이어의 동작이 지연되어 게임 플레이가 부드럽지 않게 됩니다. 이를 해결하기 위해 다양한 동기화 기법을 사용합니다.

지연 처리 기법

  1. 예측 기법(Prediction):
    • 클라이언트는 자신의 동작을 예측하여 미리 반영합니다. 서버로부터 실제 데이터가 도착하면 보정합니다.
  2. 보간(Interpolation):
    • 클라이언트는 이전 상태와 현재 상태를 보간하여 중간 상태를 계산합니다. 이를 통해 부드러운 움직임을 유지합니다.
  3. 시간 동기화(Time Synchronization):
    • 서버와 클라이언트 간의 시간을 동기화하여 지연 시간을 보정합니다.

예측 기법 구현

클라이언트 측에서 예측 기법을 구현하여 플레이어의 동작을 미리 반영하고, 서버로부터 데이터가 도착하면 보정합니다.

 

Client.h 수정

#ifndef CLIENT_H
#define CLIENT_H

#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>

using boost::asio::ip::tcp;

class Client {
public:
    Client(boost::asio::io_service& io_service, const std::string& host, const std::string& port)
        : socket_(io_service) {
        tcp::resolver resolver(io_service);
        auto endpoints = resolver.resolve(host, port);
        do_connect(endpoints);
    }

    void write(const std::string& msg) {
        boost::asio::post(socket_.get_io_service(), [this, msg]() {
            bool write_in_progress = !write_msgs_.empty();
            write_msgs_.push(msg);
            if (!write_in_progress) {
                do_write();
            }
        });
    }

    std::string read() {
        std::lock_guard<std::mutex> lock(read_mutex_);
        if (!read_msgs_.empty()) {
            std::string msg = read_msgs_.front();
            read_msgs_.pop();
            return msg;
        }
        return "";
    }

private:
    void do_connect(const tcp::resolver::results_type& endpoints) {
        boost::asio::async_connect(socket_, endpoints, [this](boost::system::error_code ec, tcp::endpoint) {
            if (!ec) {
                do_read();
            }
        });
    }

    void do_read() {
        boost::asio::async_read(socket_, boost::asio::buffer(read_msg_, max_length), [this](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                std::lock_guard<std::mutex> lock(read_mutex_);
                read_msgs_.push(std::string(read_msg_, length));
                do_read();
            }
        });
    }

    void do_write() {
        boost::asio::async_write(socket_, boost::asio::buffer(write_msgs_.front().data(), write_msgs_.front().size()), [this](boost::system::error_code ec, std::size_t /*length*/) {
            if (!ec) {
                std::lock_guard<std::mutex> lock(write_mutex_);
                write_msgs_.pop();
                if (!write_msgs_.empty()) {
                    do_write();
                }
            }
        });
    }

    tcp::socket socket_;
    enum { max_length = 1024 };
    char read_msg_[max_length];
    std::queue<std::string> read_msgs_;
    std::queue<std::string> write_msgs_;
    std::mutex read_mutex_;
    std::mutex write_mutex_;
};

#endif

 

보간 기법 구현

클라이언트 측에서 보간 기법을 사용하여 플레이어의 움직임을 부드럽게 표현합니다.

 

보간 함수

glm::vec3 interpolate(const glm::vec3& start, const glm::vec3& end, float alpha) {
    return start + alpha * (end - start);
}

 

main.cpp 수정

main.cpp 파일을 수정하여 예측 기법과 보간 기법을 적용합니다.

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <boost/asio.hpp>
#include "Shader.h"
#include "Camera.h"
#include "Enemy.h"
#include "Server.h"
#include "Client.h"

// 정점 셰이더 소스 코드
const char* vertexShaderSource = "path/to/vertex_shader.glsl";
const char* fragmentShaderSource = "path/to/fragment_shader.glsl";

// 카메라 설정
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = 400, lastY = 300;
bool firstMouse = true;

// 시간 설정
float deltaTime = 0.0f;
float lastFrame = 0.0f;

// 적 캐릭터 생성
Enemy enemy(glm::vec3(0.0f, 0.0f, -5.0f), 1.0f);

// 네트워크 설정
boost::asio::io_service io_service;
std::shared_ptr<Server> server;
std::shared_ptr<Client> client;

// 이전 상태와 현재 상태
glm::vec3 previousPosition;
glm::vec3 currentPosition;

// 콜백 함수
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
bool checkCollision(glm::vec3 pos1, glm::vec3 pos2, float radius);

int main(int argc, char* argv[]) {
    // 서버 또는 클라이언트 모드 설정
    bool isServer = false;
    if (argc > 1) {
        isServer = std::string(argv[1]) == "server";
    }

    if (isServer) {
        server = std::make_shared<Server>(io_service, 12345);
        std::thread serverThread([]() { io_service.run(); });
        serverThread.detach();
    } else {
        client = std::make_shared<Client>(io_service, "localhost", "12345");
        std::thread clientThread([]() { io_service.run(); });
        clientThread.detach();
    }

    // GLFW 초기화
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }

    // OpenGL 버전 설정 (3.3)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 창 생성
    GLFWwindow* window = glfwCreateWindow(800, 600, "3D Game Project", NULL, NULL);
    if (!window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // 마우스 캡처
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // GLEW 초기화
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        return -1;
    }

    // 셰이더 프로그램 생성
    Shader ourShader(vertexShaderSource, fragmentShaderSource);

    // 정점 데이터
    float vertices[] = {
        // 위치           // 법선           // 텍스처 좌표
         0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  0.0f,  0.

0f, -1.0f,  1.0f, 0.0f,

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

    unsigned int indices[] = {
        0, 1, 2, 2, 3, 0,  // 앞
        4, 5, 6, 6, 7, 4,  // 뒤
        0, 1, 5, 5, 4, 0,  // 위
        2, 3, 7, 7, 6, 2,  // 아래
        1, 2, 6, 6, 5, 1,  // 왼쪽
        0, 3, 7, 7, 4, 0   // 오른쪽
    };

    GLuint VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

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

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // 텍스처 로딩
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    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;
    unsigned char* image = SOIL_load_image("path/to/your/texture.png", &width, &height, 0, SOIL_LOAD_RGB);
    if (image) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
        glGenerateMipmap(GL_TEXTURE_2D);
    } else {
        std::cerr << "Failed to load texture" << std::endl;
    }
    SOIL_free_image_data(image);
    glBindTexture(GL_TEXTURE_2D, 0);

    // OpenGL 상태 설정
    glEnable(GL_DEPTH_TEST);

    // 조명 설정
    glm::vec3 lightDir(0.2f, 1.0f, 0.3f);
    glm::vec3 lightColor(1.0f, 1.0f, 1.0f);

    // 메인 루프
    while (!glfwWindowShouldClose(window)) {
        // 시간 계산
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // 입력 처리
        processInput(window);

        // 적 캐릭터 이동
        enemy.moveTowards(camera.Position, deltaTime);

        // 적 캐릭터 공격 범위 확인
        if (enemy.isWithinAttackRange(camera.Position, 1.0f)) {
            std::cout << "Enemy is attacking!" << std::endl;
        }

        // 충돌 감지
        if (checkCollision(camera.Position, enemy.Position, 0.5f)) {
            std::cout << "Collision detected!" << std::endl;
        }

        // 클라이언트 상태 전송
        if (client) {
            std::string msg = "Player position: " + std::to_string(camera.Position.x) + " " + std::to_string(camera.Position.y) + " " + std::to_string(camera.Position.z);
            client->write(msg);
        }

        // 서버로부터 데이터 수신
        if (client) {
            std::string serverMsg = client->read();
            if (!serverMsg.empty()) {
                std::istringstream iss(serverMsg);
                std::string prefix;
                float x, y, z;
                iss >> prefix >> x >> y >> z;
                previousPosition = currentPosition;
                currentPosition = glm::vec3(x, y, z);
            }
        }

        // 보간하여 위치 업데이트
        float alpha = 0.1f; // 보간 비율
        glm::vec3 interpolatedPosition = interpolate(previousPosition, currentPosition, alpha);
        camera.Position = interpolatedPosition;

        // 화면 지우기
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 셰이더 프로그램 사용
        ourShader.use();

        // 카메라 변환 행렬 설정
        glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), 800.0f / 600.0f, 0.1f, 100.0f);
        glm::mat4 view = camera.GetViewMatrix();

        GLint projectionLoc = glGetUniformLocation(ourShader.Program, "projection");
        GLint viewLoc = glGetUniformLocation(ourShader.Program, "view");

        glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));

        // 모델 변환 행렬 설정
        glm::mat4 model = glm::mat4(1.0f);
        GLint modelLoc = glGetUniformLocation(ourShader.Program, "model");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

        // 조명 설정
        glUniform3f(glGetUniformLocation(ourShader.Program, "lightDir"), lightDir.x, lightDir.y, lightDir.z);
        glUniform3f(glGetUniformLocation(ourShader.Program, "lightColor"), lightColor.r, lightColor.g, lightColor.b);
        glUniform3f(glGetUniformLocation(ourShader.Program, "objectColor"), 1.0f, 0.5f, 0.31f);

        // 텍스처 바인딩
        glBindTexture(GL_TEXTURE_2D, texture);

        // 객체 그리기
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        // 적 캐릭터 그리기
        model = glm::translate(glm::mat4(1.0f), enemy.Position);
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

        // 버퍼 교환
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 자원 해제
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

    glfwTerminate();
    return 0;
}

// 창 크기 변경 시 호출되는 콜백 함수
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

// 마우스 이동 시 호출되는 콜백 함수
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if (firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

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

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// 마우스 휠 사용 시 호출되는 콜백 함수
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    camera.ProcessMouseScroll(yoffset

);
}

// 키보드 입력 처리
void processInput(GLFWwindow *window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

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

// 충돌 감지 함수
bool checkCollision(glm::vec3 pos1, glm::vec3 pos2, float radius) {
    return glm::distance(pos1, pos2) < radius;
}

 

결론

오늘은 네트워크 동기화와 지연 처리 기법을 학습하고, 예측 기법과 보간 기법을 구현했습니다. 이를 통해 멀티플레이어 게임에서 네트워크 지연을 최소화하고, 플레이어 간의 동기화를 유지할 수 있었습니다. 질문이나 추가적인 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "Day 29: 게임 최적화 기법"에 대해 학습하겠습니다.

반응형