UE5 生存建造游戏开发:用 UGameInstanceSubsystem 管理 Actor 与碰撞检测的完整流程

发布时间:2026/10/8 23:22:25
UE5 生存建造游戏开发:用 UGameInstanceSubsystem 管理 Actor 与碰撞检测的完整流程 1. 生存建造玩法里 Actor 满天飞为什么我最后把逻辑全塞进 UGameInstanceSubsystem做生存建造类游戏绕不开一个核心循环玩家点一下建造按钮场景里出现一个半透明的预览 Actor鼠标移动它跟着走位置合法就绿色、不合法就红色左键确认落地。听起来简单但真写起来问题会一个接一个冒出来。我最早的做法是把这套逻辑全写在 PlayerController 里。结果呢采集系统要判断这个位置能不能放箱子交互系统要判断这个门会不会卡住 NPC建造系统自己还要判断预览框有没有重叠。三套代码各写一份 SweepSingleByChannel参数还不一样改一个碰撞通道要翻三个文件。更麻烦的是PlayerController 在关卡切换、PIE 重启时会被重建我存在里面的PreviewBuilding指针直接变野指针编辑器里点停止再点播放崩溃日志能刷一屏。后来我把这套东西抽出来放进一个UGameInstanceSubsystem。这个类的生命周期是游戏实例创建到销毁在 PIE 里点结束按钮它不会被销毁下次播放还是同一个实例。这意味着建造状态、预览 Actor 引用、当前选中的建筑模板全都可以安全地挂在这里不用每次开局重新初始化。这篇就按我实际项目的组织方式把整条链路拆开讲Subsystem 怎么注册、Actor 怎么生成、碰撞通道怎么配、鼠标移动怎么绑定、左键确认后怎么解绑最后附上 PIE 下我踩过的报错清单。适合已经会写基础 C Actor、但被建造预览这套交互绕晕的 UE5 开发者。核心检索词就三个UGameInstanceSubsystem 做全局管理器、SweepSingleByChannel 做碰撞检测、Actor 生命周期跟着建造流程走。先说清楚 Subsystem 的定位。它不是万能的别把采集、背包、任务全塞进去。我的划分标准是只要一个状态需要跨关卡、跨 PlayerController 存活并且被多个系统查询就放 Subsystem。建造预览恰好符合——采集系统要问它这块地能不能放交互系统要问它当前有没有在建造模式UI 要问它现在选的是哪个建筑。这三方都不该直接持有 PlayerController 的指针。UGameInstanceSubsystem的获取方式有三种我在不同场景下用不同的// 方式一从 PlayerController 拿UI 回调里最常用 UBuildSubsystem* Build GetGameInstance()-GetSubsystemUBuildSubsystem(); // 方式二从 World 拿Actor 的 BeginPlay 里用 UWorld* World GetWorld(); UBuildSubsystem* Build World-GetGameInstance()-GetSubsystemUBuildSubsystem(); // 方式三从 GEngine 拿调试、控制台命令里用正式逻辑别用 GEngine-GetCurrentPlayWorld()-GetGameInstance()-GetSubsystemUBuildSubsystem();方式三我只在写调试命令时用过因为它依赖当前 PlayWorld打包后多窗口场景会拿到错的。正式逻辑统一走方式一和方式二。这里有个容易忽略的点Subsystem 的Initialize会在 GameInstance 初始化时调用但那时候 World 可能还没准备好。所以别在Initialize里做GetWorld()相关的操作我一般只在这里初始化纯数据成员比如SpawnLocation FVector::ZeroVector;。真正涉及 World 的逻辑等第一次被调用时再懒加载。还有一个坑Subsystem 里持有AActor*指针时一定要加UPROPERTY()。我一开始图省事写成裸指针AActor* PreviewBuilding;结果 GC 在某次鼠标移动后把预览 Actor 回收了下一帧SetActorLocation直接访问已释放内存。加上UPROPERTY()后GC 会正确追踪这个引用问题消失。这个细节在官方文档里提得不多但实际项目里必踩。2. 前置准备TaoToken 接入与 UBuildSubsystem 骨架搭建在动手写建造逻辑之前我习惯先把开发环境里跟 AI 辅助编码相关的部分配好。写 UE5 的 C 时经常需要让模型帮忙补全碰撞参数、检查 UPROPERTY 宏有没有漏这时候一个稳定的 API 入口能省不少事。TaoToken 的接入方式很简单官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。如果你用的是 Claude Code 做代码补全可以在项目根目录建一个配置文件把 Base URL、Key、Model ID 三件套写全。我实测下来写 UE 的 C 时模型对GENERATED_BODY()、UFUNCTION这些宏的补全准确率还不错但碰撞通道枚举经常写错需要自己核对。{ base_url: https://taotoken.net/api, api_key: sk-你的密钥, model: claude-sonnet-4-20250514 }Key 的获取在控制台的 API Keys 页面模型对话入口可以用来先验证 Key 是否可用。长期做编码和 Agent 任务的话Coding Plan 更划算具体在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 可以看。环境配好后回到 UE 这边。先建 Subsystem 的头文件。我把它放在Source/AFarmSimulation/SystemSystem/目录下跟其他系统类放一起。// BuildSubsystem.h #pragma once #include CoreMinimal.h #include Subsystems/GameInstanceSubsystem.h #include BuildSubsystem.generated.h UCLASS() class AFARMSIMULATION_API UBuildSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase Collection) override; virtual void Deinitialize() override; // 生成预览 Actor UFUNCTION(BlueprintCallable, Category Build System) bool ShowPreview(TSubclassOfAActor BuildingTemplate, const FVector Location, const FRotator Rotation); // 销毁当前预览 UFUNCTION(BlueprintCallable, Category Build System) void DestoryCurrentActor(); // 更新预览位置 UFUNCTION(BlueprintCallable, Category Build System) void ChangePreviewActorPosition(FVector position); // 隐藏碰撞框确认建造后调用 void SetCollisionBoxVisibilityHide(); // 当前位置是否合法 bool GetCurrentIsRight(); UPROPERTY(VisibleAnywhere, Category Build System) FVector SpawnLocation; private: bool CheckBuildLocation(const FVector Location, const FRotator Rotation, const FVector Extents) const; bool BuildSystemSpawnActor(TSubclassOfAActor BuildingTemplate, const FVector Location, const FRotator Rotation, FActorSpawnParameters SpawnParams); void SetCollisionBoxColor(); UPROPERTY() AActor* PreviewBuilding; };注意PreviewBuilding前面的UPROPERTY()这是防 GC 的关键。SpawnLocation用VisibleAnywhere是为了在编辑器细节面板里能直接看到当前记录的位置调试时很方便。Initialize和Deinitialize里我只做最轻量的初始化void UBuildSubsystem::Initialize(FSubsystemCollectionBase Collection) { Super::Initialize(Collection); SpawnLocation FVector::ZeroVector; PreviewBuilding nullptr; } void UBuildSubsystem::Deinitialize() { Super::Deinitialize(); }这里PreviewBuilding nullptr是保险虽然 UPROPERTY 会处理但显式置空能避免 Deinitialize 后还有异步回调访问。接下来是碰撞检测的核心函数CheckBuildLocation。这个函数被SetCollisionBoxColor调用用来判断预览框当前位置有没有跟场景物体重叠。bool UBuildSubsystem::CheckBuildLocation(const FVector Location, const FRotator Rotation, const FVector Extents) const { UWorld* World GetWorld(); if (!World) return false; FCollisionQueryParams CollisionParams; CollisionParams.AddIgnoredActor(PreviewBuilding); // 忽略自身否则永远检测到自己 FHitResult HitResult; bool bHit World-SweepSingleByChannel( HitResult, Location, // 起始位置 Location, // 结束位置相同 原地重叠检测 Rotation.Quaternion(), ECC_WorldDynamic, // 碰撞通道按项目实际调整 FCollisionShape::MakeBox(Extents), CollisionParams ); return bHit; }SweepSingleByChannel的起始和结束位置传同一个值效果就是在当前形状位置做一次重叠检测。如果传不同值就是沿路径扫掠。建造预览用同位置检测就够了因为鼠标移动是逐帧更新的不需要预测路径。AddIgnoredActor(PreviewBuilding)这行必须加。我一开始忘了结果预览框永远检测到自己颜色一直是红的排查了半小时才发现。3. 可复制配置碰撞通道、Actor 组件与鼠标绑定碰撞通道的配置是这套流程里最容易出错的地方。UE 默认的碰撞通道有ECC_Visibility、ECC_WorldStatic、ECC_WorldDynamic、ECC_Pawn等。建造检测该用哪个取决于你的项目设置。我的做法是在DefaultEngine.ini里加一个自定义通道专门给建造检测用[/Script/Engine.CollisionProfile] DefaultChannelResponses(ChannelECC_GameTraceChannel1,DefaultResponseECR_Block,bTraceTypeTrue,bStaticObjectFalse,NameBuildTrace)然后在代码里用ECC_GameTraceChannel1代替ECC_WorldDynamic。这样建造检测跟其他系统的碰撞查询互不干扰。如果你不想改 ini直接用ECC_WorldDynamic也能跑但后期加系统时容易冲突。预览 Actor 的组件结构我用一个UStaticMeshComponent做外观一个UBoxComponent做碰撞框。Box 的大小手动调要跟 Mesh 的包围盒匹配。// 在预览 Actor 的构造函数里 UStaticMeshComponent* MeshComp CreateDefaultSubobjectUStaticMeshComponent(TEXT(MeshComp)); RootComponent MeshComp; UBoxComponent* BoxComp CreateDefaultSubobjectUBoxComponent(TEXT(BoxComp)); BoxComp-SetupAttachment(MeshComp); BoxComp-SetBoxExtent(FVector(100.f, 100.f, 100.f)); // 按实际建筑尺寸调 BoxComp-SetCollisionEnabled(ECollisionEnabled::QueryOnly); BoxComp-SetCollisionResponseToAllChannels(ECR_Overlap); BoxComp-SetGenerateOverlapEvents(true);SetCollisionEnabled(ECollisionEnabled::QueryOnly)表示只参与查询不参与物理模拟。建造预览不需要物理这样性能更好。生成 Actor 的函数bool UBuildSubsystem::BuildSystemSpawnActor(TSubclassOfAActor BuildingTemplate, const FVector Location, const FRotator Rotation, FActorSpawnParameters SpawnParams) { UWorld* World GetWorld(); if (!World) return false; PreviewBuilding World-SpawnActorAActor(BuildingTemplate, Location, Rotation, SpawnParams); if (PreviewBuilding) { TArrayUBoxComponent* BoxComponents; PreviewBuilding-GetComponentsUBoxComponent(BoxComponents); for (auto ite : BoxComponents) { ite-SetHiddenInGame(false); ite-SetVisibility(true, true); ite-SetLineThickness(2.0f); } return true; } return false; }这里SetHiddenInGame(false)和SetVisibility(true, true)是让碰撞框在游戏中可见。注意这两个函数不要放在 Tick 或循环里反复调用。我踩过的坑在鼠标移动回调里每帧调SetHiddenInGame参数没变但引擎内部状态机不响应导致后面想改成true时没效果。正确做法是只在生成时设一次后续要改状态时确保参数真的变了。SpawnParams.SpawnCollisionHandlingOverride ESpawnActorCollisionHandlingMethod::AlwaysSpawn;这行在ShowPreview里设置保证预览 Actor 即使跟场景重叠也能生成否则会被碰撞系统拒绝。鼠标移动的绑定在 PlayerController 里void AAFarmPlayerController::OpenBuildMode() { InputComponent-BindAction(MouseLeftClicked, IE_Pressed, this, AAFarmPlayerController::MouseLeftClicked); InputComponent-BindAxis(BuildMouseXY, this, AAFarmPlayerController::MouseXY); FHitResult HitResult; GetHitResultUnderCursor(ECC_Visibility, false, HitResult); FRotator Ratotor(0, 0, 0); UBuildSubsystem* Build GetGameInstance()-GetSubsystemUBuildSubsystem(); if (Build) { if (Build-ShowPreview(TreeClass, HitResult.Location, Ratotor)) { SpawnLocation HitResult.Location; } } }GetHitResultUnderCursor(ECC_Visibility, false, HitResult)本质是射线检测从摄像机穿过鼠标位置打到场景。ECC_Visibility表示只检测可见物体这样鼠标不会打到隐藏的碰撞体上。鼠标移动回调void AAFarmPlayerController::MouseXY(float XY) { FHitResult HitResult; GetHitResultUnderCursor(ECC_WorldStatic, false, HitResult); UBuildSubsystem* Build GetGameInstance()-GetSubsystemUBuildSubsystem(); FRotator Ratotor(0, 0, 0); if (Build) { Build-ChangePreviewActorPosition(HitResult.Location); } }注意这里用的是ECC_WorldStatic因为地面通常是静态物体。如果你用ECC_Visibility鼠标可能会打到预览 Actor 自己身上导致位置抖动。左键确认void AAFarmPlayerController::MouseLeftClicked() { UBuildSubsystem* Build GetGameInstance()-GetSubsystemUBuildSubsystem(); if (Build-GetCurrentIsRight()) { Build-SetCollisionBoxVisibilityHide(); InputComponent-RemoveActionBinding(MouseLeftClicked, IE_Pressed); InputComponent-RemoveAxisBinding(BuildMouseXY); } }RemoveAxisBinding(BuildMouseXY)是关键。因为预览 Actor 已经落地鼠标移动不该再改变它的位置。如果不解绑鼠标一动建筑就跟着跑玩家会以为建造失败了。4. 验证请求PIE 下跑通建造流程与成功结果配置写完后在 PIE 里验证。步骤我按顺序列一下每一步都有明确的预期结果。第一步启动 PIE按 Play。在 Output Log 里应该看到 Subsystem 初始化没有报错。如果看到LogGameInstance: Warning: Failed to get subsystem说明类没注册成功检查UCLASS()宏和模块的 Build.cs 有没有加对应依赖。第二步点击 UI 上的建造按钮。预期场景里出现一个半透明的建筑预览碰撞框显示为绿色线框。如果预览没出现先看 Output Log 有没有SpawnActor failed通常是SpawnCollisionHandlingOverride没设成AlwaysSpawn。第三步移动鼠标。预期预览 Actor 跟着鼠标在地面上滑动碰撞框颜色实时变化——位置合法绿色跟其他建筑重叠变红色。如果颜色不变检查SetCollisionBoxColor有没有被调用以及CheckBuildLocation的返回值。第四步把预览移到另一个建筑上面。预期碰撞框变红。如果还是绿色说明AddIgnoredActor没生效或者碰撞通道配错了。第五步在红色状态下点左键。预期没有任何反应预览还在。这是GetCurrentIsRight()返回 false 的保护逻辑。第六步在绿色状态下点左键。预期碰撞框消失预览 Actor 变成实体建筑鼠标移动不再影响它。Output Log 里如果有RemoveAxisBinding的调试信息说明解绑成功。我实测下来整个流程从点击到落地在普通台式机上延迟低于一帧体感是即时的。碰撞检测的开销主要在SweepSingleByChannel如果场景里建筑很多可以考虑加空间分区但一般生存建造游戏几十个建筑不需要。验证时可以用控制台命令show Collision打开碰撞体显示能直观看到 Box 的范围跟 Mesh 是否匹配。如果 Box 比 Mesh 大很多建造时会觉得明明没碰到却变红这时候调SetBoxExtent的参数。还有一个验证技巧在CheckBuildLocation里加DrawDebugBox(GetWorld(), Location, Extents, FColor::Blue, false, 0.1f);PIE 里能看到检测框的实际位置。调试完记得注释掉正式打包别带。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错对照写这套逻辑时我遇到的报错分两类一类是 UE 引擎本身的一类是 AI 辅助编码工具接入时的。分开说。UE 侧报错Access violation - code c0000005在SetActorLocation处崩溃。原因PreviewBuilding被 GC 回收了。排查检查PreviewBuilding有没有加UPROPERTY()。我一开始就是漏了这个加上后不再崩。PreviewBuilding为 nullptr 但代码继续执行。原因ShowPreview返回 true 但 Actor 没生成成功。排查在BuildSystemSpawnActor里加UE_LOG确认SpawnActor的返回值。常见原因是BuildingTemplate传了空指针或者蓝图类没编译。碰撞框颜色不更新。原因SetCollisionBoxColor只在ShowPreview和ChangePreviewActorPosition里调用如果鼠标没移动颜色不会刷新。排查确认MouseXY有没有绑定成功可以在函数开头加UE_LOG(LogTemp, Warning, TEXT(MouseXY called));。SetHiddenInGame调用后没反应。原因前面说的参数没变时引擎不响应。排查确保不在循环里反复调用只在状态真正改变时调一次。AI 工具接入侧报错401 Unauthorized。原因API Key 错了或过期。排查去控制台的 API Keys 页面重新生成确认 Key 没有多余空格。模型对话入口可以先测一下 Key 是否可用。local proxy failed。原因本地网络配置问题请求没发出去。排查检查 Base URL 是不是https://taotoken.net/api注意不要带 UTM 参数。如果公司网络有防火墙确认出口规则。reading choices报错。原因返回的 JSON 结构跟预期不符通常是模型 ID 写错了。排查确认 Model ID 跟平台文档一致别用已下线的模型名。OAuth相关报错。原因认证流程没走完或者 token 过期。排查重新走一遍授权流程确认回调地址配置正确。这三件套Base URL、Key、Model ID在任何 AI 编码工具里都要写全。我用 Claude Code 时配置文件里少写一个 Model ID就会报reading choices补上就好。排查顺序建议先看 Output Log 的最后一条 Error再往上找第一条 Warning。UE 的报错经常是连锁的第一条才是根因。6. 建造系统跑通后下一步该往哪走这套 Subsystem 架构跑通后扩展方向其实很清晰。采集系统可以直接调UBuildSubsystem::GetCurrentIsRight()来判断玩家当前选的位置能不能放采集点不用自己再写一套碰撞检测。交互系统可以监听 Subsystem 的状态变化在建造模式下禁用其他交互。我下一步准备做的是建筑旋转。现在预览 Actor 的 Rotation 是写死的FRotator(0,0,0)加个鼠标滚轮绑定每次转 15 度CheckBuildLocation的 Rotation 参数跟着变就行。碰撞框的SetBoxExtent也要跟着旋转后的包围盒调整这块稍微麻烦点但逻辑是通的。如果你在接入 AI 辅助编码时遇到 Key 或模型配置问题API Keys 页面和接入文档能解决大部分。验证模型是否可用走模型对话入口长期做编码任务看 Coding Plan。地址都在前面提过按需取用。最后说个实用技巧Subsystem 里的SpawnLocation我建议一直保留不要确认建造后就清空。因为玩家可能想连续放多个建筑保留上一个位置可以做吸附到网格或者连续建造的功能。我现在的项目里按住 Shift 点左键就是连续建造位置自动偏移一个建筑宽度体验比一个个点好很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询