程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
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):(二十五)完美捕捉精靈之神器 -- HitTest

怪物們都出現了,如何選中自己心儀的怪是主角目前首要做的事。

為了進行鼠標狀態區別,我首先對鼠標變化規則進行約束:當鼠標在屏幕上空曠地圖區域移動時,鼠標光標形態表現為默認光標 (0號光標圖片),當鼠標經過精靈(懸停於其上方)時則變成發光光標(1號光標圖片),如果指向的精靈對象為敵對狀態時則鼠標光標變為攻擊光標(2號光標圖片),當使用魔法快捷鍵時,鼠標光標變成凝法狀態(3號光標圖片)。

接下來要做的就是用代碼來實現這些規則。要實現鼠標光標的變換,我們首先得將這4個光標加入到系統中,這裡我新建一個名為Cursors的文件夾用於保存這4個光標,具體添加方法詳見第五節。然後在使用的時候如果該代號光標不存在,則通過數據流將光標添加進系統內存中:

public static Cursor[] GameCursors = new Cursor[4];

/// <summary>

/// 返回指定標號光標

/// </summary>

/// <param name="sign">標號</param>

/// <returns>光標</returns>

public static Cursor getCursor(int sign) {

 if (GameCursors[sign] == null) {

  GameCursors[sign] = new Cursor(new FileStream(string.Format(@"Cursors\{0}.ani", sign), FileMode.Open, FileAccess.Read, FileShare.Read));

 }

 return GameCursors[sign];

}

一切就緒,現在正式開始實現游戲窗體的鼠標移動事件。既然是鼠標在地圖上滑動時產生的效果,因此我們首先添加游戲窗體鼠標移動事件:MouseMove="Window_MouseMove",然後在後台代碼中的Window_MouseMove方法裡寫入相應內容:

private void Window_MouseMove(object sender, MouseEventArgs e) {

 this.Cursor = e.Source is QXSpirit ? Super.getCursor(1) : this.Cursor = Super.getCursor(0);

}

假如鼠標經過的對象是QXSpirit類型,則鼠標的光標變為1號,其他情況時,鼠標光標變為0號。這種效果對於做習慣了.NET網站開發的朋友們來說再熟悉不過了,好比導航欄上的鼠標懸停圖片切換CSS或JS效果。

這麼短短一句話即實現了最簡易的精靈對象捕獲,我們先來測試一下程序:

細心的朋友會發現,雖然是勉強實現了但這其實並不准確;因為當鼠標並不在怪物實體上時,鼠標仍然會顯示為1號光標(如下圖),是代碼出問題了嗎?

其實問題並非出在代碼上,這是因為精靈的圖片源是背景透明的PNG或GIF格式圖片,就拿上圖中的“絕對無敵”來說吧,它的每幀圖片為200*200尺寸(如下圖),

它的有效實體只是該圖片的中間區域,而它的旁邊有著比較大面積的透明無效區域,雖然在顯示上透明區域是不會顯示出來的,但是它整個作為200*200尺寸的Image類型控件而存在。因此當鼠標在游戲窗體上移動時,只要處於這200*200區域內時均會顯示為1號光標而並不會理睬它是否停留在精靈的有效實體部分。

精靈的圖片源均為位圖類型,目前我暫時還未發現在WPF/Silverlight中如何實現將位圖轉換成矢量圖的高效直接方法。因此目前解決這個問題的方式只有兩種,第一種為通過對當前拾取對象的圖片源進行點對點的顏色拾取,然後判斷當前鼠標的位置相對於圖片源中的點是否為透明,如果不透明則拾取該精靈,具體方法如下:

/// <summary>

/// 獲取圖片源某點顏色

/// </summary>

public static Color getImagePointColor(BitmapSource bitmapsource, int x, int y) {

 CroppedBitmap crop = new CroppedBitmap(bitmapsource as BitmapSource, new Int32Rect(x, y, 1, 1));

 byte[] pixels = new byte[4];

 try {

  crop.CopyPixels(pixels, 4, 0);

  crop = null;

 } catch (Exception ee) {

   MessageBox.Show(ee.ToString());

   }

  //藍pixels[0] 綠pixels[1]  紅pixels[2] 透明度pixels[3]

  return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);

}

此方法的優點是精確,可以定位到精靈有效實體的任一像素角落;而缺點是只能在WPF中使用且性能不好,更麻煩的是必須將之放 Try{}Catch{}塊內使用,否則極易出錯,因為精靈的圖片切換太快了。

解決此問題的另一方式為通過定義精靈實體區域參數public double[] EfficaciousSection來實現,此方法也是我推薦使用的方法,兼顧WPF/Silverlight。

EfficaciousSection由4個數組成,以上圖為例,它的EfficaciousSection = new double []{80,125,50,145},其中第一個數字表示紅色區域左邊線距離圖片左的距離,第二個數字表示紅色區域右邊距離圖片左邊距離,第三個數字表示紅色區域上邊距離圖片頂部的距離,第四個數字代表紅色區域底邊距離圖片頂部的距離,上面所說的紅色區域即為精靈的有效實體區域,在後面的鼠標點擊或移動判斷中,只有當鼠標進入精靈的有效實體區域時我們才變換鼠標光標。

精靈獲得了有效實體區域,是否代表可以完美准確的捕捉精靈對象了呢?我們將窗體鼠標移動方法進行如下改進:

if (e.Source is QXSpirit) {

 QXSpirit Spirit = e.Source as QXSpirit;

 Point p = e.GetPosition(Spirit);

 if (p.X >= Spirit.EfficaciousSection[0] && p.X <= Spirit.EfficaciousSection[1]

  && p.Y >= Spirit.EfficaciousSection[2] && p.Y <= Spirit.EfficaciousSection[3]) {

  this.Cursor = Super.getCursor(1);

 } else {

  this.Cursor = Super.getCursor(0);

 }

}

然後再運行一下游戲,結果更奇怪的事情出現了:

如上圖,此時當鼠標停在主角身上時竟然沒有變換光標圖片,是代碼出問題了嗎?當然也不是。我們還是得從圖片上找原因。此時怪物的圖片遮擋住了主角,因此當鼠標懸停在主角身上時,系統卻仍然判斷當前捕獲的是“絕對無敵”,並且鼠標也未進入它的有效實體范圍,因此鼠標光標仍然是0號。

怎麼辦?搞了這麼久到頭來仍然是一場空。有朋友提出了將圖片裁剪成剛好包裹住精靈有效實體區域不就好了。想法是好的,但是將造成每一幀圖片都為不同尺寸規格,在動作中如何切換?每張圖片都得定義它距離容器Canvas左上角的距離,一個怪物幾百張圖片,每張都要定義,這將大大增加游戲的開發負擔。

難道沒有完美的解決方案了嗎?WPF/Silverlight中最不起眼但卻有著極其重要作用的神器登場了!對,就是它了:HitTest(命中測試)。

稱之為命中測試,不如叫它穿透點擊來得更形象些。因為它強大到只要游戲窗口中有的東西,它都能抓出來,想抓幾個抓幾個,想抓到什麼深度(Zindex)就抓到什麼深度;更甚者,它可以肢解封裝的控件直接抓取其內部任意對象控件;完成以上各種任務如若探囊取物搬輕盈且高效,僅僅是通過模擬鼠標點擊幾乎忽略不計的敏捷捕獲。關於HitTest的更多相關知識及原理請大家自行網上查閱,這裡不具體講解了。接下來我們看下圖:

在游戲中如何使用HitTest進行對象捕獲的原理在上圖中已經描述得非常清楚了,接下來看我如何通過代碼進行實現:

首先我定義一個精靈容器用於將捕獲到的所有精靈進行收容管理:

List<QXSpirit> SpiritList = new List<QXSpirit>();

接下來定義HitTest的過濾器HitFilter,用於篩選HitTest捕獲的對象,我們只需要捕獲QXSpirit類型對象即可,然後將之添加進精靈容器:

public HitTestFilterBehavior HitFilter(DependencyObject dObject) {

 if (dObject is QXSpirit) {

  SpiritList.Add(dObject as QXSpirit);

 }

 return HitTestFilterBehavior.Continue;

}

每執行一次過濾器後,我們必須重復以上過程繼續向更深層次進行捕獲,因此在HitTest結果HitResult中執行繼續操作以供向下個節點輪循:

public HitTestResultBehavior HitResult(HitTestResult result) {

 return HitTestResultBehavior.Continue;

}

HitFilter和HitResult是HitTest中控制流程非常重要的參數,定義完它兩後接下來我們在窗體的鼠標移動事件中進行如下HitTest命中測試:

private void Window_MouseMove(object sender, MouseEventArgs e) {

 SpiritList.Clear();

 Point p = e.GetPosition(Carrier);

 VisualTreeHelper.HitTest(

 Carrier,

 new HitTestFilterCallback(HitFilter),

 new HitTestResultCallback(HitResult),

 new PointHitTestParameters(p));

 if (SpiritList.Count > 0) {

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

   if (isEfficaciousSection(SpiritList[i].EfficaciousSection, e.GetPosition(SpiritList[i]))) {

    this.Cursor = Super.getCursor(1);

    label3.Content = SpiritList[i].Name; //調試用

    break;

   } else {

    this.Cursor = Super.getCursor(0);

   }

  }

 }

}

每次鼠標移動的時候我們必須清空精靈容器,然後對鼠標當前的點在Carrier中的位置進行點擊測試,通過前面的HitFilter和HitResult過濾後得到所有位於鼠標位置的精靈放進容器,然後遍歷精靈容器裡的所有精靈,只有當該點位於精靈Canvas裡的位置處於精靈的有效實體區域時,才算真正的捕獲到了精靈。一旦捕獲到了精靈則同時更改鼠標光標為1號光標然後退出循環;這裡我為了測試是否精確的捕獲了精靈對象,設置了名叫label3的文本來顯示抓取到的精靈名字。

到此就完成了整個HitTest精確捕獲精靈流程,下面我在地圖密集的區域內添加30個擁有不同的名字的怪物精靈,然後嘗試移動鼠標去分別捕獲,通過label3中的名字顯示該方法實現起來是極其准確的,比衛星定位還要精確與高效^_^||:

已經能完美捕捉想要的精靈了,但是如何讓被捕獲的精靈進行特效顯示呢?目前的網絡游戲中最常用的方式有兩種:1、對被捕獲的精靈進行描邊;2、讓被捕獲的精靈半透明化。

第一種方法的實現需要首先為精靈控件中的身體部分控件添加一個WPF專有的OuterGlowBitmapEffect效果:

<Image x:Name="Body" Stretch="Fill">

 <Image.BitmapEffect>

  <OuterGlowBitmapEffect GlowColor="Blue" GlowSize="5" Noise="0" Opacity="1" />

 </Image.BitmapEffect>

</Image>

具體意思就是在精靈身體圖片不透明區域進行外發光:藍色,5像素寬,無噪音,完整透明度。其運行效果如下圖:

看到這張圖的時候或許大家開始有些欣喜若狂了,但是我想告訴大家:此方法絕對的行不通,為什麼?一方面此方法只能在WPF中使用,它的原理是時時動態查找圖片不透明區域的邊緣,然後對邊緣路徑進行發光濾鏡處理;而另一方面由於它是對圖片源不透明區域進行時時的邊緣查找,將極大的占用游戲的界面線程資源,是極其不友好的表現方式。

因此,為了同時適應WPF/Silverlight,我使用第二種方法作為最終解決方案。這種方法實現起來簡單多了,只需要在前面代碼的基礎上加進行如下更改:

private void Window_MouseMove(object sender, MouseEventArgs e) {

……

if (SpiritList.Count > 0) {

 bool targetIsFound = false;

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

  if (!targetIsFound && isEfficaciousSection(SpiritList[i].EfficaciousSection, e.GetPosition(SpiritList[i]))) {

   this.Cursor = Super.getCursor(1);

   SpiritList[i].Opacity = 0.6;

   targetIsFound = true;

   label3.Content = SpiritList[i].Name;

  } else {

    if (!targetIsFound) { this.Cursor = Super.getCursor(0); }

  SpiritList[i].Opacity = 1;

  }

  }

 }

}

在鼠標移動事件中僅僅增改6行代碼即可以輕松的實現,運行效果如下:

到此為止即完美實現了對精靈的精確捕獲。忽忽,是不是感覺向完整的游戲框架目標又邁出了一大步?

在此,我還想對那些極端的朋友說一下:由於目前暫時采用多線程結構,在單核CPU電腦以及Win2003以前的操作系統上運行時,怪物密集的地方會有些卡。但是這根本代表不了游戲引擎的最終性能,教程還有非常非常多的內容沒有講到,優化的技術還在後面呢,太多了就不一一羅列了,大家應該都明白本系列既然取名為教程,代表的就是一個由淺入深的過程,很多人連基礎原理都沒弄清楚,源碼對你有何意義?

小結:HitTest功能強大到幾乎無所不能,它是我們實現打怪與施放魔法的前提條件。下一節我將講解精靈面板界面,以及精靈3大基本屬性(生命、魔力、經驗值)表現形式的實現方法,敬請關注。

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

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