程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十七)

C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十七)

編輯:關於C#

C#開發WPF/Silverlight動畫及游戲系列教程(Game Course):(二十七)戰斗前夜之構建動態障礙物系統

在標准的MMORPG中,每個精靈對象都占據著一塊區域(腳底的一塊小面積),該區域同樣是障礙物系統中的一部分,並且它是動態的,隨著精靈的移動而時時變化著。實現游戲中動態的障礙物構建使障礙物系統趨向完美是全面進軍戰斗系統的必要前奏,大家不想看著主角和怪物疊在一起打來打去吧?

之前的章節中障礙物數組只有一個,它只包含地圖障礙物信息(地圖中固定不動的障礙物),因此要實現動態的障礙物構建,我們還得再增加一個障礙物數組做為當前游戲游戲中相對於主角的動態障礙物數組,以下為這兩個障礙物數組的定義:

byte[,] FixedObstruction = new byte[1024, 1024], VaryObstruction;

其中FixedObstruction即為前面章節中的Matrix,這裡為了方便理解,我將之重新命了名。

那麼如何定義精靈的角底的障礙物區域呢?我們還得為精靈控件添加如下兩個屬性:

// 獲取或設置腳底示為障礙物區域擴展寬度

public int HoldWidth { get; set; }

// 獲取或設置腳底示為障礙物區域擴展高度

public int HoldHeight { get; set; }

這兩個屬性分別代表以精靈腳底坐標(即它的X,Y)為中心,拓展的寬度與高度(見下圖)。

在本節實例中我將所有精靈的HoldWidth均設置為1,HoldHeight設置為0;這樣所有精靈的腳底障礙物區域即為上圖中左數第二個精靈所示區域。

定義好精靈腳底障礙物區域後,我們同樣還需要對目前的障礙物預測方法進行相應的調整。此時在預測障礙物的時候就必須首先排除自身精靈占據的障礙物區域,然後以該區域邊緣點來取代原先的障礙物預測點(關於障礙物預測點可以參考第二十節)。下面我以精靈向左方向移動為例,該方向的障礙物預測需要進行如下改進:

private bool WillCollide() {

 switch ((int)Leader.Direction) {

  case 6:

   return VaryObstruction[

(int)(Leader.X / GridSizeX) - Leader.HoldWidth - 1,

(int)(Leader.Y / GridSizeY)

]

== 0 ? true : false;

 }

}

黃色部分代碼為在原先基礎上新添加的部分,通過它拓展了障礙物預測區域。下圖為該例子演示圖:

一切就緒,現在讓我們著手構建動態障礙物數組VaryObstruction。

同樣的,從實現原理切入。我們首先需要拷貝一份固定障礙物數組(FixedObstruction)的淺表副本賦給動態障礙物數組(VaryObstruction);然後循環遍歷索敵區域內的所有精靈對象,將它們占據區域全部示為障礙物區域並更新VaryObstruction;最後間隔一定時間重復以上過程繼續更新動態障礙物數組。

原理總是比較簡單的,做起來往往並非一帆風順。大家可以從原理中看出兩個重點:1、此過程是一個重復無限循環過程,因此我們可以將此之放在一個計時器中,讓Tick事件去處理;2、此過程是絕對的性能消耗,如果將之放在界面線程中,很肯定的將導致游戲刷新率大副下降,這將極其影響游戲的流暢性。聰明的朋友此時一定想到了後台處理;沒錯,這裡必須使用後台處理,而且必須是異步跨線程的(即不占用界面線程,同時又能讓結果直接影響到界面)。幸運的是,在WPF/Silverlight中,我們可以輕松的使用BackgroundWorker;在Winform時代用過它的朋友都很清楚,它是一個爽歪歪的東西。呵呵,那麼接下來的過程就再簡單不過了,且看功能實現的關鍵步驟及代碼:

第一步,創建這兩個重要對象:

//設置游戲窗體輔助線程

AuxiliaryThread = new DispatcherTimer(DispatcherPriority.Normal);

AuxiliaryThread.Tick += new EventHandler(AuxiliaryThread_Tick);

AuxiliaryThread.Interval = TimeSpan.FromMilliseconds(1000);

AuxiliaryThread.Start();

//設置後台工作者

BackWorker = new BackgroundWorker();

BackWorker.DoWork += new DoWorkEventHandler(BackWorker_DoWork);

BackWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackWorker_RunWorkerCompleted);

第二步,在輔助線程(AuxiliaryThread)計時器中啟動異步後台處理:

private void AuxiliaryThread_Tick(object sender, EventArgs e) {

 //異步刷新面板及障礙物

 if (!BackWorker.IsBusy) { BackWorker.RunWorkerAsync(); }

}

第三步,定義工作委托:delegate void WorkDelegate();

然後在已經注冊的後台處理事件(BackWorker_DoWork)中執行異步委托(RefreshFace),從而實現刷新動態障礙物數組:

private void RefreshFace() {

 //將當前動態障礙物重置為原始固定值

 VaryObstruction = (byte[,])FixedObstruction.Clone();

 //重新填充動態障礙物

 for (int i = 0; i < Carrier.Children.Count; i++) {

  if (Carrier.Children[i] is QXSpirit) {

   QXSpirit spirit = Carrier.Children[i] as QXSpirit;

   if (spirit != Leader) {

    int x = (int)(spirit.X / GridSizeX);

    int y = (int)(spirit.Y / GridSizeY);

    for (int m = x - spirit.HoldWidth; m <= x + spirit.HoldWidth; m++) {

     for (int n = y - spirit.HoldHeight; n <= y + spirit.HoldHeight; n++) {

      VaryObstruction[m, n] = 0;

     }

    }

   }

  }

 }

}

private void BackWorker_DoWork(object sender, DoWorkEventArgs e) {

 //跨線程異步刷新障礙物

 this.Dispatcher.BeginInvoke(new WorkDelegate(RefreshFace), DispatcherPriority.Normal, null);

}

黃色代碼部分即為通過循環來設定精靈腳底的占據區域。在WPF/Silverlight機制中跨線程調用必須在與Dispatcher關聯的線程上執行委托(這與以往有些不同),所以此時要實現跨線程的異步處理我們必須通過this.Dispatcher.BeginInvoke()的來實現。並且此線程同樣為比較重要的處理,因此優先級別我設定為DispatcherPriority.Normal。

額外的,如果您想調試障礙物刷新的次數或處理其他的相關數據,您可以在注冊的後台工作完成事件中進行計數之類的設定,例如下面方式,這樣可以很好的對異步結果進行管理(通過e.Result捕獲傳遞過來的結果參數):

private void BackWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {

 count +=1;

}

通過以上4個步驟即輕松完成了動態障礙物的構建。BackgroundWorker的強大對於處理這區區一丁點的運算還是綽綽有余的,您還可以將更多的任務托付給它。例如我們同樣的可以將上一節中講到的主角頭像面板與監視對象頭像面板的刷新方法一起放進RefreshFace()中;這樣在游戲運行過程中,我們不再需要去管理它哥兩,後台工作者會每間隔1秒左右更新它們一次,就象保姆一樣照顧得無微不至且不打擾到您的任何其他工作。

下面讓我們來測試一下吧。我在屏幕左下角放了個按鈕,每次點擊地圖最上面的妖精怪物都會向右下方移動50象素,大家不妨走到它旁邊,然後在它每次動後在站到它移動之前的位置上,測試一下障礙物此時是否還存在,並且向它新的位置方向移動看看新的障礙物是否已成功設置:

最後總結一下:以1秒為頻率刷新動態障礙物在高精度需求的游戲中是不夠的,往往需要設置為<=500毫秒;並且也不能太小了,過小不僅會造成過度的性能消耗;同時由於異步的原因也極其容易導致錯誤的障礙物識別,直接的結果就是穿越。我個人建議是在WPF中間隔設置為500毫秒,在Silverlight中間隔設置為1秒。大家在玩2D-MMORPG游戲時是否有過這樣的經歷:在城裡人多的地方,如果游戲網速突然慢了一下或者游戲突然卡了那麼一下,一不小心你的角色精靈就與其他某位玩家的角色精靈重疊在一塊了,並且此游戲正常情況下是不允許重疊的(回合制游戲允許重疊,而ARPG類的是絕對不允許的)。根據本節講解的原理我們可以很快的找出原因:在還未來得及更新出最新的動態障礙物數組前,兩位角色已經跑到了當前被定義為非障礙物的坐標上。但是大家不要為此過份擔心了,或者說此情況屬於完全可以接受的范圍中。因為由於障礙物數組的固定值部分(即固定的障礙物) FixedObstruction在游戲地圖加載的時候就已經加載並固定了,意味著只要地圖不更換它是不變的。所以不管處於異步處理的任何階段,固定的障礙物如房屋啦、牆壁啦、樹木啦等等之類的東西將永遠無法穿越,這或許才是我們最終希望達到的目的。

這裡還需要說明一下,本節構建的動態障礙物只是相對於主角的,也就是只能為主角所用。在後期加入怪物AI後,可以有兩種常見的解決方案:第一種為所有怪物均使用FixedObstruction固定障礙物,這樣怪物之間會發生重疊。第二種為給精靈增加一個Obstruction屬性來保存相對於它們自身時時的障礙物信息,這種方式實現的結果是完美的,但過程是極其損耗空間與時間的。至於還有第三種更完美的方案嗎?這是當然的,大家不妨想想,只有當精靈在移動的時候才會用得上障礙物對吧?那麼我們是否只需要在每次發起移動前對全局的VaryObstruction進行拷貝,然後稍微處理一下此副本使之成為此次移動所要面對的障礙物,或許這才是最完美的解決之道。

那麼來張測試圖吧,此地圖上隨機布局了20個怪物,大家可以嘗試在裡面穿梭體驗饒過怪物的爽快(由於怪物的圖片是提取出來的,只做了稍微的加工調整,腳底並未完美的定位到中心,因此障礙物有時顯得並不精確,這不是系統問題,而是圖片結構引起的 ^_^):

相對於主角的完美障礙物系統總算構建完成。暴風驟雨來臨的前夜,讓激情燃燒我們的斗志吧,偉大的勇士!下期敬請期待。

出處:http://alamiye010.cnblogs.com/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved