UE 交互物设计:从射线、碰撞到状态流转与反馈

把交互物拆成感知、判定、执行、反馈四层:六种交互形态、状态机、射线瞄准、碰撞代理(碰撞箱/胶囊体/不规则形状)、材质音效、射弹弹孔与动效实体,附 UE 实现映射、性能预算与排错清单。

交互物大概是关卡里最容易被低估的一类东西。玩家看到一扇门,走过去,按一下,门开了——这条链路在 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 章 一套统一的交互契约

一句话:交互物只回答“此刻这个请求是否合法”,不替玩家做输入决策,也不自己决定网络权限。

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;
    }
}

门这类移动交互物,设计时要提前回答几个问题:

  1. 开合方向:单开、双开、推拉、滑动?玩家从哪一侧推?
  2. 玩家站位:门板扫过玩家时,是停下、反向、推开玩家,还是直接穿过?最常见的是“检测到玩家就暂停并反向”,但这会让玩家反复卡门;更好的做法是门板碰撞设为“可推移玩家”,或者干脆让门板不阻挡玩家、只阻挡子弹。
  3. 碰撞与导航:门打开后,门板的碰撞要跟着走;如果门会改变 AI 路径,需要 UNavModifierComponent 或动态导航。
  4. 网络:服务器决定开关,客户端只做表现;门的 bReplicateMovement 打开,或者用 RepNotify 同步状态和目标角度。
  5. 声音:开门声、门轴声、锁舌声、关闭撞击声,最好分层;只放一个“门开”音效会显得很薄。

电梯/移动平台还有一个额外问题:站在上面的玩家要跟着走。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 静态交互

静态交互物的特点是:位置不变、状态不多、反馈要重。拉杆、按钮、终端、宝箱、可阅读的告示牌,都属于这一类。

设计上最重要的三条:

  1. 一次性还是可重复:宝箱通常一次性;拉杆可能可重复;按钮可能带冷却。这个差异必须写进状态机,不能靠蓝图里的 DoOnce 节点糊弄。
  2. 生效时机:交互的“实际效果”不要绑在输入按下那一帧,而要绑在动画通知(Anim Notify)或时间轴的关键帧上。手还没碰到拉杆,门就开了,玩家会觉得轻飘;手穿模了门还没开,玩家会觉得延迟。
  3. 失败反馈:上锁的宝箱、没权限的终端、已经拉过的拉杆,都要有明确的“为什么不能用”。最差的做法是按键没反应。

静态交互物通常不需要复杂碰撞:一个盒体交互体积 + 一个简单碰撞即可。真正需要花时间的是动画、声音、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 动效实体

动效实体是表现层的“临时对象”:命中粒子、弹孔、火花、烟雾、音效体、镜头震动。它有三个特征:

  1. 有生命周期:生成、播放、消失;
  2. 不参与交互判定:玩家不能“按 E 使用”一团火花;
  3. 可池化:同一类实体大量重复出现,必须复用。

它和动效交互的区别是:动效交互是“交互物的一种属性”,动效实体是“一次表现的单位”。一扇门被打开时,门本身是移动交互物,开门时扬起的灰尘是动效实体。

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 同步”都是这个意思。

同步模型

  1. 服务器持有权威数据(UObject/Manager):交互状态、冷却、归属、解锁条件都在服务器。
  2. 同步层只发必要字段:InteractionId、状态、参数、资源引用/版本;不是整个对象图。
  3. 客户端收到后创建/更新本地 UObject;Assembler 按数据决定装什么。
  4. 客户端输入走宿主 RPC 回服务器;服务器校验后改数据,再同步结果。
  5. 晚加入/重连:先发全量快照,再发 delta;销毁/卸载要显式发 removal。
  6. 数据带版本号,资源引用用 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;
};

客户端怎么按需组装

  1. 收到数据/快照后,按 InteractionId 找宿主/插槽;找不到就占位或跳过。
  2. 按 AssemblyFlags 只装需要的部分:只装 Mesh、只挂音频、只挂 Niagara,或者只保留数据不装表现。
  3. 需要碰撞时:Attach 到宿主复用碰撞,或新建 UStaticMeshComponent 并设置 QueryOnly/响应。
  4. 绑定命中路由:HitComponent、BoneName 或插槽名 → UObject。
  5. 绑定时装/表现:音频、Niagara、动画;状态变化只更新参数。
  6. 对象移出兴趣范围/卸载:解绑、回收组件;数据保留或释放。

和 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 做交互物”通常有两种含义:

  1. Niagara 只是外观,交互靠代理:隐藏的 Actor/Box/Sphere 负责碰撞和选中,Niagara 负责“看起来像一团火/光/虫群”。这是最常用、最稳的做法。
  2. 真的想让粒子参与交互:需要 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 对比

维度ActorUObject + 组装Niagara
世界坐标 / Transform有无(宿主提供)组件有,粒子无
射线 / 碰撞命中原生借用宿主 / 组装组件默认无,需代理
关卡摆放 / 流送支持不支持(数据 + 宿主)随宿主 Actor
网络同步每个 Actor 通道 + 完整复制配置宿主/Manager 批量,只发字段表现层,通常不复制
同步粒度Actor / 组件 / 属性字段级 delta无 game state
客户端对象完整 Actor + 组件按需组装,可能只有数据组件 + 粒子
持久化 / 存档需自己序列化数据天然可存,需稳定 ID不适合
Tick / 更新可选无(宿主驱动)组件 Tick / 模拟
数量级成本高(通道 / 带宽)低到中(数据小,组装有成本)中到高(GPU/粒子)
表现力中(靠组件)取决于组装高
典型用途门、宝箱、NPC、机关大量数据化交互物、背包/对话能量场、光门、虫群、命中特效
主要坑Actor 数量 / 带宽序列化 / 路由 / 晚加入无碰撞 / 无权威 / 被裁剪

2.8.5 怎么选

先问四个问题:

  1. 需要独立物理、移动、复杂 RPC 或完整生命周期吗? 需要 → Actor;只是交互数据和表现 → UObject + 组装。
  2. 需要被射线/碰撞选中吗? 需要 → Actor,或 UObject + 宿主碰撞;纯 UI/数据 → UObject 直接。
  3. 数量级大、同步预算紧、表现可裁剪吗? 是 → UObject 数据 + 按需组装;否 → Actor。
  4. 视觉主体是实体还是效果? 实体 → 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 转换表

从到条件副作用
UnavailableIdle解锁条件满足播放“通电/解封”表现
IdleFocused射线命中且距离/角度合法高亮、显示提示
FocusedIdle射线离开或超时取消高亮
FocusedReadyCanInteract 为真提示从“靠近”变为“按 E”
ReadyExecuting输入按下且服务器批准播放蒙太奇、锁定输入、播放声音
ReadyReserved其他交互者已占用提示“有人正在使用”
ExecutingCooldown执行完成应用结果、播放完成反馈
ExecutingIdle取消/打断/离开范围回滚、播放取消反馈
CooldownIdle冷却结束恢复提示
任意Broken被破坏/禁用停止所有表现、断开交互

3.3 状态图

3.4 三个边界:权限、并发、中断

权限:谁能改状态?

  • 服务器:Reserved、Executing、Cooldown、Broken 的权威;
  • 客户端:Focused、本地提示、本地预测的 Pending;
  • 对象自己:Unavailable → Idle 这类条件转换,可以在服务器或本地判定,但结果要同步。

并发:两个玩家同时按同一个拉杆怎么办?

  • 单机:一个交互者,简单;
  • 多人:服务器先到先得,后到者收到 Reserved,提示“有人正在使用”;
  • AI:Smart Object 预约,避免 NPC 抢同一个座位;
  • 同一玩家:输入去抖 + 状态锁,避免一帧内触发两次。

中断:执行到一半怎么办?

常见中断源:受伤、死亡、移动超出范围、切换武器、进入过场、另一个交互抢占了手部、网络断开。每种中断都要回答:

  1. 结果是否已经应用?如果已经应用,不能回滚(例如宝箱已经打开,不能因为玩家死亡而关上);
  2. 表现是否要收尾?例如门关到一半被打断,是停在中间,还是继续关;
  3. 冷却是否要退?通常不退,防止刷;
  4. 占用是否要释放?必须释放,否则其他人永远用不了。

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 网络:服务器权威,客户端预测

交互的网络模型可以总结成三句话:

  1. 状态变更走服务器:开门、交易、拾取、造成伤害,都由服务器批准。
  2. 焦点和提示走客户端:射线检测、UI 高亮、按钮提示是本地表现,不需要同步。
  3. 表现走多播/不可靠:粒子、声音、贴花是 cosmetic,丢了就丢了,不要为了它们重传。

客户端预测的常见做法:

  • 按下时立刻进入 Pending,播一个“弱反馈”(按钮按下、UI 变化),但不要播“结果”(门已经开了);
  • 服务器批准后,广播“结果”,客户端从 Pending 进入 Executing;
  • 服务器拒绝时,客户端回滚 Pending,提示原因;
  • 不要用“客户端先开门,服务器拒绝再关回去”这种强预测,玩家会看到门抽搐。

3.8 常见状态 bug

现象根因修法
交互物永远停在 Busy中断时没释放占用在 EndInteract 和 OnUnregister 都释放
一次输入触发两次输入没去抖/状态没锁输入层去抖 + Executing 锁
准星已经移开,提示还在焦点没清每帧/定时重算焦点,或离开时显式清理
冷却只在客户端服务器没记冷却冷却时间戳放服务器,客户端用剩余时间显示
门开了又关上客户端预测 + 服务器拒绝改成 Pending 弱反馈
NPC 对话中还能被攻击交互状态没阻挡BlockedTags 加 State.InCombat

第 4 章 射线瞄准交互

一句话:射线只负责“我看见谁”,不负责“我能不能用”;把选中和许可分开,才能同时支持键鼠、手柄、触屏和 VR。

4.1 从屏幕中心到世界

最常见的第三人称/第一人称瞄准交互:

  1. 取玩家视点(GetPlayerViewPoint)或相机位置;
  2. 从视点沿相机前向发射一条射线,长度等于交互距离;
  3. 命中后取 HitActor,检查是否实现 IInteractable;
  4. 再调用 CanInteract 做距离、角度、标签、占用判定;
  5. 通过后设为焦点,显示提示。

为什么不直接从角色胸口发一条射线?因为玩家是用“屏幕准星”瞄准的,从相机发更符合直觉。但相机射线会穿过角色身后的墙,所以还要用“角色到命中点的距离”或“角色到交互物的距离”做二次校验。

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 胶囊体

胶囊体常用于两类东西:

  1. 角色:Character 类默认带 CapsuleComponent,用于移动碰撞;官方文档明确说,为了计算复杂几何,Character 的碰撞组件假设是一个竖直朝向的胶囊。[6]
  2. 交互体积:站在地上的物件、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 穿透与多命中

穿透需要多次查询:

  1. 从起点打到第一个命中;
  2. 记录入口点和法线;
  3. 沿射线继续,找到出口;
  4. 在入口和出口各生成一个弹孔/粒子;
  5. 按表面厚度和材质决定是否继续。

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 数据驱动

建议至少有三张表/资产:

  1. 交互描述表:交互标签 → 动词、距离、角度、按住时长、所需标签、动画;
  2. 表面音效表:表面类型 → 声音、音量、音高抖动、变体;
  3. 命中包表:表面类型/部位 → 贴花、粒子、声音、灯光。

再加上 Gameplay Tags 做条件,基本可以做到“新加一种材质/一种门,不改代码”。

9.3 网络清单

  • 交互状态由服务器权威;
  • 客户端只预测“输入被接收”,不预测“结果已发生”;
  • 占用/预约在服务器释放,断线/死亡/销毁都要释放;
  • 冷却时间戳在服务器,客户端只做显示;
  • 表现走多播/不可靠,不参与结算;
  • 射弹/投掷物的权威结算只有一套;
  • 弹孔/粒子/声音不做可靠同步。

9.4 性能预算

项目建议上限备注
交互射线10–20 Hz不用 Tick
交互物数量/区域按场景,50–200用空间分区/距离剔除
复杂碰撞查询仅静态世界动态物体用简单碰撞
每帧动效实体16–32超出合并
同屏命中特效64–128按距离降级
弹孔128–256/区域淡出最旧
同时交互音效24–32concurrency
交互 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 / USmartObjectSubsystemNPC 交互位的注册、查询与预约,避免两个玩家/ 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”,而是“玩家不用思考就知道这里能做什么、做完之后世界变成了什么样”。把感知、判定、执行、反馈拆开,把碰撞、声音、动效当成一等公民,交互才会从“功能”变成“手感”。

阅读打卡

每次重读都值得留一笔,久了就是一份阅读记忆。

0 累计打卡
0 连续天数
— 最近打卡