Qt基于tcp网络服务器的简易版多人贪吃蛇小游戏(多线程)

文章目录

  • 前言
  • 一、项目的简单介绍
  • 二、总体步骤
    • 1.服务器端
    • 2 客户端
  • 总结


前言

花费一周的时间,搭建了一个自己的破烂服务器,以此记录我那逝去的时间


一、项目的简单介绍

这是一个多人贪吃蛇游戏,也就是说,可以有多个人共享一个游戏,可以增进游戏的趣味性。运用了多线程的方法,基于tcp通信,哎呀,扯不下去了。废话不多说。请看下文

二、总体步骤

分为两部分,一个是服务器端通过多线程的方式与各个客户端之间进行通信,同时规定好蛇体移动的快慢,以及同步更新食物等等。另一部分就是客户端了,它的功能主要是改变移动方向,判断蛇头是否吃到食物以及判断死亡。接下来具体介绍服务器端的搭建。

1.服务器端

首先QTcpServer类是必不可少的。

this->listen(QHostAddress::Any,8888);  //用于监听端口号为8888的本地网络端口

当有一个客户端连接上服务器时,QTcpServer类会触发newConnected信号,不过在这里,我们并不需要使用到这个信号,不然在接下来使用线程的时候,会导致线程资源冲突,我们在这里重写incomingConnection函数。

void tcp_build::incomingConnection(qintptr handle){this->havefood = 0;qDebug()<<ThreadList.length()<<"连接了";Mythread *mythread = new Mythread(handle,0);ThreadList.append(mythread);connect(mythread,&Mythread::DeleteThreadFromListSignal,this,&tcp_build::DeleteThreadFromListSlot);          //从链表删除线程的信号槽connect(mythread,&Mythread::DeleteSnakeFromListSignal,this,&tcp_build::DeleteSnakeFromListSlot);            //从链表删除蛇身的信号槽connect(mythread,&Mythread::AddSnakeToListSignal,this,&tcp_build::AddSnakeToListSlot);                      //在链表中添加蛇身的信号槽connect(mythread,&Mythread::CreateFoodToServerSignal,this,&tcp_build::CreateFoodToServerSlot);              //创建食物的信号槽
//    connect(timer,&QTimer::timeout,mythread,&Mythread::TimeOutSendSnakeListToThreadSlot);                       //定时器超时处理信号与槽的连接connect(timer,&QTimer::timeout,mythread,&Mythread::TimeOutAddSnakeToListInThreadSlot);                      //定时器超时将客户端蛇身信息存储值list中connect(this,&tcp_build::SendFoodPointToThreadSignal,mythread,&Mythread::SendFoodPointToThreadSlot);        //发送食物坐标的信号与线程信号槽连接connect(this,&tcp_build::SendSnakeListToThreadSignal,mythread,&Mythread::SendSnakeListToThreadSlot);        //发送蛇身list的信号与线程信号槽链接mythread->start();
}

这里有很多信号与槽,主要是用于线程和服务器之间的通信的,下面的几个主要是定时器更新蛇坐标系以及食物创建相关的。在这里我们重点介绍mythread类。它的作用主要是创建线程,以及线程里的资源与tcp服务器之间的资源交互。
在这里讲一下我是通过QVector ThreadList 来管理线程的
使用QVector SnakeList 这个结构来管理蛇的坐标系的 snake类会在线程中讲到。接下来是他们的实现方法:

void tcp_build::DeleteThreadFromListSlot(void){                 //从链表中删除线程Mythread *thread = (Mythread *)this->sender();for(int i=0;i<ThreadList.length();i++){if(thread == ThreadList[i]){qDebug()<<"删除了"<<i<<"号线程";ThreadList[i]->quit();ThreadList[i]->wait();ThreadList[i]->deleteLater();ThreadList.remove(i);}}if(ThreadList.length() == 0){this->havefood = 0;}
}
void tcp_build::AddSnakeToListSlot(snake *Snake){               //添加蛇至链表qDebug()<<"当前AddSnakeToListSlot线程id为"<<QThread::currentThreadId();qDebug()<<"SnakeList的长度为"<<SnakeList.length();snake *S = Snake;qDebug()<<"蛇的地址为:"<<S;SnakeList.append(S);qDebug()<<"SnakeList的长度为"<<SnakeList.length();if(SnakeList.length() == ThreadList.length()){emit SendSnakeListToThreadSignal(SnakeList);SnakeList.clear();if(!this->havefood){triggerSendFoodPointToThreadSignal(FoodPoint);this->havefood = 1;}}
}
void tcp_build::DeleteSnakeFromListSlot(snake *SNake){          //从链表删除蛇qDebug()<<"当前DeleteSnakeFromListSlot线程id为"<<QThread::currentThreadId();qDebug()<<"SnakeList的长度为"<<SnakeList.length();snake *S = SNake;qDebug()<<"蛇的地址为:"<<S;for(int i=0;i<SnakeList.length();i++){if( S == SnakeList[i] ){SnakeList.removeAt(i);qDebug()<<"删除了第"<<i<<"号位置的蛇";}}qDebug()<<"SnakeList的长度为"<<SnakeList.length();
}

这些函数都是通过信号与槽的方式进行连接的。

接下来就是线程的功能实现
在mythread类中,通过重写run函数来实现线程的功能

void Mythread::run(){qDebug()<<"当前run线程id为"<<QThread::currentThreadId();MySocket *mysocket = new MySocket(sock,0);snake *Snake = new snake;if(!mysocket->setSocketDescriptor(sock))return;connect(mysocket,&MySocket::disconnected,this,&Mythread::SocketDisconnectSlot);                     //客户端断开连接时与线程之间的信号槽connect(mysocket,&MySocket::disconnected,Snake,&snake::SocketDisconnectedSlot);                     //客户端断开连接时与snake之间的信号槽,用于从链表中删除蛇connect(mysocket,&MySocket::SnakeChangeSignal,Snake,&snake::SnakeChangeSlot);                       //收到客户端信息,方向变换信号connect(this,&Mythread::SnakeInitSignal,Snake,&snake::SnakeInitSlot);                               //蛇初始化信号与槽的连接connect(this,&Mythread::TimeOutAddSnakeToListSignal,Snake,&snake::TimeOutSnakeChangeSlot);          //定时器超时向snake发送存储蛇身的信号connect(this,&Mythread::SendFoodPointSignal,mysocket,&MySocket::SendFoodPointSlot);                 //发送食物坐标的信号槽connect(this,&Mythread::SendSnakeListSignal,mysocket,&MySocket::SendSnakeListSlot);
//    connect(this,&Mythread::TimeOutSendSnakeListSignal,mysocket,&MySocket::TimeOutSendSnakeListSlot);   //发送存储蛇身链表的信号槽connect(Snake,&snake::AddSnakeToThreadSignal,this,&Mythread::AddSnakeToThreadSlot);                 //在线程中触发添加蛇身至list的信号槽connect(Snake,&snake::DeleteSnakeFromListToThreadSignal,this,&Mythread::DeleteSnakeFromListSlot);   //从存储蛇身的链表中删除某一条蛇的信号槽connect(Snake,&snake::CreateFoodToThreadSignal,this,&Mythread::CreateFoodToThreadSlot);             //需要创建食物的信号槽
//    connect(Snake,&snake::SendSnakeHeadPointSignal,mysocket,&MySocket::SendSnakeHeadPointSlot);connect(Snake,&snake::storageSnakeHeadPointSignal,mysocket,&MySocket::storageSnakeHeadPointSlot);triggerSnakeInitSignal();exec();
}

其中信号与槽可能会有些繁琐,不过没办法,线程里的资源不能直接和tcp服务器的资源进行交互,只能借助mythread来达到资源交互的目的。接下来介绍比较重要的一步。

mysocket就是继承了QTcpSocket类,不同的是。我是使用incomingConnection中的参数来构建自己的套接字,

mysocket->setSocketDescriptor(sock)  //sock就是incomingConnection中的参数

这样子,我们就能在线程中使用这一个套接字与客户端进行通信了。在mythread类中没有什么重要的函数,都是通过触发信号或者作为槽函数使用。重点都在构建的几个类里面。
接下来,讲一下snake类,这个类主要是实现蛇的初始化,以及当收到客户端传来的信号(比如说方向改变、或者吃到食物边长等等)时,蛇体所做出的改变当然每一个人的想法都会有不一样,不一样的想法也肯定可以实现。我的这一种想法仅供参考。
首先我是用这种数据类存储蛇体坐标的,我觉着这样比较简单,不过在传输数据的时候,可能会有一点麻烦

QVector<QPoint> Snake;

这个是蛇体初始化函数,其中的num为rand()%8随机生成,我这里默认两节,唉,可能会有一点丑陋

void snake::SnakeInitSlot(int num){QPoint point1;QPoint point2;switch(num%8){case 0:point1.rx() = 0;point1.ry() = 1;point2.rx() = 0;point2.ry() = 0;break;case 1:point1.rx() = 54/2;point1.ry() = 1;point2.rx() = 54/2;point2.ry() = 0;break;case 2:point1.rx() = 53;point1.ry() = 1;point2.rx() = 53;point2.ry() = 0;break;case 3:point1.rx() = 1;point1.ry() = 40/2;point2.rx() = 0;point2.ry() = 40/2;dir = 4;break;case 4:point1.rx() = 52;point1.ry() = 40/2;point2.rx() = 53;point2.ry() = 40/2;dir = 3;break;case 5:point1.rx() = 0;point1.ry() = 38;point2.rx() = 0;point2.ry() = 39;dir = 2;break;case 6:point1.rx() = 54/2;point1.ry() = 38;point2.rx() = 54/2;point2.ry() = 39;dir = 2;break;case 7:point1.rx() = 53;point1.ry() = 38;point2.rx() = 53;point2.ry() = 39;dir = 2;break;}Snake.append(point1);Snake.append(point2);
}

这是当收到客户端发来的方向改变的信号时,蛇体所做的改变,我就觉着这一样比较简单

void snake::TimeOutSnakeChangeSlot(void){qDebug()<<"蛇身变化函数";QPoint point;for(int i=Snake.length()-1;i>0;i--){Snake[i] = Snake[i-1];}switch(this->dir){case 1:point.rx() = Snake[0].rx();point.ry() = Snake[0].ry()+1;break;case 2:point.rx() = Snake[0].rx();point.ry() = Snake[0].ry()-1;break;case 3:point.rx() = Snake[0].rx()-1;point.ry() = Snake[0].ry();break;case 4:point.rx() = Snake[0].rx()+1;point.ry() = Snake[0].ry();break;}SnakeHeadPoint = point;       //拷贝蛇头坐标Snake.replace(0,point);if(SnakeState){         //当蛇状态为0时,即死亡状态时,便不会将自己的蛇身坐标传递给存储蛇的链表emit storageSnakeHeadPointSignal();emit TimeOutAddSnakeToListSignal();}
}

这是从客户端接收到吃到食物的信号时,所做出的改变。

void snake::SnakeEatFoodSlot(void){QPoint point;point = Snake[Snake.length()-1];Snake.append(point);triggerCreateFoodToThreadSignal();
}

接下来最重要的就是通信了,也就是我构建的mysocket类
这里没有什么好说的,对于我这个数据我也没有想出什么好办法,于是就只好硬着头皮上了

这个是发送连接上服务器的所有蛇的数据,唉,太难了。每一个都取两位,不够的就用零来凑,所有多了好的的判断以及加0的操作

//信息结构  蛇头横坐标 纵坐标 蛇的条数  第某一条蛇的长度  第某一条蛇的某一个节点的x,y坐标
void MySocket::SendSnakeListSlot(QVector<snake *> SnakeList){QVector<int> SnakeData;if(this->point.rx()<10){SnakeData.append(0);}SnakeData.append(this->point.rx());if(this->point.ry()<10){SnakeData.append(0);}SnakeData.append(this->point.ry());if(SnakeList.length()<10){SnakeData.append(0);}SnakeData.append(SnakeList.length());for(int i=0;i<SnakeList.length();i++){qDebug()<<"第"<<i<<"条蛇的坐标信息:";if(SnakeList[i]->Snake.length()<10){SnakeData.append(0);}SnakeData.append(SnakeList[i]->Snake.length());qDebug()<<"length = "<<SnakeList[i]->Snake.length();for(int j=0;j<SnakeList[i]->Snake.length();j++){qDebug()<<"第"<<j<<"个节点的横坐标为:"<<SnakeList[i]->Snake[j].rx()<<"  纵坐标为:"<<SnakeList[i]->Snake[j].ry();if(SnakeList[i]->Snake[j].rx()<10){SnakeData.append(0);}SnakeData.append(SnakeList[i]->Snake[j].rx());if(SnakeList[i]->Snake[j].ry()<10){SnakeData.append(0);}SnakeData.append(SnakeList[i]->Snake[j].ry());}}QString str = "";for(int i=0;i<SnakeData.length();i++){char temp[4]="";itoa(SnakeData[i],temp,10);            //将整数转换为字符串str+=temp;}qDebug()<<"发送的信息为:"<<str;this->write(str.toUtf8());                  //QString转换为QByteArray
}

这个是发送食物坐标的函数,我就感觉他有问题

void MySocket::SendFoodPointSlot(QPoint point){qDebug()<<"发送食物坐标的线程号为:"<<QThread::currentThreadId();qDebug()<<"食物横坐标为:"<<point.rx()<<"  纵坐标为:"<<point.ry();char FoodArray[20];if(point.rx()<10 && point.ry()>=10){sprintf(FoodArray,"FoodPoint:x=0%dy=%d",point.rx(),point.ry());}else if(point.rx()>=10 && point.ry()<10){sprintf(FoodArray,"FoodPoint:x=%dy=0%d",point.rx(),point.ry());}else if(point.rx()<10 &&point.ry()<10){sprintf(FoodArray,"FoodPoint:x=0%dy=0%d",point.rx(),point.ry());}else sprintf(FoodArray,"FoodPoint:x=%dy=%d",point.rx(),point.ry());this->write(FoodArray);
}

服务器端大致就差不多了。接下来的客户端就简单多了。

2 客户端

在客户端具体就讲一下,我是用什么滑稽的方法解包的,以及死亡判断。
解包方法:

void MyWidget::SocketReceDataSlot(void){QString str = socket->readAll();qDebug()<<"收到的信息为:"<<str;qDebug()<<"收到的信息长度为:"<<str.length();if(str!=""){if(str[0] == 'F'){SnakeFoodPoint.rx() = str.mid(12,2).toUInt();SnakeFoodPoint.ry() = str.mid(16,2).toUInt();qDebug()<<"食物创建的横坐标为:"<<SnakeFoodPoint.rx();qDebug()<<"食物创建的纵坐标为:"<<SnakeFoodPoint.ry();}else{if(str.contains('-')){          //正常情况下,不可能出现负数,除非蛇头出现在了图外socket->write("dead");return;}QVector<QPoint> Snake;int SnakeNum;int SnakeLength;int j=0;SnakeHeadPoint.rx() = str.mid(j,2).toUInt();j+=2;SnakeHeadPoint.ry() = str.mid(j,2).toUInt();j+=2;qDebug()<<"蛇头横坐标为:"<<SnakeHeadPoint.rx()<<"  纵坐标为:"<<SnakeHeadPoint.ry();SnakeNum = str.mid(j,2).toUInt();j+=2;SnakeList.clear();for(int i=0;i<SnakeNum;i++){SnakeLength = str.mid(j,2).toUInt();j+=2;for(int z=0;z<SnakeLength;z++){QPoint point;point.rx() = str.mid(j,2).toUInt();j+=2;point.ry() = str.mid(j,2).toUInt();j+=2;Snake.append(point);}SnakeList.append(Snake);Snake.clear();}}SnakeDeadJudge();eatFoodJudge();update();}
}

死亡判断的话,这里用了一个小技巧。 board_col 和 board_row是两个宏定义,含义是蛇运动场地的范围

if(SnakeHeadPoint.rx()>board_col-1||SnakeHeadPoint.ry()>board_row-1)      //小于零的部分已经在接收数据的时候考虑过了socket->write("dead");memset(board,0,sizeof(board));      //初始化for(int i=0;i<SnakeList.length();i++){for(int j=0;j<SnakeList[i].length();j++){board[SnakeList[i][j].rx()][SnakeList[i][j].ry()]++;if(board[SnakeList[i][j].rx()][SnakeList[i][j].ry()]>=2){socket->write("dead");return;}}}

总结

以上就是今天要讲的内容,简单介绍了我的基于tcp服务器的多人贪吃蛇游戏,由于该项目还不太完善,比如历史纪录上的还没有整上等等方面还不太完善,以及记录我的项目。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部