? RPG制作之路
???????????????????????????????????? 天眼
一、緒論
??? RPG是角色扮演游戲的英文縮寫,它是紙上冒險游戲和電腦結合的產(chǎn)物,自從誕生起就以獨特的
冒險和故事性吸引了無數(shù)的玩家。我個人認為,RPG游戲是各類游戲中最能表述故事,體現(xiàn)情感的一
種游戲。RPG游戲不但自身在不斷發(fā)展著,而且還不斷吸取著其他游戲的精華,如加入冒險類的解密
情節(jié)(大多數(shù)RPG游戲都有),加入動作游戲的戰(zhàn)斗方式(暗黑破壞神),引入戰(zhàn)棋類游戲的策略戰(zhàn)
斗部分(金庸群俠傳),隨著3D技術的發(fā)展,優(yōu)秀的三維RPG游戲也在不斷的涌現(xiàn)(如魔法門六)。
??? 然而這些優(yōu)秀的游戲并不是憑空產(chǎn)生的,它同其他軟件類產(chǎn)品一樣,它也是由許多的程序人員編
制出來的,而且它的產(chǎn)生還離不開策劃人員、美工和音樂制作人員協(xié)同的努力工作。RPG游戲的靈魂
是劇情,然而它卻和其他類型游戲不同,它的特別強的故事性使得游戲的美工、音樂幾乎同樣重要。
然而卻也不能因為重視它們而忽視了劇情,因為這三者互相失衡而導致失敗的游戲的確不在少數(shù)。
??? 我非常喜歡編程序,看著計算機在自己的程序控制下說一是一,說二是二,讓它向南它不敢往北
,哪怕是完成了一個非常小的程序,自己也會有一種成功的喜悅。當完成一個久未解決的難題時,真
想跳起來振臂高呼(我也真的這么干過,被人罵作"神經(jīng)")。相信真正沉浸其中的人都會有此感覺
!
??? 自從我剛剛接觸電腦起,我就同樣接觸了游戲,雖然那只是在APPLE-II上的非常簡單的游戲,我
就幾乎沉迷了。沉迷的當然不是玩這些游戲,而是自己編這些游戲。那時我真的很驚訝,原來可以通
過自己的努力,讓電腦給其他人提供這么吸引人的娛樂服務(那時我一直以為電腦是來進行科學計算
的)。雖然我那時在蘋果機上編制的游戲簡直沒有娛樂性,但我還是很看重那一段時光,因為我在那
時不但認識到了電腦的超凡的功能和極端廣泛的應用范圍,而且還為我打下了程序設計的基礎。
??? 這些年來,我自己編的游戲大大小小也有十來個,雖然真正的成品很少,但我還是努力嘗試了游
戲的各種類型,包括射擊、經(jīng)營模擬、即時策略、戰(zhàn)棋,當然最多的還是RPG游戲。??? 下面我就結
合自己的經(jīng)驗,講解一下RPG游戲制作的具體方法和步驟,獻給那些有程序設計經(jīng)驗且想自己制作游
戲的玩家。所有這些都是我個人的實踐和思考的結果,可能會有許多不對的地方,歡迎大家批評指正
。
二、策劃部分
1.? 策劃的必要性
??? 有句老話叫"未雨而綢繆",是說應當為即將發(fā)生的事做好準備,在這里可以理解為即將開始的
游戲制作做好準備工作。因為即使是一個很小的游戲或者是軟件,也有很多獨立的方面需要完成。如
標題畫面,操作界面,和系統(tǒng)有關的設置甚至還有安裝等很多方面。如果事先不經(jīng)過計劃,在制作的
過程中必然會不斷發(fā)現(xiàn)沒有考慮到或者考慮不周全的地方,如果這時在進行改動,你的程序、美工和
音樂大概都得跟著改,有時甚至會導致整個工作重來。這樣先前的許多工作都變成了白白的時間浪費
。我以前也走過這樣一段彎路,總是一開始就坐在計算機前開始編程序,并且不斷發(fā)現(xiàn)某些方面沒法
實現(xiàn),不得不修改以前的代碼,有時還因為一個重要的方法技術上沒有辦法實現(xiàn),被迫放棄制作。過
了一段時間后又突然解決了這個問題,再想拾起以前的工作繼續(xù)下去,幾乎已經(jīng)不可能了。經(jīng)過幾番
掙扎,我終于認識到了策劃的重要性,現(xiàn)在,無論是做什么東西,我總是"三思而后行",有時我還
會提前編一些小程序驗證某些方法的可行性,不會再盲目開始了。
??? 相信大部分有的程序設計經(jīng)驗的玩家都會同意我的看法,我之所以說這些話也是為了讓大家少走
些彎路,更快的掌握制作游戲的方法。
2. 劇情的策劃
??? 很多游戲制作者將詳細的劇情策劃放在第一步,我對此有不同的看法。
??? 劇情的策劃固然重要,因為引人入勝的劇情往往是RPG游戲制作的關鍵,然而理想化的劇情策劃
經(jīng)常為程序設計制造難題,程序雖然可以不斷完善,但出于技術和面向玩家階層的考慮,程序并不是
萬能的,策劃者卻往往不清楚程序描述劇情的能力達到到什么程度,當程序能力有所不及,就得重新
修改策劃了!這是所有人都不愿看到的。
將劇情策劃放在程序設計之后更是不可能的,因為程序設計者將無所指導,他不知道自己的程序
需要達到什么程度!
想到這里,我們不僅想到了在C語言程序中兩個互相調用函數(shù)的情況,誰先誰后呢?那時,解決
的方法是函數(shù)原形,將相對基礎的函數(shù)的函數(shù)名、輸入輸出參數(shù)以函數(shù)原形的方式寫在所有調用它的
函數(shù)之前。同樣我們可以將相對基礎的"劇情"的大體框架和對程序的要求放在工作的第一步,在程
序設計完成以后在填充它的具體細節(jié)。在程序的基礎上完成具體的劇情細節(jié),這樣就能成分發(fā)揮程序
的表述能力了!
3. 劇情框架
??? 應當主要描述劇情的時代背景,環(huán)境氛圍,畫面風格,事件的表述方法。至于具體的故事細節(jié),
等到程序完成以后在進行設計也未嘗不可。
??? 對于改編電影、小說的游戲,劇情已定,首先所需要的就是決定是否需要照搬原著的劇情?很重
要的一點是電影、小說有自己獨到的表現(xiàn)方法,有些對于游戲是值得借用的,但并不表示要完全借用
。如果完全借用,那和電影、小說本身還有什么區(qū)別呢,我相信大多數(shù)人是不喜歡VCD般的游戲和電
子小說般的游戲的。
4. 決定程序設計的基礎
???? (1) 這一部分主要是指游戲的圖形模式(分辨率、顏色深度),以及采用的特殊效果、音樂、
音效和動畫的支持。由于本人對3D編程涉獵不多,所以主要以2D為主。
???? 圖形模式由顯示卡決定,由顯示器支持,一般游戲常用的顯示模式有
320 X 200 X 8 bits
??? 640 X 480 X 8 bits
??? 800 X 600 X 8 bits
?? 1024 X 768 X 8 bits
??? 640 X 480 X 16 bits
??? 800 X 480 X 16 bits
?? 1024 X 768 X 16 bits? (需顯存2M)
??? 640 X 480 X 24 bits
??? 800 X 600 X 24 bits? (需顯存2M)
?? 1024 X 768 X 24 bits? (需顯存4M)
??????? 未注明的顯存1M即可?
??? 現(xiàn)在的顯示器大都支持640X480、800X600、1024X768這三種分辨率,而顯示卡對顏色深度的支持
與顯存有關:
?????????????????????????????????????????? 顏色深度(bit)
?? 所須顯存數(shù)量=橫坐標分辨率X縱坐標分辨率X-------------
??????????????????????????????????????????????? 8
??????? 顏色深度(bit)
????? 顏色數(shù) = 2????
????????????????????????????? 8
??????? 如顏色深度8 bit即? 2??? =? 256 色
???? 有些顯示卡支持32bit或者更高的顏色深度,但24bit的顏色深度就已經(jīng)使人眼分辨顏色的能力
達到極限,這些更高的顏色深度主要用于高質量的圖象和視頻設計。
?????? 注:256色的調色機理和16bit、24bit的不同,現(xiàn)在的編程語言大都已經(jīng)封裝了對顯示卡的控
制,制作者不用了解其中的原理就可以很方便的使用。
??? 分辨率越大、顏色數(shù)更多,圖形的表現(xiàn)自然更好,但對顯存、內(nèi)存和數(shù)據(jù)傳輸?shù)男枨缶驮礁?,圖
形數(shù)據(jù)文件也就越大??紤]到大多數(shù)電腦玩家的顯卡都至少有1M顯存,因此640x480x8bit、
800x600x8bit、640x480x16bit、800x600x16bit、640x480x24bit成了大多數(shù)游戲通用的分辨率,我
在制作游戲時也主要考慮這幾種分辨率。需要較快的屏幕顯示速度可以采用256色,需要光影等特殊
效果的最好采用16bit或24bit的顏色數(shù),這些需要依具體情況定。至于分辨率,可以單獨使用一種,
也可以同時支持幾種,因為對于各種編程軟件來說,這似乎并不是什么難題!
(2) 圖塊類型的選擇
??? 大部分的RPG游戲的圖形都是由數(shù)層圖案逐層覆蓋而成,比如最底的一層為地表圖案如草地、石
子路等等;次一層為地物,包括其中行走的主角和NPC的圖案;再高一層可以為覆蓋于前一層的物體
之上的東西,如屋頂、樹梢等等;如果需要的話,還可以再在上面加入表示天氣一層,如白云、雨水
什么的。當這些圖層重疊起來以后,上面一層鏤空的地方露出下面的一層,這樣就形成了"人在地上
走,云在人上飄"的層次高度效果。當然你也可以制定自己的圖層,實現(xiàn)所需的特殊效果。
??? 游戲的每一個圖層可以由多種方法實現(xiàn):
???? ——單張圖片構成
???????? 非常容易發(fā)揮美術效果,適合做地表,天氣效果,但所需的內(nèi)存和硬盤空間著實不小。
???? ——多張不規(guī)則圖片構成
???????? 美術效果不錯,適合做為地物層,但當需要構成遮擋效果時,計算較為麻煩 。
???? ——多張規(guī)則圖片組成
???????? 最節(jié)省內(nèi)存和硬盤空間,適合做任何層次,遮擋也非常容易處理,但對美工的要求比較高
。
???? 大多數(shù)的RPG游戲還是采用最后一種,因為技術上最為簡單(用二維數(shù)組就可以描述),所以我
也比較傾向于這種,主要講講它。
???? 規(guī)則圖案組成層次也有幾種:
????? ——矩形(出于對各個方向卷軸速度一致的考慮,多為正方型)圖片拼接而成,本層內(nèi)的圖片
不存在互相遮擋的關系,只需從左到右,從上到下依次畫出即可。只能創(chuàng)建出頂視或非常假的無遮擋
3D效果。(如《俠客英雄傳》、早期的《勇者斗惡龍》系列)
????? ——等邊六角型圖片拼接,就象蜂巢,本層內(nèi)圖片不存在遮擋關系。這種方式多用在戰(zhàn)棋類游
戲中,或RPG游戲的戰(zhàn)斗部分,在行走地圖中不常用(因為得有六個行走方向,象是《英雄無敵》系
列)。
????? ——矩形圖片逐行覆蓋而成,這種游戲的圖片按著從左到右,從上到下依次畫出,但每一行的
圖片都覆蓋前一行少許,但一個圖片只覆蓋本列的圖片。這樣的話,就可以理解為近處的物體遮擋了
遠處東西,就是屏幕上部離玩家最遠,下部離玩家最最近。產(chǎn)生一定的立體效果。(如《俠客英雄傳3》、《新蜀山劍俠》和《劍俠情緣》)
????? ——矩形圖片交錯覆蓋,每一行不但覆蓋前一行,而且橫向偏移一段距離。這樣就產(chǎn)生了視角
傾斜的效果,正方型的底面在這種視角中呈菱形。這種效果比上一種方法能產(chǎn)生更好的立體感覺,美
工好的話可以以假亂真。(如《仙劍奇?zhèn)b傳》、《金庸群俠傳》和《暗黑破壞神》等)
???? 在這一部分很難用言語描述,我只講各種類型,以后制作HTML版,再加入示意圖片。大家只要
先有一個概念就行,
至于具體的數(shù)據(jù)描述和繪制方法我會在以后的程序部分講解。
(3)? 構成游戲的基本要素。
???? 單線還是多線劇情?
???? 單線劇情的和多線劇情對于程序員的工作來說,差別不大。但對于策劃人員來說,卻決不相同
。多線劇情相對于單線擁有數(shù)倍的工作量,而且大量的分支很容易搞亂。但多線劇情的可玩性十分誘
人,越來越多的制作者開始采用它。如果你決定使用這種方式,一定要做好策劃,管好所有的資料數(shù)
據(jù)。(我的建議是專門編制一個管理多分支劇情的(信息、文本)編輯器,這并不時小題大做,因為
是你完全可以在編制下一個游戲時再次使用它。如果要求不太高的情況下,使用HTML語言是一種節(jié)省
效率的方法。)
???? 哪些人物屬性?
???? 人物屬性在RPG游戲中往往可以決定戰(zhàn)斗、升級和劇情,是玩家非常在意的東西,因此對人物屬
性的設定也要多化些工夫。擁有較多的屬性可以產(chǎn)生更多的趣味性,你不必擔心占用內(nèi)存的問題,因
為這跟圖象數(shù)據(jù)的內(nèi)存需求比起來簡直微不足道。但切記不要強硬的添加無用或是作用不大的屬性。
???? 什么樣的戰(zhàn)斗方式?
???? 戰(zhàn)斗是RPG游戲中的重頭戲,目前RPG游戲的戰(zhàn)斗方式有以下幾種:
??? ——敵我交替攻擊的回合制:主要為早期的RPG游戲所采用,代表如《仙劍奇?zhèn)b傳》
??? ——敵我同時攻擊的回合制:為《劍俠情緣》中對前一種戰(zhàn)斗方法的改進,意義不大。
??? ——戰(zhàn)棋式的回合制:最有代表的是《金庸群俠傳》
??? ——即時戰(zhàn)斗:就在行走地圖上展開的戰(zhàn)斗,如《圣光島》和《暗黑破壞神》
??? ——格斗游戲方式:這是國內(nèi)的晶合公司的在其游戲《毀滅天地》中的獨創(chuàng)。(其實我也這么想
過,就是沒有做過?。?br />??? 前兩種戰(zhàn)斗的實現(xiàn)方式較為簡單,后三種則相當于融入了另外一種游戲的概念,所以制作起來比
較困難,不過也很有挑戰(zhàn)性,很可能會吸引更多的玩家。
??? 什么樣的升級系統(tǒng)?
??? 升級系統(tǒng)模仿了人類的成長,隨著能力的提高去面對更加強大的敵人,也是RPG游戲所特有的魅
力,但近來也被不少其他類型的游戲(如即時策略和動作類)所吸收。作為傳統(tǒng)的RPG當然更不能少
。目前各類RPG游戲的升級系統(tǒng)很單一,也是目前者最有有發(fā)揮潛力地方。在這里,我講一些自己的
設想和看法。
?????? ——通過戰(zhàn)斗獲取經(jīng)驗值來升級,由等級數(shù)、資質之類的人物的屬性來決定升級所需的經(jīng)驗
值,等級的提升又提高人物的某些屬性,提高人物的戰(zhàn)斗力。這也是最基本的升級規(guī)律,只需要制定
一些公式就可以實現(xiàn),當然記著還要包含一定隨機性(使用偽隨機函數(shù))。
?????? ——吸取MUD的優(yōu)點,設置一些如"根骨"、"敏捷"、"悟性"等多項資質屬性,對角色各
項屬性的成長影響各自不同,通過劇情可以修改這些基本參數(shù),創(chuàng)建出多種不同的角色成長方式。
?????? ——對于偏重于戰(zhàn)斗的RPG游戲,設置較多"全局"角色,即在整個游戲過程都存在,他們擁
有同主角一致的升級系統(tǒng),采用半隨機(由人物參數(shù)、隨機值和劇情共同決定)獲取經(jīng)驗值。這樣就
可以產(chǎn)生即時性的人物成長,當玩家游手好閑時,對手卻在不斷成長,給喜歡戰(zhàn)斗的玩家以壓力。(
如果只通過戰(zhàn)斗來取得游戲的勝利未免有練功機器之嫌,應當提供多種成功的樂趣,不一定非要通過
戰(zhàn)斗)
??? 需要特別注意得是:這些跟升級有關的公式和數(shù)據(jù)需要仔細記錄,還需要便于后期修改。因為這
些東西直接決定著戰(zhàn)斗和升級,和游戲的可玩性息息相關,加上很難以估算和決定,為了保證可玩性
,需要在后期花大量的時間修改完善。如果每改動一次都需要重新編譯程序,那任何人都會受不了的
!(我通常的做法是使用文本格式的腳本描述語言)
??? 當你完全決定了所有上面這些時,就可以開始真正的程序工作了!當然你可能發(fā)現(xiàn)漏掉了一些細
節(jié),但它們的作用不是很關鍵,我在程序部分再講。
三、程序設計
??? 從這一章起,我開始講RPG游戲程序部分的具體實現(xiàn),涉及代碼的部分我主要采用自然描述語
言和類似C的描述語言。相信有一定程序設計基礎的人都能看懂!
1.腳本描述語言
(1) 什么是腳本描述語言?為什么要用它?
??? 玩過很多RPG游戲,打不過去的地方經(jīng)常用PCT改,有時候偶然發(fā)現(xiàn)游戲的某個文件是文本
文件,仔細閱讀發(fā)現(xiàn)竟然象是某種語言程序,不是匯編,不是PASCAL,也不是C,這究竟是什么呢?
??? 這其實是游戲制作者自己定義的一種腳本描述語言,制作者一般使用它來簡化劇情的設計工作。
為什么它能簡化劇情的設計呢,當我們了解了腳本描述語言本身后,再來說明這一點。
??? 腳本描述語言是一種解釋語言,解釋語言不同于編譯語言之處就在于它在執(zhí)行之前不需要編譯成
機器代碼,而是通解釋機將語句解釋成機器代碼再執(zhí)行。它的優(yōu)點在于源程序可以方便快捷的修改,
因為不需要修改解釋器,所以也不需要進行編譯。腳本描述語言就是針對一種某特殊平臺的描述再這
個平臺上特定行為和動作的一種解釋語言。游戲的腳本描述語言就是建立游戲本身這個平臺上,描述
游戲中的情節(jié)事件的解釋語言。
??? 為什么要用腳本描述語言呢?因為RPG游戲的劇情往往是一個龐大而充滿相互聯(lián)系的故事群,
每個故事可能又由許多小事件組成,這復復雜雜的關系就如同我們設計的程序,函數(shù)就象事件,事件
可以包含事件,函數(shù)也可以包含函數(shù),劇情中我們可以通過各種選擇實現(xiàn)不同的分支情節(jié),程序中我
們也可以通過條件分支來執(zhí)行不同的程序段。這樣的相似性必然會讓我們想到用程序語言來描述劇情
。然而如果我們在直接在游戲程序中用"IF""THEN"之類的語句來描述劇情,這樣必然使劇
情依附于程序,劇情的改動也就意味著程序的改動,在RPG游戲的制作中,這顯然是不可行的。有
了上面對腳本描述語言的了解,我們不禁會這樣想:如果用腳本描述語言呢?在游戲程序中加入解釋
機,專門解釋執(zhí)行描述劇情的腳本語言。這樣在改動腳本時,自然就不用改動程序了!如此一來,我
們修改劇情時,根本不用在意游戲程序本身,只需要一個簡單的文本編輯器就行了,如此帶來的工作
效率,相信不用我說大家也了解了!
(2)腳本描述語言的語法關鍵字和函數(shù)
??? 腳本描述語言最基本的單位是語句,它應當具備最基本語法,如表達式求值(包含各種常用的運
算),無條件轉向,條件分支,循環(huán),子函數(shù)等。變量有用戶自定義數(shù)據(jù)也有游戲的全局數(shù)據(jù);而描
述稍微復雜一些的功能可以采用全局函數(shù),這些全局函數(shù)就象C語言的庫函數(shù)或者是WINDOWS
的API一樣,和各種變量一起在表達式中引用或者作為其他函數(shù)的參數(shù)。
??? 下面是我制作的RPG游戲的一個事件腳本文件。
// 示例事件1
say(1,165,"大家好!我是張斌,這是我做的第十七個游戲。")
say(11,30,"這是一個測試!用來檢驗我的游戲引擎。")
say(32,300,"你好!在我這里可以點播MIDI樂曲。")
choose_flag=choose(4,"請選擇樂曲:","樂曲一?樂曲二?樂曲三?樂曲四")
midi(choose_flag)
say(36,30,"小子,你來找死!")
push
s0=fight(7)
if(s0==1) goto(WIN)
msg("你打輸了了!")
gameover=1
:WIN
pop
msg("你打贏了!")
end
??? 這個事件的編號在地圖裝入時賦值給了一個NPC,當主角接觸到這個NPC時,這個事件被觸
發(fā),于是這個文件被讀入內(nèi)存,開始解釋執(zhí)行。我們逐行解釋這個文件:
第一行? 前面的"http://"同C++語言一樣表示注釋一行。
第二行? 是一個函數(shù),名稱是 SAY ,有三個參數(shù),前兩個是整數(shù)1和165,第三個是字符"大家好……
"。這個函數(shù)的意義是在屏幕的縱坐標165的位置上顯示人物1(頭像和姓名)的語言"大家好…"
第三行? 同上,在屏幕縱坐標30的位置顯示人物11的話"這是一個測試……"
第四行? 同上,在屏幕縱坐標300的位置顯示人物32的語言"你…"
第五行? choose是一個選擇函數(shù),在屏幕上出現(xiàn)"請選擇樂曲"的信息和4項選擇,分別是"樂曲1","樂曲2","樂曲3","樂曲4",當玩家選擇一個并按下回車后,
玩家選擇的選項號碼(0表示第一個
選項,1表示第二個,依次類推)作為這個函數(shù)的返回值賦給變量choose_flag。
第六行? midi是一個播放MIDI音樂的函數(shù),它的唯一參數(shù)就是樂曲號,我們可以看到它的參數(shù)是變量
choose_flag,這就表示根據(jù)choose_flag中的值來選取播放的樂曲,而choose_flag中恰恰就放的是
在前一語句中我們選擇的號碼,因此midi函數(shù)就會播放我們前面選擇的樂曲。
第七行? 仍然是人物語言顯示
第八句? 因為要進入戰(zhàn)斗場景,所以用push函數(shù)將當前場景的參數(shù)保存。待戰(zhàn)斗結束后可以再用pop
函數(shù)取出,借此恢復戰(zhàn)斗前的場景。
第九句? fight是戰(zhàn)斗事件函數(shù),參數(shù)表示戰(zhàn)斗事件的號碼,這里表示第7號戰(zhàn)斗事件。戰(zhàn)斗的結果
(0表示輸1表示贏)賦值給變量s0
第十句? if語句(也可以理解為函數(shù))對裝著戰(zhàn)斗結果的標量s0進行判斷,如果s0為1(戰(zhàn)斗勝利)
,則執(zhí)行后面的goto函數(shù),跳轉到標號為WIN的語句(就是":WIN"那一行),否則繼續(xù)執(zhí)行下面的語
句。
第十一句(s0==0,戰(zhàn)斗失敗)msg函數(shù)表示再屏幕上顯示信息"你打輸了!"
第十二句 給變量gameover賦值為1,處理這個腳本事件的解釋器檢測到此變量為1,就終止事件然后
結束游戲。
第十三句 為作為跳轉語句目標行
第十四句 pop函數(shù)彈出戰(zhàn)斗前的場景信息,恢復場景
第十五句 msg顯示信息"你打贏了!"
第十六句 end函數(shù)表示事件結束
??? 事件結束后,腳本解釋器會釋放這段腳本占用的內(nèi)存。
??? 腳本中的"gameover"對應這個游戲程序中的一個全局變量"gameover",由于使用了指針,在腳本
中對它的引用就等同于對游戲程序中"gameover"的引用。同樣對應于游戲程序中的其他全局變量,也
可以通過自己定義的腳本變量來引用。如地圖大小"mapx"和"mapy",當前主角位置"cx","cy"等等,
這樣就可以直在腳本語言中引用它們。如if(cx==5&&cy==7)判斷主角是否在地圖(5,7)這個位置上
if(cx==mapx-1&&cy==mapy-1)?? 判斷主角是否在地圖的角上這段腳本中"say","msg","choose",
"fight","midi"都是描述游戲中情節(jié)的函數(shù),在游戲程序中都有相應的函數(shù)與之對應,它們有些有
返回值,有些沒有。這些返回值還可以用來構成表達式,如:
midi(choose(3,"請選擇樂曲:","樂曲一?樂曲二?樂曲三")+1)
??? 這個條語句的含義就成了選擇"樂曲一"的時候,實際放樂曲二,選擇"樂曲二"的時候放樂曲三,
選擇"樂曲三"的時候放樂曲四。
??? 上面那段腳本中的"if","goto"可以被理解為控制語句也可以被理解成函數(shù)。所有的控制語句函
數(shù)化后,可以使腳本程序程序的格式更加統(tǒng)一,便于閱讀。
??? 同樣對數(shù)組的引用也可以函數(shù)化,如對地圖(X,Y)位置的圖案類型的賦值在游戲程序中為
map[x][y]=12,是一個標準的數(shù)組元素,而在腳本程序中的引用則成了map(x,y)=12,x,y成了函數(shù)
map的兩個參數(shù)。雖然形式上是函數(shù),但實際的使用中仍然等同于變量map[x][y](因為程序內(nèi)部使用
的是指針),因此可以進行賦值運算。
??? 下面再看一段腳本文件
//示例事件2
say(12,300,"公子,你要買我的寶劍嗎?")
say(1,30,"這寶劍多少錢?")
say(12,300,"30兩銀子!")
say(1,30,"這也太貴了!")
say(12,300,"30兩也嫌貴,這劍可是削豆腐如泥哦!")
say(1,30,"讓我考慮一下!")
choose_flag=choose(2,"買嗎?","買了 不買")
if(choose_flag==1) goto(NoBuy)
if(haveobj(1)<30)? goto(NoMoney)
msg("你花30兩買下了這把破劍!")
say(12,300,"您走好!")
addobj(1,-30)
end
:NoBuy
say(12,300,"小氣鬼,30兩也不肯出!")
end
:NoMoney
say(12,300,"真是個窮光蛋,快滾!")
end
第一句 仍然是注釋
第二句 到第七句是主角(1)和賣劍的(12)人物對話
第八句 是選擇"買"還是"不買"
第九句 如果選擇了不買,跳轉到:NoBuy
第十句 haveobj對應游戲程序中的函數(shù)haveobj,參數(shù)是物品的種類返回值是擁有這種物品的數(shù)量(
物品1表示銀子,銀子的數(shù)量就是兩數(shù))這一句是判斷主角擁有銀子的數(shù)量如果小于30兩,則跳轉
到NoMoney
第十一句 顯示買下劍的信息
第十二句 賣劍的招呼你走好
第十三句 addobj函數(shù)表示為主角增加物品,第一個參數(shù)為物品的種類(1為銀子),第二個參數(shù)為
增加的數(shù)量(為負則是減少)
第十四句 事件結束
第十五句 不想買劍,跳轉到這里
第十六句 賣劍的罵你小氣鬼
第十七句 事件結束
第十八句 想賣劍但錢不夠,跳轉到這里
第十九句 賣劍的讓你滾蛋
第二十句 事件結束
??? 通過上面這兩段腳本語言文件,我們可以清楚的了解到腳本語言的的變量對應著游戲程序中的全
局變量、函數(shù)對應著游戲程序中的函數(shù),通過腳本語言,我們可以輕易的引用游戲中的各項值(主角
屬性,物品,地圖等等),引用游戲中的表述方法(人物對話,播放音樂,旁白信息等等)。如此以
來我們構建劇情不就輕而易舉了嗎?
??? 然而,我們不能高興的太早了,因為真正艱辛的,才剛剛開始!我們下一次將開始講如何構件腳
本的解釋機!
(3)解釋機的編程
??? 在任何編程語言中,表達式計算都是最重要的,它在我們的腳本描述語言中同樣存在。因此在腳
本解釋機中,我們最先實現(xiàn)的就是表達式求值。我們在寫源程序的時候進行計算非常簡單:
??? 如? 3*(3+2)-4
??? 然而在腳本描述語言中,解釋機所得到的并不是如此簡單的算式,而是一個從文本文件中提取的
一個字符串 "3*(3+2)-4"將這個字符串轉化成一個數(shù),可不是那么簡單。如果你認真學習過算法,應
該能夠從容的實現(xiàn)它。
??? 我們先看一個簡單一點的算式? "32+41*50-2"
??? 我們把自己看成是計算機,從字符串的左邊開始掃描,當掃描到'3'時,可以知道這是一個數(shù)的
開始,將它記如入一個空閑的字符串的第一位buf[0],當我們掃描到'2',它仍然是個數(shù)字,是我們
正在記錄這個數(shù)的新的一位,我們將它放入buf[1],當我們掃描到'+'時,它不是數(shù)字了,我們也就
知道第一個數(shù)讀完了,在記錄它的字符串的下一個位置buf[2]放入字符串結束標志'0'我們得到的這
個數(shù)放在buf中,是"32",通過一些編程系統(tǒng)提供的字符串轉整型數(shù)的函數(shù),我們可以將這個字符串
轉化為數(shù)值,即使你用的編程系統(tǒng)沒有這個函數(shù),根據(jù)數(shù)字字符的ASCII碼值,我們自己也可以
很容易實現(xiàn):
?? '0',"1","2"...'9'的ASCII碼值分別為 48~57,如果一個數(shù)字字符的ASCII碼為n,則
它代表的數(shù)字值為n-48。如一個數(shù)char str[5]={"2341"},它的值就可以寫成
(str[0]-48)*1000+(str[1]-48)*100+(str[2]-48)*10+str[3]-48于是我們可以寫出下面的將字符串
轉變?yōu)檎麛?shù)的C語言函數(shù)(其他語言也類似)
int stoi(char *str)???????? //str是以0為結束的字符串
{
int return_value=0,i=0;
while(str{i}?。剑埃?????????? //字符串結束
?? return_value=return_value*10+str[i++]-48;
return(return_value);
}
??? 知道了這個"32"是32,我們將它記下(裝入一個變量),再繼續(xù)往下掃描,直到字符'4',我們
知道我們得到了一個"+",它是一個二元運算符號,我們接著向下掃描利用上面掃描32的方法,我們
得到了另一個數(shù)41,我們現(xiàn)在知道了32+41,考慮一下,我們在什么情況下能將它們相加:
??? 要么是在41后字符串結束,要么緊接著41的是一個比'+'優(yōu)先級低或相等的運算符,如'-'。將它
們相加后得到73,我們就回到了剛剛得到31的那一步。
??? 如果41后面跟著的是一個更高優(yōu)先級的運算符,如本例中的'*',我們必須先進行這個乘法運算
,那只好先將31和'+'保存起來,接著向下掃描,我們又得到了50和其后的運算符'-',判斷的方法和
剛才一樣,因為'*'比'-'的優(yōu)先級高,所以我們可以放心的先將41*50算出,得到2050。這時我們現(xiàn)
在所掃描到的算式就成了32+2050-2,我們再次比較運算符'+'和'-',優(yōu)先級相同,我們就可以先算
32+2050了,得到2082,我們繼續(xù)向后掃描,了解到在2082-2后字符串結束,我們可以繼續(xù)計算最后
一步2082-2=2080,最后這個字符串表達式的結果就是2080。
??? 現(xiàn)在我們再來看看括號:如? 3*(3==2+2*5)-4這個式子,在讀完3*之后我們讀到得不是一個數(shù),
而是一個括號,這時我們需要先算括號內(nèi)的式子,所以的先將3和*保存起來,比較==和+,得先計算
加法,先將3==保存,再來比較+和*,先計算2*5得到10,因為下面一個等到算完2*5得到10,因為后
面是括號,所以要取出先前保存的數(shù)和運算符進行計算,但一直要取到前一個括號,但我們順序存了
3*、3==、和2+,怎么知道前一個括號在那里呢?方法是在遇到前括號時也保存括號的標記,這樣的
話,我們算到這一步時所保存的順序為:3*,(,3==和2+,我們遇到一個后括號,就取出以前保存的
數(shù)進行運算,先算2+10得12,再算3==12得0,這時取出了括號(,我們這才知道這個括號內(nèi)得運算完
結,現(xiàn)在的算式剩下了3*0-4,再比較*和-,先算3*0得0,最后得結果就是0-4得-4。
??? 在上面的運算中,我們在遇到高優(yōu)先級的運算時,需要將前面的數(shù)值和運算符保存,但我們并不
太清楚需要保存幾次,如這兩個算式:
??? 1=2==3+4*5??? 1+2+3+4+5
??? 它們在計算過程中需要保存的的數(shù)和運算符個數(shù)是不同的,前一個需要先算4*5結果為20,再算
3+20結果為23,再算2==23結果為0,再算1=0(在一般的語言中,象這樣給常數(shù)賦值是禁止的,但在
我們腳本語言的運算中,為了保持一致性,我們允許這樣的式子,但賦值被忽略),最多情況下需要
要保存三個數(shù)和運算符。而后一個式子一個也不用保存,從左到右依次運算就行了。
??? 我們發(fā)現(xiàn)這些數(shù)和運算符的使用都是"先存的后用,后存的先用",這不是堆棧嗎?對!我們就
用堆棧來保存它們。
??? 堆棧的實現(xiàn)很多軟件書中都已經(jīng)講過,心中有數(shù)的讀者自然可以跳過下面這一段。
??? 一般實現(xiàn)堆??梢杂面湵砘驍?shù)組,我們犯不上用鏈表這么復雜的數(shù)據(jù)結構,用比較簡單的數(shù)組就
可以了。對于使用C++的朋友可以用類的形式來實現(xiàn)
class STACK???????????? //整數(shù)堆棧
{
??? int *stack;???????? //存放數(shù)據(jù)的首地址
??? int p;????????????? //堆棧位置指示(也可以用指針)
??? int total;????????? //預先定義的堆棧大小
? public:
??? STACK(int no);????? //指定堆棧大小的構造函數(shù)
??? ~STACK();?????????? //析構函數(shù)
??? int push(int n);??? //壓棧
??? int pop(int *n);??? //出棧
};
STACK::STACK(int no)
{
? total=no;
? stack=new int [total];
? p=0;
}
STACK::~STACK()
{
? delete[] stack;
}
int STACK::push(int n)?? //壓棧
{
? if(p>total-1)
???? return(0);
? else
??? stack[p++]=n;
? return(1);
}
int STACK::pop(int *n)??? //出棧
{
? if(p<1)
??? return(0);
? else
??? *n=stack[--p];
? return(1);
}
??? 如果用C也是一樣,使用initSTACK來聲明一個堆棧,但要記著在用完之后調用freeSTACK釋放內(nèi)
存
typdef struct STACK
{
?? int *stack;???????? //存放數(shù)據(jù)的首地址
?? int p;????????????? //堆棧位置指示(也可以用指針)
?? int total;????????? //預先定義的堆棧大小
};
int initSTACK(struct STACK *stk,int no)
{
? stk=(struct STACK *)malloc(sizeof(STACK));
? stk->total=no;
? stk->p=0;
? stk->stack=new int [total];
//如果stack不為零表示分配成功,堆棧初始化也就成功
? if(stk->stack)
??? return(1);
? free(stk);???? //如果失敗釋放內(nèi)存
? return(0);
}
void freeSTACK(struct STACK *stk)
{
? if(stk)
?? {
??? delete[] stk->stack;
??? free(stk);
?? }
}
int pushSTACK(struct STACK *stk,int n)?? //壓棧
{
? if(stk->p>stk->total-1)
???? return(0);
? else
??? stk->stack[stk->p++]=n;
? return(1);
}
int popSTACK(struct STACK *stk,int *n)??? //出棧
{
? if(stk->p<1)
??? return(0);
? else
??? *n=stk->stack[--p];
? return(1);
}
??? 可以看出這種堆棧類在聲明對象時要給出堆棧的大小,對于我們的表達式求值來說,100個單
元足夠了。但有人不禁會想到,上面這些都是整數(shù)堆棧,對于運符怎么存儲呢?其實是一樣的,我們
可以給運算符編上用整數(shù)序號來代表,這樣就可以利用整數(shù)堆棧來保存了。給運算符編號的另一個好
處是可以利用運它的高位來代表運算符的優(yōu)先級!如下面一個函數(shù)將字符串運算符轉化成含優(yōu)先級的
序號,只要比較這些序號高位值的大小就可以得出誰得優(yōu)先級高了。(下面這個函數(shù)只對二元運算符
編號,沒有處理一元和多元,因為它們都可以用二元運算表示。)
int convert_mark(char *str)
{
//優(yōu)先級高
? if(strcmp(str,"*")==0) return(240);?? //0xf0
? if(strcmp(str,"/")==0) return(241);?? //0xf1
? if(strcmp(str,"%")==0) return(242);?? //0xf2
? if(strcmp(str,"+")==0) return(224);?? //0xe0
? if(strcmp(str,"-")==0) return(225);?? //0xe1
? if(strcmp(str,"<<")==0) return(208);? //0xd0
? if(strcmp(str,">>")==0) return(209);? //0xd1
? if(strcmp(str,"<")==0) return(192);?? //0xc0
? if(strcmp(str,"<=")==0) return(193);? //0xc1
? if(strcmp(str,">")==0) return(194);?? //0xc2
? if(strcmp(str,">=")==0) return(195);? //0xc3
? if(strcmp(str,"==")==0) return(176);? //0xb0
? if(strcmp(str,"!=")==0) return(177);? //0xb1
? if(strcmp(str,"&")==0) return(160);?? //0xa0
? if(strcmp(str,"^")==0) return(144);?? //0x90
? if(strcmp(str,"|")==0) return(128);?? //0x80
? if(strcmp(str,"&&")==0) return(112);? //0x70
? if(strcmp(str,"||")==0) return(96);?? //0x60
? if(strcmp(str,"=")==0) return(80);??? //0x50
? if(strcmp(str,"+=")==0) return(81);?? //0x51
? if(strcmp(str,"-=")==0) return(82);?? //0x52
? if(strcmp(str,"*=")==0) return(83);?? //0x53
? if(strcmp(str,"/=")==0) return(84);?? //0x54
? if(strcmp(str,"%=")==0) return(85);?? //0x55
? if(strcmp(str,">>=")==0) return(86);? //0x56
? if(strcmp(str,"<<=")==0) return(87);? //0x57
? if(strcmp(str,"&=")==0) return(88);?? //0x58
? if(strcmp(str,"^=")==0) return(89);?? //0x59
? if(strcmp(str,"|=")==0) return(90);?? //0x5a
//優(yōu)先級低
}
??? 在RPG得腳本描述語言中,我們基本用不上小數(shù),因此我們在實際的二元運算中得到的將是三
個整數(shù),其中兩個是參與運算的數(shù),另一個是運算符的序號,我們還得對此編出進行運算的函數(shù)。如
:
//運算求值 n1是第一個參加運算得數(shù),n2是運算符號得序號
//n3是第二個參加運算的值
int quest(int n1,int n2,int n3)
{
? int ret=0;
? switch(n2)
? {
??? case 240:ret=n1*n3;break;? // "*"?? 乘法
??? case 241:ret=n1/n3;break;? // "/"?? 除法
??? case 242:ret=n1%n3;break;? // "%"?? 求余數(shù)
??? case 224:ret=n1+n3;break;? // "+"?? 加法
??? case 225:ret=n1-n3;break;? // "-"?? 減法
??? case 208:ret=n1<<n3;break; // "<<"? 左移
??? case 209:ret=n1>>n3;break; // ">>"? 右移
??? case 192:ret=n1<n3;break;? // "<"?? 小于
??? case 193:ret=n1<=n3;break; // "<="? 小于等于
??? case 194:ret=n1>n3;break;? // ">"?? 大于
??? case 195:ret=n1>=n3;break; // ">="? 大于等于
??? case 176:ret=n1==n3;break; // "=="? 等于
??? case 177:ret=n1!=n3;break; // "!="? 不等于
??? case 160:ret=n1&n3;break;? // "&"?? 與
??? case 144:ret=n1^n3;break;? // "^"?? 異或
??? case 128:ret=n1|n3;break;? // "|"?? 或
??? case 112:ret=n1&&n3;break; // "&&"? 邏輯與
??? case 96:ret=n1||n3;break;? // "||"? 邏輯或
??? case 90:ret=n1|n3;break;?? // "|="
??? case 89:ret=n1^n3;break;?? // "^="
??? case 88:ret=n1&n3;break;?? // "&="
??? case 87:ret=n1<<n3;break;? // "<<="
??? case 86:ret=n1>>n3;break;? // ">>="
??? case 85:ret=n1%n3;break;?? // "%="
??? case 84:ret=n1/n3;break;?? // "/="
??? case 83:ret=n1*n3;break;?? // "*="
??? case 82:ret=n1-n3;break;?? // "-="
??? case 81:ret=n1+n3;break;?? // "+="
??? case 80:ret=n3;break;????? // "="?? 賦值
??? case -1:ret=n3;break;????? // 用來表示前括號
??? case? 0:ret=n1;break;????? // 空運算
? }
? return(ret);
}
??? 我們可以看到,在上面得有關賦值得運算中,我們實際上并沒有進行賦值,因為我們還沒有任何
變量來接受賦值,下一次里我們再來講講將游戲中的數(shù)據(jù)作為變量進行運算和賦值,這可是最激動人
心的哦!
注意:解釋機并不是獨立的軟件程序,它是游戲源程序的一部
????? 分,只有這樣腳本解釋語言它才可能通過它引用到游戲
????? 中的變量和函數(shù)。
??? 為了達到引用游戲中變量和函數(shù)的目的,我們專門定制一個函數(shù),用來將字符串轉變成整數(shù)(假
如起名為val,則它的函數(shù)原型就是int val(char *str))假若輸入字符串是一個數(shù)字串,我們就可
以調用前面一講講過的將數(shù)字字符串轉變?yōu)檎麛?shù)的函數(shù)將它轉化為數(shù)值;如果輸入字符串的第一個字
符是英文字母或者下劃線,我們就根據(jù)這個字串返回它所代表的游戲中的變量。
??? 例如,我們在游戲程序中定義了主角當前的位置是放在int cur_x,cur_y 當中,我們可以約定在
當在腳本語言中也用cur_x和cur_y來代表這兩個變量(只所以用同形的字串,是為了便于記憶,當然
你也可以給用另外的字串代替),假若我們的這個函數(shù)得到的輸入字串是"cur_x",我們就讓val函數(shù)
返回它變量cur_x中的值。如果是"cur_y",我們就返回變量cur_y 的值。同樣象人物屬性、物品等等
,都可以約定的字符串形式引用。但對于數(shù)組元素呢?我們在C語言中都是采用跟在變量后的方括號
內(nèi)寫入數(shù)組索引值,采用方括號的目的是在編譯時區(qū)別數(shù)組和函數(shù)。但在解釋語言中步存在區(qū)別的問
題,所以象BASIC 都采用和函數(shù)相同的圓括號。所以我們在處理數(shù)組和函數(shù)時也基本相同如:
?????? addobj(12,100)
?????? map(23,32)
??? 前一個是函數(shù),表示給主角加100個物品12(12是物品代號)后一個是二維數(shù)組,表示地圖上某
一點的物體,相當于map[23][32]。
??? 假若輸入的字串不是數(shù)字串,我們就可以將它拆分處理:如將"addobj(12,100)"分為"addobj"、
"12"、"100",共三項。對于cur_x就只得到一項"cur_x",根據(jù)它們的第一項,我們可以知道它們代
表的是那個變量或函數(shù),拆分出的其他項就是數(shù)組的索引或函數(shù)的參數(shù),因此我們可以很容易的指定
val的返回值(對于函數(shù)就是函數(shù)的返回值,對變量就是變量值)。
??? 但如果圓括號內(nèi)不僅僅是常數(shù),而且有變量或者函數(shù),或者是由函數(shù)變量組成的表達式,如:
?????? addobj(map(23-cur_x,32)+1,100)
這樣又怎么辦呢?解決方法就是交叉的遞歸調用。
我們現(xiàn)在所做的一切最終就是為了將一個字符串表達式轉變成一個整數(shù),寫成函數(shù)的形式就是(假設
函數(shù)名為cal)
???? int cal (char *str);
???? 如果輸入?yún)?shù)為1+addobj(map(23-cur_x,32)+1,100)+1,
???? 它需要調用val 函數(shù)來求參加運算的一個數(shù)值
????? addobj(map(23-cur_x,32)+1,100)
??? 而在val 函數(shù)中,對于addobj(map(23-cur_x,32)+1,100)會拆分出"map(23-cur_x,32)+1"這樣的
項,它也是一個表達式,我們只有以它為參數(shù)再次調用cal求值。cal又會調用val求值
val("map(23-cur_x,32)"),val拆分得到"23-cur_x",,再次調用cal("23-cur_x"),cal再調用
val("cur_x")得到cur_x的值,再返回給前一個調用它的cal,如此逐曾返回,最終由cal求得真正的
結果。
??? cal 調用 val,val又去調用cal,這就形成了交叉的遞歸調用,第一次調用
cal("1+addobj(map(23-cur_x,32)+1,100)"),第二次調用cal為cal("map(23-cur_x,32)+1")時,第
三次調用為cal("23-cur_x"),遞歸調用一個函數(shù)時,系統(tǒng)會自動的為它開辟內(nèi)存,生成一個副本,
這三次調用就同三個不同的函數(shù)一般。
??? 如此一來,在復雜的表達式我們也能求出起結果了,但需要注意的一點是遞歸調用太多時耗費系
統(tǒng)資源太多,也容易出錯,我們應當盡量避免。如在val中,如果對輸入串拆分得到的項是數(shù)字串自
然不用去調用cal,如果拆分得到單獨的變量或函數(shù),如"map(cur_x,32)"經(jīng)拆分得到的"cur_x"這一
項,因為沒有運算,則直接遞歸調用自己val("cur_x")就可以了,而不用再調用cal了,這樣就可以
減少遞歸調用的層次,減少內(nèi)存消耗和發(fā)生錯誤的可能性。
??? 我們可以發(fā)現(xiàn),到目前為止,我們可以引用游戲中的變量和函數(shù)了,但我們還不能給游戲中的變
量賦值,也就是說我們在腳本語言中能夠知道主角位置、主角的屬性、所處的位置、擁有的物品等等
,但卻不能改變它們,因為我們的賦值運算并沒有真正對游戲中的這些變量賦值,這將使腳本描述語
言大失光彩,沒關系,我們將在下一講解。
??? 首先我們分析我們以前進行的工作,
??? STACK(類或結構)用來暫存運算式中的數(shù)字和運算符代號
??? convert_mark 用來給運算符編含有優(yōu)先級的代號
??? quest??????? 用來計算兩個整數(shù)運算的結果
??? val????????? 用來將字符串(整數(shù),變量,函數(shù))轉化成
???????????????? 整數(shù)結果
??? cal????????? 將字符串表達式轉化成整數(shù)
??? 如果你看過以前的幾講,應該很容易搞清楚這些函數(shù)的基本調用關系。
???????? +--> STACK
???????? |
???????? +--> convert_mark
??? cal--|
???????? +--> quest
???? |?? |
???? |?? +--> val ---+
???? |?????????????? |
???? +---------+-----+
??? 我們現(xiàn)在再來考慮一下對游戲中的變量賦值,最先想到的自然是再進行實際運算的函數(shù)quest 中
實現(xiàn),但quest 函數(shù)的輸入?yún)?shù)只是三個整數(shù),即使我們知道現(xiàn)在進行的是賦值運算,知道了運算的
結果,但卻無法知道應該將結果賦值給那個變量。我們又會想到只在val 函數(shù)中才有可能拆分出代表
變量的字符串,才知道是那個變量,但在這個函數(shù)中我們并不知道這個變量參加的是何種運算,也不
知道運算的結果,所以也沒有辦法進行賦值。所以我們只有將希望放在調用它們兩個的函數(shù)cal 上了
,因為是cal 調用的val,所以cal能得到運算的類型和結果,至于參加賦值運算的那個具體的變量,
因為val 函數(shù)返回的是這個變量的值,因此我們還不能確定進行運算的是那個變量,但如果將val 的
返回值改為指向這個變量的指針,我們不是既能引用又能賦值了!我們根據(jù)參加運算變量指針所指向
的值調用quest 函數(shù)就可以得到運算結果,我們再根據(jù)這個運算是否賦值運算再決定是否將這個結果
寫入這個變量的指針。
??? 需要注意的是,在val 函數(shù)中,如果判斷出是變量,我們就返回它的指針就行,但如果是整數(shù)或
者是函數(shù),它們并沒有指向其值的指針,但我們可以定義一個靜態(tài)(static)變量,用來存放這個整數(shù)
或者函數(shù)的結果,然后返回指向這個靜態(tài)變量的指針就行了。(注意是靜態(tài)變量,因為它在函數(shù)結束
后不釋放內(nèi)存。如果是一般的動態(tài)變量,在函數(shù)結束后就會釋放,我們返回的指針指向的就是一個不
確定的值了!)當然你也可以采用全局變量。(因為它在整個程序執(zhí)行期間都不釋放內(nèi)存)。
??? 完成這幾個函數(shù),我們的字符串表達式求值部分就完成了,但我們的解釋機并沒有完成,我們還
需要無條件轉移、判斷、條件轉移、循環(huán)一些控制語句,這些我們會在下一講中完成!首先為了快速的解釋執(zhí)行,我們一般都將整個腳本文件讀入內(nèi)存,將腳本語
句一行的一行儲存。因為移動內(nèi)存指針
可比移動文件指針方便快速多了。
首先我們來看語句注釋,我們可以采用";" "http://","*"等注釋一行,也可以用成對的"/*"和"*/注釋一
整段。它的實現(xiàn)很簡單,我們只要在將腳本文件讀入內(nèi)存時將這些行忽略就行了。(遇到這種注釋一
行的標志,就讀到此行末尾,但不在內(nèi)存中保存。遇到是注釋一段的起始標志,就一直讀到注釋一段
的結束標志,這其中讀入的并不在內(nèi)存中保存?。?br />??? 首先我們來看看語句跳轉,很自然的,可以通過指定語句在腳本文件中的行號來進行跳轉(注意
不是BASIC中語句前的行號),但這樣做法實現(xiàn)很簡單,但對于腳本文件的編制和修改就麻煩大
了,因為你必許自己數(shù)出想要跳轉到的那一行的行號,而這其中又要排除忽略掉的注釋行,而當你每
次修改時刪除或者增加一行,那么相關的跳轉又要重新數(shù)行號。這恐怕會使人喪失編制腳本文件的耐
心。
??? 解決這個問題較佳的辦法是,在想要跳轉的那一行前加一個標號行,跳轉時指定哪個標號就行了
(結構化BASIC語言、匯編、C語言都是如此),在將這些腳本讀入內(nèi)存時忽略這些標號行,但
記錄下它的標號名稱和行號,再根據(jù)每個跳轉中指定的標號名稱,將它們的參數(shù)轉化成行號。如:
假設腳本文件是這樣的:
xxxxx??????????? //行0
xxxxxxx????????? //行1
//此行是注釋,讀入內(nèi)存時忽略
xxxxx??????????? //行2
:label?????????? //標號行讀入內(nèi)存時忽略
xxxxxxx????????? //行3
xxxxxx?????????? //行4
xxxxxx?????????? //行5
xxxxxx?????????? //行6
xxxxxx?????????? //行7
xxxxxx?????????? //行8
goto(label)????? //行9
xxxxxxx????????? //行10
xxxxxx?????????? //行11
/*
這其中的內(nèi)容都是注釋,讀入內(nèi)存時忽略
xxxxx
xxxx
*/
xxxxxxx????????? //行12
xxxxxx?????????? //行13
xxxxxxx????????? //行14
end????????????? //行15
讀入內(nèi)存并修改跳轉參數(shù)后變?yōu)?br />xxxxx
xxxxxxx
xxxxx
xxxxxxx
xxxxxx
xxxxxx
xxxxxx
xxxxxx
xxxxxx
goto(3)
xxxxxxx
xxxxxx
xxxxxxx
xxxxxx
xxxxxxx
end
注意到其中goto的參數(shù)變成了想要跳轉到的那一行在內(nèi)存中的行號,而注釋和標號行都被忽略了。
??? 我們現(xiàn)在再講講如何具體實現(xiàn):
??? 首先將腳本文件中的每一行讀入內(nèi)存(當然要忽略注釋),當讀入的是標號行時(此行第一個字
符是':' ),將所有的標號名稱和下一行的行號保存起來(規(guī)定標號名的長度和數(shù)量,比如規(guī)定變量
名少于32的字符,每個腳本文件中的標號不超過100個,標號名稱可以順序保存在一個二維字符數(shù)組
中,如
????? char label[100][32]
行號也順序放入一個整數(shù)數(shù)組,如
????? int labelno[100]
??? 當腳本文件讀入內(nèi)存后,再次掃描這些內(nèi)存,如果遇到goto就比較goto后的參數(shù)和label中的內(nèi)
容依次比較,如果相同,就將goto后的內(nèi)容改變成labelno內(nèi)相應的行號。
??? 如此一來,我們就得到了最終的內(nèi)存結果。
??? 當內(nèi)存中的這些腳本解釋時,我們會用一個變量來放當前即將執(zhí)行的行號,如果執(zhí)行完一行,就
將這個變量加1,然后下次就選取這個變量指示的那一行語句執(zhí)行。在進行跳轉時,只要把這個變量
改變?yōu)間oto后的行號即可。
??? 當然,goto(xx)的形式我們也可以把它當作函數(shù)處理,在我們前面講過的val 函數(shù)中,遇到goto
時將當前的命令行號變?yōu)閤x即可。
??? 這次主要講解釋機中對注釋語句和轉向語句的實現(xiàn)方法,下一次我們在來講條件分支、循環(huán)等等
。
??? 條件分支我們可以采用類似匯編語言的方法,在解釋機內(nèi)設置一個判斷專用的標志變量(
if_flag),根據(jù)if(...)括號內(nèi)的表達式設置這個變量。然后then(....)再根據(jù)這個變量的值決定是
否轉向括號內(nèi)指定的標號行(這些都是在前面講過的函數(shù) val里實現(xiàn)),如:
??? if(cur_x<10)?? //條件成立設置判斷標志為1,反之為0
??? xxxxxx
??? xxxxxx
??? then(label1)?? //判斷標志位為1則轉向label1否則繼續(xù)
??? xxxxxx
??? xxxx
??? :label1
??? xxxxxx
??? 我們在讀入腳本進內(nèi)存時時,同goto一樣也要將then括號中的標號轉變?yōu)橄鄳男刑枴?br />??? 這樣我們就可以和匯編語言一樣,結合其他變量實現(xiàn)循環(huán)。
??? s1=0?????????? //給循環(huán)記數(shù)器設置初值
??? :label1
??? xxxxxxx??????? //需要循環(huán)執(zhí)行的語句
??? xxxxxx???????? //需要循環(huán)執(zhí)行的語句
??? s2=s1*10?????? //需要循環(huán)執(zhí)行的語句
??? xxxxxx???????? //需要循環(huán)執(zhí)行的語句
??? s1+=1????????? //循環(huán)記數(shù)器自動增加
??? if(s1<10)????? //判斷循環(huán)是否結束
??? then(label1)?? //如果沒有結束跳轉到label1
??? xxxxx????????? //如果結束了繼續(xù)執(zhí)行下面這些行
??? 另外還要說明的一點是,在RPG游戲時我們經(jīng)常會遇到彈出有多項選擇。比如說在買東西的時
候,會列出多個物品讓你選擇。我們把這多項選擇也做成函數(shù):
??? int choose(char *str,int n,char *item,int must)
??? 就拿前面買東西來說,你在一個武器店的地圖中放置一個店老板的NPC,他對應的腳本如下:
??? say(12,30,"您好,歡迎光臨本店!")
??? :ask?????? //詢問您要什么
??? s1=choose("您要?",3,"買東西 賣東西 不要了",1)
??? if(s1==0)
??? then(buy)? //跳轉到買東西
??? if(s1==1)
??? then(sell) //跳轉到賣東西
??? //不買也不賣
??? say(12,30,"不要了?您慢走!")
??? end
??? :buy??????????? //買物品
??? s1=choose("買什么?",5,"匕首 竹劍 鋼劍 梭鏢 銅錘",0)
??? xxxxxxxxxx????? //根據(jù)選擇的選項s1
??? xxxxxxxxxxx???? //給玩家增加物品,減少金錢等等
??? xxxxxxxxx?????? //
??? xxxxxxx???????? //
??? xxxxxxxxxx????? //
??? if(s1==-1)????? //如果選擇"買什么"時點了ESC退出
??? then(buy)?????? //放棄買物品跳轉到"您要什么"的選擇
??? goto(ask)?????? //買了一件物品后繼續(xù)選擇要買的物品
??? :sell?????????? //賣物品
??? s1=chooseobj()? //從自己有的物品中選擇一樣
??? if(s1>100)????? //判斷物品的種類
??? then(nosell)??? //決定是否跳轉到nosell
??? xxxxxxxx??????? //根據(jù)玩家選擇的物品s1
??? xxxxxxxxx?????? //減物品,加金錢等等
??? xxxxxx????????? //
??? if(s1==-1)????? //選擇要賣的物品時點了ESC鍵退出
??? then(ask)?????? //放棄賣物品跳轉到"您要什么"的選擇
??? goto(sell)????? //賣完一件,選擇還要賣的
??? :nosell???????? //不收這種物品
??? say(12,30,"抱歉,這種東西我們不收!")
??? goto(sell)????? //繼續(xù)選擇要賣的物品
這其中的choose函數(shù)是我們在游戲程序中實現(xiàn)的一個多項選擇的函數(shù),以s1=choose("您要?",3,"買
東西 賣東西 不要了",1)為例
s1內(nèi)放置的選項號碼(第一個是0,第二個是1,依次類推)
??? "您要?"是多項選擇時的提示
??? 3是選項的個數(shù)
??? "買東西 賣東西 不要了"是三個供玩家選項,中間以' '分隔
??? 1是強制玩家必須從這些選項中選擇一個,不能按ESC鍵放棄選擇(此時返回-1給s1),如果是
0則可以按ESC鍵放棄選擇。
??? 另外 chooseobj()也是我們在游戲中實現(xiàn)的一個函數(shù)。從玩家的物品中選擇一樣,返回它在玩家
物品匣中的位置,它在地圖行走、戰(zhàn)斗中的物品使用都可以使用。
??? 前面的八篇講了有關RPG游戲腳本解釋機的實現(xiàn),從這篇起,我們就開始從一個更高的位置對游
戲做統(tǒng)籌!
一、首先我們來看看一般RPG游戲的大體包括的模塊。
?? 1 系統(tǒng)初始化/結束:(system_init/system_exit)
??? 在游戲進行之前,我們必須進行一些系統(tǒng)初始化的工作,象什么讀取圖片、音樂等數(shù)據(jù),對屏幕
、聲卡等硬件做初始化工作等等;同樣,在游戲結束后,我們還需要進行一些整理工作,象是關閉設
備驅動、釋放內(nèi)存等等。
?? 2 標題畫面:(logo)
??? 功能為顯示游戲標題,選擇菜單"新的游戲\讀取進度\退出游戲"。并根據(jù)不同的選項分別調用
不同的模塊。
??? 有時,我們不需要另外做開始新游戲的模塊,我們只要專門做一個游戲開始狀態(tài)的進度,象讀取
進度一樣讀取它,就可以完成這個功能。譬如:游戲最多能保存5個進度,從game1.sav到game5.sav,
我們事先做好一個進度為game0.sav,保存游戲初始的狀態(tài),玩家在讀取進度時可以用通過
load_game(int n)調用(n=1,2,3,4,5)。當開始新游戲時則通過load_game(0)調用game0.sav。
?? 3 讀取/保存進度:(load_game/save_game)
??? 讀取就是從文件中讀出一些數(shù)據(jù)裝入游戲的變量中,保存就是將游戲中的變量保存到文件中。當
然你也可以指定一個算法在在數(shù)據(jù)讀入和寫入之前進行變換,從而起到對進度加密的效果。一般需要
保存的數(shù)據(jù)有:主角狀態(tài)、主角所在地圖、 npc狀態(tài)、其他數(shù)據(jù),這些可以根據(jù)游戲具體情況進行取
舍。另外進度的保存在游戲中進行,讀取則在標題畫面或者游戲進行中都行。(當然使用劇情腳本的
話,你甚至可以通過和某個npc交談或者使用某件物品來保存進度。)
?? 4 游戲進程: (run_game)
??? 一般是一個較大的循環(huán),在循環(huán)中處理玩家的按鍵、控制npc的運動、處理其他實時的事件、顯
示地圖等等。
二、模塊的運行關系
??? 游戲運行時,首先進行系統(tǒng)設置system_init(),然后調用標題畫面i=logo(),如果i==0即玩者
選擇"新的游戲",那么開始新游戲load_game(0),然后進行游戲run_game();如果i==1即選擇"舊
的進度"則選擇進度號l=choose_progress(),如果l==0返回標題畫面。如果1<=n<=5則讀取進度
load_game(l),然后再進行游戲run_game();如果i==2即玩者選擇"退出游戲",則調用結束模塊
system_exit(),然后結束程序。
??? 當然在游戲進行過程中run_game()中,也可以讀取進度load_game(l)和保存進度save_game(l);
三、其它
??? 這些模塊中,除了游戲進程模塊run_game外,都比較容易實現(xiàn),所以我們就略過不講,今后著重
講有關run_game的部分。
??? 世界是在不停運動改變著的,我們用游戲所創(chuàng)造的虛擬世界也是這樣,這種不斷的運動在我們的
程序中就體現(xiàn)為循環(huán)。程序實際上是一種在計算機和用戶之間進行交互的工具,為了響應用戶的控制
,程序需要了解用戶的輸入信息,并把通過對計算機的控制做出相應的響應。但程序怎樣了解用戶的
輸入信息呢?那就需要我們的程序主動的對用戶用來輸入信息的硬件(如鍵盤、鼠標、游戲桿等等)
進行檢測。
??? 用戶可能在任何時候輸入信息,因此程序也必須隨時準備接收這種輸入,在程序中接受輸入的兩
種方法有兩種:一種是程序完全停止下來準備接收,直到接收到數(shù)據(jù)才開始繼續(xù)運行;另一種是程序
以一定的頻率在不斷的循環(huán),如果發(fā)現(xiàn)有輸入信息,就對輸入進行處理,如果沒有輸入信息時,程序
就繼續(xù)循環(huán)并處理其他的事情。(就向tc里的bioskey(0)和bioskey(1),或者是windows編程中
GetMessage和PeekMessage)
??? 注意:上面這兩種方法的劃分,是完全從編程的角度來看的,
????????? 即從某個函數(shù)或者方法來看的。實際上在硬件或者更
????????? 低級的機器語言中,輸入的接收是完全采用循環(huán)檢測
????????? 實現(xiàn)的。
??? 顯而易見,第一種方法有它的局限性,它是一種單向不可逆的交互過程,在需要用戶一步步輸入
信息的簡單程序中比較適用,但在需要雙向交互的實時程序中卻難以適應。試想在動作或者射擊類游
戲中,等待玩家每次按鍵后才運動、攻擊的敵人是多么的愚蠢可笑呀!(我原來就在APPLE-II上做過
這樣的游戲)
??? 因此第二種方法才是游戲運行中關鍵的輸入接收方法。也就是說,當玩家不進行輸入操作時,程
序的循環(huán)就會去執(zhí)行其他的事情,如控制敵人運動、攻擊等等,而不是停下來等你輸入。
??? 當然,我們在游戲中也需要程序停下來等待輸入的時候,比如"請按任意鍵 press any key...
"就是我們經(jīng)常使用它的地方。
??? 上面講的這些并不是廢話,因為在游戲中確需要區(qū)分這兩種輸入方法,正確的使用它們才能達到
你預期的效果。
??? 比如:在 RPG游戲中,人物對話顯示一屏后,就會等待玩家按鍵看下一屏對話。這時我們就可以
采用第一種方法,將程序完全停下來等待按鍵,也可以在玩家沒有按鍵的時候在人物對話框的下方閃
爍著顯示一個按鍵的圖形,提示玩家按鍵。這時就需要采用上面提到的第二種方法。在游戲中這樣的
細節(jié)很多,你完全可以自己決定采用什么的方法以達到什么樣的效果。
??? 我們的游戲主體,實際上就是在不斷地處理著這樣的用戶輸入、并對它做出響應的一個大的循環(huán)
體。有了這一概念,我們在對它進行設計時,就容易多了。
??? 循環(huán)結構開始
????? --處理NPC
????? --檢測用戶按鍵
????? --如果是方向鍵,進行主角移動處理。如果觸發(fā)事件,進
??????? 行事件處理。
????? --如果是Esc鍵,彈出游戲菜單,根據(jù)玩家選擇處理。
????? --刷新屏幕(可以設定獨立的定時事件完成)
??? 循環(huán)結構結束?
??????? 注意,其中的屏幕刷新,是由屏幕刷新率決定的,可以設置獨立的定時事件來完成,也可以
放在這個主循環(huán)內(nèi)進行。具體的實現(xiàn)方法,我們下次再講。