基于网络编程的贪吃蛇大作战小游戏

前言

在贪吃蛇网络编程项目中,面临了诸多挑战,也收获了丰富的成果。从技术角度看,熟练掌握了网络通信协议的运用,实现了玩家之间的实时交互。通过精心设计的数据结构,高效地管理蛇的位置、状态和游戏场景。在界面设计上,力求简洁直观,为玩家提供良好的游戏体验。项目过程中,解决了网络延迟、同步等问题。不断进行测试和优化,提高了游戏的稳定性和流畅性。同时,也会不断改进游戏功能。未来,将继续探索更多创新玩法,提升游戏的趣味性,为玩家带来更加精彩的贪吃蛇游戏体验。相关展示在视频处。

一、项目实现流程图

 二、项目详解

1.服务端实现逻辑

        a.通过epoll监视客户端的连接请求和操作请求信息,并做相处理;

        b.如果为连接请求,则创建游戏线程不断给客户端发送游戏数据包;

        c.如果为操作请求,则根据请求对相应客户端的游戏数据包做相应修改,再发送给对应的客户端;

        d.如果游戏结束或客户端直接关闭程序则断开连接并移除监视。

2.客户端实现逻辑

        a.连接成功后创建数据接收线程,不断接收数据并处理,主线程相互间发送用户的操作请求;

        b.通过使用第三方库easyX创建图形界面,并插入音频;

三、部分代码及原理展示

server.c

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

//服务器端
AvlTree onlinePlayers; //玩家组
int onlineMemberNum = 0; //记录连接的客户端数量
pthread_mutex_t mutex;
//AvlTree doublePlayers;
//int doubleMemberNum = 0; //记录双人游戏人数

void serve(const char *port){
    int serSock; //服务器端连接请求套接字
    struct sockaddr_in serAddr; //用来分配连接请求套接字地址信息
    socklen_t cltAddrSize; //没有实际作用
    struct epoll_event *events; //保存发生变化的epoll_event
    struct epoll_event event;
    Player tmpPlayer = {0};

    onlinePlayers = MakeEmpty(onlinePlayers); //初始化在线玩家组,暂时无玩家
    pthread_mutex_init(&mutex, NULL);


    //创建处理连接请求socket
    serSock = socket(PF_INET, SOCK_STREAM, 0);
    if(serSock < 0) errhanding("socker() fail");
    memset(&serAddr, 0, sizeof(serAddr));
    serAddr.sin_family = AF_INET;
    serAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serAddr.sin_port = htons(atoi(port));
    
    //给处理连接请求socket分配IP和端口号
    if(bind(serSock, (struct sockaddr *)&serAddr, sizeof(serAddr))) errhanding("bind() fail");
    printf("bind:%m\n");
    if(listen(serSock, EPOLL_SIZE)) errhanding("listen() fail");

    //创建epoll例程
    int epollFd = epoll_create(EPOLL_SIZE);
    events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    event.data.fd = serSock;
    event.events = EPOLLIN;
    epoll_ctl(epollFd, EPOLL_CTL_ADD, serSock, &event);

    //epoll监视
    int eventNum; //保存发生变化的套接字描述符的数量
    while(1){
        eventNum = epoll_wait(epollFd, events, EPOLL_SIZE, -1);
        if(eventNum == -1) errhanding("epoll_wait() fail");
        int i;
        for(i = 0; i < eventNum; i++){
            //新的连接请求
            if(events[i].data.fd == serSock){
                //配置玩家信息
                if(!initPlyerInfo(&tmpPlayer)){
                    continue;
                }
                //建立连接
                cltAddrSize = sizeof(tmpPlayer.cltAddr);
                tmpPlayer.cltSock = accept(serSock, (struct sockaddr *)&tmpPlayer.cltAddr, &cltAddrSize);
                //将新的cltSock加入监视
                event.data.fd = tmpPlayer.cltSock;
                event.events = EPOLLIN;
                if(epoll_ctl(epollFd, EPOLL_CTL_ADD, tmpPlayer.cltSock, &event)) errhanding("epoll_ctl() fail");
                //创建游戏线程
                pthread_create(&tmpPlayer.tid, NULL, handFunc, (void *)tmpPlayer.cltSock);
                //加入在线玩家组
                onlinePlayers = Insert(onlinePlayers, tmpPlayer);
                printf("client %s port %d connect\n", inet_ntoa(tmpPlayer.cltAddr.sin_addr), tmpPlayer.cltAddr.sin_port);
                //在线玩家数加一
                pthread_mutex_lock(&mutex);
                ++onlineMemberNum;
                pthread_mutex_unlock(&mutex);
                printf("onlineMemberNum:%d\n", onlineMemberNum);
                memset(&tmpPlayer, 0, sizeof(Player));
            }
            else{//用户端的操作请求
                //找到玩家
                Position opNode= Find(onlinePlayers, events[i].data.fd);
                int key, strLen;
                strLen = read(events[i].data.fd, &key, sizeof(int));
                //1.首先判断客户端是否close
                if(!strLen || strLen == -1){    //客户端close,标记玩家在线状态为false
                    opNode->player.state = false;
                    printf("client %s state:false\n", inet_ntoa(opNode->player.cltAddr.sin_addr));
                    epoll_ctl(epollFd, EPOLL_CTL_DEL, opNode->player.cltSock, NULL);
                    continue;
                }
                else{
                    opNode->player.readKey = key;
                    printf("client %s operate:%d\n", inet_ntoa(opNode->player.cltAddr.sin_addr), key);
                } 
            }
        }
    }
    close(serSock);
    onlinePlayers = MakeEmpty(onlinePlayers);
    return;
}

//游戏线程
void *handFunc(void *arg){
    int cltSock = (int)arg;
    //找到玩家
    Position opPlayer = Find(onlinePlayers, cltSock);
    //发送游戏数据初始帧
    packAndSend(opPlayer->player.cltSock, opPlayer->player.map, opPlayer->player.snack, 
                opPlayer->player.food, opPlayer->player.score, opPlayer->player.delayTime, false);
    //循环处理
    bool isEat;  //判断是否吃到食物
    while(1){
        isEat = false;
        //先检查玩家的在线状态
        if(!opPlayer->player.state){ //玩家主动close,直接关闭窗口等
            printf("client %s close disconnect\n", inet_ntoa(opPlayer->player.cltAddr.sin_addr));
            release(opPlayer->player.map, opPlayer->player.snack, opPlayer->player.food); //释放游戏资源
            onlinePlayers = Delete(onlinePlayers, opPlayer->player.cltSock); //将客户从玩家组中清除
            pthread_mutex_lock(&mutex);
            --onlineMemberNum;
            pthread_mutex_unlock(&mutex);
            printf("onlineMemberNum:%d\n", onlineMemberNum);
            pthread_exit((void*)1); //结束该游戏线程
        }
        //再检查方向变化
        switch (opPlayer->player.readKey){
            case KEY_UP:
                updateDir(opPlayer->player.snack, UP);
                break;
            case KEY_DOWN:
                 updateDir(opPlayer->player.snack, DOWN);
                    break;
            case KEY_LEFT:
                 updateDir(opPlayer->player.snack, LEFT);
                 break;
            case KEY_RIGHT:
                 updateDir(opPlayer->player.snack, RIGHT);
                 break;
            default:
                break;
        }
        // 先生成下一帧数据
        _updateMapAndSnack(opPlayer->player.map, opPlayer->player.snack);
        // 根据是否吃到食物来更改下一帧数据
        if (isMeet(opPlayer->player.snack, opPlayer->player.food)){
            isEat = true;
            longer(opPlayer->player.map, opPlayer->player.snack);
            _updateScore(opPlayer->player.snack, &(opPlayer->player.score));
            _adaptIndex(opPlayer->player.food, opPlayer->player.snack);
            opPlayer->player.map[opPlayer->player.food->_foodX][opPlayer->player.food->_foodY] = 2;
        }
        if(isGameOver(opPlayer->player.snack)){
            int overInfo = 6; //游戏结束信息
            write(opPlayer->player.cltSock, &overInfo, sizeof(int));
            sleep(2);
            break;
        }
        //打包发送
        updateDelayTime(opPlayer->player.snack, &(opPlayer->player.delayTime));
        packAndSend(opPlayer->player.cltSock, opPlayer->player.map, opPlayer->player.snack, 
                    opPlayer->player.food, opPlayer->player.score, opPlayer->player.delayTime, isEat);
        usleep(opPlayer->player.delayTime * 1000);
    }
    printf("client %s gameOver and disconnect\n", inet_ntoa(opPlayer->player.cltAddr.sin_addr));
    release(opPlayer->player.map, opPlayer->player.snack, opPlayer->player.food); //释放游戏资源
    onlinePlayers = Delete(onlinePlayers, opPlayer->player.cltSock); //将客户从玩家组中清除
    pthread_mutex_lock(&mutex);
    --onlineMemberNum;
    pthread_mutex_unlock(&mutex);
    printf("onlineMemberNum:%d\n", onlineMemberNum);
    return NULL;
}


bool packAndSend(int cltSock, const int(*map)[COL], const Snack *snack, const Food *food, int score, int delayTime, bool isEat) {
    //游戏包
    struct{
        int mapData[ROW][COL];
        int headXData;
        int headYData;
        int scoreData;
        int delayTimedata;
        bool isEatData;
    }gameData;

    //初始化游戏包
    memcpy(gameData.mapData, map, sizeof(int) * ROW * COL);
    gameData.headXData = snack->_bodys[0]->_x;
    gameData.headYData = snack->_bodys[0]->_y;
    gameData.scoreData = score;
    gameData.delayTimedata = delayTime;
    gameData.isEatData = isEat;

    int size = sizeof(gameData);
    //printf("gameDataSize:%d\n", size);
    //游戏包字节数组
    char gameDataMsg[size + sizeof(int)];
    memcpy(gameDataMsg, &size, sizeof(int));
    memcpy(gameDataMsg + sizeof(int), &gameData, size);
    //printf("gameDataMsgSize:%d\n", sizeof(gameDataMsg));
    write(cltSock, gameDataMsg, sizeof(gameDataMsg));
    return true;
}

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

void release(int(*map)[COL], Snack* snack, Food* food){
	free(map);
	free(food);
	//注意释放snack要先释放其指针数组中的数据
    int i;
	for (i = 0; i < snack->_len; i++) {
		free(snack->_bodys[i]);
	}
	free(snack);
}


bool initPlyerInfo(Player *player){
    player->state = true; // 标记玩家在线
    player->map = (int(*)[COL])malloc(sizeof(int) * ROW * COL);
    if(!player->map) return false;
    player->snack = (Snack *)malloc(sizeof(Snack));
    if(!player->snack) return false;
    player->food = (Food *)malloc(sizeof(Food));
    if(!player->food) return false;
    memset(player->map, 0, sizeof(int) * ROW * COL);
    player->delayTime = 300;
    player->score = 0;
    player->readKey = KEY_RIGHT;
    //2.初始化游戏数据
	bool isInit = initData(player->map, player->snack, player->food);
	if (!isInit){
        release(player->map, player->snack, player->food);
		printf("playGame fail to initData\n");
        return false;
	}
    return true;
}

client.c

#include<winsock.h>
#include<stdlib.h>
#include<stdio.h>
#include<conio.h>
#include "Client.h"
#include"SnackClient.h"

void errorHanding(const char* errmsg);
HANDLE recvAndHandle(LPVOID lpParameter);

bool client(const char *addr, const char *port){
    
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData))
		errorHanding("WSAStartup() fail");
	SOCKET cltSock;
	SOCKADDR_IN serAddr;
	//创建socket
	cltSock = socket(PF_INET, SOCK_STREAM, 0);
	if (cltSock == INVALID_SOCKET) errorHanding("socket() fail");
	//初始化目的IP和端口号
	memset(&serAddr, 0, sizeof(serAddr));
	serAddr.sin_family = AF_INET;
	serAddr.sin_addr.s_addr = inet_addr(addr);
	serAddr.sin_port = htons(atoi(port));
	//请求连接
	if (connect(cltSock, (SOCKADDR*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
		errorHanding("connect() fail");
    puts("connect----");
    //接收数据包并更新处理
    OpObject opOb;
    opOb.cltSock = &cltSock;
    opOb.state = true;
    HANDLE res = CreateThread(NULL, NULL, (PTHREAD_START_ROUTINE)recvAndHandle, (LPVOID)&opOb, NULL, NULL);
    //发送客户端的操作
    while (1) {
        if (_kbhit()) {   //按下按键后进行方向的更改
            int key = _getch();
            if (key == KEY_UP || key == KEY_DOWN || key == KEY_LEFT
                || key == KEY_RIGHT) {
                send(cltSock, (char*)&key, sizeof(int), 0);
            }
        }
        if (!opOb.state) break;
    }
    WaitForSingleObject(res, 0);
    _getch();
    menu();
    return true;
}

HANDLE recvAndHandle(LPVOID lpParameter){
    OpObject* opOb = (OpObject*)lpParameter;
    int score = 0;
    int msgSize;  //游戏包大小
    char* gameDataMsg;  //保存游戏包数据
	int strLen;
    while (1) {
        //先接收游戏包大小
        strLen = recv(*opOb->cltSock, (char*)&msgSize, sizeof(int), 0);
        if (msgSize == 6 || !strLen ||strLen == -1) {
            opOb->state = false;
			closesocket(*(opOb->cltSock));
            overScene(score);
            return NULL;
        }
        gameDataMsg = (char*)malloc(msgSize);
        if (!gameDataMsg) return (LPVOID)-1;
        //因为TCP无数据边界,所以循环接收,直到收齐
        int rcvLen = 0, rcvCnt;
        while (rcvLen < msgSize) {
            rcvCnt = recv(*opOb->cltSock, gameDataMsg + rcvLen, msgSize - rcvLen, 0);
            //if (rcvCnt == 0) break;
            rcvLen += rcvCnt;
        }
        struct {
            int mapData[ROW][COL];
            int headXData;
            int headYData;
            int scoreData;
            int delayTimedata;
            bool isEatData;
        }gameData;
        memcpy(&gameData, gameDataMsg, msgSize);
        updateScene(gameData.mapData, gameData.headXData, gameData.headYData, gameData.scoreData, gameData.isEatData);
        score = gameData.scoreData;
        free(gameDataMsg);
    }
    return NULL;
}

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

四、成果展示

1.打开服务器, 可以多个客户端连接,连接成功进入初始界面,并播放魔性音乐

2.控制W(向上),A(向左),S(向下),D(向右)来控制蛇的移动,数据实时显示在服务端

3.碰到墙或自身后触发死亡,并切换到得分界面,并播放结算音乐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值