您的位置:首页 > 编程语言 > Qt开发

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

2017-08-01 11:41 363 查看
     【更新】我的新博客: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天时间内完成,还算是比较多的。想要在博客里面完全阐述所有的设计细节,真的是不太可能,但是欢迎交流。本程序是个娱乐工作,所以代码在整理完成后,会抽时间全部上传。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息