OnBegin 先睡一帧:Sleep(0.0) 的社区偏方
一个只有一行代码的实战技巧:在 OnBegin 的第一行写 Sleep(0.0)。它来自 Epic 开发者论坛的高赞帖,是几乎每个 UEFN 开发者迟早会学到的「祖传偏方」——顺带还教会你「挂起函数里睡零秒 = 等一帧」这个通用知识点。
一个只有一行代码的实战技巧:在 OnBegin 的第一行写 Sleep(0.0)。它来自 Epic 开发者论坛的高赞帖,是几乎每个 UEFN 开发者迟早会学到的「祖传偏方」——顺带还教会你「在能等待的函数里睡零秒 = 等一帧」这个通用知识点(相当于放一个时长 0 的 Delay 节点)。
一、症状:代码没错,但第一帧「静默失败」
先描述一下病情。你在 OnBegin 里写了一段逻辑:游戏一开始就去找玩家、读取其他设备的状态、或者对场景里的对象动手。代码编译通过,设备也乖乖拖进了关卡,会话正常启动——但那段逻辑就是没生效,而且连报错都没有。
这不是你的错,而是初始化顺序的问题。OnBegin 在游戏体验开始的第一帧就被调用,而此时引擎的很多运行时对象——玩家、角色、其他设备的状态——可能还没就绪。这时候去调用依赖它们的 API,得到的往往是一次静默的失败:Verse 的可失败调用失败了,流程继续走,没有崩溃、没有红字,只有「怎么什么都没发生」的困惑。
这不是你的错,而是初始化顺序的问题。OnBegin 在游戏体验开始的第一帧就被调用,而此时引擎的很多运行时对象——玩家、角色、其他设备的状态——可能还没就绪。这时候去碰依赖它们的功能,往往会静默地失败:就像在蓝图里读一个还没就绪、还是 None 的引用,只不过 Verse 不会弹 Accessed None,而是这条线悄悄走不通、直接跳过——流程照样往下跑,没有崩溃、没有红字,只留下「怎么什么都没发生」的困惑。
Epic 开发者论坛上有一篇高赞帖,标题非常直白:「important Verse tip: ALWAYS add frame of delay to Your OnBegin() method」(重要技巧:永远给你的 OnBegin 加一帧延迟)。原帖举的例子正是这种场景:开局立刻取玩家(GetPlayspace().GetPlayers() 一类调用)再对角色做操作开局立刻抓取所有玩家(相当于蓝图的 Get All Players 一类节点)再对角色做操作,第一帧执行时对象还没就绪,直接失败;帖子给出的解法收获了大量点赞与浏览。
二、药方:第一行写 Sleep(0.0)
解法便宜到离谱——在 OnBegin 函数体的第一行,加上:
解法便宜到离谱——在 OnBegin 里面(缩进那串节点)的第一行,加上:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# 一个懂得「先等一帧」的耐心设备
patient_device := class(creative_device):
OnBegin<override>()<suspends>:void =
# 偏方:睡 0 秒 = 挂起到下一个 tick 再继续
Sleep(0.0)
Print("第二帧才开工,运行时状态已经就绪。")
读图翻译:这张设备蓝图的 Event BeginPlay 一开场,先接一个 Delay(时长填 0)节点让它等一帧;等下一帧,再接 Print String 打出「第二帧才开工,运行时状态已经就绪。」。—— 说白了,真正的活儿都往后挪了一帧再干。
关键在于理解 Sleep(0.0) 在 Verse 里的确切含义:它不是「不睡」,而是「挂起当前函数,到下一个 tick(下一帧)再继续」。哪怕参数是 0 秒,函数也会老老实实让出这一帧,等世界转完一圈再醒来。于是你的初始化逻辑从「和引擎抢第一帧」变成了「等引擎摆好桌子再上菜」——玩家、角色、设备状态这些对象在第二帧通常已经就绪,后续调用就稳了。
关键在于理解 Sleep(0.0) 在 Verse 里的确切含义:它不是「不睡」,而是「让这根执行线先停一下,到下一个 tick(下一帧)再继续」——效果就像蓝图里放一个时长 0 的 Delay 节点。哪怕填 0 秒,它也会老老实实让出这一帧,等世界转完一圈再往下走。于是你的初始化逻辑从「和引擎抢第一帧」变成了「等引擎摆好桌子再上菜」——玩家、角色、设备状态这些对象在第二帧通常已经就绪,后面再去碰它们就稳了。
这也解释了为什么 OnBegin 的固定写法里必须有 <suspends>:正因为它是可挂起函数,你才能在里面 Sleep。「挂起函数里 Sleep(0.0) = 等一帧」是个通用知识点,以后写逐帧循环(loop: + Sleep(0.0))时还会反复用到。
这也解释了为什么 OnBegin 的固定写法里必须有 <suspends>(等待徽章):正因为它允许中途等待,你才能在里面放 Sleep(Delay)。「能等待的函数里 Sleep(0.0) = 等一帧」是个通用知识点,以后写逐帧循环(一个自己绕回去、每圈都 Sleep(0.0) 的循环)时还会反复用到。
三、用药说明:偏方好使,但别当万金油
几条实践建议:
- 什么时候加:OnBegin 里要在开局立刻访问运行时状态(取玩家、读其他设备、操作场景对象)时,先睡一帧最稳;纯粹 Print 打日志、初始化自己的变量,则不需要;
- 它是偏方,不是根治:Sleep(0.0) 赌的是「一帧之后就绪」,绝大多数情况成立,但本质上仍是轮询式思维。更稳健的路线是订阅事件——比如等玩家触发某个事件再执行逻辑,而不是开局猜状态;事件订阅是后面课程的重头戏;
- 它是偏方,不是根治:Sleep(0.0) 赌的是「一帧之后就绪」,绝大多数情况成立,但本质上仍是「猜时机」的思路。更稳健的路线是订阅事件(在蓝图里就是 Bind Event——把玩家或设备的某个事件绑到你的逻辑上)——等事件真的发生了再执行,而不是开局瞎猜状态;事件订阅是后面课程的重头戏;
- 别把重活全堆在第一帧:就算不踩就绪问题,开局一帧塞太多逻辑也会造成卡顿,适当用 Sleep 分摊节奏是好习惯。
在带 <suspends> 的函数里调用 Sleep(0.0),效果是什么?在带 <suspends>(等待徽章)的函数里放一个 Sleep(0.0),效果是什么?
四、出处
本文整理自 Epic 开发者论坛的社区高赞帖: