易于增加兼容和测试的游戏客户端代码设计方法

一、前言

  本文讲的设计方法,不涉及算法、优化、接口讲解等技术介绍。

  该设计方法基于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)
View Code

  其他就不做太详细的介绍了,这里起个抛转引玉的作用,想深入了解的可以自行搜索相关内容。

  本文主要用到的是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方法终究是一种思想,是一种对某种特殊情况的巧妙的思想,但是绝不会适用于任何情况。

  这个设计方法经过一段时间的使用之后,我觉得是个非常不错的设计方法。

 

  方法很简单,一看就会,但是自己就是没有这种自觉,真正用的时候才会发现其巧妙之处,这就是代码设计的魅力。这种“玩法很简单,但是就是能让你眼前一亮”才是小游戏的乐趣。

 

posted @ 2019-11-13 11:47  二律背反GG  阅读(188)  评论(0编辑  收藏  举报