摘要
以 XNA 為基礎的遊戲可以利 3D 模型為遊戲程式加入動畫效果,也可以利用簡單的程式技巧將 2 維的圖片顯示成動畫。2 維動畫在製作上要比 3 維動畫簡單,在程式控制上也比較單純,但是在效果上當然也比較遜色,不過因為遊戲會吸引使用者使用的原因,遊戲的聲光與娛樂效果只佔其中的一部分,遊戲的內涵、趣味性、和挑戰性也是吸引玩家的重要因素。在這一篇文章中我們將要為大家介紹以 XNA 為基礎的遊戲程式製作 2D 動畫與物體碰撞偵測的技巧。
2 維動畫技術
遊戲程式一般有兩種常見的 2 維動畫效果,一種是動畫物體,另外一種是會移動的背景。動畫物體的效果可以利用一連串不同的小圖案組成的大圖當做動畫的內容,由遊戲程式一次取出一張小圖案來顯示。例如圖 1 所示即為由 12 張小圖組成的大圖:
圖 1:由 12 張小圖組成的大圖
(註:本圖材自 http://www.emutalk.net/showthread.php?t=43273)
在快速取出不同的小圖案來顯示的狀況下,遊戲程式可以顯示出生動的 2 維動畫效果,以執行在 Windows Phone 7 智慧型手機的遊戲程式而言,在正常的狀況下,每秒可以顯示 30 張不同的圖案,如果遊戲程式是執行在 Windows 平台或是 Xbox/Xbox 360 遊戲機,每秒可以顯示高達 60 張不同的圖案,因為人眼有視覺暫留功能,所以物體在快速轉變時,之前顯示的影像雖然消失,但是人的眼睛仍然能繼續保留影像的內容 0.1 秒 ~ 0.4 秒之久,所以物體可以平順地呈現不同的圖案,形成動畫的效果。
第二種常見的 2 維動畫常常應用在遊戲程式的捲動背景,可以用來營造物體往前移動或是往後倒退的效果,有關如何捲動遊戲的背景可以參考 XNA Framework 常用的類別 一文的說明。
實作 2 維動畫
欲利用一連串不同的小圖案組成的大圖當做動畫的內容,由遊戲程式一次取出一張小圖案來顯示,遊戲程式必須知道組成大圖的小圖案的高度和寬度,假設組成大圖的每一張小圖案的高度和寬度皆為 64 ,則從 X 座標為 0 ,Y 座標為 0 的位置取出高度為 64 ,寬度為 64 的圖案,就可以取出第一張小圖案,而從 X 座標為 64 ,Y 座標為 0 的位置取出高度為 64 ,寬度為 64 的圖案,就可以取出第二張小圖,如果要取出第二排的第一張小圖案,則可以從 X 座標為 0 ,Y 座標為 64 的位置取出高度為 64 ,寬度為 64 的圖案,以此類推。其座標的計算方式為 X 座標往右增加,Y 座標往下增加,而座標的原點在圖案的左上角,如圖 2 所示:
圖 2 :取用大圖中的小圖案的座標計算方式
(註:本圖材自 http://www.emutalk.net/showthread.php?t=43273)
遊戲程式只要計算好欲取用的小圖案的起始座標,以及小圖案的高度與寬度,再透過 SpriteBatch 類別的 Draw 方法的 SourceRectangle 參數指定來源圖案的矩形(指定來源圖案的矩形相當於指定來源圖案的起始座標和高度與寬度),就可以從來源圖案取出指定區域的內容,再顯示到遊戲的視窗供使用者檢視。有關支援指定 SourceRectangle 參數的 SpriteBatch 類別的 Draw 方法原形如下:
public void Draw ( Texture2D texture, Vector2 position, Nullable<Rectangle> sourceRectangle, Color color )
遊戲程式可以利用以下的公式取出組成大圖的任何一張小圖案:
Rectangle SourceRectangle=new Rectangle((Column-1) *小圖圖寬,
(Row -1) *小圖圖高,小圖圖寬,小圖圖高);
其中的 Row 代表列編號,Column 代表欄編號,所以當遊戲程式欲取用第一列的第一張小圖時,SourceRectangle 的計算方式如下:
Rectangle SourceRectangle=new Rectangle(0, 0,小圖圖寬,小圖圖高);
當遊戲欲取第一列的第二張小圖時,SourceRectangle 的計算方式如下:
Rectangle SourceRectangle=new Rectangle(小圖圖寬, 0,
小圖圖寬,小圖圖高);
當遊戲欲取第二列的第一張小圖時,SourceRectangle 的計算方式如下:
Rectangle SourceRectangle=new Rectangle(0,小圖圖高,
小圖圖寬,小圖圖高);
要將圖 2 所示的大圖顯示成 2 維動畫,請先啟動 Visual Studio 2010 Express for Windows Phone ,建立一個型態為 [Windows Phone Game(4.0)] 型態的專案,然後將圖 2 所示的圖片加入到 Content Pipeline 專案中。
[提示]
您可以視需要加入背景圖案當做遊戲的背景。
做好之後請於 Game1 類別加入以下的變數宣告:
Texture2D Mario; //管理圖檔的變數 Texture2D Background; //管理背景圖案的變數 int RowCount = 2; //圖檔每一欄的小圖案數目 int ColumnCount = 6; //圖檔每一列的小圖案數目 int RowIndex = 1; //第一個小圖案的列編號 int ColumnIndex = 1; //第一個小圖案的欄編號 Vector2 BasePosition; //存放小圖案顯示的位置的變數 Rectangle SourceRectangle; //欲取用的小圖案位置 int MarioWidth = 114; //組成大圖的小圖案寬度 int MarioHeight = 120; //組成大圖的小圖案高度
宣告好變數之後,請於 Game1 類別的建構函式中設定遊戲視窗的高度與寬度,編輯好的 Game1 類別建構函式如下:
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; graphics.PreferredBackBufferHeight = 800; //設定遊戲視窗的高度為800 graphics.PreferredBackBufferWidth = 480; //設定遊戲視窗的寬度為480 TargetElapsedTime = TimeSpan.FromTicks(333333); }
做好之後請編輯 Game1 類別的 LoadContent 方法,從 Content Pipeline 專案載入欲顯示的大圖,並設定組成大圖的小圖案欲顯示的位置,編輯好的 LoadContent 方法如下:
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); Mario = Content.Load<Texture2D>("MarioAnimate12"); //從Content Pipeline專案載入圖形 Background = Content.Load<Texture2D>("Background"); //從Content Pipeline專案載入背景 Viewport viewport = graphics.GraphicsDevice.Viewport; //取得遊戲視窗的大小 BasePosition = new Vector2(0, viewport.Height - 220); //設定小圖案顯示的位置 }
載入好欲顯示的圖形之後,請將 Game1 類別的 Update 方法編輯成以下的樣子,負責從圖 2 所示的大圖依序取出每一張小圖案來顯示:
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); SourceRectangle = new Rectangle((ColumnIndex - 1) * MarioWidth, (RowIndex - 1) * MarioHeight, MarioWidth, MarioHeight); //計算欲取用的小圖案的位置 if (ColumnIndex <= ColumnCount) //如果第一列的小圖案尚未取完 { ColumnIndex++; //遞增欲取用的小圖案編號 } else //否則 { ColumnIndex = 1; //設定要讀取第一欄的小圖案 RowIndex++; //遞增欲讀取的圖形列編號 if (RowIndex > RowCount) //如果已經讀完所有的列 { RowIndex = 1; //設定要讀取第一列的小圖案 } } BasePosition.X++; //令小圖案以每次一個單位往右移動 base.Update(gameTime); }
最後請將 Game1 類別的 Draw 方法編輯成以下的樣子,負責依據欲取用的小圖案的位置取出小圖案,並顯示在由 BasePosition 變數指定的位置:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); //宣告繪圖動作開始 spriteBatch.Draw(Background, Vector2.Zero, Color.White); //顯示背景 spriteBatch.Draw(Mario, BasePosition, SourceRectangle, Color.White); //顯示取出的小圖案 spriteBatch.End(); //宣告繪圖動作結束 base.Draw(gameTime); }
做好之後請執行專案,您就會看到圖 2 所示的大圖形中的每一張小圖案會依序被遊戲程式取出來顯示,因為圖形更換的速度很快,所以可以形成動畫的效果,如圖 3 所示:
圖 3 :以 XNA 為基礎的遊戲程式顯示 2 維的動畫的情形
碰撞偵測
當遊戲程式顯示的物體和動作過程或是移動過程發生碰撞時,遊戲程式必須能夠偵測物體是否發生碰撞,並據以做出適當的反應,例如在飛彈擊中入侵的異形時發出爆炸的聲音,或是在棒搥打中怪獸時發出慘叫的聲音,達到與使用者互動的效果,添加遊戲的趣味性與臨場感。
使用 XNA Framework 開發遊戲程式的程式設計師可以利用 BoundingBox 結構、BoundingSphere 結構、或是 BoundingFrustum 類別輔助遊戲程式判斷物體是否發生碰撞,不需要自己辛苦地利用幾何學的技巧進行判斷。
使用 BoundingBox 結構判斷物體是否發生碰撞主要的概念是利用立方體來描述物體,當兩個代表物體的立方體產生交集的時候便是物體發生碰撞;使用 BoundingSphere 結構判斷物體是否發生碰撞主要的概念是利用球體來描述物體,當兩個代表物體的球體產生交集的時候便是物體發生碰撞;而使用 BoundingFrustum 類別判斷物體是否發生碰撞主要的概念是利用斷面錐體來描述物體,當兩個代表物體的斷面錐體產生交集的時候便是物體發生碰撞。
圖 4 所示即為利用 BoundingBox 結構定義的立方體描述遊戲物體的示意圖,圖 5 所示即為利用 BoundingSphere 結構定義的球體描述遊戲物體的示意圖,圖 6 所示即為利用 BoundingFrustum 類別定義的斷面錐體描述遊戲物體的示意圖。
圖 4 :利用 BoundingBoxn 結構定義的立方體描述遊戲物體的示意圖
圖 5 :利用 BoundingSphere 結構定義的球體描述遊戲物體的示意圖
圖 6 :利用 BoundingFrustum 類別定義的斷面錐體描述遊戲物體的示意圖
欲利用 BoundingBox 結構描述遊戲的物體,程式必須定義描述物體的立方體的最小座標點和最大座標點,欲利用 BoundingSphere 結構描述遊戲的物體,程式必須定義描述物體的球體的圓心和半徑,而欲利用 BoundingSphere 類別描述遊戲的物體,程式必須定義描述物體的斷面錐體的各個頂點的座標。
表 1 所示為 BoundingBox 結構常用的屬性:
屬性名稱 | 說明 |
Max | 描述物體的立方體的頂點的座標中內容值最大者。 |
Min | 描述物體的立方體的頂點的座標中內容值最小者。 |
表 2 所示為 BoundingBox 結構常用的方法:
方法名稱 | 說明 |
Contains | 判斷BoundingBox結構描述的立方體是否包含指定的立方體、球體、座標、或是斷面錐體。 |
CreateFromPoints | 依據一組座標點建立立方體。 |
CreateFromSphere | 依據 BoundingSphere 結構描述的球體立方體。 |
CreateMerged | 依據兩個 BoundingBox 結構描述的立方體組成立方體。 |
Intersects | 判斷 BoundingBox 結構描述的立方體是否與指定的立方體、球體、斷面錐體、平面、或光束有交集。 |
BoundingSphere 結構常用的屬性可以參考表 3 的說明:
屬性名稱 | 說明 |
Center | 描述球體圓心的座標。 |
Radius | 描述球體的半徑。 |
BoundingSpheren 結構常用的方法可以參考表 4 的說明:
方法名稱 | 說明 |
Contains | 判斷 BoundingSphere 結構描述的球體是否包含指定的立方體、球體、座標、或是斷面錐體。 |
CreateFromBoundingBox | 依據 BoundingBox 結構描述的立方體建立球體。 |
CreateFromFrustum | 依據 BoundingFrustum 類別描述的斷面錐體建立球體。 |
CreateFromPoints | 依據一組座標點建立球體。 |
CreateMerged | 依據指定的兩個球體建立組合的球體。 |
Intersects | 判斷 BoundingSphere 結構描述的球體是否與指定的立方體、球體、斷面錐體、平面、或光束有交集。 |
Transform | 平移或放大/縮小 BoundingSphere 結構描述的球體。 |
BoundingFrustum 類別常用的屬性可以參考表 5 的說明:
屬性名稱 | 說明 |
Bottom | 取得 BoundingFrustum 類別描述的斷面錐體的底部平面。 |
Far | 取得 BoundingFrustum 類別描述的斷面錐體的遠平面。 |
Left | 取得 BoundingFrustum 類別描述的斷面錐體的左方平面。 |
Matrix | 取得 BoundingFrustum 類別描述的斷面錐體的矩陣表示法。 |
Near | 取得 BoundingFrustum 類別描述的斷面錐體的近平面。 |
Right | 取得 BoundingFrustum 類別描述的斷面錐體的右方平面。 |
Top | 取得 BoundingFrustum 類別描述的斷面錐體的上方平面。 |
BoundingFrustum 類別常用的方法可以參考表 6 的說明:
方法名稱 | 說明 |
Contains | 判斷 BoundingFrustum 類別描述的斷面錐體是否包含指定的立方體、球體、座標、或是斷面錐體。 |
GetCorners | 取得包含 BoundingFrustum 類別描述的斷面錐體的所有頂點的陣列。 |
Intersects | 判斷 BoundingFrustum 類別描述的斷面錐體是否與指定的立方體、球體、斷面錐體、平面、或光束有交集。 |
[提示]
不管是 BoundingBox 結構的 Min/Max 屬性,BoundingSphere 結構的 Center 屬性,還是 BoundingFrustum 類別的 GetCorners 方法傳回的陣列的元素型態,都是 Vector3 結構型態的資料,也就是 3 度空間的座標,換句話說,BoundingBox 結構、 BoundingSphere 結構、以及 BoundingFrustum 類別都可以適用於偵測 3 維遊戲的物體碰撞,如果要應用在2維遊戲的物體碰撞偵測,則將代表Z軸的座標的內容值設定成0即可,因為2維空間的物體沒有Z軸的座標。透過將Z軸的座標的內容值設定成0,2維遊戲程式一樣能夠利用 BoundingBox 結構、BoundingSphere 結構、或是 BoundingFrustum 類別來判斷物體與物體是否發生碰撞。
了解使用 XNA Framework 的遊戲程式如何利用 XNA 支援的結構和類別進行碰撞偵測判斷之後,接下來我們就要修改上述的遊戲程式,在物體移動到與背景右下角的圓柱體發生碰撞時,禁止物體繼續往右移動。
首先請在 Game1 類別中加入以下的變數宣告
BoundingBox bbActor; //負責定義移動物體大小的立方體 BoundingBox bbCylinder; //負責定義遊戲背景右下角的圓柱體的立方體
做好之後請於 Game1 類別的 LoadContent 方法中加入以下的程式碼,負責設定遊戲背景右下角的圓柱體的立方體的位置:
bbCylinder.Min = new Vector3(396, 545, 0); //設定定義圓柱體的立方體的最小座標點 bbCylinder.Max = new Vector3(463, 698, 0); //設定定義圓柱體的立方體的最大座標點
然後請編輯 Game1 類別的 Update 方法,以下的程式碼:
base.Update(gameTime);
的前面加入下列的程式碼,負責設定描述移動物體的立方體的大小,並在移動的物體觸及遊戲背景右下角的圓柱體時停止往右移動物體的動作:
bbActor.Min = new Vector3(BasePosition.X, BasePosition.Y, 0); //設定定義移動物體的立方體的最小座標點 bbActor.Max = new Vector3(BasePosition.X + MarioWidth, BasePosition.Y + MarioHeight, 0);//設定定義移動物體的立方體的最大座標點 if (!bbActor.Intersects(bbCylinder)) //如果移動的物體尚未與遊戲背景右下角 //的圓柱體發生碰撞 { BasePosition.X++; //將物體往右移動一個單位 }
做好之後請執行專案,待遊戲程式顯示的物體一直往右移動至碰撞到遊戲背景右下角的圓柱體時,就會停止繼續往右移動的動作,如圖7所示:
圖 7 :遊戲程式顯示的物體碰撞到遊戲背景右下角的圓柱體後停止繼續往右移動的情形