射线检测函数的分析

大部分FPS作弊程序都有一些必不可少的功能,包括自瞄,透视,自动扳机等。在研究这项技术时我发现这些技术的关键点在于调用游戏中的射线检测函数。下面我将介绍一些常见游戏引擎当中的射线检测功能,以及简单的讲解射线检测的实现原理。


一,射线检测的介绍

基本原理

首先射线就是一条从一个点出发一直延长的线,跟现实中的激光一样但是在游戏中是无终点并且一般是不可见的。这条从一个点射出来的线碰到障碍物(开发者定义的障碍物)就会停止。在FPS游戏从始至终一直会使用这个射线来检测与游戏场景当中一些特定物体的交互情况。

在游戏中射线检测有很多的用途,比如玩家前面有一扇门,游戏开发者设定按下E键会开门。按照一般逻辑我们只有接近门然后视角对着门按E才能开门。那么怎么知道玩家是否在门的附近,而且是否面对着门呢?

当然就是用射线检测来实现这一功能。这个功能跟蝙蝠的超声波一样,从玩家的准星当中发出一条直线类似于超声波,等遇到前方的门或者其他物品之后这条射线就停止运动并返回它碰到的物品信息。在每一帧一直检测玩家的准星发射的射线碰到的物品以及距离,再根据相应的物品做出相应的操作。

S1

在一部分游戏中摄像机发射的Line Trace(射线检测)和枪械的弹道是不一样的,尤其是在一些第三人称射击游戏当中。以下是具体步骤:

  1. 确定的起点和方向发射一条射线
  2. 计算射线与场景当中物体的相交点
  3. 返回这部分相交信息,比如是否相交,相交点(位置,距离,物体)等等

二,Unity引擎当中的射线检测

1.Ray射线的定义

首先使用射线检测之前就必须要创建一条射线,Unity直接提供了这条射线的结构体。这是一条无穷的线,开始于origin点,朝向direction方向。在Unity的API当中提供了一个叫 Ray 的结构体,定义如下:

1
public Ray(Vector3 origin, Vector3 direction);

origin是射线的起点位置,direction是射线的方向。

2.Ray的数学表达

Ray 用方程式表达形式如下:
$$
P(t)=O+t⋅D
$$

  • P(t) 是射线上的点
  • O 是射线起点(ray.origin
  • D 是射线方向(ray.direction
  • t 是参数(距离,t 大于等于 0)

3.使用方法

有常见的两种使用方法,一个是从玩家摄像机发射跟着鼠标的方向来检测的射线,还有一种是从玩家或者物体位置向前发射的射线:

1
2
3
4
// 创建从摄像机到鼠标对准位置的射线
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 创建从物体位置向前的射线
Ray forwardRay = new Ray(transform.position, transform.forward);

4.射线碰撞捕获的信息

Unity会通过 RaycastHit 结构体来返回一堆所需要的信息,这个结构体包含了关于此射线碰撞的所有重要数据:

1
2
3
4
5
6
7
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;

if (Physics.Raycast(ray, out hit, 100.0f))
{
// 现在可以访问 hit 中的所有信息
}
  1. collider —— 射线击中的碰撞体
  2. transform —— 射线击中碰撞体所属于的 Transform 组件
  3. rigidbody —— 射线击中碰撞体关联的刚体(存在的情况下)
  4. point —— 射线与碰撞体相交的精准 Vec3 位置
  5. normal —— 射线碰撞处的表面法线(向量为单位)
  6. distance —— 射线起点到相交点的距离
  7. material —— 射线击中碰撞体的物理材质
  8. textureCoord( textureCoord2 ) —— 射线碰撞点在物体纹理上的UV坐标(Vector2)
  9. triangleIndex —— 射线击中的三角形索引(针对网格碰撞体 int)
  10. barycentricCoordinate —— 射线碰撞点在击中三角形内的中心坐标点(Vector2)

5.LayerMask的概念

Unity当中的每一个游戏对象都会被分配到特定的层(Layer),Unity本身就提供了一些默认的层,比如Default,Ignore Raycast,Water,UI等等。用户自定义的层有第8层到31层(User Layers 8 -31 )。

LayerMask实际上是个32位整数,其中每一位对应一个层。比如某一位为1就代表包含在掩码中,反之为0表示在掩码外。以下是通过代码获取指定 LayerMask :

1
LayerMask playerEnemyMask = LayerMask.GetMask("Player", "Enemy");

一般的用法也很简单通过 Physics.Raycast 函数来实现,这个函数有多种用法可以通过查看官方文档来了解。这里只谈一些常用的用法:

1
2
3
4
5
6
7
8
9
10
11
public LayerMask interactableLayers;		//公开LayerMask类型
void Update() //生命周期函数,每帧都会被调用
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //创建射线,方向为鼠标位置
RaycastHit hit; //用于存储射线击中物体的信息
// 只检测指定层的物体
if (Physics.Raycast(ray, out hit, 100.0f, interactableLayers))
{
Debug.Log("射线击中了指定层的物体: " + hit.collider.gameObject.name);
}
}

代码解释:

  1. 每一帧 ( 通过 Update() ) 执行以下操作
  2. 从当前鼠标指向的位置射出一条射线
  3. 使用 Physics.Raycast( ) 检测射线是否击中指定物体
  4. 射线最远距离被限定为 100.0f
  5. 只检测 interactableLayers 指定层的物体
  6. 如果击中到了那就在控制台打印名称

6.在实际游戏中的引用

下面展示在一般团队FPS当中的引用,实际情况肯定比这复杂多了。如何使用射线检测来识别准星指向的是敌人还是队友:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using UnityEngine;		//导入Unity核心命名空间

public class TargetDetector : MonoBehaviour
{
// 射线最大检测距离
public float detectionRange = 100f;

// 当前玩家的队伍ID
public int myTeamID = 1; // 1=蓝队,2=红队

void Update() //生命周期函数,每帧都会被调用
{
// 从相机中心发射射线
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
RaycastHit hit; //收集击中信息

// 执行射线检测
if (Physics.Raycast(ray, out hit, detectionRange))
{
// 尝试获取目标的队伍组件
TeamMember targetTeam = hit.collider.GetComponent<TeamMember>();

if (targetTeam != null)
{
// 判断是敌人还是队友
if (targetTeam.teamID == myTeamID)
{
// 是队友
Debug.Log("指向的是队友: " + targetTeam.playerName);
}
else
{
// 是敌人
Debug.Log("指向的是敌人: " + targetTeam.playerName);
}
}
}
}
}

// 简单的队伍成员组件
public class TeamMember : MonoBehaviour
{
public int teamID; // 1=蓝队,2=红队
public string playerName;
}

以上只是简单的FPS游戏当中的调用,射线检测还有一大堆用途可以实现不同的功能。其他方法可以通过官方教程来探索,这里只介绍对于FPS当中的准星射线检测部分方便后续逆向分析用。


三,Unreal引擎当中的射线检测

1.通过蓝图实现

UE引擎当中一般初学者使用的都是蓝图,在蓝图中可以搜到一些射线检测相关的API蓝图:

(前缀:Multi) + 射线形状 + [后缀:ForObjects|ByChannel|ByProfile]

加上前缀Multi代表射线可以穿透多个物体,没有这个前缀的话碰到第一个对象射线就失效了。后面的射线形状代表可以定义多种形状的射线,比如线形,球形,胶囊形等等。后缀就是按特定的方式来筛选分别是按类类型,通道类型和预设类型。

UE5官方指南有详细的蓝图链接教程可以点击此教程来研究是如何实现的,以下是成品模板:

S2

这里演示的 API 是 **Single Line Trace By Channel ** 只是对首个碰撞物体的检测(不会检测叠加的物体),命中物体之后会把物体的名称显示到屏幕上。

2.通过C++方式实现

以下是通过C++的方式来实现跟Unity相似的对于不同队伍的射线检测判断。

(1)获取玩家视角

通过C++实现其实跟Unity的概念上差不多,语法和实现方式稍有差别。首先要创建一个三维向量用来存储摄像机的位置,然后创建一个旋转器变量用来存储摄像机的旋转角度。之后通过函数获取摄像机的位置和旋转:

1
2
3
4
// 获取玩家视角
FVector CameraLocation;
FRotator CameraRotation;
GetActorEyesViewPoint(CameraLocation, CameraRotation);

(2)射线起点和终点

射线的起点就是摄像机的位置,终点就是先把摄像机旋转转换为Vector向量再乘以1000.0f(用户自定义)就得到了从摄像机朝向扩展1000个单位的向量了,这个加上摄像机开始位置得到了射线的终点坐标:

1
2
3
// 计算射线起点和终点
FVector TraceStart = CameraLocation;
FVector TraceEnd = CameraLocation + (CameraRotation.Vector() * 1000.0f);

(3)执行射线检测

先创建 FHitResult 结构体来存储射线检测结果(跟RaycastHit差不多),然后通过 LineTraceSingleByChannel 执行单次射线检测返回首个碰撞结果:

1
2
3
4
5
6
7
8
// 执行射线检测
bool bHit = GetWorld()->LineTraceSingleByChannel( //bHit返回是否击中了物体
HitResult, //存储射线检测的结果
TraceStart, //射线的起点
TraceEnd, //射线的终点
ECC_Visibility, //碰撞通道,这里使用的是可见性通道
FCollisionQueryParams() //碰撞查询参数,默认值
);

再通过判断来处理 bHit 的检测结果:

1
2
3
4
5
6
// 处理检测结果
if (bHit) //是否击中了物体
{
AActor* HitActor = HitResult.GetActor(); //获取被射线击中的对象(Actor)
if (HitActor) //确保获取到了对象
{

(4)获取队伍判断敌友

在击中的对象身上找有没有 UTeamComponent 类型的组件,如果存在再继续判断是敌人还是队友:

1
2
3
4
// 获取目标的队伍组件
UTeamComponent* TeamComponent = HitActor->FindComponentByClass<UTeamComponent>();
if (TeamComponent) //检查是否成功找到了队伍组件
{

再通过击中玩家的队伍ID来判断是敌是友,之后把调试信息打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int32 TargetTeamID = TeamComponent->TeamID;		//访问队伍组件中的 TeamID 属性
int32 MyTeamID = 1; // 假设玩家队伍ID为1

if (TargetTeamID == MyTeamID) //比较队伍 ID 判断是敌是友
{
// 是队友
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green,
FString::Printf(TEXT("队友: %s"), *HitActor->GetName()));//在屏幕上显示调试信息
}
else
{
// 是敌人
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("敌人: %s"), *HitActor->GetName()));//在屏幕上显示调试信息
}

那么整体工作流程就是先获取玩家摄像机的位置和朝向,再计算射线的起点和终点,之后执行射线检测检查是否击中了物体。如果击中了物体获取被击中的对象信息,从对象信息中找队伍组件,如果有队伍组件的话就比较队伍ID判断是敌是友,最后在屏幕上显示敌友信息。以下是简单但功能完整的UE射线检测代码(实际游戏比这复杂):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 在头文件中声明
UFUNCTION(BlueprintCallable, Category = "Detection")
void PerformLineTrace();

// 在 CPP 文件中实现
void AMyCharacter::PerformLineTrace()
{
// 获取玩家视角
FVector CameraLocation;
FRotator CameraRotation;
GetActorEyesViewPoint(CameraLocation, CameraRotation);

// 计算射线起点和终点
FVector TraceStart = CameraLocation;
FVector TraceEnd = CameraLocation + (CameraRotation.Vector() * 1000.0f);

// 射线检测结果
FHitResult HitResult;

// 执行射线检测
bool bHit = GetWorld()->LineTraceSingleByChannel(
HitResult,
TraceStart,
TraceEnd,
ECC_Visibility,
FCollisionQueryParams()
);

// 处理检测结果
if (bHit)
{
AActor* HitActor = HitResult.GetActor();
if (HitActor)
{
// 获取目标的队伍组件
UTeamComponent* TeamComponent = HitActor->FindComponentByClass<UTeamComponent>();
if (TeamComponent)
{
int32 TargetTeamID = TeamComponent->TeamID;
int32 MyTeamID = 1; // 假设玩家队伍ID为1

if (TargetTeamID == MyTeamID)
{
// 是队友
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green,
FString::Printf(TEXT("队友: %s"), *HitActor->GetName()));
}
else
{
// 是敌人
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("敌人: %s"), *HitActor->GetName()));
}
}
}
}
}

以上就是两大游戏引擎的射线检测功能分析,其他游戏引擎跟这两个引擎的检测方式大差不差,在逆向分析当中可以当作参考点来学习。文章当中有需要更改的部分请在评论区指出,谢谢观看!