• 局域网多人对战飞行棋的实现


    在项目之间有段“空项期”,上个项目刚刚完成,下个项目还没落实,时间比较充裕。去年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、做完后,在家里跟老婆两个人打对战,玩了一晚上这个游戏都不累,还挺好玩的。
    好久没来博客园更新博客了,最近新项目来了。新项目做完,又有好多新知识可以总结喽!
     
     





  • 相关阅读:
    浏览器从输入URL到渲染出页面发生了什么
    按需加载controller——angular
    依赖注入——angular
    qrcode-reader——二维码识别
    Dynamic Web TWAIN——网页扫描SDK
    WebSocket-Node
    关机命令 shutdown
    datagrid——jQuery EasyUI
    双屏显示——NW.js
    css换行用省略号代替
  • 原文地址:https://www.cnblogs.com/wgp13x/p/3800030.html
Copyright © 2020-2023  润新知