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 個示範專案的樣本來衡量,這個防護機制產生了零個誤判,並找出兩個真實存在的無聲失效:SetMusicSystemVolumeGetLightSensorExists,這兩個名字,目錄裡實際登記的是 MusicSystemVolumeLightSensorExists

有兩個漏洞是刻意留著的:console.log("x")window.x = 1 依然能通過。曾經有人為它們寫過一個防護機制,經過測試後又撤回了:BASIC 是在一個會提供編譯器無法窮舉的名稱的環境裡執行的,所以拒絕一個未知的根物件,會連帶拒絕掉合法的程式。在這裡,一個誤判比放行那個漏洞更糟。

最常見的一個誤解:

Print(...)畫在螢幕上,程式每一幀都會呼叫它
Console(...)寫進編輯器的主控台,除此之外什麼都不做
Debug(...)沿用 PureBasic 的名稱,讓有肌肉記憶的人也能用
ConsoleWarningConsoleErrorClearConsole主控台記錄的其餘部分

如果在一個每幀迴圈裡呼叫 Print,會把任何真正的錯誤淹沒在主控台裡,這正是為什麼這兩者是分開的兩個指令,而不是同一個指令加一個開關。

型別#

數值型別是真實存在的,並且在每個目標平台上行為完全一致:編輯器、HTML 匯出,以及 Bevy/原生組建。

  • 有號與無號的 8、16、32 和 64 位元整數,具有確定性的型別轉換、整數 DIV,以及溢位環繞行為。
  • float32 和 float64。
  • 經典的字尾符號依然有效:name$ 是字串,speed# 是浮點數。

OPTION EXPLICIT 是選擇加入的,傳統的隱式變數依然是預設值,讓匯入的舊程式依然能執行。開啟它,是使用另外兩個必須靠它才能安全的功能的前提,也就是數值型別化和參照,因為一個拼錯字的賦值悄悄建立出一個新變數,會讓這兩者同時失效。

結構#

記錄(Records)#

TYPE 用來宣告一筆記錄。它們可以任意巢狀,並具有值語意:賦值時會做深拷貝,包含巢狀的陣列與集合。

方法(Methods)#

METHOD 把一個函式綁定到一個實例上,內部可用 SELFNew 用來建構、Delete 用來銷毀。深拷貝之後方法依然安全可用。

參照(References)#

REFBYREF 是同一件事的兩種寫法,兩者都會給出一個活動的讀寫參數,可以是純量、記錄、單一欄位、陣列、清單游標,或映射的一個條目。參照會沿著呼叫鏈往下傳遞,並在邊界處轉換成固定寬度的型別。

控制代碼(Handles)#

@target 取得一個一級參照。HANDLEPOINTER 是有型別的形式,具有經過檢查的讀取、寫入、有效性判斷和釋放操作,指向一筆已刪除記錄的控制代碼會被判定失效,而不是留在懸空狀態。

控制與安全#

結構化錯誤處理TRY / CATCH / FINALLY,可巢狀,搭配 THROW、 受保護的 RETHROW,以及結構化和字串兩種捕捉方式。

條件式編譯#IF / #ELSEIF / #ELSE / #ENDIF,可巢狀,帶有功能旗標,以及針對編輯器、HTML、Bevy、native/wasm 和 debug/release 的明確設定檔。它會在引入檔案處理之前被解析,在每個目標平台上皆是如此。

豐富的函式功能:預設值、OPTIONAL、一個放在最後的 PARAMARRAY(或 ...)、安全的參數個數多載、有型別的回傳值轉換,以及記錄和陣列回傳值的複製語意。

多檔案程式#

#INCLUDE#INSERTIncludeFile 可以把一個程式拆成多個檔案。編輯器能建立與匯入專案模組,補全引入路徑,維護依賴關係圖,且每個執行目標都已針對它接好線。

協程,以及一個刻意省略的東西#

協作式的任務是以標籤為基礎的,帶有型別化的控制代碼、結果與錯誤狀態、可在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 用來讀取,兩者都不帶 SetGet:整個目錄裡以 Set 開頭的指令只有4 個,以 Get 開頭的一個也沒有。動詞只留在有東西被建立或結束的地方: CreateSpriteLoadImageDeleteSprite

這值得知道,是因為一個拼錯字會是編譯錯誤,而 Set… 正是大家最常拼錯的那種寫法。目錄本身才是權威依據,參考文件正是從它產生出來的。

字串內插,選擇加入#

Print($"score: {points}")        ` "score: " + Str(points)

{{}} 用來寫出字面上的大括號。

⚠️ $ 前綴是必要的,而且不是裝飾用的。 內插每一個字串,曾經破壞了手寫的 JSON,而世界 API 經常就是這樣使用字串:

CreateTerrain("island", "{\"preset\":\"island\",\"size\":256}")

在那裡,大括號是資料,不是內插的洞。一個沒有加前綴的字串,無論內容是什麼,都會被原封不動地保留。

命名即是顯示#

一個經典的 BASIC 程式會建立成百上千個物件。只有被「命名」的物件才會進入 Scene GraphSpriteName(id, "player") 以及它的同類指令,正是把一個物件放進場景樹裡(讓引擎其他部分看得到它)的方法。LightNameParticlesName 對一盞燈和一個發射器做的是同一件事。

教學為你的程式所建立的物件命名會帶你走過一遍:同一個場景,一次是空的場景樹,一次則有三筆項目。

指令目錄#

2 237 個指令,2 550 個公開多載,43 個類別。 最大的幾個:

3D286Studio106File61
Core200Input95Memblock43
Tweening149Input-Raw943D Particles41
3D Physics144Multiplayer91World40
Sprite135Text68Particles38
Platform1112D Physics66Sound37

外加 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)。

StudioCompute 是 Aukimi 對這個經典方言自己的擴充:主控台指令、物件命名,以及 WebGPU 管線。

完整的指令參考文件 列出所有指令,附帶每個參數和每個多載,還有自己的搜尋功能,並從這一頁計算所依據的同一份目錄產生,因此不可能落後於它。編輯器自己的 API 面板讀取的也是同一份目錄。

這一頁講的是這個語言本身;那一頁講的是各個指令

限制與常見問題#

  • 指令的括號不是可有可無的。 PureBasic 那種不帶括號的寫法(Debug "x")會被拒絕,因為那會重新打開變數與指令之間的歧義,而編譯期的防護機制正是仰賴消除這個歧義才能運作。
  • 使用者自訂函式的優先權會蓋過同名的目錄指令,而一個變數也可以在型別宣告中、賦值式左側,或路徑的根部,與某個指令共用名稱。
  • 變數和使用者自訂函式不區分大小寫,而且它們的符號空間與目錄的符號空間是真正各自獨立的。
  • Print 不是記錄工具。 見上方說明;它正是塞爆主控台的元凶。
  • Compute 不支援匯出。 在你依賴它之前,先寫好備援路徑。