某天,當你一不小心發現已經夠隨心所欲的駕馭3D攝像機之時,任何類型的3D游戲都將成為囊中玩物,過往如煙。
回憶逝去的童年讓我極度惦記的SLG策略戰棋游戲,或許對於大多數玩家來說,它費時費力不被討好;然而深邃的內涵和無限可能的戰略戰術始終占據着我內心很大一片天地。於是,在本系列前5節2D SLG知識原理的基礎上,萌發了移植一款基於平面的3D SLG Demo計划。
首先,什么是基於平面的3D SLG游戲?大伙不妨先看看以下幾款該類型經典游戲巨作截圖 - 《英雄無敵6》、《文明5》和《三國志11》:
無論地形單元格為四邊形或六邊形,其整體地貌都不存在高低起伏(No HeightMap);用游戲開發者的話說便是:三維空間中,一條軸用做旋轉,另外兩條軸形成類似2D中的Canvs平面承載對象。這樣的設計更像是一盤3D化棋局,地形好比棋盤盤面,角色仿若棋子,附帶一個環繞棋盤的360°軌道攝像機,無論視野還是戰術方略都能得到淋漓盡致的體現。
當然,除此之外,層次感更分明,基於HeightMap地形的立面3D SLG游戲亦備受日系游戲青睞,不乏大作,比如《火焰紋章 曉之女神》、《皇家騎士團:命運之輪》和《三國志戰記2》等,該類型游戲通常需要輔以更加復雜而強大的地形編輯器,這些內容並不屬於本節范疇,后續章節中若有時間再做補充:
OK,做足了SLG游戲設計方面的知識准備,接下來我們要做的頭等大事便是打開第4節的源碼,神馬差集運算、四叉樹算法、蜂窩拓撲算法、A*算法等等統統一並拿來,將其中的Point改成Vector3(即原先的Point(X,Y)更換成Vector3(X,0,Y)),嘿嘿,原來編碼也是可以這么浮雲的。舉個例吧,其中的DirectionScan方法在移植前后的對比:
2D游戲中所有我們看得到的圖形都是通過Image圖片的形式予以呈現,而到了3D游戲中,這條路已經行不通了。比如我們要繪制3D四邊形或3D蜂窩狀地形單元格,此時就得自己編寫基於三角面合成的3D面控件:

/// 3D圖形(面)基類
/// </summary>
public abstract class Shape3D : Object3D {
protected Camera3D camera;
protected Texture2D texture;
protected BasicEffect effect;
protected short[] indices;
protected VertexPositionTexture[] vertices;
public Shape3D(ContentManager content, GraphicsDevice device, Camera3D camera)
: base(content, device) {
this.camera = camera;
effect = new BasicEffect(device) { TextureEnabled = true };
}
string _TextureName;
/// <summary>
/// 獲取或設置紋理資產名稱
/// </summary>
public string TextureName {
get { return _TextureName; }
set {
_TextureName = value;
texture = content.Load<Texture2D>(value);
effect.Texture = texture;
}
}
public override void Draw(GameTimerEventArgs e, ModelBatch modelBatch) {
effect.GraphicsDevice.BlendState = BlendState.AlphaBlend; // 設置透明覆蓋(透明顯示紋理alpha透明部分)
effect.World = World;
effect.View = camera.View;
effect.Projection = camera.Projection;
effect.CurrentTechnique.Passes[ 0].Apply();
device.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
}
}

/// 3D矩形(面)控件
/// </summary>
public sealed class Rect3D : Shape3D {
public Rect3D(ContentManager content, GraphicsDevice device, Camera3D camera, Vector2 radius)
: base(content, device, camera) {
this.Radius = radius;
}
Vector2 _Radius;
/// <summary>
/// 獲取或設置寬高半徑
/// </summary>
public Vector2 Radius {
get { return _Radius; }
set {
_Radius = value;
indices = new short[ 6] { // 2個三角形4個頂點(0,1,2,3)組成1個四邊形
0, 2, 1,
0, 3, 2
};
vertices = new VertexPositionTexture[ 4];
vertices[ 0].Position = new Vector3(-value.X, 0, value.Y);
vertices[ 0].TextureCoordinate = new Vector2( 0, 1);
vertices[ 1].Position = new Vector3(value.X, 0, value.Y);
vertices[ 1].TextureCoordinate = new Vector2( 1, 1);
vertices[ 2].Position = new Vector3(value.X, 0, -value.Y);
vertices[ 2].TextureCoordinate = new Vector2( 1, 0);
vertices[ 3].Position = new Vector3(-value.X, 0, -value.Y);
vertices[ 3].TextureCoordinate = new Vector2( 0, 0);
}
}
}

/// 3D六邊形(面)控件
/// </summary>
public sealed class Hex3D : Shape3D {
public Hex3D(ContentManager content, GraphicsDevice device, Camera3D camera, int radius)
: base(content, device , camera) {
this.Radius = radius;
}
int _Radius;
/// <summary>
/// 獲取或設置半徑
/// </summary>
public int Radius {
get { return _Radius; }
set {
_Radius = value;
indices = new short[ 18] { // 6個三角形18個頂點組成1個四邊形
0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 5,
0, 5, 6,
0, 6, 1
};
vertices = new VertexPositionTexture[ 6]; // 注意,紋理一定要邊緣匹配,否則會導致殘影
float sqrt3 = ( float)Math.Sqrt( 3);
vertices[ 0].Position = new Vector3(-value, 0, 0);
vertices[ 0].TextureCoordinate = new Vector2( 0, 0.5f);
vertices[ 1].Position = new Vector3(-value / 2, 0, -sqrt3 * value / 2);
vertices[ 1].TextureCoordinate = new Vector2( 0.25f, ( 2 - sqrt3) / 4);
vertices[ 2].Position = new Vector3(value / 2, 0, -sqrt3 * value / 2);
vertices[ 2].TextureCoordinate = new Vector2( 0.75f, ( 2 - sqrt3) / 4);
vertices[ 3].Position = new Vector3(value, 0, 0);
vertices[ 3].TextureCoordinate = new Vector2( 1, 0.5f);
vertices[ 4].Position = new Vector3(value / 2, 0, sqrt3 * value / 2);
vertices[ 4].TextureCoordinate = new Vector2( 0.75f, ( 2 + sqrt3) / 4);
vertices[ 5].Position = new Vector3(-value / 2, 0, sqrt3 * value / 2);
vertices[ 5].TextureCoordinate = new Vector2( 0.25f, ( 2 + sqrt3) / 4);
}
}
}
其中四邊形只需2個三角形即可,而六邊形則可由6個完全一樣的正三角形組合而成。
接下來再賦予這些單元格以紋理,配上之前移植過來的所有算法,很酷的3D地形即刻呈現在我們面前(額外提醒一下,在Draw時必須設置紋理的BlendState為Alpha 混合(basicEffect.GraphicsDevice.BlendState = BlendState.AlphaBlend;),否則這些紋理的透明部分將會被可惡的黑色所覆蓋):
如此漂亮的地磚Tile,也得有能夠與之相匹配的3D場景才算協調。話說3D場景與2D場景真是截然不同,3D場景大多基於模型,比如剛從網上下載的一個宮殿場景,還附贈了一個天空盒呢(順帶鄙視下該天空盒,即非半球又非四方,嗯,很有偷懶嫌疑):
將整個模型從3DMAX中導出成.X或.FBX文件后,我們便可在游戲中直接載入,很酷吧,天圓地方,魔獸出沒皇宮中:
慢着,你剛才說啥來着?魔獸?
拜托呀,大哥。對埃及神話中那個狗頭人身的死神不清楚就算了,如今,《魔獸世界》中如此偉大的“阿努比薩斯”活生生的矗立在你面前,汝等依舊能保持如此之淡定,小弟不勝佩服。
其實,此次Demo制作也印證了一個事實:《魔獸世界》中的模型大多還是以中低品質模型為主,奇跡的誕生並非與模型復雜度成正比。而目前市面上絕大多數的XNA骨骼動畫模型素材管道最多僅支持72塊骨骼解析,若想展示次世代模型還得找到更加強悍的素材管道才行(或者哪位大神幫忙寫個?哈哈):
至此,3D SLG游戲場景全部布置完畢,接下來是操作部分。
2D游戲基於Canvas平面畫布,鼠標點擊的地方可謂所見即所得; 3D游戲則大為不同,無論它的顯示載體是PC的顯示屏還是Windows Phone的觸摸屏,基於二維平面的點擊/觸碰操作要完成三維空間的精確拾取,仿佛是件不可能的事。
然而前人的智慧告訴我們,一條射線便可輕松搞定這一切,這就是傳說中的
“3D射線拾取法”:
通過屏幕點擊位置垂直於屏幕3D空間向內發射射線,利用射線的穿透效果拾取一切3D對象。
其實射線拾取法也可以通俗的理解為碰撞檢測,用開發者的話說便是Ray是否與模型的BoundingBox、BoundingSphere、BoundingFrustum或某個Plane等對象存在交點:
在3D世界里,通常為了高性能檢測模型之間的碰撞,會用到Box(立方體)、Sphere(球體)或者Frustum(錐體)包裹住模型,包裹物之間的交錯關系即視為模型之間的碰撞關系。其實很多2D游戲也效仿了類似的做法來處理各類碰撞檢測。
非常幸運的是,Engine Nine除了為我們提供強大的骨骼動畫解析外,還拓展了Model里的Intersects方法,用於檢測基於模型BoundingSphere的Pick操作,精確度還蠻高的,再配合上一些相關算法,最終便完成了3D SLG游戲中的角色模型拾取和單元格命中操作:

void inputHandler_Press( object sender, TouchEventArgs e) {
switch (Global.TouchHandler) {
case TouchHandlers.SelectLeader:
#region 命中角色處理
Ray ray = device.Viewport.GetPickRay(( int)e.Point.Position.X, ( int)e.Point.Position.Y, camera.View, camera.Projection);
scene.RoleList.ForEach(X => {
float? distance = X.IsRayPass(ray);
if (distance.HasValue) { SetLeader(X); }
});
#endregion
break;
case TouchHandlers.MoveLeader:
#region 命中單元格處理
Vector3? target = Get3DPickPosition(e.Point.Position);
bool hitPath = false;
if (target.HasValue) {
for ( int i = 0; i < scene.PathRangeList.Count; i++) {
Vector3 destination = scene.PathRangeList[i].Coordinate;
if (!hitPath && Terrain.GetCoordinateFromPosition( new Vector3(target.Value.X + Terrain.TileRadius, ( int)target.Value.Y, target.Value.Z + Terrain.TileRadius)) == destination) {
scene.PathRangeList[i].TextureName = string.Format( " Texture/{0} ", Global.TileDirectionNum == TileDirectionNums.Six ? " Hex1 " : " Box1 ");
leader.MoveTo(destination, terrain.DynamicMatrix);
hitPath = true;
} else {
scene.PathRangeList[i].TextureName = string.Format( " Texture/{0} ", Global.TileDirectionNum == TileDirectionNums.Six ? " Hex0 " : " Box0 ");
}
}
tb0.Text = string.Format( " 觸碰位置 {0} 命中場景位置 ({1},{2},{3}) ", e.Point.Position, ( int)target.Value.X, ( int)target.Value.Y, ( int)target.Value.Z);
}
#endregion
break;
}
}
角色移動處理是3D SLG游戲制作的最后環節。設計方面通常有兩種方案:第一種是由起點向終點沿尋路路徑移動,這種基於A*算法的移動在我之前的教程都講爛掉了不再贅述;而另外的則是像《英雄無敵3》那樣直接做由起點向終點的直線移動(題外話,制作完Demo后才發現,基於六方格的地形真不適合沿路徑移動,非常別扭)。后者實現方法也很簡單,按照第七節開頭所述原理,分割出X和Z分向量速度即可:

float xMoveSpeed, zMoveSpeed;
/// <summary>
/// A*尋路向目的地移動
/// </summary>
public void MoveTo(Vector3 coordinate, byte[,] matrix) {
if (movePath.Count == 0) {
// movePath.Clear();
PathFinderFast pathFinderFast = new PathFinderFast(matrix) {
TileDirectionNum = Global.TileDirectionNum,
HeuristicEstimate = 2,
SearchLimit = 200,
};
List<PathFinderNode> path = pathFinderFast.FindPath(
new Point() { X = ( int)Coordinate.X, Y = ( int)Coordinate.Z },
new Point() { X = ( int)coordinate.X, Y = ( int)coordinate.Z }
);
if (path == null || path.Count < 1) {
// 路徑不存在
return;
} else {
switch (Global.MoveMode) {
case MoveModes.Line:
movePath.Add( new Vector3(( float)path[ 0].X, 0, ( float)path[ 0].Y));
Vector3 v = Terrain.GetPositionFromCoordinate(movePath[ 0]);
float distance = ( float)Math.Sqrt(Math.Pow((v.X - Position.X), 2) + Math.Pow((v.Z - Position.Z), 2));
float countMove = distance / MoveSpeed;
xMoveSpeed = (Math.Abs(v.X - Position.X) / countMove) * (v.X < Position.X ? - 1 : 1);
zMoveSpeed = Math.Abs(v.Z - Position.Z) / countMove * (v.Z < Position.Z ? - 1 : 1);
RotationY = MathHelper.ToRadians( 90 - MathHelper.ToDegrees(( float)Math.Atan2(v.Z - Position.Z, v.X - Position.X))); // 糾正角度
break;
case MoveModes.Path:
for ( int i = path.Count - 1; i >= 0; i--) {
movePath.Add( new Vector3(( float)path[i].X, 0, ( float)path[i].Y));
}
break;
}
}
Run();
}
}
嘿嘿,收工。
啥?
人太少不給力?
那么我們刷300個《魔獸世界》里的小怪出來開心開心吧,順便也檢測下本節的各種3D算法是否正確:
本節Demo源碼下載地址:(WP)SLXnaGame3
Silverlight版本下載地址:(SL)SLXnaGame3
在線演示地址:Cangod.com
手記小結:《魔獸世界》運行於Windows Phone 和 Silverlight之上,想想都讓人口水直流;因為我們對游戲的執着與狂熱,使得這個夢想變得不再遙不可及。 3D游戲開發今非昔比,日新月異的技術進步讓它變得並非難如煉獄;長期的2D游戲積累和虔誠的設計感悟,從2D向3D轉型一日千里。磨練過的勇士將創新出更多屬於中國自己的游戲奇跡,你手中的鍵盤鼠標便是最鋒利的戰具!