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

[C++ 게임 개발 시리즈] Day 27: 멀티플레이어 게임 개발 기초

by cogito21_cpp 2024. 8. 1.
반응형

멀티플레이어 게임 개발 기초

멀티플레이어 게임은 네트워크를 통해 여러 플레이어가 동시에 게임을 즐길 수 있게 합니다. 오늘은 멀티플레이어 게임의 기초를 학습하고, 간단한 네트워크 동기화를 구현해 보겠습니다.

네트워크 동기화 기초

멀티플레이어 게임에서는 네트워크를 통해 플레이어 간의 상태를 동기화해야 합니다. 이를 위해 클라이언트-서버 모델을 사용합니다. 서버는 게임 상태를 관리하고, 클라이언트는 플레이어의 입력을 서버로 전송하여 게임 상태를 업데이트합니다.

네트워크 라이브러리 설정

네트워크 프로그래밍을 위해 Boost.Asio 라이브러리를 사용하겠습니다. Boost.Asio는 비동기 입출력 기능을 제공하여 네트워크 프로그래밍을 쉽게 할 수 있게 해줍니다.

Boost.Asio 설치 및 설정

  1. Boost 설치:
    • Boost 공식 사이트에서 Boost 라이브러리를 다운로드하거나 패키지 관리자를 통해 설치합니다.
  2. CMake 설정:
    • CMakeLists.txt 파일을 수정하여 Boost.Asio를 포함합니다.
cmake_minimum_required(VERSION 3.10)
project(3DGameProject)

set(CMAKE_CXX_STANDARD 11)

find_package(OpenGL REQUIRED)
find_package(GLEW REQUIRED)
find_package(glfw3 REQUIRED)
find_package(Boost REQUIRED COMPONENTS system)

include_directories(${OPENGL_INCLUDE_DIRS} ${GLEW_INCLUDE_DIRS} ${GLFW_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} include)

add_executable(3DGameProject src/main.cpp src/Shader.cpp src/Server.cpp src/Client.cpp)

target_link_libraries(3DGameProject ${OPENGL_LIBRARIES} ${GLEW_LIBRARIES} glfw ${Boost_LIBRARIES})

서버 구현

서버는 클라이언트의 연결을 받아들이고, 클라이언트 간의 상태를 동기화합니다.

 

Server.h

#ifndef SERVER_H
#define SERVER_H

#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <vector>

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

class Server {
public:
    Server(boost::asio::io_service& io_service, short port)
        : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)), socket_(io_service) {
        do_accept();
    }

private:
    void do_accept() {
        acceptor_.async_accept(socket_, [this](boost::system::error_code ec) {
            if (!ec) {
                std::make_shared<Session>(std::move(socket_))->start();
            }
            do_accept();
        });
    }

    tcp::acceptor acceptor_;
    tcp::socket socket_;
};

class Session : public std::enable_shared_from_this<Session> {
public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}

    void start() {
        do_read();
    }

private:
    void do_read() {
        auto self(shared_from_this());
        boost::asio::async_read(socket_, boost::asio::buffer(data_, max_length), [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                do_write(length);
            }
        });
    }

    void do_write(std::size_t length) {
        auto self(shared_from_this());
        boost::asio::async_write(socket_, boost::asio::buffer(data_, length), [this, self](boost::system::error_code ec, std::size_t /*length*/) {
            if (!ec) {
                do_read();
            }
        });
    }

    tcp::socket socket_;
    enum { max_length = 1024 };
    char data_[max_length];
};

#endif

클라이언트 구현

클라이언트는 서버에 연결하여 플레이어의 상태를 전송하고, 서버로부터 다른 플레이어의 상태를 수신합니다.

Client.h

#ifndef CLIENT_H
#define CLIENT_H

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

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]() {
            auto self(shared_from_this());
            boost::asio::async_write(socket_, boost::asio::buffer(msg.data(), msg.size()), [this, self](boost::system::error_code ec, std::size_t /*length*/) {
                if (!ec) {
                    do_read();
                }
            });
        });
    }

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() {
        auto self(shared_from_this());
        boost::asio::async_read(socket_, boost::asio::buffer(data_, max_length), [this, self](boost::system::error_code ec, std::size_t length) {
            if (!ec) {
                std::cout << "Server: " << std::string(data_, length) << std::endl;
                do_read();
            }
        });
    }

    tcp::socket socket_;
    enum { max_length = 1024 };
    char data_[max_length];
};

#endif

 

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;

// 콜백 함수
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);
        }

        // 화면 지우기
        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 28: 네트워크 동기화와 지연 처리"에 대해 학습하겠습니다.

반응형