【c++实现单机/多人联机 TankTrouble(坦克动荡) (一) —— 单机模式

项目地址: https://github.com/JustDoIt0910/TankTrouble

Server地址:https://github.com/JustDoIt0910/TankTroubleServer

TankTrouble(坦克动荡) 是一款很有意思的小游戏,我是前段时间刷b站看到有人剪辑的精彩操作才知道的这个游戏。

在这里插入图片描述

规则很简单,发射炮弹击中对方,同时躲避炮弹,炮弹可以在墙壁反弹,每发炮弹反弹一定次数就会消失。每名玩家同一时刻最多有5发炮弹在场上(只有当先前的炮弹消失后才能继续开火)。无论被谁的炮弹击中都会死亡。

我用c++17和gtkmm 3.0 实现了这个游戏,支持单机模式和多人online。在单机模式下,你的对手是一个躲闪和攻击技能都很强的AI,在online模式下,可以建立多人房间,和好友联机对战。

这里有完整演示 demo

(注: 我的实现是linux版本,在ubuntu 18 和 22.04上测试过,ubuntu 22.04 自带gtkmm-3.0, 不必额外安装,否则需要安装gtkmm-3.0)

apt-get install libgtkmm-3.0-dev

下面是单机模式下效果片段(黑色是人机,红色是玩家)

在这里插入图片描述

这一篇先说说整个PC端的设计与单机模式的实现,下篇再说online模式和服务器的实现

一、 项目结构

  • controller ------------------ 可以理解为后端,负责游戏逻辑,数据更新
    • LocalController.h LocalController.cc --------------- 单机模式下的controller, 负责游戏所有逻辑
    • OnlineController.h OnlineController.cc ----------- 联网模式下的controller, 负责和服务器交互,更新数据
  • ev ----------------------------- 参考muduo实现的极简事件驱动网络库,游戏逻辑在单独线程中驱动,独立于gui线程。联网模式下还要负责网络通信
  • event ---------------------------------- 将游戏中的操作封装成事件,方便融合进事件驱动模型
  • protocol ------------------------------ 通信协议部分,只在联网模式下用到
  • smithAI -------------------------------- 人机的所有逻辑,包括危险躲避,索敌,攻击等
  • util -------------------------------------- 主要是数学工具,包含游戏中用到的几何、向量计算、碰撞检测等
  • view ------------------------------------ 类比前端,所有gui界面
  • Controller.h Controller.cc------- LocalController和OnlineController的基类
  • Maze.h Maze.cc -------------------- 地图生成算法
  • Object.h Object.cc ---------------- 游戏中对象的多态基类
  • Tank.h Tank.cc --------------------- 坦克对象,继承Object
  • Shell.h Shell.cc --------------------- 炮弹对象,继承Object
  • Block.h Block.cc ------------------- 墙,不继承Object,根据地图生成,单独管理
  • Window.h Window.cc ----------- gui主类,管理所有views
  • defs.h --------------------------------- 游戏中一些宏定义

在这里插入图片描述

项目结构大致可以分为两层,Window和其管理的所有View,是gui部分,负责与用户交互,渲染游戏画面等等,这部分运行在gtkmm的gui主线程中。另一部分是控制层,负责单机和online的游戏逻辑,这部分运行在单独的control thread,由一个事件循环驱动(control loop)。

之所以把Controller放到独立于gui主线程的另一个线程中,由我自己的事件驱动库驱动,主要有几个考虑

  • 符合线程的单一职责原则
  • gui线程中不应该执行相对耗时的计算,单机模式中人机的计算量还是比较大的,都塞进gui线程中会降低界面响应,online模式更不用说了,gui线程处理网络通信肯定是不合适的。
  • 可以让view和controller完全解耦,view层不需要知道当前是LocalController还是OnlineController在向它提供数据,它的职责仅仅是拿到数据,绘制,有键盘事件。向controller报告,view层的代码不需要为不同Controller作修改。

所以LocalController和OnlineController都继承Controller,对外暴露统一的接口。区别在于游戏对象是自己管理还是从服务器拿的而已。

二、游戏对象和地图的实现

1. 游戏对象定义

对象类图如下,Object是Tank和Shell的虚基类,定义了共有的数据成员,包括当前位置 posInfo,下一步位置 nextPos,

颜色 color,对象id,以及移动状态movingStatus。Tank独有的数据成员是它的四个顶点坐标,还有剩余炮弹数,Shell的 ttl 是生命期,每反弹一次减一,减到0就会消失,tankId 是发射这颗炮弹的坦克的id。

在这里插入图片描述

一个游戏对象的位置信息用 PosInfo 结构体表示

struct PosInfo
{
    PosInfo(const util::Vec& p, double a): pos(p), angle(a){}
    PosInfo& operator=(const PosInfo& info) = default;
    bool operator==(const PosInfo& info) const {return (pos == info.pos && angle == info.angle);}
    PosInfo(): PosInfo(util::Vec(0.0, 0.0), 0){}
    static PosInfo invalid() {return PosInfo{util::Vec(DBL_MAX, DBL_MAX), DBL_MAX};}
    bool isValid() const
    {return (pos.x() != DBL_MAX && pos.y() != DBL_MAX && angle != DBL_MAX);}

    util::Vec pos;

    double angle;
};

其中 pos 是中心坐标,angle是相对于x轴正半轴的顺时针旋转角(°),所有与游戏对象相关的数学计算也都离不开这两个参数。

除此之外一个Object一定有其移动状态,对于坦克来说,有前进(forward),后退(backward),顺时针旋转(rotateCW),逆时针旋转(rotateCCW)等,对于炮弹则只有前进状态。Tank和Shell都实现Object中的虚函数 draw(), getCurrentPosition(), getNextPosition() 等等,LocalController存储并管理一个std::unique_ptr 的多态列表。

2. 对象的移动

Tank 和 Shell都重写了getNextPosition()方法,这个方法会根据对象的移动状态 movingStatus 和步长,计算下一步的位置。已知当前坐标(x, y),旋转角angle,计算下一步的坐标,是由util::polar2Cart() 完成的,方法名是"极坐标转笛卡尔坐标"的缩写,因为可以将下一位置看作以当前坐标(x, y)为极点O,ρ = 步长,θ = angle的极坐标,将它转换为直角坐标就是下一步的位置了

util/Math.cc

double deg2Rad(double deg){return deg * M_PI / 180;}

Vec polar2Cart(double theta, double p, Vec O)
{
    double x = O.x() + cos(deg2Rad(theta)) * p;
    double y = O.y() - sin(deg2Rad(theta)) * p;
    return Vec(x, y);
}
  • 炮弹的移动

    炮弹只有一种前进状态,而且几何形状是圆形,所以移动很简单,计算出下一位置坐标就好了。

    //这个是Shell的静态方法,因为有时候需要在没有对象实例的情况下,仅仅进行位置的一些计算,就需要对应的静态方法,下边Tank的也同理
    Object::PosInfo Shell::getNextPosition(const Object::PosInfo& cur, int movingStep, int rotationStep)
    {
        if(movingStep == 0)
            movingStep = SHELL_MOVING_STEP;
        Object::PosInfo next = cur;
        next.pos = util::polar2Cart(cur.angle, movingStep, cur.pos);
        return next;
    }
    
    //这是Shell的成员方法
    Object::PosInfo Shell::getNextPosition(int movingStep, int rotationStep)
    {
        Object::PosInfo next = getNextPosition(posInfo, movingStep, rotationStep);
        nextPos = next;
        return next;
    }
    
    void Shell::moveToNextPosition() {posInfo = nextPos;}
    
  • 坦克的移动

    坦克移动状态复杂一些,可以同时处于旋转和移动状态,而且每次移动后要重新计算矩形四个顶点坐标。

    //这个是Tank的静态方法
    Object::PosInfo Tank::getNextPosition(const Object::PosInfo& cur, int movingStatus, int movingStep, int rotationStep)
    {
        if(movingStep == 0)
            movingStep = TANK_MOVING_STEP;
        if(rotationStep == 0)
            rotationStep = Tank::ROTATING_STEP;
        Object::PosInfo next = cur;
        //顺时针旋转状态
        if(movingStatus & ROTATING_CW)
            next.angle = static_cast<int>(360 + cur.angle - rotationStep) % 360;
         //逆时针旋转状态
        if(movin
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值