这是用户在 2025-8-3 13:30 为 https://learn.unity.com/tutorial/shi-jie-jiao-hu-fei-dan?uv=2020.3&projectId=5facf921edbc2a2003a58d3... 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Unity Learn 主页
查看教程内容

世界交互 - 飞弹

教程
初级
1 小时
(534)
摘要
上一课中,我们添加了一个对 Ruby 怀有敌意并已损坏的机器人,但我们还没办法修好这个机器人。在这节课,我们将运用我们已学习到的所有内容,让角色扔出一个齿轮来修复损坏的机器人。
选择 Unity 版本
最后更新:2021 四月 12
2020.3
2020.2
2020.1
2019.4
2019.3
2019.2
2019.1
2018.4
2018.3
语言
中文

1.创建飞弹

接下来将了解如何创建飞弹:
1.要创建飞弹,需要使用 Art > Sprites > VFX 文件夹中名为 CogBullet 的精灵。
2.要缩小齿轮,可在 Project 窗口中选择齿轮,在 Inspector 中将 Pixels Per Unit 设置 300,然后单击 Apply。要放大或缩小飞弹的尺寸,可以调整这个值。
  • 将精灵拖放到 Hierarchy 窗口中。
为了检测任何碰撞,需要给飞弹游戏对象提供一个 BoxCollider2D 和一个 Rigidbody2D。这一步很重要,因为齿轮会移动,可能撞到某些对象。
这次,默认的 Box Collider(也就是精灵的完整大小)应该很合适,因为你希望整个精灵都与对象碰撞。确保将 Rigidbody2D 的 Gravity Scale 设置为 0。
3.现在,让我们在脚本文件夹中创建一个名为 Projectile 的新脚本。
先前都是你自己为主角和敌人移动飞弹,而这次你要使用物理系统。
通过给刚体施加一个力,物理系统会根据施加的力来移动刚体,不需要在 Update 函数中手动更新位置。

2.物理系统

你现在将学习如何创建自己的物理系统:
1.首先需要创建一个 Rigidbody2d 变量。然后和之前一样,将刚体存储在 Start 函数中。
你需要创建一个 Rigidbody2d 变量,然后和之前一样,在 Start 函数中将刚体存储在该变量中。
Rigidbody2D rigidbody2d; void Start() { rigidbody2d = GetComponent<Rigidbody2D>(); }
2.然后创建一个名为 Launch 的函数。使用 Vector2 direction 作为参数,并使用 float force。你将使用这些参数来移动刚体:施加的力越大,移动速度就越快。
此函数在刚体上调用 AddForce,施加的力是方向与力的乘积。当力增大时,物理引擎将根据该力和方向逐帧移动飞弹。
public void Launch(Vector2 direction, float force) { rigidbody2d.AddForce(direction * force); }
3.因为你要检测碰撞,所以需要一个 OnCollisionEnter 函数。
目前只是销毁 (Destroy) 对象,但稍后你要进行更改以修复机器人。
void OnCollisionEnter2D(Collision2D other) { //我们还增加了调试日志来了解飞弹触碰到的对象 Debug.Log("Projectile Collision with " + other.gameObject); Destroy(gameObject); }
4.将这个脚本添加到你的飞弹,然后从飞弹制作一个预制件。当飞弹成为预制件时,可以在场景中删除飞弹,因为默认情况下你不希望场景中有飞弹。

3.发射飞弹

现在你已经有了飞弹的预制件,接下来需要投掷飞弹来修复机器人:
1.在要调用 projectilePrefab 的 RubyController 脚本中,创建一个公共变量。需要将此变量设为 GameObject 类型,因为预制件就是这种类型;游戏对象会作为资源保存。此外,由于已将变量设为公共变量,因此该变量会在编辑器中显示为字段,你可以在其中分配任何游戏对象。
2.将飞弹预制件拖放到该字段中。请注意,如果已经在场景中进行了这样的操作,则将覆盖你的预制件(该条目旁边带有蓝线),因此请使用右上角的 Overrides 下拉选单来应用于预制件。
3.接下来,在 RubyController 脚本中编写一个 Launch 函数,以便在发射飞弹时(例如,按下键盘按键时)调用该函数。
void Launch() { GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity); Projectile projectile = projectileObject.GetComponent<Projectile>(); projectile.Launch(lookDirection, 300); animator.SetTrigger("Launch"); }

4.什么是 Instantiate?

Instantiate 是 Unity 的一个函数,我们之前还没有过介绍过。Instantiate 的第一个参数是一个对象,在第二个参数的位置处创建一个副本,第三个参数是旋转。
让我们看一下执行 Instantiate 的步骤:
1.你要复制的对象是预制件,然后将该对象放置在刚体的位置(但稍稍向上偏移一点,让该对象靠近 Ruby 的双手,而不是双脚),并旋转 Quaternion.identity。
四元数 (Quaternion) 是一种可以表达旋转的数学运算符,但这里只需要记住的是,Quaternion.identity 表示“无旋转”。
2.然后,你将从该新对象获取 Projectile 脚本,并调用先前编写的 Launch 函数,其中的方向是角色目视的方向,力值字段设置为 300。
力值设置得很高的原因是这里以牛顿单位表示。这个值对你的游戏来说很合适,但是如果你想尝试不同的力量,则可以公开一个公共浮点变量并将这个变量作为力。
此外,你还可以看到已为你的 Animator 设置了触发器(请参阅上一教程以了解原理)。因此,Animator 可以播放发射动画!
3. 最后一部分是检测玩家是否按下某个键,并在玩家按下某个键时调用 Launch。
将以下代码添加到 RubyController 脚本中的 Update 函数 的末尾:
if(Input.GetKeyDown(KeyCode.C)) { Launch(); }

5.发射齿轮

要发射齿轮,需要完成以下操作:
1. 使用你先前看到过的 Input 类,但这次要使用 GetKeyDown,这个函数可以测试特定的键盘按键,这里是按键“C”。只有用键盘时才有效果。
2.如果要确保在不同设备上有效,可以将 Input.GetButtonDown 与轴名称一起使用(就像前面针对移动执行的操作一样),并在输入设置 (Edit > Project Settings > Input) 中定义该轴对应的按钮。如需了解示例,请查看 Axes > Fire1。
3.现在按 Play,然后按 C 即可发射齿轮。你应该会看到没有齿轮,或者齿轮短暂显示后立即消失。

6.修复错误行

你的控制台有两个错误行:
1.null 引用异常
如果双击 null 引用异常错误,则会打开 Projectile 脚本并定位到 Rigidbody2d.AddForce (direction * force) 行,这表示尽管我们在 Start 中获得了刚体,但 Rigidbody2d 变量还是为空(包含 null)。
这是因为在你创建对象时 Unity 不会运行 Start,而是在下一帧才开始运行。因此,在飞弹上调用 Launch 时,只实例化 (Instantiate),不调用 Start,因此 Rigidbody2d 仍然为空。要解决此问题,请将 Projectile 脚本中的 void Start() 函数重命名为 void Awake()。
与 Start 刚好相反,在创建对象时(调用 Instantiate 时)就会立即调用 Awake,因此在调用 Launch 之前已正确初始化 Rigidbody2d。
2.在日志中了解到齿轮 与 Ruby 发生了碰撞。
现在要解决第二个问题:飞弹与 Ruby 碰撞。这是因为你是在 Ruby 上创建的飞弹,因此一旦创建,物理系统就会立刻告知你,飞弹刚体正在与 Ruby 碰撞体碰撞。然后你在 OnCollisitonEnter 函数中调用 Destroy,因此会立即销毁飞弹。
你可以测试一下该对象是否与 Ruby 碰撞,然后不销毁飞弹。但还是会遇到一个问题,那就是碰撞后会立即停止移动。

7.图层和碰撞

要修复这个碰撞问题,正确方法是使用图层。图层可将游戏对象分组在一起,以便可以对它们进行筛选。你的目标是创建一个角色图层来放入 Ruby 游戏对象,然后创建一个飞弹图层来放入所有飞弹。
然后可以告诉物理系统,角色图层和飞弹图层不能碰撞,因此物理系统将忽略这些图层中的对象之间的所有碰撞。
1.要查看某个游戏对象在哪个图层,请单击 Inspector 右上角的 Layers 下拉选单。所有对象都从 Default 层开始(图层编号为 0)。游戏最多可以包含 32 个不同的图层。
选择要展开的图像
输入图像描述 (可选)

2.如果单击下拉选单,则会弹出一个窗口,其中会显示 Unity 定义一些的内置图层,但是你需要创建自己的图层,因此请选择 Add Layer。此时将打开图层管理器。
选择要展开的图像
输入图像描述 (可选)

Layer 0 至 7 已由 Unity 锁定,无法更改。所以需要在 Layer 8 中输入 Character,在 Layer 9 中输入 Projectile。
3.现在打开你的 Ruby 预制件,并将其 Layer 属性更改为 Character。保存预制件,然后对飞弹预制件进行同样的操作,但是这次请将图层设置为 Projectile。
4. 然后打开 Edit > Project Settings > Physics 2D ,查看底部的 Layer Collision Matrix,便可看到哪些图层彼此碰撞:
选择要展开的图像
输入图像描述 (可选)

默认情况下,所有复选框均已勾选,因此所有图层都与其他图层发生碰撞,但你要取消选中 Character 行与 Projectile 列之间的交集,因此这两个图层不再发生碰撞。
5. 现在你可以进入运行模式,齿轮不会再与 Ruby 发生碰撞,但仍会与其他对象(例如箱子或敌人)发生碰撞。

8.修复机器人

第一步是在 Enemy 脚本中编写一个函数来修复机器人并处理机器人在修复后的反应。目前,你的飞弹在碰撞时只是销毁自身。但你的目标是用飞弹来修复那些攻击性强并已损坏的机器人。
要修复机器人,请执行以下操作:
1.要在 Enemy 脚本中编写函数,请添加一个名为 broken 的 bool 变量,并将该变量初始化为 true,这样机器人一开始就是损坏的。
2. 在 Update 函数的开头,添加一个测试来检查机器人是否没有损坏。如果没有损坏,则退出函数。你需要将此代码添加到 Update 和 FixedUpdate 函数中。
修复后,机器人将停止移动。
由于这个原因,提前退出 update 函数将导致用于移动机器人的代码停止执行:
void Update() { //注意,! 符号可以反转测试,因此,如果 broken 为 true,则 !broken 将为 false,并且不会执行 return。 if(!broken) { return; }
void FixedUpdate() { if(!broken) { return; }
3. 最后,编写可用来修复机器人的函数。这应该很简单:
//使用 public 的原因是我们希望像飞弹脚本一样在其他地方调用这个函数 public void Fix() { broken = false; rigidbody2D.simulated = false; }
你刚才已经将 broken 设置为 false 并将 Rigidbody2d 的 simulated 属性设置为 false。
这样会将刚体从物理系统模拟中删除,因此系统不会将机器人视为碰撞对象,并且修好的机器人不会再阻止飞弹,也不能伤害主角。
4.现在要做的剩余工作就是修改 Projectile 脚本中的 OnCollisionEnter2D 函数。请从与飞弹碰撞的对象上获取 EnemyController,如果对象具有 EnemyController,则意味着你已经修复了该敌人。
void OnCollisionEnter2D(Collision2D other) { EnemyController e = other.collider.GetComponent<EnemyController>(); if (e != null) { e.Fix(); } Destroy(gameObject); }
注意:你还删除了 Debug.log,因为已经不再需要它。
现在飞弹将修复停止移动的机器人。然后你可以攻击机器人,并且不会受到伤害。

9.清理

目前的解决方案有个小问题,如果让 Ruby 投掷齿轮,并且齿轮没有与其他对象发生碰撞,那么只要游戏处于运行状态,齿轮就会一直向屏幕外移动。
随着游戏的进行,如果突然有 500 个齿移动到视野之外,可能会导致性能问题。
要解决此问题,只需检查齿轮与世界中心的距离,如果齿轮距离 Ruby 足够远,使 Ruby 永远都无法到达(假设在你的游戏中为 1000),则可以销毁齿轮。
让我们将以下 Update 函数添加到 Projectile 脚本中:
void Update() { if(transform.position.magnitude > 1000.0f) { Destroy(gameObject); } }
切记,position 可以看作是从世界中心到对象所在位置的向量,magnitude 是该向量的长度。因此,position 的 magnitude 就是到中心的距离。
当然,根据游戏的不同,还有其他处理方法。例如,你可以获取角色和齿轮之间的距离(使用 Vector3.Distance (a,b) 函数计算 position a 与 position b 之间的距离)。
或者,你可以在 Projectile 脚本中使用计时器,因此在发射齿轮时,你可以将计时器设置为 4 秒之类的值,在 Update 函数中对计时器进行递减,然后在计时器达到 0 时销毁齿轮。

10.可选:动画化修好的机器人

这个步骤是可选步骤,因为不会为游戏添加功能,并且与飞弹无关,但可以使游戏更美观。
你将为修好的机器人添加动画。请参阅上一教程,了解有关如何创建动画和使用 Animator 的更多详细信息。
1.为敌人创建一段新的动画剪辑(使用 MrClockworkFixed 帧 1、2、3 和 4),命名为 RobotFixed。别忘了把 Sample 设置成 4。
2.在 Enemy Animator 中,创建从正在运行的“混合树”到这段新动画的过渡。别忘了禁用 Has Exit Time。无需进行反向的过渡,因为机器人在修好后,就一直保持修复状态。
3.创建类型为 Trigger 且名为 Fixed 的参数,并将此参数设置为过渡条件:
选择要展开的图像
输入图像描述 (可选)

现在,在 Enemy 脚本的 Fix 函数中,只需添加以下代码行:
animator.SetTrigger("Fixed");
进入运行模式,朝着机器人发射齿轮以将机器人修复。机器人现在应该活蹦乱跳了!

11.检查你的脚本

你的 RubyController 脚本现在应如下所示:
public class RubyController : MonoBehaviour { public float speed = 3.0f; public int maxHealth = 5; public GameObject projectilePrefab; public int health { get { return currentHealth; }} int currentHealth; public float timeInvincible = 2.0f; bool isInvincible; float invincibleTimer; Rigidbody2D rigidbody2d; float horizontal; float vertical; Animator animator; Vector2 lookDirection = new Vector2(1,0); // 在第一次帧更新之前调用 Start void Start() { rigidbody2d = GetComponent<Rigidbody2D>(); animator = GetComponent<Animator>(); currentHealth = maxHealth; } // 每帧调用一次 Update void Update() { horizontal = Input.GetAxis("Horizontal"); vertical = Input.GetAxis("Vertical"); Vector2 move = new Vector2(horizontal, vertical); if(!Mathf.Approximately(move.x, 0.0f) || !Mathf.Approximately(move.y, 0.0f)) { lookDirection.Set(move.x, move.y); lookDirection.Normalize(); } animator.SetFloat("Look X", lookDirection.x); animator.SetFloat("Look Y", lookDirection.y); animator.SetFloat("Speed", move.magnitude); if (isInvincible) { invincibleTimer -= Time.deltaTime; if (invincibleTimer < 0) isInvincible = false; } if(Input.GetKeyDown(KeyCode.C)) { Launch(); } } void FixedUpdate() { Vector2 position = rigidbody2d.position; position.x = position.x + speed * horizontal * Time.deltaTime; position.y = position.y + speed * vertical * Time.deltaTime; rigidbody2d.MovePosition(position); } public void ChangeHealth(int amount) { if (amount < 0) { if (isInvincible) return; isInvincible = true; invincibleTimer = timeInvincible; } currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth); Debug.Log(currentHealth + "/" + maxHealth); } void Launch() { GameObject projectileObject = Instantiate(projectilePrefab, rigidbody2d.position + Vector2.up * 0.5f, Quaternion.identity); Projectile projectile = projectileObject.GetComponent<Projectile>(); projectile.Launch(lookDirection, 300); animator.SetTrigger("Launch"); } }
你的 EnemyController 脚本现在应如下所示:
public class EnemyController : MonoBehaviour { public float speed; public bool vertical; public float changeTime = 3.0f; Rigidbody2D rigidbody2D; float timer; int direction = 1; bool broken = true; Animator animator; // 在第一次帧更新之前调用 Start void Start() { rigidbody2D = GetComponent<Rigidbody2D>(); timer = changeTime; animator = GetComponent<Animator>(); } void Update() { //注意,! 符号可以反转测试,因此,如果 broken 为 true,则 !broken 将为 false,并且不会执行 return。 if(!broken) { return; } timer -= Time.deltaTime; if (timer < 0) { direction = -direction; timer = changeTime; } } void FixedUpdate() { //注意,! 符号可以反转测试,因此,如果 broken 为 true,则 !broken 将为 false,并且不会执行 return。 if(!broken) { return; } Vector2 position = rigidbody2D.position; if (vertical) { position.y = position.y + Time.deltaTime * speed * direction; animator.SetFloat("Move X", 0); animator.SetFloat("Move Y", direction); } else { position.x = position.x + Time.deltaTime * speed * direction; animator.SetFloat("Move X", direction); animator.SetFloat("Move Y", 0); } rigidbody2D.MovePosition(position); } void OnCollisionEnter2D(Collision2D other) { RubyController player = other.gameObject.GetComponent<RubyController >(); if (player != null) { player.ChangeHealth(-1); } } //使用 public 的原因是我们希望像飞弹脚本一样在其他地方调用这个函数 public void Fix() { broken = false; rigidbody2D.simulated = false; //如果你添加了修复动画,则为可选 animator.SetTrigger("Fixed"); } }
你的 Projectile 脚本现在应如下所示:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { Rigidbody2D rigidbody2d; void Awake() { rigidbody2d = GetComponent<Rigidbody2D>(); } public void Launch(Vector2 direction, float force) { rigidbody2d.AddForce(direction * force); } void Update() { if(transform.position.magnitude > 1000.0f) { Destroy(gameObject); } } void OnCollisionEnter2D(Collision2D other) { EnemyController e = other.collider.GetComponent<EnemyController>(); if (e != null) { e.Fix(); } Destroy(gameObject); } }

12.总结

本教程深入探讨了物理系统,介绍了如何使用图层,并说明了力如何作用于刚体来使物理系统移动对象。
在下一教程中,你最后需要允许摄像机移动并跟随着玩家,从而把世界变大。

项目:
Ruby's Adventure:2D 初学者
世界交互 - 飞弹
世界交互 - 飞弹
一般教程讨论
0
0
1. 创建飞弹
0
0
2. 物理系统
0
1
3. 发射飞弹
15
2
4. 什么是 Instantiate?
7
6
5. 发射齿轮
1
5
6. 修复错误行
0
3
7. 图层和碰撞
0
1
8. 修复机器人
1
1
9. 清理
0
0
10. 可选:动画化修好的机器人
5
3
11. 检查你的脚本
1
1
12. 总结
0
0