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 | 被當成一個陳述式輸出 | 編譯錯誤 |
一個書寫上的失誤,以前會偽裝成一個引擎的錯誤。以 84 個示範專案的樣本來衡量,這個防護機制產生了零個誤判,並找出兩個真實存在的無聲失效:
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,以及溢位環繞行為。 - 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、native/wasm 和 debug/release 的明確設定檔。它會在引入檔案處理之前被解析,在每個目標平台上皆是如此。
豐富的函式功能:預設值、OPTIONAL、一個放在最後的 PARAMARRAY(或
...)、安全的參數個數多載、有型別的回傳值轉換,以及記錄和陣列回傳值的複製語意。
多檔案程式#
#INCLUDE、#INSERT 和 IncludeFile 可以把一個程式拆成多個檔案。編輯器能建立與匯入專案模組,補全引入路徑,維護依賴關係圖,且每個執行目標都已針對它接好線。
協程,以及一個刻意省略的東西#
協作式的任務是以標籤為基礎的,帶有型別化的控制代碼、結果與錯誤狀態、可在yield 點取消,並可等待單一任務或一組任務。它們在每個目標平台上都共用同一個遊戲排程器。
具有共享記憶體的執行緒是刻意排除在外的。 不是「還沒做」,而是排除。一個你能推理清楚的協作式排程器,勝過一個你無法重現的資料競爭問題。
WebGPU 運算#
型別化的 f32 / i32 / u32 儲存緩衝區、原始的 WGSL 管線、依影格排序的分派,以及記憶塊讀回,是貨真價實的 GPU 運算,直接從 BASIC 呼叫。
⚠️ 僅限 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) ` 家族是從建立指令「推導」出來的
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 一個家族,這個分析只讀取陳述式層級的宣告與賦值。這裡特別寫明,是因為它是一個真實存在的限制,不是需要你去找的錯誤。
⚠️ 目錄在屬性上省略了動詞,但在生命週期指令上保留了它。
SpritePosition 用來寫入,SpriteX 用來讀取,兩者都不帶 Set 或 Get:整個目錄裡以 Set 開頭的指令只有4 個,以 Get 開頭的一個也沒有。動詞只留在有東西被建立或結束的地方:
CreateSprite、LoadImage、DeleteSprite。
這值得知道,是因為一個拼錯字會是編譯錯誤,而 Set… 正是大家最常拼錯的那種寫法。目錄本身才是權威依據,參考文件正是從它產生出來的。
字串內插,選擇加入#
Print($"score: {points}") ` "score: " + Str(points)
{{ 和 }} 用來寫出字面上的大括號。
⚠️ $ 前綴是必要的,而且不是裝飾用的。 內插每一個字串,曾經破壞了手寫的 JSON,而世界 API 經常就是這樣使用字串:
CreateTerrain("island", "{\"preset\":\"island\",\"size\":256}")
在那裡,大括號是資料,不是內插的洞。一個沒有加前綴的字串,無論內容是什麼,都會被原封不動地保留。
命名即是顯示#
一個經典的 BASIC 程式會建立成百上千個物件。只有被「命名」的物件才會進入 Scene Graph,SpriteName(id, "player") 以及它的同類指令,正是把一個物件放進場景樹裡(讓引擎其他部分看得到它)的方法。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 不支援匯出。 在你依賴它之前,先寫好備援路徑。