斗地主代码分析】(1)"/>
【斗地主代码分析】(1)
前言
这是个斗地主游戏,觉得这个Demo的框架挺不错,通俗易懂,本篇主要分析一下这个框架。
这个框架通过消息中心来调度所有功能,想个大号的中介模式,关于中介模式可以看看这篇博客:中介模式
源码地址:斗地主Demo
总览
先看一下这个框架的结构,如下图。
可以说这个框架就这么点东西,就这三个结构。
MsgCenter
是消息中心,根据传参调用对应的ManagerBase
,而它又会根据参数执行对应的MonoBase
。
调用链:
MsgCenter.Dispatch(int areaCode, int eventCode, object message)|v
ManagerBase.Execute(int eventCode, object message)|v
MonoBase.Execute(int eventCode, object message)
消息中心
我们来自上而下看看这个调用链,先看MsgCenter
的实现。
public class MsgCenter : MonoBase
{public static MsgCenter Instance = null;void Awake(){Instance = this;gameObject.AddComponent<AudioManager>();gameObject.AddComponent<UIManager>();gameObject.AddComponent<NetManager>();gameObject.AddComponent<CharacterManager>();gameObject.AddComponent<SceneManagers>();DontDestroyOnLoad(gameObject);}/// <summary>/// 发送消息 系统里面所有的发消息 都通过这个方法来发/// 怎么转发?根据不同的模块 来发给 不同的模块 /// 怎么识别模块呢?通过 areaCode/// /// 第二个参数:事件码 作用?用来区分 做什么事情的/// 比如说 第一个参数 识别到是角色模块 但是角色模块有很多功能 比如 移动 攻击 死亡 逃跑.../// 就需要第二个参数 来识别 具体是做哪一个动作/// </summary>public void Dispatch(int areaCode, int eventCode, object message){switch (areaCode){case AreaCode.AUDIO:AudioManager.Instance.Execute(eventCode, message);break;case AreaCode.CHARACTER:CharacterManager.Instance.Execute(eventCode, message);break;case AreaCode.NET:NetManager.Instance.Execute(eventCode, message);break;case AreaCode.GAME:break;case AreaCode.UI:UIManager.Instance.Execute(eventCode, message);break;case AreaCode.SCENE:SceneManagers.Instance.Execute(eventCode, message);break;default:break;}}
}
在Awake
时把实现的ManagerBase
加进来,之后如果想调用某个功能,就调用MsgCenter.Dispatch(areaCode, eventCode, message)
。
通过这三个参数来区分所有功能,areaCode
区分这个功能所在的模块,eventCode
是对功能在模块内的具体区分,message
是这个事件需要的参数。
Dispatch
根据areaCode
参数调用对应的模块的Execute
方法,接下来看看它是怎么实现的。
模块管理
/// <summary>
/// 每个模块的基类
/// 1。保存自身注册的一系列消息
/// </summary>
public class ManagerBase : MonoBase
{/// <summary>/// 处理自身的消息/// </summary>/// <param name="eventCode">Event code.</param>/// <param name="message">Message.</param>public override void Execute(int eventCode, object message){if (!dict.ContainsKey(eventCode)){Debug.LogWarning("没有注册 : " + eventCode);return;}//一旦注册过这个消息 给所有的脚本 发过去List<MonoBase> list = dict[eventCode];for (int i = 0; i < list.Count; i++){list[i].Execute(eventCode, message);}}/// <summary>/// 存储 消息的事件码 和 哪个脚本 关联 的字典/// /// 角色模块 有一个动作是 移动/// 移动模块需要关心这个事件 控制角色位置 进行移动/// 动画模块 也需要关心 控制角色播放动画 /// 音效模块 也需要关心 控制角色移动的音效播放 走路声/// </summary>private Dictionary<int,List<MonoBase>> dict = new Dictionary<int, List<MonoBase>>();...
}
管理类有有一个成员变量dict
,它存的是一个map类型:事件id->执行动作列表。
再看它的Execute
方法,找到eventCode
对应的执行动作列表,依次执行。也就是说,这个事件id可能对应多个执行动作,也可能多个事件id对应多个执行动作。
一个执行动作,就是一个MonoBase
,这个比较简单,直接贴到下面。
/// <summary>
/// 为什么写着脚本?
/// 我们想扩展 MonoBehaviour
/// </summary>
public class MonoBase : MonoBehaviour
{/// <summary>/// 定义一个虚方法/// </summary>public virtual void Execute(int eventCode, object message){}
}
行了,整个框架就是这么三层,实现对应层的功能,由MsgCenter
统一调用,调到对应模块,再由模块调到具体功能。直接看这个框架有点儿抽象,接下来看看这个斗地主Demo是如何使用这个框架的,就能理解了。
登录功能的实现
我们通过登录功能的实现,来看这个框架如何使用。
首先分析一下登录功能的流程:
- 输入账号密码,点击登录。
- 去服务器校验,校验通过的话跳转到游戏界面。
- 校验失败的话提示。
一个简单的登录功能就这三步,来看这个Demo。
首先找到登录按钮的点击处理方法,在StartPanel.cs
里。
void OnLoginClick(){if (string.IsNullOrEmpty(inputAccount.text)){promptMsg.Text = "用户名不能为空!";Dispatch(AreaCode.UI,UIEvent.Prompt_Msg,promptMsg);return;}if (string.IsNullOrEmpty(inputPassword.text)){promptMsg.Text = "密码不能为空!";Dispatch(AreaCode.UI, UIEvent.Prompt_Msg, promptMsg);return;}if (inputPassword.text.Length < 4 || inputPassword.text.Length > 16){promptMsg.Text = "密码长度错误!应在4~16个字符之间";Dispatch(AreaCode.UI, UIEvent.Prompt_Msg, promptMsg);return;}UserInfoDto userInfo = new UserInfoDto{Account = inputAccount.text,Password = inputPassword.text};//发送数据Dispatch(AreaCode.NET, 0, new SocketMsg{OpCode = MsgType.Account,SubCode = AccountCode.Login,value = userInfo});}
前面进行一堆校验后,构造了userInfo
,这是登录需要的信息。
最后调用了Dispatch()
方法。注意这里的三个参数。
第一个AreaCode.NET
,表示要调用NET 模块的功能,这个模块也是我们自己实现的。
第二个0
,在这个模块里对应的事件id是0,一会儿再看这个0在这个模块里是什么含义。
第三个是新构造的SocketMsg
结构,把上面构造的userInfo
赋值给 value
,它是一个自定义的网络消息格式,如下所示。
//
// 摘要:
// 网络消息
public class SocketMsg
{public SocketMsg();public SocketMsg(Enum state);public SocketMsg(MsgType opCode, Enum subCode, Enum state, object value = null);//// 摘要:// 操作码public MsgType OpCode { get; set; }//// 摘要:// 子操作public Enum SubCode { get; set; }//// 摘要:// 参数public object value { get; set; }//// 摘要:// 状态public Enum State { get; set; }
}
现在看对这个NET模块调用的实现,直接看NetManager.cs
里的Execute
方法,可以看到只处理了0
事件,做的操作就是发送了这个消息。
public override void Execute(int eventCode, object message){switch (eventCode){case 0:client.Send(message as SocketMsg);break;default:break;}}
显而易见,接下来要看网络消息响应的处理了。还是这个文件,可以看到它的Update() 是一直在处理消息的,client
在收到消息后会解析并放入它的socketMsgQueue
队列里。
private void Update(){if (client == null)return;while (client.socketMsgQueue.Count > 0){SocketMsg msg = client.socketMsgQueue.Dequeue();//处理消息ProcessSocketMsg(msg);}}
然后看ProcessSocketMsg
的实现,上面已经设置过网络消息的OpCode
是MsgType.Account
,这里也看对其的处理,调用了accountHandler.OnReceive(msg);
。
HandleBase accountHandler = new AccoutHandle();HandleBase userHandler = new UserHandler();HandleBase matchHandler = new MatchHandler();HandleBase chatHandler = new ChatHandler();HandleBase fightHandler = new FightHandler();/// <summary>/// 接受网络的消息/// </summary>private void ProcessSocketMsg(SocketMsg msg){switch (msg.OpCode){case MsgType.Account:accountHandler.OnReceive(msg);break;case MsgType.User:userHandler.OnReceive(msg);break;case MsgType.Match:matchHandler.OnReceive(msg);break;case MsgType.Chat:chatHandler.OnReceive(msg);break;case MsgType.Fight:fightHandler.OnReceive(msg);break;}}
一步步跟着去看,最后会调到AccoutHandle
的LoginResponse
方法,它的实现如下,逻辑是:如果登录成功,就调用SCENE
模块的UIEvent.Scene
事件。若登录失败,则调用UI
模块的UIEvent.Prompt_Msg
事件。这里很容易猜到,前者是转换到游戏场景,后者是进行登录失败消息提示。
/// <summary>/// 登录请求处理/// </summary>/// <param name="code"></param>private void LoginResponse(AccountCode code){if (code == AccountCode.Success){Dispatch(AreaCode.SCENE, UIEvent.Scene, new LoadSceneMsg{SceneIndex = 1,OnSceneLoaded = () =>{Dispatch(AreaCode.NET, 0, new SocketMsg{OpCode = MsgType.User,SubCode =UserCode.GetInfoRequest});}});return;}switch (code){case AccountCode.AccountDoesNotExist:promptMsg.Text = "账号不存在";break;case AccountCode.AccountOnline:promptMsg.Text = "账号在线";break;case AccountCode.AccountPasswordDoesNotMatch:promptMsg.Text = "账号密码不匹配";break;}Dispatch(AreaCode.UI, UIEvent.Prompt_Msg, promptMsg);}
到这里我想就不需要我再一步步分析了,如果要接着往下分析的话,就是去SCENE
模块看看这个事件对应的执行方法,看怎么实现的。再去UI模块看看这个消息提示是怎么处理的。
总之知道这个框架的调用流程后,代码就不难看懂了,剩下就是看代码逻辑是如何实现的。
事件的绑定
还有一点需要说一下,关于事件是如何绑定的,注意ManagerBase
是继承MonoBase
的,一个事件就是一个MonoBase
。
然后先看一个示例,就看SceneManagers
场景管理,在Start()
中调用了Add
方法,把UIEvent.Scene
事件的处理绑定了this
,也就是说如果有这个事件了,就会调用到SceneManagers
的Execute
方法。
public class SceneManagers : ManagerBase
{public static SceneManagers Instance;/// <summary>/// 加载场景委托/// </summary>private Action onSceneLoadAction;private void Awake(){Instance = this;SceneManager.sceneLoaded += SceneManager_sceneLoaded;}void Start(){Add(UIEvent.Scene, this);}public override void Execute(int eventCode, object message){switch (eventCode){case UIEvent.Scene:LoadSceneMsg msg = message as LoadSceneMsg;LoadScene(msg);break;}}...
}
如果事件比较少,都挺简单的话可以这么写,如果同一个模块事件比较多,那样全都写到一个文件里的话就会显得特别长,Execute 里的switch 也会特别长。
怎么分开呢,实际每个事件都是MonoBase
,只要继承了这个接口,就可以把事件分开,而不用全部挤在一个ManagerBase
里处理,但是还需要和对应的ManagerBase
关联起来。
这里可以看下UIManager
、UIBase
、StartPanel
这三者的实现,看完就能理解如何把模块内的各个功能拆分开了。这里就不再赘述了,如果上面都理解了,这个多看两边就看懂了。
本篇到这里算是全部讲完了,主要分析了这个消息中心模式的框架是如何实现和使用的,既然都看这个Demo了,下一篇就分析一下斗地主的功能逻辑吧。
更多推荐
【斗地主代码分析】(1)
发布评论