易于增加兼容和测试的游戏客户端代码设计方法
一、前言
本文讲的设计方法,不涉及算法、优化、接口讲解等技术介绍。
该设计方法基于MVC设计模式(主要是抽出控制类),而且本文主要面向游戏开发的一些问题。
该设计方法样例由python编写,但是实际上都是伪代码,有一点代码基础的问基本看得懂。
该设计方法由师兄教授,在项目使用过之后,感觉确实不错,特地提取一个方法论出来以记录。
二、MVC简介
在游戏开发中,目前用到架构主要分为MVC和ESC架构(这部分如有异议欢迎指正,有其他架构也希望能提出,博主也可以学习)。
在一个功能复杂的模块中,通常会有很多的UI,MVC将控制和View分离可以的看清整个功能的结构,而且在扩展和代码复用方面有很大的益处(同一个控制类中,方法可以复用;以及添加一个界面或功能加文件就行了)
1 class Model(): 2 def __init__(self): 3 self.data = {} 4 5 class Ctrl(): 6 def __init__(self): 7 self.model = Model() 8 self.view = View() 9 10 class View(): 11 def __init__(self): 12 LoadUIFile(sUrl)
其他就不做太详细的介绍了,这里起个抛转引玉的作用,想深入了解的可以自行搜索相关内容。
本文主要用到的是MVC中的控制类
三、服务类抽取
这个是本文的重点,目的是将客户端具体实现逻辑和提供服务的引擎接口/通信协议分离。
1、为什么要将提供非客户端数据的接口/通信协议(主要是获得服务端数据)封装,这点的作用主要表现在下一块,本块不讲
2、为什么将引擎提供的方法分离(主要是引擎提供的数据和方法),这是本块的重点。
首先,假设我们做一个pc游戏,我们逻辑正常怎么写?
class Ctrl(): def __init__(self): self.model = Model() self.view = View() def Working(self): DoPCwork() DoNextWork()
如上所示,DoPCwork应该改成Dowork,因为我们如果只是简单制作一个游戏的话,不会考虑跨平台的问题。但是如果你是一个专业的游戏开发者,或者想要把游戏做大的话,就需要考虑这些了。
这个时候,如果我们需要兼容安卓平台,或者IOS,那应该怎么做?
def Working(self): # 我随便搜到的cocos的接口 if cc.sys.isMobile: DoMobilework() else: DoPCwork() DoNextWork()
显然,最简单的修改方式很容易想到。这样的修改方式有个问题:控制逻辑和引擎接口耦合了,所以你必须去修改控制逻辑,那怎么确保你现在的控制逻辑是正确的?需要通过测试。当然这个代码只改了一个if,测试起来方便的很,只测一个条件就够了。但是如果其他地方有细微的小改动呢?为了确保质量,必须全部测一遍!
控制逻辑是代码的核心,必须保证它的正确性。但是我只是做个兼容,本身逻辑没怎么变,居然就要直接对控制逻辑动手脚是种很危险的行为。因此,我们需要把引擎提供的数据和方法抽取出来。
class ServiceBase(): @classmethod def Dowork(cls): pass class PCService(ServiceBase): @classmethod def Dowork(cls): DoPCwork() class MobileService(ServiceBase): @classmethod def Dowork(cls): DoMobilework() def GetService(): # 这里用到了python的特性 # 效果等于返回一个实例 if cc.sys.isMobile: return MobileService else: return PCService class Ctrl(): def __init__(self): self.model = Model() self.view = View() def Working(self): GetService().Dowork() DoNextWork()
这里用到了设计模式的核心思想——面向接口编程。继承实现具体方法,接口选择用哪种去实现。好处其一,就是易扩展,也是设计模式经常考虑的问题之一,我再换个平台(比如Mac端)再写一个方法继承即可。其二就是,无论你怎么扩展,你的核心逻辑不会变,测试成功一次之后,你的这个逻辑就不会错了,错也一定是引擎相关的问题。
总结:抽出引擎提供的服务,可增加工程的扩展性,以及发生错误时能更快速准确的定位问题。
四、逻辑类测试
这一块设计的方法和上一块一致,不过把引擎提供的服务改成非客户端提供的数据服务,用人话说就是引擎提供的数据以及服务端提供的数据。
举个例子
class Ctrl(): def __init__(self): self.model = Model() self.view = View() def Working(self): Socket.GetServerTime() DoNextWork()
很明显,这个逻辑是:先获得服务器时间,再做其他逻辑。看上去没什么问题,现在这个代码交给你,你来测试这个代码,你应该怎么测试,你发现你又只能去改控制逻辑(捂脸笑哭.jpg)。因为你非客户端数据服务和逻辑又耦合了。那么,把它抽出来!
class ServiceBase(): # 服务类基础,这里其实并不需要 @classmethod def GetServerTime(cls): pass class Service(ServiceBase): # 提供具体服务 @classmethod def GetServerTime(cls): return Socket.GetServerTime() class TestService(Service): @classmethod def GetServerTime(cls): return "2019/11/11 11:11" test = True def GetService(): # 这里用到了python的特性 # 效果等于返回一个实例 global test if test: return TestService else: return Service class Ctrl(): def __init__(self): self.model = Model() self.view = View() def Working(self): GetService().GetServerTime() DoNextWork()
和之前抽引擎服务的方式一样,然后去通过继承 and 重写去伪造客户端本身所不能提供的逻辑,可以在不修改控制逻辑的情况下,完成测试。如上面代码样例,测试环境和正式环境只改一个字段就可以了。
五、总结
总结:这个设计方法的是将非客户端的数据服务,以及引擎提供的服务,进行提取,然后通过OOP继承and重写的特性去做逻辑测试和兼容。目的是避免测试和兼容过程中,对控制逻辑作修改,保证安全。
但是这个设计方法有个问题,就是如果不是和数据相关的引擎方法,即使抽取了,测试方法不变,因为他依赖图形界面,造成了代码的冗余。当然你这里可以说“我不提取也可以啊”,这句话没问题,是可以的,但是如果数据服务和非数据服务同时存在的同时,只抽取数据服务影响代码的一致性,抽取非数据服务又会造成代码的冗余,这一部分如何去择一,就要看具体需求了。
设计模式or方法终究是一种思想,是一种对某种特殊情况的巧妙的思想,但是绝不会适用于任何情况。
这个设计方法经过一段时间的使用之后,我觉得是个非常不错的设计方法。
方法很简单,一看就会,但是自己就是没有这种自觉,真正用的时候才会发现其巧妙之处,这就是代码设计的魅力。这种“玩法很简单,但是就是能让你眼前一亮”才是小游戏的乐趣。