交互物大概是关卡里最容易被低估的一类东西。玩家看到一扇门,走过去,按一下,门开了——这条链路在 demo 里十分钟能搭完,但要做到“手感对、状态不错、多人不炸、性能不崩”,往往会拖掉一个迭代的大部分时间。
这篇不是某个插件的使用教程,而是一份设计口径:先把交互物拆成可复用的四层,再分别讨论六种交互形态的差异、状态怎么流转、射线怎么打、碰撞体怎么选、声音和动效怎么接。引擎侧以 UE5 为准,API 名称在 5.4–5.8 之间基本稳定;文中的引擎行为尽量给出官方文档出处,设计取舍部分来自项目经验,不冒充标准答案。
文中带 [n] 的句子对应文末的参考链接;没有标注的部分是设计经验与项目取舍,不冒充官方结论。
一句话:交互物不是“一个 Actor 加一个按钮”,而是“感知 → 判定 → 执行 → 反馈”四层,每一层都有自己的失败方式。
0. 阅读说明
0.1 这篇文章适合谁
| 角色 | 建议读法 | 能拿走什么 |
|---|---|---|
| 策划 / 关卡 | 第 1、2、3 章,第 9.6 节清单 | 知道一类交互物该有哪些状态、反馈和失败提示 |
| 程序 | 全篇,重点第 1、3、4、5、7 章 | 一套接口、状态机、检测与生成物的落地方式 |
| TA / 音频 | 第 2.6、2.7、6、8 章 | 材质音效、动效实体、命中包的组合方法 |
| 主程 / 技术负责人 | 第 3.7、5.7、9 章 | 网络权限、碰撞分离、性能预算与验收口径 |
0.2 TL;DR
- 分层:感知(我看见谁)→ 判定(我能不能用)→ 执行(世界怎么变)→ 反馈(玩家怎么感知)。四层混在一起,后面一定返工。
- 分类看驱动源:移动、NPC、静态、动态、动效,外加表现层的“动效实体”。同一种外观可能是不同类,别按美术资产分类。
- UObject 做交互物是为了省 Actor 同步:只同步交互字段,客户端按需组装 Mesh/音频/动效;代价是自己做序列化、路由、晚加入和版本兼容。
- 状态机至少七个状态:只用
bIsOpen这类布尔,会在多人、打断、冷却、损坏上崩。 - 射线只负责选中,不负责许可:命中之后还要过
CanInteract(距离、角度、标签、占用、资源)。 - 碰撞体是交互代理,不是美术外形:交互体积和物理碰撞要分开;不规则形状用复合简单碰撞或凸包,不要无脑复杂碰撞。
- 材质决定音效,数据表决定映射:物理材质给表面类型,数据资产把表面类型映射到声音,别在蓝图里写
switch。 - 弹孔是“贴花 + 命中包”:朝向、尺寸、寿命、上限、附着,一个都不能少。
- 动效实体要池化、限额、按重要度降级:它是交互的标点符号,不是无成本的装饰。
0.3 术语最小集
| 术语 | 一句话 |
|---|---|
| 交互物(Interactable) | 能被玩家或 AI 主动触发、并改变自身或世界状态的对象。 |
| 交互者(Interactor) | 发起交互的一方,通常是玩家角色、控制器或 AI Agent。 |
| 交互代理(Interaction Proxy) | 专门用于“被选中/被检测”的碰撞体或组件,可以和物理碰撞分离。 |
| 交互体积(Interaction Volume) | 判定“是否在可交互范围内”的体积,常用盒体、球体或胶囊体。 |
| 焦点(Focus) | 当前被准星/射线选中的交互物,是客户端表现,不一定等于服务端许可。 |
| 保留(Reservation / Claim) | 多人或 AI 场景下,某个交互位被某交互者临时占用,避免重复触发。 |
| 命中包(Impact Package) | 一次命中要播放的一组表现:贴花、粒子、声音、灯光、震屏。 |
| 动效实体(Effect Entity) | 有生命周期、只负责表现、会被生成和销毁的对象,不参与交互判定。 |
| 表面类型(Surface Type) | 物理材质上的枚举,用来区分金属、石头、木头、血肉等,驱动音效与粒子。 |
| 贴花(Decal) | 投影到网格表面的材质,常用于弹孔、血迹、涂鸦。 |
0.4 四层模型
四层各自的典型故障:
| 层 | 故障表现 | 常见原因 |
|---|---|---|
| 感知 | 打不中、穿墙能点、手柄够不到 | 射线起点错、通道错、碰撞体缺失 |
| 判定 | 看起来能用,按了没反应 | CanInteract 条件太隐晦,或客户端/服务端不一致 |
| 执行 | 门开了但碰撞没动、NPC 瞬移 | 只改了表现没改状态,或没对齐根运动 |
| 反馈 | 状态对了但“感觉不对” | 反馈晚于状态、声音材质错、动效被裁剪 |
设计原则:每一层只做一件事。对象回答“这个请求是否合法”,交互者负责“何时发起”,服务端负责“是否批准”。
0.5 目录
- 第 1 章 一套统一的交互契约
- 第 2 章 交互物的六种形态
- 第 3 章 交互状态流转
- 第 4 章 射线瞄准交互
- 第 5 章 碰撞:碰撞箱、胶囊体与不规则形状
- 第 6 章 音频触发与材质音效
- 第 7 章 射弹与弹孔
- 第 8 章 动效实体
- 第 9 章 工程落地与验收
- 第 10 章 参考
第 1 章 一套统一的交互契约
一句话:交互物只回答“此刻这个请求是否合法”,不替玩家做输入决策,也不自己决定网络权限。
1.1 为什么需要契约
如果没有统一契约,项目里很快会出现这些写法:
- 门蓝图里写
Cast<APlayerCharacter>,然后直接读玩家的输入; - 宝箱里写
if (bIsOpen) return;,但没有“被占用”和“冷却”; - NPC 里写
if (Distance < 200),但距离是按 NPC 原点算的,不是按交互点; - 玩家控制器里写
if (HitActor->IsA(ADoor)),每加一类交互物就改一次控制器。
这些写法单独看都能跑,合在一起就会变成“每类交互物一套逻辑、每个新职业改一次玩家代码”。契约的作用是把变化点收敛到对象自己身上。
Blueprint Interface 的官方定义是“一组只有名字、没有实现的函数”,任何实现它的蓝图都保证拥有这些函数;接口不能加变量、编辑图表或加组件。[4] 这正好适合交互物:不同对象共享“能被交互”这一件事,但各自实现细节。C++ 侧则常用 TScriptInterface 持有接口,避免硬引用具体类。[20]
1.2 交互接口
// 交互契约:对象只回答“此刻这个请求是否合法”,不替玩家做输入决策
UINTERFACE(MinimalAPI, Blueprintable)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
class IInteractable
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
bool CanInteract(const FInteractionContext& Context) const;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
FInteractionPrompt GetPrompt(const FInteractionContext& Context) const;
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnFocusBegin(const FInteractionContext& Context);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void OnFocusEnd(const FInteractionContext& Context);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void BeginInteract(const FInteractionContext& Context);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void EndInteract(const FInteractionContext& Context, EInteractionResult Result);
};
几个约定:
CanInteract只做判定,不改状态,不播声音。它会被 UI、射线、AI 反复调用,必须是幂等的。GetPrompt返回“按 E 打开”“需要钥匙”“已被占用”这类信息。失败也要有提示,否则玩家只会觉得“游戏坏了”。OnFocusBegin/End只做高亮、描边、提示 UI,不做 gameplay 状态变更。BeginInteract和EndInteract成对出现;EndInteract必须能处理取消、失败、被打断。- 蓝图调用接口用
Execute_CanInteract(Object, Context),不要直接Cast到具体类。
1.3 上下文:把“谁、用什么、打在哪”带进去
USTRUCT(BlueprintType)
struct FInteractionContext
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly) TObjectPtr<AActor> Instigator = nullptr;
UPROPERTY(BlueprintReadOnly) TObjectPtr<AController> Controller = nullptr;
UPROPERTY(BlueprintReadOnly) FHitResult Hit;
UPROPERTY(BlueprintReadOnly) FGameplayTag InputTag;
UPROPERTY(BlueprintReadOnly) FVector InteractionPoint = FVector::ZeroVector;
};
Hit 里已经带了 HitComponent、BoneName、ImpactPoint、ImpactNormal 和 PhysMaterial。其中 BoneName 在骨骼网格上很有用:同样是“打中敌人”,打头、打甲、打弱点应该是不同反馈;PhysMaterial 则直接决定音效和粒子。[5]
1.4 交互描述符:把可调参数从代码里拿出来
UENUM(BlueprintType)
enum class EInteractionHold : uint8
{
Tap, // 按一下
Hold, // 按住
Repeat // 按住连续触发
};
USTRUCT(BlueprintType)
struct FInteractionDescriptor
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) FGameplayTag InteractionTag; // Interaction.Door.Open
UPROPERTY(EditAnywhere) FText Verb; // “打开”
UPROPERTY(EditAnywhere) float MaxDistance = 220.f;
UPROPERTY(EditAnywhere) float MaxAngleDegrees = 60.f;
UPROPERTY(EditAnywhere) EInteractionHold HoldType = EInteractionHold::Tap;
UPROPERTY(EditAnywhere) float HoldDuration = 0.f;
UPROPERTY(EditAnywhere) FGameplayTagQuery RequiredTags;
UPROPERTY(EditAnywhere) FGameplayTagQuery BlockedTags;
UPROPERTY(EditAnywhere) TSoftObjectPtr<UAnimMontage> Montage;
};
交互描述符放在数据资产里,同一套代码就能配置出“木门”“铁门”“需要钥匙的门”“只能从一侧打开的门”。这也是后面第 6 章材质音效和第 9 章数据驱动的基础。
1.5 一次交互的完整生命周期
输入按下
-> 交互者确认焦点存在
-> 本地预测:CanInteract? -> 否:显示失败提示,结束
-> 是:进入 Pending(UI 先给“正在执行”的弱反馈)
-> 服务器 RPC:ServerBeginInteract(目标, 上下文)
-> 服务器复核距离/角度/标签/占用
-> 批准:状态机进入 Executing,广播表现
-> 拒绝:客户端回滚 Pending,提示原因
-> 执行完成/取消:EndInteract(Result)
这里的关键是“本地预测”和“服务器复核”分开:本地预测让手感跟手,服务器复核保证不作弊、不重复。第 3.7 节会展开。
第 2 章 交互物的六种形态
一句话:分类看“谁在驱动状态变化”,不看外观。门和电梯都是移动交互物,拉杆和宝箱都是静态交互物,草丛和吊灯都是动效交互物。
严格说,前五类是真正的交互物,第六类“动效实体”是表现层对象,不参与交互判定。但它们在工程上共用生成、池化、预算和调试链路,所以放在同一章讲,避免把“表现”当成“玩法”或反过来。
2.1 分类矩阵
| 类型 | 驱动源 | 状态复杂度 | 是否依赖物理 | 典型例子 | 主要风险 |
|---|---|---|---|---|---|
| 移动交互物 | 位移/旋转/动画 | 中 | 可选 | 门、电梯、阀门、抽屉 | 移动时穿模、把玩家卡住 |
| NPC 交互 | 对话/任务/行为树 | 高 | 否 | 对话、交易、任务、搜刮 | 对齐、占用、打断 |
| 静态交互 | 一次性/冷却 | 低 | 否 | 拉杆、按钮、终端、宝箱 | 反馈与状态不同步 |
| 动态交互 | 物理/抓取/投掷 | 中高 | 是 | 木箱、桶、可破坏物 | 网络同步、抖动、性能 |
| 动效交互 | 表现参数 | 低 | 否 | 草、帘子、风铃、灯 | 误导玩家以为能用 |
| 动效实体 | 生成/销毁 | 无 | 否 | 命中粒子、弹孔、音效体 | 数量失控、池化缺失 |
这张表不是互斥的。一扇“可以被炸开的铁门”同时是移动交互物和动态交互物;一个“拉下后播放动画”的拉杆是静态交互物 + 动效交互。实现时选一个“主类型”负责状态,其他类型只作为表现或条件。
2.2 移动交互物
移动交互物的核心不是“让它动”,而是“它动的时候,世界仍然自洽”:碰撞跟着动、玩家站上去会被带着走、AI 导航知道路没了、网络上的位置一致。
三种常见驱动方式:
| 驱动 | 做法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 动画驱动 | 用 UAnimSequence / UAnimMontage 驱动门板或机械臂 | 表现力强、和角色动画同源 | 碰撞要跟骨骼/插槽同步,网络成本高 | 门、闸机、机械装置 |
| 插值驱动 | 在 Tick 或 Timeline 里插值 RelativeLocation/Rotation | 可控、便宜、易网络同步 | 运动曲线要自己调,容易机械感 | 电梯、抽屉、旋转门 |
| 物理驱动 | 约束 + 马达/力 | 真实、可被撞开 | 不稳定、难预测、网络贵 | 可破坏门、重物、布娃娃 |
插值驱动的门,最小实现是“目标角度 + 插值速度 + 状态”:
// 示意:门只需要“目标角度”,不需要每帧重新算逻辑
void ADoor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
const float Target = (State == EDoorState::Opening) ? OpenAngle : ClosedAngle;
const float Current = DoorMesh->GetRelativeRotation().Yaw;
const float Next = FMath::FInterpConstantTo(Current, Target, DeltaSeconds, TurnRate);
DoorMesh->SetRelativeRotation(FRotator(0.f, Next, 0.f));
if (FMath::IsNearlyEqual(Next, Target, 0.1f))
{
State = (Target == OpenAngle) ? EDoorState::Open : EDoorState::Closed;
}
}
门这类移动交互物,设计时要提前回答几个问题:
- 开合方向:单开、双开、推拉、滑动?玩家从哪一侧推?
- 玩家站位:门板扫过玩家时,是停下、反向、推开玩家,还是直接穿过?最常见的是“检测到玩家就暂停并反向”,但这会让玩家反复卡门;更好的做法是门板碰撞设为“可推移玩家”,或者干脆让门板不阻挡玩家、只阻挡子弹。
- 碰撞与导航:门打开后,门板的碰撞要跟着走;如果门会改变 AI 路径,需要
UNavModifierComponent或动态导航。 - 网络:服务器决定开关,客户端只做表现;门的
bReplicateMovement打开,或者用RepNotify同步状态和目标角度。 - 声音:开门声、门轴声、锁舌声、关闭撞击声,最好分层;只放一个“门开”音效会显得很薄。
电梯/移动平台还有一个额外问题:站在上面的玩家要跟着走。UE 的角色移动组件依赖“Base(底座)”来继承移动;平台移动太快或只改了视觉不改碰撞,玩家就会滑下去。平台至少要保证碰撞组件真实移动,并在网络端同步。
2.3 NPC 交互
NPC 交互的难点不在“对话”,而在“对齐”和“占用”。
- 对齐:NPC 的根运动、转身、坐下、伸手,和玩家站的位置往往对不上。Motion Warping 可以在动画蒙太奇里用 Anim Notify State 划出“可扭曲窗口”,运行时把根运动对齐到命名目标点。[16] 例如对话时把 NPC 的朝向对齐到玩家,或者让玩家自动走到 NPC 面前的交互位。
- 占用:两个玩家同时点同一个 NPC,不能都触发对话。Smart Object 提供了“全局数据库 + 空间分区 + 预约”的机制:交互物只提供信息(槽位、标签、行为定义),真正的执行逻辑由交互者实现。[15] 这比在 NPC 蓝图里塞一个
bInUse更稳,因为预约是系统级的,查询可以按区域和 Gameplay Tag 过滤。 - 状态:NPC 至少有
Idle、Patrol、Talk、Trade、Quest、Combat、Dead这些状态;交互许可要跟着状态走。战斗中还能交易的 NPC,要么是设计刻意,要么是 bug。 - 注视:对话时 NPC 看玩家、玩家看 NPC。可以用 Aim Offset、Control Rig 或简单的头部 LookAt;不要直接旋转整个 Actor,否则脚会滑。
- 对话相机:过肩镜头、焦点切换、景深。相机切换要避免“瞬移感”,用
PlayerCameraManager的混合,或者把相机挂在对话场景的固定机位上。
NPC 交互的状态机通常比门复杂得多,适合用 StateTree 这类层级状态机:它把行为树的 Selector 和状态机的 State/Transition 结合起来,每个状态有 Enter Condition 和 Task。[18] 但不要因为“它高级”就给每个宝箱都套 StateTree——简单对象用简单状态机,维护成本更低。
2.4 静态交互
静态交互物的特点是:位置不变、状态不多、反馈要重。拉杆、按钮、终端、宝箱、可阅读的告示牌,都属于这一类。
设计上最重要的三条:
- 一次性还是可重复:宝箱通常一次性;拉杆可能可重复;按钮可能带冷却。这个差异必须写进状态机,不能靠蓝图里的
DoOnce节点糊弄。 - 生效时机:交互的“实际效果”不要绑在输入按下那一帧,而要绑在动画通知(Anim Notify)或时间轴的关键帧上。手还没碰到拉杆,门就开了,玩家会觉得轻飘;手穿模了门还没开,玩家会觉得延迟。
- 失败反馈:上锁的宝箱、没权限的终端、已经拉过的拉杆,都要有明确的“为什么不能用”。最差的做法是按键没反应。
静态交互物通常不需要复杂碰撞:一个盒体交互体积 + 一个简单碰撞即可。真正需要花时间的是动画、声音、UI 提示和状态同步。
2.5 动态交互
动态交互物是“物理参与状态”的一类:木箱、桶、可投掷物、可破坏物、可抓取的尸体。
它和静态交互最大的区别是:物理状态不能直接塞进交互状态机。否则你会得到 Idle / Grabbed / Thrown / Resting / Sleeping / Broken 这种既像交互又像物理的混合状态,最后没人能说清“到底谁在控制它”。
更稳的做法是:
- 交互状态机只关心
Idle / Focused / Ready / Held / Cooldown; - 物理状态(是否模拟、是否睡眠、速度多大)作为
CanInteract的输入条件; - 抓取用“附着 + 关闭物理”或“物理约束 + 目标点”两种实现,按游戏需求选。
| 抓取方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 附着 | 把物体 Attach 到角色手部插槽,关闭物理 | 稳定、便宜、好同步 | 会穿模,不能和世界碰撞 |
| 物理约束 | 用约束/物理手柄把物体拉向目标点 | 真实、能碰撞 | 抖动、网络贵、容易卡住 |
投掷则要注意质量与冲量:同样大小的木箱和铁箱,如果给同样的冲量,轻的会飞、重的几乎不动;如果给同样的速度,重的会像炮弹。通常按“质量归一化的冲量”或“设计好的初速度”来调。
网络方面,动态交互物是最容易超预算的:大量物体的 bReplicateMovement、频繁的睡眠/唤醒、客户端预测后的回滚,都会造成带宽和抖动。常见做法是“只有被交互者持有的物体才高频同步,其余走休眠”。
2.6 动效交互
动效交互是“只改变表现、不改变 gameplay 状态”的交互。草被踩弯、帘子被拨开、风铃响、灯光晃、水面起涟漪,都是这一类。
它的价值是让世界“有反应”,成本低、观感收益高。但它有一个很大的风险:误导玩家的交互预期。如果草能被动、门能被动、箱子能被动,玩家会自然认为“所有会动的东西都能按 E”。所以需要一套稳定的视觉语法:
| 语法 | 含义 | 例子 |
|---|---|---|
| 有高亮/描边 + 提示 | 可交互 | 门、宝箱、NPC |
| 只有物理/风/角色触发 | 纯反应,不可交互 | 草、帘子、水 |
| 有声音但无提示 | 环境反馈 | 远处风声、脚步回声 |
| 有粒子但无状态 | 纯表现 | 灰尘、火星 |
动效交互的节奏也很重要:主反应要快(0–100 ms),次级运动可以慢(200–600 ms)。例如踩到草地,草立刻弯,然后慢慢回弹;如果回弹和弯曲一样快,就像塑料。
2.7 动效实体
动效实体是表现层的“临时对象”:命中粒子、弹孔、火花、烟雾、音效体、镜头震动。它有三个特征:
- 有生命周期:生成、播放、消失;
- 不参与交互判定:玩家不能“按 E 使用”一团火花;
- 可池化:同一类实体大量重复出现,必须复用。
它和动效交互的区别是:动效交互是“交互物的一种属性”,动效实体是“一次表现的单位”。一扇门被打开时,门本身是移动交互物,开门时扬起的灰尘是动效实体。
2.8 三种承载方式:Actor、UObject、Niagara
一句话:Actor 是“世界里的实体”,UObject 是“逻辑与数据”,Niagara 是“看得见的表现”;三者经常一起用,但谁来承担碰撞、权威状态和网络,必须选清楚。
前面的六种形态回答“这是什么交互”,这一节回答“把交互挂在什么东西上”。同一个宝箱,可以做成 Actor、可以把数据拆成 UObject 只同步必要字段再由宿主按需组装、也可以做成一团 Niagara 光效加一个隐藏代理——三种做法的成本和边界完全不同。
先说明:Actor 本身也是 UObject 的子类;下面说的“普通 UObject”特指不继承 Actor、也不继承 SceneComponent 的数据/逻辑对象。它要参与世界交互时,需要一个宿主来组装。
2.8.1 Actor 作为交互物
特点
- Actor 是 UE 里可以被放置或生成到关卡中的对象,可以包含 ActorComponent,并负责网络属性/函数复制。[22]
- 交互链路完整:射线命中碰撞组件 → Actor 实现接口或挂交互组件 → 状态、动画、声音、网络都挂在同一个对象上。
- 适合有“实体感”的交互:门、宝箱、NPC、机关、物理道具、载具。
- 能被关卡、Sequencer、Smart Object、AI 感知、流送和网络相关性管理。
局限
- “重”:每个 Actor 都有注册、Transform、组件、可选的 Tick 和网络相关性;几十个没问题,成千上万个“小草/小物件”都用 Actor,关卡和网络会先崩。
- 默认以 Actor 为交互身份:一个 Actor 上如果有多个可交互子物件(货架上的商品),要么拆成多个 Actor,要么做组件级二级交互,Actor 本身不够细。
- 网络和生命周期复杂:复制、Owner、NetCullDistance、销毁/流送都要处理,不是“放个 Actor 就完事”。每个参与复制的 Actor 都有通道和复制配置,哪怕这个交互物只有一个状态,也要带整套 Actor 配置;数量多时带宽和 CPU 都会涨。
- 纯数据/UI 交互用 Actor 是浪费:背包物品、对话选项不需要 Transform 和碰撞。
适合:需要被射线/碰撞选中、需要世界坐标、需要复制或持久存在于关卡的交互。
2.8.2 UObject 作为交互物:数据同步 + 按需组装
这里的“普通 Object”指非 Actor、非 SceneComponent 的 UObject。用它做交互物,核心动机不是“UObject 更轻”,而是把网络同步从“每个 Actor 全量同步”换成“一份紧凑数据 + 按需组装”:Actor 一旦参与复制,就有 Actor 通道、Transform、组件列表和复制配置;UObject 只带交互需要的数据,客户端拿到数据后只组装这次真正需要的部分。[23] [24]
为什么能省
| 成本 | Actor 做法 | UObject + 按需组装 |
|---|---|---|
| 网络通道 | 每个参与复制的 Actor 都要有通道和复制配置 | 宿主/Manager 批量承载,多个对象共用一条通道 |
| 同步内容 | Transform、组件、属性、RPC、生命周期 | 只同步交互字段:ID、状态、参数、资源引用 |
| 客户端对象 | 完整 Actor + 全部组件 | 只创建需要的 Mesh/音频/动效/碰撞 |
| 变更粒度 | 属性/组件级,带固定开销 | 字段级 delta,按需发 |
| 数量级 | 受通道数和带宽限制 | 更适合大量同类对象 |
| 代价 | 原生、简单 | 自己做序列化、路由、版本、晚加入 |
注意:UObject 本身不会自动复制。它需要由一个参与复制的宿主 Actor/Component 或 Manager 承载,通过 RPC、
FFastArraySerializer、自定义NetSerialize或数据通道批量发送。下面说的“UObject 同步”都是这个意思。
同步模型
- 服务器持有权威数据(UObject/Manager):交互状态、冷却、归属、解锁条件都在服务器。
- 同步层只发必要字段:
InteractionId、状态、参数、资源引用/版本;不是整个对象图。 - 客户端收到后创建/更新本地 UObject;Assembler 按数据决定装什么。
- 客户端输入走宿主 RPC 回服务器;服务器校验后改数据,再同步结果。
- 晚加入/重连:先发全量快照,再发 delta;销毁/卸载要显式发 removal。
- 数据带版本号,资源引用用 soft path/ID;客户端缺资产时降级,不崩。
数据对象长什么样
// 示意:只放同步所需的数据,不放 BeginPlay/Tick/碰撞
UCLASS()
class UInteractableData : public UObject
{
GENERATED_BODY()
public:
UPROPERTY() FName InteractionId;
UPROPERTY() uint8 State = 0;
UPROPERTY() uint16 DataVersion = 1;
UPROPERTY() uint8 AssemblyFlags = 0; // Mesh / Audio / VFX / Collision
UPROPERTY() FTransform LocalTransform;
UPROPERTY() TSoftObjectPtr<UStaticMesh> Mesh;
UPROPERTY() TSoftObjectPtr<UMaterialInterface> Material;
UPROPERTY() TSoftObjectPtr<USoundBase> Audio;
UPROPERTY() TSoftObjectPtr<UNiagaraSystem> VFX;
};
客户端怎么按需组装
- 收到数据/快照后,按
InteractionId找宿主/插槽;找不到就占位或跳过。 - 按
AssemblyFlags只装需要的部分:只装 Mesh、只挂音频、只挂 Niagara,或者只保留数据不装表现。 - 需要碰撞时:Attach 到宿主复用碰撞,或新建
UStaticMeshComponent并设置QueryOnly/响应。 - 绑定命中路由:
HitComponent、BoneName或插槽名 → UObject。 - 绑定时装/表现:音频、Niagara、动画;状态变化只更新参数。
- 对象移出兴趣范围/卸载:解绑、回收组件;数据保留或释放。
和 Actor 做法最大的区别
Actor 是“先有完整对象,再同步它的复制配置”;UObject 是“先有数据,再按需长出表现”。所以:
- 大量静态/半静态、表现可裁剪的交互物,UObject 方案更容易把带宽和对象数压下来。
- 需要独立物理、移动、复杂 RPC、完整生命周期的对象,Actor 仍然更直接。
代价与边界
| 问题 | 说明 / 建议 |
|---|---|
| 没有原生复制 | UObject 不会自己进通道;必须由宿主/Manager 批量承载 |
| 序列化与版本 | 自己定义字段、delta、版本兼容;字段变更要能向后兼容 |
| 身份与晚加入 | 稳定 InteractionId;晚加入先全量快照再 delta;删除要显式 |
| 权威 | 服务器改数据;客户端只组装和预测表现;交互结果以服务器为准 |
| 组装生命周期 | 与宿主/流送/兴趣范围绑定;卸载要解绑、回收,避免悬空 |
| 命中路由 | 多个对象共享宿主时,用组件/插槽/骨骼区分;否则点一个触发全部 |
| 资源加载 | soft ref + 异步;未加载时禁用交互/占位 |
| 调试 | 没有 Actor 的 Inspector/通道视图;需要自建调试面板/日志 |
| 物理/移动 | 需要独立物理、移动复制、复杂 RPC 的对象,仍用 Actor |
适合:大量同类交互物、静态/半静态场景物件、表现可裁剪、数据驱动关卡、网络带宽敏感的项目。
不适合:重物理/移动平台/NPC、需要独立 Actor 通道和 RPC、需要复杂组件生命周期、少量独特对象。
一句话:UObject 做交互物,省的是“每个 Actor 都要同步一整包东西”的钱;花的是“自己实现数据同步、组装和路由”的复杂度。它适合大量、可裁剪的交互物,不适合替代所有 Actor。
前提:如果每个 UObject 还是配一个 Actor 代理,那就没有省。这套设计成立的关键是“共享宿主/Manager + 批量同步 + 按需组装”,而不是把 Actor 换成 UObject 这个名字。
2.8.3 Niagara 效果作为交互物
先澄清一个常见误会:Niagara 是效果系统,UNiagaraComponent 是 Niagara System 的 primitive component,粒子本身不是 gameplay 对象。[25] 所以“用 Niagara 做交互物”通常有两种含义:
- Niagara 只是外观,交互靠代理:隐藏的 Actor/Box/Sphere 负责碰撞和选中,Niagara 负责“看起来像一团火/光/虫群”。这是最常用、最稳的做法。
- 真的想让粒子参与交互:需要 CPU 粒子、碰撞事件、数据通道或自定义查询,成本和边界都多,不适合作为通用交互方案。
特点
- 表现力强:非固体、体积感、动态形状、发光、流体、群体动画,适合“没有实体但看得见”的交互物(能量核心、幽灵、光门、虫群)。
- 可参数化:用 User Parameter 或 Data Channel 把交互状态传给 Niagara;Niagara Data Channels 用于 game code 与 Niagara 之间、或 Niagara 系统之间的数据通信。[26]
- 生成和池化方便:一次性效果用
SpawnSystemAtLocation/SpawnSystemAttached,配合池化和 scalability。 - 不参与 gameplay:默认不作为可查询碰撞体,也没有游戏状态意义上的权威与存档;粒子状态通常不做网络复制,粒子被裁剪/LOD/降级时,“交互物”可能直接消失。
局限
- 没有碰撞代理:射线打不到粒子;必须加 Actor/Component 代理。
- 没有权威和持久化:粒子状态不是游戏状态,不能拿来存档、回滚或复制;服务器不应依赖粒子位置做判定。
- 性能和可见性:GPU 粒子、碰撞、事件读取、GPU readback 都可能很贵;Scalability 可能把效果降没。
- 交互粒度粗:“打中某一颗粒子”很难,通常只能做范围/近似交互;要精确命中就用代理或 CPU 粒子。
- 调试割裂:视觉和逻辑分离,容易出现“看得到但点不到”或“点得到但看不到”。
适合:视觉主体是效果、交互是范围/近似、状态由代理维护的场景(能量场、光门、虫群、可调查的幻象)。
结论:第三种做法严格说不是“用粒子做交互”,而是“用 Niagara 当外观,用 Actor/Component 做交互”。真正要避免的是让 Niagara 承担权威状态,或让 UObject 承担碰撞。
2.8.4 对比
| 维度 | Actor | UObject + 组装 | Niagara |
|---|---|---|---|
| 世界坐标 / Transform | 有 | 无(宿主提供) | 组件有,粒子无 |
| 射线 / 碰撞命中 | 原生 | 借用宿主 / 组装组件 | 默认无,需代理 |
| 关卡摆放 / 流送 | 支持 | 不支持(数据 + 宿主) | 随宿主 Actor |
| 网络同步 | 每个 Actor 通道 + 完整复制配置 | 宿主/Manager 批量,只发字段 | 表现层,通常不复制 |
| 同步粒度 | Actor / 组件 / 属性 | 字段级 delta | 无 game state |
| 客户端对象 | 完整 Actor + 组件 | 按需组装,可能只有数据 | 组件 + 粒子 |
| 持久化 / 存档 | 需自己序列化 | 数据天然可存,需稳定 ID | 不适合 |
| Tick / 更新 | 可选 | 无(宿主驱动) | 组件 Tick / 模拟 |
| 数量级成本 | 高(通道 / 带宽) | 低到中(数据小,组装有成本) | 中到高(GPU/粒子) |
| 表现力 | 中(靠组件) | 取决于组装 | 高 |
| 典型用途 | 门、宝箱、NPC、机关 | 大量数据化交互物、背包/对话 | 能量场、光门、虫群、命中特效 |
| 主要坑 | Actor 数量 / 带宽 | 序列化 / 路由 / 晚加入 | 无碰撞 / 无权威 / 被裁剪 |
2.8.5 怎么选
先问四个问题:
- 需要独立物理、移动、复杂 RPC 或完整生命周期吗? 需要 → Actor;只是交互数据和表现 → UObject + 组装。
- 需要被射线/碰撞选中吗? 需要 → Actor,或 UObject + 宿主碰撞;纯 UI/数据 → UObject 直接。
- 数量级大、同步预算紧、表现可裁剪吗? 是 → UObject 数据 + 按需组装;否 → Actor。
- 视觉主体是实体还是效果? 实体 → Actor + Mesh;效果 → Actor 代理 + Niagara。
一句话选型:
| 需求 | 推荐 |
|---|---|
| 门、宝箱、NPC、机关 | Actor + 交互组件 |
| 需要独立物理/移动的物件 | Actor(物理/移动组件) |
| 背包物品、对话选项、任务条目 | UObject |
| 大量静态/半静态、同步预算紧的交互物 | UObject 数据 + 按需组装 |
| 能量场、光门、虫群 | Actor 代理 + Niagara |
| 命中火花、弹孔 | Niagara/Decal 动效实体,不是交互物 |
混合是常态:
宿主 Actor / SceneComponent(权威:碰撞、生命周期、网络)
├─ UInteractableComponent(契约与状态机)
├─ UObject(交互数据:ID、状态、参数、资源引用)
├─ Assembler(按数据组装 StaticMesh / 绑定表现)
└─ UNiagaraComponent(表现,读状态参数)
Actor 负责“世界身份与完整生命周期”,UObject 负责“可裁剪的数据同步”,Assembler 负责“按需长出表现”,Niagara 负责“看起来怎样”。省同步的关键不是 UObject 这个类型,而是“共享宿主/Manager + 批量同步 + 只组装需要的东西”;如果每个 UObject 还配一个 Actor 代理,那就没有省。
第 3 章 交互状态流转
一句话:状态机的价值不是“多几个状态”,而是让“谁能改状态、什么时候改、改不回去怎么办”有唯一答案。
3.1 状态集合
| 状态 | 含义 | 谁维护 | 玩家可见表现 |
|---|---|---|---|
Unavailable | 未解锁、未通电、被封印 | 对象/服务器 | 无提示,或“需要 XX” |
Idle | 可被感知,但未被选中 | 对象 | 无 |
Focused | 被准星/射线选中 | 客户端 | 高亮、描边、提示 |
Ready | 当前请求合法,等待输入 | 对象 | “按 E 打开” |
Reserved | 已被某交互者占用 | 服务器 | “有人正在使用” |
Executing | 正在执行 | 对象/服务器 | 动画、进度条、声音 |
Cooldown | 执行完,暂时不能再触发 | 对象/服务器 | 灰色、倒计时 |
Broken | 损坏、销毁、禁用 | 对象/服务器 | 破坏表现、不可用 |
注意 Focused 是客户端状态,Reserved/Executing 是权威状态。把两者混在一个枚举里,网络同步就会很痛苦:客户端一移动,焦点变了,难道要发 RPC 告诉服务器“我不看它了”?
3.2 转换表
| 从 | 到 | 条件 | 副作用 |
|---|---|---|---|
Unavailable | Idle | 解锁条件满足 | 播放“通电/解封”表现 |
Idle | Focused | 射线命中且距离/角度合法 | 高亮、显示提示 |
Focused | Idle | 射线离开或超时 | 取消高亮 |
Focused | Ready | CanInteract 为真 | 提示从“靠近”变为“按 E” |
Ready | Executing | 输入按下且服务器批准 | 播放蒙太奇、锁定输入、播放声音 |
Ready | Reserved | 其他交互者已占用 | 提示“有人正在使用” |
Executing | Cooldown | 执行完成 | 应用结果、播放完成反馈 |
Executing | Idle | 取消/打断/离开范围 | 回滚、播放取消反馈 |
Cooldown | Idle | 冷却结束 | 恢复提示 |
| 任意 | Broken | 被破坏/禁用 | 停止所有表现、断开交互 |
3.3 状态图
3.4 三个边界:权限、并发、中断
权限:谁能改状态?
- 服务器:
Reserved、Executing、Cooldown、Broken的权威; - 客户端:
Focused、本地提示、本地预测的Pending; - 对象自己:
Unavailable → Idle这类条件转换,可以在服务器或本地判定,但结果要同步。
并发:两个玩家同时按同一个拉杆怎么办?
- 单机:一个交互者,简单;
- 多人:服务器先到先得,后到者收到
Reserved,提示“有人正在使用”; - AI:Smart Object 预约,避免 NPC 抢同一个座位;
- 同一玩家:输入去抖 + 状态锁,避免一帧内触发两次。
中断:执行到一半怎么办?
常见中断源:受伤、死亡、移动超出范围、切换武器、进入过场、另一个交互抢占了手部、网络断开。每种中断都要回答:
- 结果是否已经应用?如果已经应用,不能回滚(例如宝箱已经打开,不能因为玩家死亡而关上);
- 表现是否要收尾?例如门关到一半被打断,是停在中间,还是继续关;
- 冷却是否要退?通常不退,防止刷;
- 占用是否要释放?必须释放,否则其他人永远用不了。
3.5 用 Gameplay Tags 表达条件
Gameplay Tags 是层级化的标签系统,可以用来描述对象属性、正在做什么、事件触发。[17] 对交互物很合适:
State.Interactable.Ready
State.Interactable.Busy
State.Interactable.Broken
Interaction.Door.Open
Interaction.RequiresKey
Interaction.Blocked.InCombat
用 FGameplayTagQuery 做条件,比在蓝图里连一堆布尔更可读,也方便数据驱动:
RequiredTags:必须全部满足(例如Interaction.RequiresKey+Item.Key.Bronze);BlockedTags:任一命中就拒绝(例如State.Dead、Interaction.Blocked.InCombat);InteractionTag:标识“这次交互是什么”,用于动画、声音、UI 的映射。
3.6 StateTree 适合 NPC,不一定适合门
StateTree 是通用层级状态机,把行为树的 Selector 和状态机的 State/Transition 结合起来;状态有 Enter Condition,任务(Task)可以并行执行,Transition 可以从叶子往根评估。[18] 对 NPC 这种“多状态、多条件、需要行为选择”的对象很合适。
但门、宝箱、按钮这类对象只有几个状态,用 StateTree 是杀鸡用牛刀:调试链路长、启动成本高、设计师也不一定愿意进 StateTree 编辑器。选型建议:
| 对象 | 推荐 |
|---|---|
| 门、抽屉、按钮、宝箱 | 轻量枚举状态机 + 组件 |
| NPC、Boss、复杂机关 | StateTree / 行为树 / GAS |
| 大量同类小交互物 | 数据驱动 + 共享基类,不要每个都写蓝图 |
3.7 网络:服务器权威,客户端预测
交互的网络模型可以总结成三句话:
- 状态变更走服务器:开门、交易、拾取、造成伤害,都由服务器批准。
- 焦点和提示走客户端:射线检测、UI 高亮、按钮提示是本地表现,不需要同步。
- 表现走多播/不可靠:粒子、声音、贴花是 cosmetic,丢了就丢了,不要为了它们重传。
客户端预测的常见做法:
- 按下时立刻进入
Pending,播一个“弱反馈”(按钮按下、UI 变化),但不要播“结果”(门已经开了); - 服务器批准后,广播“结果”,客户端从
Pending进入Executing; - 服务器拒绝时,客户端回滚
Pending,提示原因; - 不要用“客户端先开门,服务器拒绝再关回去”这种强预测,玩家会看到门抽搐。
3.8 常见状态 bug
| 现象 | 根因 | 修法 |
|---|---|---|
交互物永远停在 Busy | 中断时没释放占用 | 在 EndInteract 和 OnUnregister 都释放 |
| 一次输入触发两次 | 输入没去抖/状态没锁 | 输入层去抖 + Executing 锁 |
| 准星已经移开,提示还在 | 焦点没清 | 每帧/定时重算焦点,或离开时显式清理 |
| 冷却只在客户端 | 服务器没记冷却 | 冷却时间戳放服务器,客户端用剩余时间显示 |
| 门开了又关上 | 客户端预测 + 服务器拒绝 | 改成 Pending 弱反馈 |
| NPC 对话中还能被攻击交互 | 状态没阻挡 | BlockedTags 加 State.InCombat |
第 4 章 射线瞄准交互
一句话:射线只负责“我看见谁”,不负责“我能不能用”;把选中和许可分开,才能同时支持键鼠、手柄、触屏和 VR。
4.1 从屏幕中心到世界
最常见的第三人称/第一人称瞄准交互:
- 取玩家视点(
GetPlayerViewPoint)或相机位置; - 从视点沿相机前向发射一条射线,长度等于交互距离;
- 命中后取
HitActor,检查是否实现IInteractable; - 再调用
CanInteract做距离、角度、标签、占用判定; - 通过后设为焦点,显示提示。
为什么不直接从角色胸口发一条射线?因为玩家是用“屏幕准星”瞄准的,从相机发更符合直觉。但相机射线会穿过角色身后的墙,所以还要用“角色到命中点的距离”或“角色到交互物的距离”做二次校验。
4.2 单线、多线、扫描
| 方式 | API | 特点 | 适用 |
|---|---|---|---|
| 单线 | LineTraceSingleByChannel | 最便宜,只取最近命中 | 键鼠、准星精确瞄准 |
| 多线 | LineTraceMultiByChannel | 返回到第一个阻挡命中为止的所有命中 | 需要穿透/多个候选 |
| 球形扫描 | SphereTraceSingleByChannel | 有体积,容错高 | 手柄、触屏、VR 手部 |
| 盒体扫描 | BoxTraceSingleByChannel | 可调形状,适合宽交互 | 大物件、载具 |
| 重叠 | OverlapMultiByChannel | 不依赖朝向 | 范围交互、近身拾取 |
LineTraceMulti 的官方说明是:沿给定直线做碰撞追踪,返回遇到的所有命中,直到并包括第一个阻挡命中。[5] 这个语义很重要:它不会返回阻挡命中之后的物体,所以“隔墙选中”不会发生,但“多个重叠的交互物”可以一次拿到。
4.3 命中结果读什么
FHitResult 里最常用的字段:
| 字段 | 用途 |
|---|---|
GetActor() | 找到交互物 Actor |
GetComponent() | 找到具体组件(碰撞箱/骨骼/交互体积) |
ImpactPoint | 交互点、贴花位置、粒子位置 |
ImpactNormal | 贴花朝向、粒子朝向、反弹方向 |
BoneName | 骨骼命中部位(爆头、弱点、护甲) |
PhysMaterial | 表面类型,驱动音效和粒子 |
Distance | 距离判定、瞄准辅助评分 |
bBlockingHit | 是否被阻挡 |
一个常见错误是只读 HitActor,然后用 Actor 原点算距离。对于一扇大门,Actor 原点可能在门框中心,玩家站在门边时距离仍然很远;应该用 ImpactPoint 或交互物上的“交互点”组件。
4.4 通道设计
不要把交互和子弹混在一个通道里。推荐至少分出:
| 通道 | 用途 | 谁响应 |
|---|---|---|
Visibility | 通用视线、相机遮挡 | 世界静态、角色 |
Camera | 相机碰撞 | 世界静态 |
Interaction | 交互射线 | 交互物体积 |
Projectile | 子弹/射弹 | 角色、可破坏物、世界 |
InteractionProbe | 角色近身检测 | 交互物体积 |
在 Project Settings → Collision 里加自定义 Object Channel 和 Trace Channel;交互物用 Interaction 通道响应,角色胶囊体不响应,避免“射线打到自己”。[2]
4.5 瞄准辅助
手柄和触屏需要辅助,否则很难选中小物件。常见手段:
- 锥形评分:在准星周围一定角度内找候选,按“夹角 + 距离 + 优先级”排序;
- 球形扫描:用
SphereTrace代替线,给小物件一个“虚拟体积”; - 磁性吸附:当候选在阈值内,把准星/提示吸附过去;离开时要有滞回(进入 8°,退出 12°),否则会抖;
- 焦点保持:已经选中的目标,允许在更大范围内保持焦点,避免手抖丢失;
- 优先级:任务目标 > 可交互物 > 动效交互 > 背景。
评分可以写成:
float Score = FMath::Pow(1.f - Angle / MaxAngle, 2.f) * (1.f - Distance / MaxDistance);
这只是示意,实际还要乘上“交互物优先级”“是否被遮挡”“是否在屏幕内”等权重。关键是不要让辅助抢走玩家的意图:玩家明明瞄着远处敌人,准星却被近处箱子吸走,就是失败。
4.6 频率与稳定性
射线不需要每帧打。常见做法:
- 相机/角色移动时,10–20 Hz 重算;
- 静止时降到 5 Hz,或有输入时再算;
- 焦点变化时立即更新 UI;
- 用定时器而不是
Tick,方便统一降频和暂停。
稳定性的关键是滞回和去抖:进入焦点需要持续 0.05–0.1 秒,离开焦点需要离开 0.1–0.2 秒。否则玩家轻微移动,提示就会闪。
4.7 一段可用的检测代码
bool UInteractionSenseComponent::FindFocus(FHitResult& OutHit) const
{
APlayerController* PC = GetOwningPlayerController();
if (!PC) { return false; }
FVector ViewLocation;
FRotator ViewRotation;
PC->GetPlayerViewPoint(ViewLocation, ViewRotation);
const FVector End = ViewLocation + ViewRotation.Vector() * TraceDistance;
FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionFocus), false, GetOwner());
Params.AddIgnoredActor(GetOwner());
Params.bReturnPhysicalMaterial = true;
return GetWorld()->LineTraceSingleByChannel(
OutHit, ViewLocation, End, ECC_GameTraceChannel1, Params);
}
这里的 ECC_GameTraceChannel1 是自定义通道的默认枚举名;实际项目里建议在 DefaultEngine.ini 里给它起名 Interaction,代码里再用 UEngineTypes::ConvertToTraceType 或项目自己的常量封装,避免“通道 1 到底是什么”的考古。
如果想吃透最小闭环,社区的 Line Trace + 接口交互教程可以对照着看:它把“打射线 → 判断接口 → 触发交互”压缩成了一个很短的例子。[21]
4.8 常见坑
| 现象 | 原因 | 修法 |
|---|---|---|
| 射线穿过墙选中后面的物体 | 用了 Visibility 且墙不响应 | 用专用 Interaction 通道,墙设为阻挡 |
| 射线打到自己 | 没忽略自身 | Params.AddIgnoredActor(GetOwner()) |
| 小物件很难选中 | 只有细线,没有体积 | 球形扫描或给交互体积 |
| 复杂碰撞导致掉帧 | bTraceComplex = true | 交互射线用简单碰撞 |
| 子组件命中但 Actor 不对 | 组件属于别的 Actor | 用 GetActor() 而不是组件 owner 猜 |
| 焦点闪烁 | 没有滞回/去抖 | 进入/退出阈值 + 时间窗 |
第 5 章 碰撞:碰撞箱、胶囊体与不规则形状
一句话:碰撞体是交互的“代理”,不是美术的“外形”。代理要服务查询、移动和手感,而不是追求和模型完全一致。
5.1 简单碰撞与复杂碰撞
UE 的碰撞体分两类:简单碰撞是立方体、球体、胶囊体、凸包等基本形状;复杂碰撞是物体的三角网格(trimesh)。默认情况下引擎会同时生成简单和复杂形状,再根据查询类型选用。[1]
Static Mesh 的 Collision Complexity 有四个选项:
| 选项 | 含义 | 注意 |
|---|---|---|
| Project Default | 用项目物理设置,简单查询用简单碰撞,复杂查询用复杂碰撞 | 默认行为 |
| Simple And Complex | 同时生成两种,按查询类型选用 | 内存占用较高 |
| Use Simple Collision As Complex | 复杂查询也用简单碰撞 | 省内存,适合简单几何 |
| Use Complex Collision As Simple | 简单查询也用复杂碰撞 | 不能模拟该物体,但可以和其他简单碰撞物体碰撞 |
“Use Complex Collision As Simple”是最容易被误用的选项:它让复杂网格参与简单查询,适合静态场景的精确碰撞,但如果你把它用在需要模拟的物体上,物体会无法正常模拟。官方文档明确说,使用它时不能模拟该物体,但可以用来和其他模拟中的简单物体碰撞。[1]
5.2 碰撞启用与响应
每个物理体有 Object Type 和一组 Response。碰撞响应有三种:Block、Overlap、Ignore;追踪响应同理,只是追踪本身也可以被设为某种响应类型。[2]
几条容易记错的规则:
- 两个物体都设为 Block,才会真正阻挡;
- 一个 Overlap、一个 Block,结果是 Overlap,不是 Block;
- 一个 Ignore、一个 Overlap,不会产生 Overlap 事件;
- Overlap 事件需要开启
Generate Overlap Events; - 要触发
Event Hit,需要开启Simulation Generates Hit Events; - 不建议同一个物体同时处理 Collision 和 Overlap 事件,手动管理成本很高。
Collision Enabled 有四种:No Collision、Query Only、Physics Only、Collision Enabled。[3] 交互体积通常用 Query Only:它能被射线/重叠查询到,但不参与物理模拟,也不会挡住玩家。
5.3 三件套:物理体、交互体积、命中代理
一个成熟的交互物,碰撞通常不止一个:
| 组件 | 作用 | 典型设置 |
|---|---|---|
| 物理体(Mesh/Box/Capsule) | 真实阻挡、模拟、承载 | Collision Enabled,按需 Block |
| 交互体积(Box/Sphere/Capsule) | 判定“是否可被选中/在范围内” | Query Only,响应 Interaction |
| 命中代理(可选) | 让射线打到“可交互区域”,而不是整面墙 | 简单碰撞,挂在插槽/骨骼上 |
三者分开的好处:
- 美术可以把模型做得很大,但交互体积只覆盖“把手”附近,避免玩家隔着老远就能开门;
- 物理碰撞可以很粗糙,交互体积可以很精确,互不牵制;
- 交互体积可以比物理体大一圈,提升手柄/触屏的手感;
- 复杂形状可以用多个简单碰撞拼,不需要复杂碰撞。
5.4 碰撞箱
UBoxComponent 是最常用的交互体积:便宜、可旋转、边界清晰。
适合:
- 门、宝箱、终端、按钮;
- 需要“正面交互”的物体(盒体的朝向就是交互面);
- 需要精确控制交互范围的机关。
注意事项:
- 盒体的
Box Extent是半尺寸,不是全尺寸; - 盒体默认以中心为原点,挂在插槽上时要调
RelativeLocation; - 盒体可以设
Query Only,不要让它挡住玩家; - 如果盒体要跟着门板动,挂在门板组件下,而不是挂在 Actor 根节点。
5.5 胶囊体
胶囊体常用于两类东西:
- 角色:Character 类默认带
CapsuleComponent,用于移动碰撞;官方文档明确说,为了计算复杂几何,Character 的碰撞组件假设是一个竖直朝向的胶囊。[6] - 交互体积:站在地上的物件、NPC 的“可交互范围”、近身拾取,都可以用胶囊体。
胶囊体的好处是“没有尖角”,角色贴着走不会卡;坏处是“边界不直观”,尤其是竖直/水平朝向和半径/半高。对于交互体积,胶囊体适合“人形/圆柱形”的对象,不适合方形柜子。
5.6 不规则形状
不规则形状的碰撞,按成本从低到高:
| 方案 | 做法 | 成本 | 适用 |
|---|---|---|---|
| 单个简单形状 | 用一个盒/球/胶囊包住 | 最低 | 远距离、低精度 |
| 复合简单碰撞 | 多个盒/球/凸包拼起来 | 低 | 家具、机械、门框 |
| 凸包分解 | 用 V-HACD 等工具把网格拆成多个凸包 | 中 | 不规则石块、雕塑 |
| 复杂碰撞(trimesh) | 直接用三角网格 | 高 | 静态世界、精确射线 |
| 物理资产(骨骼) | 每根骨骼一个胶囊/盒 | 中 | 角色、生物、可破坏肢体 |
选择原则:
- 会被大量查询的(交互射线、子弹):用简单碰撞;
- 只做静态遮挡的(墙、地面):可以用复杂碰撞;
- 需要模拟的:绝对不能用
UseComplexAsSimple; - 角色/生物:用物理资产,命中时读
BoneName做部位反馈。
一个常见误区是“碰撞越贴合模型越好”。对于交互,贴合反而可能降低手感:门把手的碰撞如果只有几厘米,手柄玩家很难选中;加一个 20–30 cm 的交互盒,体验会好很多。对于移动,贴合会让角色卡在装饰物上;适当的“碰撞简化”是必要的。
5.7 交互体积与物理碰撞分离
这是整章最重要的一条:不要让“能走进去”和“能按 E”共用同一个碰撞体。
反例:
- 用门板的复杂碰撞做交互检测:射线打到门框边缘,判定失败;
- 用角色的胶囊体做交互范围:玩家站在箱子后面,仍然能隔箱交互;
- 用整面墙的碰撞做“按钮”的交互体积:玩家点墙的任何地方都能触发。
正例:
- 门板:物理碰撞(阻挡)+ 门把手交互盒(Query Only);
- 宝箱:箱体物理碰撞 + 正面上方的交互盒;
- NPC:角色胶囊(移动)+ 头/胸的交互盒(对话/搜刮);
- 电梯按钮:墙面物理碰撞 + 按钮面板的交互盒。
5.8 查询参数
FCollisionQueryParams 里几个常用开关:
| 参数 | 作用 |
|---|---|
AddIgnoredActor | 忽略自己、队友、载具 |
bTraceComplex | 是否用复杂碰撞;交互射线通常 false |
bReturnPhysicalMaterial | 返回物理材质,用于音效/粒子 |
bFindInitialOverlaps | 重叠查询是否包含初始重叠 |
bIgnoreTouches | 忽略 Touch 事件 |
bSkipNarrowPhase | 跳过窄相,只看包围盒(慎用) |
5.9 常见坑
| 现象 | 原因 | 修法 |
|---|---|---|
| 交互物“没有碰撞” | 组件 Collision Enabled = No Collision | 改成 Query Only |
| 射线打不到 | 通道不响应 / 组件没开查询 | 检查 Object Type 和 Response |
| 玩家被交互体积挡住 | 交互体积设成了 Collision Enabled | 改成 Query Only |
| 门开了碰撞还在原地 | 碰撞没挂在动的组件下 | 把碰撞挂到门板 |
| 复杂碰撞导致卡顿 | 每帧对复杂网格做射线 | 用简单碰撞/交互体积 |
| 角色卡在门槛 | 简单碰撞太贴合/有缝隙 | 加一个斜坡碰撞或简化 |
| 重叠事件不触发 | 没开 Generate Overlap Events | 打开并检查双方响应 |
第 6 章 音频触发与材质音效
一句话:声音是交互的“确认键”。玩家很多时候不是看到交互成功,而是听到交互成功。
6.1 三层声音
一次交互的声音,通常可以拆成三层:
| 层 | 作用 | 例子 |
|---|---|---|
| 动作层 | 告诉玩家“输入被接收了” | 按钮咔哒、拉杆吱呀、手碰到门 |
| 材质层 | 告诉玩家“这是什么材质” | 金属、木头、石头、血肉 |
| 结果层 | 告诉玩家“发生了什么” | 门锁打开、宝箱开启、机关启动 |
三层不一定要同时存在,但缺少任何一层都会让反馈变薄。只有动作层,像在按空气;只有结果层,像游戏在替你做事。
6.2 触发点
| 触发点 | 适合 | 注意 |
|---|---|---|
| 输入按下 | UI 点击、按钮反馈 | 只适合“确认输入”,不适合“世界结果” |
| 动画通知(Anim Notify) | 脚步、手接触、武器挥砍 | 和动画帧精确对齐 |
| 状态进入 | 门开始移动、机关启动 | 适合持续音/循环音 |
| 物理事件 | 碰撞、破碎、掉落 | 需要过滤和限频 |
| 射线命中 | 子弹命中、近战命中 | 用 FHitResult 的物理材质 |
最常用的是动画通知:手碰到门把手的瞬间播放“咔哒”,脚落地的瞬间播放脚步声。把声音绑在输入上,容易出现“手还没碰到,声音先响”的延迟感。
6.3 物理材质 → 表面类型 → 音效
UE 的物理材质(Physical Material)用来定义物体在物理交互中的响应,也可以携带表面类型(Surface Type)。官方文档把物理材质描述为“应用于物理模拟图元、直接或通过材质应用,用来配置和控制物理属性的资产”。[7]
官方的一个完整示例是“给角色添加脚步声”:用物理材质表示表面类型(金属、石头等),用 Sound Cue 做不同表面的声音,再用一个自定义 Data Asset 保存“表面 → 声音”的映射,最后在蓝图函数库里按检测到的表面返回对应声音。[8] 这个结构可以直接搬到交互音效上:
Mesh / Material
-> Physical Material
-> Surface Type (Metal / Stone / Wood / Flesh)
-> SurfaceAudioDataAsset
-> Sound Cue / MetaSound
-> Audio Component (3D, attenuation, concurrency)
6.4 一个数据资产示例
USTRUCT(BlueprintType)
struct FSurfaceAudioEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) TEnumAsByte<EPhysicalSurface> Surface = SurfaceType_Default;
UPROPERTY(EditAnywhere) TSoftObjectPtr<USoundBase> Sound;
UPROPERTY(EditAnywhere) float Volume = 1.f;
UPROPERTY(EditAnywhere) float PitchJitter = 0.08f;
};
UCLASS(BlueprintType)
class USurfaceAudioDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) TArray<FSurfaceAudioEntry> Entries;
USoundBase* Find(EPhysicalSurface Surface) const
{
for (const FSurfaceAudioEntry& Entry : Entries)
{
if (Entry.Surface == Surface)
{
return Entry.Sound.LoadSynchronous();
}
}
return nullptr;
}
};
实际项目里不要把 LoadSynchronous 放在高频路径上;可以在加载时预加载,或者用 TSoftObjectPtr + 异步加载。这里只是为了展示结构。
6.5 同一材质的不同音效
“不同材质不同音效”只是第一层。同一个材质,还需要变化:
- 随机变体:每个表面准备 3–5 个样本,随机选,避免重复;
- 音高抖动:±5–10% 的随机音高,让重复命中不机械;
- 力度分层:轻踩/重踩、轻击/重击,用不同的样本或音量;
- 复合声音:主材质声 + 细节声(碎石、金属余响)+ 角色 effort;
- 距离/空间:近处是 3D 声音,远处可能降级为 2D 或直接不播。
一个简单的随机化:
void UInteractionAudioComponent::PlaySurfaceSound(EPhysicalSurface Surface, const FVector& Location)
{
USoundBase* Sound = SurfaceAudio->Find(Surface);
if (!Sound) { return; }
const float Pitch = 1.f + FMath::FRandRange(-PitchJitter, PitchJitter);
UGameplayStatics::PlaySoundAtLocation(this, Sound, Location, FRotator::ZeroRotator, Volume, Pitch);
}
6.6 空间化与衰减
3D 声音需要衰减(Attenuation)。USoundAttenuation 的官方定义就是“定义声音随听者距离如何变化”。[10] 交互音效通常分为:
| 类型 | 空间化 | 衰减 | 例子 |
|---|---|---|---|
| 近身交互 | 3D | 短距离 | 开门、拉杆、拾取 |
| 环境反馈 | 3D | 中长距离 | 机关轰鸣、远处爆炸 |
| UI/确认 | 2D | 无 | 按钮点击、任务完成 |
| 持续机械 | 3D + 循环 | 中距离 | 电梯运行、传送带 |
常见错误是把 UI 音效做成 3D,玩家转个视角声音就没了;或者把环境音效做成 2D,走到哪里都一样响。
6.7 MetaSounds:让声音跟随参数
MetaSounds 是 UE5 的下一代音源系统:它本质上是一个 DSP 渲染图,支持采样级精度、运行时参数、预设复用,并且可以从 gameplay 事件触发。[9] 对交互物很有用的场景:
- 门的开合速度驱动金属摩擦声的音高/滤波;
- 机关的能量等级驱动嗡鸣的层数;
- 材质的硬度驱动撞击声的瞬态;
- 玩家的距离驱动混响/低通。
如果项目还在用 Sound Cue,也完全可以先把“材质 → 声音”的数据表跑通,再逐步把复杂交互换成 MetaSound。
6.8 音频预算与并发
| 项目 | 建议 | 说明 |
|---|---|---|
| 同时播放的交互音效 | 24–32 | 用 USoundConcurrency 限流 |
| 同一声音的实例 | 2–3 | 超过就截断最旧 |
| 随机变体 | 3–5/表面 | 避免“机关枪重复” |
| 预加载 | 交互物周边 | 不要第一次开门时卡一下 |
| 遮挡 | 重要交互 | 墙后的声音要低通/衰减 |
6.9 常见坑
| 现象 | 原因 | 修法 |
|---|---|---|
| 声音比动画慢 | 绑在动画结束/蓝图延迟 | 绑在 Anim Notify 的接触帧 |
| 所有材质一个声 | 没读 PhysMaterial | 用 Hit.PhysMaterial 或组件覆盖 |
| 声音重复到烦 | 只有一个样本 | 多变体 + 音高抖动 + 限频 |
| 转身声音消失 | 2D/3D 设置错 | UI 用 2D,世界用 3D |
| 远处声音太响 | 衰减没调 | 用距离衰减 + 低通 |
| 多人声音不同步 | 客户端各自播 | 由服务器多播,或接受 cosmetic 差异 |
第 7 章 射弹与弹孔
一句话:弹孔不是“往墙上贴一张图”,而是“命中点 + 法线 + 材质 + 生命周期 + 预算”的组合。
7.1 即时命中与射弹
| 方式 | 做法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 即时命中(Hitscan) | 按下瞬间做射线,命中点直接结算 | 便宜、确定、好同步 | 没有飞行时间,不能拐弯 | 步枪、手枪、狙击 |
| 射弹(Projectile) | 生成一个带速度的 Actor,飞行中检测碰撞 | 有飞行时间、可追踪、可反弹 | 网络复杂、性能更高 | 火箭、榴弹、箭、投掷物 |
很多游戏会混合:服务器用即时命中做权威结算,客户端播一个“假射弹”做视觉;或者反过来,服务器生成射弹,客户端做预测。关键是权威结算只有一套,否则会出现“我看到打中了,服务器说没中”。
7.2 射弹运动组件
UE 的 UProjectileMovementComponent 负责更新另一个组件的位置,支持反弹、追踪等行为。官方文档特别说明:如果被更新的组件正在模拟物理,只有初始发射参数(初始速度非零时)会影响射弹,之后由物理模拟接管。[13]
AProjectile::AProjectile()
{
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
ProjectileMovement->InitialSpeed = 12000.f;
ProjectileMovement->MaxSpeed = 12000.f;
ProjectileMovement->ProjectileGravityScale = 0.2f;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->bShouldBounce = false;
}
常用参数:
| 参数 | 作用 |
|---|---|
InitialSpeed / MaxSpeed | 初速度与最大速度 |
ProjectileGravityScale | 重力缩放,0 为直线飞行 |
bShouldBounce | 是否反弹 |
Bounciness / Friction | 反弹系数与摩擦 |
bRotationFollowsVelocity | 弹体朝向跟随速度 |
HomingTargetComponent | 追踪目标 |
OnProjectileStop | 停止时的回调,适合生成命中表现 |
7.3 命中包
一次命中要播放的东西,最好打包成数据资产,而不是在蓝图里连十个节点:
USTRUCT(BlueprintType)
struct FImpactPackage
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) TObjectPtr<UMaterialInterface> DecalMaterial;
UPROPERTY(EditAnywhere) FVector DecalSize = FVector(10.f, 6.f, 6.f);
UPROPERTY(EditAnywhere) TObjectPtr<UNiagaraSystem> ImpactVFX;
UPROPERTY(EditAnywhere) TObjectPtr<USoundBase> ImpactSound;
UPROPERTY(EditAnywhere) float DecalLifeSpan = 30.f;
};
然后按“表面类型 + 命中部位”选包:
Metal -> 火花 + 金属声 + 小弹孔
Wood -> 木屑 + 闷响 + 深弹孔
Stone -> 粉尘 + 脆响 + 浅弹孔
Flesh -> 血雾 + 湿响 + 无弹孔(或血迹贴花)
7.4 弹孔贴花
UE 的贴花(Decal)是投影到网格表面的材质。[11] 官方 API 把它描述为“渲染到网格表面的材质,像模型上的贴纸”,贴花组件有 DecalSize、FadeIn/Out、SortOrder 等参数,适合做弹孔、血迹、涂鸦。[12]
生成弹孔的常用 API 是 UGameplayStatics::SpawnDecalAtLocation:
const FRotator DecalRotation = FRotationMatrix::MakeFromX(Hit.ImpactNormal).Rotator();
UGameplayStatics::SpawnDecalAtLocation(
this,
ImpactPackage->DecalMaterial,
ImpactPackage->DecalSize,
Hit.ImpactPoint + Hit.ImpactNormal * 1.5f, // 沿法线抬一点,避免 z-fighting
DecalRotation,
ImpactPackage->DecalLifeSpan);
几个细节:
- 朝向:让贴花的投影轴与命中法线对齐。不同项目对“贴花朝里/朝外”的约定可能不同,如果发现贴花反了,把法线取反即可;先在编辑器里确认一次。
- 偏移:沿法线抬 1–2 cm,避免和表面重叠产生 z-fighting;抬太多会“浮”在表面。
- 尺寸:
DecalSize的 X 是投影深度,Y/Z 是平面尺寸;弹孔通常 5–15 cm,太大像贴纸,太小看不清。 - 随机:随机 roll(绕法线旋转)和 0.8–1.2 的缩放,避免所有弹孔一模一样。
- 附着:打在可移动物体上时,用
SpawnDecalAttached挂到组件/骨骼,否则物体会从弹孔下面走掉。 - 寿命:
LifeSpan或SetFadeOut,不要让弹孔永久堆积。
7.5 弹孔预算与池化
弹孔是最容易“看起来便宜、实际很贵”的东西:
| 问题 | 后果 | 做法 |
|---|---|---|
| 无限生成 | Draw call/材质实例爆炸 | 每个区域上限 128–256,超出淡出最旧 |
| 每个弹孔一个 Dynamic Material Instance | 内存和排序成本 | 共享材质,参数用实例 |
| 永久存在 | 关卡越玩越卡 | LifeSpan 30–120 秒,或按区域清理 |
| 高精度贴花 | 移动端 overdraw | 低配用简单面片/材质混合 |
| 打在骨骼上 | 弹孔随动画游走 | 挂骨骼/插槽,或只生成在静态表面 |
7.6 穿透与多命中
穿透需要多次查询:
- 从起点打到第一个命中;
- 记录入口点和法线;
- 沿射线继续,找到出口;
- 在入口和出口各生成一个弹孔/粒子;
- 按表面厚度和材质决定是否继续。
LineTraceMulti 可以一次拿到多个命中,但它会在第一个阻挡命中处停止;如果要穿过多个物体,需要多次单线查询,或者用 Sweep + 自定义过滤。穿透的预算要单独控制:一发子弹最多穿 2–3 层,否则性能和表现都会失控。
7.7 网络
- 弹孔、火花、声音是 cosmetic:用
NetMulticast, Unreliable广播,丢了不影响玩法; - 伤害/破坏是权威:服务器结算,客户端表现;
- 射弹的移动:服务器权威 + 客户端预测,或者客户端本地生成“假射弹”,服务器用即时命中复核;
- 弹孔数量:每个客户端自己限流,不要同步“弹孔列表”。
7.8 一个可参考的开源例子
GitHub 上的 UE5-Weapon-System 把射弹、枪口闪光、弹孔贴花、命中粒子放在一个示例里,适合对照本文的“命中包”结构看。[19] 它不是标准答案,但能帮你快速看到“一次射击要生成哪些东西”。
第 8 章 动效实体
一句话:动效实体是交互的标点符号。用得好,交互有重量;用不好,满屏都是噪音。
8.1 Niagara 是主力
UE5 的 Niagara 是主要的视觉特效系统,官方文档把它描述为“在 UE5 中做视觉特效(VFX)的主要工具”。[14] 一次交互的动效实体,通常由 Niagara 系统 + 贴花 + 声音 + 灯光组成。
8.2 一个交互的“表现包”
| 交互 | Niagara | 贴花 | 声音 | 其他 |
|---|---|---|---|---|
| 子弹命中 | 火花/碎屑/烟 | 弹孔 | 材质命中声 | 灯光闪烁、震屏 |
| 开门 | 灰尘 | 无 | 门轴/锁舌 | 灯光变化 |
| 宝箱开启 | 光柱/尘埃 | 无 | 锁扣/木头 | 镜头轻推 |
| 机关启动 | 能量环/电弧 | 无 | 嗡鸣/充能 | 灯光、手柄震动 |
| NPC 对话 | 无/氛围粒子 | 无 | 语音/环境 | 相机、景深 |
“表现包”最好也数据驱动:一个 UInteractionEffectSet 资产,按交互标签索引,运行时按标签取用。这样美术/TA 可以独立调整,不需要程序改蓝图。
8.3 生命周期
一个动效实体的生命周期:
Spawn -> Activate -> (Loop / Once) -> Deactivate -> Return to Pool / Destroy
关键点:
- Spawn:用
UNiagaraFunctionLibrary::SpawnSystemAtLocation或SpawnSystemAttached; - Attach:需要跟随物体/骨骼时用 Attached,注意插槽和缩放;
- AutoDestroy:一次性特效播完自动销毁;循环特效要手动停;
- Pooling:Niagara 系统可以开启池化,避免频繁创建/销毁;
- OnSystemFinished:需要回调时绑定,注意对象生命周期。
8.4 池化与预算
| 项目 | 建议 | 说明 |
|---|---|---|
| 每帧生成上限 | 16–32 | 超出合并/丢弃 |
| 同时存活命中特效 | 64–128 | 按距离/重要度降级 |
| 弹孔贴花 | 128–256/区域 | 超出淡出最旧 |
| 同时播放音效 | 24–32 | 用 concurrency |
| 粒子系统复杂度 | 按平台分级 | 移动端降发射数/材质 |
| 生命周期 | 0.5–3 秒 | 不要留永久粒子 |
8.5 降级
动效实体是最适合做“重要度降级”的:
- 距离远的命中特效,减少粒子数/关闭光源;
- 屏幕边缘的特效,降低更新频率;
- 低配平台,关闭次要发射器;
- 多人场景,限制同屏特效数量;
- 过场/UI 打开时,暂停或降低环境特效。
Niagara 本身有 scalability 和 effect type 的机制,可以把“低/中/高”配置做成资产,而不是在代码里写 if (Platform == Mobile)。
8.6 调试
- 用
stat niagara看粒子开销; - 用 Niagara Debugger 看发射器/粒子数;
- 用
showflag.Particles等开关隔离问题; - 用
stat game看整体帧时间; - 用
stat audio看音频线程; - 用
stat physics看物理开销。
第 9 章 工程落地与验收
一句话:把“每类交互物写一套”变成“一套组件 + 数据配置 + 少量特例”,项目才能规模化。
9.1 组件划分
| 组件 | 职责 | 不该做什么 |
|---|---|---|
UInteractableComponent | 实现接口、状态机、描述符、许可判定 | 不做射线检测、不播声音 |
UInteractionSenseComponent | 射线/重叠、焦点、瞄准辅助 | 不改交互物状态 |
UInteractionMoveComponent | 门/电梯/抽屉的位移与旋转 | 不决定“能不能开” |
UInteractionAudioComponent | 表面音效、随机化、衰减 | 不做 gameplay 判定 |
UInteractionEffectComponent | 生成命中包/动效实体 | 不管理交互状态 |
UInteractionPromptWidget | 提示 UI、进度条、失败原因 | 不直接调用交互 |
这样拆分后,一个“门”= 交互组件 + 移动组件 + 音频组件 + 碰撞;一个“宝箱”= 交互组件 + 音频组件 + 动效组件 + 动画。新交互物主要是拼组件和配数据,而不是复制蓝图。
9.2 数据驱动
建议至少有三张表/资产:
- 交互描述表:交互标签 → 动词、距离、角度、按住时长、所需标签、动画;
- 表面音效表:表面类型 → 声音、音量、音高抖动、变体;
- 命中包表:表面类型/部位 → 贴花、粒子、声音、灯光。
再加上 Gameplay Tags 做条件,基本可以做到“新加一种材质/一种门,不改代码”。
9.3 网络清单
- 交互状态由服务器权威;
- 客户端只预测“输入被接收”,不预测“结果已发生”;
- 占用/预约在服务器释放,断线/死亡/销毁都要释放;
- 冷却时间戳在服务器,客户端只做显示;
- 表现走多播/不可靠,不参与结算;
- 射弹/投掷物的权威结算只有一套;
- 弹孔/粒子/声音不做可靠同步。
9.4 性能预算
| 项目 | 建议上限 | 备注 |
|---|---|---|
| 交互射线 | 10–20 Hz | 不用 Tick |
| 交互物数量/区域 | 按场景,50–200 | 用空间分区/距离剔除 |
| 复杂碰撞查询 | 仅静态世界 | 动态物体用简单碰撞 |
| 每帧动效实体 | 16–32 | 超出合并 |
| 同屏命中特效 | 64–128 | 按距离降级 |
| 弹孔 | 128–256/区域 | 淡出最旧 |
| 同时交互音效 | 24–32 | concurrency |
| 交互 UI 更新 | 焦点变化时 | 不要每帧刷 |
9.5 调试入口
| 工具 | 用途 |
|---|---|
show Collision / showflag.Collision 1 | 看碰撞体 |
DrawDebugLine / DrawDebugSphere | 看射线和检测范围 |
| Gameplay Debugger | 看 AI/感知/标签 |
stat game | 看整体帧时间 |
stat physics | 看物理开销 |
stat niagara | 看粒子开销 |
stat audio | 看音频线程 |
| Audio Insights | 看音频并发/衰减 |
| Niagara Debugger | 看发射器/粒子 |
9.6 验收清单
一个交互物上线前,至少过一遍:
- 玩家能在 3 秒内判断“能不能交互”;
- 按下到第一反馈 < 100 ms;
- 失败有明确原因(太远/被占用/未解锁/方向不对);
- 执行中可被打断,打断后状态能恢复;
- 多人下不会重复触发/永久占用;
- 键鼠、手柄、触屏都能选中;
- 碰撞不会把玩家卡住/穿模;
- 材质音效正确,重复触发不刺耳;
- 弹孔/动效有上限,低配可降级;
- 网络延迟下有合理表现;
- 有调试开关/日志,能定位“为什么没触发”。
9.7 反模式
| 反模式 | 为什么不好 | 替代 |
|---|---|---|
在 Tick 里做射线 | 浪费、难降频 | 定时器/事件 |
用 Cast 判断每类交互物 | 每加一类改一次 | 接口 + 标签 |
只用 bIsOpen | 多人/冷却/损坏会崩 | 显式状态机 |
| 交互体积=物理碰撞 | 手感差、穿模/挡路 | 分离代理 |
| 所有查询用复杂碰撞 | 性能差 | 简单碰撞/交互体积 |
音效写 switch | 难扩展 | 数据表 + 表面类型 |
| 弹孔无限生成 | 越玩越卡 | 上限 + 淡出 |
| 客户端直接改状态 | 作弊/不同步 | 服务器权威 |
| 反馈绑在输入上 | 手还没碰到就响 | Anim Notify |
| 每个交互物一个蓝图 | 维护爆炸 | 组件 + 数据 |
9.8 交互常用接口速查
下面只列搭一个最小闭环最常用的一组接口,按“契约、检测、碰撞、反馈、NPC 与条件”分组,一句话说明用途;具体机制看前面对应章节,不在这里展开。
契约
| 接口 / 类 | 一句话说明 |
|---|---|
IInteractable(自定义 UInterface) | 统一 CanInteract、GetPrompt、BeginInteract、EndInteract;对象只回答“能不能”,不抢输入决策。 |
| Blueprint Interface | 蓝图版契约;门、宝箱、NPC 都实现同一组函数,调用方不用 Cast 到具体类。 |
检测
| 接口 / 类 | 一句话说明 |
|---|---|
LineTraceSingleByChannel | 从准星发一条线,取最近命中,最常用的交互检测。 |
LineTraceMultiByChannel | 取到第一个阻挡命中为止的多个结果,做穿透或多候选。 |
SphereTraceSingleByChannel | 带体积的射线,手柄/触屏更容易选中小物件。 |
OverlapMultiByChannel | 范围重叠检测,适合近身拾取、交互区。 |
FHitResult | 命中结果:Actor、组件、命中点、法线、骨骼名、物理材质。 |
FCollisionQueryParams | 查询参数:忽略自身、是否复杂碰撞、是否返回物理材质。 |
碰撞
| 接口 / 类 | 一句话说明 |
|---|---|
UPrimitiveComponent 碰撞三件套 | SetCollisionEnabled / SetCollisionObjectType / SetCollisionResponseToChannel,控制“能不能被查到、算什么类型、和谁响应”。 |
UBoxComponent / UCapsuleComponent / USphereComponent | 交互体积,通常设 Query Only,只被查询、不挡路。 |
反馈
| 接口 / 类 | 一句话说明 |
|---|---|
UPhysicalMaterial + EPhysicalSurface | 给表面分类,驱动材质音效和命中粒子。 |
UGameplayStatics::PlaySoundAtLocation | 在世界位置播放 3D 音效,配合 Attenuation 使用。 |
UGameplayStatics::SpawnDecalAtLocation | 生成弹孔/血迹贴花,注意朝向、法线偏移和寿命。 |
UNiagaraFunctionLibrary::SpawnSystemAtLocation | 生成命中、开门等一次性 Niagara 动效。 |
UAnimNotify | 在动画的接触帧触发声音、特效或结算,避免“手还没碰到就响”。 |
NPC 与条件
| 接口 / 类 | 一句话说明 |
|---|---|
USmartObjectComponent / USmartObjectSubsystem | NPC 交互位的注册、查询与预约,避免两个玩家/ NPC 抢同一个位置。 |
UMotionWarpingComponent | 把根运动对齐到交互位或目标点,解决“站歪了”的对齐问题。 |
FGameplayTag / FGameplayTagQuery | 表达“需要钥匙”“战斗中禁用”“已被占用”等条件。 |
第 10 章 参考
正文中以 [n] 形式标注的引用编号,对应下列条目。引擎行为以 Epic 官方文档为准;设计取舍部分为项目经验总结,不代替官方规范。
[1] Epic Games, Simple versus Complex Collision in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/simple-versus-complex-collision-in-unreal-engine
[2] Epic Games, Collision in Unreal Engine - Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/collision-in-unreal-engine---overview
[3] Epic Games, Collision Response Reference in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/collision-response-reference-in-unreal-engine
[4] Epic Games, Blueprint Interface in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/blueprint-interface-in-unreal-engine
[5] Epic Games, UKismetSystemLibrary::LineTraceMulti, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Kismet/UKismetSystemLibrary/LineTraceMulti
[6] Epic Games, Characters in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/characters-in-unreal-engine
[7] Epic Games, Physical Materials in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/physical-materials-in-unreal-engine
[8] Epic Games, Artist 11: Add Footstep Sounds to a Character, Epic Games Documentation. https://dev.epicgames.com/documentation/unreal-engine/artist-11-add-footstep-sounds-to-a-character
[9] Epic Games, MetaSounds: The Next Generation Sound Sources in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/metasounds-the-next-generation-sound-sources-in-unreal-engine
[10] Epic Games, USoundAttenuation, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Sound/USoundAttenuation
[11] Epic Games, Decals in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/decals-in-unreal-engine
[12] Epic Games, UDecalComponent, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Components/UDecalComponent
[13] Epic Games, unreal.ProjectileMovementComponent, Epic Games Python API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/python-api/class/ProjectileMovementComponent
[14] Epic Games, Creating Visual Effects in Niagara for Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-visual-effects-in-niagara-for-unreal-engine
[15] Epic Games, Smart Objects in Unreal Engine - Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/smart-objects-in-unreal-engine---overview
[16] Epic Games, Motion Warping in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/motion-warping-in-unreal-engine
[17] Epic Games, Using Gameplay Tags in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/using-gameplay-tags-in-unreal-engine
[18] Epic Games, Overview of State Tree in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/overview-of-state-tree-in-unreal-engine
[19] ArtemChig, UE5-Weapon-System (GitHub repository: projectile, muzzle flash, bullet hole decals, impact particles). https://github.com/ArtemChig/UE5-Weapon-System
[20] Epic Developer Community, Designing an Interaction System in C++: Using Interfaces and TScriptInterface. https://dev.epicgames.com/community/learning/tutorials/58Okd/unreal-engine-designing-an-interaction-system-in-c-using-interfaces-and-tscriptinterface
[21] Epic Developer Community, Unreal Engine 5: Line Trace + Interface Interaction Tutorial (Japanese). https://dev.epicgames.com/community/learning/tutorials/9mL75/unreal-engine-5-line-trace
[22] Epic Games, AActor, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/GameFramework/AActor
[23] Epic Games, UObject, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/UObject/UObject
[24] Epic Games, Game Objects in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/game-objects-in-unreal-engine
[25] Epic Games, UNiagaraComponent, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/Niagara/UNiagaraComponent
[26] Epic Games, Niagara Data Channels Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/niagara-data-channels-overview
说明:部分社区教程页面需要登录或依赖动态渲染,正文只引用其标题与主题,不依赖其具体代码;引擎行为以官方文档为准。示例代码为说明结构而写,落地时需要按项目模块、命名规范和网络模型调整。
交互物设计的终点不是“能按 E”,而是“玩家不用思考就知道这里能做什么、做完之后世界变成了什么样”。把感知、判定、执行、反馈拆开,把碰撞、声音、动效当成一等公民,交互才会从“功能”变成“手感”。