반응형
네트워크 동기화와 지연 처리
멀티플레이어 게임에서 네트워크 지연(Latency)은 플레이어 경험에 큰 영향을 미칩니다. 오늘은 네트워크 동기화와 지연 처리 기법을 학습하여 네트워크 지연을 최소화하고, 플레이어 간의 동기화를 유지하는 방법을 살펴보겠습니다.
네트워크 지연과 동기화 문제
네트워크 지연은 패킷이 서버와 클라이언트 간에 전송되는 데 걸리는 시간입니다. 지연 시간이 길어지면 플레이어의 동작이 지연되어 게임 플레이가 부드럽지 않게 됩니다. 이를 해결하기 위해 다양한 동기화 기법을 사용합니다.
지연 처리 기법
- 예측 기법(Prediction):
- 클라이언트는 자신의 동작을 예측하여 미리 반영합니다. 서버로부터 실제 데이터가 도착하면 보정합니다.
- 보간(Interpolation):
- 클라이언트는 이전 상태와 현재 상태를 보간하여 중간 상태를 계산합니다. 이를 통해 부드러운 움직임을 유지합니다.
- 시간 동기화(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: 게임 최적화 기법"에 대해 학습하겠습니다.
반응형
'-----ETC----- > C++ 게임 개발 시리즈' 카테고리의 다른 글
[C++ 게임 개발 시리즈] Day 30: 게임 배포와 커뮤니티 관리 (0) | 2024.08.01 |
---|---|
[C++ 게임 개발 시리즈] Day 27: 멀티플레이어 게임 개발 기초 (0) | 2024.08.01 |
[C++ 게임 개발 시리즈] Day 25: 3D 게임 프로젝트 시작 (2) (0) | 2024.08.01 |
[C++ 게임 개발 시리즈] Day 26: 3D 게임 프로젝트 시작 (3) (0) | 2024.08.01 |
[C++ 게임 개발 시리즈] Day 23: 3D 모델링과 텍스처링 (0) | 2024.08.01 |