小話遊戲指令碼(一)
( 題記:近來在網上學習到一個新的觀點(應該是來自劉未鵬的BLOG ):書寫是為了更好的學習,這與之前腦子裡傳道授業解惑的觀點頗為迥異,品一品又頗以為然,事物不都是兩面的嗎,這隻是看待角度的不同罷了,所以後來想想,確實應該將自己的一些學習經曆或是思想記下,不僅有益於自身的提高,說不定以後還能協助協助他人,如此這般好事,何樂而不為?(看來我是在漸漸脫離火星了,想想以前我可是強烈無視網上論壇的...) )
一.一點已知的零星知識
談到遊戲指令碼這個論題,就我目前所知大抵同編譯原理是一個概念,有所不同的可能是編譯原理大多不涉及虛擬機器,而遊戲指令碼為了靈活性和安全性的考慮,虛擬機器往往是必須考慮的範疇
對於編譯原理,我也曾經選修過這門課程,記得當時老師一直自歎該門課程的枯燥性,以致一路的上課過程都那麼無聊乏味,待到考試將至,老師似乎也是擔心我們的學分大計(估計是怕我們拖累他的職稱評比),所以臨考前發了幾套類比題下來,甚至到了後來還幫我們進一步劃清了範圍,唉,良苦用心,天地可鑒啊!不過對於我來說實在沒有什麼驚喜,該怎麼考還是怎麼考,所以對於這次名義上的考試沒有什麼太多的印象,畢竟經曆這類的事情太多了些...不過對於編譯原理的課程設計,我到有些體會,待到有機會下面再敘,現在我必須停止我這番情不自禁的感慨牢騷,回到我們的遊戲指令碼(編譯原理)。
首先講講我所知道的書籍,曾經我在圖書館淘過這類的書籍,也花費過一些時間google,所以在此推薦三本書籍,不過在下實在笨拙,這三本書都未曾精讀過,但這並不妨礙我的推銷,他們就是在編譯界分別擁有龍書、虎書及鯨書之稱的《編譯原理》、《現代編譯原理》和《進階編譯器設計與實現》。感覺上龍書注重編譯器前端,鯨書則更著眼於編譯器後端,而虎書則是一本實踐性質的書籍,個人感覺應該先以虎書的講解一步步的實踐深入,中間輔以龍書的理論支援,待到熟稔之境,觀略鯨書又能助以更上一層樓:)(個人看法...)
一般來講,編譯分為以下幾個步驟:
.詞法分析
.文法分析
.語義分析
.中間代碼產生
.中間代碼最佳化
.目標代碼產生
一般來講,以中間代碼為界,之前被稱為編譯器的前端,之後則被稱為編譯器的後端,理論上來講,除了目標代碼產生 ,其他各部分都不是必須的,但是將他們一部分一部分的分解開來,自有其深遠用意,首先由 詞法分析 將輸入的源檔案中各種形式的單詞提取成統一規格的屬性塊,稱為 Token,然後 文法分析 通過這些 Token 檢查他們是否符合文法規則,並同時填充符號表以及建立文法分析樹,然後由語義分析 進一步檢查其是否符合相應的語義規則,一切安檢通過之後,則開始產生中間代碼,並在之後對產生的中間代碼進行最佳化,最終由最佳化後的目標代碼產生可執行檔目標代碼。(更加詳細一些的資訊可參見燕良很久以前翻譯過的一篇文章)
對於一些簡單的語言,語義分析以及 中間代碼最佳化 往往會有所省略,而實際上,在文法分析的同時我們其實也可以直接產生目標代碼,但這樣做的後果是失去靈活性,試想如果將來我們因為跨平台的目標而變更目標代碼的格式,只要我們保有中間代碼的產生,便可以保持編譯器前端不變,否則就...
另外對於虛擬機器,一般用於運行那些自訂格式的指令碼代碼,目標自然是做到真正的平台無關,可惜需要付出運行速度的代價。(想想Java語言已經發展的十分成熟,但較之C/C++的運行速度,其仍然難以望其項背...)一般而言,虛擬機器生命週期中需要完成裝載、執行以及關閉的操作,考慮進一步的細節,裝載時自然需要一個指令碼裝載器,執行時自然少不了運行時堆棧而關閉則定然需要一個資源管理員等等,好了,關於虛擬機器的論題就此打住,在談論下去恐怕要貽笑大方了,下面的一個樣本雖說沒有嚴格的實現一個虛擬機器原型,但期中也有那麼一些影子,希望大家能夠參考指正
在此我想敘述的是一個十分簡單的指令碼系統,其甚至都不是面向過程的,而只能稱其為基於命令(對於這個議題,以前有幾篇文章)。而所謂基於命令,便是指令碼系統僅僅支援你調用系統固定的API(命令),而不允許你進行其他類型的任何操作(例如運算式運算),可想而知這種系統的靈活性和實用性...但本著KISS原則以及自己學習實現的需要,在此就僅對於該話題展開一些討論,其中的大多數內容都來自於《遊戲指令碼進階編程》,自己僅作轉述而已,但感覺仍然樂在其中
1.基於命令指令碼的基礎知識
即使對於一些複雜的遊戲,遊戲中的許多功能也可以通過一系列順序的動作進行完成,例如考慮以下的一段基於命令的指令碼代碼:
ShowBitmap "Image/Item0.bmp"
LoadMusic "Music/Cheer.mid"
FadeMusicIn
GetItem "Gold Of Apple"
GameMessage "Oh,Yeah!You Got The Gold Of Apple!"
這段代碼十分簡單,所要完成的功能也非常明了,於此我們便可以看出基於命令的指令碼的一大優點:形式簡單;同時也暴露了其的一大缺點:功能單一,同時我們也能大抵能夠看出這類指令碼命令的一般形式:
Command Param0 Param1 Param2 ...
一般來講,以上的代碼格式已經能夠滿足基於命令指令碼的需求,但同時,如果要加上如 C/C++ 那般的括弧、分號之類的文法格式自然也是可以的,這僅僅是解析上的問題
在者,基於命令的指令碼與特定的領域高度相關,就如上面所示的代碼可能很適用於RPG之類的遊戲,但是對于飛行類比或者體育競技之流,恐怕是無能為力了...
至此,你可能對於基於命令的指令碼語言嗤之以鼻,也可能對其開始寵愛有加,但是總結來看,事物終歸是有兩面性的,對於定義那些遊戲引擎將要執行的具有固定順序的事件,基於命令的指令碼的確不錯,因為他快捷而又方便,但是其他更加複雜的應用,如最終Boss的AI,他可能就力不從心了,所以對於其的選取與否,仍然還是一個適用範圍問題,存在著權衡。
(基於命令的指令碼有一個很好的範例,那就是很早以前金點的《聖劍英雄傳2》)
2.基於命令指令碼的進階知識
對於一門語言,無論他多麼簡單,首先仍然要解決他的資料及文法問題,首先讓我們來談談資料類型,一般來講,對於基於命令的指令碼必須支援整型 以及 字串型 的資料,前者用以表達各類參數資訊,而後者往往代表遊戲中的各種文字,但實際上,只要我們做一些簡單的擴充,基於命令的指令碼同樣可以支援 布爾型以及 浮點型 的資料,具體的做法可以參見後面部分的設計實現樣本,同時我們也可以加入對於常量的支援,至於加入常量的好處,我想已經不用在此贅述了 接著便是指令碼的文法,一般來講,基於命令的指令碼語言都由一條條的命令組成,命令之間也都是順序執行,但實際上,為了增加指令碼的靈活性,我們可以加入一些簡單的迴圈和分支,具體做法仍可參考設計實現樣本
最後讓我們談一談指令檔的載入與執行,論及指令檔的載入時,我們首先遇到的第一個問題便是預先處理的問題,在基於命令的指令碼中,如果我們決定支援 Include 操作,那麼我們就必須對指令碼進行預先處理,但是這其中存在一些問題,我們留於後面討論,在者便是是否需要對代碼進行“編譯”,你可能會奇怪為何會有“編譯”一說,實際上按照我們的普通想法,基於命令的指令碼系統的運行可能像是這副樣子(忽略一些無關的細節):
首先開啟指令檔,解析指令名稱,指令參數,然後根據解析後的指令以及參數執行相應的程式,接著繼續解析指令,如此迴圈往複,直至指令碼結束。
沒錯,這是我們想到的最普通直觀的方法,但是這期間卻存在一些問題,首先便是執行速度問題,眾所周知,動態解析字串是一個非常緩慢的過程,如果我們以上述的解析操作來運行指令碼,必不可少的會花費相當多的CPU時間,但實際上,如果我們將文本化的指令檔在運行前編譯成某種我們定義好的二進位格式,則將大大加快指令檔的執行速度,並且同時我們額外獲得了至少兩點好處:一是我們可以更加方便的發現指令碼中的錯誤,而不像先前邊解析邊執行的方式,可能引發災難性的後果;二是我們也進一步保證了指令碼的安全,相比之前可讀形式的指令檔,我們大可以在編寫正確的指令碼之後,僅提供對應的編譯版本,而無需將指令碼內容暴露在光天化日之下 所以,既然先行編譯有如此的好處,那麼就不要猶豫,讓我們的指令碼義無反顧的支援編譯吧