Verse Wiki — 写给 UE6 时代的 Verse 互动手册
第一章 · 第 3 课

注释、模块与 using:给代码留言,给工具分箱

前两课的模板里,那三行 using 你抄了两遍还没问过它们是干嘛的。这一课补上:先学会用 # 和 <# #> 给人类留言,再搞清 using 每次都在搬哪个工具箱,顺便看懂 Verse 那套域名式模块路径的设计。

一、注释:写给队友(和三个月后的你)的留言条

代码有两拨读者:一拨是编译器,另一拨是人——你的队友,以及三个月后回头看代码、已经忘光当时思路的你自己。注释就是专门写给后一拨读者的留言条:编译器读到它会直接跳过,它对程序的行为没有任何影响,却能救下无数「这行到底想干嘛」的深夜。

Verse 提供三种注释写法,各管一种场景:

comment_gallery.verse
# 行注释:# 到行尾都是写给人看的

MaxPlayers:int = 8  # 也可以跟在代码后面补一句

<# 块注释:想写几行写几行,
   直到遇到收尾符号才结束 #>

Total:int = 1 <# 甚至能塞进行中间 #> + 2

<#>
    缩进注释:这一段比上面的符号缩进更深,
    所以整段都是留言,编译器一个字都不看。

(蓝图视角:你在蓝图里留言,是拖一个灰色 Comment 注释框、或右键节点写 Node Comment;Verse 没有框可拖,改成用 #<# #><#> 这几个记号在代码里直接打字。形式换了,作用一样:只给人看,Compile 时被当空气。)

「注释永不执行」这件事,与其背下来不如亲眼看一遍。下面这台设备里埋了一段被块注释包住的旧代码,点「运行下一步」,看看它有没有戏份:

memo_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# 这台设备只干一件事:证明注释从不执行
memo_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        Print("步骤 1:开机自检")  # 行尾注释,不影响左边
        <# 这句旧台词已经退役,编译器看都不看:
           Print("我是被注释掉的代码")
        #>
        Sleep(1.0)
        Print("步骤 2:注释全程零出场")
输出日志

点「运行下一步」,看代码怎么一行行执行。

顺带一提:社区里常说块注释可以互相嵌套,但不同版本的表现有出入,课程先不依赖这个特性。# TODO: 以 Verse API Reference 为准——真要包住含 #> 的内容时,自己先 Build 一次验证。

二、模块:代码世界的分格工具墙

想象一面摆满工具的墙:如果锤子、螺丝、创可贴、薯片全堆在一个大箱子里,找任何东西都要翻半天。真实项目的代码也一样——设备、角色、随机数、UI,几百上千个定义如果全挤在一个命名空间里,重名、误用、找不到,全都会来。真实项目也一样——设备、角色、随机数、UI,成百上千个蓝图和资产如果全塞进同一个大抽屉、名字还不分家,重名、拿错、翻不到,样样都来。Verse 的解法就是模块(module):把相关的定义装进同一个盒子,盒子有名字、有边界、可以整盒搬走复用。

官方对模块的定义相当郑重:「Verse 模块是可再分发、可被依赖的原子代码单元,能在不破坏依赖方的情况下随时间演进。」翻译成人话:模块是发布 API 的最小单位,Epic 可以升级模块内部实现,而你的代码照跑不误。你在 UEFN 里用到的一切官方能力——每台设备、每个函数——都住在某个模块里;甚至你自己项目 Verse 目录下的每个文件夹,也会自动成为一个同名模块(这是组织多文件项目的关键,拓展页里有完整攻略)。

所以「写 Verse」的第一步永远是:想清楚要用的东西住在哪个盒子里,然后把那个盒子搬过来——搬盒子的动作,就是 using。

三、using:把工具箱搬上工作台

using 的语法只有一种,花括号一个都不能少:

典型的文件开头
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

它的作用:把指定模块里的 public 定义引入当前文件,之后就能直呼其名——写 creative_device 而不是每次都写全路径。不 using 也不是不能活,用完整限定名同样可以访问,只是没人愿意每行都背地址。习惯上,所有 using 都写在 .verse 文件顶部,像游戏开场先亮装备栏。

它的作用:把指定模块(那个工具抽屉)里对外开放的东西一次性引进来,之后你就能直接写短名字 creative_device,而不用每次念一长串完整地址。这一步就像在项目里勾选启用一个插件,或把某个资产文件夹拉进引用范围。不写 using 也不是不能活,把那串完整长地址从头背到尾照样能用,只是没人乐意每行都背。习惯上,所有 using 都堆在 .verse 文件最上面,像游戏开场先把要用的装备栏一次亮齐。

常用路径各管什么,先混个脸熟,忘了随时回来查:

模块路径 管什么 代表成员
/Fortnite.com/Devices creative_device 基类与全部内置设备 creative_devicebutton_device
/Fortnite.com/Characters 游戏角色 fort_character
/Fortnite.com/Game Fortnite 玩法层的通用定义
/Verse.org/Simulation 时间与仿真 Sleep
/Verse.org/Random 随机数 GetRandomIntGetRandomFloat
/Verse.org/Colors 颜色 color
/UnrealEngine.com/Temporary/Diagnostics 调试日志工具 log 日志类
/UnrealEngine.com/Temporary/SpatialMath 空间数学 vector3
/UnrealEngine.com/Temporary/UI 界面

注意到没有?这些路径长得像网址:以 / 开头,顶层是 Fortnite.comVerse.orgUnrealEngine.com 这样的互联网域名。这是有意为之的设计——域名标明了 API 的归属和稳定性承诺:/Verse.org 是语言与通用运行时层面的能力,/Fortnite.com 属于 Fortnite 生态,/UnrealEngine.com 来自引擎侧;而路径里的 Temporary 段是官方在明说「这些 API 是临时安置,未来会搬家」——所以见到它别慌,也别惊讶于某天要改 using。另一个冷知识:Print 本尊住在 /Verse.org/Verse 模块里,这个模块不用显式 using 就能用,所以前两课从没为 Print 写过导入;模板里那行 Diagnostics 提供的其实是 log 日志类这类调试工具。

动手时间:下面这台「打盹设备」的开头被挖掉了两个词——一个是搬工具箱的关键字,一个是 Sleep 所住的模块名。填回去,点「检查答案」:

(读图翻译:这台设备等于 Event BeginPlay 里串一个 Print String「眯 3 秒……」→ 一个 3 秒的 Delay → 再一个 Print String「醒了!」;开头那几行 using,就是把 Delay 和打印字所在的抽屉先勾进来。)

nap_device.verse
____ { /Fortnite.com/Devices }
using { /Verse.org/____ }
using { /UnrealEngine.com/Temporary/Diagnostics }

nap_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        Print("眯 3 秒……")
        Sleep(3.0)
        Print("醒了!")

四、自己动手建模块:先混个脸熟

用别人的模块会了,自己也能划一块地盘。最直接的写法是 module 表达式:最直接的写法,是手写一小段 module 声明,给自己开一个新抽屉:

tools.verse
# 用 module 表达式手动划一块地盘
tools<public> := module:
    Greet<public>():void =
        Print("来自 tools 模块的问候")

(读图翻译:上面这段等于新建了一个叫 tools 的工具抽屉,里面放了一个蓝图函数 Greet,函数体就一个 Print String;两处 <public> 是给抽屉和函数各挂一块「对外可见」的牌子。)

之后在别的文件里 using { tools },就能直接调用 Greet();不导入的话,写 tools.Greet() 加限定名访问也行。之后在别的文件里写 using { tools }(把 tools 这个抽屉拉开),就能直接喊 Greet();懒得拉抽屉,写全名 tools.Greet() 也能找到它。另一条更常用的路是隐式模块:项目 Verse 目录下的每个文件夹自动成为同名模块,建文件夹就是建模块,一行代码都不用写。

代码上那对 <public> 尖括号是可见性说明符代码上那对 <public> 尖括号,作用就是蓝图里给变量、函数选 Public / Protected / Private 的那个访问权限设置——只不过 Verse 把它写成尖括号挂在名字上。Verse 有四档:public(谁都能用)、internal(默认,仅当前模块内)、protected(类及子类)、private(仅当前类)。这里只需要记一条铁律:想让模块外的代码用上某个成员,模块和成员两个都得标 <public>——漏掉任何一个,对面就会撞上访问权限错误。模块的组织术、嵌套与可见性的完整玩法,我们放到第 16 课之后的进阶章节再深挖,现在混个脸熟就够。

五、常见坑:忘了 using 的那一刻

坑一:忘写 using,收获「Unknown identifier」。这是新建空白 .verse 文件时的头号事故:兴冲冲写下 my_device := class(creative_device):,一 Build,编译器甩来一句「Unknown identifier」——它不认识 creative_device。原因很简单:creative_device 不是关键字,只是 /Fortnite.com/Devices 模块里的一个类,工具箱没搬来,里面的工具自然不存在。以后凡是见到 Unknown identifier,第一反应就该是:是不是少了一行 using?

坑二:using 漏了花括号。写成 using /Fortnite.com/Devices 直接是语法错误。花括号是 using 语法的一部分,不是装饰。

坑三:块注释忘了闭合。<# 写了,#> 忘了,后面一大段正经代码全被吞成注释——更阴险的是,报错位置往往在文件很靠后的地方,离真正的案发现场十万八千里。看到莫名其妙的大面积报错,回头检查有没有没闭合的 <#

坑四:随手改文件夹名。文件夹就是模块,重命名或移动文件夹等于改了模块路径,轻则一串 using 失效,重则关卡里已放置的 Verse 设备丢失类引用。改名前先想想有多少地方引用了它。

另外,导入嵌套模块时顺序有讲究——先导入外层再导入子模块,反过来会报错。这个坑连同跨文件夹引用的完整心法,见本课拓展页。

六、关卡挑战

三道小关卡,验收这一课的战利品。答错零惩罚,可以一直重试。

新建了一个空白 .verse 文件,顶部什么都没写,直接来一句 my_device := class(creative_device): 然后 Build,结果会怎样?

想把一大段旧代码整块注释掉,应该用哪种写法?

代码里写了 Sleep(2.0),Build 时报「Unknown identifier」。最可能缺了哪行 using?

拓展阅读

拓展 · EXTRA

digest 文件:比官网更新鲜的 API 词典

每次构建都会生成的 .digest.verse 文件,是与你本机版本严格同步的全量 API 清单——老手查 API 的第一站。

进入拓展 →

拔高 · EXTRA

从 .vmodule 到「目录即模块」

UEFN 23.20 的模块体系变革:为什么今天建个文件夹就是建个模块,以及默认 internal 背后的设计动机。

进入拓展 →

技巧 · EXTRA

跨文件夹引用自己模块的正确姿势

同目录、子文件夹、嵌套模块三种场景的 using 写法速查,外加「先外层后子模块」的导入顺序铁律。

进入拓展 →