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