从零开始的2.5D游戏开发

 

游戏按照镜头视角来分,可以分为2D游戏、3D游戏,除此之外还有一类游戏被称为2.5D游戏。这是一个比较有争议的分类,这个分类有着不同的解释。有的人认为这只是厂商的噱头,它本身就是2D游戏(我曾经也这么认为,直到亲自做了一款2.5D游戏);也有的人认为他是介于2D和3D之间的一种游戏类型,通常把斜视角的2D游戏称作2.5D游戏。

历史

2001年,盛大游戏推出了一款大型多人在线角色扮演游戏(MMORPG),风靡大江南北,那就是《热血传奇》。《热血传奇》被誉为中国网游的鼻祖,除了它很多开创性的玩法以外,更重要的在于在那个年代其画面的震撼程度。在那个年代,市面上很少看见画面如此精美的真3D游戏,不少人都被其画面视角蒙蔽,认为这就是一款精美的3D游戏。网络上有这么一句俗话:“游戏之于玩家,就像女人之于男人,必须重于画面”。《热血传奇》这一手以假乱真的伪3D,忽悠得不少懵懂无知的少年为其欲罢不能。

热血传奇新手村

对比起同时代的Pokémon等2D游戏来说,其3D感更强。其中原因,除了绘画的精致外,更重要的是镜头朝向着游戏世界的侧面,而不是那么“耿直”的朝向游戏世界的正面。

宝可梦

除了《热血传奇》,还有一些即时战略游戏(RTS)游戏也尝试了2.5D视角,并取得了成功,比如《红色警戒》、《星际争霸》。

随着游戏渲染技术的发展,大部分游戏都已经抛弃了2.5D的做法,转而做纯3D游戏了,但是现如今也还有一批游戏仍旧以2.5D的方式去渲染游戏。简单的分析可能是基于以下几点因素:

  • 游戏成本限制,3D游戏需要复杂于2D游戏的制作流程、人员配置,会提高游戏成本
  • 3D模型细节程度和画面渲染效率成反比,为了在高效运行效率的前提下,保留更多的模型细节

比如绝大部分“三消+”游戏,代表就是俄罗斯公司Playrix的《梦幻家园》,可见房间内部的每一个物件的细节都很精美地表现了出来。这些细节要是用3D模型的方式去做将会带来巨大的性能开销。

梦幻家园截图

问题

2.5D游戏仅仅是在2D游戏基础上把视角横向旋转了45度,但是制作难度却高出了不少。

2.5D视角带来的最核心的问题是每个图片和其他图片之间的遮挡关系如何处理,才能更符合人类对3D世界的常识性认知呢,也就是用2D的方式来模拟3D。

2D游戏的做法很简单粗暴。2D游戏世界中每一个物件都会用一个2维坐标来表示其位置,x表示其横向位置,y表示其纵向位置。当一个物件的y值越小,也就是其越靠近画面底部,则渲染顺序越靠后。就像一个画家在Photoshop上作画一样,离相机越近的图层要越后面画,才能盖住离相机远的图层,所以画家要从远到近地画。

但是2.5D却不能用这么简单的规则去解决问题,如下图所示。

2.5D物件摆放

P1点的y值大于P2点的y值,如果按照2D游戏的做法,桌子应该是挡住花瓶的,那样就无法模拟3D世界了。

建立坐标系

在构建坐标系之前,我们需要了解真3D游戏是如何渲染出来的。3D游戏的渲染,简单来说可以理解为将三维数据在二维平面上做投影的过程。所以所谓的3D游戏,呈现在玩家面前依然是一个二维的画面,三维空间中的物件移动表现在二维画面上,也就是二维坐标位置的移动而已。

基于此结论,为了尽可能模拟3D视角下物件的移动表现,我们建立一个新的坐标系,这个坐标系是基于视口平面的坐标系,如下图所示。

 

 

x,y,z三个向量在游戏引擎的3D世界空间中,其实是跟相机视口共面的。我们以这三个向量为基底,构建出一个3D空间坐标系,下文中我们把这个空间坐标系命名为Iso空间。

如图所示,从视觉上我们很容易将图中菱形元素看做一个斜视角下的立方体,如果立方体在Iso空间中位置的x值增大,那么立方体将会向屏幕的右上方移动,视觉上好像立方体向着自己的水平方向移动了一样。因此我们完全可以在一个2D平面上通过斜方向移动图片,来欺骗视觉,造成物件在3D空间中移动的错觉。

视角选择

通常2.5D游戏会被人们称为斜45度视角游戏,但是斜45度真的是观察3D物件最佳的倾斜角度吗?

评估最佳与否,我们姑且确定一个标准,在不影响美观的前提下,尽可能地简化开发难度,节约开发成本。

要做这个评估,我们首先得了解3D物件投影到屏幕上的具体情况。

立方体正交投影

如图所示,如果我们要把3D空间中的一个立方体(图中蓝色部分)显示到相机屏幕(图中红色部分)上,将会经历上图投影变换。

假设现在要把线段AB投影成相机平面上的线段A'B',我们可以作两条辅助线BB'和AA',使得BB'平行于AA',过A点作辅助线AC垂直于BB',那么求A'B'就转化为求AC。

由三角函数公式可以知道,AC = AB * sin(θ),又因为θ就是相机的俯仰角,即为我们的所求。所以3D线段和投影线段之间的关系为:投影线段 = 3D线段 * sin(俯仰角)。

再回到3D相机视角,如果相机正对着的是一个正方形,那么斜视角下投影到相机屏幕上将会是一个菱形,这个菱形的宽高比随着相机俯仰角度不同而不同。为了模拟表现出这种视角关系的画面,美术人员就需要做一张菱形的图片贴在屏幕上,以拟合正交3D投影。

如上图所示,如果美术想表示一个3D世界中的正方形,那么他需要制作一张菱形贴图(虚线框内部分)。由于相机俯仰运动时的旋转轴平行于AB方向,故而AB的长度即为正方形在3D世界中的真实长度。CD长度垂直于俯仰旋转轴,根据上述公式可得CD长度=3D世界中正方形CD长度 * sin(俯仰角)。

为了切图方便,美术会把贴图的长宽像素值都设为整数,因为美术无法切出0.5个像素的图片,那么就要求AB和CD都是整数。如果俯仰角θ也是整数,将会让美术很方便地调整模型制作软件(如Maya、3ds max等)的相机角度,通过3D渲染,将3D模型烘焙成2D图片,以供游戏中直接使用。

遍历三角函数查找表,只有sin(30°)的分子分母都为整数,也就是说只有30°这个角度有可能让长宽都为整数,具体可参看尼文定理:

Niven's Theorem - ProofWiki​proofwiki.org

 

故而30°俯视角是最佳倾斜角,也就是说图片宽高比为1:2

通过观察与分析,市面上许多游戏都是斜30°视角游戏,例如《梦幻家园》。这些游戏都通过实践验证了最佳倾斜角理论。因此人们通常说的斜45度视角游戏只是人们通过臆测而给2.5D游戏取得俗名,准确来说我们应该称这类游戏叫做斜30度视角游戏。或者可以采用另一种对斜45度视角游戏的解释,斜45度指的是相机水平方向上(围绕世界空间Y轴)的旋转角度。

空间变换

上文讲到我们为了模拟3D运动,建立了一套坐标系,那么这套坐标系和世界空间坐标系有什么样的联系呢?

通常为了描述两个坐标系之间的关系,我们会求出一个变换矩阵,通过变换矩阵和变换矩阵的逆矩阵,将两个不同坐标系下的对应点位置进行转换。

那么,Iso空间的坐标点是如何转化为世界空间坐标点的呢?下图展示了一个Iso空间下的正方形如何转化为世界空间下的菱形的过程。

Iso空间到世界空间的变换

由图可知,为了做这个变换,需要三个关键的参数:tileSize、tileAngle、tileRatio。例如图中所表达的意思就是:正方形大小为16个Unit;相机水平旋转45度(通过围绕世界空间Z轴旋转图片来模拟);相机倾斜30°,也就是上文中所说宽高比为1:2(通过Y方向缩放图片大小来模拟)。我们可以定义一个类IsoWorld来存放这些变换所需的参数,以及提供空间变换的接口。

public class IsoWorld
{
    float tileSize { get; set; }    // 图中的A值
    float tileAngle { get; set; }   // 图中的D值(其实不准确是D值,参考变换过程动图)
    float tileRatio { get; set; }   // 图中的B和A的比值:B除以A
    float tileHeight { get; set; }  // 图中的C值

    // 从Iso空间的三维坐标转换为屏幕上世界空间的二维坐标
    // iso          - Iso空间的三维坐标
    // [return]     - 转换后的屏幕上世界空间的二维坐标
    Vector2 IsoToScreen(Vector3 iso);

    // 从屏幕上世界空间的二维坐标转换为Iso空间的三维坐标
    // pos          - 屏幕上世界空间的二维坐标
    // iso_z        - 指定的Iso空间Z值
    // [return]     - 转换后的Iso空间的三维坐标
    Vector3 ScreenToIso(Vector2 pos, float iso_z = 0.0f);
}

其中核心的坐标变换,也就是变换过程的动图效果的实现代码如下所示。

// public static Vector3 vec3OneZ { get { return new Vector3(0.0f, 0.0f, 1.0f); } }

_isoMatrix =
    Matrix4x4.Scale(new Vector3(1.0f, tileRatio, 1.0f)) *
    Matrix4x4.TRS(
        Vector3.zero,
        Quaternion.AngleAxis(90.0f - tileAngle, IsoUtils.vec3OneZ),
        new Vector3(tileSize * Mathf.Sqrt(2), tileSize * Mathf.Sqrt(2), tileHeight));

因此可以给每一个物件定制一个组件,更直观地控制该物件的Iso坐标。

从上图可以看出,我们不再去操作transform.position,取而代之的是通过修改IsoObject.position去调整物件位置。

遮挡排序

上文中已经构建出一套完整的坐标系体系,这套坐标系不仅可以和世界空间相互转换,还能像3D工程一样去思考问题、摆放物件。这点很重要,虽然维度从二维升为了三维,但是这让问题更容易理解,因为可以借用3D工程解决问题的思路去解决2D工程很难处理的一些问题。游戏世界维度的上升反而使得思考问题的难度下降了。

那么回到最开始我们提出的问题上,如何去解决2.5D游戏的遮挡排序问题呢?

3D游戏中,通常是离摄像机越远的物体越先绘制,那么怎么判断一个物件离相机远还是近呢?假设给每一个物件都套上一个AABB盒,那么通过AABB盒去判断离摄像机远近将会是一件非常容易的事。

同理,在Iso空间中,我们给每一个物件生成一个AABB盒,盒子恰好能包裹住物件,如下图(左)所示红色外框就是物件的AABB盒。

在2.5D世界中根据AABB盒,怎么确定一个物件是先渲染还是后渲染呢?如上图(中)所示。

  • 如果B物体的AABB盒在A物体的AABB盒的「黄色面」或者「绿色面」之后,那么A物体遮挡B物体,B物体要先于A物体绘制。
  • 如果B物体的AABB盒在A物体的AABB盒的「蓝色面」之下,那么A物体叠加在B物体之上,B物体要先于A物体绘制。
  • 如果A物体的AABB盒和B物体的AABB盒相交,如上图(右),那么相交立方体V取最薄的方向,决定使用「黄色面」、「绿色面」还是「蓝色面」来判断位置关系,再返回前两条规则继续计算。
例如如图(右)所示,因为A物体和B物体相交形成的立方体在X轴方向上最薄,所以需要以「绿色面」来判断A和B的位置关系。

核心代码如下。

// 判断AB物体绘制的先后顺序
// a_min        - A物体AABB盒的最小坐标值
// a_size       - A物体AABB盒的尺寸
// b_min        - B物体AABB盒的最小坐标值
// b_size       - B物体AABB盒的尺寸
// [return]     - 是否B物体要先于A物体绘制
bool IsIsoObjectDepends(Vector3 a_min, Vector3 a_size, Vector3 b_min, Vector3 b_size)
{
    a_min = a_min - a_size * 0.5f;
    b_min = b_min - b_size * 0.5f;

    var a_max = a_min + a_size;
    var b_max = b_min + b_size;
    var a_yesno = a_max.x > b_min.x && a_max.y > b_min.y && b_max.z > a_min.z;
    var b_yesno = b_max.x > a_min.x && b_max.y > a_min.y && a_max.z > b_min.z;
    // 如果A和B有相交部分
    if (a_yesno && b_yesno)
    {
        var da_p = new Vector3(a_max.x - b_min.x, a_max.y - b_min.y, b_max.z - a_min.z);
        var db_p = new Vector3(b_max.x - a_min.x, b_max.y - a_min.y, a_max.z - b_min.z);
        var dp_p = a_size + b_size - IsoUtils.Vec3Abs(da_p - db_p);
        // 比较相交部分立方体的X、Y、Z三个方向的厚度,取最薄的部分来作位置关系判断
        if (dp_p.x <= dp_p.y && dp_p.x <= dp_p.z)
        {
            return da_p.x > db_p.x;
        }
        else if (dp_p.y <= dp_p.x && dp_p.y <= dp_p.z)
        {
            return da_p.y > db_p.y;
        }
        else
        {
            return da_p.z > db_p.z;
        }
    }
    // 如果a_yesno == true && b_yesno == false,则说明B物体要先于A物体绘制
    return a_yesno;
}

总结

本文代码主要思想提取自一款名为Isometric 2.5D Toolset的插件。本文主要介绍了一种2.5D游戏的实现的底层方案,以及介绍了其原理,解决了以下问题:

  • 3D世界空间和2.5D世界空间的相互转换
  • 2.5D世界中的遮挡排序算法

如果有兴趣可以详细研究一下该插件的API及其源代码,除了以上问题,插件还提供了3D&2D混合排序、2.5D物理、鼠标输入处理等功能。谢谢观看。

 转自https://zhuanlan.zhihu.com/c_1236676342307475456

  • 4
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 1. 概述 3 1.1 实训项目简介 3 1.2 实训功能说明 3 1.2.1 基本功能 3 1.2.2 附加功能 3 2. 相关技术 4 2.1 Windows定时器技术 4 2.2 透明贴图实现技术 4 2.3 CObList链表 5 2.4获取矩形区域 6 2.5使用AfxMessageBox显示游戏过程中的提示信息 6 2.6内存释放 6 2.7 CImageList处理爆炸效果 6 2.8对话框的应用 6 3. 总体设计与详细设计 7 3.1 系统模块划分 7 3.2 主要功能模块 8 3.2.1 系统对象类图 8 3.2.2 系统主程序活动图 9 3.2.3 系统部分流程图 9 4. 编码实现 12 4.1 绘制游戏背景位图程序 12 4.2 飞机大战游戏对象的绘制程序 13 4.3 飞机大战游戏对象战机位置的动态控制 15 4.4 飞机大战游戏对象之间的碰撞实现 17 4.5 游戏界面输出当前信息 19 5. 项目程序测试 20 5.1战机移动及子弹发射模块测试 20 5.2 敌机及炸弹模块测试 20 5.3 爆炸模块测试 20 6. 实训中遇到的主要问题及解决方法 21 7. 实训体会 21 1. 概述 1.1 实训项目简介   本次实训项目是做一个飞机大战的游戏,应用MFC编程,完成一个界面简洁流畅、游戏方式简单,玩起来易于上手的桌面游戏。该飞机大战项目运用的主要技术即是MFC编程中的一些函数、链表思想以及贴图技术。 1.2 实训功能说明 1.2.1 基本功能   (1)设置一个战机具有一定的速度,通过键盘,方向键可控制战机的位置,空格键发射子弹。   (2)界面中敌机出现的位置,以及敌机炸弹的发射均为随机的,敌机与敌机炸弹均具有一定的速度,且随着关卡难度的增大,数量和速度均增加。   (3)对于随机产生的敌机和敌机炸弹,若超过矩形区域,则释放该对象。   (4)添加爆炸效果,包括战机子弹打中敌机爆炸、敌机炸弹打中战机爆炸、战机与敌机相撞爆炸以及战机子弹与敌机炸弹相撞爆炸四种爆炸效果。且爆炸发生后敌机、子弹、炸弹均消失,战机生命值减一。 1.2.2 附加功能   (1) 为游戏界面添加了背景图片,并在战机发射子弹、战机击中敌机、敌机击中战机、以及战机敌机相撞时均添加了背景音效。   (2)为游戏设置了不同的关卡,每个关卡难度不同,敌机与敌机炸弹的速度随着关卡增大而加快,进入第二关以后敌机从上下方均会随机出现,且随机发射炸弹。   (3)第一关卡敌机从上方飞出,速度一定,战机每打掉一直敌机则增加一分,每积十分,则为战机增加一个生命值,当战机得分超过50分则可进入下一关;进入第二、三关时敌机速度加快,分别从上下两方飞出,此时战机每得分20、30分,才会增加一个生命值,得分超过100、150分则进入下一关、通关。   (4) 在游戏界面输出当前游戏进行信息,包括当前得分、当前关卡以及击中敌机数量。   (5)增加了鼠标控制战机位置这一效果,战绩的位置随着鼠标的移动而移动,并且点击鼠标左键可使得战机发射子弹。   (6)实现了暂停游戏的功能,玩家可通过键盘上的‘Z’键,对游戏进行暂停。   (7)通过对话框的弹出可提示玩家是否查看游戏说明、是否进入下一关、是否重新开始等消息,使得玩家可自己选择。 2. 相关技术 2.1 Windows定时器技术   Windows定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔后就通知应用程序一次。程序将时间间隔告诉Windows,然后Windows给您的程序发送周期性发生的WM_TIMER消息以表示时间到了。本程序中使用多个定时器,分别控制不同的功能。在MFC的API函数中使用SetTimer()函数设置定时器,设置系统间隔时间,在OnTimer()函数中实现响应定时器的程序。 2.2 透明贴图实现技术   绘制透明位图的关键就是创建一个“掩码”位图(mask bitmap),这个“掩码”位图是一个单色位图,它是位图中图像的一个单色剪影。   在详细介绍实现过程之前先介绍下所使用的画图函数以及函数参数所代表的功能;整个绘制过程需要使用到BitBlt()函数。整个功能的实现过程如下:    (1) 创建一张大小与需要绘制图像相同的位图作为“掩码”位图;    (2) 将新创建的“掩码”位图存储至掩码位图的设备描述表中;    (3) 把位图设备描述表的背景设置成“透明色”,不需要显示的颜色;    (4) 复制粘贴位图到“掩码”位图的设备描述表中,这个时候“掩码”位图设备描述表中存放的位图与位图设备描述表中的位图一样;    (5) 把需要透明绘制的位图与对话框绘图相应区域的背景进行逻辑异或操作绘制到对话框上;    (6) 把“掩码”位图与这个时候对话框相应区域的背景进行逻辑与的操作;    (7) 重复步骤5的操作,把需要透明绘制的位图与对话框绘图相应区域的背景进行逻辑异或操作绘制到对话框上;    (8) 最后把系统的画笔还给系统,删除使用过的GDIObject,释放非空的指针,最后把新建的设备描述表也删除。 2.3 CObList链表 MFC类库中提供了丰富的CObList类的成员函数,此程序主要用到的成员函数如下:(1) 构造函数,为CObject指针构造一个空的列表。 (2) GetHead(),访问链表首部,返回列表中的首元素(列表不能为空)。(3) AddTail(),在列表尾增加一个元素或另一个列表的所有元素。   (4) RemoveAll(),删除列表中所有的元素。   (5) GetNext(),返回列表中尾元素的位置。   (6) GetHeadPosition(),返回列表中首元素的位置。   (7) RemoveAt(),从列表中删除指定位置的元素。   (8) GetCount(),返回列表中的元素数。 在CPlaneGameView.h文件中声明各游戏对象与游戏对象链表:   (1)//创建各游戏对象 CMyPlane *myplane; CEnemy *enemy; CBomb *bomb; CBall *ball; CExplosion *explosion; (2)//创建存储游戏对象的对象链表 CObList ListEnemy; CObList ListMe; CObList ListBomb; CObList ListBall; CObList ListExplosion; 2.4获取矩形区域   首先,使用CRect定义一个对象,然后使用GetClientRect(&对象名)函数,获取界面的矩形区域rect.Width() 为矩形区域的宽度,rect.Height()为矩形区域的高度。   使用IntersectRect(&,&))函数来判断两个源矩形是否有重合的部分。如果有不为空,则返回非零值;否则,返回0。 2.5使用AfxMessageBox显示游戏过程中的提示信息   AfxMessageBox()是模态对话框,你不进行确认时程序是否往下运行时,它会阻塞你当前的线程,除非你程序是多线程的程序,否则只有等待模态对话框被确认。   在MFC中,afxmessagebox是全局的对话框最安全,也最方便。 2.6内存释放   在VC/MFC用CDC绘图时,频繁的刷新,屏幕会出现闪烁的现象,CPU时间占用率相当高,绘图效率极低,很容易出现程序崩溃。及时的释放程序所占用的内存资源是非常重要的。   在程序中使用到的链表、刷子等占用内存资源的对象都要及时的删除。Delete Brush, List.removeall()等。 2.7 CImageList处理爆炸效果   爆炸效果是连续的显示一系列的图片。如果把每一张图片都显示出来的话,占用的时间是非常多的,必然后导致程序的可行性下降。CImageList是一个“图象列表”是相同大小图象的集合,每个图象都可由其基于零的索引来参考。可以用来存放爆炸效果的一张图片,使用Draw()函数来绘制在某拖拉操作中正被拖动的图象,即可连续绘制出多张图片做成的爆炸效果。 2.8对话框的应用    在设置游戏难度、炸弹的速度等,使用对话框进行设置非常方便,又体现出界面的友好。    对话框的应用过程如下:    (1). 资源视图下,添加Dialog对话框。然后添加使用到的控件,并修改控件的ID以便于后面的使用。    (2). 为对话框添加类,在对话框模式下,点击项目,添加类。    (3). 在类视图中,为对话框类添加成员变量(控件变量)。设置变量的名称、类型、最值等信息。    (4). 在资源视图菜单中,选择相应的菜单项,右击添加时间监听程序,设置函数处理程序名称。    (5). 在处理程序函数中添加相应的信息。 3. 总体设计与详细设计 3.1 系统模块划分   该飞机大战游戏程序分为游戏背景位图绘制模块、各游戏对象绘制模块、游戏对象之间的碰撞模块、爆炸效果产生模块、游戏界面输出玩家得分关卡信息模块。   其中在游戏对象绘制模块中,战机是唯一对象,在游戏开始时产生该对象,赋予其固定的生命值,当其与敌机对象、敌机炸弹碰撞时使其生命值减一,直至生命值为零,便删除战机对象。敌机对象与敌机炸弹对象的绘制中采用定时器技术,定时产生。爆炸对象初始化为空,当游戏过程中即时发生碰撞时,在碰撞位置产生爆炸对象,添加到爆炸链表中。 3.2 主要功能模块 3.2.1 系统对象类图            CGameObject是各个游戏对象的抽象父类,继承自CObject类,其他的类:战机类、敌机类、爆炸类、子弹类、炸弹类、文字类都继承了此类。   每个游戏对象类中既继承了来自父类CGameObject的属性,又有自己的特有属性和方法。 3.2.2 系统主程序活动图    3.2.3 系统部分流程图 (1) 该飞机大战游戏执行流程图: (2) 利用定时器定时产生敌机并绘制敌机流程图 4. 编码实现 4.1 绘制游戏背景位图程序   CDC *pDC=GetDC();   //获得矩形区域对象   CRect rect;   GetClientRect(&rect;);   //设备环境对象类----CDC类。   CDC cdc;   //内存中承载临时图像的位图   CBitmap bitmap1;   //该函数创建一个与指定设备兼容的内存设备上下文环境(DC)   cdc.CreateCompatibleDC(pDC);   //该函数创建与指定的设备环境相关的设备兼容的位图。   bitmap1.CreateCompatibleBitmap(pDC,rect.Width(),rect.Height());   //该函数选择一对象到指定的设备上下文环境中,该新对象替换先前的相同类型的对象。   CBitmap *pOldBit=cdc.SelectObject(&bitmap1;);   //用固定的固体色填充文本矩形框   cdc.FillSolidRect(rect,RGB(51,255,255)); //添加背景图片   CBitmap bitmap_BackGround;   bitmap_BackGround.LoadBitmap(IDB_BACKGROUND);   BITMAP bimap2;//位图图像   bitmap_BackGround.GetBitmap(&bimap2;);   CDC cdc_BackGround;//定义一个兼容的DC   cdc_BackGround.CreateCompatibleDC(&cdc;);//创建DC   CBitmap*Old=cdc_BackGround.SelectObject(&bitmap;_BackGround);   cdc.StretchBlt(0,0,rect.Width(),rect.Height(),&cdc;_BackGround,0,0,bimap2.bmWidth,bimap2.bmHeight,SRCCOPY); 4.2 飞机大战游戏对象的绘制程序 //画战机对象(唯一) if(myplane!= NULL) { myplane->Draw(&cdc;,TRUE); } //设置定时器,随机添加敌机,敌机随机发射炸弹,此时敌机速度与数量和关卡有关 SetTimer(2,300,NULL);//敌机产生的定时器 SetTimer(3,500,NULL);//敌机炸弹产生的定时器   if(myplane!=NULL&& is_Pause == 0) { switch(nIDEvent) { case 2://设置定时器产生敌机 { if(pass_Num == 1)//第一关 { int motion =1;//设置敌机的方向,从上方飞出 CEnemy *enemy=new CEnemy(motion); ListEnemy.AddTail(enemy);//随机产生敌机 }//if else if(pass_Num >= 2)//第一关以后的关卡 { int motion1 = 1; //设置敌机的方向,从上方飞出 CEnemy *enemy1=new CEnemy(motion1); enemy1->SetSpeed_en((rand()%5 +1)* pass_Num); ListEnemy.AddTail(enemy1);//随机产生敌机 int motion2 = -1;//设置敌机的方向,从下方飞出 CEnemy *enemy2=new CEnemy(motion2); enemy2->SetSpeed_en((rand()%5 +1)* pass_Num); ListEnemy.AddTail(enemy2);//随机产生敌机 }//else if }//case break; }//switch //判断产生的敌机是否出界,若已经出界,则删除该敌机 POSITION posEn=NULL,posEn_t=NULL; posEn=ListEnemy.GetHeadPosition(); int motion = 1; while(posEn!=NULL) { posEn_t=posEn; CEnemy *enemy= (CEnemy *)ListEnemy.GetNext(posEn); //判断敌机是否出界 if(enemy->GetPoint().xGetPoint().x>rect.right ||enemy->GetPoint().yGetPoint().y>rect.bottom) { ListEnemy.RemoveAt(posEn_t); delete enemy; }//if else { enemy->Draw(&cdc;,TRUE); switch(nIDEvent) { case 3://设置定时器产生敌机炸弹 {   CBall*ball=newCBall(enemy->GetPoint().x+17,   enemy->GetPoint().y+30,enemy->GetMotion()); ListBall.AddTail(ball); }//case break; }//switch }//else }//while //判断产生的敌机炸弹是否出界,若已经出界,则删除该敌机炸弹 POSITION posball=NULL,posball_t=NULL; posball= ListBall.GetHeadPosition(); while(posball!=NULL) { posball_t=posball; ball= (CBall *) ListBall.GetNext(posball); if( ball->GetPoint().xGetPoint().x>rect.right || ball->GetPoint().yGetPoint().y>rect.bottom) { ListBall.RemoveAt(posball_t); delete ball; }//if else { ball->Draw(&cdc;,1); }//else }//while }//if 4.3 飞机大战游戏对象战机位置的动态控制 if(myplane!= NULL) { myplane->Draw(&cdc;,TRUE); } //获得键盘消息,战机位置响应,战机速度speed为30 if((GetKeyState(VK_UP) <0 || GetKeyState('W') GetPoint().ySetPoint( myplane->GetPoint().x,rect.bottom); else myplane->SetPoint(myplane->GetPoint().x,( myplane->GetPoint().y - speed) ); }//if if((GetKeyState(VK_DOWN) <0|| GetKeyState('S') < 0)&& is_Pause== 0)//下方向键{}//if if((GetKeyState(VK_LEFT) <0|| GetKeyState('A') < 0)&& is_Pause== 0)//左方向键{}//if if((GetKeyState(VK_RIGHT) <0|| GetKeyState('D') < 0)&& is_Pause== 0)//右方向键{}//if if((GetKeyState(VK_SPACE)GetPoint().x, myplane->GetPoint().y,1); ListBomb.AddTail(BombOne); CBomb*BombTwo=newCBomb(myplane->GetPoint().x+35, myplane->GetPoint().y,1); ListBomb.AddTail(BombTwo); PlaySound((LPCTSTR)IDR_WAVE2,AfxGetInstanceHandle(),SND_RESOURCE |SND_ASYNC); }//if if(GetKeyState('Z')SetPoint(point.x,point.y); } //鼠标控制战机,发射战机子弹 void CPlaneGameView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: 在此添加消息处理程序代码和/或调用默认值 CView::OnLButtonDown(nFlags, point); if( is_Pause == 0) { CBomb *BombOne=new CBomb( myplane->GetPoint().x, myplane->GetPoint().y,1); PlaySound((LPCTSTR)IDR_WAVE2, AfxGetInstanceHandle(), SND_RESOURCE |SND_ASYNC); ListBomb.AddTail(BombOne); CBomb *BombTwo=new CBomb( myplane->GetPoint().x+35, myplane->GetPoint().y,1); ListBomb.AddTail(BombTwo); } } 4.4 飞机大战游戏对象之间的碰撞实现 本飞机大战游戏中的碰撞考虑了飞机子弹打中敌机、敌机炸弹打中战机、战机与敌机相撞、敌机炸弹与战机子弹相撞四种情况,根据游戏对象的矩形区域是否有交叉,而确认两者是否相撞,而产生爆炸对象,添加到爆炸链表中。以战机与敌机相撞为例: if(myplane != NULL&& is_Pause== 0) { POSITION enemyPos,enemyTemp; for(enemyPos= ListEnemy.GetHeadPosition();(enemyTemp=enemyPos)!=NULL;) { enemy =(CEnemy *) ListEnemy.GetNext(enemyPos); //获得敌机的矩形区域 CRect enemyRect = enemy->GetRect(); //获得战机的矩形区域 CRect myPlaneRect = myplane->GetRect(); //判断两个矩形区域是否有交接 CRect tempRect; if(tempRect.IntersectRect(&enemyRect;,myPlaneRect)) { CExplosion *explosion = new CExplosion( enemy->GetPoint().x+18 , enemy->GetPoint().y + 18); PlaySound((LPCTSTR)IDR_WAVE,AfxGetInstanceHandle(), SND_RESOURCE |SND_ASYNC); ListExplosion.AddTail(explosion); //战机生命值减一 lifeNum_Me--; //删除敌机 ListEnemy.RemoveAt(enemyTemp); delete enemy; if(lifeNum_Me == 0) { //删除战机对象 delete myplane; myplane=NULL; }//if break; }//if }//for }//if 战机子弹打中敌机、敌机炸弹打中战机以及战机子弹与敌机炸弹对象的碰撞实现同上。 4.5 游戏界面输出当前信息   if(myplane != NULL&& is_Pause== 0)    {    HFONT font;    font=CreateFont(20,10,0,0,0,0,0,0,0,0,0,100,10,0);    cdc.SelectObject(font);    CString str;    cdc.SetTextColor(RGB(255,0,0));    str.Format(_T("当前关卡:%d"),pass_Num);    cdc.TextOutW(10,20,str);    str.Format(_T("当前得分:%d"),score_Me);    cdc.TextOutW(10,40,str);    str.Format(_T("剩余生命:%d"),lifeNum_Me);    cdc.TextOutW(10,60,str);    }//if    if(myplane !=NULL && lifeNum_Me >0)    {    if(score_Me > 10*count_Life*pass_Num)    {    lifeNum_Me++;//生命值加1    count_Life++;//已增加生命值加1    }    } 游戏进入下一关,以及结束游戏界面设计代码与上类似。 5. 项目程序测试 5.1战机移动及子弹发射模块测试 用例 预期结果 实际结果 问题描述 修改方案 点击A键或鼠标左移 战机向左移动 战机向左移动 点击D键或鼠标右移 战机向右移动 战机向右移动 点击W键或鼠标上移 战机向上移动 战机向上移动 点击S键或鼠标上移 战机向下移动 战机向下移动 5.2敌机及炸弹模块测试 用例 预期结果 实际结果 问题描述 修改方案 玩家得分50(通过第一关后) 敌机从上下两方向均可飞出,且速度不断增加 敌机从上下两方向均可飞出,且速度不断增加 5.3爆炸模块测试 用例 预期结果 实际结果 问题描述 修改方案 战机子弹打中敌机 敌机位置处爆炸,敌机消失,战机生命-1 敌机位置处爆炸,敌机消失,战机生命-1 敌机炸弹打中战机 战机位置处爆炸,战机生命-1 战机位置处爆炸,战机生命-1 敌机战机相撞 敌机位置处爆炸,敌机消失,战机生命-1 敌机位置处爆炸,敌机消失,战机生命-1 战机子弹与敌机炸弹相撞 敌机炸弹处爆炸,子弹与炸弹均消失消失 敌机炸弹处爆炸,子弹与炸弹均消失消失 战机生命值==0 战机消失,GameOver或者过关 战机消失,GameOver或者过关 6. 实训中遇到的主要问题及解决方法   (1)由于对C++的面向对象的思想和逻辑思路不熟悉,不明白其中的封装之类的以及多态的思想,致使开始真正的进入实训接触到项目时没有开发思路,通过逐步查询书籍整理C++面向对象编程思路,才逐步理清项目的开发步骤。   (2)本飞机大战的游戏要求使用链表实现游戏对象的存储和释放,由于链表知识掌握的不牢固,使用起来总是出现这样那样的错误,给整个游戏开发带来了很大的障碍,通过不断的调试修改,最终使程序正确运行。   (3)在绘制各种游戏对象—敌机和敌机炸弹时,开始使用随机函数,画出敌机时而很少,总是打不到预定的效果,后来经过修改使用定时器产生敌机和敌机炸弹,使整个游戏更加人性化。 7. 实训体会 (1)在本次飞机大战游戏项目的开发过程中遇到很多问题,大部分是因为对MFC编程的不熟悉以及链表掌握不牢固所导致的。 (2)MFC编程中有很多可以直接调用的函数,由于之前缺乏对这方面编程的经验,以至于本次项目开发过程中走了很多弯路。 (3)通过寻求老师和同学的帮助,解决了开发中遇到的很多问题,也提升了自己调试错误的能力。 (4)通过本次实训,使我熟悉了MFC编程技术、巩固了链表的使用方法并加深了对面向对象编程思想的理解,对以后程序的编写打下了良好的基础。
Unity是一款流行的游戏开发引擎,能够开发各种类型的游戏,包括2.5D游戏。下面将简要介绍Unity开发2.5D游戏的教程。 首先,你需要安装Unity软件,并创建一个新项目。在项目中,你可以使用Unity的工具和资源来构建你的游戏。 其次,你需要选择一个适合的2.5D游戏场景。2.5D游戏是指使用2D的背景和角色,但是具有3D的效果和深度感。你可以选择一个平面或者地图作为游戏场景,并添加合适的纹理和贴图来增加细节和美观度。 接下来,你需要创建游戏中的角色和物体。你可以使用Unity的2D工具来绘制和设计你的角色和物体,也可以从Asset Store中下载和导入已有的角色和物体资源。 然后,你需要为角色和物体添加适当的动画和交互。Unity提供了强大的动画制作工具,通过添加关键帧和动画控制器来创建角色的移动、跳跃和攻击等动作。你还可以使用Unity的C#脚本语言编写代码来实现交互逻辑,例如控制角色的移动和碰撞检测等。 最后,你需要为你的游戏添加声音效果和UI界面。Unity提供了丰富的音效资源和UI工具,可以让你为游戏增添音乐、音效和用户界面等元素,提升游戏的沉浸感和用户体验。 通过以上步骤,你可以完成一个简单的2.5D游戏开发。当然,你还可以根据自己的需求和创意进一步扩展和完善你的游戏,例如添加关卡和敌人、设计游戏规则和目标等。祝你开发一款成功的2.5D游戏

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值