跳棋游戏第二阶段实验报告¶
一、小组分工¶
周小琳 : 代码的编译和调试,调整qt上的显示效果,编写实现用于测试客户端的服务端,添加了展示玩家列表和实现倒计时的功能,上传作业。
杨爽 : 第二阶段客户端主要代码编写,和周同学一起调试代码,写实验报告。
赵铎淞:搜集图片素材,游戏试玩反馈。
二、代码框架设计¶
第二阶段主要是要搭建起整个游戏实现的基本框架。
我们需要一个用于测试的服务端,然后主要编写客户端(由于我们小组只是想实现客户端,所以编写的服务端仅仅是用于测试的,点击后会有一个空白的窗口产生,然后后台的服务端会开始运行)。
我们可以通过阅读助教提供的代码,发现每生成一个客户端的窗口,都需要一个NetworkSocket的对象和它进行绑定,从而实现服务端和客户端之间的信息的传送,所以这个和NetworkSocket绑定的类对象,能够直接接触到棋盘类对象,从而能够对其进行一些操作并接受棋盘类对象发送的数据。再考虑到我们需要给游戏用户提供的功能(包括单机模式和联机模式的选择,几人模式的选择,输入房间号昵称等),可以设计整个程序的框架如下所示:

程序运行后会生成mainwindow的对象,是一个主⻚面,点击登入以后,进入choose1的类产生的⻚面,选择联机 模式还是单机模式,选择单机模式就是对应第一阶段的实现;选择联机模式以后进入Network类产生的⻚面,选择 服务端后产生一个服务端的对象,点击客户端后,会进入netchoose类产生的几人模式的选项,选择后进入room 类对应产生的⻚面,输入房间号和名字,等待每一个客户端都进入准备状态,然后就会跳出棋盘类的⻚面。
其他还有一些不太重要的窗口类如connetfai,connectsuc, gameover等,主要作用是弹出具有提示作用的窗口,或者是提示某种操作出错,这部分比较简单,也不是主体内容,在这里就不过多讲述。
接下来,根据助教给出的代码,我们需要在Room类里面实现相应的recieve函数,来接收来自于服务端的数据,然后 根据不同的操作的具体的要求,每一个Room的对象对其绑定的棋盘类的对象进行操作,更改每个棋盘类对象里面保存的信息以及画面的展示。为此,我们可以在每个Room类对象里面保存相应的Widget类对象的指针,通过Widget类提供的接口,实现修改。
棋盘类Widget里面的主要接口和作用如下所示:
void setplayer(int _player); // 当前窗口对应哪一个玩家
void setLeave (QString _name); // 某个玩家离开窗口
void setorder(); // 设置玩家所在⻆落区域,保存玩家顺序信息
void player_turn(int _player); // 提示某个玩家下棋
void setmove(int _player, QString player_trail); // 某个玩家的行棋操作
void endGame(QString _list); // 结束游戏
void setWin(); // 当前窗口的玩家胜利
void moveFail(int _player); // 提示某个玩家行棋不合法
QString getTrail(); // 获取当前窗口玩家的行棋路径
void storeplayer(int _player, QString _name); // 存储同一次游戏其他玩家的信息
与此同时,当棋盘对象需要向服务端发送数据的时候,会发送信号给room对象,然后room对应利用绑定的 Networksocket向服务端发送数据。棋盘类中主要的信号如下所示:
void exit(); // 玩家主动按下离开房间的按钮
void signal_move(); // 请求移动行棋
对于这些信号,对应Room类里面的槽函数如下所示:
void on_Leave_clicked(); //玩家主动离开房间
void try_move(); // 接受到玩家的请求行棋的信号
三、重要功能实现及代码展示¶
1、首先最重要是Room类里面recieve函数的实现,代码和详细注释如下所示:
void Room::receive(NetworkData data)
{
if(data.op==OPCODE::END_GAME_OP)
{
//此时应该结束游戏,并且客户端将data1内包含的玩家排名进行展示
winArray -> endGame(data.data1);
this -> close(); // 棋盘窗口关闭后,也关闭当前页面
}
else if (data.op == OPCODE :: END_TURN_OP)
{
// 表示目前这个玩家已经获得了胜利
winArray -> setWin();
}
else if(data.op==OPCODE::JOIN_ROOM_OP)
{
//下面需要在每个room页面的列表里面都加上对应的玩家
//找到第一个为空的用户名 加上新玩家的信息
for(int i=1;i<=type;i++)
{
label=findChild<QLabel *>("Player"+QString::number(i)+"_name");
label2=findChild<QLabel *>("Player"+QString::number(i)+"_state");
if(label->text()=="")
{
label->setText(data.data1);
label2->setText("0");
break;
}
}
}
else if(data.op==OPCODE::JOIN_ROOM_REPLY_OP)
{
// 接收到这个请求的是一个刚刚进入房间的新玩家
QString str = "";
int len = data . data1.length();
int j = 0; //表示玩家状态序号
// 显示用户名和状态
if(!data.data1.isEmpty())
{
for (int i = 0; i < len; i++)
if (data . data1.at(i) == ' ')
{
label=findChild<QLabel *>("Player"+QString::number(j+1)+"_name");
label2=findChild<QLabel *>("Player"+QString::number(j+1)+"_state");
label->setText(str);
label2->setText(data.data2.at(j));
str = "";
j++;
}
else str = str + data . data1.at(i);
// 最后一个用户末尾没有空格,不要忘记处理
label=findChild<QLabel *>("Player"+QString::number(j+1)+"_name");
label2=findChild<QLabel *>("Player"+QString::number(j+1)+"_state");
label -> setText(str);
label2 -> setText(data . data2.at(j));
// 同时还要加上玩家自己
label=findChild<QLabel *>("Player"+QString::number(j+2)+"_name");
label2=findChild<QLabel *>("Player"+QString::number(j+2)+"_state");
label -> setText(player_name);
label2 -> setText("0");
}
else
{
ui->Player1_name->setText(player_name);
ui->Player1_state->setText("0");
}
}
else if(data.op==OPCODE::LEAVE_ROOM_OP)
{
//表示有一名玩家离开房间, server向剩下的所有玩家发送该op
//data1 为离开的房间名,data2 为离开玩家的用户名
if (data.data1 == this -> room_num && inRoom)
{
winArray -> setLeave(data.data2);
}
// 下面是将对应的玩家列表进行修改
for(int i=1;i<=type;i++)
{
label=findChild<QLabel *>("Player"+QString::number(i)+"_name");
if(label->text()==data.data1)
{
label2= findChild<QLabel *>("Player"+QString::number(i)+"_state");
label->setText(" ");
label2->setText(" ");
for(int j=i;j<=type;j++)
{
if(j+1<=type)
{
label3=findChild<QLabel *>("Player"+QString::number(j+1)+"_name");
label4=findChild<QLabel *>("Player"+QString::number(j+1)+"_state");
label->setText(label3->text());
label2->setText(label4->text());
}
else
{
label3=findChild<QLabel *>("Player"+QString::number(j)+"_name");
label4=findChild<QLabel *>("Player"+QString::number(j)+"_state");
label3->setText(" ");
label4->setText(" ");
}
}
break;
}
}
}
else if(data.op==OPCODE::MOVE_OP)
{
/*
MOVE-OP data2为-1的情况:
超时判负,此时的MOVE-OP为服务端向全体玩家发出的op
data1为该玩家所处区域,data2为-1
此时所有玩家应该更新界面并将超时玩家的棋子清空
*/
//data1为该玩家的初始区域编号,data2为该玩家的移动轨迹
// 获取行棋区域的字母
QChar ch = data.data1.at(0);
int this_player; // 得到对应的玩家
if(ch =='A')
this_player = 1;
else if (ch =='B')
this_player = 3;
else if(ch =='C')
this_player = 5;
else if(ch == 'D')
this_player = 2;
else if(ch == 'E')
this_player = 4;
else if(ch=='F')
this_player = 6;
if(data.data2 != "-1")
{
winArray -> setmove(this_player, data.data2);
}
else
{
winArray -> setOutofTime(this_player);
}
}
else if(data.op==OPCODE::PLAYER_READY_OP)
{
//将相应列表上玩家的状态设置为1
for(int i=1;i<=type;i++)
{
label=findChild<QLabel *>("Player"+QString::number(i)+"_name");
if(label->text()==data.data1)
{
label2= findChild<QLabel *>("Player"+QString::number(i)+"_state");
label2->setText("1");
break;
}
}
}
else if(data.op==OPCODE::START_GAME_OP)
{
//data1为玩家列表,data2为每个玩家被分配的初始区域
//A B C D E F
//1 3 5 2 4 6
QString str = "";
int len = data . data1.length();
int j = 0;
// 显示用户名和状态
for (int i = 0; i <= len; i++)
//最后一个后面没有空格,不要忘记处理
if (i == len || data . data1.at(i) == ' ')
{
// 找到用户名对应的编号
QChar field = data . data2.at(j);
int _pos;
if(field=='A')
{
_pos = 1;
}
else if(field=='B')
{
_pos = 3;
}
else if(field=='C')
{
_pos = 5;
}
else if(field=='D')
{
_pos = 2;
}
else if(field=='E')
{
_pos = 4;
}
else if(field=='F')
{
_pos = 6;
}
if (player_name == str) // 如果对应是对应当前玩家的信息
{
winArray -> setplayer(_pos); // 设置对应玩家在棋盘的位置
player_pos = _pos;
}
winArray -> storeplayer(_pos,str); // 不管是不是当前玩家的信息都要进行存储
str = "";
j+=2;
}
else str = str + data . data1.at(i);
// 根据传送过去的player设置对应的游戏顺序,和画出对应有棋子地方的棋盘,
winArray -> setorder();
//下面开始游戏,展示出棋盘画面
winArray -> show();
this -> hide();
}
else if(data.op==OPCODE::START_TURN_OP)
{
// 当轮到某玩家移动棋子时,server 应向全体 client 发送 START_TURN_OP 请求,并将数据段 data1 和 data2 分别设置为「行棋方的初始区域编号」和「回合开始时刻的 Unix 时间戳」
QChar ch = data.data1.at(0); // 获取行棋区域的字母
int this_player;
if (ch == 'A')
this_player = 1;
else if (ch == 'B')
this_player = 3;
else if (ch == 'C')
this_player = 5;
else if (ch == 'D')
this_player = 2;
else if (ch == 'E')
this_player = 4;
else if (ch == 'F')
this_player = 6;
//在所有窗口设置好新的倒计时
if(winArray->flag==0)
{
winArray->timer=new QTimer(this);
}
winArray->timer->start(1000);
winArray->Countnum = 15;
if(winArray->flag == 0)
{
connect(winArray->timer,&QTimer::timeout,this->winArray,&Widget::Countdown);
winArray->flag=1;
}
else
{
//qDebug()<<"0";
//delete winArray->timer;
disconnect(winArray->timer,&QTimer::timeout,this->winArray,&Widget::Countdown);
connect(winArray->timer,&QTimer::timeout,this->winArray,&Widget::Countdown);
}
winArray -> player_turn(this_player); //在所有玩家窗口设置轮换
}
else if(data.op==OPCODE::ERROR_OP)
{
// 这里应该是错误码的数值的字符串形式
if(data.data1=="400003") // 表示加入房间的时候失败
{
// 当前页面弹出加入失败的窗口,并且直接关闭当前窗口,提示用户重新来
JoinFail* a = new JoinFail();
a -> show();
this -> close();
}
else if(data.data1=="400005") //非法行棋
{
//在所有这个房间的用户页面上提示对应的某一方非法行棋
winArray -> moveFail(player_pos);
}
else if(data.data1=="400006") // is not supposed to happen
{
qDebug() << "INVALID_REQ";
}
else if(data.data1=="400000") // is not supposed to happen
{
qDebug() << "NOT_IN_ROOM";
}
else if(data.data1=="400004") // is not supposed to happen
{
qDebug() << "OUTTURN_MOVE";
}
else if(data.data1=="400001")
{
//表示当前房间正在进行游戏,是在玩家申请加入房间时服务端给出的提示
// 弹出窗口,提示当前用户换一个房间重新加入,正在进行游戏的房间不再让新的玩家加入
NewRoom* a =new NewRoom();
a -> show();
this -> close();
}
else if(data.data1=="400002") // is not supposed to happen
{
qDebug() << "ROOM_NOTRUNNING";
}
else if(data.data1=="400007") // is not supposed to happen
{
//表示有除上述错误以外的错误
qDebug() << "OTHER_ERROR";
}
}
}
2、然后是Wiget类里面提供的一些重要接口,也就是receive函数里面调用的接口,代码和详细的注释展示如下:
(这里选取的接口是比较重要的部分,是用来保存和设置棋盘初始化的信息,控制玩家的轮换,以及控制玩家的走棋的接口)
void Widget :: setorder() // 根据传进来的参数设置玩家的顺序
{
int temp = 0;
//下面按照顺时针的顺序依次判断各个位置,看看哪个位置上面被分配了玩家
// 轮换会由服务端进行控制,这里存储下顺序是为了AI部分的使用
// player_order[i]依次存储了第一个下棋到最后一个下棋的玩家顺序
if (ishere[1]) {temp++; player_order[temp] = 1;}
if (ishere[3]) {temp++; player_order[temp] = 3;}
if (ishere[5]) {temp++; player_order[temp] = 5;}
if (ishere[2]) {temp++; player_order[temp] = 2;}
if (ishere[4]) {temp++; player_order[temp] = 4;}
if (ishere[6]) {temp++; player_order[temp] = 6;}
//设置初始化参数
this->clickTime = 0;
this->control = player_order[1]; // 初始化是第一个玩家操作
this->select = nullptr;
// 调整视角,使得玩家的棋子在正下方,顺时针旋转
// 获取Coordinate_pos的值,对应表示每个玩家在展示画面的时候应该对应Coordinate的哪一个横坐标
// re_Coordinate_pos则应该是一个反向的函数,表示这个坐标下对应展现的是哪一个玩家的棋子
int temp_pos[10];
temp_pos[1] = 1; temp_pos[2] = 3; temp_pos[3] = 5;
temp_pos[4] = 2; temp_pos[5] = 4; temp_pos[6] = 6;
// temp_bias[i]代表i号玩家在正下方的时候应该顺时针转动多少个位置
// 这样设置就可以取消旋转的效果
// for (int i = 1; i <= 6; i++) temp_bias[i] = 0;
// 下面是带有旋转效果的情况
temp_bias[2] = 0; temp_bias[5] = 1; temp_bias[3] = 2;
temp_bias[1] = 3; temp_bias[6] = 4; temp_bias[4] = 5;
// 下面根据当前的玩家来确定每个玩家在展示的时候对应的位置
temp = 0;
for (int i = 1 + temp_bias[player]; i <= 6; i++)
{
temp++;
Coordinate_pos[temp_pos[temp]] = temp_pos[i];
re_Coordinate_pos[temp_pos[i]] = temp_pos[temp];
}
for (int i = 1; i <= temp_bias[player]; i++)
{
temp++;
Coordinate_pos[temp_pos[temp]] = temp_pos[i];
re_Coordinate_pos[temp_pos[i]] = temp_pos[temp];
}
// 根据参数绘制对应的棋盘
if (type == 2)
set2people_mode();
else if (type == 3)
set3people_mode();
else
set6people_mode();
}
void Widget :: player_turn(int _player) // 响应服务器进行轮换
{
// 移交控制权
this -> control = _player;
switch (_player)
{
case 1:
{
ui->label->setText("绿色下棋");
break;
}
case 2:
{
ui->label->setText("紫色下棋");
break;
}
case 3:
{
ui->label->setText("蓝色下棋");
break;
}
case 4:
{
ui->label->setText("橙色下棋");
break;
}
case 5:
{
ui->label->setText("黄色下棋");
break;
}
case 6:
{
ui->label->setText("红色下棋");
break;
}
default:
;
}
}
void Widget :: setmove(int _player, QString player_trail)
// 传送进来的这个player_trail对应表示的是玩家1在上方时候的斜坐标轨迹
{
int tempx[1000]; //存储行棋轨迹的坐标,这里存储的是展现在画面上的坐标轨迹
int temp_num = 0;
QString str = "";
int len = player_trail.length();
for (int i = 0; i <len; i++)
if (player_trail.at(i) == ' ')
{
temp_num++;
tempx[temp_num] = str.toInt();
str = "";
}
else str = str + player_trail.at(i);
// 最后一个没有空格不要忘记处理
temp_num++;
tempx[temp_num] = str.toInt();
// 转换后得到实际画面上对应的坐标
for (int i = 1; i <= temp_num; i += 2)
{
int x = tempx[i]; int y = tempx[i+1];
tempx[i] = reconvert_x(rotate_x(x,y),rotate_y(x,y));
tempx[i+1] = reconvert_y(rotate_y(x,y));
}
int startx = tempx[1]; int starty = tempx[2]; // 路径的起始点
int endx = tempx[temp_num - 1]; int endy = tempx[temp_num]; // 路径的结束点
// 最后我们更新一下内部保存的信息即可
int sx = get_Coordinatex(g[startx][starty]); int sy = get_Coordinatey(g[startx][starty]);
this->select = Coordinate[sx][sy].chess;
Update(startx, starty, endx, endy);
switch (_player)
{
case 1:
{
this->select->setIcon(QIcon(":/image2/12.png"));
break;
}
case 2:
{
this->select->setIcon(QIcon(":/image2/22.png"));
break;
}
case 3:
{
this->select->setIcon(QIcon(":/image2/32.png"));
break;
}
case 4:
{
this->select->setIcon(QIcon(":/image2/42.png"));
break;
}
case 5:
{
this->select->setIcon(QIcon(":/image2/52.png"));
break;
}
case 6:
{
this->select->setIcon(QIcon(":/image2/62.png"));
break;
}
}
this -> select = nullptr;
}
3、在Room类里面响应棋盘类发送的信号的槽函数的代码和注释如下所示:
void Room :: on_Leave_clicked()
{
// 在 client 离开房间之前,应向 server 发送 LEAVE_ROOM_OP 请求,并将数据段 data1 和 data2 分别设置为房间号和当前用户名
inRoom = 0; //代表离开了房间
NetworkData sendata = NetworkData(OPCODE::LEAVE_ROOM_OP, room_num, player_name);
this -> socket->send(sendata);
this -> close(); // 用户已经离开,关闭当前的页面
}
void Room :: try_move() // 对应的client发出move_op的请求
{
int pos = player_pos;
QString field;
switch (pos)
{
case 1:
{
field = "A";
break;
}
case 3:
{
field = "B";
break;
}
case 5:
{
field = "C";
break;
}
case 2:
{
field = "D";
break;
}
case 4:
{
field = "E";
break;
}
case 6:
{
field = "F";
break;
}
}
QString str;
//获取行棋的路径
str = winArray -> getTrail();
// 发送move_up的请求
NetworkData sendata = NetworkData(OPCODE ::MOVE_OP,field,str);
this -> socket->send(sendata);
}
4、另外,我们小组在第一阶段为每个点保存的都是直角坐标,而这里传送的一律为斜坐标,但是由于直角坐标下,我们实际绘制的是等腰三角形,所以不能用矩阵乘法的方法直接把直角坐标转换成斜坐标,具体转换的代码和注释如下所示:
int Widget :: convert_x(int _x, int _y) // 直角坐标转换得到斜坐标的x
{
int ty = (_y - center_y) / _edge;
ty = -ty;
int tx = (_x - center_x - ty * half_edge) / (half_edge * 2);
return tx;
}
int Widget :: convert_y(int _y) // 直角坐标转换得到斜坐标的y
{
int ty = (_y - center_y) / _edge;
ty = -ty;
return ty;
}
int Widget :: reconvert_x(int _x, int _y) // 斜坐标转换成直角坐标的x
{
int tx = (_y * half_edge) + (_x * half_edge * 2) + center_x;
return tx;
}
int Widget :: reconvert_y(int _y) // 斜坐标转换成直角坐标的y
{
int ty = - (_y * _edge);
ty = ty + center_y;
return ty;
}
四、遇到的问题以及解决方法¶
我觉得这一阶段的任务主要的难点是要构建起整体的框架。因为给出的任务并没有非常具体的要求,刚开始对整个程序运作并没有一个具体的概念,所以整个程序应该由哪些窗口搭建起来,怎样构造才是一个可行的方案是一个比较困难的部分。所以我在网上看了一下别人做的跳棋游戏运行起来的成果展示,理清楚了应该怎么搭建这个框架。
另外一个难点就是数据传送部分的处理,因为每一个部分的实现都会相互影响,整体的实现需要一个和谐统一的标准,和比较严格具体的功能划分。我在编写代码的过程中,会遇到觉得之前写的某一部分不合适的情况,那么前面有一些细节就得重新再改,这一部分会比较花时间,也是比较麻烦的部分,但与此同时我也从中学习到了不少经验。
然后针对每一个传送的数据的操作,并不难处理,主要的功能性的部分也在第一阶段就已经完成,我们只需要特别注意协调所有的客户端的操作,保存每个对象需要的信息就行,
五、代码运行截图¶
首先是展示整个代码框架流程的截图:¶







然后是客户端在下棋过程当中的截图:¶
