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