作者:Ravi Krishnaswamy
相關技術:Visual Studio 2003、.NET Compact Framework、 Windows Powered Pocket PC
難度等級:★★★☆☆
讀者類型:Windows CE 開發人員、移動設備遊戲開發人員
[導讀]本文講述瞭如何創建基於.NET框架精簡版的遊戲以及編寫面向小型設備的遊戲的主要要求。在提高遊戲性能方面,本文介紹了一些高級性能優化技術幫助我們突破遊戲的速度瓶頸。
簡介
Microsoft .NET框架精簡版是完整Microsoft .NET框架的子集。它是對完整的.NET框架進行精簡後得到的版本雖然其規模大大減小,但多數功能仍然保持完整。
使用.NET框架精簡版:可以針對Pocket PC和其他Windows CE .NET設備進行單一的二進制部署,提高開發人員的工作效率,加快產品投放市場的速度。
本文將討論編寫面向小型設備的遊戲的主要環節以及部分高級性能優化技術,我們可以使用這些技術來突破遊戲的極限。總之,我們將瞭解到使用.NET框架精簡版來開發和優化遊戲是多麼容易。這將是一次有趣的旅行,請繫好安全帶並充分享受其中的樂趣吧。
全屏遊戲窗體
在遊戲應用程序中,我們經常會使用設備的全屏顯示功能。佔據整個屏幕區域的窗體稱爲全屏窗體(也稱爲遊戲窗體)。換句話說,全屏窗體佔據桌面(即工作區)以及非工作區,如頂部的標題/導航欄、邊框和底部的菜單欄。
應用程序通過將其WindowState設置爲Maximized來創建全屏窗體,如下所示:
form.WindowState = FormWindowState.Maximized;
如果該窗體附加了菜單欄(和/或Pocket PC中的工具欄),則不能使其成爲全屏窗體。
在Pocket PC的.NET框架精簡版1.0中,要創建全屏應用程序,必須在窗體的OnLoad內部設置WindowState屬性。
下面的圖1和圖2闡明瞭Pocket PC中的全屏窗體和非全屏窗體之間的區別。
圖1 非全屏窗體
圖2 全屏窗體
擁有全屏窗體的主要特徵是沒有標題/導航欄或菜單欄。應用程序必須考慮這些因素,並且在必要時提供儘量迴避調用菜單功能的手段。
如果我們只是希望我們的窗體僅填充可用的桌面區域(而不是全屏),則無須做任何事情。默認情況下,.NET框架精簡版會自動調整窗體的大小以填充Pocket PC的屏幕。
事實上,我們最好不要明確設置窗體的ClientSize以達到這種效果,因爲這可能會妨礙應用程序在各種Windows CE.NET設備之間的互操作性。例如,如果我們明確調整應用程序的大小以匹配其中一個設備的窗體指數,則該大小在其他設備上可能並不理想。明智的做法是採用窗體的默認大小。
重寫OnPaint和OnPaintBackground
典型的遊戲應用程序會對窗體內容進行自定義繪製。它採取的辦法是重寫控件的OnPaint()事件並對窗體的繪製進行自定義處理。
protected override void OnPaint(PaintEventArgs paintg)
{
Graphics gx = paintg.Graphics;
// Custom draw using the graphics object
}
每當控件開始繪製時,都會首先自動刷新其背景。例如,OnPaint事件將首先用this.Backcolor中指定的顏色來繪製背景。但如果應用程序前景繪製完成之前調用了自動背景繪製,則屏幕會出現瞬間的閃爍。
要避免這一情況,我們建議每當應用程序重寫OnPaint方法時,都要重寫OnPaintBackground方法並自己來繪製背景。應用程序可以選擇在OnPaint內部處理所有繪製工作,並將OnPaintBackground保留爲空,如下面的示例所示。
protected override void OnPaintBackground(PaintEventArgs paintg)
{
// Left empty, avoids undesirable flickering
}
用於繪畫的屏外[1](Off-Screen)位圖技術
通過控件的this.CreateGraphics方法來獲取屏幕的圖形對象並直接向其進行繪製,我們可以進行屏上繪製。必須記住在不再需要屏上圖形對象時將其處理。如果不這樣做,可能導致有限的顯示設備資源出現不足。
如上一節所示,我們可以在OnPaint和OnPaintBackground方法內通過PaintEventArgs.Graphics來訪問屏幕的圖形對象。在執行繪圖方法以後,這些圖形對象將被自動處理。
對於直接在屏幕上繪製的遊戲應用程序,,當我們在屏幕上繪製多個對象時,將會看到屏幕閃爍。爲避免這種現象,遊戲開發人員通常會採用屏外繪製技術。
這一技術的思想是創建屏外位圖,爲其獲取圖形對象,在內存中執行所有繪圖操作,然後將得到的屏外位圖複製到屏幕上。
// Create off-screen graphics
Bitmap bmpOff = new Bitmap(this.ClientRectangle.Width,
this.ClientRectangle.Height);
Graphics gxOff = Graphics.FromImage(bmpOff);
在該例中,我將創建一個大小恰好與遊戲窗體的工作區相同的屏外位圖。在離屏和屏上繪圖界限之間保持1:1的大小關係將有很大好處,尤其是在屏內(On-Screen)和屏外之間變換子圖形的座標時。
// Draw to off-screen graphics, using Graphics.Draw APIs
// Create on-screen graphics
Graphics gxOn = this.CreateGraphics();
// Copy off-screen image onto on-screen
gxOn.DrawImage(bmpOff, 0, 0, this.ClientRectangle, GraphicsUnit.Pixel);
// Destroy on-screen graphics
gxOn.Dispose();
這一技術可以避免屏幕閃爍,並且速度更快,因爲所有屏外繪圖操作都發生在內存中。
子圖形(Sprites)
像位圖這樣的柵格圖形都以矩形表示,而大多數實際的子圖形都具有不規則的形狀(不是矩形)。因此,我們需要找到相應的方法,以便從矩形光柵圖形表示中提取形狀不規則的子圖形。
顏色鍵透明
遊戲開發人員常用的一種技術是顏色鍵技術,即在渲染時忽略位圖中的指定顏色鍵。這一技術也稱爲色度鍵屏蔽、顏色取消和透明混合。
圖3 子圖形
圖4 帶有顏色鍵透明
圖5 不帶顏色鍵透明
在子圖形位圖(圖3)中,非對象區域被填充了品紅色,該顏色被用作顏色鍵。使用以及不使用該技術時得到的混合效果分別在圖4和圖5中進行了說明。
透明混合的第一步是設置需要在渲染時屏蔽的顏色鍵(色度鍵)。我們需要指定精確的顏色鍵值。
ImageAttributes imgattr = new ImageAttributes();
imgattr.SetColorKey(Color.Magenta, Color.Magenta);
與使用Color類提供的標準顏色集不同,我們可以通過指定紅色、綠色和藍色(RGB)值來直接構建自己的顏色,如下所示:
imgattr.SetColorKey(Color.FromArgb(255, 0, 255),
Color.FromArgb(255, 0, 255));
我們經常用來指定顏色鍵的另外一種技術是直接使用像素值。這可以避免處理RGB值的需要。而且,顏色鍵不是硬編碼的,可以在位圖中獨立進行更改。
imgattr.SetColorKey(bmpSprite.GetPixel(0,0), bmpSprite.GetPixel(0,0));
現在,讓我們看一下如何使用已經設置的顏色鍵來透明地繪製子圖形。
gxOff.DrawImage(bmpSprite, new Rectangle(x, y, bmpSprite.Width,
bmpSprite.Height), 0, 0, bmpSprite.Width, bmpSprite.Height,
GraphicsUnit.Pixel, imgattr);
在上述代碼中,目標矩形被指定爲new Rectangle(x, y, bmpSprite.Width, bmpSprite.Height),其中x和y是子圖形的預期座標。
通常,在繪製時可能需要放大或縮小子圖形。我們可以通過調整目標矩形的寬度和高度來做到這一點。同樣,還可以調整源矩形以便僅繪製子圖形的一部分。
在設置子圖形位圖時,最好考慮一下目標設備的顏色分辨率。例如,24位顏色位圖在12位顏色分辨率設備上可能不會按預期方式渲染;由於所用顏色的差異,這兩者之間的顏色梯度差異可能非常明顯。而且,在選擇要屏蔽的ColorKey時,請確保顏色值在目標顯示器所支持的範圍之內。請記住,只有精確的ColorKey匹配纔會受到支持。
顏色鍵透明是.NET框架精簡版中唯一受支持的混合技術。
作爲嵌入式資源的圖像
我們可以將圖形資源嵌入到我們的程序集中,方法是將該圖形添加到項目中,並將其Build Action屬性設置爲“Embedded Resource”。這樣,我們就可以按如下方式使用嵌入的bmp資源:
Assembly asm = Assembly.GetExecutingAssembly();
Bitmap bmpSprite = new Bitmap(asm.GetManifestResourceStream("Sprite"));
BMP、JPG、GIF和PNG圖形格式受到Bitmap類的支持。
優化繪圖方法
遊戲中的繪圖例程需要進行優化以便得到最佳的性能。比較笨拙的屏幕繪圖方法是以離屏方式擦除並重繪所有子圖形,然後用以離屏方式得到的圖形進行屏上刷新。這樣做效率很低,因爲我們每次都不必要地重繪整個屏幕。這時,我們的幀速率將取決於框架精簡版刷新整個屏幕的速率。
一種更好的辦法是計算遊戲中子圖形的髒區,並且只刷新屏幕變髒的部分。子圖形可能因爲多種原因而變髒,例如,移動、圖形/顏色發生改變或者與其他子圖形發生衝突,等等。在本章中,我們將討論可用於有效計算髒區的各種技術。
髒區計算
讓我們考慮一個移動的子圖形,一種計算髒區的簡單方法是獲取舊界限和新界限的並集。
RefreshScreen(Rectangle.Union(sprite.PreviousBounds, sprite.Bounds));
其中,RefreshScreen()是一個以屏上方式刷新指定矩形區域的方法。
圖6 髒區:舊界限和新界限的並集
圖7 巨大的增量產生巨大的髒區
注: 如圖7所示,如果舊座標和新座標之間的增量很高並且/或者子圖形很大,則這種技術會產生過大的髒矩形(爲便於說明,舊的界限用不同顏色顯示)。
這種情況下,更有效的方法是將子圖形的髒區計算爲多個單元矩形,這些矩形放到一起時表示髒區。
首先,讓我們弄清楚舊的界限和新的界限有沒有相互重疊。如果沒有,則可以簡單地將髒區計算爲兩個單獨的矩形,分別表示舊的界限和新的界限。因此,對於圖7說明的情形,明智的做法是將舊的界限和新的界限視爲兩個單獨的髒區。
if (Rectangle.Intersection(sprite.PreviousBounds,
sprite.Bounds).IsEmpty)
{
// Dirty rectangle representing old bounds
RefreshScreen(sprite.PreviousBounds);
// Dirty rectangle representing current bounds
RefreshScreen(sprite.Bounds);
}
當我們並不介意將重疊區域重繪兩次時,上述技術也將有效,如下面的圖8所示。
圖8 將髒區拆分爲舊的界限和新的界限
現在,讓我們看一下如何將髒區計算爲多個單元矩形(這些矩形共同表示部分重疊的舊界限和新界限),以便不會重繪任何髒區,也就是說所有單元矩形都是互斥的。
首先,包含新的界限作爲一個完整單元。請注意,這包括舊界限和新界限之間的重疊區域。
單元髒區1:表示當前界限的髒矩形
RefreshScreen(sprite.Bounds);
圖9 髒區拆分爲多個單元
現在,將舊界限的非重疊部分拆分爲兩個獨立的單元,如下面的代碼所示:
Rectangle rcIx, rcNew;
// Calculate the overlapping intersection
rcIx = Rectangle.Intersection(sprite.PreviousBounds, sprite.Bounds);
單元髒區2:
rcNew = new Rectangle();
if (sprite.PreviousBounds.X < rcIx.X)
{
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = rcIx.X - sprite.PreviousBounds.X;
rcNew.Y = rcIx.Y;
rcNew.Height = rcIx.Height;
}
else
{
// Means sprite.PreviousBounds.X should equal to rcIx.X
rcNew.X = rcIx.X + rcIx.Width;
rcNew.Width = (sprite.PreviousBounds.X +
sprite.PreviousBounds.Width) - (rcIx.X + rcIx.Width);
rcNew.Y = rcIx.Y;
rcNew.Height = rcIx.Height;
}
RefreshScreen(rcNew);
單元髒區3:
rcNew = new Rectangle();
if (sprite.PreviousBounds.Y < rcIx.Y)
{
rcNew.Y = sprite.PreviousBounds.Y;
rcNew.Height = rcIx.Y - sprite.PreviousBounds.Y;
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = sprite.PreviousBounds.Width;
}
else
{
rcNew.Y = rcIx.Y + rcIx.Height;
rcNew.Height = (sprite.PreviousBounds.Y +
sprite.PreviousBounds.Height) - (rcIx.Y + rcIx.Height);
rcNew.X = sprite.PreviousBounds.X;
rcNew.Width = sprite.PreviousBounds.Width;
}
RefreshScreen(rcNew);
衝突檢測
現在,讓我們看一下一個子圖形與另一個子圖形衝突的情形。從繪圖角度來看,可以簡單地忽略兩個子圖形之間的衝突,而只是使用前面討論的技術逐個更新這些子圖形的髒區。
但是,我們經常需要檢測子圖形的衝突以便使遊戲做出響應。例如,在射擊遊戲中,當子彈擊中目標時,我們可能希望通過爆炸或類似形式直觀地做出反應。
在本文中,我們將所有重點討論衝突檢測技術其中的常用的幾種技術。
可以回憶一下,大多數子圖形的形狀都是不規則的,但用於表示它們的光柵圖形是矩形。很難將子圖形的界限(或包絡線)表示爲開放的區域,因此我們將通過封閉的矩形來表示它。
當玩家看到屏幕上的子圖形時,他/她實際上看到的是子圖形區域,而覺察不到非子圖形區域。因此,子圖形之間的任何衝突檢測都必須僅發生在其各自的子圖形區域之間,而不應包括非子圖形區域。
對於較小的子圖形,在計算衝突並直觀地避免衝突時,通常可以使用整個子圖形界限。因爲對象較小並且移動迅速,肉眼將不會注意到錯覺。簡單地計算兩個子圖形界限的矩形交集就足夠了。
Rectangle.Intersect(sprite1.Bounds, sprite2.Bounds);
如果子圖形的形狀是圓形,則可以簡單地計算它們的圓心之間的距離並減去其半徑;如果結果小於零,則表明存在衝突。
對於逐個像素的碰撞檢測,可以使用Rectangle.Contains方法:
if (sprite.Bounds.Contains(x,y))
DoHit();
首先應該應用快速邊界相交技術來檢測子圖形之間的邊界衝突。如果發生了衝突,則我們可以使用一種更爲精確的方法(如衝突位圖屏蔽技術)來確定相互重疊的像素。
如果我們不關心像素粒度,則可以使用我們已經在髒區計算一節中看到的技術來計算衝突區域。我們將處理兩個發生衝突的子圖形的界限,而不是處理一個子圖形的舊界限和新界限。
衝突檢測技術是專用的,應該根據具體情況加以確定。應該根據多種因素來選擇特定的技術,如子圖形的大小和形狀、遊戲的特性等。在一個遊戲中使用上述技術中的多個技術是很常見的。
子圖形速度
幀速率經常被單純理解爲移動子圖形的速度,我們還應該控制子圖形在每幀中移動的距離,以獲得期望的淨速度。
讓我們考慮下面的示例,在該示例中,我們希望子圖形每秒鐘縱向移動100個像素單位。現在,我們可以將幀速率固定爲10fps,並且將子圖形每幀縱向移動10個像素,以便達到上述淨速度,或者我們還可以將幀速率增加到20fps,並且使子圖形的每幀縱向移動距離下降至5個像素。
採用任一種方法都可以達到相同的淨速度,區別在於:在前一種情形下,子圖形的移動看起來可能有一點跳躍性,因爲與後一種情形相比,它移動相同距離所用的刷新週期要短一些。但是,在後一種情形中,我們依賴於遊戲以20fps的幀速率渲染。因此,在決定使用哪一種方法之前,我們需要絕對確定硬件、系統和.NET 框架精簡版的功能。
遊戲前進技術
當屏幕隨着時間的推移而發生變化並且直觀地響應用戶交互時,就說遊戲正在前進。
遊戲循環
在遊戲內部,我們開始、維護和破壞循環,這使我們在必要時有機會渲染屏幕。通常,當遊戲啓動時,遊戲循環開始,然後根據需要休眠和循環返回來進行維護,直到遊戲結束爲止,此時遊戲循環將被彈出。
這種技術可以提供動作遊戲所需的最大的靈活性和快速的週轉速度。現在,我們將爲我們的足球遊戲實現一個遊戲循環。讓我們假設該遊戲具有多個級別。
private void DoGameLoop()
{
// Create and hold onto on-screen graphics object
// for the life time of the loop
m_gxOn = this.CreateGraphics();
do
{
// Init game parameters such as level
DoLevel();
// Update game parameters such as level
// Ready the game for the next level
}
while (alive);
// End game loop
// Dispose the on-screen graphics as we don't need it anymore
m_gxOn.Dispose();
// Ready the game for next time around
}
private void DoLevel()
{
int tickLast = Environment.TickCount;
int fps = 8;
while ((alive) && (levelNotCompleted))
{
// Flush out any unprocessed events from the queue
Application.DoEvents();
// Process game parameters and render game
// Regulate the rate of rendering (fps)
// by sleeping appropriately
Thread.Sleep(Math.Abs(tickLast + 1000/fps) - Environment.TickCount));
tickLast = Environment.TickCount;
}
}
注意,在循環返回之前我們每次都要在循環內部調用Application.DoEvents(),我們這樣做的目的是保持與系統之間的活動通訊,以及便於對事件隊列中掛起的消息進行處理。這是有必要的,因爲當我們的應用程序處於循環中時,我們基本上失去了處理任何來自系統的傳入消息的能力;除非我們明確調用Application.DoEvents(),否則我們的應用程序將不會響應系統事件,並因此具有不合需要的副作用。
在遊戲循環中需要考慮的另一個重要因素是渲染速率。大多數遊戲動畫至少需要每秒鐘8-10幀(fps)的速率。爲了使我們有一個大致的概念,以典型的卡通影片爲例,它的渲染速率是每秒鐘14-30幀。
一種簡單的幀速率控制技術是決定所需的fps以及循環內部的休眠(1000/fps)毫秒。但是,我們還需要將處理當前走時所需的時間考慮在內。否則,我們的渲染速率將比期望的速率慢。處理時間可能相當可觀,對於速度較慢的硬件尤其如此,因爲這涉及到開銷較高的操作,如處理用戶輸入、渲染遊戲等等。
因此,在繼續循環之前,我們需要休眠1000/fps減去處理當前走時所需的時間(毫秒)。
Thread.Sleep(Math.Abs(tickLast + (1000 / fps) - Environment.TickCount));
tickLast = Environment.TickCount;
計時器回調
另一項技術是設置一個定期回調的系統計時器。對於遊戲循環而言,通常情況下,在遊戲啓動時實例化計時器,在遊戲結束(此時計時器被處理)之前對計時器計時事件(定期發生)進行處理。
這要比遊戲循環簡單,因爲我們讓系統爲我們處理計時器循環。我們只需要處理計時器回調,並且通過一次繪製一個幀來使遊戲前進。
但是,我們必須非常小心地選擇計時器的走時間隔,因爲它決定了遊戲的幀速率。
在遊戲循環技術中,兩次走時之間的時間間隔完全在我們的控制之下,並且正如前面所看到的,可以輕鬆地對其進行控制以便將處理時間考慮在內。另一方面,計時器回調意味着該時間間隔不能變化。因此,我們需要選擇足夠大的走時間隔以便完成每個回調的處理,或者通過顯式跟蹤處理一次走時所需的時間來控制走時的處理,並且在必要時跳過走時以保持節奏。
計時器回調的一個主要缺陷是我們需要依賴於操作系統計時器的分辨率。可能存在的最短走時間隔由該計時器可能具有的最高分辨率決定。在計時器分辨率較低的Pocket PC上,這可能成爲一個限制因素,從而意味着這種方法中可能具有的fps也會比較低。另外,操作系統計時器事件的優先級非常低,這意味着遊戲的響應速度也比較低。
雖然有這些侷限,但對於前進速度較低的遊戲(此時幀速率不太重要)而言,這一技術比較適用。例如,屏幕保護程序
private void StartTimer ()
{
int fps = 8;
// Create and hold onto on-screen graphics object
// for the life of the game/timer
m_gxOn = this.CreateGraphics();
// Setup timer callback to happen every (1000/fps) milliseconds
m_tmr = new System.Windows.Forms.Timer();
m_tmr.Interval = 1000/fps;
// Specify the timer callback method
m_tmr.Tick += new EventHandler(this.OnTimerTick);
// Start the timer
m_tmr.Enabled = true;
// Init game params such as level
}
protected void OnTick(object sender, EventArgs e)
{
if (alive)
{
// Regulate tick to include processing time,
// skip tick(s) if necessary
if (processTick)
{
// Process game parameters and render game
if (levelCompleted)
{
// Update game params such as level
}
}
}
else
EndTimer ();
}
private void EndTimer ()
{
// End game
// Dispose timer
m_tmr.Dispose();
// Dispose the on-screen graphics as we don't need it anymore
m_gxOn.Dispose();
m_gxOn= null; // Make sure the garbage collector gets it
// Ready the game for next time around
}
注:遊戲結束時必須處理計時器。
請注意,沒有必要使用Application.DoEvents(),因爲我們既未循環也未阻塞任何系統事件,而計時器回調實際上只是一個系統事件。同時,請注意大多數遊戲邏輯序列被放入到OnTimerTick()事件處理程序中。
無效-更新
另一種使遊戲前進的方式是adhoc(內置動態尋址) 的基礎上渲染它。每當遊戲邏輯檢測到屏幕需要刷新時,我們可以請求系統使屏幕的相應部分無效並對其進行刷新。
這一技術是最簡單的,並且最適合基於用戶交互前進的遊戲。這是指那些不是持續不斷地走時和渲染(如遊戲循環、計時器回調中那樣)的遊戲,而是僅當用戶與其交互時才前進的遊戲,例如智力測驗遊戲。
我們可以通過調用this.Invalidate()使遊戲窗體的整個工作區無效,或者通過調用this.Invalidate(dirtyRect)僅使其一部分無效。
只調用this.Invalidate()不能保證繪圖操作會及時發生。我們必須通過調用this.Update()來確保屏幕在繼續前進之前被刷新。在某些情況下,異步調用Invalidate()和Update()可能有助於獲得更高的性能。但是如果不使用適當的幀同步技術,可能導致屏幕上出現不自然的畫面,或者幀被丟棄。
如果我們能夠承擔得起每次都刷新整個屏幕的開銷,則可以簡單地調用this.Refresh(),它可以確保Invalidate()和Update()依次發生。
當我們作爲所有者繪製窗體時,我們可以在屏幕的某個部分需要刷新時調用this.Refresh(),並且在內部跟蹤屏幕的髒區,以及在OnPaint()和OnPaintBackground()內部有選擇地刷新屏幕。
優化啓動時間
在遊戲中,開發人員通常會預先初始化所有遊戲參數,從而在遊戲進行過程中避免不必要的運行時延遲。這一方法的缺陷在於延遲被轉移到遊戲啓動過程中,如果遊戲花費太長的時間加載,然後用戶才能與其交互,則尤其會令人感到不快。
這種情況下,可取的做法是儘可能快地顯示一個含有與遊戲相關信息的啓動畫面,以便吸引用戶。然後,我們可以在後臺執行啓動活動,如加載資源、初始化遊戲參數等等。
我們可以使用單獨的全屏窗體作爲啓動畫面,也可以使用僅含有基本遊戲信息的主遊戲窗體本身。
public Game()
{
// Set visibility first
this.Visible = true;
// Create on-screen graphics
Graphics gxOn = this.CreateGraphics();
// Display Splash screen
DoSplashScreen(gxOn);
// Destroy on-screen graphics
gxOn.Dispose();
// Proceed with your Game Screen
}
void DoSplashScreen(Graphics gxPhys)
{
// Load minimal resources such as title bitmap
Assembly asm = Assembly.GetExecutingAssembly();
Bitmap bmpTitle =
new Bitmap(asm.GetManifestResourceStream("title"));
// Draw the title screen a€_ this is your splash screen
gxPhys.DrawImage(bmpTitle, 0, 0);
// Now proceed with loading rest of the resources
// and initializing the game
// Regulate the splash time if necessary
}
重要的是不要在啓動畫面中提供任何功能,而只應該將其用作簡介信息頁。並不總是需要通過啓動畫面來啓動。
遊戲按鈕
導航鍵
在Pocket PC中,導航鍵(即向左鍵、向右鍵、向上鍵和向下鍵)在遊戲中發揮着至關重要的作用。我們可以通過重寫遊戲窗體的各個事件方法,訪問這些鍵的KeyDown、KeyPress和KeyUp事件。
通常,我們需要處理這些導航鍵的KeyDown事件,並提供遊戲級功能。
protected override void OnKeyDown(KeyEventArgs keyg)
{
switch(keyg.KeyData)
{
case Keys.Left:
// Provide game functionality for Left key
break;
case Keys.Right:
// Provide game functionality for Right key
break;
case Keys.Up:
// Provide game functionality for Up key
break;
case Keys.Down:
// Provide game functionality for Down key
break;
default:
// We don't care
break;
}
// Always call the base implementation
// so that the registered delegates for this event are raised.
base.OnKeyDown(keyg);
}
Pocket PC筆針
Pocket PC的筆針類似於臺式電腦的鼠標。我們可以通過重寫遊戲窗體的各個事件方法來訪問MouseDown、MouseMove和MouseUp事件。
protected override void OnMouseDown(MouseEventArgs mouseg)
{
Point ptHit = new Point(mouseg.X, mouseg.Y));
}
在Pocket PC上,到1.0版爲止,.NET框架精簡版不支持鼠標右鍵和硬件按鈕。
其他技術
如有可能,應繪製圖形,而不是使用位圖。這將減小內存佔用提高性能。例如,在太空射擊遊戲中,最好不要使用滾動的位圖作爲背景,可以通過用黑色的矩形填充背景然後繪製星星來獲得相同的效果。
嘗試將類似的位圖邏輯地組合爲一個大型位圖,以後根據需要使用相關座標提取適當的單元位圖。擁有一個大位圖而不是多個小位圖可以降低資源大小。
儘可能嘗試使用其他圖形格式而不是BMP,以便利用更好的圖形壓縮技術(例如JPEG)。
避免在所有者繪製的遊戲窗體上使用控件。我們應該作爲所有者繪製所有內容。例如,如果我們需要一個Label,應該使用Graphics.DrawString()而不是創建自定義的子圖形。
以靜態方式儘可能多地初始化遊戲邏輯,從而在運行時避免執行開銷較大的計算。例如,在智力測驗遊戲中,應該儘可能地預先靜態存儲獲勝組合,而不是在遊戲運行過程中使用開銷較大的動態算法以及類似的功能。
小結
在爲諸如Pocket PC這樣的設備編寫遊戲時,需要記住顯示屏幕尺寸要比桌面計算機小得多,並且硬件的功能也沒有桌面計算機那樣強大。
因此,對於這些小型設備,應該比桌面計算機更加嚴格地優化遊戲。同時在設計遊戲時,還應該認真考慮目標硬件、操作系統和.NET框架精簡版的功能。
遊戲的性能高低主要取決於它的繪圖例程。高效的繪圖技術決定了遊戲的響應速度,尤其是在諸如Pocket PC這樣的小型設備中。因此,應該使用前面討論的所有繪圖優化技術(如髒區計算),以便節省每幀的繪圖時間。
幀速率是另一個需要記住的重要參數,請基於目標設備的功能明智地加以選擇。