简介:《Torque3D规范及说明书》是针对Torque 3D开源游戏引擎的入门与进阶技术文档,适合希望快速掌握引擎框架、脚本编写与游戏逻辑开发的初学者,也适合有一定经验、需要系统查阅语法和命令的开发者。文档从游戏服务器端、客户端到运行调用流程逐层拆解框架结构,并分章节讲解UI编辑、世界地图编辑、游戏脚本编辑以及命令、语法、变量和对象定义等细节,内容组织清晰,便于边学边查。资源为单份doc文档,压缩包整体大小为442KB,以文字规范与代码说明为主,便于直接阅读和检索。目前已有245人浏览学习。通过这份材料,读者能够理解Torque 3D的核心模块划分,掌握其内置脚本的基本语法与常用命令,并据此搭建游戏逻辑、编辑器界面和交互流程,是一份兼具框架概览和语法速查价值的实用参考资料。
1. Torque3D规范及说明书:demo能跑之后,真正的工程化从读规范开始
你花了几天把Torque3D源码编译通过、能进到默认demo里跑一圈,觉得差不多了。然后开始自己往里加东西——加一个NPC、改一段物理参数、把地形换掉。当天就翻车:脚本文件改了但运行没变化,贴图不知道该往哪个目录放,模块链接报错找不到符号。这几乎是每个入门Torque3D的人都会撞上的墙。标题里"规范及说明书",指的不是网上流传的零散笔记,而是引擎自己沉淀下来的工程约定:模块边界怎么切、命名怎么统一、资产从美术工具到运行时走哪条管线、$Pref开头的配置变量去哪查、以及到底应该从哪个文档入口开始读。这篇文章把这一整套拆开,落到你能直接照做的目录和命令上。适合刚编译通过引擎、准备开始做真实项目的开发者。
2. Torque3D规范体系:模块边界、命名约定与资产生命周期
2.1 引擎分模块的"宪法":为什么每个功能单元都要有main.cpp
Torque3D的工程结构不是一堆散文件堆出来的,它在设计上被拆成了若干个功能模块:核心、平台、渲染、GUI、音频、光照、地形、网络等等。打开engine/source下的任意一个子目录,几乎都能看到一个main.cpp。它并不负责具体功能,只干三件事:声明这个模块的存在、声明它依赖谁、注册它的初始化和关停函数。
这套机制的核心价值是让启动顺序可控。如果所有模块的初始化代码都塞进同一个initAll()里,顺序一乱就互相踩,渲染模块可能在平台层还没建立窗口时就去创建设备,音频模块可能在资源管理器还没起来时就尝试加载音效。Torque3D的做法是让每个模块自己声明依赖关系,引擎启动时构建一张有向图,按拓扑序挨个执行MODULE_INIT,关停时逆序执行MODULE_SHUTDOWN。所以你添加自己的功能模块时,不是往里塞代码,而是要遵守同一套约定:建一个目录,写一个main.cpp,里面放好模块宏。
// 一个符合Torque3D模块规范的新模块入口 #include "core/module.h" #include "myFeature/myFeature.h" MODULE_BEGIN(MyFeature) MODULE_INIT_BEFORE(Sim) // 在Sim模块之前初始化 MODULE_INIT_AFTER(Audio) // 在Audio模块之后初始化 MODULE_DEPEND(Sim, 1, 0, 0) // 显式依赖Sim模块,版本1.0.0 MODULE_INIT { // 注册控制台命令、绑定资源目录、启动子系统的默认状态 MyFeature::init(); return true; } MODULE_SHUTDOWN { MyFeature::shutdown(); } MODULE_END;这段代码是模块的骨架模板。MODULE_BEGIN(MyFeature)给模块起名,这个名字会出现在引擎的模块列表和日志里;MODULE_INIT_BEFORE和MODULE_INIT_AFTER声明执行顺序,顺序写错就可能导致你初始化时调用了还没就绪的子系统;MODULE_DEPEND则是对版本关系的硬性约束。我在实际项目里见过一个很典型的误用:为了省事,把所有自定义玩法代码塞进一个巨大模块里,不声明依赖,结果引擎先初始化了渲染再初始化逻辑模块,第一帧就疯狂报空指针。严格按模块拆分不是为了好看,是为了让编译器和引擎都能验证你代码之间的依赖关系,把运行时的错提前到编译期。
2.2 命名约定与编码规范:从m前缀到DescribeClass宏
Torque3D源码读起来比很多引擎好懂,是因为它的命名规律性很强。C++类名用大写开头,比如GameBase、ClientControl;成员变量统一用m前缀加首字母大写,比如mSpeed、mDamage,一眼能看出这是内部状态;静态变量用s前缀;方法名也用大写开头。宏全部大写,比如TORQUE_DEBUG、MODULE_BEGIN。你不需要背这份规范,读代码时认识这些标记就够了。真正要注意的是C++类怎么暴露给TorqueScript,这是引擎脚本系统的核心约定。
// 把自己的C++类暴露给TorqueScript // 头文件里声明类,实现文件里注册 class MyPlayer : public GameBase { typedef GameBase Parent; public: MyPlayer(); private: F32 mSpeed; // 成员变量约定:m前缀 String mDisplayName; }; // 实现文件里的脚本系统注册 IMPLEMENT_CO_NETOBJECT(MyPlayer); DescribeClass(MyPlayer, GameBase); ConsoleMethod(MyPlayer, setSpeed, void, 3, 3, "(F32 speed) 设置移动速度") { object->setSpeed(dAtof(argv[2])); }DescribeClass把C++类的继承关系告诉脚本系统,ConsoleMethod则把一个C++方法暴露给TorqueScript,argv[2]是脚本传入的第一个参数。参数里那个3, 3表示这个控制台函数最少和最多都只收3个参数(object、方法名、speed),写错会被脚本系统在调用时直接报错。这就是一份"活接口说明书":维持规范的命名和签名,游戏内控制台help输出、网络对象的数据复制、存档序列化才能都正常识别。我一般要求项目里新增的C++类都照这个模板来,哪怕只是内部工具类,因为以后改用TorqueScript调的时候不用再回头补注册。
2.3 资产侧规范:从DTS到DSO,引擎凭什么认你的文件
资产规范容易被当成"文件夹整理习惯",但在Torque3D里它是运行时依赖。常见资产类型有固定格式和固定落点:
| 资产类型 | 运行时格式 | 默认存放目录 | 说明 |
|---|---|---|---|
| 模型 | DTS(Shape) | art/shapes | 引擎认的静态网格格式 |
| 动画序列 | DSQ | art/shapes | 与DTS配套的骨骼动画 |
| 贴图 | TGA/JPG/PNG | art/textures | 直接由资源管理器加载 |
| 场景关卡 | MIS | game/missions | 关卡编辑器保存的文本场景 |
| 地形数据 | DAT | art/terrain | 编辑器生成的地形块 |
| 脚本 | CS/DSO | game/scripts | CS会被编译成DSO缓存 |
引擎的资源管理器按路径字符串去匹配资源,大小写和相对路径都敏感。美术从DCC工具导出模型时,如果文件叫TreeHouse.dts,而脚本里写的是treehouse.dts,在Windows上可能能蒙混过关,换到Linux服务器或别的同事机器上就必然加载失败。所以资产管线的规范第一条就是全小写、无空格、目录归属固定。遇到"模型加载不出来"的问题时,先不要怀疑引擎,去检查路径字符串和实际文件名,这是最高频的翻车原因。Shaders等配置改动后有时也需要清理缓存才会生效,这一点跟脚本DSO的坑是连在一起的,后面避坑章里再展开。
3. Torque3D说明书怎么读:几个必须掌握的入口
3.1 从文档目录找到引擎自带的"手册"
很多开发者拿到Torque3D源码后第一反应是去网上搜教程,其实最可靠的第一手说明就在源码树里。引擎源码里通常带一份doc目录,里面按主题组织:核心开发文档、关卡编辑器说明、渲染与光照解释、脚本系统参考。我建议的阅读顺序是:先看核心开发文档,理解引擎的运行循环和模块机制;再看脚本系统文档,知道.cs文件在哪个阶段被执行;最后才看编辑器操作。因为Torque3D的编辑器本身就是用引擎脚本搭出来的,你不懂脚本系统时,连编辑器里的按钮行为都看不明白。
读文档不要求从头到尾背下来,重点是建立"问题到文件的映射"。比如你被物理参数困惑,要知道物理相关说明在哪一章;你想调渲染反射,知道光照章节里有对应解释。这样遇到具体问题时能快速定位。还有一类容易被忽略的文档是引擎源码里的注释:Doxygen风格的块注释里经常带@see和@ingroup标记,这些标记指向相关模块的入口类和关键函数,对你理解一个大模块的边界非常有用。
3.2 通过配置参数反查文档:在代码里定位默认行为
线上文档再全,也可能跟你手里这份源码不太一样。更靠谱的办法是让源码自己开口——直接搜索配置变量。Torque3D的配置都是$Pref::开头,从分辨率到阴影质量,从音频设备到网络端口,全部可查。当我不知道某个参数会影响什么时,我会用命令行搜索它的引用位置。
# 反查一个配置变量的默认值与所有引用位置 grep -rn "\$Pref::Video::resolution" engine/ scripts/ \ --include="*.cs" --include="*.cpp" --include="*.h"engine/下面是C++源码,能看到这个变量在初始化时怎么被读取;scripts/下面是TorqueScript,能看到游戏侧赋值和默认值。跑完这条命令后,你通常会看到三样东西:默认值写在某个.cs里、读取逻辑在某个C++文件里、显示逻辑在某个GUI脚本里。这就把参数名、默认值和效果串起来了。遇到"某个效果开关叫什么名字"这类问题,我也用同一种方法:先想一个关键词在$Pref::变量列表里搜,再顺着引用找到含义。这个方法比翻文档快得多,而且永远匹配你手上这份代码。
3.3 接口这块"说明书":C++注册给脚本的函数去哪看
Torque3D有一份动态生成的接口清单,就在引擎自己的控制台里。运行引擎后按波浪号键打开控制台,输入help,引擎会列出所有已注册的控制台命令;输入dumpStatics,所有控制台静态变量会打印出来。这份清单的价值在于它只显示当前这份编译产物里真实存在的能力——如果某个模块因为你没编译进去,它注册的函数就不会出现。
所以,"判断某个接口是否存在"的快速办法是启动demo打开控制台查一下,而不是去翻文档猜。我排查模块缺失时也用这招:比如怀疑音频模块没链接进去,就输入help看有没有sfx相关命令。它还能用来检查命名冲突,如果你注册的类名和引擎已有类重名,控制台输出的列表里会有重复项。这份动态清单加上源码注释,比你保存的任何第三方速查表都准。
4. 把Torque3D规范落进项目:目录组织、构建命令与脚本调参
4.1 从模板目录开始,而不是从零搭骨架
官方模板里已经帮你排好了目录层级:game/放脚本与配置,tools/放编辑器工具,art/放美术资产。这些路径不是装饰,引擎启动时会用相对路径去定位资源,把art目录换个地方,很多默认配置就直接失效。所以我的做法永远是复制模板目录,重命名成项目名,然后在这个基础上改,而不是新建一个工程再把文件挪进去。
复制之后第一件事是做一次空构建。什么都不改,直接把模板编一遍,确认当前环境能跑通引擎、能加载默认关卡。这一步能暴露环境依赖问题:缺第三方库、缺少依赖包、编译选项不对。空构建通过了再开始改项目名,你就能确定后续的报错是改动引起的,而不是环境本身的问题。这一步看起来很笨,但能帮你省掉后面整整一周的排查时间。
4.2 CMake构建与从本地跑通最小命令
Torque3D的构建走标准CMake流程,Windows下用Visual Studio生成器,Linux下用Unix Makefiles。这里给出Linux命令行的最小示例:
# 在构建目录执行CMake配置并编译 cmake -G "Unix Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ -DT3D_DLL=OFF \ -DT3D_APPNAME=MyProject \ -DCMAKE_INSTALL_PREFIX="$HOME/t3d_build" \ .. make -j8参数说明:T3D_DLL=OFF让引擎和一个项目合成单个可执行文件,调试时符号更直观;T3D_APPNAME=MyProject决定生成的可执行文件名;CMAKE_BUILD_TYPE控制优化等级,调试用Debug。make -j8是并行编译的核数,按你机器实际核数改。命令里的..指向源码根目录,你需要在引擎源码下先建一个build目录再执行这行命令,避免把CMake生成的中间文件混进源码目录。
构建完成后,引擎会生成一个可执行文件。启动时带一个参数告诉引擎去哪个目录找main.cs和配置,常见做法是直接指定你的项目目录名。如果启动后控制台能输出启动日志、能加载模板里的默认场景,就算跑通了。后续每次改C++代码都要重新走一遍make,所以我习惯写一个构建脚本把CMake和make两条命令串起来,避免漏步骤。
4.3 常用配置参数改成自己的值:一个实例
项目的玩家偏好配置通常集中在scripts/client/prefs.cs里。这个文件是引擎启动早期就会被读的,在这里改参数比每次敲命令行稳定得多。看一个实例:
// 本地偏好设置:窗口模式、分辨率、阴影开关 $pref::Video::fullScreen = 0; $pref::Video::resolution = "1920 1080"; $pref::Shadows::enable = 1; $pref::Shadows::numShadowMaps = 4;这些$Pref变量不需要额外注册,脚本里直接赋值,引擎在对应子系统初始化时读取。注意分辨率是一个带空格的字符串,不是数字;全屏和阴影开关用0/1表示。numShadowMaps这个值是阴影质量的关键,调高了会影响性能但提升画面,调低了阴影会分层明显。想知道每个参数取值范围是多少,就用上一章说的grep方法反查C++读取处,通常能看到对应的枚举或上下界检查。
这里提醒一个项目规范问题:prefs.cs是会被引擎写入的——玩家在游戏里改画面设置后引擎会重写这个文件。如果你把自己的项目特定配置也塞进去,下次玩家改动设置就可能把你的配置覆盖掉。所以项目级配置和玩家偏好配置要分开,玩家偏好进prefs.cs,项目逻辑配置进server/或game/scripts下你自己的配置文件,这是Torque3D规范里很容易被忽略的一条约定。
5. Torque3D规范落地避坑清单:几个反复翻车的点
5.1 改了脚本不生效:DSO缓存还在捣乱
现象:改了一个.cs文件,重新运行引擎,行为没有任何变化,日志里执行的还是旧逻辑。
原因:Torque3D首次运行会把.cs脚本编译成DSO字节码并缓存到本地,启动时如果DSO的时间戳比.cs新,引擎会直接用缓存。这个优化本意是加速加载,但它是所有刚接触Torque3D的人遇到的第一块绊脚石。
解决:删除项目目录下的cache文件夹和所有.dso文件,再重新运行。更稳定的做法是在构建脚本里加一个清理步骤,每次启动项目前自动清掉缓存。我个人的习惯是永远保留一个一键清理脚本,避免手滑忘删导致"改了半天其实没生效"的白忙活。
5.2 资源路径大小写不一致:模型加载直接挂
现象:往art/shapes放入新模型后,引擎报Unable to load shape,但文件明明就在那里。
原因:资源管理器按路径字符串匹配,大小写敏感。Windows开发机上文件系统不区分大小写,所以自己的机器上一直正常,一旦拿到Linux环境或者别人拉代码后,路径就断了。
解决:项目规范里强制所有资产文件名和引用路径统一小写、不用空格。我一般会在CI或构建脚本里加一个小检查脚本,扫描所有脚本里出现的资产路径,比对文件系统里实际文件名。这个坑最讨厌的地方在于它不一定当场爆,可能过几天换环境才爆,趁早用脚本检查比靠人眼稳得多。
5.3 脚本报错只有行号没有文件名
现象:控制台报Line: 42却没有文件名,PlayUp一个脚本错误只能看到行号,代码一大就不知道去哪改。
原因:Release构建里脚本编译信息被精简,错误回调里没有带出脚本所属模块路径。
解决:用-log参数启动引擎,把完整脚本日志写到文件里,日志里会带模块路径。排查脚本错误时我再习惯性地加-debug构建参数,Debug构建下DSO缓存会失效,错误信息更完整。日常写脚本时也建议把编辑器的"显示行号"打开,配合日志里的模块名很快能定位到文件。如果只有行号没有文件名,还有一个笨办法:全局搜索这一行内容,通常能跨文件找到唯一匹配。
5.4 对象句柄与资源生命周期:黑匣子式的崩溃
现象:程序跑一段随机崩溃,崩溃栈停在某个SimObject的析构,有时甚至没有任何报错,只是进程消失。
原因:Torque3D的对象体系靠SimObject句柄管理生命周期,直接delete一个对象,会让其他还在引用它的对象拿到悬挂指针;模块关停顺序不对时,一个模块清掉对象,另一个模块还在用。
解决:不要对SimObject直接调用delete,在TorqueScript侧用delete还是可以的,但C++侧务必经SafeDelete或先注销再释放,并保证对象从场景中解绑。关停顺序靠MODULE_SHUTDOWN保证:依赖方先关,被依赖方后关。排查这类问题,用Debug构建加-debug参数启动,对象注销时控制台会给出提示,再配合崩溃位置的类名,基本能锁定是谁在越界持有指针。
5.5 模块依赖顺序导致的重复符号或未解析符号
现象:新增一个自建模块后,C++链接阶段报重复符号或者unresolved external symbol。
原因:模块间依赖声明不完整。两个模块各自实现了一个同名全局函数,或者一个全局对象在初始化时依赖尚未初始化的模块符号。
解决:先检查MODULE_INIT_BEFORE和MODULE_INIT_AFTER有没有写反,再把所有全局符号完整地放进命名空间,不要裸奔。把T3D_DLL从OFF改成ON,用动态链接库隔离能更快定位是哪个模块导出了重复符号。预防手段更简单:每次新建模块前,照着一个已有模块的main.cpp抄一句MODULE_DEPEND,不要凭感觉猜依赖关系。
6. 一个落地技巧:用规范约束快速生成新模块骨架
新增功能之前先生成规范骨架,能省掉大量重复劳动。我把这个流程做成了一个小脚本,放在我们项目的tools/目录里,每次新模块只传一个名字就够了。
#!/usr/bin/env python3 # 生成符合Torque3D模块规范的新模块骨架 import os, sys def main(): name = sys.argv[1] path = os.path.join("engine/source", name) os.makedirs(path, exist_ok=True) main_cpp = f'''#include "core/module.h" #include "{name}/{name}.h" MODULE_BEGIN({name}) MODULE_INIT_AFTER(Sim) MODULE_DEPEND(Sim, 1, 0, 0) MODULE_INIT {{ {name}::init(); return true; }} MODULE_SHUTDOWN {{ {name}::shutdown(); }} MODULE_END; ''' with open(os.path.join(path, "main.cpp"), "w") as f: f.write(main_cpp) # 同时生成空实现文件和头文件骨架 with open(os.path.join(path, name + ".h"), "w") as f: f.write(f"namespace {name} {{ void init(); void shutdown(); }}\n") with open(os.path.join(path, name + ".cpp"), "w") as f: f.write(f'#include "{name}/{name}.h"\nvoid {name}::init() {{ }}\nvoid {name}::shutdown() {{ }}\n') if __name__ == "__main__": main()运行方式是python3 gen_module.py MyFeature,它会在engine/source下建好目录、生成main.cpp、头文件和空的实现文件。目录名和文件名保持一致,模块名大小写统一,C++命名空间与目录名统一,这样从第一步就避免了"命名花式"带来的搜索困难。生成脚本里只声明了对Sim的依赖,实际项目里你还要按功能补上音频、渲染、GUI等依赖,但不补也比不声明强。我习惯把新功能的需求先列成依赖清单,再填进生成后的main.cpp。
这个脚本本身也是一种团队规范:它规定了每个新模块的初始形态,后续所有人写新功能都从同一份骨架出发,代码风格不会漂移。它不能帮你自动写业务逻辑,但它把"从哪里开始"变成了一条确定命令,后面维护时少很多"这个模块为什么跟别的长不一样"的困惑。踩了两年Torque3D的坑,我最深的教训就是:凡是能变成命令的操作,就不要靠记忆和习惯;希望帮到你。
本文还有配套的精品资源,点击获取