索引
通杀爆改 Unity FPS 游戏系列-序章
通杀爆改 Unity FPS 游戏系列-第一章
通杀爆改 Unity FPS 游戏系列-第二章
通杀爆改 Unity FPS 游戏系列-第三章
本章内容
- 效果展示
- 子弹命中的判定机制
- Unity 提供的物理碰撞检测
- 分析子弹范围
- 实现子弹范围
效果展示

子弹命中的判定机制
目前子弹的判定机制有 3 种
- Hitscan
- Projectile
- Hybrid(Hitscan+Projectile)
Hitscan
直译为命中扫描
可以简单地理解为:命中的结果是扫描出来的,而非实际命中碰撞得到的
这里的扫描:在发射的位置打出一条射线,通过射线来判断是否命中
如下图所示,从枪口发射出了一条射线,这条射线途径的物体会被判定为命中

根据 Hitscan 的特性,可以推断出:在开枪(发出射线)的那一刻,命中结果就已经确定了
但这也会存在一个问题:损失了真实性,无法通过重力,风力等其他外部因素来影响命中的轨迹
同时假设一个敌人在很远的距离,理论上子弹需要飞行一段时间才会命中,这段时间内敌人是有机会躲避的
但是如果只是采用 Hitscan 进行判定,在开枪的那一刻结果就已经注定了
Projectile
Projectile 弹射
子弹是真实存在的物理对象,可以设定子弹的速度、重力 等一系列影响子弹运行轨迹的参数
只有当子弹真正 "运动" 到了对应的位置,才会判定为命中
拿个最简单的对比图:

左图为 Hitscan
右图为 Projectile
Hybrid
前面提到的 Hitscan 和 Projectile 各有其优缺点
|
轨迹 |
性能 |
真实性 |
子弹 |
Hitscan |
直线 |
计算简单 |
差 |
射线替代 |
Projectile |
曲线 |
相对复杂 |
好 |
真实生成 |
而 Hybrid 就是共同使用 Hitscan 和 Projectile
在 Projectile 的基础上加上 Hitscan
以最高点为例子:

红线表示子弹在 Projectile "真实运动" 到最高点时发出的 Hitscan 射线
如果红线命中了物体,则判定子弹击中了物体
可以简单地做如下理解:
Hitscan 是在子弹发射点(一般为枪口) 发出一条较长的射线,命中结果在开枪的这一刻就已经决定了
Projectile 则是模拟子弹真实运动,子弹的运动轨迹受重力和风速等参数影响,命中结果得在运动过程中实际碰到物体才行
Hybrid 则是在 Projectile 的基础上额外加上了 Hitscan 的命中判定,不过此时的射线对比 Hitscan 就短得多
|
轨迹 |
性能 |
真实性 |
子弹 |
应用场景 |
Hitscan |
直线 |
计算简单 |
差 |
射线替代 |
狙击枪发射的子弹(飞行速度极快) |
Projectile |
曲线 |
相对复杂 |
好 |
真实生成 |
普通的子弹,比如霰弹枪,下坠很大 |
Hybrid |
曲线 |
结合 |
好 |
真实生成 + 射线替代 |
相对自由 |
Unity 提供的物理碰撞检测
碰撞体
首先明确一点,物理碰撞检测的对象是碰撞体
碰撞体可以简单的理解为 不可见 的用于物理碰撞的游戏对象的形状,通常采用粗略近似而非完全贴合游戏对象(节省性能)

如上图所示,绿色边框为一个 Sphere Collider (球形碰撞体),可以看到并没有和敌人完全贴合,而是粗略近似的覆盖
常用的碰撞体有:为盒型碰撞体、球形碰撞体和胶囊碰撞体,限于篇幅不展开介绍,简单的区别就是形状不同
而较复杂的碰撞体:MeshCollider(网格碰撞体) 虽然更精准和真实但受限于性能开销,一般较少使用
射线检测
在前面提到的 Hitscan 中,以一个点作为命中判定的起点,然后由该点沿着某个方向发出一条射线,得到这条射线途径的碰撞体
点射线
常用(这里只列举了一个)对应的 Unity 函数为:
public static int RaycastNonAlloc(Vector3 origin, Vector3 direction, RaycastHit[] results, [UnityEngine.Internal.DefaultValue("Mathf.Infinity")] float maxDistance, [UnityEngine.Internal.DefaultValue("DefaultRaycastLayers")] int layerMask, [UnityEngine.Internal.DefaultValue("QueryTriggerInteraction.UseGlobal")] QueryTriggerInteraction queryTriggerInteraction)
{
return defaultPhysicsScene.Raycast(origin, direction, results, maxDistance, layerMask, queryTriggerInteraction);
}
参数 |
说明 |
origin |
射线的起点 |
direction |
射线的方向 |
results |
命中结果 |
maxDistance |
允许射线命中距射线起点的最大距离 |
layerMask |
用于在投射射线时选择性地忽略碰撞器 |
queryTriggerInteraction |
指定此查询是否应命中触发器 |
返回值:命中结果的数量
这里会注意到一个不太 "真实" 的地方,那就是忽略了子弹的体积,直接把子弹看作了一个点,好处显而易见,节省性能
当然在大多数情况下,由于被命中的碰撞体和子弹体积相差较大,因此将子弹看作一个点并不会导致体感上的不真实
但是当子弹体积相对较大时,比如发射一个光波(镭射激光),这个时候用一个点进行判定显然就不太合适了
除了使用前面提到的 Projectile 命中判定(给子弹也挂一个碰撞体,当子弹的碰撞体和其它碰撞体实际接触时才判定命中)外
还可以使用球形射线
球形射线
球形射线:对场景中的所有碰撞器投射一个球体并返回每个被击中的碰撞器的详细信息

常用(这里只列举了一个)对应的 Unity 函数为:
public static RaycastHit[] SphereCastAll(Vector3 origin, float radius, Vector3 direction, [UnityEngine.Internal.DefaultValue("Mathf.Infinity")] float maxDistance, [UnityEngine.Internal.DefaultValue("DefaultRaycastLayers")] int layerMask, [UnityEngine.Internal.DefaultValue("QueryTriggerInteraction.UseGlobal")] QueryTriggerInteraction queryTriggerInteraction)
{
float magnitude = direction.magnitude;
if (magnitude > float.Epsilon)
{
Vector3 direction2 = direction / magnitude;
return Query_SphereCastAll(defaultPhysicsScene, origin, radius, direction2, maxDistance, layerMask, queryTriggerInteraction);
}
return new RaycastHit[0];
}
参数 |
说明 |
origin |
射线的起点 |
radius |
球体的半径 |
direction |
射线的方向 |
results |
命中结果 |
maxDistance |
允许射线命中距射线起点的最大距离 |
layerMask |
用于在投射射线时选择性地忽略碰撞器 |
queryTriggerInteraction |
指定此查询是否应命中触发器 |
可以发现,相较前面的RaycastNonAlloc,多了一个参数也就上球体的半径
球体的运动路径合是一个圆柱体,圆柱体扫过的区域如果包含碰撞体则判定命中
触发器
对于 Projectile 判定机制,则是使用触发器进行碰撞检测
触发器通常用于检测对象之间的重叠或进入某个区域。它们通过 OnTriggerEnter
、OnTriggerStay
和 OnTriggerExit
等回调函数触发事件
简单理解就是,触发器这一机制将碰撞检测移交给了引擎,当引擎发现有其它碰撞体触发了对象时,会通知给对应事件
特性 |
触发器(Triggers) |
射线(Raycasts) |
基本概念 |
用于检测物体进入、停留或离开触发区域。 |
用于检测射线与物体的碰撞。 |
实现方式 |
通过 Collider 组件设置为触发器。 |
通过代码发射射线进行检测。 |
回调函数 |
使用 OnTriggerEnter 、OnTriggerStay 、OnTriggerExit 。 |
通过 Physics.Raycast 等方法手动处理。 |
依赖组件 |
需要 Collider(设置为触发器)和 Rigidbody。 |
不需要特定组件,但通常与 Collider 一起使用。 |
检测频率 |
低频,基于物体的物理运动触发。 |
高频,可在每帧或按需调用。 |
性能 |
对于大量动态对象,可能会影响性能。 |
频繁调用可能导致性能问题,需优化使用。 |
适用场景 |
区域检测、进入/退出事件。 |
精确检测、射击、视线检测。 |
灵活性 |
依赖物理引擎的更新周期,较为自动化。 |
手动控制,灵活性高。 |
复杂性 |
实现简单,适合初学者。 |
需要编写更多代码,适合复杂检测。 |
使用限制 |
需要物体进入触发器区域才能检测。 |
可以检测任意方向和距离的碰撞。 |
对于子弹来说,通常不会使用触发器进行检测,因为触发器低频的特性不大适合作用于子弹
只有少数特定的子弹可以采用触发器进行检测,比如火箭筒的子弹火箭弹,飞行速度较慢,体积较大,频次不会过高
所以这里的触发器只是作为扩展科普内容,不具体展开
分析子弹范围
子弹范围,即子弹打到目标的附近,而不用是目标身上即可判定命中
要修改实现子弹范围,首先得要找到对应进行射线判定的函数,前面已经提到了,大多数子弹都是采用射线检测的
首先理一下逻辑
- 武器 → 开火 → 消耗武器备弹量 → 生成子弹
- 子弹飞行 → 飞行过程中打射线判定命中
所以可以以子弹备弹量为突破口,找到开火的逻辑,在开火逻辑附近找到生成子弹的逻辑
从生成子弹的逻辑那确定子弹的类名,在子弹这个类里查找 刷新 的函数
在 刷新 函数里找到射线函数
简单概括一下路径就是 备弹量消耗 → 武器类 → 子弹类 → 刷新函数 → 射线函数
查找子弹类
通过备弹量的消耗可以很快找到对应消耗的函数:Unity.FPS.Game.WeaponController.HandleShoot
在这个函数的开头,偏移 1FE (Unity.FPS.Game.WeaponController.HandleShoot+1FE) 可以发现一个调用
GameAssembly.dll+1FC33E - E8 ED170600 - call GameAssembly.dll+25DB30
到函数里面可以发现

GameAssembly.dll+25DC3E - E8 0DEE9300 - call UnityEngine.Object.Instantiate
UnityEngine.Object.Instantiate
函数是 Unity 引擎中用于创建对象实例的重要方法。它的主要作用是根据给定的原型(Prefab)创建一个新的对象实例。这个函数可以用来在游戏运行时动态生成游戏对象,例如角色、道具、敌人等。
可以推测这里是生成一个子弹实例
回到函数外,在函数调用结束的返回值处下断点
GameAssembly.dll+1FC343 - 8B F0 - mov esi,eax

开一枪,断下来以后将 EAX 的地址丢入 Tools → Dissect data/structures Ctrl+D 中解析

得到了此时发射的子弹类名:ProjectileStandard
可以在 .NET info 中找到该类的所有函数

定位命中判定
根据前面的分析,判定子弹在飞行过程中(每帧刷新时)会进行命中判定,进入 Update 函数中查找命中判定相关的逻辑
可以很轻松地在函数中找到前面提到的球形射线检测函数
GameAssembly.dll+206F7D - E8 6E869D00 - call UnityEngine.Physics.SphereCastAll
显然本游戏 Demo 使用了射线进行命中判定,再结合子弹的 Fields 中包含 Speed (速度) 和 Gravity (重力) 等关键字,可以确定命中判定机制为 Hybrid
实现子弹范围
实现子弹范围,其核心就是影响物理碰撞检测的判定
有 3 个方向可以修改
- 修改碰撞体大小,通常为修改要被命中的敌人的碰撞体大小,因为修改子弹大小可能会让子弹在飞行过程中先撞上其它障碍物
- 修改子弹的生成逻辑,把子弹"修正"到能命中敌人,修正可以直接把子弹位置改到敌人身上,也可以把子弹方向”对准“到敌人身上
- 修改射线检测输入参数,通常为起始点和方向,对于球形射线检测则多了个半径作为参数
本篇以第三个方向进行实现,前 2 个方向当做是扩展不具体展开
在前面定位到的球形射线检测函数那下断点,观察堆栈信息

不难推断出前 3 个数值为子弹的位置,第四个数值固定为 0.01 也就是球形射线检测的半径
所以将第四个数值改大即可实现子弹范围
开始 Auto assemble 写脚本,关于脚本部分在第二章已经说得比较详细了,这里不再赘述
直接给出脚本代码:
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
mov [esp+c],(float)1
originalcode:
call UnityEngine.Physics.SphereCastAll
exit:
jmp returnhere
"GameAssembly.dll"+206F7D:
jmp newmem
returnhere:
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
"GameAssembly.dll"+206F7D:
db E8 6E 86 9D 00
//call UnityEngine.Physics.SphereCastAll
作业
因为本 Demo 采用的是球形射线检测,所以修改判定时的球体半径可以很轻易实现子弹追踪的功能
但如果是普通的射线,就会比较麻烦,同时这种修改半径的方式也有局限性,中间遇到障碍物会先命中障碍物
而且也不够"爽",无法背对着敌人随便开枪都能追踪过去
如何实现全图子弹范围?留作一个课后小作业o(*≧▽≦)ツ
总结
本篇的主要内容并不在于修改本身,而侧重于命中判定以及物理引擎的说明
子弹追踪实际上就是影响命中判定,让客户端"认为"命中了
命中判定的三要素:
修改的难点不在于改,而在于定位,定位命中判定相关的位置
通常可以通过 备弹量消耗 → 武器类 → 子弹类 → 刷新函数 → 射线函数 这一路径来定位