凉鞋:我所理解的框架 【Unity 游戏框架搭建】

前言

架构和框架这些概念听起来很遥远,让很多初学者不明觉厉。会产生“等自己技术牛逼了再去做架构或者搭建框架”这样的想法。在这里笔者可以很肯定地告诉大家,初学者是完全可以去做这些事情的。

初识架构和框架

架构和框架是非常接地气的,离我们其实并不遥远。

什么是架构?

架构是一个约定,一个规则,一个大家都懂得遵守的共识。那这是什么样的约定、什么样的规则、什么样的共识呢?

我以包为例,我经常出差,双肩背包里装了不少东西。笔记本电脑、电源、2 个上网卡、鼠标、USB 线、一盒大的名片、一盒小的名片、口香糖、Mini-DisplayPort 转 VGA 接口、U 盘、几根笔、小螺丝刀、洗漱用品、干净衣服、袜子、香水、老婆给我带的抹脸膏(她嫌我最近累,脸有点黄)、钱包、Token 卡、耳机、纸巾、USB 线、U 盘等。这个包有很多格子,最外面的格子我放常用的,比如笔、纸、一盒小的名片等;中间的格子一般放的是衣服、袜子、洗漱用品、香水等;靠背的那个大格子放了笔记本电脑,和笔记本电脑相近的小格子放的是两个上网卡、Mini-DisplayPort 转 VGA 接口、大盒名片、记事本,和笔记本电脑相近的大格子放的是电源、鼠标、口香糖等。

我闭着眼睛都可以将我的东西从包里掏出来,闭着眼睛都可以将东西塞到包里!但是,非常不幸的是,一旦我老婆整理过我的包,那我就很惨了,老是因为找不到东西而变得抓狂!更不幸的,要是我那个不到两岁的“小可爱”翻过,就更不得了了。

这个包就是我放所有物品的“架构”,每一个东西放置的位置就是我的“约定、规则、共识”。倘若我老婆也知道我的“架构”、我的“约定、规则、共识”,那么不管她怎么动我的包,我都照样能够轻易的拿东西或者放东西。进一步,如果我的同事也知道我的“架构”,知道我的“约定、规则、共识”,那么他们什么时候动我的包,我也毫无所知!——道法自然 《10 年感触:架构是什么?——消灭架构!》

什么是框架?

框架(framework)是一个框子--指其约束性,也是一个架子--指其支撑性。——360 百科

小结

本小节对框架和架构概念做了简单的认识,得出了以下两个结论:

  • 架构是“约定、规则、共识”
  • 框架具有约束性和支撑性

到这里,大家应该对这两个概念有点感觉了。但是还是会有很多疑问,比如“如何去做架构?”、“框架的约束性和支撑性分别指的什么?”等等,没关系,笔者坚信“带着问题去阅读”往往是最有效的阅读方式。接下来笔者将分享这两年来对框架和架构探索的经历,以及对这两个概念认识的演变,希望给大家带来一些启发,顺便大家心中的一些问题得到解答。


QFramework

两年前,笔者毕业半年,刚从 cocos2d 转 Unity 不到两个月,当时所在的公司有一套游戏开发框架。笔者用它做了两个月的项目,使用框架做项目的时候并没有去思考框架是什么,只是开始的时候觉得很新鲜,而且越用越顺手,尝到了它的甜头。

后来笔者接到了一个跑酷游戏项目,于是就把工作辞掉了,决定出来全职做这个项目。辞职后,公司的框架由于保密协议就不可以用了。项目就只能从零开始开发,那么结果就是在跑酷项目的开发的过程中各种中水土不服。

于是,笔者就开始了市面上开源框架的选型,折腾了几天,发现要么上手太难,要么学习成本很高文档不齐全,有的框架光是理解概念就要很久,对于像笔者一样刚毕业的初学者来说,市面上的开源框架真的很不友好。

从那时候笔者就决定要 为自己,开发一套符合自己使用习惯的框架,也就是现在的 QFramework。

为什么叫 QFramework ?

笔者在做 cocos2dx 的时候,市面上有个叫 Quick-Cocos2d-x 的开源框架,用两个词形容就是简单、强大。

笔者认为好的工具就应该简单。

QFramework 的目标是要做到像 Quick-Cocos2d-x 一样 “简单、强大”。当时笔者纠结过很多名字,比如 QuickEngine,QuickUnity 等等。Q 代表 Quick,并且 Q 这个字母给人感觉灵活有弹性,所以最终确定为 QFramework。

在决定要做框架之后,笔者就开始了边搭建框架边进行着跑酷项目开发的工作生活。

启蒙资料 《Unity 架构设计与开发管理》

很幸运地是,在跑酷项目开发之初,笔者接触到了一个非常好的关于 Unity 项目架构的学习资料,就是刘钢老师在 Unite 大会上的讲座视频《Unity 架构设计与开发管理》,视频中所提出的 Manager Of Managers 很好地为笔者开发 QFramework 指明了方向。虽然刘老师讲得通俗易懂,但是里边有很多话都很值得回味,笔者之后也花了很长时间去消化里边的内容,直到今天,笔者再看一遍视频还是会有很多收获的。在读此文时我们先不着急看里边的内容,视频链接会在文章的最末尾贴出。

跑酷项目准备

一个项目开始立项的时候,最常见的一个情况就是:几个人一个小团队,开始什么也不做,开始写代码,验证逻辑,然后 game 就开始写起来了。公司的一些的所谓的领导层一开始就把游戏定义为“我们要做的一个大作”,那么这个事情本身就是一个笑话。没有任何的规划和设计,我们就妄图就写出一个所谓的杰出的作品出来是不现实的。Unity 再好用,以这个心态去做游戏,一定会写不出来好的游戏来。——刘钢《Unity 项目架构和开发管理》

看到视频中的这段话,吓得笔者赶紧为跑酷项目做了些准备。比如最常见的表现和逻辑的分离。

表现逻辑分离

我们大家都知道,做项目尽可能地要把表现和逻辑分离。同样的跑酷项目也是如此,而最常用最经典的方式就是使用 MVC。

经典的 MVC 架构模式

MVC 是在软件开发时最常用的架构模式,我想大家都应该接触过,所以 MVC 的概念在这里笔者不会浪费口舌去赘述,不了解的同学可以参阅阮一峰前辈的《谈谈MVC模式》

跑酷项目中的 MVC

跑酷项目代码的架构使用的是很简单的 MVC。笔者当时是按照如下的方式去进行划分的:

  • View 层是各个 UI 的 Transform 本身。
  • Ctrl 层则是挂在各个 View GameObject 上的 MonoBehaviour。
  • Model 层则是若干个提供数据增删改查的并且可以全局访问的单例。

跑酷的 MVC 架构图如下所示:

从今天的来看,这种 MVC 设计是一种很粗糙的设计,尤其是其 Model 层,颗粒度太大,其实再可以分出个 DataAccess 层。不过粗糙的好处就是初学者能够驾驭,是有存在的意义的。

到这里,架构这个词终于出现了。MVC 是一种架构模式,对程序进行 MVC 的划分是在进行架构活动。除了 MVC 架构模式,还有几种其他的架构模式。

而对程序进行 MVC 的划分,实际上是对代码进行结构的设计,所以对程序进行结构的设计是在进行架构活动。

到这里,我们知道了架构是我们每天都在做的事情(划分 MVC 或者说将代码的表现与逻辑层分离)。而对代码进行结构的设是否和架构的“约定、规则、共识”有关系呢?答案是肯定的。我们接着往下进行探索。

文件结构

很多时候我们说的一个所谓的好的架构,直接就等于你要有一个好的标准,指定一些好的规则。——刘钢《Unity 架构设计与开发管理》

Unity 好的规则 3:

我们在起一些文件夹的名字的时候,尽量和我们的 GameObject 对应起来,如果我们的 GameObject 叫做 PoolManager,我下面所有的代码也都起一个同样的名字叫 PoolManager,那么这样在以后找对应的程序结构的时候,会比较好理解一些。——刘钢《Unity 架构设计与开发管理》

而跑酷项目最初的部分代码文件结构如下:

  • Scripts
    • App
      • Objects
        • Stages // 关卡单元
          • XXStage.cs
          • ...
        • Enemy // 敌人
          • EnemyCtrl.cs
          • EnemyModel.cs
        • Player // 角色
          • PlayerCtrl.cs
          • PlayerModel.cs
        • ...
      • Managers
      • EnemyManager.cs * StageManager.cs
      • ...

对于 MVC 的文件结构的划分,笔者在进行跑酷项目之前的其他项目的时候也尝试了别的方案。当时觉得这种方式找起来最方便。

在项目结束之后不久看到了一篇吴秦前辈的一篇好文《Unity3D手游开发实践》:

一般客户端用得比较多的 MVC 框架,怎么划分目录?

先按业务功能划分,再按照 MVC 来划分。“蛋糕心语”就是使用的这种方式。

先按MVC划分,再按照业务功能划分。“D9”、“宝宝斗场”、“魔法花园”、“腾讯桌球”、“欢乐麻将”使用的这种方式。

根据使用习惯,可以自行选择。个人推荐“先按业务功能划分,再按照 MVC 来划分”,使得模块更聚焦(高内聚),第二种方式用多了发现随着项目的运营模块增多,没有第一种那么好维护。——吴秦《Unity3D手游开发实践》

而笔者所采用的方式就是“先按照业务功能划分,再按照 MVC 来划分”。在这里仅仅是个建议,并不是一定要使用的方式。

QFramework 的第一个工具“单例模板”

在跑酷项目的 MVC 中,笔者把 Model 层设计成了单例。为什么使用单例呢?

抛开粗糙的设计不说,先让我们简单分析下跑酷项目的 Model 层的特点:

  • Model 层的数据在整个软件的生命周期里只有一份。
  • Model 层的数据要对任何 Ctrl 都提供增删改查。

很自然地就想到了使用单例模式实现。而很多开源库里都会提供可复用的单例模板,一般叫 Singleton 和 MonoSingleton。所以单例的模板作为第一个工具被收录到框架中。

“代码是资产”思维

在写一个项目的时候,不要短视的说我就把这个项目做完了,就是交一个差上线了就完了,我们希望每写一个游戏的时候,我们都积累一些东西,把写的每一行代码,都当成是一个可以收藏的,甚至是可以传递下去的这样的一个资产。有了这样一个思想,可能我们在写代码的时候,整个的思维模式会完全不一样。——刘钢《Unity 项目架构和开发管理》

听到上面这段话,在当时的笔者心中埋下了一颗种子。在这个基础上进行思考,会产生很多很有价值的想法。本小节先讲到这里。

小结

文章读到这里我们简单进行总结一下。

  • 架构是“约定、规则、共识”
  • 框架具有约束性和支撑性
  • 好的架构直接就等于一套好的规则,好的准则
  • Unity 好的规则
    • 3.起文件夹的名字时尽量和 GameObject 对应起来
    • MVC 文件结构:先按照业务功能划分,再按照 MVC 来划分
  • “代码是资产”思维
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils

结合跑酷项目准备阶段,以上的结论应用的结果如下图所示:

图中的需求收集/业务分析是本小节没有讲的,由于这个跑酷项目是合作项目,有一些需求合作方本身也没太想清楚,在进行准备这个阶段之前,跑酷项目已经完成了一版 Demo 进行了需求上的确认。

由于受到“代码是资产”思维的影响,QFramework 的开发模式最初是以收集工具为目的的,此时的 QFramework 并不是真正意义上的框架,而是一个库或一个工具集。

跑酷项目开发

在准备完毕之后,跑酷项目就开始了大量的业务/逻辑开发。

语言学习

在做跑酷项目时,笔者当时的水平怎么样呢?三个字,非常菜。有很多很基本的功能要学习 Unity API 才能完成,比如跑酷的关卡生成器等等。对 C# 语言的掌握也是靠着以前一点 Java 经验,才能勉强能应付逻辑开发,之前所说的单例的模板,也只是知道怎么去用,并不知道实现的原理。这时候笔者觉得必须要对 C# 进行基础学习。于是就开始每天看一点传智的 C# 基础视频。学习的过程中,一些语言特性不知道怎么用,而有的语言特性觉得很有用。所以此时只是为了完成项目而进行学习,自然而然地就没有太多的精力去深究语法细节。我们大家都知道,这不是一个很好的学习方式。

QFramework 是知识积累工具

随着对 C# 语言的了解程度加深,慢慢地可以看懂一些工具的源码了,也可以自己实现一些很简单的封装。而笔者在跑酷项目的开发期间先后收录了有限状态机、消息分发器和一些数学工具。以上收集的工具与单例的模板一样,都是同一性质的工具,所以这里没什么好说的。值得一提的是笔者当时做了一件事,笔者按照之前 cocos2dx 的使用习惯把一些 Unity 的 API 简单封装了一下,最初这么做只是为了提高自己的开发效率,扩大自己在 Unity 里的舒适区(笔者之二前一种用 cocos2d)。做了这件事之后给了笔者很大启发,笔者为什么不把一些新学习的 Unity 的 API 或者 C# 特性简单封装一下然后收录到 QFramework 中呢?这样以后使用这些 API 的时候就不用再查询搜索引擎了,直接使用封装的工具就好了。这样还能让 QFramework 帮助笔者“记住” Unity 的 API 和 C# 的特性。从那以后 QFramework 不止是一个工具集,也是笔者的一个知识积累工具。这样耗能解决上文中笔者对“学习的知识没有用武之地”的困扰。这样既能激发笔者的学习动力,又对 QFramework 本身也有好处,一石二鸟。

业务支撑工具

我们都知道,做一个游戏项目,都会用到 UI、音效、配置表和数据存储等模块。跑酷项目也是一样的,在刘钢老师的《Unity 架构设计与开发管理》视频里提出了一个叫做 Manager Of Managers 的架构方案,可以把以上模块全部做成一个单例,比如 UIManager,AudioManager 等。而笔者认为,这些工具模块都是为了支撑游戏业务的,比如游戏音频管理方案,界面层级管理方案等等。也就是说大多数项目都用得到。而不像单例的模板、有限状态这些工具,它们不是为了支撑业务而积累的。为什么这么说呢?单例的模板是设计工具,解决的问题不是业务问题是设计问题,而有限状态机则是一种数据结构,是简化一部分问题的思维模型。而 UIManager、AudioManager 等等。每个模块都是独立的解决方案,是为了解决某一业务问题而设计的。所以笔者在这里称它们为业务支撑工具。

而刘钢老师的在视频中列出了以下模块:

  • MainManager
  • EventManager
  • AudioManager
  • GUIManager
  • PoolManager
  • LevelManager
  • GameManager
  • SaveManager
  • MenuManager

这里除了 GameManager 以外,其他的全部可以在别的项目中复用。

这时跑酷项目中已经实现了

  • SoundManager(AudioManager)
  • ConfigManager(LevelManager)

这两个工具自然也收录到了 QFramework 中。这样 QFramework 的目标有了一些变化。而 QFramework 除了是工具集和知识积累工具之外,还是一个支撑业务工具的集合。而之后笔者要做的就是把就是 Manager Of Managers 提出的这些模块一一收录。

项目结束

很快,跑酷项目接近尾声。拿到结款后分析了一下当时工作生活状态的利弊。决定还是找一家公司继续沉淀。一是为了让 QFramework 多接触一些不一样的项目,二是笔者非常渴望与同行能交流的,三是开发跑酷项目的这两个半月对未知的恐惧太强烈了,比如对未掌握的技术,没把握实现的功能等等,四是合作方的第二个项目需要组个完整团队去做,当时身边没有太合适的人。总之跑酷这个项目到此完美结束了。

小结

在文章的最开始,笔者提出了框架具有约束性和支撑性。 QFramework 目前已经具备了其中的支撑性,也就是支撑业务。而业务是其中一种框架所能支撑的领域,除此之外还有其他的领域,比如团队协作,工作流等等,这里先不多说。而约束性到目前还没有提过。没关系,我们接着往下进行。

文章读到这里我们再进行总结一下。

  • 架构是“约定、规则、共识”
  • 框架具有约束性和支撑性
  • 好的架构直接就等于你要有一套好的规则,好的准则
  • Unity 好的规则
    • 3.起文件夹的名字时尽量和 GameObject 对应起来
    • MVC 文件结构:先按照业务功能划分,再按照 MVC 来划分
  • 代码是资产
    • 做完每一个项目都积累一些东西
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils
    • 笔者知识积累工具
    • 业务支撑工具集(支撑性)
      • SoundManager
      • ConfigManager

新的公司

2016 年 3 月下旬,跑酷项目做完之后来到了一家新的公司,来的时候公司已经具有一定的规模,其游戏技术团队也积累了一些 Unity 的插件和工具。而笔者所加入的团队是技术支持团队。技术支持的工作就是平时负责攻克技术难点,做一些预研,再做一些工具来给项目团队使用,有的时候项目人手不够了还要顶上去。而在这家公司的两年则是笔者成长最快的两年,一是遇到了好 Leader,二是做的事情非常喜欢。

UIManager

到了新公司,工作的同时,业余时间 QFramework 的开发还是要进行的。而公司的项目是没有一个很统一的框架,每个项目组都是各干各的。但是我们部门的好处就是都可以看到项目组的代码。每个项目的 UIManager 都是基于 Dictionary 来提供查询,然后简单地用 GameObject 的前后关系来管理 UI 的层级。笔者当时也看了一些市面上开源的 UI 框架,实现原理都差不多。所以索性就自己开发了一套很平庸的实现,没有什么太大的亮点。不过好在,算是完成了对 ManagerOfManagers 中 GUIManager 的收集。

AssetBundleManager

AssetBundleManager 是看一个公司项目时候看到的,本身是一个开源免费的 Asset Store 插件。笔者之前对于资源管理没有太大的概念。像之前做的跑酷项目,都是直接都把 GameObject 拖到场景里完成的。很少用到动态加载卸载内存。但是看了 AssetBundleManager 之后,很看好它的 Simulation Mode。所以就收录到 QFramework 里了。使用 AssetBundle 的好处有很多,支持热更啊,控制包体大小啊等等。缺点就是坑多,而且有一些学习成本,但是还是非常值得去研究的。

加班

在收集了 UIManager 和 AssetBundleManager 之后开始了时长较长的加班。加班的原因就不说了,从这时候开始 QFramework 就搁置了一段时间。

小结

本小节的 UIManager 和 AssetBundleManager 都是一种很普通的实现,没有太大的亮点。

文章读到这里我们再进行总结一下。

  • 架构是“约定、规则、共识”
  • 框架具有约束性和支撑性
  • 好的架构直接就等于你要有一套好的规则,好的准则
  • Unity 好的规则
    • 3.起文件夹的名字时尽量和 GameObject 对应起来
    • MVC 文件结构:先按照业务功能划分,再按照 MVC 来划分
  • 代码是资产
    • 做完每一个项目都积累一些东西
  • QFramework
    • 工具集
      • FSM
      • Singleton&MonoSingleton
      • MsgDispatcher
      • MathUtils
    • 笔者知识积累工具
    • 业务支撑工具集(支撑性)
      • UIManager
      • AssetBundleManager
      • SoundManager
      • ConfigManager

命名的力量

Unity 好的规则 2:我们在命名的时候要起一个比较有含义的名字。——刘钢《Unity 架构设计与开发管理》

在笔者刚毕业的时候,读了《代码大全》这一本书,其中第 11 章的《变量名的力量》反复读了三遍。这一章的内容对笔者之后养成良好的命名习惯产生很大的影响。书中重点讲了如何命名和编码规范的重要性。

编码规范

笔者很认同做项目要遵循命名规范这个准则的。但是现实是规范的执行过程中会遇到很多阻碍。比如笔者最开始去网上找了一个编码规范文档。一份文档 50 多页,看都觉得很痛苦,只好放弃。最终经过一段时间探索之后得出了一个结论:编码规范只要解决问题就好,其他的尽量确保容易遵守就好。

编码规范解决的是什么问题呢?

  1. 减少项目交接时,由于代码风格水土不服所带来的风险。
  2. 当某个项目人力不足时,可以减少加人时所带来的人力浪费(可以让一个人花更少的时间去看懂某个项目的代码)。
  3. 防止项目过了一段时间一些实现自己都看不懂了。

以上当然要靠一个编码规范是无法完全解决的,除了编码规范之外,还有资源命名规范,项目结构规范等等。
经过多次因为以上原因的加班之后,深有感触。编码规范是非常有必要做的。

为自己制定一个编码规范

在公司,笔者还是最基础的员工,没有什么权利。所以对于定义规范这种事情,在公司想想就好了。不过这并不阻碍笔者为自己制定一个编码规范。于是笔者根据自己的编码习惯,定制了如下的编码规范。

/****************************************************************************
 * Copyright (c) 2017 liangxieq
 * 
 * https://github.com/liangxiegame/QCSharpStyleGuide
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ****************************************************************************/
 

namespace QFramework.Example 
{	
	using UnityEngine;
	using UnityEngine.UI;
	using System.Collections;
	using System.Collections.Generic;
	
	/// <summary>
	/// 展示编码风格
	/// </summary>
	public class ProgrammingStyle : MonoBehaviour 
	{
		#region Basic App
		/// <summary>
		/// 1.private/protected使用m开头+驼峰式
		/// 2.前缀最好展示所属的Component类型比如Button->Btn
		/// </summary>
		[SerializeField] Button mBtnEnterMainPage;

		/// <summary>
		/// public类型使用首字母大写驼峰式
		/// </summary>
		public int LastIndex = 0;

		/// <summary>
		/// public 类型属性也算public类型变量
		/// </summary>
		public int CurSelectIndex 
		{
			get { return mCurSelectIndex; }
		}

		void Start () 
		{
			mBtnEnterMainPage = transform.Find ("BtnEnterMainPage").GetComponent<Button>();

			// GameObject命名
			// 临时变量命名采用首字母小写驼峰式
			GameObject firstPosGo = transform.Find ("FirstPosGo").gameObject;
		}

		/// <summary>
		/// 方法名一律首字母大写驼峰式
		/// </summary>
		public void Hide() 
		{
			gameObject.SetActive (false);
		}
		#endregion

		#region Advanced
		/*
		 * GameObject->Go
		 * Transform->Trans
		 * Button->Btn
		 * 
		 * For->4
		 * To->2
		 * Dictionary->Dict
		 * Number->Num
		 * Current->Cur
		 */

		/// <summary>
		/// 1.Bg肯定是图片
		/// </summary>
		[SerializeField] Image mBg;

		/// <summary>
		/// GameObject->Go
		/// </summary>
		[SerializeField] GameObject mDialogGo;

		/// <summary>
		/// Transfom->Trans
		/// </summary>
		[SerializeField] Transform mScrollViewTrans;

		/// <summary>
		/// Index、Num、Count等肯定是int
		/// </summary>
		[SerializeField] int mCurSelectIndex;

		/// <summary>
		/// RectTransform->RectTrans;
		/// </summary>
		[SerializeField] RectTransform mScrollContentRectTrans;

		/// <summary>
		/// 1.Pos肯定是Vector3、Vector2
		/// 2.Size肯定是Vector2
		/// </summary>
		[SerializeField] Vector3 mCachedPos;
		[SerializeField] Vector2 mCachedSize;

		/// <summary>
		/// 后缀s表示是个数组
		/// </summary>
		[SerializeField] Vector3[] mCachedPositions;

		/// <summary>
		/// 1.List后缀
		/// 2.4->for 表示所属关系可以表示Dict
		/// 3.Dict后置
		/// </summary>
		[SerializeField] List<Vector3> mCachedPosList;
		[SerializeField] Dictionary<string,Vector3> mPos4ChildName;
		[SerializeField] Dictionary<string,Vector3> mChildPosDict;
		#endregion
	}
}

规范直接使用代码展示,容易看懂自然而然也就会容易遵循。

破窗效应

一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应。

我们做项目也是一样的。一定要好好写代码,不要让“破窗”在我们的项目中发生,不能让项目有任何变混乱的趋势,保持项目清爽,这可以给我们开发者到来很好的工作体验,也就是所谓的心流体验。

如何命名?

在一些命名格式上,可以遵循编码规范就好了。但是如何给一个类/方法/变量/枚举命名呢?

在问这个问题前,我们来问另外一个问题,那就是程序语言,所谓的语言是给谁看的?一是给计算机或者编译器能看懂。二是给我们人类看的。让计算器或者编译器看懂很容易,只要遵循程序的语法去写就 OK 了。但是如何让人更容易看懂,当然答案也很简单,就是好好命名。关于如何命名,一些笔者至今受用的命名准则这里这里简单介绍下。

  • 使用业务相关的词汇命名而不是计算机相关的词汇

比如,SaveMgr 中的 Save 是保存,是业务相关的词汇。而 SerializeHelper 中的 Serialize 则是计算机相关的词汇。这条准则在 业务/逻辑/UI 层会有很大的效果。而在 Framework 层或者说底层还是使用计算机相关的词汇比较好。

  • 方法/函数命名用谓语 + 宾语方式命名
    比如 PlayerData.Save,或者 SavePlayerData

  • 类名和方法参数使用名词

  • 表示一个动作状态时通过动词的不同时态进行命名。

比如 Connecting,Connected,Connect 表示连接的三种状态。

关于命名和规范就先讲到这里,命名是一门学问,其内容多得可以去写一本书去介绍了。如果想深入学习,建议首先看《代码大全》的第 11 章 《命名的力量》。

小结

还记得在前边说的架构的定义嘛?架构是“约定、规则、共识”,而确定各种规范也是准备阶段要做的事情,也是架构的一部分。

再次起航

在新的公司度过了一段加班生活,之后加班次数慢慢就减少了。这时候就又有时间去搞点东西了。

一个视频教程的学习

首先是当时,在某教育网站上学习了《万能游戏框架》视频教程,笔者从头到尾跟着手敲了一遍。教程里的一个基于模块的消息框架实现得很有意思。这里简单说一下。 QFramework 之前收录的 MsgDispatcher 就是一个全局的字典,字典的 key 是事件名字,而 value 则是 委托 List,所以不管怎么定义消息,它们都是全局的。当消息的规模变大之后,会有很大的性能压力。如图所示:

全局消息与单例模式一样都是用起来很方便,但是风险很大的设计。

而 《万能游戏框架》里的消息则是以模块为单位的。比如 UI 模块则只负责 UI 界面相关的消息收发和注册,Audio 模块同理也是。 而跨模块之间则用一个简单的 switch 进行转发。比较出彩的是其中的关于频段的设计。我们都知道 C# 里的 ushort 的最大值是 65536,视频中每个模块的频段长度设为 3000,这样最多可以有 21 个 模块,足够使用了。每个模块可以注册 3000 个消息。如何实现,这里看下代码就明白了。

public enum MgrId
{
	UI = 0 * 3000,
	Audio = 1 * 3000,
	...
}

public enum UIXXEvent
{
	Start = (ushort)MgrId.UI,
	XX,
	YY,
	End,
}

public enum UIYYEvent
{
	Start = (ushort)UIXXEvent.End,
	ZZ,
	End,
}

笔者当时看到这里才觉得自己对语言的了解真的是很浅,一个简单的 ushort + 枚举就可以很巧妙地设计出基于模块的消息框架,这种思想非常值得借鉴。笔者马上在 QFramework 中实现了一套类似的消息框架。很简单,一个 QMsgCenter 充当跨模块之间的消息转发。一个 QMgrBehaviour 作为模块的基类,负责收发和注册模块内的消息,一个 QMonoBehaviour 只要一个脚本继承它,就可以发送消息和注册处理消息。而事实上,有了这套消息框架,QFramework 才算是一个真正的 Manager Of Managers 框架。

初涉工作流

公司的以为前辈也有一套类似的框架,不过在以上这套消息框架的基础之上,做了 UI 的脚本生成。在此之前笔者都是用 transform.Find 方式来获取感兴趣的 UI 控件的。比如 Button、Image 等等。而前辈的 UI 脚本生成省去了这些工作量。实现方式也是比较容易理解。就是在一个 UI 的 Prefab 上,对于感兴趣的控件挂上一个脚本,比如 UIMark/UIBind。然后从 UI 的 Prefab 的 Root 开始进行深度优先搜索。搜索过程中记录每个标记脚本的路径,之后根据路径生成一行行的 transform.Find(路径)就好了。而这个工具则是节省了制作 UI Prefab 过程中的体力劳动。是对工作流上的优化。QFramework 又收录了一个工具。

支撑团队协作

团队协作的一个基础就是将业务模块化。而业务很多时候是在完成大量的 UI 界面。在这里简单分享下笔者的做法。笔者首先会为每个 UI 界面都建立一个测试场景,只要运行 Unity 就可以看到 UI 界面的效果。这样做的目的很简单,就是方便快速修改,并且界面之间互相独立,只要约定好谁来负责哪个模块,就不会造成版本控制冲突。 还有一个建议要做的就是,为每个 UI 界面都提供一个 Init 接口。一些 UI 界面要用的数据,笔者建议是从一开始通过初始化传进去,而不是在 UI 里面去访问某个 Manager。这一点要做到需要花些功夫,不过好处就是 UI 作为一个黑盒,没有上下文,可以传入一些测试数据而不是真实数据就可以看效果并做一些测试了,当项目规模变大时,改一个 UI 界面或者查找一个 bug 都会变得很容易。这样的做法解决了多个问题,一石 N 鸟。

C# 进阶

C# 真的是越用越觉得它的强大。QFramework 的进步是离不开 C# 语言的学习的。这里笔者遇到了一个决定 QFramework 未来的语法特性,就是静态 this 扩展。语法细节这里不多说,大家自行百度。学习了这个语法之后,一些本来要靠继承才能实现的 cocos2dx 风格的 API 全部可以用这个语法实现。简直不要太好用!都后来的链式结构编程全都是以这个为基础的。

QFramework 成为公司的指定框架

在笔者的坚持下,经过了团队的 Code Review 之后,大家终于统一了使用 QFramework 作为公司的框架。从这时候开始 QFramework 开始飞速发展。

第一个项目

第一个项目三个人完成的,架构阶段以笔者之前定的代码规范为基础与团队成员共同完成了项目的代码规范,随后完成了项目结构目录约定等等一系列约定,之后与项目的负责人根据项目需求确定了插件的选择,而框架自然就用 QFramework 了。除了以上这些还做了一件事,就是画了一张不知道是什么的图。

上边又有排期,又有分工,又有一些技术实现细节,还有各个模块的划分。总之看着很乱,但是它的作用就是让我们三个人很清晰地对项目的各个结构,以及近期的排期等信息,项目的难点也一目了然。做好排期和分析后,就开始进行开发了,最终这个项目不管是时间还是品质上,都完成得很不错。这就是充分(相比之前)做架构的好处。

竞争对手出现

在做第一个项目的时候,来了一位大牛,带着一套 MMO 框架。框架好用的工具真的很多。

其中的 EventSystem(消息系统)和 ResSystem(资源系统)是两大亮点。EventSystem 的 EventId 是用泛型进行注册的。把一个泛型转换为 int 。这个解决了之前笔者注册事件时需要把枚举强转成 ushort 的问题,这样的代码写起来很不愉快,于是笔者把原来 MgrBehaviour 和 QMonoBehaviour 里关于消息注册和转发的代码杀掉,直接换成了 EventSystem 就 OK 了,QMonoBehaviour 和 MgrBehaviour 里的代码变得非常精简。而 ResSystem 使用非常简单和强大。ResSystem 是在 AssetBundleManager 的功能基础之上有抽象出来了 ResLoader。这样做有什么好处呢?

首先 AssetBundleManager 在哪里加载了什么资源和卸载了资源需要使用人脑进行记忆,项目体量很大时很容易由于忘记卸载资源而造成内存泄露。而 ResLoader 是一个对象,可以每个界面都申请一个 ResLoader 对象。所有在这个界面加载过的资源的信息都会记录到 ResLoader 里,而卸载很简单,只要在 OnDestroy 里直接进行 ResLoader 的卸载就好了,非常方便。但是这时候笔者已经用惯了 AssetBundleManager 的打包方式,所以只收录了 ResSystem 中除打包以外的代码。这里简单提一下,ResLoader 是用对象池实现对象的申请和回收的。而 ResSystem 里的资源积累则是使用引用计数器决定资源的释放的。在这里 QFramework 收录了 EventSystem、ResSystem、引用计数器、对象池,可以说收获颇丰。

提拔带人

做完第一个项目之后,被 Leader 提拔,开始带人带团队。在框架和架构进行探索的时间少了很多。QFramework 在这之后边也加了一些库,比如 UniRx,ActionKit 等等。最终就是现在的 ActionKit、UI Kit、Res Kit 为核心的 QFramework 了。ActionKit 专注异步逻辑和状态机,可以很好地完成 GamePlay 需求和异步需求。而 UI Kit 是 UI 的解决方案,里边还是包含着之前的基于模块的消息框架。 Res Kit 则是解决资源管理方案。这里不多说 Action Kit。在这里笔者的经历分享完了。

总结

  • 架构是“约定、规则、共识”

  • 框架具有约束性和支撑性

  • 好的架构直接就等于你要有一套好的规则,好的准则

  • Unity 好的规则

    1. 使用 C# 而不用 JavaScript
    2. 命名的时候要起一个比较有含义的名字
    3. 起文件夹的名字时尽量和 GameObject 对应起来
  • MVC 文件结构:先按照业务功能划分,再按照 MVC 来划分

  • 代码是资产

  • 做完每一个项目都积累一些东西

  • QFramework
    * 工具集
    * FSM
    * Singleton&MonoSingleton
    * QEventSystem
    * MathUtils
    * 笔者知识积累工具
    * 业务支撑工具集(支撑性)
    * UI Kit
    * UIManager
    * 代码生成
    * MgrBehaviour、MonoBehaivour(约束性)
    * Res Kit
    * AudioManager
    * Action Kit
    * 建议
    1. 为每个 UI 界面都建立一个测试场景。
    2. 为每个 UI 界面都提供一个 Init 接口。
    3. UI 界面的 Init 接口传数据,而不是在 UI 里面去访问某个 Manager Or Instance。

  • 在项目准备的架构活动

    1. 需求/业务整理、收集、分析
    2. 编码规范、项目结构约定、资源命名规范、程序结构约定、模块/MVC 划分、成员分工
    3. 插件购买、造轮子、框架选型

这里可以得出框架与架构关系的结论,框架可以解决一部分架构问题,使用框架 本身就是一种“约定、规则、共识”。

直到文章的结尾,QFramework 还是没有收集到关于框架的约束性相关的内容。唯一能扯上点关系的就是基于模块的消息框架这块了。其实像 StrangeIOC、uFrame、PureMVC 等框架可以更容易去讲解约束性相关的内容。Any way 这次就讲到这里吧,我们以后见。

推荐资料

  1. 《UNITE -Unity项目架构设计与开发管理》
  2. 《架构漫谈》
  3. 《Unity3D手游开发实践》
  4. 《10年感触:架构是什么?——消灭架构!》
  5. 《凉鞋的笔记》

转载请注明地址:凉鞋的笔记:liangxiegame.com

更多内容

posted @ 2020-03-24 10:49  凉鞋的笔记  阅读(5348)  评论(5编辑  收藏  举报