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

世界交互 - 伤害区域和敌人

教程
初级
1 小时
(808)
摘要
在上一教程中,你使用了触发器来检测 Ruby 接触生命值可收集对象的情况。 在本教程中,你将使用相同的知识在角色处于特定区域时对角色造成伤害。
由于你已了解 Rigidbody 组件,后续你还将使用碰撞体(而不是触发器)将伤害区域扩展到来回移动的敌人。
材料
选择 Unity 版本
最后更新:2021 四月 12
2020.3
2020.2
2020.1
2019.4
2019.3
2019.2
2019.1
2018.4
2018.3
语言
中文

1.重置 Ruby 的生命值

首先,确保你初始没有将 Ruby 的生命值设置为 1(在上一教程中,你更改了此值):
1.打开 RubyController 脚本。 2.确认 Start 函数已将 currentHealth 设置为 maxHealth 而不是 1currentHealth = maxHealth;
现在,Ruby 再次在游戏开始时获得满血生命值,接下来你可以开始学习本教程了。

添加伤害区域

首先,添加一个可以伤害 Ruby 的区域。这一点与生命值可收集对象完全相同,不同之处在于生命值的变化为 -1,并且在角色触发该区域时不会导致该区域自毁。 先前的教程中已介绍添加伤害区域所需的所有知识,因此请先自己尝试这个操作!你将在 Assets > Art > Sprites > Environment 中为区域找到一个名为 Damageable精灵

伤害区域解决方案

现在,让我们来查看解决方案:
1.Damageable 精灵创建一个新的游戏对象。你可以从以下方法中选择:
  • 精灵拖放到 Hierarchy 窗口中。
  • 新建一个游戏对象,然后添加 Sprite Renderer 组件并分配该精灵
2.Box Collider 2D 组件添加到 Damageable 游戏对象,然后调整盒子的大小以适应精灵,并在 Inspector 中启用 Is Trigger 复选框。
3.创建一个名为 DamageZone 的新脚本。在 Project 窗口中,找到 Assets > Scripts 文件夹。右键单击,然后选择 Create > C# Script
4.Project 窗口中双击该脚本以在代码编辑器中打开脚本,然后将以下代码复制到新脚本中以替换其中已有的类。DamageZone 脚本与上一教程的 Collectable 脚本完全一样,只是这里的生命值变化为 -1,而且删除了对 Destroy 的调用:

public class DamageZone : MonoBehaviour { void OnTriggerEnter2D(Collider2D other) { RubyController controller = other.GetComponent<RubyController >(); if (controller != null) { controller.ChangeHealth(-1); } } }
  • 然后将该脚本添加到 Damageable 游戏对象。按 Play 并让 Ruby 围绕此区域走动。查看控制台,其中会打印 Ruby 的新生命值!
这很好,但只有在角色进入区域时才会伤害角色。如果角色停留在区域内,则不会再伤害角色。可以通过将函数名称从 OnTriggerEnter2D 更改为 OnTriggerStay2D 来解决此问题。刚体触发器内的每一帧都会调用此函数,而不是在刚体刚进入时仅调用一次。
现在,Ruby 会一直受到伤害,并且受到的伤害可能有点过多了!Ruby 在不到一秒的时间内生命值就变成了 0(帧数恰好等于她的生命值)。而且你可能还注意到,当你停止移动 Ruby 时,你在控制台上不会收到任何消息,因此 Ruby 站着不动时不会受到伤害。
要解决最后这个问题,你需要打开角色预制件,然后在 Rigidbody 组件中将 Sleeping Mode 设置为 Never Sleep
选择要展开的图像
输入图像描述 (可选)

为了优化资源,物理系统刚体停止移动时会停止计算刚体的碰撞;此时刚体进入“睡眠状态”。但在你这个情况中,你希望始终进行计算,因为即使在 Ruby 停止移动时也需要检测她是否受到伤害,因此你要指示刚体永远不要进入睡眠状态。
现在,要解决 Ruby五帧内便失去所有生命值的问题,你可以使 Ruby 在短时间内处于无敌状态。这样,只要 Ruby 处于无敌状态,你就可以忽略她受到的任何伤害。让我们回到 RubyController 脚本并进行以下修改:
public class RubyController : MonoBehaviour { public float speed = 3.0f; public int maxHealth = 5; public float timeInvincible = 2.0f; public int health { get { return currentHealth; }} int currentHealth; bool isInvincible; float invincibleTimer; Rigidbody2D rigidbody2d; float horizontal; float vertical; // 在第一次帧更新之前调用 Start void Start() { rigidbody2d = GetComponent<Rigidbody2D>(); currentHealth = maxHealth; } // 每帧调用一次 Update void Update() { horizontal = Input.GetAxis("Horizontal"); vertical = Input.GetAxis("Vertical"); if (isInvincible) { invincibleTimer -= Time.deltaTime; if (invincibleTimer < 0) isInvincible = false; } } 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); } }
让我们来仔细看看脚本的更改!

  • 添加三个新变量
变量 1:一个名为 timeInvincible 的公共浮点变量。将此变量设置为 public 是因为你希望能够在 Inspector 中动态更改变量以调整该值。
变量 2:一个名为 isInvincible 的私有 bool 变量,用于存储当前是否处于无敌状态。bool(boolean 的缩写)可让我们存储“true”或“false”。这在 if 语句中特别有用。
变量 3:一个名为 invincibleTimer 的私有浮点变量。此变量将存储 Ruby 在恢复到可受伤状态之前剩下的无敌状态时间。

  • 然后,你需要修改一些函数:

ChangeHealth 函数
ChangeHealth 函数中,你添加了一项检查以查看当前是否正在伤害角色(换言之,如果变化小于 0,则表示减小生命值)。如果是这样,你首先要检查 Ruby 是否已经处于无敌状态,如果是,那么将退出该函数,因为她现在无法受到伤害。否则,由于 Ruby 正受到伤害,你将 isInvincible bool 设置为 true 并将 invincibleTimer 变量设置为 timeInvincible,从而使 Ruby 处于无敌状态。
Update 函数
Update 函数中,如果 Ruby 处于无敌状态,则从计时器减去 deltaTime。这实际上是在倒计时。当该时间小于或等于零时,计时器结束,Ruby 的无敌状态也结束,因此通过将 bool 重置为 false 来消除她的无敌状态。这样,下次调用 ChangeHealth 来伤害 Ruby 时,你就不会提前退出并再次伤害她、重置她的无敌状态等等。
让我们进入运行模式并测试你的脚本。如果你让 Ruby 停留在伤害区域,她应该每两秒才受到伤害,因为你将无敌时间设置为两秒。尝试在 Inspector 中使用不同的值,必要时更改时间。

2.有关图形的旁注

代码学习暂告一段落,接下来重点介绍精灵渲染器 (Sprite Renderer) 的功能。现在,如果你想通过使用矩形工具T 键)调整伤害区域的大小来创建更大的伤害区域,则精灵会拉伸而显得很难看:
选择要展开的图像
输入图像描述 (可选)

但是,你可以让 Sprite Renderer 平铺(而不是拉伸)精灵。因此,如果将伤害区域的大小调整到足以将精灵容纳两次,则 Sprite Renderer 会多次并排绘制精灵
选择要展开的图像
输入图像描述 (可选)

为此需要执行以下操作:
  • 首先,确保游戏对象的缩放在 Transform 组件中设置为 1,1,1
  • 然后在 Sprite Renderer 组件中将 Draw Mode 设置为 Tiled,并将 Tile Mode 更改为 Adaptive
选择要展开的图像
输入图像描述 (可选)

此时将显示警告,告诉你精灵可能显示有误。
选择要展开的图像
输入图像描述 (可选)

可以遵循相关说明来解决此问题。在 Project 窗口中选择 Damageable 精灵,并将 Mesh Type 更改为 Full Rect
选择要展开的图像
输入图像描述 (可选)

按下 Inspector 底部的 Apply。现在,如果单击 Hierarchy 中的 Damage ZoneInspector 应该不再显示警告。
现在,当你使用矩形工具调整游戏对象的大小时,你会看到游戏对象拉伸到可以容纳两个精灵的程度,然后显示两个精灵而不是过度拉伸!注意:仅在你使用矩形工具而不是缩放工具时才有效,因为缩放会更改游戏对象的比例,不再是 Sprite Renderer Inspector 中的 Draw Mode 字段下可以看到的平铺大小。
选择要展开的图像
输入图像描述 (可选)

但是你会发现自己的碰撞体没有按比例缩放。请选中 Box Collider 2D 组件的 Auto Tiling 属性,以便碰撞体随精灵一起平铺。

3.敌人

以前都是在空荡荡的世界中游走,现在是时候向场景中添加其他一些角色了。让我们添加一些敌人,因为敌人是一种移动的伤害区域。
从本教程顶部的资料中下载以下图像,将图像保存在计算机上,然后将图像导入 Unity 并放置在场景中。
选择要展开的图像
输入图像描述 (可选)

就像你的主角一样,由于你希望敌人移动并与主角和环境碰撞,因此敌人需要 Rigidbody2DBoxCollider2D
务必像设置主角一样,将敌人的 Gravity Scale 设置为 0 并限制旋转。
选择要展开的图像
输入图像描述 (可选)


4.来回移动

创建一个名为 EnemyController 的新脚本,并将这个脚本附加到敌人角色。现在,让我们编写一个脚本来使让敌人循环上下移动。
你应该已经从前面的所有教程中充分学习到了此过程所需的知识,因此你可以自己尝试编写这个脚本,然后再查看答案。以下是一些重要的提醒信息:
  • 你需要在脚本中通过变量获取 Rigidbody2D,并在此基础上使用 MovePosition 来移动游戏对象。务必在 FixedUpdate 中进行此过程。
  • 别忘记,你可以通过 public 公开一个变量,以便在编辑器中调整此变量(例如速度)。
下面是答案:
public class EnemyController2 : MonoBehaviour { public float speed; Rigidbody2D rigidbody2D; // 在第一次帧更新之前调用 Start void Start() { rigidbody2D = GetComponent<Rigidbody2D>(); } void FixedUpdate() { Vector2 position = rigidbody2D.position; position.x = position.x + Time.deltaTime * speed; rigidbody2D.MovePosition(position); } }
注意:返回到 Unity 并编译此脚本之后,控制台中会显示一条警告:
“warning CS0108: 'EnemyController.rigidbody2D' hides inherited member 'Component.rigidbody2D'.”
这是因为 Monobehaviour(你用于脚本的基类)有一个(现在未使用)成员也名为 rigidbody2d
Unity 的这条信息表示你的 rigidbody2d 将取代该同名的成员,但这就是你想要的结果并且很有用,因此你可以忽略这条警告,一切都是正常的!
现在,让我们更改脚本,以便你可以在编辑器内以水平或垂直方向来回移动敌人。
对于方向,让我们使用名为 vertical 的公共 bool 变量。你可以在 Update 中进行测试以查看 vertical 是否为 true。如果为 true,则在你的世界中将敌人沿着 y 轴(而不是 x 轴)移动。
public class EnemyController : MonoBehaviour { public float speed; public bool vertical; Rigidbody2D rigidbody2D; // 在第一次帧更新之前调用 Start void Start() { rigidbody2D = GetComponent<Rigidbody2D>(); } void FixedUpdate() { Vector2 position = rigidbody2D.position; if (vertical) { position.y = position.y + Time.deltaTime * speed; } else { position.x = position.x + Time.deltaTime * speed; } rigidbody2D.MovePosition(position); } }
对于来回移动,你需要计时器(就像你用于无敌状态的计时器一样)。
只要你的计时器不为,你就可以使敌人向一个方向前进,然后反转方向并重置计时器,依此无限执行这一循环过程。
为此,你需要创建更多变量
public class EnemyController : MonoBehaviour { public float speed = 3.0f; public bool vertical; public float changeTime = 3.0f; Rigidbody2D rigidbody2D; float timer; int direction = 1;
因此,你添加了:
  • 一个公共浮点变量 changeTime,表示你反转敌人方向之前的时间。
  • 一个私有浮点计时器,用于保存计时器的当前值。
  • 一个 int 变量,表示敌人的当前方向,值为 1-1
你现在已设置新变量,接下来可以将 EnemyController 函数更改为:
public class EnemyController : MonoBehaviour { public float speed; public bool vertical; public float changeTime = 3.0f; Rigidbody2D rigidbody2D; float timer; int direction = 1; // 在第一次帧更新之前调用 Start void Start() { rigidbody2D = GetComponent<Rigidbody2D>(); timer = changeTime; } void Update() { timer -= Time.deltaTime; if (timer < 0) { direction = -direction; timer = changeTime; } } void FixedUpdate() { Vector2 position = rigidbody2D.position; if (vertical) { position.y = position.y + Time.deltaTime * speed * direction;; } else { position.x = position.x + Time.deltaTime * speed * direction;; } rigidbody2D.MovePosition(position); } }
让我们来分解这些更改:
  • Start 函数中,你可以将计时器初始化为反转敌人方向之前的时间。
  • Update 中,你将计时器递减,然后进行测试以查看计时器是否小于 0,如果小于 0,则可以更改方向并重置计时器。由于这与物理无关,因此不必在 FixedUpdate 中执行此操作。
  • 最后,可以将 speed 乘以 direction

5.伤害

你得到了一个移动的敌人!现在,在 Ruby 与敌人碰撞时,可以让敌人伤害 Ruby,从而实现敌人与她的对抗。
与你的伤害区域不同,你不能使用触发器,因为你希望敌人碰撞体为“实心”并与物体实际碰撞。幸好,Unity 提供了第二组函数!
就像你用过的 OnTriggerEnter2D 一样,你也可以使用 OnCollisionEnter2D(这是刚体与某个对象碰撞时调用的函数)。在此示例中,你的敌人与世界或主角发生碰撞时,便会调用 OnCollisionEnter2D。就像你对伤害区域所做的那样,你也可以进行测试以查看敌人是否与你的主角发生了碰撞。
为此,你要检查与敌人碰撞的游戏对象是否具有 RubyController 脚本。如果有,那么你就知道这个游戏对象是主角,因此可以对其实施伤害:
void OnCollisionEnter2D(Collision2D other) { RubyController player = other.gameObject.GetComponent<RubyController>(); if (player != null) { player.ChangeHealth(-1); } }
确保参数类型函数名称的拼写正确无误!
另一个变化是,没有使用 other.GetComponent,而是使用的 other.gameObject.GetComponent
这是因为此处的 other 类型是 Collision2D,而不是 Collider2D。Collision2D 没有 GetComponent 函数,但是它包含大量有关碰撞的数据,例如与敌人碰撞的游戏对象。因此,在这个游戏对象上调用 GetComponent
你现在已创建敌人,不要忘了将这个敌人制作成预制件!
将敌人从 Hierarchy 拖放Project 窗口。这样,你就可以在场景中放置多个敌人。你甚至可以用机器人制作两个预制件,将其中一个命名为快速机器人,将另一个命名为慢速机器人,然后更改两者的速度值颜色,从而创建多种类型的敌人,等等。

6.检查你的脚本

你的 RubyController 脚本现在应如下所示:
public class RubyController : MonoBehaviour { public float speed = 3.0f; public int maxHealth = 5; public float timeInvincible = 2.0f; public int health { get { return currentHealth; }} int currentHealth; bool isInvincible; float invincibleTimer; Rigidbody2D rigidbody2d; float horizontal; float vertical; // 在第一次帧更新之前调用 Start void Start() { rigidbody2d = GetComponent<Rigidbody2D>(); currentHealth = maxHealth; } // 每帧调用一次 Update void Update() { horizontal = Input.GetAxis("Horizontal"); vertical = Input.GetAxis("Vertical"); if (isInvincible) { invincibleTimer -= Time.deltaTime; if (invincibleTimer < 0) isInvincible = false; } } 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); } }
你的 DamageZone 脚本现在应如下所示:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DamageZone : MonoBehaviour { void OnTriggerStay2D(Collider2D other) { RubyController controller = other.GetComponent<RubyController >(); if (controller != null) { controller.ChangeHealth(-1); } } }
你的 EnemyController 脚本现在应如下所示:
public class EnemyController : MonoBehaviour { public float speed; public bool vertical; public float changeTime = 3.0f; Rigidbody2D rigidbody2D; float timer; int direction = 1; // 在第一次帧更新之前调用 Start void Start() { rigidbody2D = GetComponent<Rigidbody2D>(); timer = changeTime; } void Update() { timer -= Time.deltaTime; if (timer < 0) { direction = -direction; timer = changeTime; } } void FixedUpdate() { Vector2 position = rigidbody2D.position; if (vertical) { position.y = position.y + Time.deltaTime * speed * direction;; } else { position.x = position.x + Time.deltaTime * speed * direction;; } rigidbody2D.MovePosition(position); } void OnCollisionEnter2D(Collision2D other) { RubyController player = other.gameObject.GetComponent<RubyController >(); if (player != null) { player.ChangeHealth(-1); } } }

7.总结

在本教程中,你给角色所在的世界设置了一些挑战。你现在知道了如何使用伤害区域移动的敌人伤害角色。但这样的世界仍然有点过于静态。
在下一教程中,你将学习如何向角色和敌人添加动画

项目:
Ruby's Adventure:2D 初学者
世界交互 - 伤害区域和敌人
世界交互 - 伤害区域和敌人
一般教程讨论
0
0
1. 重置 Ruby 的生命值
0
0
2. 有关图形的旁注
0
1
3. 敌人
0
0
4. 来回移动
2
6
5. 伤害
0
1
6. 检查你的脚本
0
0
7. 总结
0
0