U
17 / 22 · 10 分钟
常见问题

射线检测:问世界你面前有什么

简短回答

怎么判断玩家正在瞄着什么、或站在什么上面?

Physics.Raycast 射出一条看不见的线,并报告它碰到的第一个 collider,附上命中点、距离和表面法线。一定要传入 LayerMask 和最大距离——否则射线会对所有层一直检测到无穷远。

Raycast 是一个问题:从这里朝这个方向射出一条看不见的线,它最先碰到什么?开枪、着地检测、玩家手指下面是什么、镜头正看着哪里、敌人能不能看见你 —— 说到底就是一个函数、四个参数的事。

动态图解

一条看不见的线,最先碰上的 collider 把命中点、表面法线和距离交回给你。

它的形状

Every raycast you will ever write looks like this
using UnityEngine;

public class Shooter : MonoBehaviour
{
    public float range = 50f;
    public LayerMask hittable;      // set this in the Inspector

    void Fire()
    {
        // origin, direction, out result, how far, what counts
        if (Physics.Raycast(transform.position, transform.forward,
                            out RaycastHit hit, range, hittable))
        {
            Debug.Log("hit " + hit.collider.name + " at " + hit.distance + "m");
        }
    }
}

命中能告诉你什么

  • hit.point —— 世界空间里的精确位置。弹孔、火花或贴花就摆在这里。

  • hit.normal —— 表面朝向。让特效对齐它,火花就平贴在墙上,而不是戳出墙外。

  • hit.distance —— ray 走了多远。做随距离衰减的伤害、画长度正确的曳光弹道都用得上。

  • hit.collider —— 被打中的东西。在它上面调用 GetComponent 找血量脚本,或者用 CompareTag 分清是队友还是墙。

Placing an effect so it sits on the surface
// Quaternion.LookRotation(hit.normal) makes the effect's forward axis point
// straight out of the wall.
Instantiate(impactPrefab, hit.point, Quaternion.LookRotation(hit.normal));

把 LayerMask 用对

LayerMask 不是 layer 的序号。它是 32 个比特,每个 layer 占一位,要算哪个 layer 就把那一位打开。把字段声明成 LayerMask 类型,Inspector 里给你的就是那个带勾选框的下拉列表,从此不必自己做位运算。

The bug, and the two right ways
// WRONG: layer 8 as a number means bit 3, which is layer 3.
Physics.Raycast(origin, dir, out hit, range, 8);

// Right, in code:
int mask = 1 << LayerMask.NameToLayer("Enemy");

// Right, and better: let the Inspector do it.
public LayerMask hittable;

// Everything EXCEPT the player's own layer:
int allButPlayer = ~(1 << LayerMask.NameToLayer("Player"));

从镜头发出,或者从指尖发出

ScreenPointToRay — the same line for mouse and touch
void Update()
{
    // Mouse. On a phone, Input.mousePosition follows the first touch too,
    // so this works on both without changing anything.
    if (!Input.GetMouseButtonDown(0)) return;

    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

    if (Physics.Raycast(ray, out RaycastHit hit, 100f, selectable))
    {
        hit.collider.GetComponent<Selectable>()?.Select();
    }
}

让这条线看得见

Raycast 本来是看不见的,不把它画出来,调试就只能靠猜。两行代码,就能把谜团变成游戏运行时你在 Scene view 里看得见的东西。

Draw what you cast
// Green to where it hit, red for the full length when it missed.
if (Physics.Raycast(origin, dir, out RaycastHit hit, range, mask))
    Debug.DrawLine(origin, hit.point, Color.green, 1f);
else
    Debug.DrawRay(origin, dir * range, Color.red, 1f);

当一条线太细的时候

  • SphereCast 是沿着 ray 滚过一个球。地面检测和粗弹体要的都是它 —— 因为换作一个实体过不去的缝隙,一条线会直接漏过去。

  • RaycastAll 会返回线上的所有命中,但不排序。可以用它来射击穿透好几个敌人 —— 不过要自己按距离排序,因为顺序并不可靠。

  • RaycastNonAlloc 不再新分配数组,而是把结果填进你自己持有的那个数组。在手机上,每帧都调 RaycastAll 的 Update 会不停制造垃圾,最后垃圾回收器不得不停下游戏来清理。

  • trigger 是被忽略还是参与检测,取决于 Physics 设置。别指望那个随时可能被人改掉的全局默认值,显式传入 QueryTriggerInteraction。

做出这个的那一课

弹道、伤害与特效

这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 角色与战斗,第 05 课。