BASIC 语言
aukimi 的经典 BASIC 方言:类型、结构、两种简写形式,以及那些谁都会踩到的规则。背后是 2 237 个命令。
一句话概括#
aukimi 自带一种经典 BASIC 方言:是一门完整的语言,而不是一个玩具级的宏系统,带有类型化的数值、记录、方法、引用、结构化错误、协程和 WebGPU 计算。它存在于 Engine 的 TXT 视口模式里,也存在于脚本编辑器的 BASIC 视图里。
JavaScript 依然是标准的脚本 API。 BASIC 是一个可选的前端:两者都会编译成同一个私有的分发器,所以一段 BASIC 程序和一段 JavaScript 程序,触及的是完全相同的引擎。没有什么是二等公民,也没有什么是另一套独立的运行时。
那条谁都会踩到的规则#
一次命令调用永远带着括号。
mode = ClipSpaceMode()
SpritePosition(player, 320, 180)
这不是风格问题。正是它让编译器能区分一个命令和一个变量,也正是这一点,让一次拼写错误现在能在编译期就被发现:
| 你写的 | 以前会发生什么 | 现在会发生什么 |
|---|---|---|
SpritePositon(1, 10, 20) | 编译顺利通过,精灵没有移动,故障之后才以 ... is not defined 的形式浮现出来 | 编译报错,并提示目录里最接近的拼写 |
Blorp(1, 2) | 原样转发进生成出来的 JavaScript | 编译报错 |
ceci nest pas du basic | 被当作一条语句输出 | 编译报错 |
一个书写上的失误,以前会以一个引擎的 bug 的样子出现。基于 84 个 demo 组成的语料库测量,这道防护产生了零误报,并发现了两个真实存在的静默失败:
SetMusicSystemVolume和GetLightSensorExists,而目录里对应的正确名字分别是MusicSystemVolume和LightSensorExists。
有两个漏洞是刻意留着不管的:console.log("x") 和 window.x = 1 依然能通过。曾经为它们写过一道防护,也做过测量,但最终撤回了:BASIC 运行在一个上下文里,这个上下文提供了编译器无法穷举的名字,所以拒绝一个未知的根名会误伤合法的程序。在这里,一个误报比这个漏洞本身更糟。
Print 是绘制。Console 是记录。#
最常见的一个意外:
Print(...) | 在屏幕上绘制,一个程序会在每一帧都调用它 |
Console(...) | 写入编辑器的控制台,除此之外什么都不做 |
Debug(...) | 用 PureBasic 的名字实现的同一个命令,照顾大家的肌肉记忆 |
ConsoleWarning、ConsoleError、ClearConsole | 日志功能的其余部分 |
如果在一个逐帧循环里用 Print,控制台里的任何真实报错都会被淹没,这正是这两者被设计成两个独立命令、而不是靠一个开关来切换的原因。
类型#
数值类型是真实存在的类型,在每一个目标平台上行为都完全一致:编辑器、HTML 导出,以及 Bevy/原生构建。
- 有符号和无符号的 8、16、32 和 64 位整数,具有确定性的类型转换、整数
DIV,以及**回绕(wrapping)**式的溢出处理。 - float32 和 float64。
- 经典的类型后缀符号依然有效:
name$是字符串,speed#是浮点数。
OPTION EXPLICIT 是可选启用的,经典的隐式变量依然是默认行为,所以导入的老程序照样能跑。启用它,是另外两个功能能够安全工作的前提条件,也就是数值类型化和引用,因为一次拼写错误导致悄悄创建出一个新变量,会让这两者都失效。
结构#
记录(Records)#
TYPE 用来声明一个记录。它们可以任意嵌套,并且拥有值语义:赋值即深拷贝,包括嵌套的数组和集合。
方法(Methods)#
METHOD 把一个函数绑定到一个实例上,函数内部可以用 SELF,用 New 来构造,用 Delete 来销毁。深拷贝之后方法依然可以安全使用。
引用(References)#
REF 和 BYREF 是同一个东西的两种写法,两者都会给出一个实时读写的参数,可以是一个标量、一个记录、一个单独字段、一个数组、一个列表游标或一个映射条目。引用会随调用一路传递,并在边界处强制转换成固定宽度的类型。
句柄(Handles)#
@target 会取得一个一等公民的引用。HANDLE 和 POINTER 是它们的类型化形式,带有经过检查的读取、写入、有效性判断和释放,指向一个已删除记录的句柄会被判定为失效,而不是被留成悬空指针。
控制与安全#
结构化错误:TRY / CATCH / FINALLY,可以嵌套,配有 THROW、受保护的 RETHROW,以及结构化和字符串两种捕获方式。
条件编译:#IF / #ELSEIF / #ELSE / #ENDIF,可以嵌套,带有特性标记,以及针对编辑器、HTML、Bevy、原生/wasm 和 debug/release 的显式配置档案。它会在包含文件展开之前被解析,在每一个目标平台上都是如此。
丰富的函数特性:默认值、OPTIONAL、放在最后的 PARAMARRAY(或 ...)、安全的元数重载、类型化的返回值转换,以及会被克隆的记录和数组返回值。
多文件程序#
#INCLUDE、#INSERT 和 IncludeFile 让一个程序可以拆成多个文件。编辑器可以创建和导入项目模块,为 include 路径提供补全,维护依赖关系图,并且每一个运行时目标都对此做了适配。
协程,以及一个刻意省略的东西#
协作式任务是基于标签的,带有类型化的句柄、一个结果和错误状态、在让出点才会发生的取消,以及针对单个任务或一组任务的等待。它们在每一个目标平台上都共享同一个游戏调度器。
带共享内存的线程是被刻意排除在外的。 不是"暂时还没做",而是排除在外。一个你能推理清楚的协作式调度器,胜过一个你无法复现的数据竞争。
WebGPU 计算#
类型化的 f32 / i32 / u32 存储缓冲区,原始的 WGSL 管线,按帧排序的调度,以及内存块回读,这是从 BASIC 里发起的真正的 GPU 计算。
⚠️ 仅限 Engine。 HTML 目标平台会抛出一个明确的能力错误,而 Bevy 目标平台会在构建之前就报告这个不兼容问题,而不是产出一个在第一次调度时才失败的二进制文件。
两种简写形式,解决同一个问题#
597 个命令都叫做 Set/Get<对象><属性>:Sprite 66 个、Tween 58 个、Text 32 个、Edit 29 个。对象的名字就藏在函数名里,它的 id 是每一行的第一个参数。下面这两种简写都能去掉这种重复,而且都不是新的 API:编译器会重新构造出真正的命令名,再去目录里查找它。
在一个代码块内使用 WITH <family>#
WITH SPRITE s
\position 100, 200 ` SpritePosition(s, 100, 200)
\size 64, 64 ` SpriteSize(s, 64, 64)
x# = \x ` SpriteX(s)
ENDWITH
在一条语句的开头,\prop 表示写入;出现在其他位置则表示读取。由位置决定,所以不需要再学一套新的写法。WITH hero,单单一个词,依然是那个记录代码块;WITH SPRITE s 则是在选择目录里的一个族。
在其他任何地方使用 s.position(...)#
s = CreateSprite(img) ` the family is DEDUCED from the creation command
s.position(100, 200) ` SpritePosition(s, 100, 200)
s.visible = 1 ` SpriteVisible(s, 1)
x# = s.x ` SpriteX(s)
这个族来自 Create<F> / Load<F> / Clone<F>,或者在 id 是从别处传进来时,来自一个显式的 s AS Sprite 声明。只有那些族已经明确可知的变量才会被改写:记录保留自己的字段,列表保留自己的方法,SELF.field 则完全不受影响。
⚠️ 一个带类型的参数不会被识别。 FUNCTION move(s AS Sprite) 不会让 s 获得一个族,这道处理只读取语句级别的声明和赋值。这里特意说明,是因为这是一个真实存在的限制,而不是一个需要你去追查的 bug。
⚠️ 目录在属性上去掉了动词,却在生命周期命令上保留了它。 SpritePosition 用于写入,SpriteX 用于读取,两者都不带 Set 或 Get:整个目录里,只有4 个命令以 Set 开头,而没有任何一个以 Get 开头。动词只保留在事物被创建或结束的地方:CreateSprite、LoadImage、DeleteSprite。
这一点值得了解,因为一次拼写错误是编译期报错,而 Set… 正是大家最常犯的那种拼写错误。目录才是权威来源,参考手册 就是从它生成出来的。
字符串插值,需要主动开启#
Print($"score: {points}") ` "score: " + Str(points)
{{ 和 }} 用来写出字面的花括号。
⚠️ $ 前缀是强制性的,而且不是装饰。 让每一个字符串字面量都参与插值,会破坏手写的 JSON,而这正是 world API 日常要用到的东西:
CreateTerrain("island", "{\"preset\":\"island\",\"size\":256}")
在这里,花括号是数据,不是插值占位符。一个没有加前缀的字符串,不管里面装了什么,都会被原样保留。
命名即显现#
一个经典程序会创建成百上千个对象。只有被命名过的那些才会进入 Scene Graph,SpriteName(id, "player") 以及它的同族命令,正是把一个对象放进那棵树、让 Engine 的其余部分能看见它的方式。LightName 和
ParticlesName 对一盏灯和一个发射器做的是同一件事。
教程为你的程序创建的对象命名会带你走一遍这个过程:同一个场景,一次是空空如也的树,一次是里面有三条记录的树。
命令目录#
2 237 个命令,2 550 个公开重载,43 个类别。 最大的几个:
| 3D | 286 | Studio | 106 | File | 61 |
| Core | 200 | Input | 95 | Memblock | 43 |
| Tweening | 149 | Input-Raw | 94 | 3D Particles | 41 |
| 3D Physics | 144 | Multiplayer | 91 | World | 40 |
| Sprite | 135 | Text | 68 | Particles | 38 |
| Platform | 111 | 2D Physics | 66 | Sound | 37 |
外加 JSON(36)、Image(35)、Skeleton(34)、XML(34)、Music(29)、 Preference(27)、Extras(21)、HTTP(21)、String(21)、Math(19)、 Regular Expression(19)、Video(18)、Dictionary(17)、Benchmarking(15)、 Compute(14)、Date(13)、Maths(12)、Time(12)、StringBuilder(9)、 Compression(7)、Font(8)、Error(4)、Array(3)、Cipher(2)、Sort(2)。
Studio 和 Compute 是 aukimi 自己给这门经典方言添加的东西:控制台命令、对象命名,以及 WebGPU 管线。
完整命令参考 列出了所有这些命令,每一个参数、每一种重载,并带有自己的搜索功能,它是从这个页面所统计的同一份目录里生成出来的,所以不会与之脱节。编辑器自己的 API 面板读取的也是同一份目录。
这个页面记录的是这门语言本身;那一个页面记录的是这些命令。
限制与常见坑#
- 命令的括号不是可选的。 那种裸写的 PureBasic 形式(
Debug "x")会被拒绝,因为它会重新打开变量和命令之间的歧义,而编译期的这道防护正依赖于消除这种歧义。 - 一个用户函数会盖过同名的目录命令,而在一个类型化声明里、在一次赋值的左侧、或作为路径的根,一个变量也可以和某个命令同名。
- 变量和用户函数大小写不敏感,它们的符号空间与目录的符号空间是真正分开的。
Print不是日志。 见上文;这正是那个会把控制台填满的常见错误。- Compute 不支持导出。 在依赖它之前,先写好回退路径。