原文:How to make a Power-Up System in Unity
作者:Kevin Small
如果音速小子中没有金色戒指和电动鞋,超级马里奥中没有了蘑菇,或者吃豆人中没有强力豆会是什么样子呢?游戏就不会那么有趣了!
道具系统是一个关键的游戏组件,因为它们增加了额外的复杂性和策略层,来保持移动的动作。
在本教程中你将学会:
- 设计、构建一个可重用的道具系统。
- 在游戏中使用基于消息的通信。
- 在一个上下“躲避”游戏中加入你自己的道具系统!
注:本教程假设你熟悉 Unity 并具有 C# 中级技能。如果你需要回顾这些知识,您可以查看我们的其他 Unity 教程。
本教程需要 Unity 2017.1或更高版,所以如果你还没有升级 Unity,请先升级。
开始
这个游戏是一个自上而下的战场躲避游戏,就像是一个不能射击、也没有赢得商业成功的几何战争游戏。你戴着头盔的主角必须躲开敌人才能到达出口,撞上敌人会减少生命。当所有的生命耗尽,游戏就结束了。
将开始项目下载并解压缩到你指定的位置。用 Unity 打开项目,查看项目文件夹:
- Audio:游戏的音效文件。
- Materials:游戏的材质。
- Prefabs:游戏的预制件,包括游戏场景、玩家、敌人、粒子和道具。
- Scenes:主游戏场景。
- Scripts:游戏的 C# 脚本,包含详尽的注释。如果想在开始前熟悉一下它们,请随意查看这些脚本。
- Textures:游戏和启动屏的图片。
打开 main 游戏场景,按 Play 按钮。
你会发现游戏还没有任何道具。因此,它很难完成,也有点无聊。你的任务是增加一个道具系统并让游戏生动一点。当玩家收集到一个道具,屏幕上会显示一句来自知名电影中的台词。看你能不能猜到是哪部电影,本教程最后会有答案!
道具的生命周期
每个道具都会有一个生命周期,它有几种不同的状态:
- 第一阶段是创建,它发生在游戏期间或在设计时,你手动将道具游戏对象放到场景中时。
- 接下来是吸引模式,道具要么会动,要么做一些吸引你注意的事情。
- 收集阶段是指拿取道具的行为,它会触发声音、粒子系统或其他特殊效果。
- 收集会导致相关功能的生效,于是道具会“做它该做的事情”。道具效果可以是任何你能想象到的东西,从一个不起眼的加血到给玩家一些超能力。生效阶段还会设置过期检查。如果经过一定的时间之后,玩家受伤或在一定数量的使用之后,或者在其他游戏条件发生之后,道具就会过期。
- 过期检查后会来到过期阶段。过期阶段将销毁道具,标志其生命周期结束。
上面的生命周期包含了你想在每个游戏中重复使用的元素,以及只属于当前游戏的元素。例如,检测玩家是否收集到一个道具,这是你在每个游戏中都想要的一个功能,但是让玩家隐形的效果可能只想用在当前游戏中。在设计脚本逻辑时,这一点很重要。
创建一个简单的星星道具
你可能很熟悉“低级”的道具,比如金币、星星或戒指,会提供简单的加分或加血之类的奖励。现在,你将在场景中创建一个星星道具,它将立即为玩家提供一定的生命值,让你的英雄能活得更好。
只靠星星还是不能很好地进行躲避,但你可以在后面继续增加其他的道具,这绝对会给你的英雄更大的竞争力。
创建一个精灵,命名为PowerUpStar,并将其放置在玩家上方(X:6,Y:-1.3)。为了保持场景的整洁,把精灵放到空文件夹 Powerups 下面:
现在设置精灵的外观。设置 Transform Scale 为 (X:0.7, Y:0.7),在Sprite Renderer 组件中,将 Sprite 栏设置为 star,将 Color 设置为淡褐色 (R:211, G:221, B:200)。
添加一个方形 2D 碰撞体组件,勾选 Is Trigger 选项框,设置 Size 为 (X:0.2, Y:0.2):
你已经创建了你的第一个道具。试玩一下游戏,看看效果。道具显示出来了,但当你捡起它后什么也不会发生。要解决这个问题,需要写点脚本。
将游戏逻辑分离到不同类
作为一名一丝不苟的开发人员,你希望将自己的时间利用到极致,并重用以前项目中的元素。如果要在道具系统中这样做,你需要设计它的类层次结构。类层次结构可以将道具逻辑分离成可重用的引擎部分和特定于游戏的部分。如果你对类层次结构和继承的概念还不清楚,那么你可以看看这个视频的解释。
上图显示中的 PowerUp 是父类。它包含了独立于游戏的逻辑,所以可以在其他项目中重用。教程项目已经包含父该类。父类负责管理道具的生命周期,管理道具的各种状态,并处理碰撞、收集、生效、消息和过期。
父类实现了一个简单的状态机,它跟踪了道具的生命周期。你必须实现它的子类并针对每个新道具在检查器中设置一些值就行了!
道具的编写步骤
注:要编写一个道具的脚本,你必须创建一个 PowerUp 子类,并遵循以下步骤。本教程会多次提到这个步骤,请让它保持随时可查状态!
- 实现 PowerUpPayload 执行想要的效果。
- 此步可选,实现 PowerUpHasExpired 移除上一步的效果。
- 当道具过期时,调用 PowerUpHasExpired。如果道具需要立即过期,请在检查器中勾选 ExpiresImmediately 选择框,这样就不会调用 PowerUpHasExpired。
具体到星星道具上看,它只是简单的增加一点生命值。你只需要写很少的代码就能实现。
创建你的第一个道具脚本
为 PowerUpStar 游戏对象新建一个脚本,命名为 PowerUpStar,然后用编辑器打开。
加入以下代码,替换原来的 Unity 代码,保留头部的 using 语句。
class PowerUpStar : PowerUp
{
public int healthBonus = 20;
protected override void PowerUpPayload() // 步骤 1
{
base.PowerUpPayload();
// 道具的效果是加血
playerBrain.SetHealthAdjustment(healthBonus);
}
}
代码非常简短,但这就是你需要在行星道具中实现的全部逻辑!这个脚本实现了道具编写步骤中的每一步:
- PowerUpPayload 方法中调用了 playerBrain.SetHealthAdjustment 来增加生命点。PowerUp 父类中已经声明了一个 playerBarin 引用。因为你继承了父类,你需要调用 base.PowerUpPayload 方法以确保在你的代码执行之前先执行所有核心逻辑。
- 你不需要实现 PowerUpHasExpired 方法,因为生命点的增加是永久性的。
- 这个道具是立即过期的,因此,你不需要做任何事情,只需要在检查器中勾上 ExpiresImmediately 即可。保存代码,回到 Unity 开始设置检查器。
创建场景中第一个道具
保存并返回 Unity 后,StarPowerUp 游戏对象的检查器是这个样子:
设置下列属性:
- Power Up Name: Star
- Explanation: Recovered some health…
- Power Up Quote: (I will become more powerful than you can possibly imagine)
- Expires Immediately: 勾选
- Special Effect: 从项目文件夹中将 Prefabs/Power Ups/ParticlesCollected 预制件拖入
- Sound Effect: 从项目文件夹中将 Audio/power_up_collect_01 音频剪辑拖入
- Health Bonus: 40
完成后的道具看起来是这个样子:
设置好 PowerUpStar 游戏对象后,将它拖到 Prefabs/Power Ups 文件夹,创建一个预制件。
使用新预制件来添加几颗星星到场景右边,位置随意。
运行场景,操作角色到第一个道具。伴随着你的收集,会发出某些声音和闪耀的粒子特效。酷!
基于消息的通信
下一个你将创建的道具需要用到一些背景信息。要获得这些信息,你需要一种方式让游戏对象之间进行通信。
例如,当你收集一个道具时,UI必须知道要显示什么信息.当玩家的生命值发生变化时,生命栏需要知道如何更新生命值。有很多方法可以做到这一点,Unity 手册就罗列了其中几种机制。
每一种通信方法都有其优缺点,而且也不可能有万能的方法。你在这个游戏中看到的是一种基于通信的消息机制,在“Unity 手册”中也大致描述过。
您可以将游戏对象分为消息广播者、消息监听者,或者两者的混合:
图的左边是消息广播者。你可以认为当某些有趣的事情发生时,这些对象会“大声喊出来”。例如,玩家会广播“我受伤了”的消息。图的右边是消息监听者。正如其名所示的,它们会监听消息。监听者不一定要监听所有消息,它们只需要监听自己想响应的那些消息。
消息广播者也可以同时是消息监听者。例如,道具会广播消息,但也会监听玩家的消息。举个例子,一个道具在玩家受伤的同时过期。
你可以想象有许多广播者和监听者,左边和右边会有许多交叉的线条相连。为了简单起见,Unity 提供了一个 EventSystem 组件,位于二者之间,就像这样:
Unity 使用可扩展的 EventSystem 组件来处理输入。该组件还能管理众多的发送/接收事件逻辑。
嘿!这理论也太多了点,但无论怎么说,这样的消息传递系统将允许道具轻易地监听并减少了对象之间的硬连线。这将使添加新的道具变得非常简单,特别是在开发的后期,因为大多数消息都已经被广播了。
建立消息通信的步骤
这是你真正开始动手之前的最后一点理论。要广播消息,你需要注意以下步骤:
- 广播者将他们想要广播的消息定义为 C# 接口。
- 然后,广播者将消息发送给存储在侦听者列表中的侦听者。
如果你需要复习一下,请看这个视频: C# 接口。
简而言之,接口定义了方法签名。实现接口的任何类都承诺通过这些方法来提供功能。
来看一个例子,你会更好理解一些。看下 IPlayerEvents.cs 文件中的代码:
public interface IPlayerEvents : IEventSystemHandler
{
void OnPlayerHurt(int newHealth);
void OnPlayerReachedExit(GameObject exit);
}
这个 C# 接口定义了 OnPlayerHurt 和 OnPlayerReachedExit提 方法。这些是玩家可以发送的信息。现在查看 PlayerBrain.cs 文件中的SendPlayerHurtMessages 方法。代码后面的说明中引用的数字和代码片段中的数字一一对应:
private void SendPlayerHurtMessages()
{
// Send message to any listeners
foreach (GameObject go in EventSystemListeners.main.listeners) // 1
{
ExecuteEvents.Execute<IPlayerEvents> // 2
(go, null, // 3
(x, y) => x.OnPlayerHurt(playerHitPoints) // 4
);
}
}
上面的方法负责 OnPlayerHurt 消息的发送。foreach循环遍历列表EventSystemListeners.main.listers中的每个监听者,并对每个监听者调用 ExecuteEvents.Execute,在这个方法发送消息。
上面的代码可以分为几个步骤:
- EventSystemListener.main.listeners 是单例对象 EventSystemListeners 中一个存放全局可见的 GameObjects 列表。任何想要侦听消息的游戏对象都必须在此列表中。你可以将GameObjects添加到此列表,通过在检查器中为 GameObject 提供一个 Listener 标签,或者调用EventSystemListeners.main.AddListener 方法。
- ExecuteEvents.Execute 是 Unity 提供的将消息发送到 GameObject 的方法。尖括号中的类型是包含要发送的消息的接口名称。
- 依照 Unity 手册的语法示例,GameObject 表明将消息发送给谁,NULL 用于表示额外的事件信息。
- 一个 lamba 表达式。这是一个超出本教程范围的高级C语言主题。基本上,lambda 表达式允许将代码作为参数传递到方法中。在这种情况下,代码包含要发送的消息(OnPlayerHurt)及其所需的参数(PraveHeToPoT)。
该项目已经为广播所有必要消息进行了设置。如果你想要扩展项目并在以后添加自己的道具,你可能会发现一些东西很有用。按照惯例,所有接口名称都以字母I开头:
- IPlayerEvents: 当玩家受伤或到达出口处时的消息。
- IPowerUpEvents: 当一个道具被收集或过期时的消息。
- IMainGameEvents: 当玩家获胜或失败时的消息。
如果你查看上述接口,它们都会有详尽的注释。在本教程中理解它们并不重要,因此如果你愿意,可以继续往下看。
一个加速道具
现在你已经学习了消息通信,你需要把它付诸实践,来监听一条信息吧!
你将会创建一个道具,给玩家提升额外的速度,直到他们撞到一些东西。当玩家在游戏中“进行监听”时,道具会检测什么时候玩家碰到某样东西。更具体地说,道具会监听玩家发送的“我受伤了”的消息。
要监听一个消息,你可以遵循以下步骤:
- 实现适当的 C# 接口来定义属于监听者的 GameObject 想要监听的内容。
- 确保监听者 GameObjects 本身添加到 EventSystemListeners.main.listers 数组。
创建一个新的精灵,命名为 PowerUpSpeed,并将其放置在游戏场景左上角的某个位置。将其 Scale 设置为(X:0.6,Y:0.6)。这个游戏对象是一个监听者,所以在检查器中给它加上一个 Listener 标记。
添加一个方形 2D 碰撞体,将 Size 调整到(X:0.2,Y:0.2)。在 Sprite Renderer 中,将 Sprite 设为 fast,颜色设置为之前小星星的颜色。确保您也启用了 Is Trigger。完成之后,该游戏对象应该如下所示:
为这个游戏对象添加一个脚本,名为 PowerUpSpeed,添加代码如下:
class PowerUpSpeed : PowerUp
{
[Range(1.0f, 4.0f)]
public float speedMultiplier = 2.0f;
protected override void PowerUpPayload() // Checklist item 1
{
base.PowerUpPayload();
playerBrain.SetSpeedBoostOn(speedMultiplier);
}
protected override void PowerUpHasExpired() // Checklist item 2
{
playerBrain.SetSpeedBoostOff();
base.PowerUpHasExpired();
}
}
注意代码中的 Cheklist item 编号。对应每个编号说明如下:
- PowerUpPayload。这会调用基类方法,以确保调用父类代码,然后在提升玩家速度。注意,父类中定义了 playerBrain,它包含对获得道具的玩家的引用。
- PowerUpHasExpired。你必须减去之前增加的速度,然后调用基类的方法。
- 最后一个 Checklist item 是当道具过期时调用 PowerUpHasExpired。你将在监听玩家消息时进行处理。
将类声明修改为实现玩家消息接口:
class PowerUpSpeed : PowerUp, IPlayerEvents
注:如果你使用的是 Visual Studio,你可以在输入 IPlayerEvents 后将鼠标放到上面,然后选择 Implement interface explicitly 选项。这将为你创建方法存根。
添加或修改这几个方法,确保它们如下所示并仍然位于 PowerUpSpeed 类定义之内:
void IPlayerEvents.OnPlayerHurt(int newHealth)
{
// You only want to react once collected
if (powerUpState != PowerUpState.IsCollected)
{
return;
}
// You expire when player hurt
PowerUpHasExpired(); // Checklist item 3
}
/// <summary>
/// You have to implement the whole IPlayerEvents interface, but you don't care about reacting to this message
/// </summary>
void IPlayerEvents.OnPlayerReachedExit(GameObject exit)
{
}
方法 IPlayerEvents.OnPlayerHurt 每当玩家受到伤害时都会被调用。这是“监听广播消息”的一部分。在这个方法中,你首先检查以确保道具只有在被收集之后才会起作用。然后,代码调用父类中的 PowerUpHasExpired,它将处理过期逻辑。
保存方法,回到 Unity,进入检查器进行一些设置。
在场景中创建加速道具
SpeedPowerUp 游戏对象的检查器看起来是这个样子:
在检查器进行如下设置:
- Power Up Name: Speed
- Explanation: Super fast movement until enemy contact
- Power Up Quote: (Make the Kessel run in less than 12 parsecs)
- Expires Immediately: 反选
- Special Effect: ParticlesCollected (和 Star 道具相同)
- Sound Effect power_up_collect_01 (和 Star 道具相同)
- Speed Multiplier: 2
这样,你的道具就会变成这样:
一旦你对加速道具设置好之后,就把它拖到项目树文件夹 Prefabs/Power Ups 中来创建预制件。你不会在演示项目中这样用,但是为了完整起见,这样做是个好主意。
运行游戏场景,移动角色收集加速道具,你将获得一些加速,直到碰上下一个敌人。
推挡道具
下一个道具允许玩家通过按下 P 键来推开物体,但是有使用次数的限制。到目前为止,你应该已经熟悉创建道具步骤了,因此,为了简单起见,你将只查看代码中感兴趣的部分,然后拖入预置文件。这种道具不需要监听消息。
在项目结构视图中,找到并查看 PowerUpPush 预制件。打开它的 PowerUpPush 脚本,查看代码。
你会看到其中有我们熟悉的方法。推挡道具的关键动作全部在 Update 方法中,如下所示:
private void Update ()
{
if (powerUpState == PowerUpState.IsCollected && //1
numberOfUsesRemaining > 0) //2
{
if (Input.GetKeyDown ("p")) //3
{
PushSpecialEffects (); //4
PushPhysics (); //5
numberOfUsesRemaining--; //6
if (numberOfUsesRemaining <= 0)
{
PowerUpHasExpired (); //7
}
}
}
}
上述代码做了以下工作:
- 这个脚本只在道具被收集时执行。
- 这个脚本只在还有剩余的使用次数时执行。
- 这个脚本只在玩家按下 P 键时执行。
- 这个脚本会在玩家四周播放漂亮的粒子特效。
- 这个脚本将玩家身边的敌人推开。
- 这个道具可以多次使用。
- 如果这个道具使用次数用完,则道具就过期。
这个推挡道具很有趣,你可以根据情况在场景中放入 2 个或更多。再次运行游戏场景,用新道具把附近搞得一团糟吧。
附加作业:伤害免疫道具
本节是可选内容,但很有趣。利用你学到的知识,创造一个道具,使玩家在一定的时间内无懈可击。
关键点和建议:
- Sprite: 看下项目文件夹 Textures/shiled 下面的精灵图。
- Power Up Name: Invulnerable
- Power Up Explanation: You are Invulnerable for a time
- Power Up Quote: (Great kid, don’t get cocky)
- 编码: 编码步骤和星星道具、加速道具一样。你需要用一个定时器来控制道具的过期时间。PlayerBrain 中的 SetInvulnrability 方法可以切换免疫属性开或者关。
- Effects: 项目中已经包含了一个漂亮的当玩家处于伤害免疫时围绕在玩家周围的脉冲效果。请看 Prefabs/Power Ups/ParticlesInvuln 中的预制件。当玩家伤害免疫时,你可以实例化该预制件,作为玩家的子节点。
需要完整的答案,或者检查自己答案是否与我们答案一致,请看下面:
创建新精灵,命名为 PowerUpInvuln,将它放到 (X:-0.76, Y:1.29)。设置 Scale 为 X:0.7, Y:0.7。这个游戏对象不需要监听任何消息,只会在设定的时间后过期,因此不需要在检查器中设置其 tag。
添加一个方形 2D 碰撞体,设置 Size 为 X = 0.2, Y = 0.2。在 Sprite Renderer,设置 Sprite 为 shield ,颜色和其它道具一致。确保勾上 Is Trigger。做完后,这个游戏对象会是这个样子:
为该游戏对象添加一个脚本,名为 PowerUpInvuln 然后加入以下代码:
class PowerUpInvuln : PowerUp
{
public float invulnDurationSeconds = 5f;
public GameObject invulnParticles;
private GameObject invulnParticlesInstance;
protected override void PowerUpPayload () // 步骤 1
{
base.PowerUpPayload ();
playerBrain.SetInvulnerability (true);
if (invulnParticles != null)
{
invulnParticlesInstance = Instantiate (invulnParticles, playerBrain.gameObject.transform.position, playerBrain.gameObject.transform.rotation, transform);
}
}
protected override void PowerUpHasExpired () // 步骤 2
{
if (powerUpState == PowerUpState.IsExpiring)
{
return;
}
playerBrain.SetInvulnerability (false);
if (invulnParticlesInstance != null)
{
Destroy (invulnParticlesInstance);
}
base.PowerUpHasExpired ();
}
private void Update () // 步骤 3
{
if (powerUpState == PowerUpState.IsCollected)
{
invulnDurationSeconds -= Time.deltaTime;
if (invulnDurationSeconds < 0)
{
PowerUpHasExpired ();
}
}
}
}
再次注意代码中的步骤编号。对应于步骤的编号,每段脚本分别进行了如下工作:
- PowerUpPayload: 调用基类方法确保父类代码被执行,然后设置玩家的 invulnerability 为 on。同时添加脉冲粒子效果。
- PowerUpHasExpired: 移除前面添加的伤害免疫属性,调用基类方法。
- 最后一个步骤是在道具过期时调用 PowerUpHasExpired 方法。这样,你需要在 Update() 方法中计算时间的流逝。
保存脚本,回到 Unity 运行游戏。你现在将能够无视伤害安全地打击一切来犯之敌——直到道具过期!
希望你在看答案之前先自己尝试一下!
接下来做什么
你可以下载完整的项目。
如果你想把这个项目继续做下去,你可以:
- 添加更多道具。也许是一个能够发射激光杀死敌人的道具?
- 创建一个工厂类,在运行时在战场中随机生成道具。
- 如果你想进一步学习 Unity,请去我们的商店中看一下这本 Unity 游戏教程。
还不知道是哪部电影的台词吗?答案在这里:
哇喔!你可能是网络上唯一一个点这个按钮的人了!答案就是《星际战争》系列。
希望喜欢本教程!有任何问题或建议,请在下面留言。