[Qt] 我的一款射击游戏及设计模式 - Hori Miona

     【更新】我的新博客:www.ryuzhihao.cc,当然这个csdn博客也会更新

              本文在新博客中的链接:点击打开链接


     时间:2017年7月28日-7月30日

     耗时:2天

       此前一直在玩OpenGL,做的都是三维世界的东西,已经好久没有写过2D类的小游戏了。上学期,玩了《charles》之后,就一直想要写一款类似的射击类小游戏,只是前段时间有很多跟图形学有关的程序还要填坑,而且还要忙着参加夏令营。到现在终于闲了下来,就开始着手做一下这款小游戏《Hori Miona》吧。(在家一周也没见体重长回去多少)

       这篇博文,可能更多的涉及到“游戏开发架构”。这俩天对于我自己来讲,也算是是摸索出了比较不错的架构。不管是3D还是2D游戏,render和modeling虽然不同,但是架构应该是个可以通用的模式。

       在正文开始之前,还是惯例上一下我的实现结果。


射击游戏《Hori Miona》

一、下载链接:

       http://download.csdn.net/detail/mahabharata_/9916762

二、游戏截图:

  





本游戏的部分设计细节

一、类图

      程序的可视化界面为GameWidget类,并进行游戏循环,利用计时器Timer更新游戏状态。玩家Player类,用于记录玩家的状态信息;同时,玩家的所有操作由状态控制,每次游戏循环中,根据玩家当前所处的行为状态(攻击、移动?),更新玩家的信息。在Player类中,还有一个攻击管理器AttackManager,用于对玩家的攻击进行处理,包括:攻击的渲染、攻击模式的转化、攻击的碰撞检测等等。此外,在GameWidget中,还有一个成员EnemyManager,用于对游戏中的AI单位进行控制,根据玩家的当前位置,更新敌人的状态。

      如下是简化的类图(在进行大致设计时采用):

 

二、基于四叉树的场景管理。

       在游戏中的敌人的数目,这里采取了基于四叉树的场景管理的方法。虽然前面的类图在设计阶段是list<Enemy>来代替的。但是为了提高检索效率还是必须要引入四叉树进行场景对象的管理。

      叉树并不仅仅针对二维的游戏场景上,这里所指的二维是因为四叉树本身仅仅考虑了两个维度。在三维的游戏场景中,我们也可以使用四叉树,因为在四叉树应用最广泛的室外地形场景中,z轴往往只代表地形高度信息,或者说它的信息相比起xy轴而言,并没有特别重要。这是我们在场景管理中可以适当忽略的部分。

      四叉树有很多变种,先谈一个简单的情况,就是假设所有物体是一个点,这样比较容易理解。在后面可以利用AABB包围盒进行碰撞检测。

      把每个点放到正方形空间里,若该正方形内含有超过1个点(2个或2个以上的点),就把该正方形分割,直至每个小正方形(叶节点)仅含有一个点,就可以得出以下的分割结果:

       

       这个做法是可以调整的,也就是说当空间分割到一定大小就不能继续分割(如最多只能对一个结点分割4层,因为层数太多也会影响检索效率)。关于四叉树节点的数据结构可按如下进行定义:

 struct treeNode {
       bool isLeaf;   //是否是叶节点

       // 四个子树
       treeNode* upLeft;  
       treeNode* upRight;
       treeNode* downLeft;
       treeNode* downRight;       

       int objectNum; //包含对象个数
       List<Object> objects; //包含的对象

       AABB box;     //所在的AABB包围盒
}
          了解了最基本四叉树后,可把问题从质点扩充到占有一定体积的物体。虽然我们可以每次比较场景物体和正方形的相交,但是为了提高性能,一般使用物体的包围盒进行碰撞检测:

           这里推荐我的另外一篇文章《对Obj模型的AABB包围盒加载器》:http://blog.csdn.net/mahabharata_/article/details/72593925


三、状态图在游戏中的应用:

        相比起一般的软件开发,游戏开发对状态图的依赖应该是比较强的了。因为游戏中通常会涉及很多复杂的状态转换过程。比如:实现飞机移动的状态转换。这里也简单拿这个例子来进行一下说明:在程序中,飞机的非战斗状态主要包括以下几种行为(用枚举类型表示):

class Player
{
private:

    // 飞机运动状态
    enum STATE
    {
        _STOP,      // 停止
        _RUN,       // 直行
        _LEFT,      // 静止左转
        _RIGHT,     // 静止右转
        _RUN_LEFT,  // 运动左转
        _RUN_RIGHT  // 运动右转
    };
    
    short m_curState;  // 飞机当前运行状态
    
public:
    void setCurrentState(short state);  // 设置飞机的当前状态
    short getCurrentState();            // 获取飞机的当前状态


    void updateStates();  // 更新

    void render(QPainter* painter);
};

     这是玩家类Player的简化代码,预设了6种非战斗的状态。为了能理顺完整的状态转换,因此设计了如下的状态图(为了方便绘制,没有体现左转的情况,但是可以对称得到):


       在有了上面的状态图的情况下,便可以非常轻松地写出相应的状态转换代码:

void GameWidget::keyPressEvent(QKeyEvent *event)
{
    if(event->isAutoRepeat())
        return;
    switch(event->key())
    {
    case Qt::Key_W:              // 前进
        if(m_player.getCurrentState()==Player::_STOP)
            m_player.setCurrentState(Player::_RUN);
        if(m_player.getCurrentState()==Player::_LEFT)
            m_player.setCurrentState(Player::_RUN_LEFT);
        if(m_player.getCurrentState()==Player::_RIGHT)
            m_player.setCurrentState(Player::_RUN_RIGHT);
        break;
    case Qt::Key_A:             // 左
        bA = true;
        if(m_player.getCurrentState()==Player::_STOP)
            m_player.setCurrentState(Player::_LEFT);
        if(m_player.getCurrentState()==Player::_RIGHT)
            m_player.setCurrentState(Player::_LEFT);
        if(m_player.getCurrentState()==Player::_RUN || m_player.getCurrentState()==Player::_RUN_RIGHT)
            m_player.setCurrentState(Player::_RUN_LEFT);
        break;
    case Qt::Key_D:             // 右
        bD = true;
        if(m_player.getCurrentState()==Player::_STOP)
            m_player.setCurrentState(Player::_RIGHT);
        if(m_player.getCurrentState()==Player::_LEFT)
            m_player.setCurrentState(Player::_RIGHT);
        if(m_player.getCurrentState()==Player::_RUN || m_player.getCurrentState()==Player::_RUN_LEFT)
            m_player.setCurrentState(Player::_RUN_RIGHT);
        break;
    case Qt::Key_J:             //攻击
        m_attackManager.setAttacked(true);
        break;
    case Qt::Key_Space:        // 切换攻击模式
        m_attackManager.changeAttackMode();
        ui->lblAttackMode->setText(m_attackManager.getAttackMode());
        break;
    case Qt::Key_R:
        if(m_timer.isActive())
            m_timer.stop();
        else
            m_timer.start();
        break;
    case Qt::Key_Escape:
        m_timer.stop();
        emit sig_closeGameWidget();
        break;
    }
}

void GameWidget::keyReleaseEvent(QKeyEvent *event)
{
    if(event->isAutoRepeat())
        return;

    switch(event->key())
    {
    case Qt::Key_W:
        if(m_player.getCurrentState()==Player::_RUN)
            m_player.setCurrentState(Player::_STOP);
        if(m_player.getCurrentState()==Player::_RUN_LEFT)
            m_player.setCurrentState(Player::_LEFT);

        if(m_player.getCurrentState()==Player::_RUN_RIGHT)
            m_player.setCurrentState(Player::_RIGHT);
        break;
    case Qt::Key_A:
        bA = false;
        if(m_player.getCurrentState() == Player::_RUN_LEFT && !bD)
            m_player.setCurrentState(Player::_RUN);
        if(m_player.getCurrentState() == Player::_LEFT && !bD)
            m_player.setCurrentState(Player::_STOP);

        if(m_player.getCurrentState() == Player::_RUN_LEFT && bD)
            m_player.setCurrentState(Player::_RUN_RIGHT);
        if(m_player.getCurrentState() == Player::_LEFT && bD)
            m_player.setCurrentState(Player::_RIGHT);

        break;
    case Qt::Key_D:
        bD = false;
        if(m_player.getCurrentState() == Player::_RUN_RIGHT && !bA)
            m_player.setCurrentState(Player::_RUN);
        if(m_player.getCurrentState() == Player::_RIGHT && !bA)
            m_player.setCurrentState(Player::_STOP);

        if(m_player.getCurrentState() == Player::_RUN_RIGHT && bA)
            m_player.setCurrentState(Player::_RUN_LEFT);
        if(m_player.getCurrentState() == Player::_RIGHT && bA)
            m_player.setCurrentState(Player::_LEFT);
        break;
    case Qt::Key_J:
        m_attackManager.setAttacked(false);
        break;
    }
}

四、火焰碰撞的矩阵计算:

       在2D游戏的开发中,其实会发现和3D游戏有很多相似之处。用到的主要数学方法有:

       变换矩阵、 插值、图像遮罩等等。

       这里介绍其中一个处理方法:火焰跟随飞机旋转的效果——借助变换矩阵去实现。

        这个和OpenGL中三维变换矩阵的计算类似,只是相应的转换成了2维,但是为了方便计算,我们还是要使用齐次坐标。火焰虽然在视觉上看到的是不规则且不断变化的图形,但是在游戏中,我是用的是一个凸多边形作为一个检测区域,如下图:

                 

      已知玩家的坐标pos(Vec3),和倾斜角度dir(float)。那么我们可以首先确定一个原始的菱形(顶点坐标pi已知,为齐次坐标x,y,w),再计算相应的三维变换矩阵matrix(3*3),将顶点坐标pi乘以三维变换矩阵matrix便可求出火焰的范围。

     计算的数学过程如下:

            (1) 已知火焰顶点pi(x,y,w),和变换角度dir,

            (2) 计算pi到火焰中心位置的平移矩阵mat1;

                      计算绕火焰中心位置旋转dir角度的旋转矩阵mat2;

                      计算从火焰中心到原位置pi的平移助阵Mat3;

            (3) 得到三维变换矩阵matrix = mat3* mat2*mat1;

            (4) 由点pi* matrix即可得出变换后的坐标pi'。

可以用如下代码得到:

    // 将 pos从玩家中心移动到火焰中心
    pos.setX(pos.x()+sin(3.14*dir/180.0)*90);  
    pos.setY(pos.y()-cos(3.14*dir/180.0)*90);

    QPolygonF polygon;          // 未变换的多边形
    polygon<<QPointF(pos.x(),pos.y()-50)
           <<QPointF(pos.x()-35,pos.y()+35)
           <<QPointF(pos.x(),pos.y()+75)
           <<QPointF(pos.x()+35,pos.y()+35);

    QMatrix matrix;   // 计算旋转矩阵matrix
    matrix.translate(pos.x(),pos.y());    // 平移到火焰中心
    matrix.rotate(dir);    // 旋转
    matrix.translate(-pos.x(),-pos.y());   // 平移回原位置

    polygon = polygon*matrix;   // 多边形的坐标



结语:

         整体来说,这个项目的代码量要在2天时间内完成,还算是比较多的。想要在博客里面完全阐述所有的设计细节,真的是不太可能,但是欢迎交流。本程序是个娱乐工作,所以代码在整理完成后,会抽时间全部上传。


©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页