跳转至

跳棋游戏第二阶段实验报告

一、小组分工

周小琳 : 代码的编译和调试,调整qt上的显示效果,编写实现用于测试客户端的服务端,添加了展示玩家列表和实现倒计时的功能,上传作业。

杨爽 : 第二阶段客户端主要代码编写,和周同学一起调试代码,写实验报告。

赵铎淞:搜集图片素材,游戏试玩反馈。

二、代码框架设计

第二阶段主要是要搭建起整个游戏实现的基本框架。

我们需要一个用于测试的服务端,然后主要编写客户端(由于我们小组只是想实现客户端,所以编写的服务端仅仅是用于测试的,点击后会有一个空白的窗口产生,然后后台的服务端会开始运行)。

我们可以通过阅读助教提供的代码,发现每生成一个客户端的窗口,都需要一个NetworkSocket的对象和它进行绑定,从而实现服务端和客户端之间的信息的传送,所以这个和NetworkSocket绑定的类对象,能够直接接触到棋盘类对象,从而能够对其进行一些操作并接受棋盘类对象发送的数据。再考虑到我们需要给游戏用户提供的功能(包括单机模式和联机模式的选择,几人模式的选择,输入房间号昵称等),可以设计整个程序的框架如下所示:

image-20220609201135029

程序运行后会生成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;
}

四、遇到的问题以及解决方法

我觉得这一阶段的任务主要的难点是要构建起整体的框架。因为给出的任务并没有非常具体的要求,刚开始对整个程序运作并没有一个具体的概念,所以整个程序应该由哪些窗口搭建起来,怎样构造才是一个可行的方案是一个比较困难的部分。所以我在网上看了一下别人做的跳棋游戏运行起来的成果展示,理清楚了应该怎么搭建这个框架。

另外一个难点就是数据传送部分的处理,因为每一个部分的实现都会相互影响,整体的实现需要一个和谐统一的标准,和比较严格具体的功能划分。我在编写代码的过程中,会遇到觉得之前写的某一部分不合适的情况,那么前面有一些细节就得重新再改,这一部分会比较花时间,也是比较麻烦的部分,但与此同时我也从中学习到了不少经验。

然后针对每一个传送的数据的操作,并不难处理,主要的功能性的部分也在第一阶段就已经完成,我们只需要特别注意协调所有的客户端的操作,保存每个对象需要的信息就行,

五、代码运行截图

首先是展示整个代码框架流程的截图:

image-20220609221807253

image-20220609221839802

image-20220609221858143

image-20220609221930607

image-20220609221953562

image-20220609222239085

image-20220609222512530

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

image-20220609222315891