使用Managed DirectX编写游戏
轉自 ?http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_01.htm
?? ? ??http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_02.htm
?? ? ??http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_03.htm
?? ? ??http://dev.gameres.com/Program/Visual/DirectX/ManagedDirectX9Game_04.htm
3D游戲編程,顯然,這是一個很復雜的主題。首先,讓我們來學習一些編寫任何游戲都會用到的基礎知識。把你的創意轉變為真實的游戲是一種很有趣的經歷。把你的想法雕琢為一個人們可以獲得樂趣的游戲,是每個開發者的目標。
所有的一切都來源于創意,游戲也是如此。游戲創意可以來自于生活中所有地方。也許你看到了另一個很好玩的游戲,但總覺得稍做修改它將會更好玩。也許你昨晚的夢就能創造一個完美的游戲。無論你的靈感來自于哪里,都必須先有了靈感再編寫游戲。
當然,對于我們即將要編寫的游戲來說,你將使用我的創意。選擇這幾個游戲是有很多原因的,但主要原因還是和難度有關。無論如何,我們將會盡可能的覆蓋所有你可能遇到的游戲類型。
第一種類型是益智游戲(puzzle game)。經典的Tetris(俄羅斯方塊)就是這類游戲的代表。幾乎所有學習游戲編程的人都克隆過這個游戲,因此,跳過它,編寫一些特別一點的東西。游戲中將有一個可以在板子上跳動的角色。板子由一系列的立方體構成,類似一個棋盤。每個立方體都有一種特定的顏色,當角色跳到這些立方體時,他將會變為另一種指定的顏色。當所有方塊都和這一關制定的顏色相同時,這一關就算贏了。
設計細節
當游戲創意浮現出來之后,就應該花一些時間把它寫為游戲方案。并確保對所有可能遇到的問題都有解決方法。對于這個游戲來說,我們列出了一下條目作為方案的細節。
*名字叫做Blockers的益智游戲
*單人游戲
*全3D環境
*基于完成每一關的時間來計算得分
*每一關都由一系列相連的立方體構成,就像建立在棋盤上一樣
*每個立方體都是單獨的實心顏色
*當每一個立方體的顏色都和本關預定的結束顏色相同時,管卡結束
*每個立方體都有一個顏色列表,這個列表最多包含6種顏色,最少2種
*玩家通過調到另一個立方體來移動,同時,所跳到的立方體變為列表中的下一個顏色
*游戲開始時,顏色列表中只有2種顏色
*高級關卡通過增加顏色列表中的顏色來增減難度
*如果玩家通關了,難度將回到一開始的狀態
*如果玩家不能在最大的制定時間完成本關,則游戲結束。
這是否是一份毫無遺漏的文檔了呢?也許不是吧。但他回答了大部分我們將如何編寫游戲的問題。明白在開發之前,你不可能把所有性都設計好了是很重要的,隨著開發和思考的深入,游戲將會需要越來越多的特性,但是,無論如何都不能貿然在沒有考慮到底想完成些什么特性時就開始編碼,這會導致更大的混亂。
有了方案,就可以開始設計游戲中的對象模型了。你將有一個神奇的游戲引擎,由他來維護玩家信息、當前關卡、以及渲染的設備。渲染的設備用來完成所有美妙的繪圖工作。玩家總是需要一些直觀的表示,渲染設備會處理好這個工作。
大多數的游戲都需要當前關卡的信息。當前的時間是最重要的,因為最終的得分和是否結束游戲都是由他來決定,因此,當前的管卡必須可以訪問時間。
實際的關卡將被儲為文件,保存在程序的media目錄中。當前的關卡必須是所存在關卡中的一個,所以關卡必須能訪問這些文件。實際上,關卡只要追蹤組成每一關的立方體列表就可以了。每一關最少有兩個立方體,當然高級別的管卡中將添加大量立方體。
雖然編寫游戲不會很容易,但你看,要達到目標,所創建的對象并不多。另外,為了讓游戲有具有可玩性,只要讓高級關卡不會難到使玩家有挫敗感就可以了。沒有比讓玩家苦惱的游戲更糟的東西。如果游戲不好玩,那么就不會有人玩它,就不會成為成功的游戲。
為什么需要3D游戲
你需要認識到這個游戲(實際上是所有游戲)根本不“需要”作為全三維的場景來渲染。考慮到所有顯示器都是平面的,再好看的3D場景都要映射為2D的圖片。完全可以把游戲中所有可能的場景設置為一組2D的精靈(sprite),當然,這樣需要更多的美工操作。
假設你安裝了DirectX SDK的任何版本,打開DirectX Sample Broeser,確保只選擇了左邊的C#選項,點擊左上的Direct3D標題,找到Empty Project項目,并安裝它。之后,在VS中打開項目。
這樣我們就創建了一個“空白”項目。(實際上它還繪制了一些UI控件,但先忽略他們)。但是,這里還沒有任何3D 的物體,這一部分的內容是告訴你為什么我們需要編寫3D游戲,因此,來繪制一些東西吧。
添加幾行代碼來顯示一個緩慢旋轉的茶壺。添加如下變量
private Mesh teapotMesh = null;
private Material teapotMaterial;
接下來創建茶壺和材質,在OnCreateDevice方法的最后添加如下代碼:
teapotMesh = Mesh.Teapot(e.Device);
teapotMaterial = new Material();
teapotMaterial.DiffuseColor = new Colorvalue(1.0f,1.0f,1.0f,1.0f);
為了讓茶壺更好看,接下來添加燈光,在OnResetDevice方法中添加代碼:
e.Device.Lights[0].DiffuseColor = new Colorvalue(1.0f,1.0f,1.0f,1.0f);
e.Device.Lights[0].Direction = new Vector3(0,-1,0);
e.Device.Lights[0].Type = LightType.Directional;
e.Device.Lights[0].Enabled = true;
準備工作到這里就結束了。找到OnFrameRender方法,在BeginScene方法之后添加代碼:
e.Device.Transform.View = camera.ViewMatrix;
e.Device.Transform.Projection = camera.ProjectionMatrix;
e.Device.Transform.World = Matrix.RotationX((float)appTime);
e.Device.Material = teapotMaterial;
teapotMesh.DrawSubset(0);
運行程序,可以看到一個渲染的茶壺。茶壺在3D世界里有一段悠久的歷史。在那個建模軟件還不發達的時代,獲得一個真實的3D模型相當困難,而茶壺就是最早供人們免費使用的模型之一。另外,茶壺有許多不錯的特性適合于用來做測試:它有圓滑的曲面,可以顯示光影漸變的效果,并且容易識別。
需要注意的是,雖然茶壺是這個程序里唯一的模型,但我們并沒有使用任何額外資源來省城模型,而是使用了Mesh類的一個靜態方法。我們使用了最少的資源得到了一個不錯的茶壺。
對于3D版本來說,我們可以從任何角度觀察茶壺。但是假設我們需要在2D世界里實現同樣的效果呢?我們需要為茶壺旋轉的每一個角度保留一張位圖(就是360張圖),如果還想讓茶壺沿任意軸旋轉,那么就需要4664600張不同的位圖。那么對于虛幻競技場這樣的游戲來說,就算他什么都不干,只繪制2D精靈,也至少需要一張DVD來保存數據,同時還要有一大群藝術家花上幾年時間來創建這些東西。
如果你的藝術家可以創建高質量的3D模型,顯然,你就可以使用相對很少的資源更加自由的創建場景 。可以用許多不同方法來渲染一個簡單的模型,比如不同的燈光、縮放比例、位置、角度等等,而這一切使用2D來實現都是不切實際的。
3D程序的這種自由性需要強大的處理能力來實現,為了滿足這種處理能力,甚至形成了一整個產業。nVidia和ATI就是這個產業中的領導者。現代的圖形卡有了飛速的創新,甚至在處理能力上超越了CPU。
現在的圖形卡(支持DirectX9的圖形卡意味著他至少支持shader model2.0)每秒能渲染數百萬三角形。不要擔心,我們稍候會討論關于著色器(shader)的內容。如果你想知道為什么需要那么多三角形,那么你需要知道三角形是構成3D模型的基本多邊型。以下就是線框模式之下的茶壺模型。
你看,整個茶壺都是由三角形構成。為什么使用三角形呢?因為它是唯一能夠保證共面的閉合多邊形,這樣,在渲染時更容易計算。任何3D圖形都可以用三角形模擬出來。
至此,我們的游戲需要是3D的嗎?當然不需要,你可以完全使用精靈來編寫游戲,但這還有什么樂趣呢?大部分游戲編成的書籍都只涉及2D部分,但3D世界才是真正讓游戲精彩的地方。我們即將開始三維之旅。
編寫文檔
接下來,我們將來到編寫游戲最重要的一個環節,編寫文檔。在一行代碼都沒有編寫的情況下,我實在無法強迫你花一點時間來考慮可能遇到的問題。事實上,我遇到的所有年青游戲開發者在開發第一個游戲時都是直接開始編碼。慢慢你才會明白快速的開始編碼會帶來很多麻煩。對于這個游戲,需要解決什么問題?當然,需要一個游戲引擎作為游戲的大腦。一個玩家對象,一個用于渲染的設備,以及一個維護關卡信息的方法。最常見的現實開發文檔就是UML。如下圖所示:
如果你熟悉UML,那么各熟悉之間的關系應該是很明顯的。如果不熟悉,就需要花點時間來看看這篇文檔在說什么。首先,他把問題分成了組成游戲的幾個邏輯組件。這里,四個對象分別是游戲引擎、玩家、關卡、方塊列表,以及單獨的方塊。每個對象的屬性和方法都列在了對象盒里,這樣就可以快速的對每個對象有一個總攬。
把圖放到一邊,我們到底需要做些什么呢?顯然,需要有一個控制中心來控制所有操作。在這里,就是游戲引擎。注意看UML圖,游戲引擎包含了用于渲染的device(從InitializeGraphics方法可以看出來)。游戲引擎需要知道以下東西:
*玩家對象
*當前關卡
*游戲是否結束
*如果是,那么玩家是否贏了當前關卡
*如果是,那么玩家是由通關了
游戲引擎還需要保存其他信息,比如渲染的設備,以及一些其他對象,但這些都是私有方法,因此,不現示在UML中。接下來的對象是玩家,這是一個相當簡單的類,我們只需要知道它的位置,同時,讓他可以渲染它自己就可以了。在我們的引擎里,玩家更像一個抽象的概念而不是實際對象。這個對象主要是為了控制任何現實玩家。
游戲引擎所需的其它信息都來自于levels對象。實際上這也是一個很簡單的對象,他也只包含了幾個其他對象,比較重要的就是blocks集合。每一個方塊包含了在不同層次如何控制它自己的信息,包括可能的顏色列表,以及是否需要翻顏色。
好了,有了這最最簡單的文檔,可以開始編碼了。
?
理解框架
首先創建工程,添加對DirectX程序集的引用。接下來,把sample framework添加到工程中。我們把這些文件放到一個單獨的文件夾中,在解決方案管理器中點擊右鍵---添加---新建文件夾,并把它命名為framework。右鍵點擊新創建的文件夾,選擇添加現有項,導航到SDK的\Samples\Managed\Common目錄下,把每一個文件添加到項目中。
好了,現在回到我們剛才創建的Form1.cs文件中來,可以看到大部分自動生成的代碼都是用來創建Windows Form應用程序的。因此刪除所有代碼,并添加如下代碼:
using System;
using System.Configuration;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using Microsoft.Samples.DirectX.UtilityToolkit;
public class GameEngine : IDeviceCreation
{
??? /// <summary>
??? /// Entry point to the program. Initializes everything and goes into a
??? /// message processing loop. Idle time is used to render the scene.
??? /// </summary>
??? static int Main()
??? {
??????? using(Framework sampleFramework = new Framework())
??????? {
??????????? return sampleFramework.ExitCode;
??????? }
??? }
}
這里有三個需要注意的地方 。首先,我們只留下了一個修改過的main方法。由于其他代碼都是窗體設計器為Windows Form程序生成的,所以完全可以刪除它們。其次,代碼現在還不能通過編譯,應為GameEngine類還有兩個接口沒有實現。第三,這段代碼實際上什么也沒做。
首先我們來實現IDeviceCreation接口,你將通過他來控制枚舉和創建device。在這里,枚舉的含義包括檢查目標機器上有幾塊顯卡、幾個顯示器、可以支持多少種顯示模式、控制刷新率等等。
public bool IsDeviceAcceptable(Caps caps, Format adapterFormat,Format backBufferFormat, bool windowed)
{
??? if (!Manager.CheckDeviceFormat(caps.AdapterOrdinal, caps.DeviceType,adapterFormat, Usage.QueryPostPixelShaderBlending, ResourceType.Textures, backBufferFormat))
??????? return false;
??? if (caps.MaxActiveLights == 0)
??????? return false;
??? return true;
}
public void ModifyDeviceSettings(DeviceSettings settings, Caps caps)
{
??? if ( (caps.DeviceCaps.SupportsPureDevice) && ((settings.BehaviorFlags & CreateFlags.HardwareVertexProcessing) != 0 ) )
??????? settings.BehaviorFlags |= CreateFlags.PureDevice;
}
第一個方法將在device初始化時調用,用來檢查device最低能支持什么特性(capability)。當sample framework在系統上枚舉設備時,他會對所找到的每一種可能組合調用這個方法。注意看這個方法是如何決定返回值的。他的第一個參數包含了大量關于制定設備的信息,可以幫你決定它是否是你希望創建的device類型。接下來的參數一個是關于后備緩沖的,另一個則是關于設備格式。最后一個參數則是檢查是否支持窗口模式。雖然大多數游戲都是運行在全屏模式,但在窗口模式之下調試程序會方便一些。這個方法默認情況下將返回true,但他還做了兩個特別的檢查。首先,檢查它是否支持alpha混合(創建游戲的用戶界面將用到他)。其次,檢查是否支持動態燈光----沒有燈光物體看起來會很單調而且不真實,所以至少使用一個燈光。
再來看第二個方法:在創建device之前調用它,來修改創建設備的參數。Setting參數包含了框架為device所制定的設置,你可以自由的修改這些設置。需要特別注意的是sample framework不會驗證這些設置的有效性,因此,你需要自己來驗證。
在繼續之前,還有一件事情要做。由于sample framework包含一些unsafe的代碼塊,因此,必須允許工程中包含不安全代碼:
現在,可以使用框架來枚舉設備了。首先,為GameEngine類添加一個構造函數,把從main方法中創建的sample framework實例作為參數:
private Framework sampleFramework = null;
public GameEngine(Framework f)
{
??? // Store framework
??? sampleFramework = f;
}
在調用了sample framework之后,他所做的第一件事就是枚舉系統設備。在工程中,打開之前添加的dxmutenum.cs文件。這個文件包含了枚舉設備的所有代碼。由于知道如何以及為什么枚舉設備是很重要的,我們來仔細看一下這些代碼。
首先注意到Enumeration是不能被實例化的,塔頂每一個方法和成員也都是靜態的。因為就目前來說,你的硬件圖形設備在運行時不太可以改變,所以枚舉的過程在程序一開始運行一次就可以了。
大多數的枚舉過程都是在創建device之前,通過sample framework調用Enumerate方法開始的。這個方法所接受的唯一參數,就是我們至今為止在GameEngine類中實現的接口。在枚舉設備狀態組合的時候,需要調用IsDeviceAcceptable方法來判斷這個設備狀態是否應該添加到正確的設備列表中。那么到底是如何來枚舉設備的呢?實際上大多數功能都是通過Manager類來完成的。如果你熟悉普通的DirectX API,那么這個類實際上就是COM接口Idirect3D9的映射。(注:枚舉設備的過程這里不再講解,請看我以前翻譯過的文章)
把所有符合框架要求的顯示模式都保存到一個列表中,最后,通過實現Icomparer接口對他們進行排序。
public class DisplayModeSorter : IComparer
{
??? public int Compare(object x, object y)
??? {
??????? DisplayMode d1 = (DisplayMode)x;
??????? DisplayMode d2 = (DisplayMode)y;
??????? if (d1.Width > d2.Width)
??????????? return +1;
??????? if (d1.Width < d2.Width)
??????????? return -1;
??????? if (d1.Height > d2.Height)
??????????? return +1;
??????? if (d1.Height < d2.Height)
??????????? return -1;
??????? if (d1.Format > d2.Format)
??????????? return +1;
??????? if (d1.Format < d2.Format)
??????????? return -1;
??????? if (d1.RefreshRate > d2.RefreshRate)
??????????? return +1;
??????? if (d1.RefreshRate < d2.RefreshRate)
??????????? return -1;
??????? return 0;
??? }
}
這里的算法很簡單,大家自己看吧。保存了可用的顯示模式之后,調用EnumerateDevices方法。
private static void EnumerateDevices(EnumAdapterInformation adapterInfo, ArrayList adapterFormatList)
{
??? // Ignore any exceptions while looking for these device types
??? DirectXException.IgnoreExceptions();
??? // Enumerate each Direct3D device type
??? for(uint i = 0; i < deviceTypeArray.Length; i++)
??? {
??????? // Create a new device information object
??????? EnumDeviceInformation deviceInfo = new EnumDeviceInformation();
??????? // Store the type
??????? deviceInfo.DeviceType = deviceTypeArray[i];
??????? // Try to get the capabilities
??????? deviceInfo.Caps = Manager.GetDeviceCaps((int)adapterInfo.AdapterOrdinal, deviceInfo.DeviceType);
??????? // Get information about each device combination on this device
??????? EnumerateDeviceCombos( adapterInfo, deviceInfo, adapterFormatList);
??????? // Do we have any device combinations?
??????? if (deviceInfo.deviceSettingsList.Count > 0)
??????? {
??????????? // Yes, add it
??????????? adapterInfo.deviceInfoList.Add(deviceInfo);
??????? }
??? }
??? // Turn exception handling back on
??? DirectXException.EnableExceptions();
}
觀察一下這段代碼,你應該注意到并且記住兩件事。猜猜是什么?如果答案是DirectXException類方法調用,那么恭喜,答對了。 DirectXException.IgnoreExceptions();方法關閉了Managed DirectX程序集中所有異常拋出。你可能會問這樣做有什么好處,答案是性能的提升。捕捉和拋出異常是系統花費很大的操作,而這段代碼有可能導致多個異常的拋出。你只希望快速的完成枚舉,因此,暫時忽略所有異常,在這段代碼結束的時候,再打開異常機制。雖然這里的代碼很簡單,但你可能會問問什么會導致異常。
很高興你問到了這個問題,那么我們就來仔細討論一下吧。最常見的問題就是你的設備不支持DirectX 9。有可能你沒有更新驅動程序,或者當前的驅動程序沒有正確安裝。也有可能是你的顯卡太老了無法使用DirectX 9。通常PCI接口的顯卡都不能很好的支持DirectX 9了。
這段代碼嘗試獲得關于顯卡能力的信息,并為適配器枚舉合適的組合。可用的設備類型有以下幾種:Hardware, Reference, software。
假設枚舉時找到了合適的設備設置組合,就把他保存到一個列表中。Enumeration類為之后創建device保存了一些列表,觀察EnumerateDeviceCombos方法。注意,如果IsDeviceAcceptable方法返回false,那么就忽略這種設備組合類型。
?
理解sample framework之事件處理
Framework類是sample framework中最重要的類,完成了創建窗體,初始化設備,創建命令行,事件處理(render loop)以及調節各種參數的任務。Framework類包含在dxmut.cs文件中。其中,比較特別的就是事件處理模型(或render loop)。
為了獲得高性能的渲染以及事件處理機制,framework類在初始化的方法中使用Device.IsUsingEventHandlers = false;關閉了事件處理模型。我們先來看看為什么默認的事件處理機制會導致性能的損失。默認情況下,Managed DirectX中的類在每創建一個資源時,都會為device訂閱一些必然的事件。在最簡單的情況下,每個資源(比如紋理或頂點緩沖)將會訂閱Disposing事件,以及其他一些諸如DeviceLost和DeviceReset的事件。這個步驟在整個對象的生存期都會發生。但是為什么我們在程序中不需要這種行為呢??
主要原因就是需要對這種行為付出一定代價,有些情況下,代價還會很大。我們舉個例子來說明這一點。看看下面這段簡單的偽代碼:
SomeResource res = new SomeResource(device);
device.Render(res);
這段代碼看起來幾乎是“無害”的。只是創建了一個資源,并且渲染它。當這個對象不再使用時,垃圾回收器應該會智能的清除它。但是,這個想法是完全錯誤的。當創建新資源時,至少需要對device訂閱一個事件,允許device正確的清除它。這種訂閱實際上是一把“雙刃劍”。
首先,訂閱事件時需要分配(allocation)EventHander對象完成實際的訂閱工作。雖然這種分配的代價很小,但是,我們稍候就會看到,就算是很小的分配也會迅速膨脹。第二,訂閱事件之后,資源和設備之間就有了一個硬連接(hard link)。因此,在垃圾回收器的眼里,這個對象在device的生存期里仍然處于使用狀態,并且沒有取消事件的訂約。設想一下,這代碼如果在渲染每幀的時候都運行一次;再設想一下,你的程序每分鐘需要進行上千次渲染,并且程序已經運行了兩分鐘。結果,你創建了120000個在device生存期間不會被回收的對象,以及120000個事件句柄。所創建的這些對象不但會迅速消耗內存,而且會導致額外的垃圾回收,嚴重影響程序性能。如果你的資源都位于顯存中,那么很快就會耗盡顯存。
這里,我們還沒有考慮當最后釋放設備時可能發生的情況。在前面的例子中,當釋放device時,首先觸發Disposing事件,此時,有120000個監聽者訂約了這個事件。你是否已經考慮到,調用這個巨大的事件句柄列表會將花費很多時間?實際上這將會花去幾分鐘時間,并且讓用戶認為程序已經處于死鎖狀態。
因此,最好只在最簡單的Direct3D程序中使用Managed Direct3D內建的事件處理機制。在任何需要考慮內存容量和性能的應用中(比如游戲),都必須避免這些處理過程。
接下來,我們來看看framework中是如何實現事件處理模型的。實際上,SDK中的事件處理模型也是幾經修改,現在使用的方法最早由Tom Mille 在他的bolg上貼出了具體的實現:
public void MainLoop()
{
??? // Hook the application's idle event
??? System.Windows.Forms.Application.Idle += new EventHandler(OnApplicationIdle);
??? System.Windows.Forms.Application.Run(myForm);
}
private void OnApplicationIdle(object sender, EventArgs e)
{
??? while (AppStillIdle)
??? {
??????? // Render a frame during idle time (no messages are waiting)
??????? UpdateEnvironment();
??????? Render3DEnvironment();
??? }
}
private bool AppStillIdle
{
??? get
??? {
??????? NativeMethods.Message msg;
??????? return !NativeMethods.PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
??? }
}
And the declarations for those two native methods members:
[StructLayout(LayoutKind.Sequential)]
public struct Message
{
??? public IntPtr hWnd;
??? public WindowMessage msg;
??? public IntPtr wParam;
??? public IntPtr lParam;
??? public uint time;
??? public System.Drawing.Point p;
}
[System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
[DllImport("User32.dll", CharSet=CharSet.Auto)]
public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
這里,通過平臺調用,使用了一些Win32 API。首先,在main方法中訂閱了Application.Idle事件。在程序處理完了所有消息(如果不熟悉消息,那么可以把消息理解為系統定義的一個32位的值,他唯一的定義了一個事件,向Windows發出一個通知,告訴應用程序某個事情發生了。例如,單擊鼠標、改變窗口尺寸、按下鍵盤上的一個鍵都會使Windows發送一個消息給應用程序。)之后,將會觸發Application.Idle事件。我們的目標是讓程序盡可能快,盡可能多的處理消息,同時不打斷wendows消息的輸入。
在OnApplicationIdle事件處理程序中,使用的了簡單的Win32 API PeekMessage來檢查程序是否有任何未處理的消息。這里使用while循環的原因是保證在處理完所有消息,同時消息隊列還為空時,只觸發一次Application.Idle事件。所以,我沒一直循環,直到有新的消息,然后,跳出循環。普通的.Net WinForm窗體消息句柄將會選擇未處理的消息。
接下來,我們將使用框架,來顯示一些物體了(源碼請參考SDK中的empty project)。由于框架已經為我們完成了以上工作。我們只需要選擇訂閱那些事件就可以了。Sample framework通過這些事件通知應用程序關于改變設備、用戶輸入以及各種窗口消息。這些事件是可選的,但是,如果你沒有設置,那么框架就不會為你處理相應的事件。在main方法中,創建了GameEngine對象之后,添加代碼:
sampleFramework.Disposing += new EventHandler(blockerEngine.OnDestroyDevice);
sampleFramework.DeviceLost += new EventHandler(blockerEngine.OnLoseDevice);
sampleFramework.DeviceCreated += new DeviceEventHandler(blockerEngine.CreateDevice);
sampleFramework.DeviceReset += new DeviceEventHandler(blockerEngine.OnResetDevice);
sampleFramework.SetWndProcCallback(new WndProcCallback(blockerEngine.OnMsgProc));
sampleFramework.SetCallbackInterface(blockerEngine);
(注意,雖然在SDK October 2005的文檔中還可以查到framework對象的SetKeyboardCallback方法,但實際上這個方法已經被刪除了,老版本的SDK示例中使用了整個方法。)
這一段代碼作了很多工作,首先,為四個事件訂閱了事件處理程序,分別是創建設備,失去設備,重置設備,銷毀設備。我們將在后面實現這些事件處理程序。SetWndProcCallback方法訂閱了處理windows消息的方法。隨后,使用當前game engine實例作為參數,調用SetCallbackInterface方法。之后,編寫事件處理程序:
private void OnCreateDevice(object sender, DeviceEventArgs e)
{
??? SurfaceDescription desc = e.BackBufferDescription;
}
private void OnResetDevice(object sender, DeviceEventArgs e)
{
??? SurfaceDescription desc = e.BackBufferDescription;
}
private void OnLostDevice(object sender, EventArgs e)
{
}
private void OnDestroyDevice(object sender, EventArgs e)
{
}
public IntPtr OnMsgProc(IntPtr hWnd, NativeMethods.WindowMessage msg, IntPtr wParam, IntPtr lParam, ref bool noFurtherProcessing)
{
}
由于之前的SetCallbackInterface需要接收一個IframeworkCallback的變量作為參數,但是我們的game engine類并沒有實現這個類,所以添加以下代碼:
public class EmptyProject : IFrameworkCallback, IdeviceCreation
實現這個接口所定義的方法:
public void OnFrameMove(Device device, double appTime, float elapsedTime)
{
}
public void OnFrameRender(Device device, double appTime, float elapsedTime)
{
}
哦,框架性的東西總算是弄的差不多了。在SetCallbackInterface之后加上以下代碼:
try
{
??? sampleFramework.SetCursorSettings(true, true);
??? sampleFramework.Initialize( false, false, true );
??? sampleFramework.CreateWindow("haha");
??? sampleFramework.Window.KeyDown += new System.Windows.Forms.KeyEventHandler(blockerEngine.onKeyEvent);
??? sampleFramework.CreateDevice( 0, true, Framework.DefaultSizeWidth, Framework.DefaultSizeHeight, blockerEngine);
??? sampleFramework.MainLoop();
}
#if(DEBUG)
catch (Exception e)
{
??? sampleFramework.DisplayErrorMessage(e);
#else
??? catch
??? {
??????? // In release mode fail silently
#endif
??????? // Ignore any exceptions here, they would have been handled by other areas
??????? return (sampleFramework.ExitCode == 0) ? 1 : sampleFramework.ExitCode; // Return an error code here
??? }
??? return sampleFramework.ExitCode;
}
}
現在運行程序看看,雖然只是一個藍色的窗口,但是我們背后所搭建的框架已經可以實際應用到一個游戲之中了。為了讓程序開起來有一點點交互,我們還訂閱了鍵盤事件,通過空格鍵可以改變程序的顏色。
使用Managed DirectX創建三維地形
使用Height Map作為輸入
首先,什么是高度圖(Height Map)呢?所謂高度圖實際上就是一個2維數組。創建地形為什么需要高度圖呢?我們這樣考慮,地形實際上就是一系列高度不同的網格而已,這樣數組中每個元素的索引值剛好可以用來定位不用的網格(x,y),而所儲存的值就是網格的高度(z)。正是由于這個簡單的映射關系,最常見的地形生成方法都使用高度圖作為輸入數據。同時,為了減小數組的尺寸,通常使用Byte類型來保存高度值,因此,地形中最低點將用0表示,而最高點使用255表示(當然,這樣做可能會出現一些問題,比如,地形中大部分區域的高度差別都不大,但是有少數地方高度差特別大時,不過大多數情況下這個系統都能運行的很好)。使用2D Byte數組的另一個好處就是我們高度圖剛好可以用一張灰度位圖(grayscale bitmap) 來表示。對于位圖中的每個像素來說,同樣使用0~~255之間的值來表示一個灰度。這樣,我們又能把不同的灰度映射為高度,并且用像素索引表示不同網格。
那么如何來創建高度圖呢?有兩種方法:直接使用程序創建2D數組或者使用其他繪圖軟件創建灰度位圖。先來看看兩種方法的優缺點。直接創建數組,通過特定算法填充每個元素的值(只為每個元素賦隨即值是不可行,這樣會導致你的地面看起來極度不真實,不連續的高度值可能創建出很扭曲的地形。),你不需要任何額外的工具就能創建地形。但是,通過這種方法創建的地形基本是隨機的,雖然可以通過調節算法的參數控制大概的地形形狀,卻不能精確控制每個點應該凹下還是凸起。而使用灰度圖,你不必掌握復雜的地形生成算法,可以把3維軟件建好的地形模型渲染為灰度圖,也可以使用通過衛星采樣的圖片作為灰度圖。我們的示例程序將使用后一種方法,不過首先,我們還是來看看完全使用程序生成地形的算法。
使用Midpoint Displacement方法生成高度圖
這里我們介紹一種比較常用,也比較簡單的地形生成算法,稱為Midpoint Displacement中點偏移算法。使用這個方法,我們先創建一張平坦的高度圖,然后再來升高或降低不同的網格創建隨機地形。為了避免生成的值是完全沒有規則的,我們先把整個平面分為4個正方形區域,接下來重復對這四個正方形進行同樣的分割,同時,調整每個正方形頂點的高度。隨著細份層次的增加,相應減少頂點高度調整的幅度。?
使用[0,255]之間的浮點值來進行調整,以保證最后能用8位的灰度值來表示所有高度。每一步,都在一個確定范圍內產生一個隨機值來作為頂點偏移值。對于第一步來說,隨機值將在[-128,128]之間(為了方便說明,我們把這個隨機值范圍記為[-delta,delta]產生,并且賦給上圖左邊的A,B,C,D四個頂點。接下來,用虛線把它分為4個小區域,這將創建5個新的頂點。計算每個新頂點所在邊兩個頂點高度的平均值作為這個點的基準值(比如 把點A和B的高度平均值作為點1的基準值),其中,點5的基準值是由四個頂點A,B,C,D的平均值來決定的。再計算[-delta,delta]之間的一個隨機值,對基準值進行偏移,作為這個點的最終值。5個點的值都計算完畢之后,我就調到下一階段,使用同樣的方法,計算個頂點值,如上圖右邊所示。
為了引導地形的產生,再把delta和一個縮放因子相乘。我們把這個因子稱為roughness,它是一個1~0之間的值,這樣,每個階段都會減小delta的值。
delta = delta * roughness
roughness的值越大,地形起伏就越明顯,而越小,相應的地形也就越平坦。
使用Perlin Noise生成高度圖
任何沒有討論噪聲函數的程序地形算法都是不完整的。最重要的噪聲函數就是Perlin Noise。他幾乎是現代圖形軟件包生成各種火焰,云彩,奇形怪狀的巖石,以及樹木和大理石表面等許多應用的基礎。這里不對Perlin Noise的理論做詳細介紹,我們主要看看如何使用它為我們的地形添加噪聲。
Perlin噪聲可以適用于任何維度的空間,但這里我們只討論二維的情況。本質上,2D Perlin噪音就是對每個網格頂點法線的一種插值,來仔細看看這個技術吧。
首先,使用網格把整個圖片劃分為幾個不同部分。如上圖所示,我們使用了一個4X4的網格來劃分整個圖片。這里,網格的多少控制著噪聲的復雜性。網格越多,噪聲越密集(tiger),類似于電視沒有信號時顯示出的雪花點;而網格越少,噪聲的波形就越明顯,類似于云朵的效果。
對于每個網格頂點我們都分配一個隨機法線(normal vector)。這些法線實際上就是一些指向不同方向的單位矢量而已。這里,常見的方法是創建一張有256個指向不同方向(形成一個圓周)的向量查找表。然后為每個網格隨機分配一個向量,如上圖所示。
對于圖片中的每個像素來說,我們先找到包含它的網格單元。然后,再創建4個從網格頂點指向所要計算的像素的方向矢量,如下圖所示。現在,每個網格頂點有2個向量:一個隨機的單位向量以及一個指向像素的方向向量。計算每對向量的點積,把它作為每個網格頂點的梯度高度值(scalar height value)。接下來,混合這4個值決定所計算像素的高度。這里,不同的混合方法可以產生不同效果,最常見的就方法就是通過目標像素與每個頂點位置的權重來計算。
我們將執行3次混合操作。首先需要計算混合權重。使用如下公式:
W = 6t^5 – 15t^4 + 10t^3 (^符號表示冪運算)
其中w表示權重,t根據需要替換為x或y值。這個方法與最早Perlin提出的公式(w = 3t^2 – 2t^3)有些區別。它雖然計算起來比較慢,但效果要好得多。
首先,計算x方向上的權重,使用公式:
V = Ca(w) + Cb(1-w)
混合網格上邊的兩個頂點。其中Ca和Cb分別為上面兩個頂點的梯度高度值,w是上一個公式計算出的權重值。然后,使用同樣的方法混合下面兩個頂點。最后,使用前兩部混合的結果,以及y方向上的權重再進行一次混合。最后為這個像素計算出的高度值位于[0,1]之間,我們再把它縮放為相應的灰度值。
舉個例子,假如網格上邊兩個頂點的坐標分別為Ca[2,0]和Cb[8,0],梯度高度值分別為h0和h1,所求像素位置為[4,2],那么兩個頂點指向這個像素的矢量就是:
Vector2 d0(4 -2,2-0)
Vector2 d1(4-8,2-0);
X軸方向的權重就為:
Sx = 6*d0.x^5 – 15d0.x^4 + 10d0.x^3
相應的插值就為:
avgX0 = h0*Sx + h1(1 –Sx)
如果下面兩個頂點的插值為avgX1,則最后的插值就是:
Result = avgX0 * Sy + avg2(1- Sy)
通常情況下,為了獲得真實的地形,會選取不同網格粒度,分別對圖像進行多次Perlin噪音處理,最后把這些處理過的圖加到一起,獲得最終結果。
生成地形
現在來看看如何把高度圖轉變為為多邊形網格。一開始就說過,把高度圖中像素的x,y值轉換為頂點的x,y值,把像素的顏色值轉換為頂點高度。我們可以把這些值縮放為所需要的尺寸。
每2X2個像素就對應著2X2個頂點,同時可以組成2個三角形。可以把把頂點數據儲存為一個簡單的(x,y,z)列表,三角形數據儲存為三個索引值一組的頂點列表。這兩個列表之后就轉變為頂點緩沖和索引緩沖。
public class Terrain
{
??? private Device device;
??? private VertexBuffer vb;
??? private IndexBuffer ib;
??? private int numVertices, numIndices, numTriangles;
??? //保存從高度圖中提取的數據
??? float[,] heights;
??? //地形大小
??? private float terrainSize;
??? public unsafe Terrain(Device d,float Min, float Max,float terrainSize)
??? {
??????? device = d;
??????? //加載高度圖
??????? Bitmap heightMap = new Bitmap(@"..\..\heightmap.bmp");
??????? //根據位圖大小創建數組
??????? heights = new float[heightMap.Width,heightMap.Height];
??????? //鎖定數據
??????? BitmapData data = heightMap.LockBits(new Rectangle(0,0,heightMap.Width,heightMap.Height, ImageLockMode.ReadOnly,PixelFormat.Format24bppRgb);
??????? //獲得位圖中第一個像素的地址
??????? byte* p = (byte*) data.Scan0;
??????? //遍歷位圖,獲得最高和最低點的灰度值
??????? byte lowest = 255;
??????? byte hightest = 0;
??????? for(int i=0;i<heightMap.Width;i++)
??????? {
??????????? for(int j=0;j<heightMap.Height;j++)
??????????? {
??????????????? if ( *p < lowest)
??????????????????? lowest = *p;
??????????????? if( *p > hightest)
??????????????????? hightest = *p;
??????????????? //由于每個像素是24位,而指針是8位,所以+3指向下一個像素
??????????????? p += 3;
??????????? }
??????? }
??????? //填充數組,max表示地形最高點的位置,min標志最低點。
??????? p = (byte*) data.Scan0;
??????? for(int i=0;i< heightMap.Width;i++)
??????? {
??????????? for(int j=0; j< heightMap.Height; j++)
??????????? {
??????????????? heights[i,j] = (float)(*p - lowest) / (float)(hightest - lowest) * (Max - Min) + Min;
??????????????? p += 3;
??????????? }
??????? }
??????? heightMap.UnlockBits(data);
??????? //計算頂點,索引,三角形數量
??????? numVertices = heightMap.Width * heightMap.Height;
??????? numIndices = 6 * (heightMap.Width - 1) * (heightMap.Height - 1);
??????? numTriangles = 2 * (heightMap.Width - 1) * (heightMap.Height - 1);
??????? //創建頂點數組
??????? Vector3[] verts = new Vector3[numVertices];
??????? int[] index = new int[numIndices];
??????? int x = 0;
??????? int n = 0;
??????? float dx = terrainSize / (float) heightMap.Height;
??????? float dy = terrainSize / (float) heightMap.Width;
??????? //填充頂點數組
??????? for ( int i = 0; i < heightMap.Height; i ++)
??????? {
??????????? for ( int j = 0; j < heightMap.Width; j ++)
??????????? {?
??????????????? verts[i*heightMap.Width+j] = new Vector3((float)j*dx -terrainSize/2f,heights[j,i],(float)i*dy -terrainSize/2f);?
??????????? }
??????? }
??????? //填充索引數組
??????? for ( int i = 0; i < heightMap.Width-1; i ++)
??????? {
??????????? for ( int j = 0; j < heightMap.Height-1; j ++)
??????????? {
??????????????? x = i * heightMap.Width + j;
??????????????? index[n++] = x;?
??????????????? index[n++] = x+1;
??????????????? index[n++] = x+heightMap.Width+1;
??????????????? index[n++] = x;
??????????????? index[n++] = x+heightMap.Width;
??????????????? index[n++] = x+heightMap.Width+1;
??????????? }
??????? }
??????? //設置頂點以及索引緩沖
??????? vb = new VertexBuffer(typeof(Vector3),numVertices,device,Usage.None,VertexFormats.Position,Pool.Default);
??????? vb.SetData(verts,0,0);
??????? ib = new IndexBuffer(typeof(int),numIndices,device,Usage.None,Pool.Default);
??????? ib.SetData(index,0,0);
??? }
??? public void DrawTerrain()
??? {
??????? device.VertexFormat = VertexFormats.Position;
??????? device.SetStreamSource(0,vb,0);
??????? device.Indices = ib;
??????? device.Transform.World = Matrix.Translation(0,0,0);
??????? device.DrawIndexedPrimitives(PrimitiveType.TriangleList,0,0,numVertices,0,numTriangles);
??? }
}
好了,看看我們的工作成果吧,還不錯把。源碼中我們使用了一張位圖作為高度圖。
當然,這只是初級的地形技術而已,我們沒有為地形貼紋理,頂點沒有法線信息,以至于不能使用燈光照亮他,另外,也沒有進行任何LOD處理。下一次,我們將仔細討論這些問題。
轉載于:https://www.cnblogs.com/Blanche/archive/2011/11/02/2233074.html
總結
以上是生活随笔為你收集整理的使用Managed DirectX编写游戏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP5.1时间相差8小时问题解决。
- 下一篇: HDU_4014 Discont (wa