一、關於飛機大戰
要說微信中最火爆的小游戲是哪款,可能既不是精心打造的3D大作,也不是《植物大戰僵屍2》,而是微信5.0剛開啟時的《飛機大戰》。
就是這樣一款鉛筆手繪風格的簡單到不能再簡單的“打飛機”游戲,讓國內的微信用戶一次又一次地嘗試,並表示似乎又找回了童年時玩電子游戲的那份單純的快樂。至於游戲的玩法都不用加以介紹,就是簡單的“打飛機”。
二、關於游戲設計
2.1 總結游戲印象
(1)一個玩家飛機,多個電腦飛機
① ②
③
④
(2)玩家飛機可以發射子彈,電腦飛機也可以發射子彈
① ②
(3)玩家和電腦飛機被擊中后有爆炸效果,並且有一定幾率出現大型飛機
① ②
2.2 總結設計思路
(1)萬物皆對象
在整個游戲中,我們看到的所有內容,我們都可以理解為游戲對象(GameObject),每一個游戲對象,都由一個單獨的類來創建;在游戲中主要有三類游戲對象:一是飛機,二是子彈,三是背景;其中,飛機又分為玩家飛機和電腦飛機,子彈又分為玩家子彈和電腦子彈。於是,我們可以對飛機進行抽象形成一個抽象父類:PlaneBase,然后分別創建兩個子類:PlanePlayer和PlaneEnemy;然后對子彈進行抽象形成一個抽象類:BulletBase,然后分別創建兩個子類:BulletPlayer和BulletEnemy。但是,我們發現這些游戲對象都有一些共同的屬性和方法,例如X,Y軸坐標,長度和寬度,以及繪制(Draw())和移動(Move())的方法,這時我們可以設計一個抽象類,形成了GameObject類:將共有的東西封裝起來,減少開發時的冗余代碼,提高程序的可擴展性,符合面向對象設計的思路:
(2)計划生育好
在整個游戲中,我們的玩家飛機對象只有一個,也就是說在內存中只需要存一份即可。這時,我們想到了偉大的計划生育政策,於是我們想到了使用單例模式。借助單例模式,可以保證只生成一個玩家飛機的實例,即為程序提供一個全局訪問點,避免重復創建浪費不必要的內存。當然,除了玩家飛機外,我們的電腦飛機集合、子彈集合等集合對象實例也保證只有一份存儲,降低游戲開銷;
(3)對象的運動
在整個游戲過程中,玩家可以通過鍵盤上下左右鍵控制玩家飛機的上下左右運動,而飛機的運動本質上還是改變游戲對象的X軸和Y軸的坐標,然后一直不間斷地在窗體上重繪游戲對象。相比玩家飛機的移動,電腦飛機的移動則完全是通過程序中設置的隨機函數控制左右方向移動的,而玩家飛機發出的子彈執行的運動則是從下到上,而電腦飛機發出的子彈執行的運動則是從上到下。
(4)設計流程圖
三、關鍵代碼實現
3.1 客戶端開發
(1)設計GameObject類:封裝所有游戲對象的公有屬性

/// <summary> /// 抽象類:游戲對象基類 /// </summary> public abstract class GameObject { public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public int Speed { get; set; } public int Life { get; set; } public Direction Dir { get; set; } public GameObject(int x, int y, int width, int height, int speed, int life, Direction dir) { this.X = x; this.Y = y; this.Width = width; this.Height = height; this.Speed = speed; this.Life = life; this.Dir = dir; } public GameObject(int x, int y) { this.X = x; this.Y = y; } // 實例方法:返回所在矩形區域用於碰撞檢測 public Rectangle GetRectangle() { return new Rectangle(this.X, this.Y, this.Width, this.Height); } // 抽象方法:游戲對象的繪制各不相同 public abstract void Draw(Graphics g); // 虛方法:游戲對象的移動各不相同 public virtual void Move() { // 根據指定的移動方向進行移動 switch (Dir) { case Direction.Up: this.Y -= this.Speed; break; case Direction.Down: this.Y += this.Speed; break; case Direction.Left: this.X -= this.Speed; break; case Direction.Right: this.X += this.Speed; break; } // 移動之后判斷是否超出了邊界 if (this.X <= 0) { this.X = 0; } if (this.X >= 380) { this.X = 380; } if (this.Y <= 0) { this.Y = 0; } if (this.Y >= 670) { this.Y = 670; } } }
一切皆對象,這里封裝了游戲對象:飛機、子彈以及其他游戲對象共有的屬性,以及兩個抽象方法,讓對象們(飛機?子彈?爆炸效果?等)自己去實現。
(2)設計SingleObject類:保證游戲中的類都只有一個實例

/// <summary> /// 單例模式類 /// </summary> public class SingleObject { private SingleObject() { } private static SingleObject singleInstance = null; public static SingleObject GetInstance() { if (singleInstance == null) { singleInstance = new SingleObject(); } return singleInstance; } #region 單一實例對象列表 // 1.游戲背景單一實例 public GameBackground Background { get; set; } // 2.游戲標題單一實例 public GameTitle Title { get; set; } // 3.玩家飛機單一實例 public PlanePlayer Player { get; set; } // 4.玩家飛機子彈集合單一實例 public List<BulletPlayer> PlayerBulletList { get; set; } // 5.敵人飛機集合單一實例 public List<PlaneEnemy> EnemyList { get; set; } // 6.敵人飛機子彈集合單一實例 public List<BulletEnemy> EnemyBulletList { get; set; } // 7.玩家飛機爆炸效果單一實例 public List<BoomPlayer> PlayerBoomList { get; set; } // 8.敵人飛機爆炸效果單一實例 public List<BoomEnemy> EnemyBoomList { get; set; } #endregion // 為游戲屏幕增加一個游戲對象 public void AddGameObject(GameObject go) { if (go is GameBackground) { this.Background = go as GameBackground; } if (go is GameTitle) { this.Title = go as GameTitle; } if (go is PlanePlayer) { this.Player = go as PlanePlayer; } if (go is BulletPlayer) { if(this.PlayerBulletList == null) { this.PlayerBulletList = new List<BulletPlayer>(); } this.PlayerBulletList.Add(go as BulletPlayer); } if (go is PlaneEnemy) { if (this.EnemyList == null) { this.EnemyList = new List<PlaneEnemy>(); } this.EnemyList.Add(go as PlaneEnemy); } if(go is BulletEnemy) { if (this.EnemyBulletList == null) { this.EnemyBulletList = new List<BulletEnemy>(); } this.EnemyBulletList.Add(go as BulletEnemy); } if (go is BoomPlayer) { if (this.PlayerBoomList == null) { this.PlayerBoomList = new List<BoomPlayer>(); } this.PlayerBoomList.Add(go as BoomPlayer); } if (go is BoomEnemy) { if (this.EnemyBoomList == null) { this.EnemyBoomList = new List<BoomEnemy>(); } this.EnemyBoomList.Add(go as BoomEnemy); } } // 移除指定的游戲對象 public void RemoveGameObject(GameObject go) { if (go is GameTitle) { this.Title = null; } if (go is BulletPlayer) { this.PlayerBulletList.Remove(go as BulletPlayer); } if (go is PlaneEnemy) { this.EnemyList.Remove(go as PlaneEnemy); } if (go is BulletEnemy) { this.EnemyBulletList.Remove(go as BulletEnemy); } if (go is BoomPlayer) { this.PlayerBoomList.Remove(go as BoomPlayer); } if (go is BoomEnemy) { this.EnemyBoomList.Remove(go as BoomEnemy); } } // 為游戲屏幕繪制游戲背景對象 public void DrawFirstBackground(Graphics g) { if (Background != null) { Background.Draw(g); } if (Title != null) { Title.Draw(g); } if (Player != null) { Player.Draw(g); } } // 為游戲屏幕繪制所有游戲對象 public void DrawGameObjects(Graphics g) { if (Background != null) { Background.Draw(g); } if (Player != null) { Player.Draw(g); } if (PlayerBulletList != null) { for (int i = 0; i < PlayerBulletList.Count; i++) { PlayerBulletList[i].Draw(g); } } if (EnemyList != null) { for (int i = 0; i < EnemyList.Count; i++) { EnemyList[i].Draw(g); } } if(EnemyBulletList != null) { for (int i = 0; i < EnemyBulletList.Count; i++) { EnemyBulletList[i].Draw(g); } } if (PlayerBoomList != null) { for (int i = 0; i < PlayerBoomList.Count; i++) { PlayerBoomList[i].Draw(g); } } if (EnemyBoomList != null) { for (int i = 0; i < EnemyBoomList.Count; i++) { EnemyBoomList[i].Draw(g); } } } // 玩家得分 public int Score { get; set; } }
這里借助單例模式,保證玩家飛機對象只有一個存儲,電腦飛機集合也只有一個,而具體的電腦飛機對象則分別在單例類中的集合中進行Add和Remove。
(3)設計CollisionDetect方法:不停地進行碰撞檢測
①Rectangle的IntersectsWith方法
在游戲界面中,任何一個游戲對象我們都可以視為一個矩形區域(Rectangle類實例),它的坐標是X軸和Y軸,它還有長度和寬度,可以輕松地確定一個它所在的矩形區域。那么,我們可以通過Rectangle的IntersectsWith方法確定兩個Rectangle是否存在重疊,如果有重疊,此方法將返回 true;否則將返回 false。那么,在飛機大戰中主要是判斷兩種情況:一是玩家或電腦飛機發射的子彈是否擊中了對方?二是玩家是否撞到了敵人飛機?
②在定時器事件中定期執行碰撞檢測方法

// 碰撞檢測方法 public void CollisionDetect() { #region 1.判斷玩家的子彈是否打到了敵人飛機身上 for (int i = 0; i < PlayerBulletList.Count; i++) { for (int j = 0; j < EnemyList.Count; j++) { if(PlayerBulletList[i].GetRectangle().IntersectsWith(EnemyList[j].GetRectangle())) { // 1.敵人的生命值減少 EnemyList[j].Life -= PlayerBulletList[i].Power; // 2.生命值減少后判斷敵人是否死亡 EnemyList[j].IsOver(); // 3.玩家子彈打到了敵人身上后將玩家子彈銷毀 PlayerBulletList.Remove(PlayerBulletList[i]); break; } } } #endregion #region 2.判斷敵人的子彈是否打到了玩家飛機身上 for (int i = 0; i < EnemyBulletList.Count; i++) { if(EnemyBulletList[i].GetRectangle().IntersectsWith(Player.GetRectangle())) { // 使玩家發生一次爆炸但不陣亡 Player.IsOver(); break; } } #endregion #region 3.判斷敵人飛機是否和玩家飛機相撞 for (int i = 0; i < EnemyList.Count; i++) { if (EnemyList[i].GetRectangle().IntersectsWith(Player.GetRectangle())) { EnemyList[i].Life = 0; EnemyList[i].IsOver(); break; } } #endregion }
3.2 服務端開發
(1)創建監聽玩家連接的Socket,不停地監聽玩家的游戲連接請求

private void btnBeginListen_Click(object sender, EventArgs e) { if (isEndService) { SetTxtReadOnly(); if (socketWatch == null) { // 創建Socket->綁定IP與端口->設置監聽隊列的長度->開啟監聽連接 socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socketWatch.Bind(new IPEndPoint(IPAddress.Parse(txtIPAddress.Text), int.Parse(txtPort.Text))); socketWatch.Listen(10); threadWatch = new Thread(ListenClientConnect); threadWatch.IsBackground = true; threadWatch.Start(socketWatch); } isEndService = false; this.btnStartGame.Enabled = true; ShowMessage("^_^:飛機大戰服務器端啟動服務成功,正在等待玩家進入游戲..."); } else { MessageBox.Show("服務已啟動,請不要重復啟動服務!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void ListenClientConnect(object obj) { Socket serverSocket = obj as Socket; while (!isEndService) { Socket proxSocket = null; try { // 注意:Accept方法會阻斷當前所在的線程 proxSocket = serverSocket.Accept(); dictClients.Add(proxSocket.RemoteEndPoint.ToString(), proxSocket); ShowMessage("*_*:玩家<" + proxSocket.RemoteEndPoint.ToString() + ">連接上了,請准備開始游戲。"); playerCount++; ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData), proxSocket); } catch (SocketException ex) { ShowMessage("#_#:異常【" + ex.Message + "】"); // 讓方法結束,終結當前監聽客戶端數據的異步線程 return; } catch (Exception ex) { ShowMessage("#_#:異常【" + ex.Message + "】"); // 讓方法結束,終結當前監聽客戶端數據的異步線程 return; } } }
在.NET中進行網絡編程,一般都會涉及到Socket,其過程大概會經歷如下圖所示的流程:
PS:Socket非常類似於電話插座,以一個電話網為例:電話的通話雙方相當於相互通信的2個程序,電話號碼就是IP地址。任何用戶在通話之前,首先要占有一部電話機,相當於申請一個Socket;同時要知道對方的號碼,相當於對方有一個固定的Socket。然后向對方撥號呼叫,相當於發出連接請求。對方假如在場並空閑,拿起電話話筒,雙方就可以正式通話,相當於連接成功。雙方通話的過程,是一方向電話機發出信號和對方從電話機接收信號的過程,相當於向Socket發送數據和從Socket接收數據。通話結束后,一方掛起電話機相當於關閉socket,撤消連接。
(2)使用線程池ThreadPool新開線程,不停地接收玩家發送的信息
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData), proxSocket);
在監聽線程中使用了線程池,開啟了一個新的線程來接收客戶端發送過來的數據,那么這個ReceiveData方法如何實現的:

private void ReceiveData(object obj) { Socket proxSocket = obj as Socket; byte[] data = new byte[1024 * 1024]; int length = 0; while (!isEndService) { try { length = proxSocket.Receive(data); } catch (SocketException ex) { ShowMessage("#_#:異常【" + ex.Message + "】"); StopConnection(proxSocket); // 讓方法結束,終結當前接收客戶端數據的異步線程 return; } catch (Exception ex) { ShowMessage("#_#:異常【" + ex.Message + "】"); StopConnection(proxSocket); // 讓方法結束,終結當前接收客戶端數據的異步線程 return; } if (length <= 0) { ShowMessage("*_*:玩家<" + proxSocket.RemoteEndPoint.ToString() + ">退出了游戲"); StopConnection(proxSocket); if (playerCount > 0) { playerCount--; } // 讓方法結束,終結當前接收客戶端數據的異步線程 return; } else { // 接受客戶端發送過來的消息 string playerScore = Encoding.UTF8.GetString(data, 0, length); dictScores.Add(proxSocket.RemoteEndPoint.ToString(), Convert.ToInt32(playerScore)); if (dictScores.Count > 0 && dictScores.Count == playerCount) { ComparePlayerScores(); } } } }
(3)當所有玩家都發送完游戲分數,服務器端對所有分數進行排序並發送最終名次

private void ComparePlayerScores() { List<KeyValuePair<string, int>> scoreList = dictScores.OrderByDescending(s => s.Value).ToList(); for (int i = 0; i < scoreList.Count; i++) { string result = string.Format("您本次的成績是第{0}名,分數為{1}分", i + 1, scoreList[i].Value); byte[] bytes = Encoding.UTF8.GetBytes(result); byte[] data = new byte[bytes.Length + 1]; data[0] = 2; Buffer.BlockCopy(bytes, 0, data, 1, bytes.Length); dictClients[scoreList[i].Key].Send(data, 0, data.Length, SocketFlags.None); } }
在服務端有一個鍵值對集合專門存儲玩家對應分數,然后對其按分數進行降序排序,排序后再遍歷集合一一向玩家發送名次信息;
四、個人開發小結
4.1 服務端開啟服務
服務器端主要開啟監聽玩家連接請求的服務,當幾個處在同一局域網的玩家連接后,服務端管理員點擊“開始游戲”則客戶端會啟動游戲。
4.2 客戶端開始游戲
在客戶端中,玩家飛機可以通過不停地發射子彈向不同類型的電腦飛機來獲取得分,但是如果被敵人飛機的子彈擊中分數也會被扣去一部分。
4.3 服務端計算成績客戶端顯示
當兩個玩家連接游戲服務端后,便開始了“打飛機”的戰斗,當指定時間后游戲結束,顯示各自的游戲名次和分數。
當然,還有很多核心的內容沒有實現。希望有興趣的童鞋可以去繼續完善實現,這里提供一個我的飛機大戰實現僅供參考,謝謝!
參考資料
趙劍宇,《C#開發太空大戰》:http://open.itcast.cn/net/3-106.html
附件下載
MyPlaneGame:https://github.com/EdisonChou/The-Fighting-of-Planes