标签:
最近花了几天时间梳理了一下新游戏的客户端框架,虽然本身就有相对明确的方向,但是在一开始写的时候还是有些混乱,不过最终梳理完成后,个人感觉代码清爽很多。
这篇文章不是设计模式的教学,而是自己的一些想法和实践,我把代码梳理成自己喜欢的结构,保证逻辑和结构的清晰,但是这并不意味者它是符合所有人习惯的。
我之前有写过一两篇文章讨论客户端的结构,也吐槽过一些其他人的设计。可以说我在写代码之初就有一个相对明确的方向,多年的经验也可以告诉我什么样的代码是漂亮的,什么样的代码是有坏味道的。
首先我把客户端结构分成三层,一层为显示层Display,它处理游戏最核心的战斗模块,如ARPG中的场景、角色、技能表现都是在这一层处理。一层为逻辑层Logic,它处理游戏的各个系统,如资源管理、角色管理、任务管理等等都是放在这一层处理。一层为UI层,它就是由各个界面所组成的。
各个层之间没有严格的划分,比如逻辑层内是各个单体管理器,显示层也会有一个如Game或者BattleScene这样的单体类,UI层会有一个UIManager来管理。但是如果各层相互穿插可能造成客户端的混乱。所以做了一些界限的划分。UI层可以调用逻辑层和显示层的东西,各个界面之间不运行相互调用,而是通过事件来交互。逻辑层几乎只处理逻辑,不调用另外两个层的东西。 显示层可以调用逻辑层的管理者来获取数据或者与服务器通信。
以上为大的框架或者思想,这个并不是很多人会提到的MVC框架,而是根据开发经验总结出来的一套类似的东西。反正思想是一致的,那就是通过合理的层次划分达到逻辑和显示的分离,当一个地方修改的时候,不会影响到其他地方。这个就是“开闭原则”。虽然面向对象的一些原则非常教条,但是确实是代码设计的指导方向。
在框架确定的基础上,还有一些细节的设计需要考量一下,基本上下面提到的一些问题都是各有优劣,所以我只作描述,不做选择,具体看个人的习惯。
1、是要一个全局公共的DataCenter来保存数据,还是将数据分散到各个管理器内部? 使用公共的DataCenter来保存数据,可以避免各个管理器之间的相互访问,所有管理器都是从DataCenter来取数据,管理器之间通过事件来交互。但是不可避免的DataCenter会随着时间的增长变得越来越庞大。 而管理器持有数据,则可能会造成各个管理者之间的相互访问。如玩家要卸下身上装备的武器,就要先从物品或者背包管理器里面获取背包剩余空间,这样两个管理器就相互依赖了。
2、消息是每个消息一个文件来处理其自身逻辑,还是注册一个id给对应的管理者来处理消息数据?
3、是否要保持逻辑管理者之间的独立性,以保证去掉某一个管理者,游戏仍然可以编译并运行?
在上面这些结构问题梳理清楚后,就开始游戏最核心的角色和战斗系统的设计了。这里体现了开发者抽象的能力,如果抽象能力很好,就可以一步到位,否则就要看重构的能力了。
我的设计如下(请原谅我不会画uml图,就纯口述了。。。):
角色是一个继承自MonoBehaviour的类,基类为Unit(也可以叫Actor或者Role),其下会有一些列的派生类如Player NPC Hero Monster等等。 这些是最基本的角色。
每个角色会持有一个data数据,如PlayerData NPCData HeroData等等,这些是逻辑层的东西,是纯数据处理的,根据服务器的消息创建。如玩家的id,血量,攻击力等等都是在这里处理。
Unit内部会管理一些列的Component,它们描述了某件事情该怎么做,比如会有MoveComponent AttackComponent AnimationComponent HUDComponent等等,这些Component会派生出一些列的子类,来具体实现一个行为,如AttackComponent可以有MeleeAttackComponent RangeAttackComponet等等。 Player和Monster可以通过这些Component组合成自己想要实现的具体效果。而当实现需要修改的时候可以只关注Component内部,添加一个新的行为也仅仅是添加一个新的Component。同时Component是可以共享的,如Player和Hero很多Component是一样的,但是HUDComponet不一样,这个就是组合比继承优越的地方。 这个设计模式就是策略模式。它通过封装易变的算法,达到从容应对变化的效果。
与策略模式非常类似的是状态模式。有一些时候它们之间的界限也并不是非常明确,某些套路既可以说成是一种策略,也可以说成是一种状态。比如搜寻敌人,这个可以是一个行为,也可以是一个搜寻敌人的状态(可以考虑分层状态机,这样就可以一边跑一边搜寻敌人,状态之间可以并行)。
我这边的处理是每个角色类会对应一些列的状态实现,如Player有PlayerIdleState PlayerMoveState PlayerAttackState,而Monster有MonsterIdleState MonsterMoveState,State可以有公共的基类如UnitMoveState,也可以没有,这个就看Player和Monster之间状态的相似度有多高,如果有大量重复的代码的话,那么就考虑有一个基类。
状态模式描述了一种状态之间的流转,比如进入Idle状态应该做什么,什么时候要进入Move状态,通过状态划分,可以保证角色在某一个时刻处于一个确定的状态,而此状态的逻辑处理都在该状态类里面完成。这样就不会有各种混乱(比如在Update内部写各种IsInState(xxxx)),达到解耦合的目的。同时状态之间的流转也可以清晰的表达出来,我们的逻辑也会清晰很多。状态模式的具体实现就是有限状态机,它是实现AI非常重要的手段。
总结一下我的实现就是 角色持有一些列的行为,以及一些列的状态,通过行为描述具体做什么,通过状态描述怎么做。这样就可以把原本都写在Player内部的代码分散出来。而这个分散并不会使我们写代码更加麻烦,而是会让我们的逻辑更加清晰,代码更加容易阅读。 如果没有达到这样的效果,而是感觉如果这些代码都写在Player内部可以更加方便的调用,那么就一定是某个环节出了问题。
我个人并不是很推崇设计模式,总是标榜自己是实用主义,但是回过头来看的话,我们认为漂亮的设计很多时候就是一种模式。几十个设计模式我们可能只熟悉其中最常用的几个就受用无穷了。真正在开发中吃透这几个模式,才可以真正了解面向对象、封装,也才会了解什么是漂亮的代码,什么是优雅的设计。
Unity客户端框架笔记(状态模式和策略模式在游戏中的应用)
标签:
原文地址:http://blog.csdn.net/langresser_king/article/details/45791949