任务介绍
- 为贪吃蛇游戏添加多人游戏功能:多个玩家可以分别控制贪吃蛇在游戏区域里驰骋,避免碰到自己、同类或障碍物,尽可能地吃更多的食物以生长!
开发环境
- OpenGL3
- GLUT
- GLAD
- C++
要求:
- 完成多人贪吃蛇游戏的C-S框架搭建(50%)(达成)
- 多客户端能够分别连接服务器并有正确的响应(连接成功、连接失败)(20%)(达成)
- 服务器能够正确处理客户端的动作请求,客户端能够正确更新蛇的移动状态(25%)(达成)
- 蛇的碰撞检测、奖励生成、胜负判断等内容的实现(10%+5% with bonus)(未达成)
参考资料
1.《网络多人游戏架构与编程》
实现内容
1. 当前功能介绍
- 客户端登入时询问是否进入多人游戏,如果否,则不与服务器连接,正常进行单人游戏
- 与服务器成功连接后返回用户编号,当连接用户数达到指定数目后进入游戏(当前指定人数为2),人数已满后其他用户无法加入
- 服务器缓存蛇的移动状态,客户端根据服务端返回的数据正确更新蛇的移动
- 未实现两人以上连接功能
2. 服务端
服务端的缓存区缓存一定的状态数,客户端加载蛇的时候通过当前状态的序号向客户端查询,客户端向服务端更新蛇的时候服务端更新当前状态的下一个状态
服务端返回字符串:
wait:蛇不进行移动,状态序号不更新
ok:更新移动状态后返回讯息
整数(1,2,3……):用户编号
状态字符串(ad,aw……):蛇的移动状态,第1个字符是编号1的蛇,第2个字符是编号2的蛇,以此类推
while (true) {
int message = recvfrom(server, receBuf, 1024, 0, (SOCKADDR*)&addrClient, &fromlen);
if (message > 0) { //判断接收到的数据是否为空
receBuf[message] = '\0';//给字符数组加一个'\0',表示结束了。不然输出有乱码
// 返回编号
if ((receBuf[1] == 'i')) {
if (id == 0) {
id++;
const char* Response = "1";
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
}
else if (id == 1) {
id++;
const char* Response = "2";
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
}
}
// 超出人数
else if (id < 2) {
const char* Response = "wait";
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
}
// 返回对应移动状态
else if (receBuf[1] == 'g') {
int num = atoi(receBuf + 2);
if (lastTime >= 0 && control == 0) {
int index = (num) % 100;
int index1 = (num + 1) % 100;
dirBuffer[index1] = dirBuffer[index];
}
// 还有玩家未更新时其他玩家等待
if (num > lastTime) {
const char* Response = "wait";
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
}
else {
int index = (num)%100;
const char* Response = dirBuffer[index].c_str();
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
control++;
if (control == 2) {
lastTime++;
control = 0;
}
}
}
// 更新对应移动状态
else {
int index1 = receBuf[0] - '1';
int index2 = (lastTime + 1) % 100;
dirBuffer[index2][index1] = receBuf[1];
const char* Response = "ok";
sendto(server, Response, strlen(Response), 0, (SOCKADDR*)&addrClient, sizeof(SOCKADDR));
}
}
}
3. 客户端
在上一次作业的基础上,增加一个蛇停止的状态,修改键盘事件,根据是否进入多人游戏决定触发事件。
服务端发送字符串(identity + dir + time):
用户id:由服务端返回,用于标识用户
命令:i表示请求id,g表示请求状态,wasd表示更新(成功返回ok)
状态序号:time表示客户端当前所处状态
string send = identity + dir + to_string(time);
int last = recvfrom(sock_Client, receBuf, 1024, 0, (SOCKADDR*)&sock, &len);
if (last > 0) {
receBuf[last] = '\0';
if (dir == "i") {
id = receBuf;
}
else if (strcmp(receBuf, "wait") == 0) {
snake1->move(D_STOP);
snake2->move(D_STOP);
}
else if (strcmp(receBuf, "ok") == 0) {
cout << " UPDATE..." << endl; // update direction success
}
else if (dir == "g") {
time++;
switch (receBuf[0]) {
case 'w':
snake1->move(D_UP);
break;
case 's':
snake1->move(D_DOWN);
break;
case 'a':
snake1->move(D_LEFT);
break;
case 'd':
snake1->move(D_RIGHT);
break;
default:
break;
}
switch (receBuf[1]) {
case 'w':
snake2->move(D_UP);
break;
case 's':
snake2->move(D_DOWN);
break;
case 'a':
snake2->move(D_LEFT);
break;
case 'd':
snake2->move(D_RIGHT);
break;
default:
break;
}
}
}
实验思考及感想
这个学期各个课程的作业很多,这次作业的工作量对我而言还是大了点,和上次一样花了很多时间。实现的和课件提到的功能部分有不少出入,但要求是大部分完成了的(大概)。上一次的作业只实现了一些基本的功能,代码也写得比较混乱,但由于时间关系没办法进一步改进了,希望以后这项作业能有更充分的时间吧。
从难度上来讲,使用UDP传输数据是很简单的,但实现不同客户端之间的绝对同步相对较难,耗时也最长。