네트워크 프로그래밍 기반의 뱀 게임 개발

서론

네트워크 기반 뱀 게임 프로젝트는 여러 기술적 도전을 극복하며 풍부한 성과를 거두었습니다. 기술적 관점에서, 네트워크 통신 프로토콜의 활용에 능숙해져 플레이어 간 실시간 상호작용을 구현했습니다. 정교하게 설계된 데이터 구조를 통해 뱀의 위치, 상태 및 게임 시나리오를 효율적으로 관리했습니다. 인터페이스 설계에서는 단순하고 직관적인 방식을 추구하여 플레이어에게 우수한 게임 경험을 제공했습니다. 프로젝트 과정에서 네트워크 지연, 동기화 문제를 해결하고, 지속적인 테스트와 최적화를 통해 게임의 안정성과 부드러움을 향상시켰습니다. 앞으로도 게임 기능을 지속적으로 개선하고, 더 많은 혁신적인 게임 플레이를 탐색하여 게임의 재미를 높여 더욱 훌륭한 뱀 게임 경험을 선사할 것입니다.

프로젝트 구현 흐름도

구현된 프로젝트의 주요 구성 요소와 데이터 흐름을 나타냅니다.

프로젝트 상세 설명

1. 서버 구현 로직

  • epoll을 사용하여 클라이언트의 연결 요청과 조작 요청을 감시하고 처리합니다.
  • 연결 요청인 경우, 게임 스레드를 생성하여 클라이언트에 지속적으로 게임 데이터 패킷을 전송합니다.
  • 조작 요청인 경우, 해당 클라이언트의 게임 데이터 패킷을 수정하고 다시 전송합니다.
  • 게임 종료 또는 클라이언트 프로그램 종료 시 연결을 해제하고 감시에서 제거합니다.

2. 클라이언트 구현 로직

  • 연결 성공 후 데이터 수신 스레드를 생성하여 지속적으로 데이터를 수신하고 처리하며, 메인 스레드는 사용자의 조작 요청을 전송합니다.
  • third-party 라이브러리 easyX를 사용하여 그래픽 인터페이스를 생성하고 오디오를 삽입합니다.

일부 코드 및 원리 설명

server.c

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include"GameServer.h"
#include"AVL.h"

// 서버 구현
AvlTree activePlayers; // 활성 플레이어 그룹
int playerCount = 0; // 연결된 클라이언트 수
pthread_mutex_t dataMutex;
//AvlTree doublePlayers;
//int doubleMemberNum = 0; // 2인용 게임 인원 수

void startServer(const char *port){
    int serverSocket; // 서버 연결 요청 소켓
    struct sockaddr_in serverAddress; // 연결 요청 소켓 주소 정보 저장
    socklen_t clientAddressSize; // 실제 사용되지 않음
    struct epoll_event *events; // 변경된 epoll_event 저장
    struct epoll_event event;
    GamePlayer tempPlayer = {0};

    activePlayers = MakeEmpty(activePlayers); // 초기화된 활성 플레이어 그룹
    pthread_mutex_init(&dataMutex, NULL);

    // 연결 처리 소켓 생성
    serverSocket = socket(PF_INET, SOCK_STREAM, 0);
    if(serverSocket < 0) handleError("socket() 실패");
    memset(&serverAddress, 0, sizeof(serverAddress));
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddress.sin_port = htons(atoi(port));
    
    // 소켓에 IP와 포트 번호 할당
    if(bind(serverSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress))) handleError("bind() 실패");
    printf("서버 바인딩 완료\n");
    if(listen(serverSocket, MAX_EVENTS)) handleError("listen() 실패");

    // epoll 인스턴스 생성
    int epollFd = epoll_create(MAX_EVENTS);
    events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * MAX_EVENTS);
    event.data.fd = serverSocket;
    event.events = EPOLLIN;
    epoll_ctl(epollFd, EPOLL_CTL_ADD, serverSocket, &event);

    // epoll 감시 시작
    int eventCount; // 변경된 소켓 디스크립터 수 저장
    while(1){
        eventCount = epoll_wait(epollFd, events, MAX_EVENTS, -1);
        if(eventCount == -1) handleError("epoll_wait() 실패");
        int i;
        for(i = 0; i < eventCount; i++){
            // 새 연결 요청
            if(events[i].data.fd == serverSocket){
                // 플레이어 정보 설정
                if(!initializePlayerInfo(&tempPlayer)){
                    continue;
                }
                // 연결 설정
                clientAddressSize = sizeof(tempPlayer.clientAddress);
                tempPlayer.clientSocket = accept(serverSocket, (struct sockaddr *)&tempPlayer.clientAddress, &clientAddressSize);
                // 새로운 클라이언트 소켓을 감시에 추가
                event.data.fd = tempPlayer.clientSocket;
                event.events = EPOLLIN;
                if(epoll_ctl(epollFd, EPOLL_CTL_ADD, tempPlayer.clientSocket, &event)) handleError("epoll_ctl() 실패");
                // 게임 스레드 생성
                pthread_create(&tempPlayer.threadId, NULL, gameHandler, (void *)tempPlayer.clientSocket);
                // 활성 플레이어 그룹에 추가
                activePlayers = Insert(activePlayers, tempPlayer);
                printf("클라이언트 %s 포트 %d 연결됨\n", inet_ntoa(tempPlayer.clientAddress.sin_addr), tempPlayer.clientAddress.sin_port);
                // 활성 플레이어 수 증가
                pthread_mutex_lock(&dataMutex);
                ++playerCount;
                pthread_mutex_unlock(&dataMutex);
                printf("현재 접속자 수: %d\n", playerCount);
                memset(&tempPlayer, 0, sizeof(GamePlayer));
            }
            else{// 클라이언트의 조작 요청
                // 플레이어 찾기
                Position playerNode= Find(activePlayers, events[i].data.fd);
                int inputKey, messageLength;
                messageLength = read(events[i].data.fd, &inputKey, sizeof(int));
                // 1. 클라이언트가 닫혔는지 확인
                if(!messageLength || messageLength == -1){    // 클라이언트 닫힘, 플레이어 온라인 상태 false로 표시
                    playerNode->player.isActive = false;
                    printf("클라이언트 %s 상태: 오프라인\n", inet_ntoa(playerNode->player.clientAddress.sin_addr));
                    epoll_ctl(epollFd, EPOLL_CTL_DEL, playerNode->player.clientSocket, NULL);
                    continue;
                }
                else{
                    playerNode->player.inputKey = inputKey;
                    printf("클라이언트 %s 조작: %d\n", inet_ntoa(playerNode->player.clientAddress.sin_addr), inputKey);
                } 
            }
        }
    }
    close(serverSocket);
    activePlayers = MakeEmpty(activePlayers);
    return;
}

// 게임 핸들러 스레드
void *gameHandler(void *arg){
    int clientSocket = (int)arg;
    // 플레이어 찾기
    Position currentPlayer = Find(activePlayers, clientSocket);
    // 게임 데이터 초기 프레임 전송
    prepareAndSend(currentPlayer->player.clientSocket, currentPlayer->player.gameMap, currentPlayer->player.snake, 
                currentPlayer->player.food, currentPlayer->player.score, currentPlayer->player.delayTime, false);
    // 게임 루프
    bool foodEaten;  // 음식 섭취 여부 확인
    while(1){
        foodEaten = false;
        // 플레이어 온라인 상태 확인
        if(!currentPlayer->player.isActive){ // 플레이어가 연결을 종료
            printf("클라이언트 %s 연결 종료\n", inet_ntoa(currentPlayer->player.clientAddress.sin_addr));
            releaseGameResources(currentPlayer->player.gameMap, currentPlayer->player.snake, currentPlayer->player.food); // 게임 리소스 해제
            activePlayers = Delete(activePlayers, currentPlayer->player.clientSocket); // 플레이어 그룹에서 제거
            pthread_mutex_lock(&dataMutex);
            --playerCount;
            pthread_mutex_unlock(&dataMutex);
            printf("현재 접속자 수: %d\n", playerCount);
            pthread_exit((void*)1); // 게임 스레드 종료
        }
        // 방향 변경 확인
        switch (currentPlayer->player.inputKey){
            case UP_KEY:
                updateSnakeDirection(currentPlayer->player.snake, UP);
                break;
            case DOWN_KEY:
                 updateSnakeDirection(currentPlayer->player.snake, DOWN);
                    break;
            case LEFT_KEY:
                 updateSnakeDirection(currentPlayer->player.snake, LEFT);
                 break;
            case RIGHT_KEY:
                 updateSnakeDirection(currentPlayer->player.snake, RIGHT);
                 break;
            default:
                break;
        }
        // 다음 프레임 데이터 생성
        updateGameMapAndSnake(currentPlayer->player.gameMap, currentPlayer->player.snake);
        // 음식 섭취 여부에 따라 다음 프레임 데이터 변경
        if (checkFoodCollision(currentPlayer->player.snake, currentPlayer->player.food)){
            foodEaten = true;
            growSnake(currentPlayer->player.gameMap, currentPlayer->player.snake);
            updateScore(currentPlayer->player.snake, &(currentPlayer->player.score));
            generateNewFood(currentPlayer->player.food, currentPlayer->player.snake);
            currentPlayer->player.gameMap[currentPlayer->player.food->foodX][currentPlayer->player.food->foodY] = 2;
        }
        if(checkGameOver(currentPlayer->player.snake)){
            int gameOverSignal = 6; // 게임 종료 신호
            write(currentPlayer->player.clientSocket, &gameOverSignal, sizeof(int));
            sleep(2);
            break;
        }
        // 데이터 패킹 및 전송
        updateDelayTime(currentPlayer->player.snake, &(currentPlayer->player.delayTime));
        prepareAndSend(currentPlayer->player.clientSocket, currentPlayer->player.gameMap, currentPlayer->player.snake, 
                    currentPlayer->player.food, currentPlayer->player.score, currentPlayer->player.delayTime, foodEaten);
        usleep(currentPlayer->player.delayTime * 1000);
    }
    printf("클라이언트 %s 게임 종료 및 연결 종료\n", inet_ntoa(currentPlayer->player.clientAddress.sin_addr));
    releaseGameResources(currentPlayer->player.gameMap, currentPlayer->player.snake, currentPlayer->player.food); // 게임 리소스 해제
    activePlayers = Delete(activePlayers, currentPlayer->player.clientSocket); // 플레이어 그룹에서 제거
    pthread_mutex_lock(&dataMutex);
    --playerCount;
    pthread_mutex_unlock(&dataMutex);
    printf("현재 접속자 수: %d\n", playerCount);
    return NULL;
}


bool prepareAndSend(int clientSocket, const int(*gameMap)[COL], const Snake *snake, const Food *food, int score, int delayTime, bool foodEaten) {
    // 게임 데이터 패킷
    struct{
        int mapData[ROW][COL];
        int headX;
        int headY;
        int score;
        int delay;
        bool foodConsumed;
    }gamePacket;

    // 게임 패킷 초기화
    memcpy(gamePacket.mapData, gameMap, sizeof(int) * ROW * COL);
    gamePacket.headX = snake->bodySegments[0]->x;
    gamePacket.headY = snake->bodySegments[0]->y;
    gamePacket.score = score;
    gamePacket.delay = delayTime;
    gamePacket.foodConsumed = foodEaten;

    int packetSize = sizeof(gamePacket);
    // 패킷 전송
    char packetData[packetSize + sizeof(int)];
    memcpy(packetData, &packetSize, sizeof(int));
    memcpy(packetData + sizeof(int), &gamePacket, packetSize);
    write(clientSocket, packetData, sizeof(packetData));
    return true;
}

void handleError(const char *errorMessage){
    fputs(errorMessage, stdout);
    fputc('\n', stdout);
    exit(1);
}

void releaseGameResources(int(*gameMap)[COL], Snake* snake, Food* food){
	free(gameMap);
	free(food);
    // snake의 bodySegments 배열 내 데이터 먼저 해제
    int i;
	for (i = 0; i < snake->length; i++) {
		free(snake->bodySegments[i]);
	}
	free(snake);
}


bool initializePlayerInfo(GamePlayer *player){
    player->isActive = true; // 플레이어 온라인 상태 표시
    player->gameMap = (int(*)[COL])malloc(sizeof(int) * ROW * COL);
    if(!player->gameMap) return false;
    player->snake = (Snake *)malloc(sizeof(Snake));
    if(!player->snake) return false;
    player->food = (Food *)malloc(sizeof(Food));
    if(!player->food) return false;
    memset(player->gameMap, 0, sizeof(int) * ROW * COL);
    player->delayTime = 300;
    player->score = 0;
    player->inputKey = RIGHT_KEY;
    // 2. 게임 데이터 초기화
	bool initializationSuccess = setupGameData(player->gameMap, player->snake, player->food);
	if (!initializationSuccess){
        releaseGameResources(player->gameMap, player->snake, player->food);
		printf("게임 초기화 실패\n");
        return false;
	}
    return true;
}

client.c

#include<winsock.h>
#include<stdlib.h>
#include<stdio.h>
#include<conio.h>
#include "GameClient.h"
#include"SnakeClient.h"

void showError(const char* errorMessage);
HANDLE receiveAndProcess(LPVOID lpParameter);

bool connectToServer(const char *serverAddr, const char *port){
	
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData))
		showError("WSAStartup() 실패");
	SOCKET clientSocket;
	SOCKADDR_IN serverAddress;
	// 소켓 생성
	clientSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET) showError("socket() 실패");
	// 목적지 IP와 포트 번호 초기화
	memset(&serverAddress, 0, sizeof(serverAddress));
	serverAddress.sin_family = AF_INET;
	serverAddress.sin_addr.s_addr = inet_addr(serverAddr);
	serverAddress.sin_port = htons(atoi(port));
	// 연결 요청
	if (connect(clientSocket, (SOCKADDR*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR)
		showError("connect() 실패");
    puts("서버 연결 성공");
    
    // 데이터 패킷 수신 및 처리
    GameOperation opObj;
    opObj.clientSocket = &clientSocket;
    opObj.isActive = true;
    HANDLE receiverThread = CreateThread(NULL, NULL, (PTHREAD_START_ROUTINE)receiveAndProcess, (LPVOID)&opObj, NULL, NULL);
    
    // 클라이언트 조작 전송
    while (1) {
        if (_kbhit()) {   // 키 입력 감지
            int inputKey = _getch();
            if (inputKey == UP_KEY || inputKey == DOWN_KEY || inputKey == LEFT_KEY
                || inputKey == RIGHT_KEY) {
                send(clientSocket, (char*)&inputKey, sizeof(int), 0);
            }
        }
        if (!opObj.isActive) break;
    }
    WaitForSingleObject(receiverThread, 0);
    _getch();
    showMenu();
    return true;
}

HANDLE receiveAndProcess(LPVOID lpParameter){
    GameOperation* opObj = (GameOperation*)lpParameter;
    int currentScore = 0;
    int messageSize;  // 게임 패킷 크기
    char* packetData;  // 게임 패킷 데이터 저장
	int receivedLength;
    while (1) {
        // 패킷 크기 수신
        receivedLength = recv(*opObj->clientSocket, (char*)&messageSize, sizeof(int), 0);
        if (messageSize == 6 || !receivedLength ||receivedLength == -1) {
            opObj->isActive = false;
			closesocket(*(opObj->clientSocket));
            showGameOverScene(currentScore);
            return NULL;
        }
        packetData = (char*)malloc(messageSize);
        if (!packetData) return (LPVOID)-1;
        // TCP는 데이터 경계가 없으므로 모든 데이터 수신까지 반복
        int totalReceived = 0, receiveCount;
        while (totalReceived < messageSize) {
            receiveCount = recv(*opObj->clientSocket, packetData + totalReceived, messageSize - totalReceived, 0);
            totalReceived += receiveCount;
        }
        struct {
            int mapData[ROW][COL];
            int headX;
            int headY;
            int score;
            int delay;
            bool foodEaten;
        }gameData;
        memcpy(&gameData, packetData, messageSize);
        updateGameScene(gameData.mapData, gameData.headX, gameData.headY, gameData.score, gameData.foodEaten);
        currentScore = gameData.score;
        free(packetData);
    }
    return NULL;
}

void showError(const char* errorMessage){
	fputs(errorMessage, stdout);
	fputc('\n', stdout);
	exit(1);
}

결과물

1. 서버 시작, 다중 클라이언트 연결

서버를 시작하면 여러 클라이언트가 연결할 수 있으며, 연결 성공 후 초기 화면으로 진입하고 중독성 있는 배경음악이 재생됩니다.

2. 조작 방법: W(위), A(왼쪽), S(아래), D(오른쪽)

W, A, S, D 키를 사용하여 뱀의 이동을 제어하며, 서버에 실시간으로 데이터가 표시됩니다.

3. 벽이나 자신과 충돌 시 게임 오버

벽이나 자신의 몸과 충돌하면 게임 오버가 발생하고 점수 화면으로 전환되며, 배경음악이 변경됩니다.

태그: C언어 네트워크 프로그래밍 epoll 멀티스레딩 게임 개발

7월 16일 19:53에 게시됨