
Verse
用 Verse 在 UEFN 中编写第一个 creative_device 并掌握失败上下文
面向 UEFN 创作者:从安装环境、生成第一个 creative_device,到变量、函数、控制流、数组与失败上下文的完整 Verse 入门。每一步都给出可直接粘贴的代码,并说明编译、放置、测试的循环流程。
Verse 是 Epic Games 为 UEFN(Unreal Editor for Fortnite)提供的编程语言,用来在 Fortnite 岛屿中编写自定义游戏逻辑。它是一门静态类型的函数式与逻辑式混合语言,代码写在继承自 creative_device 的类里,取代了标准 Unreal Engine 中的蓝图与 C++。如果你想把「玩家进入区域后计分」「倒计时结束后结算」「随机传送」这类规则真正跑起来,而不是靠一堆设备连线拼出来,Verse 就是必须跨过的那道门槛。本文按「写代码 → 编译 → 放置 → 测试」的循环展开,把语言核心概念和 UEFN 里的实际操作串成一条可执行的路径。站内也有对应的工具条目可供查阅:Verse。
本文面向的读者包括有 TypeScript、C#、Python、Lua 经验的人,也包括完全没有编程背景的创作者。前者可以重点看语法对照与失败上下文,后者建议按顺序读完全部小节,因为 Verse 的很多设计与其他语言差别很大,跳读容易在编译报错上卡住。
准备工作
安装 UEFN
Verse 不能脱离 UEFN 单独运行,所以第一步是把编辑器装好:
- 安装 Epic Games Launcher。
- 在启动器的库中找到 Unreal Editor for Fortnite 并安装。UEFN 免费,支持 Windows 与 macOS。
- 启动 UEFN,新建一个岛屿项目。学习阶段用空岛屿模板最合适,场景干净,不会干扰观察输出。
认识开发循环
在 UEFN 里写 Verse,动作顺序基本固定为四步,之后不断重复:
- 写:在
creative_device类中编写逻辑。 - 编译:执行 Verse 编译,修掉报错。
- 放置:把编译出的设备拖到岛屿上,设置
@editable引用的对象。 - 测试:启动会话进入游戏,观察行为与输出日志。
这四步是后面所有内容的基础。任何一次改动都要走完这一圈,才能确认代码真的生效。
创建第一个 Verse 文件
从工具栏的 Verse 菜单打开 Verse Explorer 面板,右键项目名称,选择 Add new Verse file to project,模板选 Verse Device,然后给它起个名字。UEFN 会生成一个继承自 creative_device 的起始类,内容大致如下:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# A Verse-authored creative device that can be placed in a level.
hello_verse_device := class(creative_device):
# Runs when the device is started in a running game.
OnBegin<override>()<suspends> : void =
Print("Hello, Verse! This device is now running.")
这段模板里有三处值得注意:
- 顶部的
using语句导入 API 模块。少了它们,Print这类名字会直接报「未声明」。 - 类名用
:=声明,继承关系写在括号里,即class(creative_device)。 OnBegin是入口点,它重写了基类的同名方法,并带有<suspends>效果,因此可以在其中使用异步代码。
接着执行 Verse → Build Verse Code 编译。编译通过后,新设备会出现在内容浏览器里。把它拖到岛屿任意位置,按 Launch Session 进入试玩,打开 Output Log 就能看到打印的欢迎信息。到这一步,第一个 Verse 设备就算跑通了。
操作步骤
第一步:理解 Verse 的定位与边界
Verse 不是通用语言,它专为 UEFN 与 Fortnite 生态设计,运行在 Epic 的沙箱中。这意味着 Verse 代码无法访问文件系统、网络套接字,也无法触碰 UEFN API 之外的任何东西。这种限制是刻意的:创作者代码不能危害玩家或平台。
它的几个关键特征决定了你写代码的方式:
- 静态类型:每个变量在编译期就有确定的类型,类型错误在运行前被拦下。
- 默认不可变:变量默认是常量,要可变必须显式加
var。 - 失败上下文:可能失败的操作(数组下标访问、类型转换等)必须显式处理,因此不存在空指针异常。
- 缩进决定作用域:与 Python 类似,用缩进而非花括号划分代码块。
- 异步与并发:内置
sync、race、rush、branch、spawn等并发原语。 - 事件驱动:订阅设备事件与玩家动作来驱动逻辑。
第二步:对照你熟悉的语言
如果你有别的语言基础,下面这张对照表能帮你快速建立映射:
| 概念 | Verse | TypeScript | C# | Python |
|---|---|---|---|---|
| 可变变量 | var X : int = 5 | let x: number = 5 | int x = 5; | x = 5 |
| 不可变变量 | X : int = 5 | const x: number = 5 | const int x = 5; | 约定俗成的大写命名 |
| 函数 | Add(A:int, B:int):int = A+B | function add(a:number, b:number):number | int Add(int a, int b) | def add(a, b): |
| 字符串 | Name : string = "Player" | const name: string = "Player" | string name = "Player"; | name = "Player" |
| 数组 | var Scores:[]int = array{1,2,3} | let scores: number[] = [1,2,3] | int[] scores = {1,2,3}; | scores = [1,2,3] |
| 空值 / 可选 | ?agent(option 类型) | agent | null | agent? | Optional[agent] |
第三步:声明变量与类型
变量声明采用 名称 : 类型 = 值 的写法。默认不可变,加 var 才可变,修改可变变量要用 set 关键字:
# 不可变变量(赋值后不能修改)
PlayerName : string = "DefaultPlayer"
MaxHealth : int = 100
GravityMultiplier : float = 1.0
IsGameActive : logic = true
# 可变变量
var CurrentScore : int = 0
var PlayerSpeed : float = 5.0
var RoundCount : int = 1
var IsAlive : logic = true
# 更新可变变量(使用 set 关键字)
set CurrentScore = CurrentScore + 10
set PlayerSpeed = 7.5
set RoundCount += 1
set IsAlive = false
核心类型如下:
int:整数(64 位),例如Score : int = 42。float:小数(64 位浮点),例如Speed : float = 3.14。string:文本,例如Name : string = "Player1"。logic:布尔值,取true或false。void:无返回值,用作函数返回类型。
第四步:编写函数
函数由名称、参数、返回类型和函数体组成,既可以写成单表达式,也可以写成缩进块。Verse 函数返回函数体最后一个表达式的值,大多数情况下不需要写 return:
# 单行函数
Double(X : int) : int = X * 2
# 带缩进块的多行函数
CalculateDamage(BaseDamage : float, Multiplier : float, Armor : float) : float =
RawDamage := BaseDamage * Multiplier
ReducedDamage := RawDamage - Armor
if (ReducedDamage < 0.0):
0.0
else:
ReducedDamage
# 无返回值的函数
LogMessage(Message : string) : void =
Print("LOG: {Message}")
# 修改可变状态的函数
AddScore(Points : int) : void =
set CurrentScore = CurrentScore + Points
Print("Score updated to: {CurrentScore}")
注意字符串插值使用花括号,例如 "LOG: {Message}",这与很多语言不同,但用起来很直观。
第五步:掌握控制流
Verse 的 if 同时是失败上下文,所以它既做条件判断,也做可失败表达式的处理。这是理解整个语言的关键。
# 基本 if / else
if (CurrentScore > 100):
Print("High score!")
else if (CurrentScore > 50):
Print("Good score.")
else:
Print("Keep trying.")
# if 作为失败上下文(处理可失败表达式)
if (Player := GetRandomPlayer[]):
Print("Found a player!")
else:
Print("No player found.")
# 多条件
if (IsAlive = true, CurrentScore > 0):
Print("Player is alive and has scored.")
循环方面,for 可以遍历区间、数组,也可以带过滤条件:
# 遍历区间
for (I := 0..9):
Print("Index: {I}")
# 遍历数组
Scores : []int = array{10, 20, 30, 40, 50}
for (Score : Scores):
Print("Score: {Score}")
# 带过滤的遍历
for (Score : Scores, Score > 25):
Print("High score: {Score}")
loop 是无限循环,必须包含挂起表达式或 break,否则会卡死:
loop:
Sleep(1.0)
set Timer = Timer - 1
Print("Timer: {Timer}")
if (Timer <= 0):
break
block 则把若干可失败表达式打包成一个失败上下文,任意一行失败,整个块就优雅失败:
block:
Player := GetRandomPlayer[]
Position := Player.GetTransform().Translation
Print("Player at position: {Position}")
第六步:使用数组
数组用 []类型 声明,是有序集合。数组下标访问是可失败的,因为索引可能越界,所以必须放在失败上下文中:
var Scores : []int = array{100, 200, 300, 400, 500}
Names : []string = array{"Alice", "Bob", "Charlie"}
EmptyArray : []float = array{}
# 长度
Count := Scores.Length
# 访问元素(可失败,必须在失败上下文中)
if (FirstScore := Scores[0]):
Print("First score: {FirstScore}")
# 添加元素(生成新数组,数组是值类型)
set Scores = Scores + array{600}
# 遍历
for (Score : Scores):
Print("Score: {Score}")
# 带索引遍历并过滤
for (Index -> Score : Scores, Score > 250):
Print("Score {Score} at index {Index}")
删除元素的做法是构造一个不含该下标的新数组:
RemoveAtIndex(Arr : []int, Index : int) : []int =
var Result : []int = array{}
for (I -> Element : Arr, I <> Index):
set Result = Result + array{Element}
Result
第七步:理解可选类型与失败上下文
失败上下文是 Verse 与大多数语言最根本的区别。它不用空指针异常,不用未检查错误,也不用 try-catch,而是让可失败表达式在编译期就被强制处理,从而消除一整类运行时崩溃。
可选类型用 ?类型 声明,表示一个可能存在也可能不存在的值。取值时在失败上下文中用 ? 操作符解包。数组下标访问、类型转换、以及返回可选值的 API 调用,都属于可失败表达式。把它们放进 if 或 block 里,成功分支拿到有效值,失败分支走 else,编译器会保证两条路都被考虑到。
实践中的判断很简单:只要某个表达式后面带方括号或问号,它就需要失败上下文。忘记这一点是新手最常见的编译错误来源。
一个完整示例
下面把前面几节的内容拼成一个能跑通的最小设备:玩家进入游戏后,设备维护一个倒计时,每秒递减,同时记录分数,倒计时归零时打印结算信息。代码可直接粘进新建的 Verse 设备文件:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
round_timer_device := class(creative_device):
var RemainingTime : int = 10
var CurrentScore : int = 0
OnBegin<override>()<suspends> : void =
Print("Round starting. Time: {RemainingTime}")
AddScore(50)
loop:
Sleep(1.0)
set RemainingTime = RemainingTime - 1
Print("Time left: {RemainingTime}")
if (RemainingTime <= 0):
break
Print("Round over. Final score: {CurrentScore}")
AddScore(Points : int) : void =
set CurrentScore = CurrentScore + Points
Print("Score updated to: {CurrentScore}")
跑通它的步骤:
- 保存文件,执行 Verse → Build Verse Code,确认没有编译错误。
- 在内容浏览器中找到
round_timer_device,拖到岛屿上任意位置。 - 按 Launch Session 进入试玩。
- 打开 Output Log,应依次看到开局信息、分数更新、每秒递减的时间,以及最后的结算信息。
如果日志里什么都没有,先检查设备是否真的被放到了岛屿上——只编译不放置,代码不会运行。如果编译报错,优先检查缩进是否一致、using 语句是否齐全、以及可失败表达式是否被放进了失败上下文。
这个例子还可以继续扩展:把 RemainingTime 改成 @editable 属性,就能在编辑器面板里直接调整时长而不用改代码;把 AddScore 绑定到某个设备事件上,就能让分数随玩家行为变化。扩展时仍然遵循同一个循环:改代码、编译、放置、测试。
注意事项
- 沙箱限制:Verse 代码无法访问文件系统、网络套接字或 UEFN API 之外的资源。需要外部数据时,只能通过 UEFN 提供的接口。
- 默认不可变:忘记加
var就赋值,会直接编译失败。修改可变变量必须用set。 - 可失败表达式必须处理:数组下标、类型转换、返回可选值的调用,都要放进
if或block中,否则编译不通过。 - 缩进即作用域:缩进不一致会导致难以定位的语法错误,编辑时保持统一。
- 无限循环要有出口:
loop必须包含挂起表达式(如Sleep)或break,否则会卡住会话。 - 数组是值类型:添加元素会生成新数组,不会原地修改,涉及性能敏感的场景要留意。
- 只编译不放置不会运行:设备必须拖到岛屿上,代码才会在会话中执行。
- 版本与平台信息:UEFN 的安装方式、支持的平台、API 模块路径等可能随版本变化,具体以 Epic 官方当前信息为准。