AB
AiBoss
チュートリアル

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 单独运行,所以第一步是把编辑器装好:

  1. 安装 Epic Games Launcher。
  2. 在启动器的库中找到 Unreal Editor for Fortnite 并安装。UEFN 免费,支持 Windows 与 macOS。
  3. 启动 UEFN,新建一个岛屿项目。学习阶段用空岛屿模板最合适,场景干净,不会干扰观察输出。

认识开发循环

在 UEFN 里写 Verse,动作顺序基本固定为四步,之后不断重复:

  1. 写:在 creative_device 类中编写逻辑。
  2. 编译:执行 Verse 编译,修掉报错。
  3. 放置:把编译出的设备拖到岛屿上,设置 @editable 引用的对象。
  4. 测试:启动会话进入游戏,观察行为与输出日志。

这四步是后面所有内容的基础。任何一次改动都要走完这一圈,才能确认代码真的生效。

创建第一个 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 等并发原语。
  • 事件驱动:订阅设备事件与玩家动作来驱动逻辑。

第二步:对照你熟悉的语言

如果你有别的语言基础,下面这张对照表能帮你快速建立映射:

概念VerseTypeScriptC#Python
可变变量var X : int = 5let x: number = 5int x = 5;x = 5
不可变变量X : int = 5const x: number = 5const int x = 5;约定俗成的大写命名
函数Add(A:int, B:int):int = A+Bfunction add(a:number, b:number):numberint 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 | nullagent?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}")

跑通它的步骤:

  1. 保存文件,执行 Verse → Build Verse Code,确认没有编译错误。
  2. 在内容浏览器中找到 round_timer_device,拖到岛屿上任意位置。
  3. 按 Launch Session 进入试玩。
  4. 打开 Output Log,应依次看到开局信息、分数更新、每秒递减的时间,以及最后的结算信息。

如果日志里什么都没有,先检查设备是否真的被放到了岛屿上——只编译不放置,代码不会运行。如果编译报错,优先检查缩进是否一致、using 语句是否齐全、以及可失败表达式是否被放进了失败上下文。

这个例子还可以继续扩展:把 RemainingTime 改成 @editable 属性,就能在编辑器面板里直接调整时长而不用改代码;把 AddScore 绑定到某个设备事件上,就能让分数随玩家行为变化。扩展时仍然遵循同一个循环:改代码、编译、放置、测试。

注意事项

  • 沙箱限制:Verse 代码无法访问文件系统、网络套接字或 UEFN API 之外的资源。需要外部数据时,只能通过 UEFN 提供的接口。
  • 默认不可变:忘记加 var 就赋值,会直接编译失败。修改可变变量必须用 set。
  • 可失败表达式必须处理:数组下标、类型转换、返回可选值的调用,都要放进 if 或 block 中,否则编译不通过。
  • 缩进即作用域:缩进不一致会导致难以定位的语法错误,编辑时保持统一。
  • 无限循环要有出口:loop 必须包含挂起表达式(如 Sleep)或 break,否则会卡住会话。
  • 数组是值类型:添加元素会生成新数组,不会原地修改,涉及性能敏感的场景要留意。
  • 只编译不放置不会运行:设备必须拖到岛屿上,代码才会在会话中执行。
  • 版本与平台信息:UEFN 的安装方式、支持的平台、API 模块路径等可能随版本变化,具体以 Epic 官方当前信息为准。