ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Torque 3D 入门:引擎架构、启动流程与脚本调试

Torque 3D 入门:引擎架构、启动流程与脚本调试

简介:《Torque3D规范及说明书》是一份面向Torque 3D游戏引擎初学者与进阶开发者的中文技术文档,重点解决引擎框架理解、脚本入门与项目调试缺少系统参考的问题。文档从游戏服务器端、客户端及运行调用过程讲起,涉及网络通信、状态同步、图形渲染和界面交互,并延伸到UI编辑、世界地图编辑与游戏脚本编辑,同时整理常用命令、语法规则、变量声明和物体定义等脚本基础,适合原型开发、引擎学习与团队内部查阅。资源包共1个doc文件,约442KB,体量轻便,便于本地保存和关键词检索。已有246人学习,说明其在Torque3D中文资料中具备一定参考价值。读者可据此梳理引擎运行脉络,理解服务端与客户端的分工,并对照脚本语法、对象定义和模块说明建立较完整的学习框架,减少初期摸索成本。

1. 搜 Torque 3D 入门的人,先别急着关掉网页

搜 Torque 3D 入门的人,十有八九点进官网之后会先愣三分钟:下载页、文档页、论坛页全是英文,不清楚该从哪看起,甚至不确定自己下载的到底是不是一个完整的游戏引擎。其实可以先把它想成一套宜家柜子——板子切好了,孔打好了,螺丝配齐了,图纸也附上了,你要做的不是种树炼铁,而是照图纸把柜子装起来。Torque 3D 就是一个把渲染、网络连接、碰撞判断、界面按钮这些底层活儿提前打好包的 3D 游戏引擎,你上手写的是脚本,描述“谁站在哪、按下某个键之后发生什么”。这份《Torque3D 规范及说明书》相当于那张图纸,它把引擎拆成 UI 编辑、世界地图编辑、游戏脚本三块,还专门写了一件事:引擎启动时到底先读哪个文件。对零基础的人来说,这一点很重要——很多教程上来就贴一堆代码,却不说清楚这些代码该放进哪个目录、由谁调用,照着抄也只能停在照抄。这份说明书没绕这个弯,它先把框架摊开给你看,再讲语法、变量、物体定义,顺序正适合新手。

2. 拆分引擎的两半:服务器端六个模块和客户端七个模块

新手看引擎架构图,最容易犯的错是把它当成一份“背诵材料”,想着一口气全记下来。真没必要。你只需要先建立一个大印象:这个引擎跑起来有两半,一半负责“算”,一半负责“显示”。负责算的叫服务器端,负责显示的叫客户端。哪怕你只打算做单机小 demo,这两半也照样都在,只是跑在同一台机器上而已。把这两半的分工弄明白,后面写脚本时你才知道该把代码往哪个文件里放。

2.1 服务器端那六个模块,各管哪一摊

Network Server 网络服务模块。它负责服务器与所有客户机之间的连接和数据传输,也负责版本升级这类事务。把它想成小区门口的传达室:所有进出的人、送来的包裹,都得从这儿过一遍。你以后遇到“连不上”“延迟高”“版本对不上”这类问题,第一个该怀疑的就是它。

World Map 世界地图模块。服务器端也会载入一份地图镜像,在服务器上模拟出一个跟客户端看起来差不多的场景。它的用途不是给玩家看,而是用来算碰撞、判断谁撞到了谁。相当于裁判手里那套场地模型——球员看到的是球场,裁判看的是同一块场地的尺寸和边界,谁踩线了,裁判说了算。这样做的好处是,所有客户机看到的位置和时间能尽量保持一致。

Database Connection 数据库连接模块。它负责让游戏内的数据和数据库保持同步与及时更新。你可以把它当账本:玩家攒了多少金币、买了什么装备,光存在内存里,一关服就没了,所以要写进数据库。

ServerCommand 服务器执行命令。客户机发来的可执行命令,比如移动、攻击,最终都在这里被处理。玩家在客户端按了一下键,服务器接收后执行对应动作,这个模块就是执行动作的那个环节。

Message 客户消息处理。它负责转发和记录客户机之间、客户机与服务器之间的消息,聊天功能主要靠它。要是你想过“游戏里的聊天框是怎么实现的”,答案就在这儿。

Object Data 对象数据处理。场景里所有的 Player、AIPlayer、物品,它们的动作、碰撞、特效,全都归这个模块控制和处理。这是六个模块里你日后打交道最频繁的模块——改角色属性、加一个新道具,最后基本都落到这里定义的数据上。

2.2 客户端那七个模块,画面是怎么来的

Canvas 画面显示处理模块。它管的是整个游戏窗口的显示设置:显示驱动类型、分辨率、色深这些。相当于投影仪的设置菜单,先得把投影仪调对,画面才谈得上好不好看。

Gui 用户交互界面模块。从游戏开始到结束,所有可见的画面、按钮都归它。主菜单、血条、背包格子,玩家眼睛能点能看的东西基本都在这个范畴里。

Action Map 行为动作模块。它是一张映射表:哪个功能键对应哪个操作。你按下 W 角色往前走,按 Esc 弹出菜单,背后就是这张表在把“按键”翻译成“动作”。

ServerConnection 服务器连接模块。它是客户端与服务器之间的桥,所有游戏信息都要经它传输交换。桥断了,客户端就成了一个只会放画面的空壳。

ClientCommand 客户端命令执行模块。方向跟 ServerCommand 相反:服务器让客户端做的事,比如让某个角色坐下,从这里下来执行。一个管“往上发”,一个管“往下发”,别记混。

Message 服务器消息处理模块。收到服务器消息之后,负责显示和提示。你在屏幕中央看到的系统公告,多半是从这儿出来的。

World Map 世界地图模块。载入服务器指定的地图,并负责地图的显示和相应处理。注意它跟服务器端那个 World Map 名字一样、活儿不一样:那边算碰撞,这边管显示。

2.3 为什么新手也要懂这个:一次“按键开枪”的旅行

光看模块名没什么感觉,我们跟着一次按键走一遍。玩家在客户端按下开火键,Action Map 先查表,确认这个键对应“开火”;客户端把这个意图通过 ServerConnection 发出去;服务器端的 ServerCommand 接到命令,交给 Object Data 去算这一枪打没打中、撞到了什么;算完之后,Message 把结果广播给相关客户机;客户端收到消息,由 Canvas 和 Gui 把开火特效、血量变化画到屏幕上。一条链走下来,可能只花了几十毫秒。

这条链的意义在于:以后你想改任何一个环节,都得先知道它挂在哪。想改按键,去 Action Map;想改伤害算法,去 Object Data 相关的数据定义;想改命中提示的显示,去客户端 Message。不知道这条链,改代码就是碰运气,改完不知道为什么不生效,也不知道为什么突然好了。

注意:单机跑 demo 的时候,服务器端和客户端在同一台机器上,你可能感觉不到它们的存在。但只要涉及网络,这两半的分工就变得非常实在,改动之前先想清楚这段逻辑该在哪一边。

3. 跟着启动流程走一遍:从 exe 到 main.cs 的那几行代码

环境搭好之后最劝退的一步,是不知道该改哪个文件。这一章就解决这件事。双击游戏可执行文件之后,引擎不是满硬盘找脚本,它有一条固定的路线。路线走通了,你后面所有的改动才有落点。这段代码用的是 TorqueScript,语法接近 C++ 和 JavaScript,下面代码块按 javascript 着色,只是为了看着顺眼。

3.1 双击 exe 之后,引擎第一眼找的是 main.cs

主目录下有一个 main.cs,它是整场游戏的起点。里面干的事不复杂:把要用的游戏目录登记进去,然后逐个执行这些目录下的 main.cs。

$defaultGame = "starter.fps"; // 默认游戏目录名 // 把某个目录登记进引擎的模块搜索路径,再执行该目录下的 main.cs function loadDir(%dir) { setModPaths(pushback($userMods, %dir, ";")); // 往分号分隔的路径列表尾部追加 exec(%dir @ "/main.cs"); // @ 是字符串拼接 } // 递归拆开分号分隔的路径列表,逐个执行其中的 main.cs function loadMods(%modPath) { %modPath = nextToken(%modPath, token, ";"); // 切出第一段路径和剩余部分 if (%modPath !$= "") loadMods(%modPath); // 还有剩余就继续递归 if (exec(%token @ "/main.cs") != true) $modcount--; // 执行失败就把计数减一 } loadMods($userMods); // 正式启动

这段代码里几个关键点值得记住。$userMods是全局变量,装着以分号分隔的目录列表;pushback往列表尾部追加一项;nextToken按分隔符把字符串切成第一段和剩余部分,注意第二个参数写的是token,不带 % 前缀,这是这个函数要求的特殊写法;exec执行一个脚本文件,返回值可以拿来判断成功与否;@是字符串拼接,跟加号不是一回事,后面讲变量时还会专门说。

%dir、%modPath这些以 % 开头的都是局部变量,只在函数内部有效。$defaultGame、$userMods、$modcount以 $ 开头,是全局变量。这套前缀规则是 TorqueScript 最基础的一条,记不住它,后面看代码会很吃力。

3.2 进到游戏目录,看 package StarterKit 里那两个函数

轮到游戏目录下的 main.cs 干活时,第一件事通常是加载 common 目录,那里放的是基本命令、函数和 GUI 文件。接着你会看到一个 package 块,里面包着onStart和onExit两个函数。

package StarterKit { function onStart() { Parent::onStart(); // 必须调用,引擎内部初始化 // 依次载入客户端、服务器、数据三套脚本 exec("./client/init.cs"); exec("./server/init.cs"); exec("./data/init.cs"); // 服务器对所有会话都要启动,因为客户端也能在游戏内开服 initServer(); // 二选一:专用服务器模式,还是客户端模式 if ($Server::Dedicated) initDedicated(); else initClient(); } function onExit() { export("$pref::*", "./client/prefs.cs", False); // 保存客户端设置 if (isObject(moveMap)) moveMap.save("./client/config.cs", false); // 保存改过的按键映射 export("$Pref::Server::*", "./server/prefs.cs", False); // 保存服务器设置 BanList::Export("./server/banlist.cs"); // 导出封禁名单 Parent::onExit(); // 必须调用,释放资源 } }

几个容易被忽略的细节。package的作用是给一组函数套个壳,壳里的同名函数会优先于引擎原有版本执行,这是一种不修改原文件就能扩展行为的方式。Parent::onStart()和Parent::onExit()是在调用被覆盖掉的那个原版函数,漏写会直接导致初始化或释放流程中断,游戏可能出现莫名其妙的问题,这是新手最常犯的错误之一。

onStart和onExit这两个名字是引擎自己会去调的,你不需要在别处主动调用它们。$Server::Dedicated是引擎内置的全局变量,为真就走专用服务器分支,否则走客户端分支。./表示当前脚本文件所在的目录,这个相对路径写法后面会反复出现。

3.3 三个 init.cs 各管什么,先记这张表

onStart里那三行 exec 是分水岭,脚本从此分成三摊。刚开始不用把每个文件都读一遍,记住各自的职责就够了:

文件大致职责你什么时候会去改它
./client/init.cs载入界面、鼠标、输入映射等客户端侧脚本和 GUI 文件想改启动界面、往主菜单加个按钮
./server/init.cs载入玩家、武器、AI、游戏规则等服务器侧脚本想加一把新武器、调 AI 行为
./data/init.cs载入共享的数据定义,各类 datablock 多半在这一层想改角色血量、跳跃高度

需要提醒一句:这三个文件是入口,不是终点。它们内部还会 exec 更多文件,一层套一层。比如服务器侧的 game.cs 里就会继续 exec 玩家脚本、武器脚本、AI 脚本。所以找代码的正确姿势是顺藤摸瓜,从这三个入口往下翻,而不是在目录里瞎翻。

提示:第一次读的时候可以只读 onStart 那三十来行,把 exec 的路径在纸上抄一遍,画成一棵树。树画出来,你就知道这个游戏是怎么一块一块拼起来的了。

4. 第一个能跑的例子:按下 ~ 键,敲一行 echo

理论看到这儿,该动手了。这一章的所有操作都在游戏自带的控制台里完成,不用新建文件,不用改目录,出错也不会把项目搞坏。控制台是新手最好的练习场,也是老手最常用的排查工具。

4.1 第一步:唤出控制台,敲响第一声

运行游戏的可执行文件,进到主界面或者进游戏都行,然后按键盘左上角那个 ` 键——就是跟 ~ 同一个位置、不用按 Shift 的那个。控制台窗口会弹出来,底下有一行输入框。

输入下面这行,回车:

echo("hello!"); // 控制台立刻回显 hello!

echo就是把括号里的内容打印到控制台。看着不起眼,但它是你后面调试脚本的主要手段:怀疑某段代码没执行,就在里面加一行 echo;怀疑某个变量值不对,就把它 echo 出来看。分号必须加,跟 C++ 一样,它是语句结束的标记。

多行语句可以一口气粘进去,引擎会按分号分行执行:

$a = 1; echo($a); // 输出 1

这里$a是全局变量,=是赋值,echo($a)打印它的值。你可能会问,为什么不能直接写echo($a)而不先赋值?因为没赋值的变量是空的,打印出来什么都没有。养成“先赋值、再打印”的习惯,能省掉很多困惑。

4.2 第二步:变量、字符串、数组,一样样试

先试一个能让大部分人愣一下的例子:

echo("1" + "2"); // 输出 3 echo("1" @ "2"); // 输出 12

同样是两个带引号的字符,加号把它当数字算,结果是 3;@才是字符串拼接,结果是 12。这就是 TorqueScript 灵活的地方,也是新手最容易栽的地方——你以为在做字符串拼接,引擎却在做加法。想做拼接,认准@。

还有几个拼接操作符,注意必须大写:

echo("abcd" NL "CDEF"); // 输出 abcd,换行,再输出 CDEF

NL是插入换行,TAB是插入制表符,SPC是插入一个空格。写成小写nl是不认的,这个坑不少人踩过。

接着看变量和数组的关系:

$a = 2; $a[0] = 3; echo($a); // 输出 2 echo($a[0]); // 输出 3 echo($a0); // 输出 3,是 $a[0] 的另一种写法

变量和数组同名互不影响,$a和$a[0]是两个独立的值。更有意思的是$a0这种写法,它等价于$a[0];多维数组$a[0,0]还可以写成$a0_0。这种灵活性看着方便,读别人代码时却容易认不出来,心里有个数就行。

最后试一下颜色控制,这个在写提示信息时挺实用:

echo("\c2hello!\c0hello!"); // 第一个 hello 红色,第二个恢复默认色

\c0到\c9是切换字体颜色,\c0是默认色;\n换行,\t制表,\\表示一个反斜杠本身。颜色只影响当前行后面的字符,不会改掉整个控制台的默认色。

4.3 第三步:写第一个函数和一个 datablock

函数定义跟 C++ 很像,参数用 % 开头:

// 定义一个打招呼的函数,参数是局部变量 function sayHello(%name) { echo("hello, " @ %name); // @ 拼接字符串 return 1; // 返回值可选,不写也行 } sayHello("newbie"); // 输出 hello, newbie

function是关键字,函数名后面跟括号里的参数列表,参数用 % 前缀表示局部变量意味着它只在函数内有效。return是可选的,不写就默认返回空。函数名和变量名都不区分大小写,sayHello和sayhello是同一个函数,这一点跟 C++ 不一样,得适应一下。

然后是 TorqueScript 特有的东西——数据块:

datablock PlayerData(PlayerBody) { renderFirstPerson = false; // 第一人称视角下是否渲染自己的身体 shapeFile = "~/data/shapes/player/player.dts"; // 模型文件,~/ 指主游戏目录 cameraMaxDist = 3; // 第三人称最大镜头距离 cameraDefaultFov = 90.0; // 默认视野角度 cameraMinFov = 5.0; // 最小视野角度 };

数据块的作用是给某一类物体预先定义一堆属性。定义完之后,它就在内存里存了一份,创建物体时直接引用这个定义就行,不用每次都把属性重写一遍。注意结尾那个分号不能少,少了会引发一串让人摸不着头脑的报错。

定义好了,创建物体长这样:

%player = new Player() { dataBlock = PlayerBody; // 必须项,指定用哪套属性 client = %this; // 归属哪个客户端连接 }; MissionCleanup.add(%player); // 加进场景,退出时自动清理

new返回一个句柄,赋给%player。这个句柄本质上是个整数,在当前客户端里唯一,相当于物体的本地 ID。MissionCleanup.add那行也别省,它的作用是让这个物体在场景切换或退出时被自动清理掉,不加的话容易留下残余对象。

4.4 第四步:用 F10 和 F11 打开那两个编辑器看一眼

脚本之外,还有两个可视化编辑器值得花十分钟摸一下。它们不写代码,但能让你直观看到“界面”和“地图”这两样东西是怎么组织的。

  1. 进游戏后按 F10,打开 GUI 编辑器,从控件面板拖一个按钮到画面上,改个名字,保存。
  2. 按 F11,打开世界编辑器,在场景里选中一个物体挪个位置,保存。
  3. 回到游戏里退出再进,看看刚才的改动还在不在。

需要有个心理准备:编辑器只负责“摆样子”,按钮点下去要发生什么、物体被碰到会怎样,这些功能都得回到脚本里补。文档里把 UI 编辑和世界地图编辑单独列成两节,讲的就是这个分工。

提示:第一次拖控件不用追求好看,能保存、能重新加载出来就算成功。这一步的目的是让你相信“改动是生效的”,后面写脚本才有底气。

5. 新手最容易踩的五个坑:语法、路径和 ID 上的翻车现场

这一章的五条,都是我见过新手最容易反复踩的。每一条按“现象、原因、解决办法”三段说,照着对号入座就行。

5.1 语法上的三个翻车现场

坑一:语句末尾漏了分号。现象是整段脚本像没生效,或者控制台报错的行号跟你写的位置对不上,报在下一行甚至下一段。原因是 TorqueScript 靠分号断句,漏一个,后面的语句会被拼到上一句里一起解析,错误自然就漂移了。解决办法是看报错时别只盯着报的那一行,往上翻一两行找漏掉的分号;另外函数调用、赋值、数据块结尾,这三处是重灾区。

坑二:把双引号和单引号混着用。现象是echo出来看着一模一样,但比较的时候死活不相等,条件判断走进奇怪的分支。原因是"abcd"是普通字符串,'abcd'是标记字符串,两者是不同的东西,不能互换。解决办法是统一用双引号,除非你明确知道自己需要标记字符串;接手别人代码时,先扫一眼引号用得统不统一。

坑三:自己起的全局变量跟引擎原有的撞了。现象是某个数值怎么改都不对,或者界面突然错乱、鼠标失灵,而且找不到直接原因。原因是$开头的全局变量在整个客户端里只有一份,你起了个跟引擎内置变量同名的,等于把人家的值覆盖掉了。解决办法是起名前加自己的前缀,比如$myGame_score这种;能用局部变量 % 解决的,就别用全局。

5.2 路径和 ID 上的两个坑

坑四:用了绝对路径,目录一挪就找不到资源。现象是模型不显示、贴图变白,或者启动时提示找不到某个文件,但文件明明就在硬盘上。原因是绝对路径把整条硬盘路径写死了,主目录一改名、一移动,路径立刻失效。解决办法是用相对路径关键字:./表示当前脚本文件所在目录,~/表示主游戏文件 main.cs 所在目录。养成习惯之后,整个项目文件夹可以随便搬。

坑五:把本地 ID 当成网络 ID 用。现象是单机测得好好的,一联网,别人的角色在你屏幕上乱套,或者你操作的角色不是你想操作的那个。原因是每个物体有两个 ID:一个是客户端本地唯一的句柄,一个是整个网络唯一的网络 ID。单机逻辑用本地句柄完全没问题,但跨机器传数据时传本地句柄,对方根本对不上号。解决办法是记住这个区分,涉及联网的逻辑要拿网络 ID,别顺手把%player直接发出去。

注意:第五个坑在单机阶段几乎不会暴露,所以特别阴。做单机 demo 时就把“本地”和“网络”这两个概念分开想,等真联机了能少熬几个晚上。

6. 接下来学什么:从看懂脚本到自己写一个能动的角色

入门阶段最容易犯的第二个错,是学完语法就一头扎进一个完整游戏项目里。结果被几千行脚本淹没,三天后放弃。比较稳的路径是四周,每周只盯着一个目标。

阶段主要做什么怎么算过关
第一周在控制台里把 echo、变量、字符串拼接、if/while、函数都敲一遍不看资料能写出一个带参数、有返回值的函数
第二周读 server 侧入口那几个 init.cs,顺着 exec 往下翻两三层能说出某个功能大概在哪个文件里
第三周只改数值:血量、镜头距离、视野角度、移动速度改完进游戏能明显感觉到变化
第四周加一个小东西:一个能被捡起来的物品,或者给现有武器换个伤害值加的东西能正常出现、正常生效、退出不报错

四周之后再回头碰schedule()和eval()这两个东西。schedule用来做延时和循环,eval能把一段字符串当命令执行,两个都很方便,也都很容易把代码写得别人看不懂。刚入门时优先用直白的写法,等真的遇到非它不可的场景再学。

还有个小习惯强烈建议早点养成:每次动脚本之前,先在要改的那段前后各加一行 echo,确认这段代码真的被执行到了。听起来笨,但排查问题时它能帮你把范围从“整个项目”缩到“某一行”。我最早学的时候跳过语法直接去改玩家脚本里的数值,结果一个漏掉的分号让我找了整整一晚,控制台报的行号指向一段我根本没动过的代码。从那以后我每次改脚本都强制走一遍:加 echo、确认执行、再改逻辑,三步走完才敢继续往下写。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进