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/