📚 脚本&框架

脚本文件&引擎框架作为模组逻辑的控制单元,完全控制着模组何时、如何执行某些行为。

📒 逻辑入口

模组的所有逻辑都由代码控制组成,而代码(由英文、字符组成)则被编写到一个文本文档*.js格式)。

这个“文本文档”即是我们所说的“脚本”(包含代码的文本)。

常规情况下,编程与我们的人脑思维非常相似,就好像在阅读某篇文章时,均是采用由前到后、由上到下的阅读方式,阅读每一行文本的内容,并同时尝试理解它的含义。

沙盘引擎 模组运行第一步(逻辑入口)即在脚本目录(Script/Main),这里包含着模组的初始代码

也就是说,模组加载完成后,即在内部读取Script/Main下的脚本文件,然后“一行一行”的阅读并执行脚本所写的代码内容。

想想一下,您曾经玩过的其他游戏,无非都是按顺序执行的N个过程

例如:1. 展示Logo 2. 进入主菜单 3. 弹出欢迎画面 4. XXX 5. XXX ...

游戏的开发过程即是如此:将N个简单的代码按顺序拼凑起来,最终组成复杂的玩法

📒 初识脚本

让我们大胆一点!已知无论如何模组会先从Script/Main读取脚本,那我们就打开这个脚本文件夹

此时通常应该会看到至少1~2个文件(文件扩展名为*.js,表示JavaScript脚本文件)。

以常规(示例)模组为例,我们可能会看到World.jsClient.js两个文件,如果文件不存在,也可以手动创建文件(注意扩展名)

沙盘引擎 是World(Server)+Client同步的脚本编写模式,也就是服务端脚本、客户端脚本同时存在的逻辑模式。

在不同脚本文件编写的代码(及API规范)有所不同,用途也大不相同。

脚本 说明 用途示例
World.js 世界脚本(服务端)
玩法主要编写在此处,会自动多人同步
控制世界天气、生成角色、生成载具等
Client.js 客户端脚本(本地端)
主要编写仅针对本地的玩法,不会自动多人同步
控制游戏相机镜头、本地UI显示等

为什么要将脚本分成两份?

沙盘引擎 是原生支持多人联机的玩法创作,因此在脚本编写和设计上并非仅考虑本地形式。

世界脚本 [World]用于编写主要玩法逻辑,在实际游戏过程中,只有主机\服务端玩家会运行此脚本,普通玩家(后续加入的玩家)不会运行世界脚本。

客户端脚本 [Client]用于编写本地辅助逻辑,在实际游戏过程中,无论是主机\服务端还是普通玩家都会运行客户端脚本。

由此得知,世界脚本更像是一个控制全局的关键,而客户端脚本主要是为玩家本地可见的内容做辅助。

简单总结:编写 世界脚本 时应站在全局(所有人)的角度,而编写 客户端脚本 时应站在自身的角度。

📒 脚本构成

脚本文件(xxx.js)意义上仍然属于文本文档,其特殊的后缀*.js以供 沙盘引擎 识别为脚本文件,并由引擎识别其内部的代码文本含义。

因此,编写模组脚本起点非常简单,最简单至用记事本等工具即可开始编写。

让我们先从脚本的结构入手,用任何代码或文本工具打开Script/Main/World.js脚本文件。

不同的模组脚本内容均不相同,本文将以部分脚本代码解释意义,抛砖引玉。

📘 事件 (Event)

此类样式的代码,通常表示监听事件,也就是当某情况发生时代码应该做些什么。

根据代码相关的命名规范,通常以OnXXX为开头的“方法”代码,均被认为是事件函数,这多数是 沙盘引擎 内置的,当然也可以自行编写。

function OnScriptLoad()
{
 
}

解释:function是一个保留关键字,表示在脚本中声明一个方法体,其后面的"签名"OnScriptLoad是关键部分,不同的“签名”表示不同的含义,而方法体中{...}即可编写相关代码,表示此方法触发后将执行的内容。

📘 方法 (Function)

此类样式的代码,通常表示执行方法,也就是调用代码API做一些事情。

World.Core.SetHostName("Test Server");

解释:World.Core.SetHostName是 沙盘引擎 内置的方法API,括号内表示方法的“参数”,不同的方法有不同的参数,具体需要参考脚本API。

📘 属性 (Property)

此类样式的代码,通常表示获取或修改某属性,也就是调用代码API修改一些内容。

World.Core.Player.Find(0).Name = "Hello";

解释:World.Core.Player.Find(0)是 沙盘引擎 内置的方法API,此代码用于获取ID为0的玩家对象,关键在于后面的.Name = "Hello";,表示将其玩家昵称修改为Hello

Name 是玩家对象的一个属性,在玩家对象后.Name,即为访问此属性,需要注意的是,并非所有属性均可写,部分为只读属性,例如Player.FPS,显而易见的,玩家的帧数不可能被我们修改,这取决于玩家的帧数限制&设备配置。

📒 基础探索

通过上述的脚本构成介绍,我们了解 沙盘引擎 通常依靠这几种关键代码形式来实现功能。

对于初学者来说,看起来有些复杂难懂吗?

不要灰心,只需要将代码理解成特定形式的编写格式,特殊的符号(点、空格、括号、双引号)有着不同的含义。

由浅入深,可以先适应代码的编写样式,逐一根据单词的意思理解其用途和含义。

让我们继续探索一段相对完整的代码,例如它编写在World.js脚本文件中:

function OnScriptLoad()
{
    World.Core.SetHostName("Test Server");
    World.Core.SetWorldWeather(6);
    World.Core.SetWorldTime(22, 00);
}

这段代码主要是一个事件方法,其签名为OnScriptLoad(当,脚本,加载),顾名思义,也就是当脚本被加载后要做些什么。

根据JavaScript代码编写要求,我们可以将要执行的代码写在方法体内(也就是{...}两个大括号之间,不限长度)。

在示例代码的方法体中,我们看到了许多以World.Core作为开头的前缀,这表示执行某个代码类下的子方法

什么是类的子方法?

首先,前缀的World表示是世界脚本World.js)下支持的代码,因此要以World作为前缀,反之Client作为客户端脚本也是一样。

其次,随着代码功能数量的增加(可能成千上万),我们需要系统的整理和快速定位某个方法,相当于是一个书架&书签的功能。

例如,当需要设置世界主要内容时,就使用World.Core(世界,核心)下的代码内容。

抛砖引玉,假设需要设置玩家相关内容时,自然就使用World.Player(世界,玩家)下的代码内容。

结合以上的解释,我们稍微试着一字一句的阅读一遍代码,就能理解此代码的如下含义:

  • 当世界脚本加载后执行
  • World.Core.SetHostName()设置服务器名称为Test Server
  • World.Core.SetWorldWeather()设置世界天气6(雪天)
  • World.Core.SetWorldTime()设置世界时间为22, 00(晚上十点)

📒 深入探索

当我们完整阅读并理解上述代码,距离学会脚本已经成功一大步了!

尽管正式模组开发的过程并非如此简单,但游戏玩法的制作就是一项多个简单组成一个复杂的工作,让我们进一步学习看看!

function OnScriptLoad()
{
    World.Core.SetWorldTime(12, 00);
}
 
function OnPlayerJoin( player )
{
    World.Core.SetWorldTime(20, 00);
    World.Core.Message("Player: " + player.Name + " joined!");
}

这段代码包含两个事件方法,根据代码的样式,我们大致可以看出这是完整且独立的两个结构

两个方法的签名(方法名)、参数(括号里的内容)、方法体代码均有所不同,因此至少说明这段代码可以在2个条件触发下执行一些事情。

首先,最上方的OnScriptLoad我们已经熟悉了,表示脚本加载后做的事情,这里的方法体编写了World.Core.SetWorldTime(12, 00);,表示将世界时间调整为12点。

至此,方法OnScriptLoad的内容到}就执行完毕了,编程执行的逻辑顺序和我们想的一样,随后它会继续向下阅读并执行。

接下来,继续执行到OnPlayerJoin(当,玩家,加入),在括号中还有一个名为player的参数(一种代码编写形式),我们可以理解为它是一个资料袋,里面装着关于这个参数的相关属性和功能。

那么考虑到此方法及参数的命名,我们能够直接理解这是一个关于“玩家”的事件及附带参数。

向下继续阅读,方法体编写了World.Core.SetWorldTime(20, 00);,表示将世界时间调整为20点。

再向下一行,方法体编写了World.Core.Message("Player: " + player.Name + " joined!");,表示发送一条公屏信息,内容是"Player: " + player.Name + " joined!"也就是:“玩家:玩家名称 加入了!”(此处的[玩家名称]会被动态识别为玩家名称)。

通过以上的案例,也许会有疑问:为什么此处可以识别到玩家的名称?

World.Core.Message()是一个发送公屏消息的方法,在括号(参数)内我们需要填写要发送的内容。

而代码中的"Player: " + player.Name + " joined!"实际上是三个不同的内容,最终组合为一段话

分别是:"Player: " + player.Name(这里是代码属性 [Property]) + " joined!"

其中的player.Name即是引用自方法参数player,也就是获取player.Name(玩家,名称)。

通过这段代码的案例,我们知道代码的编写是非常自由的,只要确保格式的规范正确,可以几乎无限制的编写

📒 更多实践

沙盘引擎 在设计时尽可能将复杂的代码进行封装和简化,因此许多功能只需简单几行代码即可调用实现。

更多情况下,需要的是开发者扩展的思维,以及在何时、何处编写什么样的代码,以及用多个相关的代码最终组成想要实现的功能。

JavaScript是一个比较容易上手的编程语言,在正式模组开发之前,非常建议至少掌握任意一门编程语言的基础语法知识

必要掌握的知识:数据类型、语法格式、if分支语句、方法及参数、变量、数组等。

在上述内容中,我们已经大致学习了语法、格式、方法等基础知识。

在后续的学习和编写过程中,会逐渐遇到更多的编程知识,建议至少掌握上述必要知识在后再继续向下实践。

通常情况下,0基础学习至上述要求的必要知识进度,正常学习时间1~2小时即可掌握。

📒 重载和调试

当模组内的脚本等文件发生更改(保存)后,引擎并不会直接更新最新的逻辑,而是会在下次模组重载时自动更新。

此过程手动触发也非常简单,通常在模组\脚本修改完成后,可以点击或使用重连按钮(F12)快速重新建立连接,已达到更新模组的作用。

每次重连(重载)后,模组即会使用最新的模组内配置及脚本。

在少数情况下,如果系统文件更新有延迟,也可以使用Shift+F12强制完整更新并重连,但通常无需此操作。

📒 参考API文档

在实际脚本开发的过程中,代码API几乎贯彻着开发全局,因此我们精心整理了脚本开发及API文档。

对于游戏中可能遇到的各类功能、事件、属性,均在文档中有所说明。

当开发准备就绪时,您至少应该阅读一遍基础相关API,再进行后续的开发工作。

📒 进阶脚本工具

在实际脚本开发的过程中,开发者可能更喜欢有代码补全的智能编写体验,沙盘引擎 为此做了充足的支持工作。

例如:编写输入 Chara 时,编辑器会自动帮助补全 Character ,以及相关参数、属性、返回值的类型和说明。

沙盘引擎 推荐使用VSCode进行脚本编写,并且对此编辑器的支持更加完善。

📘 安装步骤

  1. 下载并安装VSCodeTrae等编辑器工具
  2. 启动 沙盘引擎,加载想要编写的模组
  3. 启动游戏控制台(按键[~],需要在游戏设置里开启控制台)
  4. 控制台输入updatesdk,为当前已加载的模组更新SDK(VSCode开发工具)
  5. 正常情况下,关于VSCode的辅助开发文件(代码提示文件)已经安装到模组目录
  6. 模组目录下应该存在SEngine.code-workspace | SEngine.d.ts | jsconfig.json相关文件
  7. 通常可以打开SEngine.code-workspace快速进入VSCode模组工作区,或者正常使用VSCode加载模组文件夹
  8. 享受代码自动补全、内置事件补全等功能