📚 脚本开发及API

本文档篇幅较长且相当重要,开发之前务必预览至少一次

📒 索引

📒 前置概念

沙盘引擎 脚本采用世界脚本&客户端脚本”二合一的开发方式。

引擎每次加载世界场景时,将加载一个整体脚本组World.js + Client.js),前者负责实现世界主要逻辑(服务端),后者负责实现客户端逻辑。

模组加载完成后,引擎会默认加载Main脚本组内的脚本,即加载Mod/Script/Main目录下的Client.jsWorld.js脚本。

📘 脚本分配指引

在设计脚本时,应该做好世界脚本及客户端脚本的功能责任分配,避免出现过度相互依赖的情况,有助于后续的版本开发和日常维护。

常规情况下,应优先考虑世界脚本API来实现主要逻辑,需要客户端本地相关的内容,才在客户端脚本进行设计和实现。

正确举例:制作一个载具的进度条(或仪表盘),首先在客户端创建一个进度条GUI,然后创建一个更新函数,在客户端每帧进行更新,直接从客户端获取Vehicle.Speed进行进度条赋值,这样每帧更新且没有卡顿。

错误举例:同上,制作一个GUI进度条,在服务端判断玩家上车后,在帧循环事件中给玩家发送自定义客户端数据(ServerToClientData),参数附带Character.Vehicle.Speed,客户端收到后把参数赋值给进度条Progress。这样虽然能实现相同的数据效果,但进度条会因网络延迟等导致显示不流畅(虽然可以通过插值方式处理,但有些功能并不适合服务端开发,通常应该避免这种快速每帧、定时循环发送客户端数据的情况),并且造成了不必要的网络通讯开销浪费。

📘 功能开发的常规步骤

  1. 需求分析:确定想要实现的功能,对其进行简单的描述

    例如:想要设计和实现一个赛车的玩法,需要清楚规则是什么,路线是什么,哪些车参赛,人数怎么安排,比赛怎么进行等

  2. 设计:根据需求分析,在开发层面进行抽象、分类和归纳,总结出需要存在哪些数据,功能,流程

    例如:设计赛车的路线由一个个点连接组成,参赛者用列表统一管理,参赛车有什么属性、功能,人数限制的功能描述,什么时候在什么地方会发生什么事(流程)等

  3. 实现:用具体的代码和环境等来实现以上的设计(也就是编写代码)

    例如:赛车的路线用Checkpoint(检查点)来实现,参赛者用数组的方式实现存储,以及自定义的各种函数来实现具体的功能等等

  4. 调试:开发的最后一步,及时发现异常,确保项目能正常运行

    例如:从使用者角度,对各个功能进行实际的上手测试(黑盒测试);从开发者角度,对代码可能存在的隐患和边界条件进行实际的上手测试(白盒测试)

📒 脚本文件

沙盘引擎的所有模组脚本均在模组目录下Script/XXX目录。

其中入口脚本有两个,分别是World.jsClient.js

脚本 说明
World.js 主动入口脚本
负责一切与主世界相关的内容(服务端权威,如世界时间、天气、其他服务端及玩法类逻辑)
Client.js 主动入口脚本
负责一切与本地客户端相关的内容(如GUI、相机视角、本地事件、本地世界事件等)

特别注意:两种脚本的作用空间不是互通的,只在各自的代码空间生效。

除此之外,也有一些原生通用(Native)的代码API,此分类下的代码可以在两种脚本空间通用,但数据通常不互通。

📘 加载子脚本

实际开发过程中,特别是针对设定复杂的游戏,显然只有两个脚本文件是不利于开发者逻辑清晰的。

模组允许开发者使用API加载其他脚本,开发者可以根据情况自行分配多个子脚本,帮助开发者提高开发效率。

成功加载子脚本后,实际相当于将代码叠加在同一个作用空间,并不代表真正意义分割为“其他脚本”

当开发者需要使用子脚本的方法时,直接正常执行即可,不需要增加文件名前缀等。

LoadScript("newscript.js");
LoadScript("Func/Functions.js");

任何新建的子脚本必须放置在Script目录内(或子目录),放置后的子脚本必须通过API进行加载脚本,否则没有作用(抛砖引玉,可通过此机制实现模块化开发)。

📘 脚本加密

在 沙盘引擎 模组开发中,无论是世界脚本还是客户端脚本,都将被放置在模组目录中,因此模组的脚本源代码是透明的

由于JavaScript脚本的透明性,如果不希望别人查看自己的脚本,可以通过网络上的混淆、加密工具进行加密,但是务必确保源文件的备份,否则可能是不可逆的,建议只在确认打包之前进行一次混淆,本地测试阶段并不需要混淆和加密。

注意:由于 沙盘引擎 的联机自动同步机制,即使在开发过程中,也可能有玩家会连接到当前服务器,并且会自动下载最新的模组全部文件(包括脚本),因此如果不希望其他人进入,可以提前设置服务器密码或相关权限。

JavaScript在线加密工具:https://obfuscator.io/

混淆工具并不是完全不可逆的,只能起到加密和阻止完全修改的作用。***

📘 本体思维扩展

此部分可能包含过时内容,暂时仅供参考。

通过了解,沙盘引擎两个主要入口脚本Client.jsWorld.js分别有各自的激活场景和作用。

这其实也可以扩展出一些其他思维,比如你有版权或用户隐私方面的顾虑或其他打算,你完全可以进行C\S客户端和服务端的分别开发和打包。

也就是说,客户端如果没有“建立主机服务器”的需求,完全可以将World.js留空或删除,实现网游化的开发。

例如:可能开发者不想让任何人都能架设服务器,那么就只需将World.js连同工程单独拷贝出来一份,很简单的形成了单独的“服务端”

因为开发者发布给玩家的模组本体可以不包含世界脚本World.js),所以玩家无法建立服务器(客户端也不应该有开启服务器的UI接口),因此只能进行加入游戏,而加入游戏是可以不需要世界脚本的。

此开发思路只是针对某些特殊玩法网游服务器需求情况,如果是常规好友派对游戏情况下,还是建议将建立主机的能力附带给玩家。

📘 开放性利弊分析

由于JavaScript本身语言的透明性,无法进行二进制等真正意义的编译打包,尽管上面介绍了混淆加密工具的方式,但仍然会有小概率可能会被修改。

无论如何,最终版本进行混淆和加密或许是有必要的,否则可能会出现一些预期之外的情况。

例如:没有进行任何混淆和加密,可能脚本会被其他人参考(在某些情况下,这是有助于学习的),但也可能会被他人进行违背作者初衷的魔改。

举例:开发者制作了解谜小游戏,玩家直接修改其未加密的代码,添加了许多达到作弊效果的功能和指令,破坏了模组作者设计的初衷。

考虑到类似这样的情况,尽管官方会按照初版发布时间大众意愿评定来进行区分和保护正版模组,但如果开发者抛开混淆和加密仍然有困扰,可以考虑本文其他的扩展方法,综合判定是否要进行“独立服务端”的开发方式等。

📒 脚本框架及来源

沙盘引擎 游戏本体是基于Unity进行开发的,引擎中动态编译解析JavaScript脚本的功能来源于PuerTS框架(致敬)。

尽管引擎脚本是以JavaScript作为编译基础的,但引擎代码的设计思维来自于松鼠语言脚本(另外一个脚本语言)。

📒 脚本API文档指南

有关脚本API文档将被分成几个主要部分,分别是原生及通用代码客户端代码(Client)世界端代码(World)

公开范围内所有的Event(事件)、Property(属性)、Function(方法)等代码都将在以上三个文档内记录呈现。

文档中部分代码(类)实际上可能是静态类名,这里需要根据API文档内首字母大小写进行区分(例如:Timer是一个静态类,而timer则是一个生成的实例)。

Timer.Create() []
timer.Create() [×] //no instance function
 
Timer.Repeat == 0 [×] //No static property
timer.Repeat == 0 []

为了更好的理解API文档的内容规范,以下是文档的部分类型名词解释。

数据类型 说明
int 整数(50
float 浮点小数(1.01.1234
double 双精度浮点小数(1.00000001
string 字符串("Hello"
char 单个字符
bool | boolean 布尔逻辑型
any 通用类型,自动进行装箱\拆箱
虽然没有固定的限制,但是开发者应该确保数据类型正确

在本分类下的API文档中,您可能会看到类似TypeScript格式的代码样式,但通常并不能直接开箱即用,请使用最终JavaScript支持的标准格式。

例如:function OnPlayerJoin( player: Player ),此代码不是标准格式,仅用于表示类型、参数等信息。

实际应用时,应该是类似以下的代码(标准格式):

function OnPlayerJoin( player )
{
 
}

📘 脚本API文档标题

由于引擎主要功能是由WorldClient两个主类下的众多子类实现控制的,所以脚本文档采用了XXX 类参考的命名方式。

也就是说,例如查找关于世界脚本下的玩家方法,那么应该前往世界脚本——Player 类参考进行查找。

除此之外,每个类型参考文档内拥有多个H1、H2标题进行分类,通常会按照以下命名进行区分。

名称 说明
Event 事件入口分类
Static 静态功能分类
只能直接使用类名调用,如Character.Create()注意大小写区分)
Property 属性分类
通常为创建实例化类型后才可使用的属性数据
(例如Character.Pos是不能直接使用的,应该是var newChara = Character.Create(XXX);生成后newChara.Pos来使用)
Function 功能函数分类
通常为创建实例化类型后才可使用的功能函数
(例如Character.Disarm()是不能直接使用的,应该是var newChara = Character.Create(XXX);生成后newChara.Disarm()来使用)

📒 脚本编写及编译

沙盘引擎使用JavaSciprt作为开发脚本语言,所以开发者可通过任何文本编辑器进行脚本编写

脚本开发时并不需要专门的IDE以及编译器,只需要进行增删查改及修改后的保存“文本”即可,重新建立世界(重连、重载)时将会自动动态编译

📒 脚本开发基础入门

注意:此处指的基础入门不是JavaScript语言的基础教程,而是指引已有编程基础的开发者,快速进行了解脚本代码的一些差异和规范

如果你是一位编程初学者或JavaScript初学者,可以考虑参考以下视频教程

四十分钟JavaScript快速入门:https://www.bilibili.com/video/BV15L4y1a7or

哔哩哔哩JavaScript入门教程搜索:https://search.bilibili.com/all?keyword=javascript

《沙盘引擎》的开发初衷就是为了精简开发流程以及代码的复杂程度,所以实际上对模组脚本的基础开发并不需要非常过硬的编程技术。

如果开发者曾对PawnSquirrel其他编程语言有相关经验,甚至不需要完全学习JavaScript,只需掌握基础的语法使用即可开始体验。

📘 事件和函数

事件(Event):表示某件事情开始发生,客户端和世界端可能有不同的事件(也有部分是几乎一样的,可通用)

函数(Function):执行一段引擎内置的代码,以来实现某个功能(例如PlaySound API在某处执行播放声音的功能)

//此处表示事件:当玩家加入到服务器
function OnPlayerJoin( player )
{
	//以下是“事件”下的“函数”
	DLog(player.Name + " joined."); //DLog是一个引擎下内置的方法(SE.DLog)
    Message(player.Name + " joined server."); //Message就是一个World.Message函数
}

📘 默认类空间

为了脚本开发思路更加清晰,所以引擎采用了必要类名前缀的调用方式,实际上类似于命名空间的概念,。

脚本环境 默认类 示例
Client.js Client Client.Core.XXXXX()
World.js World World.Core.XXXXX()
通用(Native- CreateHost()
Random.Range()
Debug.Log()

注意:Client.jsWorld.js完全不同的两个程序空间,即使有些内容(如Player类)命名相同,但不代表在底层是基于同一个类。