1. 控制台命令的本质:一套跑在运行时的调试总线
做 UE4 项目时间长了,你会发现一个规律:凡是能在编辑器里点出来的东西,背后几乎都对应着一条控制台命令。它不是某个插件附带的小工具,而是引擎在运行时给自己留的一整套调试总线,负责把外部的字符串指令翻译成对引擎内部状态的读写。会用它的人和不会用的人,排查同一个 Bug 的时间差可能是十倍。
我把这套东西理解成三个层次:最外面是执行入口(在哪敲),中间是命令解析与分发(谁来处理),最里面是实际生效的变量或函数(改了什么)。很多同行卡在"命令敲了没反应",本质上是没搞清楚这三层里哪一层断掉了。比如在 Shipping 包上敲stat fps没反应,问题不在命令本身,而在于控制台这一层被裁剪掉了。这篇文章我就按这三个层次往下拆,顺带把大家问得最多的两个问题——外接设备映射怎么调、查询和物理模拟到底差在哪——一起讲透。
先给个整体印象:UE4 的控制台命令大致分两类。一类是控制台变量,业内习惯叫 CVar,本质是引擎里注册过的一个有名字、有类型、有默认值的全局变量,r.ScreenPercentage、p.EnableAsyncScene、t.MaxFPS都属于这一类。另一类是执行命令,它对应一次函数调用,比如open切关卡、Slomo改时间膨胀、obj gc强制垃圾回收。这两类的调用方式完全一样,但一个改状态、一个做动作,理解这个区别对后面排查问题很关键。
1.1 从 ShowFlags 和 CVar 聊起
ShowFlag系列是很多人接触 UE4 控制台命令的起点,因为它直观——敲ShowFlag.Collision 1,屏幕上立刻出现绿色的碰撞体线框。但这里有个容易搞混的点:ShowFlag.Collision并不等同于大家常说的Show Collision。前者是直接操作渲染层的显示标记位(ShowFlags 位掩码),后者是引擎提供的一个语法糖式的命令,内部会转发到对应的 ShowFlag 上,同时也可能会做一些额外的处理。
我自己的用法是:ShowFlag.前缀适合做批量、精确的控制,尤其是脚本化的时候,因为它的语法非常规整,ShowFlag.<名字> 0/1,写进配置文件或者启动参数里不会有歧义。而Show前缀适合手敲,因为短、好记,敲起来顺手。两者不是替代关系,是场景不同。
CVar 这边要注意的是作用域和生命周期。CVar 不是永久生效的,它分几个层级:代码里IConsoleManager::Get().RegisterConsoleVariable()注册时给的默认值,是最底层的;然后DefaultEngine.ini里的[SystemSettings]段会覆盖它;再往上,运行时手敲的命令又覆盖配置。所以当你发现"我在 ini 里改了却没生效",十有八九是别的地方又把它改回去了,或者你改的那个 CVar 在运行时被游戏逻辑每帧重设。
注意:不要盲目相信"ini 里写了一定生效"。有些 CVar 带有
ECVF_SetByCode之类的标志位,代码里的设定优先级高于配置文件,这种情况下只能从代码层面改。
再说类型。CVar 支持的有 bool、int、float、字符串,还有一类特殊的"可执行型 CVar",表现形式像变量但实际是命令助手,比如stat本身就是这类。类型不对会静默失败——比如你给一个 bool 型的变量传2,它可能会当true处理,也可能直接拒绝,行为取决于版本和标志位。我踩过一次坑:给某个 float 变量写了个带空格的表达式,结果整个命令被截断,改成了另一个变量的值,排查了半小时才发现是解析问题。
1.2 三种调用入口与各自的适用场景
第一个入口是运行时控制台输入框,默认按反引号键(` 或 ~,取决于键盘布局)唤出。在 PIE(编辑器内运行)和 Development 构建里都可用,最灵活,可以随时改随时看。缺点是没法复现,你的操作不会留下痕迹,第二天想再验一遍得凭记忆。
第二个入口是启动参数,用-ExecCmds="命令1, 命令2"的形式在启动时就执行一批命令。这个特别适合做"预设调试环境",比如跑性能测试时固定好 FPS 上限、固定好分辨率缩放、固定好 LOD 强制等级,保证每次测量的起点一致。我一般会给一个专用的性能测试快捷方式来挂这串参数,避免每次手敲导致变量不一致。
第三个入口是代码里调用,C++ 用GEngine->Exec(GetWorld(), TEXT("命令")),蓝图用Execute Console Command节点。这条路子适合做游戏内的调试面板,或者做自动化测试——把一系列命令按顺序执行,然后把stat的结果抓出来比对,就能做成一套回归检测。
这三个入口走的是同一条分发链路,但权限不同。启动参数在最早阶段执行,那时候很多东西还没初始化,所以有些命令此时执行会无效或者被后续初始化覆盖掉。这点在做"启动就固定画质"的时候经常遇到,后面第 4 章会详细说怎么处理。
1.3 命令解析链路:为什么有的命令在有的地方不响应
引擎处理一条命令字符串的流程大致是:控制台输入框把字符串交给UGameEngine或UEditorEngine的Exec,逐层往下传,最后落到ULocalPlayer::Exec,再转发给当前PlayerController,然后是它关联的 Pawn 和 HUD。
这个链路意味着两件事。第一,必须存在一个有效的 LocalPlayer 和 PlayerController,命令才可能被处理到后半段。在关卡加载过程中、在无缝切换的过渡帧里敲命令,很可能就没反应。第二,UFUNCTION(Exec)标记的自定义函数,只有挂在链路会遍历到的那几个类上才生效。我见过同事把 Exec 函数写在了一个普通的 Actor 上,然后疑惑为什么敲了没反应——它确实不会响应,因为引擎不会遍历世界里所有 Actor 去找这个函数。
理解了链路,排查就有了方向:命令没反应,先确认有没有 LocalPlayer,再确认这个命令要求的上下文是不是满足,最后才怀疑命令拼写。顺序反过来会浪费大量时间。
2. 命令命名体系拆解:前缀就是一张分类地图
UE4 的命令数量在几千条量级,没人能全背下来。但它的命名遵循一套相当一致的约定,记住前缀就相当于记住了索引目录。这套约定不是强制的,所以偶尔会有"漏网之鱼"用错前缀,但九成以上都遵守。
2.1 常见前缀速查与含义
把下面这张表记住,你查命令的效率会有质的变化。
| 前缀 | 归属领域 | 典型命令 | 什么时候用它 |
|---|---|---|---|
r. | 渲染 | r.ScreenPercentage、r.ShadowQuality | 画面表现、帧率、画质分级 |
p. | 物理 | p.EnableAsyncScene、p.VisualizeMovement | 物理开销、角色移动、碰撞调试 |
a. | 动画 | a.URO.Enable、a.URO.ForceAnimRate | 骨骼动画开销、更新频率 |
t. | 引擎/时序 | t.MaxFPS、t.HDR | 帧率上限、引擎级开关 |
fx. | 特效 | 旧版粒子系统相关 | 老项目里还能见到,新项目基本被 Niagara 取代 |
ai. | AI 逻辑 | AI 调试输出 | 行为树、感知系统 |
nav. | 导航 | 导航网格相关 | 寻路异常、导航网格重生成 |
net. | 网络 | 网络驱动配置 | 联机同步、带宽与包处理 |
gc. | 垃圾回收 | gc.CollectGarbageEveryFrame | 内存抖动、卡顿定位 |
slate. | UI 框架 | Slate 渲染调试 | UMG 性能、界面重绘 |
stat | 统计面板 | stat unit、stat rhi | 性能分析的核心入口 |
Show/ShowFlag | 调试可视化 | Show Collision、ShowFlag.Bounds | 看碰撞、包围盒、导航、骨架 |
表格里最容易混淆的是stat和ShowFlag的关系。前者回答"花了多少时间",后者回答"画了什么东西"。做性能排查时这两个要配合用:先用stat找到时间花在哪一阶段,再用对应的ShowFlag看看这一阶段到底在渲染什么。
还有一类命令没有前缀,是"动词型"的,比如open、Pause、Slomo、RestartLevel、Teleport、Fly、Ghost、Walk、Reload、Kill。这类命令的特点是依赖当前上下文,比如Teleport需要一个视图目标,Fly需要一个可控的 Pawn。所以它们只有在合适的场景里才工作,不像 CVar 那样随处可用。
2.2 怎么在不查文档的情况下找到想要的命令
这是我最想分享的部分,因为很多人做 UE4 好几年了还在靠搜索引擎找命令名。
第一招:Help。不带参数执行Help,引擎会尝试打开或生成一份命令列表。带上参数,比如Help stat,会给出这个命令的用法说明。要注意的是这个输出的完整度在各版本间不一致,有时候只有一行描述,有时候什么都没有。
第二招:DumpConsoleCommands。这是真正的大杀器。执行后引擎会把当前注册的所有控制台命令导出到一个文本文件,落在工程的Saved目录下。有了这个文件,你就能用本地搜索找关键词了——比在网页上翻文档快得多,而且导出的内容是你当前这个版本、当前这些插件实际可用的命令,不会有版本不匹配的问题。
我的习惯是每建一个新项目,先跑一次DumpConsoleCommands,把结果存进项目的Docs目录,起个带引擎版本号的名字。后面做画质分级或者性能测试时直接在里面搜,效率极高。
第三招:查源码里的注册点。如果某个变量你想知道它的确切含义、默认值和取值范围,最可靠的办法是在引擎源码里搜RegisterConsoleVariable和变量名。CVar 的注册代码通常紧挨着使用它的逻辑,注释也往往比外部文档详细。特别是那些名字含糊的变量,看一眼使用位置就明白了。
第四招:关键字联想。知道前缀之后,可以大胆猜。比如你想控制阴影距离,r.Shadow试一下;想调纹理流送池大小,r.Streaming试一下。多数情况下引擎会有自动补全提示(新版编辑器支持),实在没有就靠猜加筛选。
3. 高频命令分组实操清单
下面这几组命令是我日常用得最多的,按用途分组,每条都说明它解决什么问题、什么时候用、有什么坑。
3.1 诊断可视化:Show、ShowFlag、ShowDebug 三兄弟
这三者的定位完全不同,混用会很痛苦。
Show是最老的接口,语法是Show <名字>,一次只能开关一个。常用的大致有这几个:Show Collision显示碰撞体线框,Show Bounds显示包围盒,Show Navigation显示导航网格,Show Splines显示样条,还有一个特殊的Show Bounds能顺便看到 Actor 的 Tick 状态。
ShowFlag是新接口,语法是ShowFlag.<名字> 0/1,名字用的是显示标记的枚举名。它的优势是可以精细控制——ShowFlag.StaticMeshes 0会把所有静态网格关掉,ShowFlag.SkeletalMeshes 0关掉骨骼网格,ShowFlag.Particles 0关掉粒子。做渲染排查时,逐个关掉这些显示类别,能快速定位到"是哪一类物体在拖后腿"。
ShowDebug是完全不同的一类的,它负责在屏幕上叠加一块文本调试面板。不带参数执行ShowDebug会列出所有可用的调试类别;ShowDebug AI显示 AI 状态,ShowDebug Animation显示动画状态,ShowDebug Camera显示相机参数,ShowDebug Input显示输入状态。关闭用ShowDebug None。
提示:
ShowDebug面板的内容是按帧刷新的,如果某个字段一直是空的,通常是这个类别的 Debug 数据没有被对应的系统写入,而不是显示有问题。检查一下相关系统是否在那个上下文里被激活。
3.2 性能统计:stat 家族与 ProfileGPU、stat startfile
stat家族是性能排查的主力,它把引擎内部的计时器数据实时贴在屏幕上。最核心的几条:
stat fps只显示帧率,最轻量,适合长时间挂着观察波动。stat unit显示四个数字——Frame(整帧时间)、Game(游戏线程)、Draw(渲染线程)、GPU(显卡),这是判断瓶颈在哪一端的第一条命令,没有之一。如果 Game 明显高于其他,去查游戏逻辑;如果 Draw 高,去查场景里的绘制调用;如果 GPU 高,去查像素开销。
stat unitgraph把stat unit的数值画成曲线图,做性能回归的时候比看数字直观得多。它还有stat unitmax和stat unitavg两个变体,分别显示峰值和平均值,做长时间采样特别有用——因为瞬时数值会骗人,平均值和峰值才能反映真实体验。
stat scenerendering给出场景渲染的整体开销,包括各种 Pass 的时间分布和可见物体数量。stat rhi关注渲染硬件接口层,能看到 DrawPrimitive 调用次数和显存相关数据。stat game拆解游戏线程的开销,stat physics看物理,stat anim看动画,stat particles看粒子,stat memory看内存,stat slate看 UI。
这些全开会让屏幕很挤,而且统计本身也有开销,测量结果会失真。我的做法是一次只开一两个相关的,测完立刻Stat None全关。
ProfileGPU是一条容易被忽略但非常好用的命令。执行后它会把 GPU 各阶段的耗时输出到日志里,粒度比stat gpu更细。缺点是会打断当前帧,有轻微卡顿,但抓一次性数据时很合适。
做长时间性能分析就得上stat startfile和stat stopfile。这对命令会把这段时间内的详细统计写入一个文件,然后用 Session Frontend 里的 Profiler 打开,能看到每个函数、每个系统的耗时排序。这是做深度优化的标准流程。要注意的是文件会比较大,采样时间别拉太长,一般抓个十几秒就够定位问题了。
实操心得:
stat startfile期间不要做长时间的无操作停留,否则会把大量空转数据也记进去,分析时反而干扰。最好让角色在典型场景里跑一圈,覆盖主要的玩法路径。
3.3 渲染画质调优:r. 家族里最该记住的十几条
r.家族成员极多,我按用途挑出最实用的。
分辨率相关:r.ScreenPercentage控制渲染分辨率缩放,这是做画质档位的核心参数。默认 100,降到 70 能明显提升帧率,代价是画面模糊。注意它影响的是渲染分辨率,UI 通常还是按原生分辨率绘制(取决于设置)。r.ResolutionQuality在不同版本里语义有细微差别,用之前最好确认一下当前版本的说明。
抗锯齿:r.PostProcessAAQuality是主开关,取值 0 到 6,0 是关闭,1 大致对应快速近似抗锯齿,2 以上是时间性抗锯齿的不同质量档。做低端机档位时把它降到 1 或者 0,收益很直接。r.DefaultFeature.AntiAliasing是项目默认值的开关,影响的是新建场景的默认行为。
后处理特效的开关是一组r.DefaultFeature.*,比如r.DefaultFeature.MotionBlur、r.DefaultFeature.Bloom、r.DefaultFeature.AutoExposure。这些关掉能省一笔开销,而且关掉动态曝光后画面亮度会变得稳定可预测,做美术验收时反而更省事。
阴影:r.ShadowQuality是质量总开关,r.Shadow.DistanceScale控制阴影距离缩放(直接影响阴影渲染的覆盖范围,是性价比很高的一个参数),r.Shadow.MaxCSMResolution控制级联阴影贴图的分辨率。移动端项目里这几个参数是必调的。
LOD 相关:r.ForceLOD强制所有物体用某个 LOD 等级,设为 -1 恢复自动。做模型检查时常用r.ForceLOD 0强制最高精度,确认模型的近景效果。r.SkeletalMeshLODBias单独控制骨骼网格的 LOD 偏移。
纹理流送:r.TextureStreaming 0关闭流送,所有纹理全量加载,用来判断"卡顿是不是流送引起的"。注意关掉之后显存占用会飙升,测试完记得开回来。r.Streaming.PoolSize控制流送池的大小,单位是 MB,移动端和低配机上这个值调小是常规操作,但太小会导致纹理频繁抖进抖出,反而更卡。
光源:r.VolumetricFog 0关体积雾,r.AmbientOcclusionLevels控制环境光遮蔽的层级数。
注意:
r.变量里有很多是"只在初始化时读取一次"的,运行时改不会立刻生效。判断方法很简单——改完看画面有没有变化。没变化的话,可能需要重启关卡甚至重启进程。这类变量一般和着色器编译或渲染资源分配相关。
3.4 物理与查询:p. 家族,以及"查询"和"物理模拟"到底差在哪
这一节回答一个被问得特别多的问题:UE4 里"查询"和"物理模拟"到底有什么区别,控制台命令上怎么体现。
先说概念。物理模拟是刚体在力、重力、约束作用下,由求解器计算位置和旋转的变化过程,它会真正改变物体的 Transform,并且产生碰撞响应。查询(Query)是向物理世界提问:"从这个点沿这个方向射一条线,会撞到什么"、"这个盒子和哪些物体重叠了"、"这个胶囊体沿这条路径移动会不会受阻"——它只读,不写,不会推动任何物体。
一个生活化的类比:物理模拟像是把一堆球倒进箱子里,球会互相挤压、滚动、最终静止;查询像是拿一根棍子伸进箱子里探一探,你能知道碰到了哪个球,但球不会因为你的棍子而动。
控制台层面,show Collision是两者共同的观察窗口,它会把物理世界的几何表示画出来。但要注意它画的是碰撞几何,也就是物理世界真正在用的形状,而不是你在建模软件里看到的渲染网格。很多"看着明明碰到了却没触发"的问题,根源就是碰撞几何和渲染网格不一致——show Collision一看就明白了。
p.VisualizeMovement 1是排查角色移动问题的利器,它会把胶囊体的 Sweep 轨迹、地面检测射线、落脚点都画出来。角色在斜坡上抖动、卡在台阶上、莫名其妙掉下去,这类问题用它基本都能定位。它背后用的就是查询——每帧若干次向下的射线检测加一次水平方向的扫掠。
p.ShowInitialOverlaps用来高亮那些在生成时就和其他物体重叠的组件。这个问题会引发一堆奇怪的现象,比如角色被卡住、物理物体突然弹飞,因为物理引擎在初始化时就在处理一个本不该存在的穿模状态。打开这个命令,出问题的物体会被醒目地标出来。
p.EnableAsyncScene控制异步物理场景。这个功能把一部分物理计算放到单独的线程,能减轻主线程压力,但它有明确的使用限制——异步场景里的物体不会和其他物体碰撞。用它之前一定要确认目标物体的交互需求,用错了会出现"物体穿过地面"这种诡异现象。
至于引擎版本较新的 UE4,物理后端逐步引入了新的实现,相关参数挂在p.Chaos.前缀下。具体哪些可用、行为如何,取决于你锁定的引擎小版本,用之前建议先在本地验证一遍,不要直接照搬网上的参数。
我踩过的坑:曾经为了提帧把异步物理打开,结果关卡里几个需要堆叠的箱子全部穿模叠在了一起。异步场景适合那些不需要交互的纯装饰性物理物体,凡是参与玩法交互的,老老实实留在主场景里。
3.5 输入与外接设备映射的调试命令
外接设备映射是实际项目里很容易出问题的一块,尤其是接手柄、方向盘、飞行摇杆这类设备的时候。症状通常是:编辑器里模拟输入一切正常,接上真设备后要么没反应,要么轴方向反了,要么数值范围完全不对。
ShowDebug Input是第一步要开的。它会把当前所有输入动作的状态、轴的数值实时显示出来。设备接没接上、按键有没有映射对、轴的符号是不是反的,看一眼就知道。我一般会这样排查:先确认按键按下去时有没有任何数值变化,有变化说明设备识别和映射链路是通的,问题只在方向或范围;完全没变化就往前推,去看设备的识别层。
关于轴的范围,有个特别容易踩的坑:编辑器里配置的轴映射,默认会有一个死区设置和缩放系数。外接设备尤其是模拟摇杆类,原始输出范围往往是 -1 到 1 之外的区间,或者存在零点漂移。如果死区设得太大,轻微推杆就没有响应;设得太小,松手后数值会一直抖动。这个只能在真设备上反复试,没有通用值。
设备识别的干扰也值得说一下。同一台机器上如果同时接了多个同类设备,索引可能会串。这时候可以逐个拔插来确认,或者用调试面板看设备索引和绑定关系是否对应。多设备场景下,建议在游戏设置里提供一个明确的设备选择入口,而不是依赖自动识别——用户体验会好很多。
另外,输入相关的调试还可以配合慢放来观察。用Slomo 0.2把时间放慢,输入响应的过程会被拉长,能看清一次按键触发了哪些动作、轴的数值是怎么变化的。排查组合键、长按判定这类逻辑时特别好用。用完记得Slomo 1恢复。
3.6 内存与资产:obj、MemReport、gc 组合拳
内存问题排查有一套固定的命令组合,我把它叫"三板斧"。
第一板斧是stat memory,先看整体。它给出的是各子系统的内存占用概览,能快速判断大头在哪——是纹理、是网格、还是蓝图或音频。
第二板斧是MemReport -full,它会把详细的内存报告输出到日志。这个报告非常全面,包括按类别统计的资产数量、大小,还有各种内存池的状态。我第一次看会觉得信息过载,但它确实是定位内存问题时信息最全的入口。常用的是MemReport -full,不加参数会简单一些。
第三板斧是obj系列,用来做资产级别的精确定位。obj list列出所有已加载的 UObject,输出的是类名和数量统计。更进一步可以用类名过滤,比如只看纹理,能发现"哪一类资产加载得异常多"。obj refs可以查某个对象的引用链,用来回答"这个东西为什么还没被释放"——这是内存泄漏排查的关键命令。obj gc强制触发一次垃圾回收,用来验证"这些对象是不是只是等着被回收"。
关于垃圾回收,还有一条gc.CollectGarbageEveryFrame 1。打开它之后每帧都会做一次 GC,虽然性能会明显下降,但能瞬间判断出"卡顿是不是 GC 引起的"。如果打开后卡顿消失,说明原本的卡顿是 GC 峰值导致的;如果卡顿照旧,那问题在别处。这个方法简单粗暴但极其有效。
提示:
MemReport的输出默认进日志,需要把日志窗口打开或者去看Saved/Logs目录下的文件。用-log启动参数可以直接把日志窗口拉出来,省去来回切窗口的麻烦。
4. 把命令固化进工程:ini、代码、打包三件事
命令敲得再熟,也只能解决你自己的问题。要让团队里所有人都能用,或者让某个设置在所有机器上保持一致,就得把它固化下来。
4.1 DefaultEngine.ini 里固化 [SystemSettings]
最常用的方式是在工程的DefaultEngine.ini里加一段[SystemSettings],把要固定的 CVar 写进去。引擎启动时会读取这个段并应用里面的设定。
[SystemSettings] r.ScreenPercentage=100 r.Shadow.DistanceScale=0.8 r.Streaming.PoolSize=1500 t.MaxFPS=60 p.EnableAsyncScene=0这段配置的好处是所有开发者和所有构建都会带上,不会出现"我这台机器上帧率正常、你那台就掉帧"的情况。做画质档位时,我会准备几份不同的配置片段,打包脚本根据目标平台选择性地拼接。
但要注意优先级问题。前面说过,CVar 有三层来源,[SystemSettings]属于中间层。如果某个变量在代码里被以更高优先级设置,ini 里的值会被覆盖。判断方法是:启动后手动敲一遍同名命令,看数值有没有变化。如果变了,说明之前的设定没生效,得从代码层面找原因。
还有一个常见的疑惑是[SystemSettings]和[ConsoleVariables]的区别。前者是引擎级的系统设定段,覆盖面更广,用法也更标准;后者在部分场景下也有效但语义不太统一。我的建议是统一用[SystemSettings],除非你明确知道某个变量必须走另一个段。
4.2 C++ 和蓝图里执行命令
做游戏内调试面板是很多项目的刚需,尤其是在做主机或者不方便开键盘的平台。
C++ 里执行命令的核心是GEngine->Exec:
if (GEngine && GetWorld()) { GEngine->Exec(GetWorld(), TEXT("stat unit")); }这里有个细节值得说:第一个参数传 World 是有讲究的,有些命令需要世界上下文才能找到目标对象。传nullptr在部分命令上能工作,但另一些会静默失败。养成传 World 的习惯能省掉很多迷惑行为。
蓝图里对应的是Execute Console Command节点。它需要一个 World Context 来确认命令执行的上下文,所以通常挂在 PlayerController 或者有世界引用的蓝图里。这两个路径最终走的是同一套分发逻辑,行为一致。
做调试面板时,我建议把命令做成配置驱动的,而不是硬编码。用一张 DataTable 存"按钮标题 → 命令字符串",面板按表生成按钮。这样调整命令不用改代码,重新加载配置就行,策划和测试也能自己加。这个做法在多平台项目里特别省事。
另外要提醒的是,命令的返回值不要指望。多数命令返回的是布尔值表示"是否被处理",具体执行结果要从屏幕显示或者日志里看。所以做自动化测试时,通常是执行命令后等几帧,然后去读日志或者截图比对,而不是读返回值。
4.3 自定义 Exec 命令与 Shipping 构建的限制
当内置命令不够用时,可以自己注册。C++ 里给成员函数加UFUNCTION(Exec)标记,再把这个类放在引擎会自动遍历的那条链路上(PlayerController、Pawn、HUD、GameMode、GameInstance 这类),函数名就会变成一条控制台命令。
UFUNCTION(Exec) void SetQualityPreset(int32 Level);这样在控制台里敲SetQualityPreset 2就会调用到你的函数。这个能力在做工具链时非常好用——你可以把一堆调试开关打包成一条语义清晰的命令,团队里谁都能用,不需要知道背后的 CVar 名字。
关于 Cheat 类命令,它依赖于当前的作弊开关状态。在编辑器内运行(PIE)时默认是允许的,但在正式构建里通常被关掉。所以凡是给策划和测试用的调试命令,建议不要做成 Cheat 类,而是在 Development 构建里直接可用。
最后说 Shipping 构建。Shipping 是发行用的最高优化等级,为了包体和安全性,引擎会裁剪掉大量调试能力,包括控制台输入框和一部分命令。所以你会遇到"Development 包里能用的命令,Shipping 包里敲不出来"的情况。这是设计如此,不是 Bug。
那 Shipping 下怎么做调试?我的做法有三条路:一是保留一个 Development 构建专门给测试团队用来抓问题;二是在 Shipping 里预留一套轻量的自定义调试入口,通过预设的按键组合触发有限的功能,而不是暴露完整控制台;三是把常用的诊断信息做成常驻的性能监控 UI,只在特定条件下显示。第三条路对线上问题定位帮助最大,因为用户遇到问题时你能直接让他们截个图。
5. 一次完整的排查实操:从 45 帧到 60 帧
讲理论不如讲一次真实的过程。我挑一个印象比较深的案例,整个排查就是靠控制台命令完成的。
5.1 现象与第一轮定位
项目是一个中等规模的开放区域场景,在目标配置的机器上跑出 45 帧左右的平均值,目标是稳定 60。场景里没有明显的巨型模型或者超高清贴图,美术资源看起来是合理的。
第一步,stat unit。结果是 Frame 约 22ms,Game 约 8ms,Draw 约 20ms,GPU 约 21ms。这里就能读出信息了:Game 线程不是瓶颈,Draw 和 GPU 都接近 20ms,说明渲染侧压力大。同时 Draw 高而 GPU 略高于 Draw,说明既有提交开销也有像素开销,两头都有。
第二步,stat scenerendering。重点看两个数:可见物体数量和各个渲染 Pass 的耗时分布。这里发现可见静态网格数量远超预期,而且阴影相关的 Pass 占了相当大一块。
第三步,ShowFlag.Shadow 0把阴影全关,再看stat unit。GPU 时间从 21ms 掉到 13ms 左右。确认阴影是 GPU 侧的大头。
5.2 收窄到渲染线程的过程
接下来要解释 Draw 为什么高。Draw 高通常意味着在 CPU 侧提交了太多绘制调用。
先用stat rhi看 DrawPrimitive 的调用次数,发现明显偏高。然后用ShowFlag.StaticMeshes 0关掉所有静态网格,Draw 时间立刻降到 6ms 左右,确认是静态网格的提交量问题。
进一步定位:用ShowFlag.Bounds 1和Show Collision辅助观察,发现大量小物件被渲染,而且很多是在远处根本看不清的细节。同时ShowFlag.Navigation 1时也发现场景里存在一些不该渲染的辅助几何体。
这里有个关键的判断:不是单个模型太重,而是数量太多导致提交开销累积。这类问题用合并网格、做 LOD、调整剔除距离都能解决,但优先级要按收益排。
5.3 结论与验证
最终的调整方向有三条:一是优化阴影配置,把r.Shadow.DistanceScale从默认值降到 0.8,同时降低级联阴影的分辨率,并关掉不需要投影阴影的物体;二是对场景里的静态网格做合并,减少绘制调用;三是给远处的细节物件配置更激进的 LOD 和剔除距离。
改完后用t.MaxFPS 60锁定上限,再跑一遍完整路径,stat unit稳定在 Frame 约 16ms,Game、Draw、GPU 三项均衡。为了确认没有引入回归,我用stat unitgraph挂了十分钟,曲线平稳,没有周期性的尖峰——说明也没有引入 GC 或者流送引起的抖动。
最后把这次的参数调整固化进DefaultEngine.ini的[SystemSettings],并把整个排查过程用到的命令整理成一张清单放进项目文档。下次遇到同类问题,新人照着清单走一遍就行,不用重新摸索。
这个案例里真正花时间的不是敲命令,而是判断哪个数值代表什么问题。命令只是把数据摆出来,解读数据的能力才是核心。我的建议是每次排查完都记录下来,慢慢会形成自己的判断直觉。
6. 常见问题速查与避坑清单
最后把平时被问得最多的问题整理成一张表,遇到问题按顺序排查,基本能覆盖九成情况。
6.1 命令敲了没反应的排查顺序
| 现象 | 最可能的原因 | 验证方式 |
|---|---|---|
| 控制台完全打不开 | Shipping 构建裁剪了控制台 | 换 Development 构建试一次 |
| 命令输入后无任何输出 | 命令不存在或拼写错误 | 用Help 命令名确认 |
| 数值型的命令改完没变化 | 变量被更高优先级覆盖,或需重启才生效 | 重启关卡后再看 |
| 自定义 Exec 命令不响应 | 函数所在类不在执行链路上,或未标记 Exec | 换到 PlayerController 上试 |
| ShowFlag 类命令无效 | 名字拼错(大小写敏感)或该版本无此标记 | 用ShowDebug列出可用项 |
| 启动参数里的命令没生效 | 执行时机太早,被后续初始化覆盖 | 改成进游戏后再手动执行验证 |
这张表里最值得展开的是第三条。CVar 不生效的原因五花八门,我遇到过的情况包括:变量被游戏逻辑每帧重设、变量在 ini 里写了两遍后者覆盖前者、变量本身的优先级标志位高于配置文件。排查方法是一致的——先确认命令本身有效(手动执行看有没有变化),再确认有没有别的地方在改它(在代码里搜变量名),最后确认生效时机。
另外,ShowFlag的名字是大小写敏感的,而且不同引擎版本的可选标记不完全一样。写配置文件时如果照抄了别的版本的写法,可能会静默失效。稳妥做法是在当前版本里先用ShowDebug或者帮助命令确认一遍。
6.2 几个容易踩的坑
坑一:stat全开导致数据失真。所有统计面板都开着的时候,统计自身的开销可能占到几毫秒。做对比测试时务必保持"只开必要项",而且两次测试开的项目要一致,否则数据没法比。
坑二:在加载过程中敲命令。无缝切换、关卡流送、资产异步加载这些阶段,LocalPlayer 和执行上下文可能不完整,命令会静默失败。养成习惯——等画面稳定了再敲。
坑三:把调整后的 CVar 忘了写进配置。手敲的变量只对当前会话有效。我见过团队里每个人都在自己机器上手动调,结果打包出来的表现完全不同。凡是确定要保留的调整,立刻写进DefaultEngine.ini。
坑四:用运行时命令去验证只在初始化时读取的变量。前面提过,一部分渲染相关的变量只在启动时读取。你运行时改了看不到变化,不代表这个变量没用,可能只是生效时机不对。这种情况要配合启动参数或者 ini 来验证。
坑五:忽略命令对性能的干扰。像r.TextureStreaming 0、gc.CollectGarbageEveryFrame 1这类命令,用它们做判断是可以的,但不能用来测性能数值。切换完记得恢复默认值,一个干净的基线比什么都重要。
坑六:跨版本照搬命令。UE4 从 4.20 到 4.27 之间,命令的增删和语义变化相当多。看别人的排查记录时,先确认对方用的引擎版本和你是否接近。差距大的话,先验证一遍再用。
6.3 建立自己的命令清单
说了这么多,最后还是落到一个习惯上:建一份自己的命令清单。我的做法是按用途分几个文件——性能排查、画质调优、内存分析、输入调试,每个文件里只放实际用过的命令,附一行说明和一次真实的使用场景。
这份清单的价值在于它是你验证过的,不是从网上抄的。用过的命令你知道它的行为、知道它的坑、知道什么版本能用。新项目启动时把清单过一遍,往往能提前避免很多重复劳动。
另外,清单最好带一个"注意事项"栏,记录那些非直观的行为。比如"这条命令会打断当前帧"、"这条改了之后必须重启"、"这条只在 PIE 下有效"。这些信息文档里通常不写,但实际用起来非常关键。
我个人在实际操作中的体会是:控制台命令的价值不在于你会背多少条,而在于你能不能在出问题的时候,快速想到该用哪条命令去把数据取出来。取数据的能力决定了排查效率,而这份清单就是把这个能力沉淀下来的方式。