【UE基础】03.蓝图与编辑器工作流

第一次在编辑器里双击打开 BP_PlayerController,看到满屏五颜六色的连线,我的第一反应跟很多 C++ 出身的人一样:这玩意儿能干啥正经事?把它全部重写成 C++ 不就完了。

劝你别这么想。ActionRPG 这个项目,C++ 只负责定义"能力",真正把数据和逻辑装配起来的活,一大半在蓝图里。你要是看不懂蓝图,等于这个项目你只能读懂三分之一。这一篇就是带你跨过这道坎,目标不是让你会画蓝图,而是让你能读懂别人画的蓝图,并且能完成一次最基本的 C++ 和蓝图的来回协作。

先认门:编辑器五件套,各管一摊

在钻进那团连线之前,先花两分钟认认门。UE 编辑器窗口多到吓人,但你天天打交道的就这五个,搞清楚它们各自干嘛,你就不会迷路了。

内容浏览器(Content Browser) ,一般在底部。它就是 Content/ 目录的可视化文件管理器,所有 .uasset.umap 都在这儿。找资产、新建资产、双击打开资产,都从这里开始。它对应的就是你硬盘上 Content\Blueprints\Content\UI\ 这些文件夹,只不过显示的是资产的友好名字和缩略图,而不是文件名。

关卡编辑器(Level Editor) ,就是中间那个能看到 3D 场景的主窗口。它本身又拼着几块子面板:Viewport (视口)是你能拖动相机、摆放物体的 3D 预览;World Outliner (世界大纲,右上角,有的版本就叫 Outliner)列出当前关卡里所有的 Actor,是一棵树------在 Viewport 里点一个箱子,World Outliner 里对应那行会高亮,反过来也一样;左侧通常还停着一个 Place Actors(放置 Actor 面板),从这儿能把灯光、摄像机、基础几何体、触发器这类东西直接拖进场景。World Outliner 和 Place Actors 都是「搭关卡」用的------本课只读蓝图、走一次 C++ 往返,并不摆关卡,所以用不太上,你点开界面看到它们时知道是干嘛的就行。

细节面板(Details) ,通常在右侧。选中任何东西(关卡里的一个 Actor、蓝图里的一个变量、一个数据资产)它就显示这个东西所有可编辑的属性。你 C++ 里写的 UPROPERTY(EditAnywhere),最终就显示在这个面板上让人改。比如 ARPGCharacterBase 里的 CharacterLevel 标了 EditAnywhere,在细节面板里就是一个能填数字的格子。

蓝图编辑器(Blueprint Editor) ,双击任意 BP_ 资产打开的那个独立窗口。后面要讲的 Event Graph 就在这里,还包括 Viewport(看这个蓝图自己的组件长啥样)、My Blueprint 面板(变量和函数列表)、以及编译按钮。改完蓝图记得点左上角的 Compile,不然不生效。

数据资产编辑器 ,其实没有一个单独叫这名字的窗口。当你双击一个数据资产(比如 Content/Items/ 下的物品,那些继承自 URPGItem 的东西)时,打开的就是一个纯属性编辑界面,整个窗口基本就是一个大号的细节面板。因为数据资产没有逻辑,只有数据,所以它没有 Event Graph。这正好呼应了项目"数据驱动"的思路:物品的数值全填在这种资产里。

门认完了,其中最该花时间的是蓝图编辑器------因为这个项目一大半逻辑都在它的 Event Graph 里。下面就钻进那团连线。

蓝图到底是什么

一句话:蓝图就是可视化的、面向设计师的脚本。它编译之后跑在虚拟机上,本质和你写的 C++ 类是一回事,只是你用拖拽连线代替了打字。

打开任意一个 BP_ 资产,你会看到几个标签页,最关键的是 Event Graph (事件图表)。这里就是写逻辑的地方。图里那些方块叫节点(Node),一个节点可能是一次函数调用、一个事件触发、一次分支判断。节点和节点之间靠两种线连起来,分清这两种线,你就读懂了蓝图的一半。

白色的线是执行流(Execution)。它表示"先做这个,再做那个",就是你 C++ 里那种从上往下一行行执行的顺序。每个节点左上角有个朝右的白色三角箭头是入口,右上角的三角是出口。白线串起来的顺序,就是代码的执行顺序。如果一个节点没有白色引脚,说明它是纯计算节点(Pure),不参与执行流,谁用到它的结果它才算一次。

彩色的线是数据流(Data)。蓝色、绿色、红色、黄色各代表一种数据类型,比如布尔是红色、浮点是绿色、对象引用是蓝色、整数是青色。这些线把一个节点算出来的值,喂给另一个节点当输入参数。数据引脚是圆点,实心表示已连接。

这么对应你就清楚了:

  • 节点 = 函数调用 / 语句
  • 白线 = 语句执行顺序(control flow)
  • 彩线 = 变量在表达式之间的传递(data flow)
  • 事件 (Event)= 入口点,比如 Event BeginPlayEvent Tick,等价于回调函数被引擎调用
  • 变量(Variable)= 成员变量,在左侧 My Blueprint 面板里,拖进图里就是读,按住 Alt 拖进去就是写
  • 函数(Function)= 你自己封装的子图,等价于成员函数

举个本项目里的真实例子。BP_Character 这个蓝图继承自 C++ 的 ARPGCharacterBase。C++ 里有这么个函数:

cpp 复制代码
UFUNCTION(BlueprintCallable)
virtual float GetHealth() const;

在蓝图的 Event Graph 里你就能拖出一个 Get Health 节点,它右侧有个绿色输出引脚(float),把这根绿线拉到比如一个判断节点的输入上,就能做"血量低于某值就播放受伤动画"这种逻辑。整个过程不写一行代码。这就是 C++ 定义能力、蓝图装配逻辑的典型样子。

读蓝图的诀窍:顺着白线走主流程,遇到不懂的节点再看它的彩线从哪来、到哪去。别一上来就盯着某根线死磕。

关卡:一个装 Actor 的容器

关卡(Level / Map)你可以直接理解成一个容器,里面装的是这个场景中所有的 Actor:地形、灯光、角色出生点、触发器、敌人刷新框,等等。本项目 Content/Maps/ 下有四个:ActionRPG_MainActionRPG_PActionRPG_Dungeon02_AssetActionRPG_Dungeon02_Lights。打开任意一个,Outliner 里列出来的就是这个关卡装的所有 Actor。

这里有个团队协作的大坑,C++ 程序员尤其要早点知道。

.umap.uasset 全是二进制文件 。你没法用文本编辑器打开,没法 git diff 看改了啥,更没法 git merge 解决冲突。看看本仓库现在的 git 状态就明白了:

bash 复制代码
M Content/Maps/ActionRPG_Dungeon02_Lights.umap

这一行 M(modified)你只知道这张地图被改过,但具体改了哪盏灯、移了哪个 Actor,git 一个字都告诉不了你。两个人同时改同一张地图,提交时必然冲突,而这种冲突 git 没法自动合并,结果就是必有一个人的改动整个丢掉。

所以真实团队里处理二进制资产,靠的不是工具而是约定:要么用文件锁(Perforce 那套 exclusive checkout,谁在改谁锁住,别人改不了),要么就是在群里喊一嗓子"这张图我在动,别人先别碰"。用纯 Git 的小团队基本只能靠后者。改动二进制资产之前先确认没别人在动,这是纪律问题,不是技术问题。

实操:从 C++ 加个函数,到蓝图里把它跑起来

光看不练记不住。下面走一遍完整的 C++ 和蓝图来回。这里说的"来回"其实有两个方向 :一个是蓝图调 C++BlueprintCallable),另一个是C++ 调蓝图BlueprintImplementableEvent)。先把前一个方向走通------在 ARPGCharacterBase 上加一个 BlueprintCallable 函数,编译后到 BP_Character 里调用它、运行验证------再回头补上反方向。

第一步,在 C++ 里加函数声明。 打开 Source/ActionRPG/Public/RPGCharacterBase.h,在那一堆 Get* 函数旁边加一个:

cpp 复制代码
/** 测试用:返回当前血量百分比 0~1 */
UFUNCTION(BlueprintCallable, Category = "Test")
float GetHealthPercent() const;

BlueprintCallable 这个标记是关键,它告诉反射系统"这个函数要暴露给蓝图调用"。没有它,蓝图里根本搜不到这个节点。Category 决定它在蓝图右键菜单里归到哪个分组,纯粹为了好找。

第二步,在 cpp 里实现。 打开 Source/ActionRPG/Private/RPGCharacterBase.cpp,加上实现:

cpp 复制代码
float ARPGCharacterBase::GetHealthPercent() const
{
    const float Max = GetMaxHealth();
    return Max > 0.f ? GetHealth() / Max : 0.f;
}

这里直接复用了项目已有的 GetHealth()GetMaxHealth(),它俩本来就是 BlueprintCallable

第三步,编译。CLAUDE.md 里的命令编译编辑器目标,或者直接在编辑器工具栏点 Compile 按钮(C++ 的热重载)。编译成功后,新函数才会出现在蓝图里。

第四步,到蓝图里连线。 在内容浏览器里找到 Content/Blueprints/BP_Character,双击打开。进 Event Graph,在空白处右键,菜单里搜 GetHealthPercent,就能看到你刚加的节点,把它拖出来。给它接个执行流:从某个事件(比如临时拖一个 Event BeginPlay)拉白线进来;再拖一个 Print String 节点,把 GetHealthPercent 输出的绿线接到 Print String 的 In String 输入上(中间会自动插一个 float 转 string 的节点)。连完点 Compile、Save。

需要注意的是,默认配置是2秒,改的长一些(如下图所示12.0)才能看到日志

第五步,运行验证。 关掉蓝图窗口,回到关卡编辑器,点工具栏的 Play 。游戏一开始,屏幕左上角就会打印出当前血量百分比(1.0)。看到数字,这趟 C++ 到蓝图的往返就闭环了。

整个过程你体会一下分工:函数的逻辑在 C++ 里(你的主场),但什么时候调它、调完拿结果干嘛,是在蓝图里装配的。这就是 UE 正向开发的日常。

延伸:把阶段一那个 GetMagicNumber 也打印出来

还记得阶段一验收清单里加的那个 GetMagicNumber 吗?当时只让你「改一行 C++ → 编译过」,留了一句「运行看到效果,等阶段二三学会连线再说」。现在就来把这半程补上------它和上面 GetHealthPercent 的手顺一模一样,唯一的区别是函数名和返回值。

编译后回到 BP_Character 的 Event Graph,重复上面的第四步:右键搜节点,这次搜 GetMagicNumber(注意它归在 Debug 分组下,跟 GetHealthPercentTest 分组不是一处),拖出来,输出的绿线接到 Print String,执行流从 Event BeginPlay 拉白线进来。Compile、Save,Play。

这回屏幕左上角打印的不是血量百分比,而是固定的 42------因为这个函数不读任何状态,就硬返回一个常量。看到这个 42,阶段一那条「改一行 C++ → 编译 → 运行看到效果」的闭环才算真正合上。

顺带体会一个对比:GetHealthPercent 的返回值会随血量变化(它读了 GetHealth()),而 GetMagicNumber 永远是 42。同样是 BlueprintCallable,一个是「把 C++ 算出来的动态结果递给蓝图」,一个是「给蓝图一个写死的常量」。蓝图侧的连线方式完全不分这两者------它只管拿到一个 float 往下用,至于这个值是算出来的还是写死的,是 C++ 那头的事。这正是「C++ 定义能力、蓝图装配逻辑」分工的一个小注脚。

反方向:让 C++ 来调用蓝图

上面两个例子都是蓝图调 C++ :函数实现写在 C++,蓝图把它当节点拖出来调。但 ActionRPG 里更常见的其实是反过来的方向------C++ 声明一个事件,蓝图去实现它,再由 C++ 在合适的时机触发 。这座桥就是 BlueprintImplementableEvent

不用自己造例子,项目里现成的就有一堆。打开 Source/ActionRPG/Public/RPGCharacterBase.h,能看到这么几个:

cpp 复制代码
UFUNCTION(BlueprintImplementableEvent)
void OnHealthChanged(float DeltaValue, const struct FGameplayTagContainer& EventTags);

UFUNCTION(BlueprintImplementableEvent)
void OnDamaged(float DamageAmount, const FHitResult& HitInfo, const struct FGameplayTagContainer& DamageTags, ARPGCharacterBase* InstigatorCharacter, AActor* DamageCauser);

注意一个反常的地方:这俩函数在 .cpp根本没有实现体 。你去 RPGCharacterBase.cpp 翻,找不到 ARPGCharacterBase::OnHealthChanged 的函数体。这不是漏写------BlueprintImplementableEvent 的意思就是"C++ 只管声明,函数体由蓝图来画",代码生成器会替你生成一个跳进蓝图的桩子。

那 C++ 怎么"调用"一个实现在蓝图里的函数?看 RPGCharacterBase.cpp 里这段:

cpp 复制代码
void ARPGCharacterBase::HandleHealthChanged(float DeltaValue, const struct FGameplayTagContainer& EventTags)
{
    // 技能初始化阶段的血量变动不通知蓝图,避免误触
    if (bAbilitiesInitialized)
    {
        OnHealthChanged(DeltaValue, EventTags);   // 执行流从这里瞬间跳进蓝图
    }
}

血量一发生变化,C++ 这边走到 HandleHealthChanged,它调一下 OnHealthChanged(...),执行流就瞬间跳进 BP_Character 里你为这个事件连的那串节点------比如弹个飘字、刷新血条、播放受伤特效。OnDamaged 同理,由 HandleDamage 触发。

蓝图侧怎么实现它?打开 BP_Character 的 Event Graph,因为 C++ 已经声明了这些事件,左侧 My Blueprint 面板就能找到 On Health Changed,把它拖进图里,它就是一个红色的事件节点 (带白色执行输出),从它的白线往后接你想做的事就行。这和你拖一个 Event BeginPlay 出来本质一样,只不过这个事件的触发者是 C++,不是引擎。

把两座桥摆在一起,往返就完整了:

方向 修饰符 谁写实现 谁来调用 蓝图里长什么样
蓝图调 C++ BlueprintCallable C++ 蓝图 一个可以拖出来调用的函数节点 (如 Get Health Percent
C++ 调蓝图 BlueprintImplementableEvent 蓝图 C++ 一个由 C++ 触发的事件节点 (如 On Health Changed

这座 BlueprintImplementableEvent 桥在阶段二讲 BP_GameModeK2_OnGameOver 时已经露过脸,这里换成角色身上的真实例子再走一遍,是为了让本节的"往返"名副其实------一来(蓝图调 C++)、一回(C++ 调蓝图),才是完整的协作闭环。

给 C++ 程序员的几句真心话

最容易犯的错,是看蓝图不顺眼就想全搬回 C++。我理解这种冲动,蓝图改大了确实难维护,连线乱成一锅意大利面。但你得分清场景。

逻辑骨架、性能敏感的计算、复杂的数据结构,这些放 C++ 没问题,本项目也是这么做的。可那些设计师要反复调的东西,比如某个技能的伤害数值、某个怪的血量、UI 上一个按钮的位置和触发的事件,全塞进 C++ 意味着改一个数字就得重新编译一次,设计师还得来找你。这活儿留在蓝图和数据资产里,人家自己就改了。

UE 正向开发的常态就是混合开发。把蓝图当成"可视化的、面向设计师的那一层 C++",而不是低级玩具。你读得懂蓝图,才能读懂这个项目超过一半的逻辑。真到了蓝图复杂得维护不动那天,再把其中稳定的部分下沉到 C++ 也不迟,这是优化,不是起手式。

验收清单

照着下面这几条自检,全过了这一阶段就算扎实了:

  • 随便打开一个 BP_ 资产(比如 BP_PlayerCharacter),能分清白线(执行流)和彩线(数据流),能顺着白线讲出主流程大概在干嘛。
  • 说得出编辑器五件套各自是干啥的:内容浏览器、关卡编辑器(Viewport + Outliner)、细节面板、蓝图编辑器、数据资产编辑器。
  • 理解关卡是 Actor 的容器,并且清楚 .umap/.uasset 是二进制、不能 diff/merge,知道团队里靠文件锁或口头约定避免冲突。
  • 独立完成一次"C++ 加 BlueprintCallable 函数 → 编译 → 蓝图里连进 Event Graph → Play 验证"的完整往返。
  • 分得清两座桥的方向:BlueprintCallable 是蓝图调 C++(实现在 C++),BlueprintImplementableEvent 是 C++ 调蓝图(实现在蓝图的事件节点里,由 C++ 触发);能在 RPGCharacterBase 里指认出 OnHealthChangedOnDamaged 这类反方向的例子。
  • 想清楚了一件事:哪些逻辑该留蓝图、哪些该进 C++,并且不再有"把蓝图全重写成 C++"的冲动。
相关推荐
VidDown1 小时前
视频帧率技术详解:从 24fps 到 120fps,帧率如何影响你的观看体验?
网络·网络协议·编辑器·音视频·视频编解码·视频
zyplayer-doc16 小时前
企业知识库安全与权限管理完全指南:从加密到审计的六层防护
人工智能·安全·pdf·编辑器·创业创新
古德new16 小时前
鸿蒙PC迁移:Photoflare Qt 图片编辑器鸿蒙PC适配全记录
qt·编辑器·harmonyos
春日见20 小时前
vscode的AI编程插件推荐:
大数据·ide·vscode·算法·机器学习·编辑器·ai编程
夜猫逐梦1 天前
【UE基础】02.基类与蓝图子类的分工
ue·蓝图·ue基础
jieshenai1 天前
VScode sys.path,并使CTRL+左键可访问源码
ide·vscode·编辑器
qq_448011161 天前
VSCode环境搭建
ide·vscode·编辑器
hadage2331 天前
我做了一款能秒开打开 13G 文件的编辑器
编辑器
by————组态1 天前
Ricon组态可视化编辑器 - 所见即所得的工业画布
前端·javascript·物联网·架构·编辑器·组态