Unity游戏正向分析与修改
论坛里很多帖子讨论的是游戏的逆向分析,即从汇编代码和数据反推其业务逻辑。这样虽然可以做出修改,但没有涉及到游戏开发时用到的对象结构和脚本功能。对新手来说信息量太少。如果要提高自己的修改能力,了解游戏的开发过程很有必要。只有当你掌握了游戏引擎的用法后,才能为逆向分析提供更多思路。
本文以Unity游戏开发的教学案例“涂鸦跳跳”小项目为例,讲解游戏开发的思路,其中包括:场景、游戏对象、方法、属性、摄像机、C#脚本、组件、业务逻辑等。然后用dnSpy反编译程序集,修改代码实现无敌功能。本文是新手教程,高手可以忽略,阅读需要15分钟。
“涂鸦跳跳”游戏玩法很简单,如下图所示:左右方向键移动主角,当他接触地面或跳板时将向上跳跃,按下鼠标左键发射子弹攻击怪物。当主角碰到怪物或从高空中坠落时,游戏结束。到达的层数越高,分数越高。
此游戏的源代码在网上可以找到,作为教学案例使用,没有版权问题。如果你安装了Unity开发环境,可以在本地构建项目,脚本后端选择Mono。如果没有游戏的开发环境,可以使用Visual Studio打开工程文件来查看代码。
下面将结合官方文档说明Unity游戏引擎中的重要概念,这是理解游戏业务逻辑的基础知识。
参考链接:https://docs.unity3d.com/cn/2022.2/Manual/
场景
场景是您在 Unity 中加工内容的界面。场景是包含游戏或应用程序的全部或部分内容的资源。例如,您可能会在单个场景中构建一个简单的游戏,而对于更复杂的游戏,您可能每个级别使用一个场景,每个场景都有自己的环境、角色、障碍物、装饰和 UI。您可以在一个项目中创建任意数量的场景。
游戏对象
游戏对象是 Unity Editor 中最重要的概念。
游戏中的每个对象(从角色和可收集物品到光源、摄像机和特效)都是游戏对象。但是,游戏对象本身无法执行任何操作;您需要向游戏对象提供属性,然后游戏对象才能成为角色、环境或特效。
游戏对象是 Unity 中的基础对象,表示角色、道具和景物。它们本身并没有取得多大作为,但它们充当组件的容器,而组件可实现功能。四种不同类型的游戏对象:动画角色、光源、树和音频源(下图)
为了向游戏对象提供成为光源、树或摄像机所需的属性,需要向游戏对象添加组件。根据要创建的对象类型,可以向游戏对象添加不同的组件组合。Unity 拥有许多不同的内置组件类型,而且还可以使用 Unity Scripting API 来创建自己的组件。例如,通过将光源组件附加到游戏对象来创建光源对象。
组件
组件是每个游戏对象的功能部件。组件包含属性,您可以编辑这些属性来定义游戏对象的行为。有关组件和游戏对象之间关系的更多信息,请参阅游戏对象。
要在属性编辑器窗口中查看附加到游戏对象的组件列表,请在“层次”窗口或“场景”视图中选择一个游戏对象。
你可以将许多组件附加到一个游戏对象上,但每个游戏对象必须有一个且只有一个变换组件。这是因为变换组件决定了游戏对象的位置、旋转和比例。要创建一个空的游戏对象,请选择游戏对象->新建空对象。当您选择新的游戏对象时,属性编辑器会显示具有默认值的Transform组件。
摄像机
正如电影中使用摄像机向观众展现故事一样,Unity 中的摄像机用于向玩家展示游戏世界。在场景中至少要有一个摄像机,但也可以有多个摄像机。多个摄像机可以提供双人分屏或营造高级自定义效果。可以将摄像机动画化,也可以通过物理方式来控制摄像机。基本上能够想象的任何东西都可以通过摄像机来呈现,还可以使用典型或独特摄像机来适应游戏风格。
脚本文件
脚本是使用 Unity 开发的所有应用程序中必不可少的组成部分。大多数应用程序都需要脚本来响应玩家的输入并安排游戏过程中应发生的事件。除此之外,脚本可用于创建图形效果,控制对象的物理行为,甚至为游戏中的角色实现自定义的 AI 系统。
重要的类:在编写脚本时可能需要使用的一些最常用和最重要的 Unity 内置类。
- GameObject:表示可以存在于场景中的对象的类型。
- MonoBehaviour:基类,默认情况下,所有 Unity 脚本都派生自该类。
- Object:Unity 可以在编辑器中引用的所有对象的基类。
- Transform:提供多种方式来通过脚本处理游戏对象的位置、旋转和缩+ 放,以及与父和子游戏对象的层级关系。
- Vectors:用于表达和操作 2D、3D 和 4D 点、线和方向的类。
游戏业务逻辑分析
游戏开发类似于拍电影,摄像机对准场景就能得到游戏画面。场景之间可以切换,例如第一关通关后进入第二关,任务失败则回到主界面。复杂的游戏场景数量有很多,但在这个案例中有且只有一个场景。游戏对象是场景中存在的实体,有敌人、摄像机、火箭、金币、跳板、玩家等。(如下图所示)
主摄像机(MainCamera)使用脚本组件(FollowTarget.cs)跟踪玩家(Player),总是把镜头移动到玩家所在的位置。此方法每帧触发一次,移动时有平滑函数,视觉效果很流畅。
主摄像机中有个子对象叫隐藏地面(Floor),它与摄像机的相对位置固定,或者说它跟随摄像机移动,总是在可视区域的下方。隐藏地面有碰撞器组件,当它与玩家碰撞时判定游戏结束。因为玩家在空中跳跃,如果没有踩到跳板就会落到隐藏地面。判断代码在Player.cs中,隐藏地面并没有附加代码。
当游戏开始时,玩家在平台(Platform)上跳跃。平台对象有碰撞器,当玩家落到平台后获得向上的速度。碰撞处理代码也在Player.cs中。平台的脚本(Adjust.cs)作用是初始化时调整它的宽度到屏幕的宽度。
玩家(Player)是游戏的重要对象。它包含的组件有:
- Transform 描述对象的位置、旋转角度、缩放,所有游戏对象都有这个组件
- SpriteRender 根据设置的图片、颜色、物料渲染对象的可视图形
- Rigidbody2D 刚体组件为对象提供物理效果,质量、重力、速度
- Collider2D 碰撞器组件,和刚体组件配合使用,检测与其他物体的碰撞
- PlayerScript 脚本组件,包含代码实现特定功能
玩家的子对象表示其处于不同状态。当玩家捡到帽子道具时激活Hat子对象,看起来就像戴了帽子一样,游戏里表示乘坐竹蜻蜓往上飞。Rocket子对象同理,坐火箭往上飞,如下图所示。
接下来详细讲解Player.cs中的代码
- Start() 开始时调用一次。获取摄像机的左右边界保存在字段中。这里想让左右边界重叠,从左边界穿越到右边界,从右边界穿越到左边界。
- Update() 每帧调用一次。根据用户按键设置角色坐标、状态、是否发射子弹等。
- OnTriggerEnter2D(Collider2D collision) 碰撞时调用此方法。如果玩家碰到隐藏地面则游戏结束,如果碰到平台,则向上跳跃。
- Jump(float x) 向上跳跃方法。获取玩家的刚体组件,设置速度为0,添加向上的力,播放音效。
背景摄像机(BGCamera)和背景对象(Background)提供网格图片作为游戏背景。
下面这些对象是Obj的子对象,默认不显示,在游戏管理器(GameManager)中以此为模板动态创建对象。
-
跳板(Tile)有刚体、碰撞器和脚本组件,但重力为0,所以漂浮在空中。Update()方法根据自身类型上下左右移动跳板位置,如果它的位置低于摄像机的下边界,则调用游戏管理器回收此对象(AddInActiveObjectToPool)。OnTriggerEnter2D(Collider2D collision)开始碰撞方法判断自身类型,调用玩家的跳跃方法。
-
金币(Coin)没有刚体组件,有一个圆形的碰撞器。脚本组件功能如下:如果它和玩家碰撞,则调用游戏管理器,金币加一,然后回收此对象。如果它的位置低于摄像机的下边界,则调用游戏管理器回收此对象。
-
助推器(Power)与金币一样,都是游戏中的道具。玩家捡到后获得竹蜻蜓和火箭效果,往上飞。它有圆形碰撞器和脚本组件。
-
子弹(Bullet)是玩家发射攻击敌人的武器。它的刚体组件提供了重力,向上发射后会向下掉落。圆形碰撞器是为了检测与敌人的碰撞,但这部分代码在敌人的脚本中。当子弹激活时获得向上的力,当位置超出区域时回收此对象。
-
敌人(Enemy)有碰撞器和脚本组件。Update()方法里在周围随机移动,如果位置超出下边界则调用游戏管理器回收此对象。当它与玩家碰撞时游戏结束。当它与子弹碰撞时回收对象,增加分数。
发射子弹的情况如下:
游戏管理器是此项目中最复杂的对象,代码有500行,这里我简单概括一下它的功能。
- 整合用户界面管理器(GUIManager),提供了游戏开始前、结束后、暂停中的UI界面,需要时切换。
- 属性包括Obj的所有子对象作为预制体(Prefabs),以这些实例为模板动态创建对象。
- 游戏开始时创建动态对象(跳板、敌人、子弹、金币、助推器)到对象池(
Queue<GameObject>
)中,需要用时从中取出,不用时回收。避免频繁创建删除对象造成的性能开销。
- 记录游戏状态、分数、金币数量、游戏设置(道具出现的概率,移动速度、跳板的大小等)
- 生成随机跳板、金币、敌人,难度随着时间的推移逐渐增加。
- 音乐播放器,提供游戏时的背景音乐。
实现无敌功能
经过以上分析,我们知道游戏结束只在两点地方触发。一个是玩家碰到敌人时,另一个是玩家碰到隐藏地面时。对应到的代码片段是Player.cs和Enemy.cs的OnTriggerEnter2D(Collider2D collision)方法。修改方法为:跳过玩家与敌人碰撞检测代码,当与隐藏地面碰撞时向上跳跃。
打开dnSpy,拖入文件DoodleJump_Data\Managed\Assembly-CSharp.dll
,左侧选中程序集,定位到相关类,右侧找到方法名修改函数。修改完成后保存模块,替换原来的DLL,替换之前记得备份。
修改后的效果图如下,玩家与敌人碰撞后不再死亡,坠落时自动跳跃,游戏可以一直进行下去。