局域網多人對戰飛行棋的實現


在項目之間有段“空項期”,上個項目剛剛完成,下個項目還沒落實,時間比較充裕。去年9月份就經歷了這么一次短暫的“空項期”,那時偶還是一名前端工作者,C#使用起來毫不含糊,還自己整過一個類SCSF的MVP框架AngelFrame(詳見之前博客:http://www.cnblogs.com/wgp13x/p/99c2adc52d8f0dff30a038841ac32872.html)。在那段“空項期”之前,有位朋友托我做個小游戲,偶也滿口的答應,只可惜之前項目太忙沒時間做,就一直耽擱了,正好有這段“空項期”,所以做了一下,現在回想起來,做這個小游戲的過程中還是學習到了不少東西的,因為做游戲跟做項目的常用技術不同,於是在這里總結一下,別把這么寶貴的經驗給弄丟了。

 
這個小游戲的需求很簡單,就是在局域網的環境里能夠自組織一個飛行棋平台,多個玩家在里面你一步我一步的玩,看誰先飛完全程。在做這個游戲之前,偶連什么是飛行棋,飛行棋怎么玩的都不懂,就先在網上試玩了一小把,查了查飛行棋的規則。會玩兒了,就要想想怎么做了,規則實現肯定要有,用戶交互是個問題。在網上搜索了一下現成的C#飛行棋實現例子,發現不是給自家女兒做的就是給自家兒子做的,單機版的,手機版的,就是沒有局域網版的。好吧,只有看我來創造了。
 
關鍵詞:飛行棋, C#, 局域網, 多人對戰
摘要:很久之前就有個朋友 托我做個游戲了,這個游戲 的需求很簡單,就是在局域網的環境里能夠自組織一個飛行棋平台,多個玩家在里面你一步我一步的玩 。游戲規則有了,實現的難點在於自建多人對戰平台, C#飛行棋實現例子有很多,可就是沒有局域網版的,下面就是我抽出時間寫的一個 局域網多人對戰飛行棋,在這里總結一下。

 
先看一下我做的游戲的運行界面,網上有個單機版本的飛行棋,我借鑒了它的界面及游戲邏輯,由於它是開源的,也不知道源代碼提供者是誰,這里就不做相關鏈接了。
解釋一下,打開主界面就是上面這個樣子,如果局域網內同時在線的游戲客戶端多,那么在當前在線列表中會顯示同時在線的客戶端IP和用戶名,這時你先選擇要進行游戲的客戶端,再點擊創建游戲,就可以開創一輪新游戲了,游戲者在兩到四之間。如果你要添加跨網段的客戶端,那就要點擊邀請好友按鈕,填寫IP,如果它們在線就會添加到當前在線列表中,這時你再選擇它們,點擊創建游戲,就能夠在開創的新游戲里 他們玩 了。
創建游戲成功后,在左下角處會出現一個色子,游戲面板上會出現各類顏色的飛機,每個游戲者對應一類 顏色。由一個游戲者先擲色子, 擲到6后才能夠起飛,其它的游戲者輪循着來,每個游戲者的動作都能夠被其它游戲者收到,在下方文字欄中,會作出說明。
 
網絡版的游戲,要做只能做成一個Server和多個Client,每個Client的每一步都要通知Server,由Server來運行游戲邏輯,指導Client的運行流程;或只是多個Client,每個Client都要維護其它的Client信息,Client的運行流程都要通知其它所有的Client,每個Client都運行游戲邏輯。我這里陰差陽錯的選擇了第二種,每個Client都是平等的,沒有Server。通知用的是UDP,局域網的用戶交互就全靠它了,下面是網絡通知相關的代碼,使用單例模式,它是局域網游戲運行起來的核。
 
public class Network
    {
        private static Network _instace;
        private const int Port = 100;
        public static UdpClient UdpClient;
        private static  Encoding encoding = Encoding.GetEncoding("gb2312");
        public static string HostName;
        public static IPAddress IpAddress;
        private static Thread listener;
 
        public event EventHandler<GameMsg> ReceivedMsg;
        
        public static Network Instance
        {
            get
            {
                if (_instace == null
                    _instace = new Network();
                return _instace;
            }
        }
 
        private Network()
        {
            UdpClient = new UdpClient(Port);
            HostName = Dns.GetHostName();
            foreach (IPAddress ip in Dns.GetHostAddresses(HostName))
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    IpAddress = ip;
                    break;
                }
            }
        }
       
        //局域網內廣播,在上線時調用,通知其它Client有人上線了
        public void Broadcast(GameMsg msg)
        {
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Broadcast, Port);
            byte[] bytes = Tools.Serialize(msg);
            UdpClient.Send(bytes, bytes.Length, ipEndPoint);
        }
 
        //接收消息線程入口,收到消息后觸發各類事件
        public void ReceiveMsg()
        {
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Broadcast, Port);
            while (true)
            {
                byte[] bytes = null;
                try
                {
                    bytes = UdpClient.Receive(ref ipEndPoint);
                }
                catch (SocketException ex)
                {
                    return;
                }
                GameMsg msg = (GameMsg)Tools.Deserialize(bytes);
                if (ReceivedMsg != null && !msg.IpAddress.Equals(IpAddress))
                    ReceivedMsg(this, msg);
            }
        }
 
        //向某IP定向發送消息
        public void Send(IPAddress ip, GameMsg msg)
        {
            IPEndPoint ipEndPoint = new IPEndPoint(ip, Port);
            byte[] bytes = Tools.Serialize(msg);
            UdpClient.Send(bytes, bytes.Length, ipEndPoint);
        }
 
        //向一些IP定向發送消息
        public void Send(IPAddress[] ips, GameMsg msg)
        {
            foreach (IPAddress ipAddress in ips)
            {
                if (!ipAddress.Equals(IpAddress))
                    Send(ipAddress, msg);
            }
        }
    }
下面是游戲中, Client之間 需要交互的一些消息體定義。 GameMsg 是消息體,里面包含消息類型,IP地址,消息內容,消息內容有可能是 LandGameMsg 對象,也有可能是 CreateGameMsg 對象...
[Serializable]
    public class GameMsg : EventArgs
    {
        public MsgTypeEnum MsgType;   
        public IPAddress IpAddress;
        public object MsgContent;
    }
 
    [Serializable]
    public class LandGameMsg    //OnlineReply, Hello
    {
        public string HostName;
        public string UserName;
    }
 
    [Serializable]
    public class CreateGameMsg
    {
        public string CreaterHostName;
        public string CreaterUserName;
        public IPAddress[] SelectedIpAddresses;
        public string[] SelectedHostNames;
    }
......
在主界面中,有些對接收到的消息共同的處理邏輯,在這里也列出來給大家看一下吧。
public partial class MainForm : Form
    {
        public static Thread Listener = new Thread(new ThreadStart(Network.Instance.ReceiveMsg)) { Name = "receiveMsg", Priority = ThreadPriority.Highest };
        public static CurrStatEnum CurrStat = CurrStatEnum.Idle;
        private static readonly MsgTypeEnum[] interestMsgTypes = new MsgTypeEnum[] { MsgTypeEnum.QuitGame };
 
            public  MainForm()
        {
            Listener.Start();
            Network.Instance.ReceivedMsg += new EventHandler<GameMsg>(_network_ReceivedMsg);
            Network.Instance.Broadcast(new GameMsg() { MsgType = MsgTypeEnum.LandGame, IpAddress = Network.IpAddress, MsgContent = new LandGameMsg() { HostName = Network.HostName } });
        }
 
            private   delegate   void  Delegate_ReceivedMsg(GameMsg msg);
        void _network_ReceivedMsg(object sender, GameMsg e)
        {
            Delegate_ReceivedMsg myDelegate = new Delegate_ReceivedMsg(handleReceivedMsg);
            if (interestMsgTypes.Contains(e.MsgType))
                Invoke(myDelegate, e);
        }
 
        void handleReceivedMsg(GameMsg msg)
        {
            switch (msg.MsgType)
            {
                case MsgTypeEnum.QuitGame:    //收到某人退出游戲請求
                    MessageBox.Show("有小伙伴要求退出游戲");
                    ucUsersInGame_QuitGame(nullnull);
                    break;
            }
        }
}
 
如上所示,在主界面中主要是對用戶退出游戲請求做出一些邏輯處理,這里的線程監控網絡發來的所有消息,但只對退出游戲請求感興趣,提示用戶,本局游戲結束。
在其它界面也是類似的過程,下面再列出邀請好友界面里的,對其它游戲者的反饋信息做出邏輯處理的代碼段。
public partial class InviteOthers : Form
    {
        private static readonly MsgTypeEnum[] interestMsgTypes = new MsgTypeEnum[] { MsgTypeEnum.OnlineReply };
        public List<GameMsg> OnlineReplys = new List<GameMsg>();
 
         void  _network_ReceivedMsg( object  sender, GameMsg msg)
        {
            if (interestMsgTypes.Contains(msg.MsgType))
            {
                switch (msg.MsgType)
                {
                    case MsgTypeEnum.OnlineReply:
                         OnlineReplys.Add(msg);
                        break;
                }
            }
        }
  }
游戲引擎,游戲邏輯處理在這里就不多列了,那是在單機飛行棋里的實現了的。
游戲的基本功能實現了后,還實現了一些添喜的功能,比如隱藏到桌面上方、下方、旁邊,就是你可以把游戲窗口拖動到屏幕的最上方,然后松鼠標,游戲會縮到屏幕上方,當你再把鼠標移動到屏幕上方時,它還會出來,就跟掛QQ的功能一樣。它是這樣實現的,在主界面中添加3個Windows.Forms.Timer,timer1的Enabled=True,Interval=100;timer2、timer3的Enabled=False,Interval=1,再各自添加如下事件處理邏輯。
        /// 監控鼠標和窗口位置
        private void timer1_Tick(object sender, EventArgs e)
        {
            int mouse_x = Cursor.Position.X, mouse_y = Cursor.Position.Y;
            int window_x = this.Location.X, window_y = this.Location.Y;
            int window_width = this.Size.Width, window_height = this.Size.Height;
            if (isHiding == false && window_y == 0)
            {
                if (window_x - mouse_x > 10 || mouse_x - window_x - window_width > 10
                    || mouse_y - window_y - window_height > 10)
                {
                    timer1.Enabled = false;
                    timer2.Enabled = true;
                }
            }
            if (isHiding == true && mouse_y <= 1 && mouse_x > window_x &&
                mouse_x < window_x + window_width)
            {
                timer1.Enabled = false;
                timer3.Enabled = true;
            }
        }
        /// 隱藏界面
        private void timer2_Tick(object sender, EventArgs e)
        {
            int window_height = this.Size.Height;
            startY += window_height / 8;
            if (startY < window_height)
            {
                this.Location = new Point(this.Location.X, -startY);
            }
            else
            {
                this.Location = new Point(this.Location.X, 1 - window_height);
                isHiding = true;
                timer2.Enabled = false;
                timer1.Enabled = true;
            }
        }
        /// 顯示界面
        private void timer3_Tick(object sender, EventArgs e)
        {
            int window_height = this.Size.Height;
            startY -= window_height / 8;
            if (startY > 0)
            {
                this.Location = new Point(this.Location.X, -startY);
            }
            else
            {
                this.Location = new Point(this.Location.X, 0);
                isHiding = false;
                timer3.Enabled = false;
                timer1.Enabled = true;
            }
        }
 
就這樣把局域網多人對戰飛行棋給實現了,回看這次的編程設計經歷,覺得這種P2P式的,無Server式的游戲設計是個問題。這種設計影響到游戲編碼方式,使得游戲編碼雜亂無章,沒有一個主心骨來對游戲步驟進行統一管理,這樣如若數據在網絡中丟失,很容易導致多客戶端不同步的現象。應該在選取完游戲伙伴創建新游戲時,自主選擇一個Server來處理游戲邏輯,這樣,8個人、10個人、更多的人同時在線都可以自組織游戲平台了,不同步也可以避免了。
 
其它的一些收獲:
1、System.Windows.Forms.Application.DoEvents();可以督促主線程處理當前在消息隊列中的所有Windows消息。
2、朋友機器上的操作系統是WinXP,自帶的沒有.net4.0,自帶.net3.0,剛開始運行不起來,后來全部換到 .net3.0調試、改代碼,才運行得起來。
3、做完后,在家里跟老婆兩個人打對戰,玩了一晚上這個游戲都不累,還挺好玩的。
好久沒來博客園更新博客了,最近新項目來了。新項目做完,又有好多新知識可以總結嘍!
 
 






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM