最近看了看一個C#游戲開發的公開課,在該公開課中使用面向對象思想與Unity3D游戲開發思想結合的方式,對一個簡單的賽車游戲場景進行了實現。原本在C#中很方便地就可以完成的一個小場景,使用Unity3D的設計思想(即一切游戲對象皆空對象,拖拽組件才使其具有了活力)來實現卻需要花費大量時間與精力,究竟它神奇在什么地方?本文通過實現這個小例子來看看。
一、空對象與組件
在Unity3D最常見的就是GameObject,而一個GameObject被實例化后確啥特性與行為都沒有,只有當我們往其中拖拽了一個或多個組件(Component)后才會有行為。例如上圖中,我們創建了一個Cube球體,我們想要它能夠具有重力,這時我們可以為其添加一個剛體組件,該組件幫我們實現了重力的效果,如下圖所示,該球體具有了重力,會進行自由落體運動。
組件(Component)是用來綁定到游戲對象(Game Object)上的一組相關屬性。本質上每個組件是一個類的實例。Unity3D常見的組件有:MeshFilter、MeshCollider、Renderer、Animation等等。其實不同的游戲對象,都可以看成是一個空的游戲對象,只是綁定了不同的組件。比如:Camera對象,就是一個空對象,加上Camera、GUILayer、FlareLayer、AudioListener等組件。其他對象綁定的組件,可自行觀察。
下面的代碼則展示了在Unity3D中實現為GameObject加入剛體組件,可以看到GameObject提供了一個實例方法:AddComponent<T>
GameObject goCube = GameObject.CreatePrimitive(PrimitiveType.Cube); goCube.transform.position = new Vector3(0, 0, 0); // 為Cube添加剛體組件 goCube.AddComponent<Rigidbody>();
到底有哪些組件可以添加呢?可以說有無數種組件,只是有一些特別常用的,被Unity3D預先弄好了。組件的目的是為了控制游戲對象,通過改變游戲對象的屬性,以便同用戶或玩家進行交互。不同的游戲對象可能需要不同的組件,甚至有些需要自定義的組件才能實現。
二、設計思路
2.1 GameObject—基本對象
在GameObject的設計中,首先定義了一個Transform類,定義游戲對象的Position(坐標位置)、Scale(縮放比例)等基本信息,然后提供方法供接受拖拽到自己身上的游戲組件並記錄到集合中。利用事件的特性(事件鏈),當GameObject的特定事件(這里主要是KeyDown、KeyUp與Update三個事件)被觸發時,會依次觸發注冊到該GameObject的所有組件的特定事件方法。
可以從類圖中看出,GameObject作為基本對象,沒有實現具體的表現和行為,而是提供了可供添加組件的方法來實現讓我們可以將組件拖拽到其上邊,讓組件來控制GameObject的行為和展現。
2.2 Component—萬能組件
在對組件的設計中,采用了完全的面向對象思想設計。首先,IComponent接口定義了在本游戲中各個組件需要實現的一個或多個方法,各個組件只需要實現IComponent接口便可以被注冊到GameObject中。其次,由於各個組件都具有一些公有的特性,因此設計了一個組件基類BaseComponent,它實現了一個Start()方法,並確保該方法只被調用一次。最后,繼承於BaseComponent設計實現各個不同的游戲組件,他們重寫了一個或多個基類中實現IComponent中的方法。有了這些組件,我們就可以將其注冊到游戲對象上,游戲也就因此有了活力。
三、實現流程
3.1 實現GameObject類
(1)設計Delegates類,它定義了游戲中需要的所有的委托定義,方便了事件的實現。
public class Delegates { public delegate void UpdateEventHandler(GameObject sender, Rectangle rect, Graphics g); public delegate void KeyDownEventHandler(GameObject sender, KeyEventArgs e); public delegate void KeyUpEventHandler(GameObject sender, KeyEventArgs e); }
(2)在GameObject中定義所有Delegates中的委托為事件實例,並提供執行事件的公有方法。
(3)在GameObject中定義AddComponet方法,提供對為游戲對象添加組件的代碼實現。(PS:這里方法定義時需要使用泛型)
public class GameObject { // 控制游戲對象變換的屬性Transform public Transform Transform { get; set; } // 控制能夠拖拽到游戲對象的組件集合 public IList<IComponent> Components { get; set; } public GameObject() { this.Transform = new Transform() { Position = new Position(), Scale = new Scale(1, 1), Rotation = 0 }; this.Components = new List<IComponent>(); } // Update事件 public event Delegates.UpdateEventHandler Update; // KeyDown事件 public event Delegates.KeyDownEventHandler KeyDown; // KeyUp事件 public event Delegates.KeyUpEventHandler KeyUp; // 執行Update事件 public void OnUpdate(object sender, PaintEventArgs e) { if (Update != null) { Update(this, e.ClipRectangle, e.Graphics); } } // 執行KeyDown事件 public void OnKeyDown(object sender, KeyEventArgs e) { if (KeyDown != null) { KeyDown(this, e); } } // 執行KeyUp事件 public void OnKeyUp(object sender, KeyEventArgs e) { if (KeyUp != null) { KeyUp(this, e); } } // 提供方法供接受拖拽到自己身上的游戲組件 public TResult AddComponent<TResult>() where TResult : IComponent, new() { // 創建游戲組件 TResult component = new TResult(); // 為游戲對象注冊組件的事件 this.Update += component.Update; this.KeyDown += component.KeyDown; this.KeyUp += component.KeyUp; return component; } }
3.2 實現游戲對象的事件
(1)設計BaseComponent類,它是各個游戲組件的基類,實現了IComponent接口,並定義了Start方法(該方法只會在開始時被執行一次)。
public abstract class BaseComponent : IComponent { public GameObject GameObject { get; set; } protected bool isStarted = false; // 游戲組件啟動時的事件(該事件只被執行一次) public virtual void Start(GameObject sender, Rectangle rect, Graphics g) { // 記錄當前的游戲對象 this.GameObject = sender; } // 游戲組件每一幀都要執行的Update事件 public virtual void Update(Common.GameObject sender, System.Drawing.Rectangle rect, System.Drawing.Graphics g) { // 首先確保Start方法只被執行一次 if (!isStarted) { Start(sender, rect, g); isStarted = true; } } // 當用戶按下鍵盤某個鍵時觸發的KeyDown事件 public virtual void KeyDown(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e) { } // 當用戶松開鍵盤某個鍵時觸發的KeyUp事件 public virtual void KeyUp(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e) { } }
(2)實現游戲組件子類:BackgroudBehavior(游戲背景組件)、SpriteRender(對象渲染組件)、UserControl(用戶控制組件):為BackgroudBehavior添加一個SpriteRender組件已實現渲染游戲背景圖片,SpriteRender則負責將圖片屬性進行渲染到窗體界面中,UserControl則負責實現玩家控制賽車的上下左右移動。這里以UserControl組件為例,通過重寫KeyDown和KeyUp兩個事件完成對玩家小車方向的控制(通過改變x,y兩個滑動值,然后再窗體中通過定時器迅速地更新坐標值,最后重繪整個窗體界面,只不過刷新地頻率很快)。
public class UserControl : BaseComponent { private int x; private int y; private Timer timer; public override void Start(Common.GameObject sender, System.Drawing.Rectangle rect, System.Drawing.Graphics g) { base.Start(sender, rect, g); timer = new Timer(); // 為Timer注冊Tick事件讓玩家可以進行移動的操作 timer.Tick += (s, e) => { Move(this.x, this.y); }; timer.Interval = 20; timer.Start(); } // 實現控制玩家賽車的移動->當前坐標+=x,y這兩個滑動值的值 private void Move(int x, int y) { var pos = GameObject.Transform.Position; pos.X += x; pos.Y += y; // 將改變后的坐標重新賦值給游戲對象的坐標 this.GameObject.Transform.Position = pos; } // 實現玩家控制賽車的上下左右移動->為x,y這兩個滑動值賦值 public override void KeyDown(Common.GameObject sender, System.Windows.Forms.KeyEventArgs e) { if (e.KeyCode == Keys.W) { this.y = 5; } else if (e.KeyCode == Keys.S) { this.y = -5; } else if (e.KeyCode == Keys.A) { this.x = -5; } else if (e.KeyCode == Keys.D) { this.x = 5; } } // 當鍵盤鍵抬起時將x,y這兩個滑動值均賦為0 public override void KeyUp(Common.GameObject sender, KeyEventArgs e) { if (e.KeyCode == Keys.W) { this.y = 0; } else if (e.KeyCode == Keys.S) { this.y = 0; } else if (e.KeyCode == Keys.A) { this.x = 0; } else if (e.KeyCode == Keys.D) { this.x = 0; } } }
3.3 實現游戲窗體與游戲場景
(1)BaseForm為所有Form的基類,它重寫了OnLoad方法,使用雙緩沖解決屏幕閃爍問題。MainForm為BaseForm的子類,作為游戲的主界面顯示。
(2)GameScene類為游戲場景類,這里只有一個場景,所以只有一個GameScene類。GameScene通過記錄當前的游戲場景與當前場景中所有的游戲對象(通過集合記錄),通過Timer定時使窗體觸發重繪,還提供了AddGameObject與RemoveGameObject方法供窗體添加和移除游戲對象使用。
public class GameScene { // 記錄當前正在運行的游戲窗體 public BaseForm target { get; set; } // 記錄游戲場景中的所有游戲對象 public IList<GameObject> GameObjects { get; set; } public GameScene(BaseForm target, int fps) { // 初始化當前正在運行的游戲窗體 this.target = target; // 初始化游戲對象集合 GameObjects = new List<GameObject>(); // 啟動一個定時器不停的刷新當前場景使其發生重繪 var timer = new Timer(); timer.Interval = 1000 / fps; timer.Tick += (s, e) => { // 使界面無效並發生重繪 this.target.Invalidate(); }; timer.Start(); } // 將游戲對象添加到集合中並且注冊相應的事件給窗體 public void AddGameObject(GameObject go) { GameObjects.Add(go); // 為游戲場景窗體添加相應的事件 this.target.Paint += go.OnUpdate; this.target.KeyDown += go.OnKeyDown; this.target.KeyUp += go.OnKeyUp; } // 將游戲對象從集合中移除並移除相應的組件事件 public void RemoveGameObject(GameObject go) { GameObjects.Remove(go); // 為游戲場景窗體移除相應的事件 this.target.Paint -= go.OnUpdate; this.target.KeyDown -= go.OnKeyDown; this.target.KeyUp -= go.OnKeyUp; } }
3.4 初始化游戲
(1)創建一個游戲場景對象,傳入主窗體實例與FPS幀率;
(2)創建一個GameObject作為游戲背景對象(GameObject最初都是空對象),然后加入BackgroundBehavior組件,最后加入游戲場景的GameObjects集合中。
(3)創建一個GameObject作為玩家對象,設置其Position與Scale,並為其加入UserControl組件與SpriteRender組件,最后加入游戲場景的GameObjects集合中。
protected override void OnLoad(EventArgs e) { base.OnLoad(e); // Step1.創建一個游戲場景 this.GameScene = new GameScene(this, FPS); // Step2.創建游戲背景對象(空對象) var background = new GameObject(); // U3D精妙之處:為空對象添加背景組件即變成了游戲背景對象 background.AddComponent<BackgroundBehavior>(); // 將游戲背景添加到游戲場景中的集合中 this.GameScene.AddGameObject(background); // Step3.創建游戲玩家對象(空對象) var player = new GameObject(); player.Transform.Position = new Position(0,-200); player.Transform.Scale = new Scale(0.15, 0.15); player.AddComponent<CrazyCar.Component.UserControl>(); var render = player.AddComponent<SpriteRender>(); // 設置渲染組件要顯示的圖片 render.Source = Resources.Player1; this.GameScene.AddGameObject(player); }
最終的運行效果如下圖所示:
這里一個簡單的賽車游戲場景就實現完畢,雖然這樣一個場景十分簡單,但是通過將面向對象思想與Unity3D中的組件化思想結合起來,我們發現實現一個游戲會很麻煩。但是,Unity3D正是幫我們做了這樣的基礎工作,所以才有了我們可以方便的拖拽組件的便利,在擴展性方面展現了很好的威力。
附件下載
CrazyCar v0.2 : http://pan.baidu.com/s/1o61MDv0
參考資料
(1)趙劍宇,《借助Unity思想開發C#版賽車游戲》
(2)騰雲駕霧,《Unity3D之GameObject與Component的聯系》
(3)飯后溫柔,《Unity3D筆記二:基於組件的設計》