易于跨引擎和测试的游戏客户端代码设计方法
Posted end-emptiness
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了易于跨引擎和测试的游戏客户端代码设计方法相关的知识,希望对你有一定的参考价值。
一、前言
本文讲的设计方法,不涉及算法、优化、接口讲解等技术介绍。
该设计方法基于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方法终究是一种思想,是一种对某种特殊情况的巧妙的思想,但是绝不会适用于任何情况。
这个设计方法经过一段时间的使用之后,我觉得是个非常不错的设计方法。
方法很简单,一看就会,但是自己就是没有这种自觉,真正用的时候才会发现其巧妙之处,这就是代码设计的魅力。这种“玩法很简单,但是就是能让你眼前一亮”才是小游戏的乐趣。
以上是关于易于跨引擎和测试的游戏客户端代码设计方法的主要内容,如果未能解决你的问题,请参考以下文章
Express实战 - 应用案例- realworld-API - 路由设计 - mongoose - 数据验证 - 密码加密 - 登录接口 - 身份认证 - token - 增删改查API(代码片段