这是用户在 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(也就是精灵的完整大小)应该很合适,因为你希望整个精灵都与对象碰撞。确保将 Rigidbody2DGravity 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.在要调用 projectilePrefabRubyController 脚本中,创建一个公共变量。需要将此变量设为 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?

InstantiateUnity 的一个函数,我们之前还没有过介绍过。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 07 已由 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 脚本中编写函数,请添加一个名为 brokenbool 变量,并将该变量初始化为 true,这样机器人一开始就是损坏的。
2.Update 函数的开头,添加一个测试来检查机器人是否没有损坏。如果没有损坏,则退出函数。你需要将此代码添加到 Update 和 FixedUpdate 函数中。
修复后,机器人将停止移动。
由于这个原因,提前退出 update 函数将导致用于移动机器人的代码停止执行:
void Update() { //注意,! 符号可以反转测试,因此,如果 broken 为 true,则 !broken 将为 false,并且不会执行 returnif(!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 aposition 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