Wednesday, June 29, 2011

Embedding Lua for scripting




最近 Lua 似乎挺火。它被設計爲一門輕量級的、可供嵌入到其它語言作爲膠水 (Gluing) 的語言。
一直很好奇這是怎麼做到的,於是仔細研究了一下,在網上發現了這篇文章,作者提供了一個 minimal 的源代碼。
閱讀完大概明瞭,但原來的程序有些 bug,而且有些小細節沒有說明清楚(比如,原文基於 Lua 5.0,但最新的 Lua 5.2 有 major changes)。
於是 fork 之修改完成,並記錄如下。

Disclaimer:
正如作者聲明,裏面的 C 以及 Lua 都沒有寫得非常優美或規範。
由於是做示範性用途,所以許多地方都採取了 quick & dirty 的方法,還有各種 magic number。
雖然在代碼裏沒有標明,程序版權屬於原作者。

Prerequisites:
OS X (測試環境,10.6.8 ) / Linux (原文提到支持,但未測試。不過沒道理不成功 :p )
Lua 5.2 & liblua:
- 在 OS X 下, 可以安裝 Lua.framework,我用的是自己編譯的版本。
- 在 linux 下,可以通過各種包管理工具直接安裝 lib 以及 interpreter。
注: LuaJIT 應該也可以,但現階段尚未更新至 5.2 compatible
pkg-config
SDL 1.2.14: OSX 可直接下載 SDL.framework,注意必須 copy sdl.dmg 裏面的 devel-lite 裏面的 SDLmain.{m,h},以把程序包裝成一個 Cocoa 程序。Linux 無此問題。
OpenGL

介紹及原理:
Python/Ruby/Lua 之類的語言都可以作爲嵌入式的語言使用,然而 Lua 以其簡潔的語法、支持多範式編程以及極小的庫體積(~100K),受到衆人的青睞。
它被大量運用在 Game Programming 裏,比如魔獸世界(WoW)就是用它來做各種 extension 的。
這些動態語言比起編譯型語言的主要好處,就是更加抽象,更加高層次,支持 Functional programming, 有各種語法糖衣輔助等寫出 human-readable 的簡潔代碼。
不足之處在於,本質上它們的運行都需要經過一層中間層—— Virtual Machine 來與系統底層進行交互。所以效率往往比起 C/C++ 等要低。
所以人們想出了一個好方法:主要的程序邏輯使用 scripting language 編寫,而一些要求高性能或者底層的代碼段,則還是由編譯語言完成。
這裏有一份更詳細的介紹文檔。示意圖來自該文檔。



程序流程:
程序本身應該是一個編譯型語言生成的程序,在裏面提供了各種基礎設施,如繪製圖形等。
在程序裏面會鏈接到腳本語言的庫,然後主要的工作在腳本語言裏面完成,比如定義各種 Object,
它們之間的 interaction 等。這裏可以看到另外一個好處:一些經常需要改動的值,我們不需要
寫在C/C++裏,因此不需要重複編譯程序,而只需要把它們保存在腳本文件裏即可方便讀出。
因此腳本語言也可以當做配置文件使用。

The Pong Game (這個遊戲不用介紹了吧?):



讓我們來看看具體是怎麼實現的。在這裏採用了 C+Lua 的組合。
在 C 程序裏,定義了如何繪製榘形 (draw_rectangle),以及處理方向鍵的函數(由 SDL 提供)。
以上的函數都被 register 到 Lua 裏面(具體來說,是程序運行后生成的 Lua VM instance)。
而 C 裏也可以通過 Lua 提供的 API 調用 Lua VM 定義的函數。
在這裏,是一個名爲 "pulse" 的函數 (好吧, 老外覺得這個詞很直觀形象, 請深入理解...)
在初始化好 SDL 的一些咚咚後, 程序進入 main loop.
如果寫過 OpenGL 或者 Win32 之類的採用事件模型 (Event model)的程序, 就知道這個
main loop 拿來做什麼的. 基本上, 這個 main loop 就是監聽外部輸入 (這裏是鍵盤),
每次循環會間隔一定時間, 並調用 Lua 的 pulse 函數進行畫面的更新.

在 Lua script 裏, 定義了兩個類, 分別是作爲板子的 paddle 以及球 ball.
當程序運行后, Lua 會創建兩個 paddle 以及 一個 ball 並更新它們的位置.
如果有按鍵事件, 則根據它更新 paddle 的位置; 同時也進行碰撞檢測.

簡單來說:
(in C) main loop -> call Lua pulse -> (in Lua) crash detection, drawing (call C's draw_rectangle) -> (in C) main loop ...
That's all!

Note: 原來版本的程序裏, 如果 ball 飛出了 y 方向, 則它真的飛了出去...
於是我稍微改了一下腳本, 增加了 y 方向的邊緣碰撞檢測.
注意, 這裏完全沒有改 C 程序代碼或編譯, 而是直接修改腳本即可.
可以修改的地方還有很多, 比如改變 paddle 的大小, 球的速度, 電腦 AI 等.
另外, x 方向上的碰撞檢測也沒有完成 :)

我適當地在源代碼裏加上了一些註釋, 需要知道得更清楚的, 可以 RTFSC.

No comments: