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

世界交互 - 对话射线投射

教程
初级
1 小时
(387)
摘要
Ruby 现在可以修复损坏的机器人,但这是一个孤独的世界,她是唯一的居民。为了解决此问题,让我们在场景中添加另一个角色。为了让新角色有用,我们希望让 Ruby 能够与这个新角色对话,并给 Ruby 下达一个艰巨的任务:修好所有机器人!
选择 Unity 版本
最后更新:2021 四月 12
2020.3
2020.2
2020.1
2019.4
2019.3
2019.2
2019.1
2018.4
2018.3
语言
中文

1.创建角色

第一个任务是创建一个角色,名为 Jambi 的青蛙。现在你已经习惯了 Unity,应该可以直接快速完成这个任务。为了使操作更加简单,这个角色使用一段循环动画。
要使用的精灵图集是 Art > Sprites > Characters 文件夹中的 JambiSheet
1.使用精灵图集4 x 4 精灵格式对精灵切片。
2.精灵导入场景以创建角色。
提示:如果你在 Project 文件夹中选择了三个精灵,然后将它们全部一次拖动到 Hierarchy 中,则 Unity 会自动为你创建这三个帧的动画,并将动画分配给新创建的游戏对象。该动画会自动播放,不需要你采取其他操作:
选择要展开的图像
输入图像描述 (可选)

3.返回到 Project 窗口资源中的 Jambi 精灵图集,然后将 Pixel per Unit 设置更改为你认为合理的值。更改这个值,按 Apply,然后在 Scene 视图中检查 Jambi 的大小是否符合你的要求。在本示例中使用的是 150
4.添加一个 Box Collider 2D 并缩放该组件以覆盖精灵的底部,就像我们之前对主角或敌人执行的操作一样。
5.创建一个名为“NPC”(表示非玩家角色)的图层,并将你的角色游戏对象设置到该图层。
6.最后,将游戏对象重命名为 Jambi 并使用该游戏对象创建一个预制件
现在,你可以进入运行模式并测试你的角色是否具有适当的动画效果并能正确碰撞。
如果自动创建的动画看起来运行得太快,你随时可以在 Animation 窗口Window > Animation > Animation 菜单)中更改采样率,就像对手工动画剪辑所做的那样。

2.射线投射

现在需要添加代码,以便 Ruby 能与青蛙角色对话。首先需要知道 Ruby 是否站在青蛙角色的前面。
你可以在青蛙角色的前面放置一个触发器,如果 Ruby 走到这个触发器上,则对话开始。但这意味着 Ruby 可能会转过脸背对青蛙,而仍能与青蛙对话。
为避免此问题,你可以改用物理系统功能“射线投射”,这个功能在交互式应用程序中非常有用。
射线投射是将射线投射到场景中并检查该射线是否与碰撞体相交的行为。射线具有起点、方向和长度。之所以使用射线“投射”这种说法,是因为要从射线的起点一直到终点进行测试。
在这里,你将从主角的位置朝着她目光的方向投射射线,投射距离很小,例如 11.5 个单位。
为此,请在 RubyControllerUpdate 函数中添加以下代码:
if (Input.GetKeyDown(KeyCode.X)) { RaycastHit2D hit = Physics2D.Raycast(rigidbody2d.position + Vector2.up * 0.2f, lookDirection, 1.5f, LayerMask.GetMask("NPC")); if (hit.collider != null) { Debug.Log("Raycast has hit the object " + hit.collider.gameObject); } }
让我们详细看一下该代码:
在第一行中,你要检查玩家是否按了 X 键,就像在飞弹教中一样。
这将成为你的“交谈”按钮。如果希望在多种设备上均有效,则可以使用输入轴(如需了解相关提示,请参阅角色控制器移动)。
如果玩家按了这个键,则进入 if 代码块并启动 Raycast
首先,你将声明一个类型为 RaycastHit2D 的新变量。此变量存储由 Physics2D.Raycast 提供的 Raycast 的结果。Physics2D.Raycast 有多种版本(要了解所有变体,可以看看脚本 API),但我们在这里使用的版本有四个参数:
1. 你的示例中的起点是从 Ruby 的位置向上偏移,因为你想从 Ruby 精灵中心进行测试,而不是从她的双脚。
2.方向,这是 Ruby 注视的方向。
3.射线的最大距离应设置为 1.5,这样射线投射不会测试相距起点 1.5 个单位的相交点。
4.一个图层遮罩,允许我们仅测试某些图层。在相交测试期间,所有不属于遮罩的图层都将被忽略。在这里,你将选择 NPC 图层,因为该图层包含你的青蛙。
最后,测试一下你的射线投射是否命中了碰撞体。如果射线投射没有与任何对象相交,则为 null,因此不执行任何操作。否则,RaycastHit2D 将包含与射线投射相交的碰撞体,这样你将进入最后的 if 代码块,以记录刚刚通过射线投射找到的对象。
要在控制台中查看此日志记录,请执行以下操作:
  • 进入运行模式
  • 将 Ruby 放在靠近青蛙并面向青蛙的位置
  • 然后按 X

3.对话 UI

为简单起见,你需要让对话显示在你的青蛙朋友 Jambi 上方的一个小框中。
你将像在 UI 教程中一样使用画布,但这次我们将使画布出现在世界空间中。这意味着画布将存在于世界中,位于 Jambi 的头顶上方,而不是始终叠加在屏幕上。
要添加画布,请执行以下操作:
  • Hierarchy右键单击 Jambi(或选择 Jambi,然后单击 Hierarchy 顶部的 Create 按钮)。
  • 选择 UI > Canvas。此时将为 Jambi 创建一个子级 Canvas 游戏对象
目前,你的画布是叠加在屏幕上的。在 Hierarchy 中选择 Canvas 后,转到 Inspector 并将 Render Mode 更改为 World Space
选择要展开的图像
输入图像描述 (可选)

你可以忽略 Event Camera 设置,因为此设置仅对 UI 交互(如按下按钮)有用,而你只是希望在画布上显示文本。
现在我们的画布太大了。画布大小以像素为单位,在 Inspector 的 Rect Transform 中,我们可以看到 WidthHeight 是几百的数值(你的值将有所不同,因为这个值是基于你将 Canvas 模式切换为 World SpaceGame 视图的大小):
选择要展开的图像
输入图像描述 (可选)

你可以更改这些值以便在场景中为画布提供适当的尺寸(例如,宽度为 3 个单位,高度为 2 个单位),但这样在构建 UI 时会更困难。
所有 UI 元素(例如图像和文本)均以像素为单位,因此宽度为 3 且高度为 2画布将创建一个 3 x 2 像素的框。
你要改用的做法是缩放画布,以便减小场景中的大小,但将画布宽度和高度保持为正确的像素值。
设置 Rect Transform 值:
  • Pos XPos Y 设置为 0
  • Width 设置为 300Height 设置为 200
  • 然后将 ScaleXYZ 设置为 0.01
选择要展开的图像
输入图像描述 (可选)


4.更改场景

现在,画布场景中的尺寸已经正确了,让我们将这个画布移动到青蛙角色上方。
现在我们来添加一张图像: 1. Hierarchy 中,选择 Canvas 游戏对象2.右键单击该游戏对象,然后从上下文菜单中选择 UI > Image3.Project 窗口中,选择 Assets/Art/Sprites/UI4.选择 UIDialogBox 精灵,将该精灵拖入 Source Image 字段中。
别忘了在 Rect Transform 中按住 Alt 键的同时选择右下角来放大图像以填充画布
选择要展开的图像
输入图像描述 (可选)

你可能会注意到,画布图像出现在场景中的某些元素后面。这是因为你的画布存在于场景中,所以就像其他任何游戏对象一样,可能会在这些游戏对象后面渲染。
为确保在画布上层没有任何渲染内容,请在 Hierarchy 中选择 Canvas,然后在 Inspector 中将 Order in Layer 设置为较高的值(例如 10):
选择要展开的图像
输入图像描述 (可选)


5.向画布中添加文本

要向画布中添加文本,请执行以下操作:
1.Hierarchy 中右键单击 Image 游戏对象,然后选择 UI > Text - TextMeshPro。此时将显示以下对话框:
选择要展开的图像
输入图像描述 (可选)

2.单击 Import TMP Essentials。导入完成后(Import TMP Essential 按钮灰显),便可以关闭该窗口。文本现在就创建好了。
3.就像之前对图像所做的一样,在按住 Alt 的同时单击扩展锚点控制柄以将文本扩展到图像的整个尺寸。
4. 然后使用白色的小控制柄移动黄色框(文本边界),给文本框留出边距:
选择要展开的图像
输入图像描述 (可选)

5.现在,在 Inspector 中,你可以编写文本并更改文本样式。尝试所有参数,然后输入希望让 Jambi 说出的文字
选择要展开的图像
输入图像描述 (可选)


6.显示对话

最后,当 Ruby 与 Jambi 对话时,需要显示对话内容
为此,我们通过禁用画布将画布隐藏起来
1. 在 Hierarchy 中选择 Canvas,然后在 Inspector取消选中 Inspector 顶部的复选框
2.然后创建一个名为 NonPlayerCharacter 的新 C# 脚本,并将该脚本添加到 Jambi 上。
3. 在该脚本中,创建两个公共变量:
  • 一个是浮点类型,名为 displayTime,已初始化为 4,这个变量将存储显示对话框的时长(以秒为单位)。
  • 另一个是 GameObject 类型,名为 dialogBox,这个变量将存储画布游戏对象,因此你可以在脚本中启用/禁用它。
4.然后是一个私有变量:
  • 一个浮点类型,名为 timerDisplay,这个变量将存储显示对话的时长。
public float displayTime = 4.0f; public GameObject dialogBox; float timerDisplay;
5.Start 函数中,我们需要确保禁用 dialogBox,并将 timerDisplay 初始化为 -1
void Start() { dialogBox.SetActive(false); timerDisplay = -1.0f; }
6.Update 函数中,通过测试 timerDisplay 是否大于等于 0,检查当前是否显示对话。
如果大于零,则当前正在显示对话框。在这种情况下,你将减小 Time.deltaTime 以进行倒计时,然后检查 timerDisplay 是否已达到 0。这意味着是时候再次隐藏对话框了,因此这时需要禁用对话框:
void Update() { if (timerDisplay >= 0) { timerDisplay -= Time.deltaTime; if (timerDisplay < 0) { dialogBox.SetActive(false); } } }
7.最后,你需要编写一个名为 DisplayDialog 的公共函数,当 Ruby 与 NPC 青蛙交互时,你的 RubyController 将调用该函数。此函数将显示对话框,并将 timeDisplay 初始化为 displayTime 设置:
public void DisplayDialog() { timerDisplay = displayTime; dialogBox.SetActive(true); }
8.现在可以再次打开 RubyController 脚本,并使用以下代码替换先前为射线投射编写的 Debug.Log
if (hit.collider != null) { NonPlayerCharacter character = hit.collider.GetComponent<NonPlayerCharacter>(); if (character != null) { character.DisplayDialog(); } }
9.你在这里完成的所有工作是为了检查是否命中了目标,然后尝试在射线投射命中的对象上找到 NonPlayerCharacter 脚本,如果该对象上存在这个脚本,你将显示对话。
10. 别忘了在 Inspector 中的 NonPlayerCharacter 脚本的“Dialog Box”设置中分配 Jambi 的 Canvas 子对象。
11.现在,再次尝试让 Ruby 与 Jambi 互动,即在她面向 Jambi 时按 X。这次,对话框将出现,然后在四秒钟后消失(或者是在 Inspector 中为 displayTime 设置的时间长度值)。

7.检查你的脚本

你的 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(); } if (Input.GetKeyDown(KeyCode.X)) { RaycastHit2D hit = Physics2D.Raycast(rigidbody2d.position + Vector2.up * 0.2f, lookDirection, 1.5f, LayerMask.GetMask("NPC")); if (hit.collider != null) { NonPlayerCharacter character = hit.collider.GetComponent<NonPlayerCharacter>(); if (character != null) { character.DisplayDialog(); } } } } 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); UIHealthBar.instance.SetValue(currentHealth / (float)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"); } }
你的 NonPlayerCharacter 脚本现在应如下所示:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class NonPlayerCharacter : MonoBehaviour { public float displayTime = 4.0f; public GameObject dialogBox; float timerDisplay; void Start() { dialogBox.SetActive(false); timerDisplay = -1.0f; } void Update() { if (timerDisplay >= 0) { timerDisplay -= Time.deltaTime; if (timerDisplay < 0) { dialogBox.SetActive(false); } } } public void DisplayDialog() { timerDisplay = displayTime; dialogBox.SetActive(true); } }

8.总结

在本教程中,你了解了射线投射在交互式应用程序中的强大作用,因为该功能使你可以在指定方向上检测诸如碰撞体之类的对象。
除了检查角色前方的对象外,射线投射还具有许多其他用途,具体取决于要制作的游戏类型。
例如,要检查主角的位置和敌人的位置之间是否有任何对象,可以在这两点之间建立一个射线投射,如果返回命中结果,则说明它们之间有对象,所以敌人看不到主角。
在下一教程中,你将在交互式游戏中添加另一个重要部分:音频和声音。

项目:
Ruby's Adventure:2D 初学者
世界交互 - 对话射线投射
世界交互 - 对话射线投射
一般教程讨论
0
0
1. 创建角色
0
0
2. 射线投射
0
0
3. 对话 UI
0
0
4. 更改场景
0
0
5. 向画布中添加文本
0
2
6. 显示对话
0
0
7. 检查你的脚本
0
0
8. 总结
0
0