国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

【翻譯】Managed DirectX(第六章)

2019-11-17 05:24:56
字體:
來源:轉載
供稿:網友
第六章 使用Managed DirectX編寫游戲
翻譯:claymanclayman_joe@yahoo.com.cn僅供個人學習之用,勿用于任何商業用途,轉載請注明作者^_^
選擇游戲
      雖然很多關于3D游戲編程的高級主題還沒有討論,但我們已經有足夠的背景知識來寫一個簡單游戲了。這一章,我們將使用至今學過的知識,再加上一點點新的東西來創建游戲。
     真正開始寫游戲之前,最好先擬一份計劃。我們需要確定寫什么類型的游戲,它將有哪些最基本的特性,等等。考慮到目前的技術限制,自然不能寫太復雜的游戲。這將是一個簡單的游戲。在MS-DOS環境下,曾經有一個叫做“Donkey”的游戲,玩家控制著車不能撞到路上的donkey。聽起來足夠簡單吧,我們將創建一個三維版本,并且用普通的障礙物來代替donkey。這個游戲叫做“躲避者(Dodger)”。
     開始編碼之前,需要花一點時間來策劃和設計游戲。我們需要怎樣的游戲,玩的時候來控制。Well,顯然,要有一個Car類來控制交通工具。接下來,使用另一個類來控制障礙物將會很不錯。除此之外,主要的游戲引擎類必須完成所有的渲染操作并把所有對象組織起來。
     假如嘗試商業游戲,那么大部分時間將會花在游戲創意上。游戲創意將會寫成具體的文檔,包括了游戲主題和特性的各種細節。本書的著重于討論游戲的實際開發工作,而不是游戲發行和創意,所以我們將略過這一步。
     通常開之發寫還必須寫完整的技術文檔(technical specification)(簡稱為spec)。它包以適當的細節列出了所以類,以及需要實現的各種方法、屬性。通常還包括表示對象之間關系的UML圖。這份文檔的目的是讓你在編碼前坐下來認真考慮程序的設計。由于本書聚焦于代碼的編寫,我們同樣略過這一步。需要說明的是,強烈建議你在寫任何代碼前花點時間撰寫技術文檔。
 
編寫游戲
     現在可以打開VS創建項目了。創建一個名為Dodger的windows應用程序。使用DodgerGame代替代碼中所有出現Form1的地方。添加對DirectX程序集的引用。創建私有的device成員,如下修改構造函數:
     public DodgerGame()
     {
         this.Size = new Size(800,600);
         this.Text = “Dodger Game”;
         this.SetStyle(ControlStyles.AllPaintingInWmPaint ControlStyles.Opaque,true);
}
這將會把窗口設置為800×600(注:實際代碼中我將會創建一個全屏的游戲,另外假如現在運行程序,會發現我們創建了一個透明的窗口),設置窗口標題和樣式(style),這樣渲染代碼才會正常工作。接下來修改程序的入口點:
static void Main() {詳見源碼}
這個應該很熟悉了吧,基本上就是之前每一章用來啟動程序的代碼。創建窗體、初始化圖形引擎,運行窗體。在initializeGraphics內做如下改動:
PRivate void InitializeGraphics() {詳見源碼};
創建了presentation parameters結構之后,確保有它有深度緩沖。這里有什么新內容呢?首先,保存了默認的適配器的序數號,接下來保存了creation flags,并把它的默認值設為software vertex processing。但是,現代圖形卡都在硬件層實現了vertex processing。何必把寶貴的CPU資源用在顯卡可以完成的任務上呢?答案是不需要這樣做,但你不知道是否真的支持這種特性,于是有了接下來的代碼。在真正創建device之前,需要先保存顯卡的功能(capabilities,簡稱Caps),這樣可以用來決定使用那一種flags創建device。因為你只是創建一個硬件設備,所以只儲存這幾個Caps就可以了。關于檢查適配器所有Caps的內容回憶一下第二章吧。
還記得使用頂點緩沖時需要在重置設備之后重建緩沖嗎?我們為device訂閱了created事件。當device重置之后,設定device的所有默認狀態,添加如下代碼:
private void OnDeviceReset(object sender,EventArgs e) {詳見源碼};
(注重:類似于這里的代碼,你可能會使用一個層(layer)來檢查支持的燈光。這種情況下,先檢查是否支持一盞燈,假如可以,則創建它。然后再用類似的方法檢測是否支持第二盞燈。這樣即使最差的情況你也能獲得一盞燈光)
這里和前面學過的代碼也很類似,通過projection fransform和view transform來設置攝像機。對于這個游戲來說,我們的攝像機不需要移動,所以只需要在重置設備之后設置一次就可以了(與設備相關的狀態都會在重值之后丟失)。
環境光不是最好的選擇,我們已經知道他不能產生真實的光影效果,所以方向光將是不錯的選擇。但并不能確定設備是否支持這種光源。創建了設備之后,就不需要再使用先前的Caps結構了,device會為你保留著這些信息。假如device支持方向光,而且支持一盞以上的燈光,你應該使用它;否則,使用默認的環境光。它雖然不真實,但總比黑色的場景要好吧。最后,重載OnPaint方法,:
protected override void OnPaint(PaintEventArgs e){詳見源碼};
這里沒有什么新內容,當然你可以把背景改為任何你喜歡的顏色。現在已經為加載模型做好了預備。創建變量來儲存.X文件中的賽道模型吧。
     private Mesh roadMesh = null;
     private Material[] roadMaterials = null;
     private Texture[] roadTextures = null;
接下來修改一下前一章里的load mesh方法。最大的改變是將把它改為靜態方法,因為不止一個類會調用它,同樣把所有的材質和紋理作為參數來傳遞,而不是作為類成員來訪問。添加如下代碼:
public static Mesh LoadMesh(Device device,string file,ref Material[] meshMaterials,ref Texture[] meshTextures){詳見源碼};
這個方法前面已經深入討論過了。使用這個方法來加載賽道模型,還需要在重置設備的事件里添加它,在OnDeviceReset最后加上如下代碼:
roadMesh = LoadMesh(device,@"../../road.x",ref roadMaterials,ref roadTextures);
確定你已經把賽道模型和紋理文件復制到了源文件的目錄下。這段代碼將會加載模型以及紋理,并儲存紋理、裁制以及模型。每一幀道路mesh都需要渲染很多次,因該創建一個方法來完成渲染工作。添加如下代碼:
private void DrawRoad(float x, float y ,float z) {詳見源碼};
你應該還記得這個方法吧,它和我們之前使用的方法如此類似。把mesh變換為正確的位置然后渲染每一個子集。我們需要每次渲染兩段賽道mesh:一段是賽車現在行駛的賽道,一段是即將行駛到的賽道。實際上我們的賽車并沒與移動,而是賽道在移動。這樣做的原因有兩個:假如每一幀都移動賽車,那么還必須同時移動攝像機來跟上它。這些而外的計算實際上是不必要的。還有一個更重要的原因:假如賽車向前移動,而且玩家很厲害,那么賽車的位置可能會超出浮點值的范圍,甚至導致溢出。因為我們的游戲世界并沒有邊界(游戲不會有終點),所以讓賽車停留在原地,移動賽道。
自然,需要一些變量來控制賽道。添加如下代碼:
public const float RoadLocationLeft = 2.5f;
     public const float RoadLocationRight = -2.5f;
     private const float RoadSize = 100.0f;
     private const float MaxRoadSpeed = 250.0f;
     private const float RoadSpeedIncrement = 0.5f;
     private float RoadDepth0 = 0.0f;
     private float RoadDepth1 = -100.0f;
private float RoadSpeed = 30.0f;
作為mesh的賽道模型是已知的,長寬各為100個單位。RoadSize常量就是賽道的長度,兩個location常量標記了賽道兩邊的中點。最后兩個常量用來控制游戲操作。最大速度讓游戲每秒移動250個單位,每次加速多移動0.5個單位。
最后,設置兩段賽道的深度。把地一段賽道設置為0,第二段緊跟著上一段賽道。添加繪制賽道的代碼,使用這幾個變量來繪制賽道。在BeginScene方法之后添加如下代碼:
DrawRoad(0.0f,0.0f,RoadDepth0);
DrawRoad(0.0f,0.0f,RoadDepth1);
現在運行程序,可以看到已經正確的繪制了賽道,但是這條瀝青的賽道看起來極度可怕。這種結果是由Direct3渲染計算像素的方式引起的。當一個texel要覆蓋屏幕中的多個像素時,這些像素需要通過一個放大過濾器來補償(magnify filter to compensate)。當幾個texel需要被繪制為一個像素時,他們會通過一個縮小過濾器。兩種情況下的默認過濾器是一個名為Point的過濾器,它將會使用texel最接近的顏色作為像素的顏色,因此導致了這種情況。
有很多種方法來過濾紋理,但是,device不一定支持。我們只需要一個可以在texel之間插值計算,讓賽道紋理看起來比較平滑的過濾器就可以了。在OnDeviceReset方法里添加如下代碼:
詳見private void OnDeviceReset(object sender,EventArgs e)中的代碼
如你所見,先檢查設備在放大(magnification)和縮小(minification)上是否支持各向異性(anisotropic)的過濾器。假如可以,就使用它。不行的話,再檢測是否支持線性(linear)過濾器。假如兩者都不可用,那么只能什么都不作,保留這種粗糙的效果。假設你的圖形卡能支持其中一種過濾器,那么現在可以看到效果要好多了。
賽道以及處在了屏幕的中間,但還沒有移動。還需要一個方法來更新游戲狀態,完成移動賽道,進行碰撞檢測。應該再OnPaint方法一開始就調用這個方法(再clear方法前):
OnFrameUpdate();
以下則是這個方法的代碼:
private void OnFrameUpdate(){詳見源碼}
整個游戲編寫完之后會有很龐大的代碼,但現在,我們所需的只是讓路動起來而已。先忽略elapsedTime,這段代碼所作的只是移動路面而已。最后還需要添加一個變量:
private float elapsedTime = 0.0f;
 
非凡提示:
為什么需要使用時間呢?為了方便討論,假設我們每一幀都把賽道移動相同的距離。也許在你的電腦上它運行的很完美,但在其他的系統上呢?找一臺比你的系統配置低的系統運行看看吧,賽道看起來會運行的相當緩慢。同樣換到配置較高的系統上,賽道又會移動的快很多。原因在于你的計算是基于幀速率(frame rate)。假設在你的系統上,每秒可以跑60幀,那么所有的計算過程都是依靠于這個靜態的幀速率而來的。因此,在每秒可以跑40幀或80幀的系統中,自然會得到不同的計算結果。讓你的程序在每一個系統下運行都得到同樣的結果是我們的基本目標之一,因此無論如何都應該避免基于幀速率的計算。
解決這個問題一個比較好的方法就是根據時間來計算位移。比如,賽道的最大速度定義為每秒250個單位。首先,我們需要獲得自上一次“更新”過后過去的時間間隔。.net運行時內建的一個屬性(tick count)可以用來獲得系統的tick count。但它它并不完美:這個計時器的精度太低。它的值大約每15毫秒才更新一次,因此,在一個高幀速率的系統中(每秒60幀以上),賽道的移動將是不連續的,因為所用的時間不是平滑的。
假如你的系統支持的話,在DirectX SDK包含了一個高精度(通常精度為1毫秒)的計時器類DirectXTimer。但如何你的系統不支持,那么則只能使用tick count了。本書都將使用這個計時器來計算時間。(注:這里的DirectXTimer實際上是作者通過P/Invoke自己實現的一個計時器,代碼在Utility.cs文件中,書上沒有具體講解實現方法,但大家應該都能看明白吧^_^)
 
為場景添加一輛可移動的賽車吧
好了,現在已經有了渲染好的、并且可以沿著場景移動的賽道了,接下來應該添加實際與玩家交互的對象了:一輛賽車。可以簡單的再添加賽道的那個主要的類里加上一些關于賽車的變量和常量就可以了,但這樣的代碼將不是模塊化的。你應該把關于賽車的代碼分離出來,成為一個獨立的類。為工程添加一個名為“Car”的新類吧。
Car類應該完成些什么任務呢?因為當其他物體移動的使用它仍然是靜止不動的,不需要向前,也不需要向后。但為了讓賽車能躲避路上的障礙物,它應該能夠左右移動,同樣,它還需要能渲染自身。好了,有了這些信息,就可以為類添加成員了:
 詳見源碼
這些變量已經足夠用于控制賽車了。Height和depth都為靜態的常量。賽車向兩旁的移動速度的增量也是常量。使用最后一個常量的原因是賽車模型的大小剛好比賽道大,所以需要把它縮小一點點。
其他的成員基本上一看名字就知道它的用途了。有賽車當前的位置數據,默認情況下賽車位于賽道的左邊。賽車的直徑(Diameter),稍后會使用它來進行碰撞檢測。有賽車的側滑速度。當然,還有用來檢測賽車在向哪個方向移動的兩個布爾變量。最后,是有關mesh的變量。
Car類的構造函數需要完成兩個任務:創建mesh對象(包括與它相關的結構)以及計算賽車的直徑。添加一下構造函數:
public Car(){詳見源碼}
創建car mesh的方法和創建road mesh的方法基本上一樣。接下來計算直徑的新的代碼則是比較有趣的。這里實際上是在計算賽車的邊界球體(bounding sphere,mesh的所有的頂點都包含在這個球體內)。Geometry類包含了這個方法,只要把需要計算邊界的頂點作為參數傳給這個方法就可以了。
這里所需的就是從mesh獲得頂點。你已經知道頂點保存在頂點緩沖內的,因此直接使用這塊頂點緩沖。為了讀取頂點緩沖中的數據,必須調用lock方法。在下一章中,會學到更多來自于VertexBuffer類的lock方法重載。現在,只需要知道這個方法會使用一個流返回所有頂點數據。還可以使用ComputeBoundingSphere方法獲得這個mesh的“中心”以及邊界球體的半徑。因為我們并不需要關心mesh的中心,所以只需要把半徑乘2獲得直徑就可以了。但是,模型經過了縮放,所以直徑也需要縮放同樣的比例。最后(在必不可少的finally塊中),確定解鎖并且釋放了頂點緩沖。
接下來,添加繪制賽車的方法。Car類已經保存了賽車的位置,只需要獲得device對象就可以繪圖了。這個方法幾乎和DrawRoad方法一樣,區別在于變量不同以及在變換前需要縮放mesh,添加如下代碼:
public void DrawCar(Device device) {詳見源碼}
在使用Car類之前,還需要讓外部可以訪問類的私有成員,添加如下公共屬性:
{詳見源碼}
現在,應該在主要的游戲引擎類里添加成員來使用Car類了。在DogerGame類里添加如下代碼:
private Car car = null;
由于car類的構造函數需要device作為變量才能初始化,所以只有在創建了device之后才能調用它。在OnDeviceReset方法里創建car是個不錯的主意,在創建了road mesh之后添加如下代碼:
car = new Car(device);
創建了賽車之后,就可以更新渲染部分的代碼了。在OnPaint中兩個DrawRoad方法之后添加以下代碼:
car.Draw(device);
可以看到,已經在路上正確的渲染了賽車。可是,如何才能控制賽車左右移動呢?先忽略鼠標的存在,假設玩家擁有鍵盤,并且將使用鍵盤來控制游戲。使用鍵盤上的4個方法鍵來控制游戲是不錯的選擇。重載OnKeyDown方法:
protected override void OnKyeDown(KeyEventArgs e) {詳見源碼}
這里沒有什么非凡的內容。假如按下了ESC則游戲結束同時關閉窗口。按下左鍵或者右鍵,則把相應的moving變量設置為true,另一個則設為false。現在運行程序,按下按鍵可以正確更新賽車的兩個moving變量。但賽車本身并不會移動,還需要為賽車添加一個函數更新它的位置:
public void Update(float elapsedTime)
這個方法接受逝去的時間值作為參數,所以無論在任何系統上,都會得到相同的結果。這個方法本身很簡單,哪一個moving變量的值為true,則向那個方向移動移動相應的距離(根據所經過的時間長短)接下利檢查是否已經移動到了邊界,假如是的話則完成移動。但是,這個方法是不會自己調用自己的,還需要更新OnFrameUpdate方法,加入以下代碼:
car.Update(elapsedTime);
(注:假如你是按著教程一步一步來,沒有偷看最后源碼的話,會發現此時賽車根本不會移動,郁悶吧,呵呵,原因是根本沒有啟動計時器。在初始化圖形設備的InitializeGraphics()方法中加上如下代碼吧 Utility.Timer(DirectXTimer.Start); )
 
添加障礙物
恭喜,這就是你創建的第一個3D互動程序了。已經完成了模擬賽車的移動。雖然實際上是賽道在移動,但顯出的效果確實是賽車在移動。至此,游戲已經完成大半。接下來是添加障礙物的時候了。與添加Car類一樣,添加一個名為Obstacle的類。
我們將使用不同顏色外形的mesh作為障礙物。通過mesh類創建stock對象可以改變mesh的類型,同時,使用材質來改變障礙物的顏色。添加如下的變量和常量:
{詳見源碼}
第一個常量表示將會有5種不同類型的mesh(球體、立方體、圓環、圓柱以及茶壺)。其中大多數的物體都有一個長度或半徑的參數。我們希望所有障礙物都有同樣的尺寸,所以應該把這些參數都設置為常量。很多種mesh類型都有一個而外的參數可以控制mesh中的三角形數量(stacks,slices,rings等等)。最后一個常量就是用來控制這些參數的。可以增大或減小這個參數來控制mesh的細節。
接下來的color數組用來控制mesh的顏色。我只是隨即的選擇了一些顏色而已,也可以把它們改為任何你喜歡的顏色。應該注重到這個類里既沒有任何的材質數組,也沒有紋理數組。你應該知道默認的mesh類型只包含了一個沒有材質和紋理的子集,因此,額外的信息是不需要的。
由于障礙物需要放置在路面之上,并且實際上是路在移動,所以必須保證它們是和路面同時移動的。需要position屬性來保證在路面移動時障礙物會同時更新。最后由于在創建茶壺時不能控制它的大小,需要檢查創建的是否為茶壺,平且對它進行相應的縮放。為Obstacle類添加如下構造函數:
     注重到這里我們使用了來自utility的Rnd屬性。它的具體實現非常簡單位于utitity.cs文件中,只是用來返回一個隨即的時間而已。Obstacle默認的構造函數保存了障礙物的默認位置,而且默認的為一個“非茶壺的”mesh。接下來選擇創建某個類型的mesh。最后,選擇一個隨機的顏色作為材質顏色。
     在把障礙物添加到游戲引擎之前,還有一些額外的工作需要完成。首先,添加一個方法來和賽道同步更新障礙物的位置。,添加如下代碼:
     public void Update(float elapsedTime,float speed)
     再一次使用elapsed time作為參數來保證程序在任何系統都能正常工作。同時,把當前賽道的速度也作為參數,這樣物體就似乎是“放置”在賽道上一樣。接下來,還需要一個方法渲染障礙物:
public void Draw(Device device) {詳見源碼}
因為茶壺沒有經過正確的縮放,因此假如渲染的是茶壺,那么應該先對他進行縮放,再移動到正確的位置。之后,設置材質顏色,把紋理設置為null,繪制mesh。
顯然,同一時間賽道上需要多個障礙物。你需要一個方法來簡單的在游戲引擎里添加或者移除障礙物。使用數組是一個可行的方法,卻不是最好的:數組不能重置大小。集合是一個不錯的選擇,為obstacles添加一個集合來儲存障礙物:
public class Obstacles : Ienumerable {詳見源碼}
當然,別忘了對System.Collections名稱空間的引用。這個類包含了可以直接訪問集合成員的索引器,可以讓foreach方法正確工作的迭代器,以及三個值得注重的方法:add,remove以及clear。obstacle文件有了這些基本的方法之后,可以為游戲引擎添加障礙物了。
首先,需要一個變量來儲存當前場景里的障礙物。為DodgerGame類添加如下變量:
private Obstacles obstacles;
接下來,需要一個方法用新的障礙物填充即將出現的一段賽道,添加如下代碼:
private void AddObstacles(float minDepth) {詳見源碼}
這個方法是把障礙物添加到游戲的起點。首先,計算需要添加到這段賽道的障礙物數量。同時,還必須保證在障礙物之間有足夠的距離讓賽車躲避,否則,對玩家而言很不公平。接下來,就把障礙物隨機的添加到路上。同時,把它添加到當前的obstacles集合中。注重到在創建障礙物時使用了一個名為ObstaclesHeight的常量,以下是它的聲明:
 
在障礙物出現在場景之前,還有3件事要做:you need to add a call into our obstacle addition method somewhere,你需要取保為場景中的每一個障礙物都調用了update方法,你最后還需要渲染障礙物。因為在開始游戲前,需要把所有成員變量都重置為默認狀態。是添加一個新方法的時候了,使用這個方法來初始化AddObstacles。添加如下代碼:
private void LoadDefaultFameOptions()
{
         RoadDepth0 = 0.0f;
         RoadDepth1 = -100.0f;
         RoadSpeed = 30.0f;
         car.Location = RoadLocationLeft;
         car.Speed = 10.0f;
         car.IsMovingLeft = false;
         car.IsMovingRight = false;
         foreach(Obstacle o in obstacles)
         {
              o.Dispose();
         }
          obstacles.Clear();
         AddObstaxles(RoadDepth1);
         Utility.Timer(DirectXTimer.Start);
}
這個方法重置了大量我們關心的成員變量。同時依次對集合中的對象進行dispose操作,并且在重新填充集合之前刪除所有元素。最后,啟動計時器。應該在InitializeGraphics方法中創建了device之后的地方調用這個方法。千萬不要把這個方法添加到OnDeviceReset方法中;只需要在每次游戲開始的時候調用一次就足夠了。
LoadDefaultGameOptions();
現在需要在OnFrameUpdate方法中添加一個方法來更新障礙物。因為每一幀都需要更新所有障礙物,所以應該迭代他們。在OnFrameUpdate方法中,car.Update(elapsedTime)之前,添加如下代碼:
foreach(Obstacle o in obstacles)
     {
         o.Update(elapsedTime,roadSpeed);
}
把障礙物添加到游戲引擎的一步就是渲染他們了。在OnPaint方法中,緊跟在繪制賽車的代碼之后,添加我們熟悉的方法來渲染障礙物:
foreach(Obstacle o in obstacles)
     {
         o.Draw(device);
}
嘗試著運行程序吧!(注:哈哈,程序拋出異常了吧,記住,還要在OnDeviceReset的最后添加 obstacles = new Obstacles();)。當你沿賽道行駛了一段距離,避開了幾個障礙物之后,發生了什么?看起來在避開了最初的幾個障礙物之后,就不在有新的障礙物出現了。回想一下先前的代碼,你只是在第一段賽道添加了障礙物,之后,不停的移動這段賽道。但對于新的“賽道段”,你并沒有調用任何方法填充障礙物。在添加新賽道段的地方添加如下代碼吧:
if(roadDepth0 > 75.0f)
     {
         roadDepth0 = roadDepth1 - 100.0f;
         AddObstacles(roadDepth0);
     }
     if(roadDepth1 > 75.0f)
     {
         roadDepth1 = roadDepth0 - 100.0f;
         AddObstacles(roadDepth1);
}
不錯,現在看起來好多了。你獲得了一輛在賽道上緩緩行駛并且可以穿越(至少現在可以)障礙物的賽車。障礙物看起來有些呆板。應該讓他動起來,讓障礙物在賽車經過的時候旋轉起來。首先,需要添加一些新的成員變量讓obstacle類能控制旋轉:
private float rotation = 0;
     private float rotationspeed = 0.0f;
     private Vector3 rotationVector;
障礙物的旋轉速度和轉軸都應該是隨機的,這樣他們才會看起來與眾不同。在obstacle類構造函數的最后添加以下兩行代碼就可以實現這個功能:
rotationspeed = (float)Utility.Rnd.NextDouble() * (float)Math.PI;
     rotationVector = new Vector3((float)Utility.Rnd.NextDouble(),(float)Utility.Rnd.NextDouble(),(float)Utility.Rnd.NextDouble());
為了使障礙物旋轉起來,還有兩個需要修改的地方。首先,需要把實現旋轉的代碼添加到Update函數中:
Rotation += (rotationspeed * elapsedTime);
這里沒什么非凡的,只是根據時間和隨機的旋轉速度來增加旋轉角度而已。最后,真正改變通過world transform來實現旋轉,這樣,渲染出來的物體才是旋轉的。更新以下代碼:
if(isTeapot)
     {
         device.Transform.World = Matrix.RotationAxis(rotationVector,rotation)
         *Matrix.Scaling(ObjectRadius,ObjectRadius,ObjectRadius)*Matrix.Translation(position);
     }
     else
     {
         device.Transform.World = Matrix.RotationAxis(rotationVector,rotation) * Matrix.Translation(position);
}
再次運行程序,可以看到當賽車經過的時候,障礙物已經在隨機旋轉了。下一步該干什么了呢?Well,讓賽車真正的能和障礙物碰撞將會是很酷的,同時可以記錄下總分來,看看你到底開了多遠。當添加計分系統的同時,你也應該實時的記錄游戲狀態。在游戲引擎的DodgerGame類中,添加如下變量:
private bool isGameOver = true;
     private int fameOverTick = 0;
     private bool hasGameStarted = false;
private int score = 0;
所有關于游戲的信息都儲存在這里。你可以知道游戲是否結束了,是否是第一次開始游戲,上一次和當前的游戲得分。這些都是不錯的特性,不過怎么才能實現他呢?首先從計分系統開始吧,究竟這才是玩家最關心的內容。玩家每經過了一個障礙物,就獲得一定的分數。當然,你也希望游戲更具挑戰性:加快賽道的速度,這樣障礙物也會來到更快。另外,很重要的一點是:在LoadDefaultGameOptions方法中添加一行代碼來重置總分,這樣在新游戲開始的時候玩家才不會獲得額外的分數。
sorce = 0;
接下來,在OnFrameUpdate方法中,在移動障礙物之前,添加如下代碼:
Obstacles removeObstacles = new Obstacles();
     foreach(Obstacle o in obstacles)
     {
         if(o.Depth > car.Diameter - (Car.Depth * 2))
         {
              removeObstacles.Add(o);
              roadSpeed += RoadSpeedIncrement;
              if(roadSpeed >= MaxRoadSpeed)
              {
                   roadSpeed = MaxRoadSpeed;
              }
              car.IncrementSpeed(); 
score += (int)(roadSpeed * (roadSpeed / car.Speed));
              }
         }
         foreach(Obstacle o in removeObstacles)
         {
              obstacles.Remove(o);
              o.Dispose(); 
}
     removeObstacles.Clear();
使用這段代碼獲得了玩家已經經過的障礙物的列表(這個“表”同一時間只應該包含一個元素)。列表中每次增加一個障礙物,就相應的增加總分,增加賽道的速度提升難度,增加賽車的速度(雖然賽車的速度沒有賽道增加的快)。完成了這些操作之后,把障礙物從列表中移除。注重觀察,我們還使用了一個公式來根據賽道的速度計算得分,因此,開的越遠,得分就越多。同時你可能已經注重到我們使用了一個還沒有實現的方法來增加賽車的速度。不用多說,添加代碼:
public void IncrementSpeed()
     {
         carSpeed += SpeedIncrement;
}
現在,需要添加一個新的方法來判定賽車是否撞到了障礙物。在obstacle類中,添加如下代碼:
public bool IsHittingCar(float carLocation,float carDiameter)
     {
         if(position.Z > (Car.Depth - (carDiameter / 2.0f)))
         {
              if((carLocation < 0) && (position.X < 0))
                   return true;
              if((carLocation > 0) && (position.X > 0))
                   return true;
         }
         return false;
}
這里都是些很簡單的東西;首先檢查賽車和障礙物的深度(depth)是否相同,能否發生碰撞,假如相同,并且位于路的同一邊,那么返回ture,否則賽車和障礙物不會碰撞,,返回false。有了這些代碼,就可以在游戲引擎中實現碰撞檢測了。更新OnFrameUpdate方法:
foreach(Obstacle o in obstacles)
     {
         o.Update(elapsedTime,roadSpeed);
         if(o.IsHittingCar(car.Location,car.Diameter))
         {
              isGameOver = true;
              gameOverTick = System.Environment.TickCount;
              Utility.Timer(DirectXTimer.Stop);
         }
}
每次更新了障礙物之后,檢查是否發生了碰撞。假如發生了,那么游戲結束。設置游戲狀態,并且停止計時器。
 
最后一步
至今為止,我們還沒有使用過那些狀態變量。你應該先完成游戲的邏輯設計。你將要求玩家通過按下任意鍵來啟動游戲。游戲結束之后,將會有一瞬間停頓(大約一秒左右),接下來再次按下任意鍵就可以重新啟動游戲。你首先要確定的是一旦游戲結束了,其它狀態就不應該再更新了。因此,OnFrameUpdate方法的第一行應該是這樣的:
if((isGameOver) (!hasGameStarted))
     return;
接下來解決通過按下任意鍵啟動游戲,在OnKeyDown方法重載的最后一行,添加如下的邏輯部分:
if(isGameOver)
     {
         LoadDefaultGameOptions();
     }
     isGameOver = false;
haGameStarted = true;
好了,這就是我們所要求的行為。當游戲結束了,玩家按下任意鍵,一個恢復為默認設置的新游戲就開始了。假如你愿意,可以從InitializeGraphics方法中刪除LoadDefaultGameOptions的調用了,因為在每次按下任意鍵啟動游戲的時候就會調用它。但是,我們還沒有添加碰撞后讓畫面短暫停留一瞬間的代碼。同樣也在OnKeyDown方法中來實現;而且因該在檢查是否按下了ESC鍵之后的添加代碼:
這將會在游戲結束后的一秒之內忽略所有擊鍵(除了可以按下ESC退出游戲)。現在可以嘗試著玩玩我們的游戲了!雖然它們并不完整。我們記錄了得分,卻并沒有顯式的把這個得分告訴玩家。現在來完善這一步吧。Direct3D名稱空間下有一個稱為Font的類可以用來繪制文本。注重,在System.Drawing名稱空間里也有一個Font類,而且假如沒有前綴修飾的情況下使用“Font”,那個這兩個類會發生沖突。幸運的是,可以使用using語句來做如下申明:
Using Driect3D = Microsoft.DirectX.Direct3D;
你所創建的每個字可以是不同的顏色,但最好使用相同的大小以及字體。對這個游戲來說,你需要兩種不同的文本類型,自然,也需要2種不同的字體。為DodgerGame類添加如下變量:
private Direct3D.Font scoreFont = null;
private Direct3D.Font gameFont = null;
你只有在創建了device之后才能初始化這些變量。在創建了device之后的地方添加代碼。它們并不需要在OnDeviceReset事件中初始化,應為這些類會自動處理重置設備時的事件。在InitializeGraphics的最后一行添加如下代碼:
scoreFont = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font("Arial",12.0f,FontStyle.Bold));
     gameFont = new Microsoft.DirectX.Direct3D.Font(device, new System.Drawing.Font("Arial",36.0f,FontStyle.Bold FontStyle.Italic));
     這里創建了兩種不同大小的Arial字體。之后,更新進行渲染的方法來繪制字體。字體是最后才需要繪制的內容,因此在繪制賽車的代碼之后添加如下代碼:
     if(hasGameStarted)
     {
         scoreFont.DrawText(null,string.Format("Current score:{0}",score),
         new Rectangle(5,5,0,0), DrawTextFormat.NoClip,Color.Yellow);
     }
if(isGameOver)
     {
         if(hasGameStarted)
         {
              gameFont.DrawText(null,"You crashed. The game is over.",
              new Rectangle(25,45,0,0),DrawTextFormat.NoClip,Color.Pink);
         }
         if((System.Environment.TickCount - gameOverTick) >= 1000)
         {
              gameFont.DrawText(null,"Press any key to begin.", new Rectangle(25,100,0,0),
              DrawTextFormat.NoClip, Color.WhiteSmoke);
         }
     }
     我們將在后面的章節里具體討論DrawText方法。現在只需要知道它能完成他的名字所表示的功能。好了,現在可以看到當游戲已開始,就可以看到當前的分數了。除此而外,當游戲結束之后,你告訴玩家他撞車了。最后,游戲結束了一秒鐘之后,提醒玩家按任意鍵可以可以重新開始游戲。
     Wow,至今為止,你已經完成了一個完整的游戲了。試玩一下吧。還有什么遺漏的嗎?把最高分保存起來將是不錯的嘗試^_^。
     (注:以下部分作者演示了如何把最高分和玩家的姓名作為一個結構保存到注冊表中,由于大部分是代碼,且這部分內容基本與圖形無關,就不翻譯了)
     First off, we will need a place to store the information for the high scores. We will only really care about the name of the player as well as the score they achieved, so we can create a simple strUCture for this. Add this into your main games namespace:
public struct HighScore
{
          private int realScore;
         private string playerName;
         public int Score { get { return realScore; } set { realScore = value; } }
         public string Name { get { return playerName; } set {
         playerName = value; } }
}
 
Now we will also need to maintain the list of high scores in our game engine. We will only maintain the top three scores, so we can use an array to store these. Add the following declarations to our game engine:
Private HighScore[] highScores = new HighScore[3];
private string defaultHighScoreName = string.Empty;
All we need now is three separate functions. The first will check the current score to see if it qualifies for inclusion into the high score list. The next one will save the high score information into the registry, while the last one will load it back from the registry. Add these methods to the game engine:
private void CheckHighScore()
{
         int index = -1;
         for (int i = highScores.Length - 1; i >= 0; i--)
         {
              if (score >= highScores[i].Score) // We beat this score
        {
            index = i;
        }
      }
 
    // We beat the score if index is greater than 0
    if (index >= 0)
    {
        for (int i = highScores.Length - 1; i > index ; i--)
        {
            // Move each existing score down one
            highScores[i] = highScores[i-1];
        }
        highScores[index].Score = score;
        highScores[index].Name = Input.InputBox("You got a highscore!!","Please enter your name.", defaultHighScoreName);
    }
}
 
private void LoadHighScores()
{
         Microsoft.Win32.RegistryKey key =Microsoft.Win32.Registry.LocalMachine.CreateSubKey("Software//MDXBoox//Dodger");
         try
         {
              for(int i = 0; i < highScores.Length; i++)
              {
                   highScores[i].Name = (string)key.GetValue(string.Format("Player{0}", i),  string.Empty);
highScores[i].Score = (int)key.GetValue(string.Format("Score{0}", i),  0);
              }
              defaultHighScoreName = (string)key.GetValue("PlayerName", System.Environment.UserName);
         }
         finally
         {
              if (key != null)
              {
                   key.Close(); // Make sure to close the key
             
         }
}
 
/// <summary>
/// Save all the high score information to the registry
/// </summary>
public void SaveHighScores()
{
         Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey("Software//MDXBoox//Dodger");
try
         {
              for(int i = 0; i < highScores.Length; i++)
              {
                   key.SetValue(string.Format("Player{0}", i),highScores[i].Name);
                    key.SetValue(string.Format("Score{0}", i),highScores[i].Score);
              }
              key.SetValue("PlayerName", defaultHighScoreName);
}
         finally
         {
              if (key != null)
                 key.Close(); // Make sure to close the key
           }
}
 
I won't delve too much into these functions since they deal mainly with built-in .NET classes and have really nothing to do with the Managed DirectX code. However, it is important to show where these methods get called from our game engine.
The check for the high scores should happen as soon as the game is over. Replace the code in OnFrameUpdate that checks if the car hits an obstacle with the following:
if (o.IsHittingCar(car.Location, car.Diameter))
{
isGameOver = true;
gameOverTick = System.Environment.TickCount;
         Utility.Timer(DirectXTimer.Stop);
         CheckHighScore();
}
You can load the high scores at the end of the constructor for the main game engine. You might notice that the save method is public (while the others were private). This is because we will call this method in our main method. Replace the main method with the following code:
using (DodgerGame frm = new DodgerGame())
{  
         frm.Show();
         frm.InitializeGraphics();
         application.Run(frm);
        frm.SaveHighScores();
}
 
The last thing we need to do is actually show the player the list of high scores. We will add this into our rendering method. Right before we call the end method on our game font, add this section of code to render our high scores:
gameFont.DrawText(null, "High Scores: ", new Rectangle(25,155,0,0),
     DrawTextFormat.NoClip, Color.CornflowerBlue);
for (int i = 0; i < highScores.Length; i++)
{
         gameFont.DrawText(null, string.Format("Player: {0} : {1}",highScores[i].Name, highScores[i].Score), new Rectangle(25,210 + (i * 55),0,0), DrawTextFormat.NoClip,
               Color.CornflowerBlue);
}
  再花一點時間來復習一下所完成的工作吧,這可是你的第一個游戲
可以到這個地方去下載源碼http://bbs.gameres.com/showthread.asp?threadid=32844



發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 原阳县| 银川市| 华宁县| 淮阳县| 美姑县| 涪陵区| 浮梁县| 克拉玛依市| 衡阳市| 富蕴县| 朝阳市| 渑池县| 三亚市| 清新县| 龙州县| 大悟县| 开原市| 新营市| 镇平县| 墨玉县| 福清市| 左权县| 资阳市| 土默特右旗| 蛟河市| 长葛市| 建德市| 株洲县| 陇川县| 阳江市| 丹巴县| 云霄县| 太湖县| 嘉兴市| 泾阳县| 武城县| 广安市| 依兰县| 德兴市| 北海市| 林州市|