基于团队的持续优化之道

基于团队的持续优化之道

这是侑虎科技第396篇文章,感谢作者贾伟昊供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者个人主页:https://zhuanlan.zhihu.com/p/36930662,同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!


四月份的博客也欠下了,大部分业余的时间花在准备UWA DAY 2018的分享上面。当时也说了,做这次分享也算实现了我去年的一个“小愿望”,借这个机会认识了不少做技术的朋友。回到杭州之后,也有不少人跟我要PPT,讨论一些分享中的细节。在准备分享的时候,因为时间关系,也将一些计划扩展开的内容删除掉了。所以这次想再根据准备时候的逐字稿整理一篇博客,以作记录,也为没有参加UWA DAY 2018的朋友提供参考。

所以这篇博客的主题还是以这次分享的核心内容为主线,顺便把PPT的内容也提供出来(以引用块或者(补)的方式标注,以方便听过分享的朋友阅读)。


题目和提纲

这次分享的题目是《Connecting The Dots——基于团队的持续优化之道》。优化是一件琐碎而又繁复的事情,它通常是一个又一个的点,而我们如果要让一个比如说30人甚至50人的大型团队,花1到2年的时间去开发一款大型的手游项目,不但需要了解和掌握这些优化的细节,而且要借助团队的力量在整个游戏开发的生命周期过程中不断地进行持续优化,才能够让优化效果可以保持下去。
请输入图片描述

在聊具体的分享内容之前,我想先抛出这样一个问题——在优化这样的过程中,程序应该承担一个什么样的角色?
请输入图片描述

这也是和我刚才所想要聊的持续优化关联性非常大的问题。在优化过程中,程序可能有两种角色,一种是像救火员这样的角色,哪里冒火了,就去扑灭它。我自己也有过作为救火员去做优化的经历。另外一种工作方式是像园丁一样,可能不需要特别紧急地在最后关头才处理那些性能问题,而是从项目的初期就开始规划整个项目的性能指标,然后让整个团队可以按照指标的要求合理地产出美术资源、编写代码,从而让整个产品一直处于一个比较健康的状态。

救火员的职责很有挑战性,同时也比较容易做出成就感,比如我也有过用1到2周时间让游戏的性能有非常大提升的经历。而园丁在做的事情可能看上去比较普通,都是些日常且琐碎的事情。在游戏开发中,这两种角色并不冲突,而是同时存在的。但是在我的理解里,对于性能更友好的状态,是让团队里的成员都在以一个园丁的角色在工作,从根本上来保持游戏的性能一直处于一个较好的状态。因为——每一个神级优化的背后,都隐藏着一个2B的bug!

那这次分享我就想从这样三个方面来聊一下我所理解的以“园丁”的角色来让团队进行继续优化的方式。
请输入图片描述

首先是美术资源的优化,美术资源是一个会对客户端运行效率产生非常重要影响元素之一,我想和大家聊一下,怎么样让我们团队可以在美术资源产出方面一直保持一个比较好的状态。然后我想回归到程序的本职工作,从一个更加宏观的角度来讨论一下程序代码的部分要怎么样去做一些优化,对于代码质量和一些底层模块的设计,如何去做一些提前的思考和设计。第三部分,团队开发效率的优化,这块可能看上去跟性能优化的题主没有直接的关系,但是在我看来优化不仅仅是游戏运行效率优化,而且应该包括整个团队开发效率的优化,这也是在项目开发过程中非常重要的部分。


1.美术资源优化

首先来看一下美术资源优化的部分。
请输入图片描述

大家在日常开发中可以感受到,对于美术资源部分,美术和程序,尤其是要进行性能优化的程序,他们的关注点会有不同。美术更多的是关注美术效果是不是足够的好,而程序可能更多的关注美术资源在设备上的运行效率是否可以达到目标帧率,这其中就有一些目标导向上的冲突。我认为,解决这一冲突的一个非常重要的手段是尽早来建立合理的美术资源制作规范。
请输入图片描述

这其实是一个美术资源制作的过程,首先通过在立项时制作Demo来确定适合的美术资源规范,然后通过给美术灌输效率意识、提供辅助工具让美术在制作资源的过程中可以严格按照规范进行,最后是要借助QA的力量对产出的资源进行检查,确保达标。

1.1 规范制定

为什么制定美术规范这么重要呢?我想借自己之前的一个项目中的真实经历来聊一下。
请输入图片描述

《无尽战区·觉醒》这款手游是我作为主程带领团队开发的第一款手游项目。当这个项目进行到中期的时候,收包了一批场景的资源。在使用这些资源的时候发现它们的运行效率明显偏低,Profile发现面数和DrawCall都超标了。面数很简单,使用减面工具进行减面即可,DrawCall也使用类似Unity的Static Batching的方式进行优化,但是结果发现一些场景的DrawCall只能从400减少到300多,并不能达到预期。仔细检查发现很多模型无法合并的原因是使用了端游常用的一种贴图制作方法——四方连续的贴图。这种方式虽然可以使用很小的贴图尺寸通过tiling制作出精细的效果,但是对于DrawCall敏感的手游来说并不合适。最终项目又请了好几个外派美术进行资源的整合和制作,导致多花费了几个周时间和几十万的美术成本。虽然对于大公司来说几十万的美术成本不算什么,但是我依然觉得这是我作为一个主程的失职——因为没有提前和美术沟通好制作方法,导致了这样的问题发生。所以在出来创业的项目中,在项目最初期Demo制作完毕之后,客户端团队就和美术一起制作了非常详细的美术资源制作规范。

制定规范的步骤大致可以整理为如下的几个部分:
请输入图片描述

在制定美术规范的第一步是要进行游戏信息的收集,因为不同的游戏类型以及镜头视角会对美术资源的制作产生非常大的影响,比如2.5D和3D自由视角的游戏是有不同的制作和优化方式。

在收集了足够的信息之后,需要和美术敲定一些大的技术方向,比如线性空间、HDR和动态光影等。线性空间是团队应当提前关注的一个点,我个人的观点是在写实的游戏风格中,能上线性空间还是尽量使用线性空间,对于美术效果的提升还是很有帮助的。在正确的基础上,才更容易出正确的效果。Unity目前的版本和OpenGL ES 3.0的普及率,个人观点是完全可以在移动平台上使用线性空间的。当然这也要考虑具体项目的内容以及开发团队成员的能力和经验等因素。HDR的开关在Unity中还是比较简单的,但在性能方面对于带宽的影响比较大,收益也不小,建议追求效果的团队提前考虑。动态光影是我们研发团队的美术特别想追求的一个效果,但是因为我们项目今年要上线,而且要考虑低配的效果,所以程序一直卡着不让场景使用全实时的动态阴影,而是尽量使用烘焙的方案。

确定目标机型就不多说了,考虑主流的高中低三档,建议提前购买一些设备方便后续的性能测试。我对于这三档的基本分类大致如下(这个也可以参考UWA测试中的机型选择):

高档:大部分iOS设备,主流的安卓旗舰设备;
中档:少部分希望支持的iOS设备,比如目前的iPhone 5s、6/6s,ipad mini等,1-3年前的旗舰以及1-2年的1500元左右的安卓设备;
低档:1-3年前的千元机。

请输入图片描述

接下来是要针对面数和DrawCall这两个核心点,对整体的标准进行定义,并将这些标准分配到场景、角色、特效、UI等资源分类上。因为这些不同类型的资源是由不同的美术同学产出的,他们之间通常不会去沟通各自的性能消耗,因此需要针对每部分进行规则的细化。
请输入图片描述

场景部分强调一下贴图“像素密度”的概念。在制作场景的时候需要提前定义贴图使用的像素密度,否则会导致贴图精度过高或者不够等问题。所谓的“像素密度”是指在正常的游戏视角下,一个一立方米的Cube应当使用多少尺寸像素的贴图。这个在场景制作的初期应当定义好,美术才好去使用正确的贴图尺寸,否则到后期优化可能发现大量超标的贴图在使用,如果此时已经进行了贴图的合并,美术修改的工作量会非常大。
请输入图片描述

角色部分的规范需要把面数和DrawCall的标准细化到每个角色中,这时候需要和策划沟通期望的同屏显示人数,根据不同的游戏类型会有很大的差异。比如《崩坏3》不会同时有很多战斗单元,因此可以给每个角色非常高的精度,而像《御龙在天》这样的国战游戏,追求百人同屏的效果,对于每个角色的消耗限制就会比较大。

骨骼数量的部分最好在前期和美术沟通好,比如手指骨骼的减少和合并,脚部骨骼的简化,来减少CS骨骼的数量,给飘带等Bone骨骼留出足够的空间。因此美术规范制定的过程并不是单纯的程序给美术添加制作限制,而是一个和美术一起来讨论如何在有限的资源下制作出更好效果的过程。
请输入图片描述
请输入图片描述

UI制作规范是我们初期没有特别关注的部分,也因此踩了一些坑:

UI组件数量较多导致加载顿卡。我们有些复杂界面有上千个GameObject,几百个UI组件,这是非常恐怖的。我们测试发现UI的Prefab加载的时间消耗在Unity 5.5.6上和UI组件的数量几乎成线性关系。这里补充一张我们测试的GameObject数量和加载时长的测试结果图(横轴是GameObject个数,纵轴是单纯的Prefab加载的时间消耗,单位秒,测试环境:小米Max2):
请输入图片描述

因此建议其他团队提前考虑UI部分的异步加载和分块加载。(或者升级引擎到5.6.5+或者2017.3+,对于Prefab的加载速度有了很大的提升)

UI中的粒子特效建议提前考虑异步加载,我们最初直接放在了UI的Prefab中,不但因为Unity不支持Prefab的嵌套导致维护麻烦,而且因为每一个ParticleSystem的初始化都占用一个几乎固定的时长(我们自己测试在PC上也大约有3ms左右),导致UI初始化的时候非常卡。使用一个间接的引用,就可以比较容易地做到既方便更新又可以异步加载。
请输入图片描述

最后针对标准,要进行真机的压力测试,并且在组内推广,形成大家都认可的美术资源制作规范。

1.2 美术资源制作

在确定好美术资源的制作规范之后,就是需要在美术铺量制作的阶段让美术可以按照规范进行资源的制作,在这个阶段程序需要注意去做的事情我觉得有两点:

帮助美术建立效率意识;
为美术提供便利的制作和检查工具。

请输入图片描述

在我们这样的一个初创团队里,没有经验丰富的TA存在,而且有部分美术同学之前来自外包团队,对于游戏运行时的效率意识比较薄弱,因此需要客户端程序同学不断和美术沟通来灌输对于游戏运行效率的关注。通常的做法包括日常的沟通、规范和技术点的分享等。

而在提供便利的工具方面,除了教会美术使用Unity已经提供的那些性能检查工具(Batches和面数查看、Overdraw、Mipmaps、Wireframe等渲染模式)之外,我们也为美术提供了非常多的开发和检查工具。
请输入图片描述
请输入图片描述

从截图中可以看到我们为美术提供了大量的工具,选择几个来着重介绍一下:

1.2.1 场景镜头同步功能

在场景资源制作的时候,需要美术去关注的除了常规的DrawCall之外,还有Overdraw、Mipmap以及面数。Unity已经在Scene View下提供了这三种渲染方式的检查工具,但是在我们的使用中发现,由于游戏运行时镜头规则的复杂和多变,导致美术在Scene View下无法准确判断是否存在资源不合理的情况。因此美术提出希望可以在Game View下查看这些状态。
请输入图片描述

最初的时候我们也是想在Game View下实现这些不同的渲染方式,并且已经集成了OverDraw的检测方式,基于的也是钱康来建议的Unite2017/OverdrawMonitor。但是后来觉得这种方式会影响美术对于游戏的操作,程序实现起来也要多花一些时间,比如Mipmap的实现效果就不是很满意。后来就想到另外一个思路——在Scene View下来做镜头和Game View下的镜头同步。结果就非常简单,只需要为Game View下的Camera身上添加一个Component就好:

namespace ThorProfile
{
    public class SyncSceneView : MonoBehaviour
    {

#if UNITY_EDITOR
        private SceneView view = null;

        // Use this for initialization
        void Awake()
        {
            view = SceneView.lastActiveSceneView;
        }

        private void LateUpdate()
        {
            if (view != null)
            {
                view.LookAt(transform.position, transform.rotation, 0f);
            }
        }

        private void OnDestroy()
        {
            if (view != null)
            {
                view.LookAt(transform.position, transform.rotation, 5f);
            }
        }
#endif
    }
}

核心的代码只有LateUpdate中的那一句:"view.LookAt(transform.position, transform.rotation, 0f);",实现的效果就是正常操作游戏,Scene View下的Camera可以一直跟随移动,视角和Game View下非常接近。(有需要动态图自己使用上述代码进行试验即可。)
请输入图片描述

1.2.2 批量烘焙功能(补)

Unity的烘焙在Unity 5.X的版本速度还是比较低,我们虽然为美术专门购买了CPU强劲的烘焙机,但是比如在制作大世界的时候,因为场景拆分得比较细所以需要一次性连续烘焙多个场景,于是为美术提供了批量烘焙的功能。之前在知乎上也有朋友问过类似问题,代码非常简单,直接贴一下,需要的自取好了:

    [MenuItem("美术工具/烘焙选中场景(同步)")]
    public static void BakeSelectedScenes()
    {
        Object[] selectedAsset = Selection.GetFiltered(typeof(SceneAsset), SelectionMode.DeepAssets);
        foreach (Object obj in selectedAsset)
        {
            string scenePath = AssetDatabase.GetAssetPath(obj);
            Debug.Log("开始烘焙场景: " + scenePath);
            EditorSceneManager.OpenScene(scenePath);
            Lightmapping.Bake();
            EditorSceneManager.SaveOpenScenes();

            //如果有更新Prefab的需求,可以放这里。

            EditorSceneManager.SaveOpenScenes();
            Debug.Log(scenePath + " 场景烘焙完成!");
        }
    }

使用同步的方式烘焙,会卡住Unity,但是烘焙速度应该会有些提升(没有对比过……)。

1.2.3 场景合法性检查(补)

我们为美术添加了场景的合法性检查工具,因为我们对于场景中的相机设置等有些特别的要求。这块跟具体项目相关,只罗列一下我们在检查的内容以及要检查它们的原因。

顶点格式,对于带有color等逻辑上不需要的数据的部分给出警告。因为我们的场景是采用静态合批的,如果有资源不小心导入了color等不需要的顶点数据,会导致整个合并之后的mesh变大很多。
地形数据,我们使用了T2M的工作流程,美术使用Terrain进行地表的制作,然后通过T2M插件导出成mesh,为了保留原始的编辑数据便于以后修改和调整,Terrain会保留在原始场景里,但是在发布场景里不允许带有这块数据,即使Disable也不行,会对场景加载带来额外负担。
光源参数,不允许设置光源的ShadowType的Resolution属性,必须为Use Quality Settings。在做UWA的深度优化的时候,我们发现有些情况下ShadowMap的尺寸从8M增加到了32M,检查后发现是美术为了更好的效果自己调整了这个参数导致的。脱离程序高中低效果控制的配置是不被允许的。
物理碰撞体,在目前的手游上,我们的希望是尽量少地使用物理,因此我们无论是在寻路还是在动态阻挡方面都尽量少地使用物理,同样在场景里禁止摆放MeshCollider组件,对于其他类型的Collider组件也进行检查,对于我们项目通常都是不需要的。
摄像机设置,我们游戏中镜头是完全由程序逻辑控制的,因此不允许在场景中遗留用于预览的Camera组件。

就像之前说的那样,这部分非常零散,通常是在项目开发中不断发现的各种问题通过统一的检查工具来让美术在上传之前进行自查,确保资源提交到svn上的时候就是正确的。

1.3 美术资源检查

在美术大量产出资源的过程中,除了美术自查之外,还需要其他职位的同事同时对美术资源进行检查。
请输入图片描述

在我们项目中,对于美术资源的检查主要有三个部分:

程序检查。程序会根据发现的性能问题进行针对性的检查,比如会使用Profile、FrameDebug工具等进行问题的排查。
我们也是UWA产品的深度用户,购买了专业会员,几乎每个月都会提交一份安卓版本的包让UWA团队帮忙进行在线的性能诊断与优化,频繁的优化周期内可能会每周提交一份包。去年的年底也邀请UWA团队来公司针对我们项目进行了深度优化,发现和解决了不少美术资源的问题。
QA每周的性能测试。每周QA团队会在周版本之后,生成《性能测试报告》,以邮件的方式发送到全公司所有人的邮箱里。

这里以我们的《性能测试报告》为例来说一下QA团队进行性能监测的内容。

1.3.1 包体大小监控

我们经历过在测试上线之前要进行包体大小优化的情况,因此将包体大小的监控放到了每周的性能测试报告里。
请输入图片描述

我们会统计整体包体大小、资源占用大小、按照场景、UI、角色、特效等分类之后统计各自的大小,以及如果包体大小有变动,主要原因是什么。这部分使用的工具主要是基于打包时候产生的中间文件来统计分析。如下图所示,左侧是按照文件夹分类的列表,右侧是选中的节点下的所有文件,可以进行排序、关键字过滤,以及多选统计等操作。
请输入图片描述

在进行资源大小统计和分析方面,我们也开发了一套在Unity内的根据资源的引用关系进行分析的工具,统计对象为所有要打包出去的资源,可以查看资源之间的引用关系、被引用关系、资源消耗统计(粒子系统数量、GameObject数量等)。借助这个工具可以方便地进行一些不再需要的资源筛查等分析工作。
请输入图片描述

1.3.2 游戏帧率统计

另外一块需要持续关注的是游戏在设备上的运行帧率。
请输入图片描述

我们会分别在高配和低配机器上对战斗外、不同的战斗类型进行帧率的统计,并和之前的记录进行对比。

1.3.3 具体资源的检查

在具体资源的检查方面,由程序提供尽量简便的测试和统计工具,由QA进行自动化的检测,主要包括场景资源检查、场景合法性检查、技能特效检查、UWA GOT性能数据等部分。
请输入图片描述

场景部分主要是统计Batch和面数。我们制作了一个工具,按照填写的检查点在默认镜头下统计四个方向的数据,同时截图记录。这个工具也提供了跳转到指定坐标直接查看检查的功能。
请输入图片描述

前文已经介绍了场景的合法性的部分,这里是由QA每周进行一次合法性检查,对于不合法的场景反馈给美术进行修改。
请输入图片描述

特效检查主要集中在Drawcall、描述以及粒子数量这几个比较常规的方面,同样会列出不合格的特效让美术修改。
请输入图片描述
请输入图片描述

对于特效的Overdraw的统计,我们是针对技能进行的,因为技能释放过程中会有镜头的轨迹,因此这种方式更加合理。工具的功能是逐个释放技能,然后记录技能释放过程中最大的那帧Overdraw的数据和截图,方便美术排查。
请输入图片描述

我们也让QA团队在时间充裕的情况下使用UWA的GOT工具进行性能数据的记录,方便程序进行对比和检查。对于内存的统计数据也会从Overview测试中获取。

PPT截图的右下角是工具的制作者在我们组内的外号。标注在这里也说想说明这些工具的开发工作是整个团队一起来分工协作完成的,而不仅仅是分享者的功劳。

1.4 小结

第一部分的最后,我们做一下小结。经过规范制定、规范执行以及资源检查这些步骤,美术资源的优化就形成了一个闭环,这个闭环中的各个环节是由不同职位的同事协作来完成的,程序在其中起到了穿针引线的作用。在规范制定阶段,是由程序和美术主导,同时注意从策划和运营等处收集游戏设计的信息;在美术制作阶段的主角是美术,程序则为美术提供更好用的检查工具辅助美术自查;资源检查阶段由QA主导,程序负责提供便利的自动化检查工具,美术则需要对发现的问题进行修正,同时这个阶段的检查结果也可能反馈到规范制定的内容上,对一些规范细节可能会进行修正和调整。
请输入图片描述


2. 程序代码优化

在聊完美术资源的优化之后,我们回归到程序的部分,从一个比较宏观的角度来看一下程序代码的优化部分。
请输入图片描述

Donald Knuth在他的一篇文章里说:

We should forget about small efficiencies, say about 97% of the time:
premature optimization is the root of all evil. Yet we should not pass
up our opportunities in that critical 3%.

这段话是那句很有名的——“过早优化是万恶之源”的出处。首先我很赞同这句话,在没有必要的情况下进行盲目的优化浪费时间而且有可能有负面作用,但是正如这句话后半句所说,在少量的情况下,我们也不应该放过那些会产生严重影响的机会,而这些地方,往往是踩了坑之后才知道的,比如前文所说的美术大量使用四方连续的贴图这样的例子。所以这部分我想聊的主题内容着眼在“过早优化是万恶之源”的另一面,在程序中越早关注我觉得收益越高的部分。

底层模块。这点比较容易理解,即便是在做好层次划分的情况下,越底层的模块对于上层的影响越大,因此早期花费较多的时间和精力在重要的底层模块上,对于后期的优化可能会有意想不到的收益。
代码质量。我觉得保证团队的代码质量,对于优化和开发效率有着潜移默化的重要影响。
全员参与(补)。在一些团队里,优化主要由那么1-2个资深的同学主导进行,而在我们的团队里,优化的工作是一件全员参与的事情。
请输入图片描述

2.1 底层模块

首先来看下底层模块,这一部分的重要性不言而喻,我想举两个例子来说明一下它们设计得好坏对于我们项目的影响。
请输入图片描述

其中一个是我觉得我们做得比较好的地方——Lua与C#的职责划分,另外一个是我们踩了很多坑的资源管理模块。

2.1.1 Lua与C#的职责划分

Lua与C#的职责划分可以使用这样一张图来描述:
请输入图片描述

这里涉及到的语言有C#、Lua和C三种,在项目最初期,我们就决定将大部分的业务逻辑放在Lua层来做。这样做的原因和大部分使用Lua的项目一样——为了保证大部分的业务逻辑可以被Hotfix以及Patch更新。同时我们客户端团队有大量的Python脚本使用经验,因此对于Lua来说上手也是没有特别大的问题。

当决定了核心的业务逻辑存放位置的时候,数据的存放也就比较明了了。我们遵循的一个设计理念是——让数据尽量靠近它的使用者。

数据越靠近最终的使用者,中间需要进行转换的CPU和内存消耗就越小。因此我们使用Lua这种原生就支持数据存储的方式,即Lua Table的方式来存储客户端使用的数据。
接下来是网络部分。在项目最初的时候我也考虑过是否应当将网络放置在C#层,因为对于Unity引擎来说,C#是更加原生的语言,对于一些库的支持也特别方便。但是经过一些思考和讨论之后,我们决定将网络放在C层,这样做的原因和数据放置在Lua层是一样的。因为对于客户端来说,网络是数据的来源和发送者,相当于一个数据源的角色,因此也应该更加靠近它的使用者。不放在Lua层的原因是网络传输中还是有不少计算量的,加密、内存拷贝等,因此放在C层效率更高。同时我们在C层集成了一些Lua中缺少的扩展库。

选择放在C#层的部分有这么几种:

引擎功能,这个毋庸置疑,Unity本身就是通过C#来提供引擎接口的;
计算密集型逻辑,对于一些可能涉及到CPU消耗的计算密集的逻辑,封装成C#的接口供Lua调用;
Tick逻辑,即一些需要持续Update的逻辑,放在C#层通过Component的Update函数来实现;
交互频繁的逻辑,一些需要Lua和C#频繁交互的逻辑放置在了C#层来实现,同样封装成接口供Lua调用。

这样的设计依据另外一个设计理念——将Unity作为引擎层来使用,让C#层尽量少地关注具体的业务逻辑。

在这样的设计下,C#和Lua之间的交互就非常清晰:

C#通过tolua# warp出来的接口让Lua调用,Lua是逻辑的驱动者;
C#提供每帧一次的Update/LateUpdate调用,具体内部需要的分发由Lua自己来做,大量的间隔逻辑通过Timer模块来实现,减少每帧的tick逻辑;
C#对于Lua的感知仅仅是一些异步操作的回调,比如按键点击事件、异步加载完成的回调逻辑。

在这样的设计之下,UWA对我们项目进行深度测试的时候给出的测试报告结论如下:
请输入图片描述

UWA团队告诉我们在他们测试的重度项目里,这个数据已经是非常好的了,我们项目在这块也只使用自己开发的调用次数统计工具进行常规的优化,在项目后期并没有花费太多的时间。
请输入图片描述
同事增练开发的调用次数统计工具

这里提一下跨语言编程的时候关于对象/资源生命周期的设计理念:谁创建谁销毁。

简单明了,如果一个对象是由Lua创建的,那它一定要由Lua来显示地负责销毁;而如果一个对象是由C#逻辑来创建的,那一定由C#来进行销毁。只有这样才能够避免生命周期错乱导致的泄露或者提前销毁的错误,对于泄露的检查也更加明了。

2.1.2 资源管理模块

我们项目中的资源管理模块是我觉得由于最初设计不够导致后期踩了很多坑的一个部分,其中一个表现就是每次游戏要进行测试上线之前都要花时间解决加载模块的各种奇怪问题。我们熬夜修复的问题有这么一些:
请输入图片描述

最初的版本因为一些迭代导致资源的引用计数存在问题,出现了Asset被卸载了但是又被使用的情况,再次尝试加载资源就报错了。后来又发现非常严重的内存泄露,表现是几乎所有的资源都残留在了内存=_=……为了修复泄露问题,我们将底层AssetBundle的管理从原来的Unload(false)修改为了Unload(true),虽然解决了泄露问题,但是需要上层逻辑有一些迭代工作。后续我们还尝试处理同一个资源在异步加载过程中有同步加载请求的问题。

现在回头来看,对于资源管理模块可以进行反思的内容有如下几点:
请输入图片描述

最初的时候由于团队内对于Unity的经验不是很足,面对资源管理模块这个非常重要的部分,想法是借助比较成熟的开源框架来弥补经验上的缺失。所以在大致了解了基本原理之后,选择了KSFramework这套开源框架。它对于资源管理模块有一套基于Loader设计的封装,我们又根据自己的需求和发现的问题进行了一些迭代工作。在初期编辑器模式下,这套东西帮助我们快速建立了Demo和推进前期功能的开发,但是也隐藏了很多设备上的问题。这应当说是非常标准的技术债务,只是没想到需要付出这么高昂的利息。

在后期的维护中,因为技术团队的扩张和一些“不可抗力”的原因,这个模块先后经手三个负责人的维护,在交接以及讨论中因为理念不同也产生过一些设计上的误解,埋下了一些问题。

最后,因为编辑器模式下没有使用异步加载的方式,因此运行逻辑和设备上是不同的,导致很多异步的问题在真正进行大范围的真机测试的时候才暴露出来,需要在比较高压的条件下进行修复,带来了很多挑战。

最后,想说的一个点是——即使面临很大的压力,对于一些奇怪的问题,不要尝试用一些临时手段进行掩盖和容错的方式进行处理,而是尽量地去找到问题产生的根源,从根本上进行解决。

有时候在没搞清楚根本原因的情况下贸然通过“补洞”的方式来进行问题的修复,可能会把坑埋得更加深,让问题更难复现和排查。我在项目中就经历过这样的情况,也都是血与泪的教训。

2.2 代码质量

代码质量的重要性我依然想讲一个我在《无尽战区·觉醒》这样一款手游开发项目中的例子来进行说明。
请输入图片描述

在项目中后期,我们进行Python层性能优化的时候发现:dict这样的属性访问占用非常高。用过Python的同学可能都知道这是Python中进行属性访问的方式。排查调用源发现很多优化的点都是类似上面这样的局部变量的优化。(图中的代码只是示例,并非真实代码。)

而对于每一个入职的同事,在进入公司的Python课程里,都会学习到在脚本语言中尽量使用局部变量来进行性能优化的方法和原理。然而在真正的项目开发中,还是会有很多人忽略这种优化,这是代码质量偏低的一种表现。

在接下来的三四天时间里,我们不断地Profile、修改,对dict性能消耗比较大的地方使用局部变量的方式进行性能优化之后,整个脚本的性能有了大约10%-20%的性能提升。这是非常大的一个优化了,而且完全是无损的优化。如果我们的开发人员可以在日常的开发中就注意维护代码质量,对于这些优化时间的消耗就可以节省掉不少。

在我看来,在项目开发中可以提升程序团队的代码质量的方式包括如下几个方面:
请输入图片描述

针对性的培训和定期的技术分享。技术分享可能会花费挺多的时间,但是在时间相对宽松的研发期坚持进行技术分享还是会给团队带来有多正向的收益。我们一年多的创业时间内,技术分享大约做了十几场,虽然和大厂的分享相比不算很多,但是在促进团队技术进步、提升代码和设计质量等方面还是起到了很好的作用。
代码Review。也有不少人和我讨论过在团队内进行大范围的Code Review的可行性。首先Code Review对于提升代码质量肯定是有很大帮助的,但是从我个人的项目经验来说,要在手游这样一个需要快速开发迭代的团队里推行严格的Code Review代价还是非常大的。比如工作压力比较大的情况下,我们一个同事可能会在一天产出上千行的Lua代码,如果想要另外一个同样有这样大工作压力的同事抽出时间来进行完整的Review,几乎是一件不可能的事情。因此我们选择只在关键节点进行Review,包括核心代码和线上Bug修复代码,以及新同事入职的第一个月提交的代码。我们有过一次集体Review和迭代的过程,对于项目中会由多人共同维护的一段逻辑,大家都花时间进行迭代,然后分享自己迭代的思路。这种方式虽然会花费团队挺多时间,但是偶尔针对特定代码进行还是比较有效果的,可以统一大家对于关键部分代码的设计理念和使用方式。
静态分析工具。这块我们在使用的有LuaChecker和UnityEngineAnalyzer,针对代码进行检查,可以发现一些优化的点。

2.3 全员参与的优化(补)

我们客户端程序团队在进行优化的时候和一些团队不同的做法是大家都针对自己负责的部分进行优化。这样做和团队自身的特点有关,我们客户端团队对于一个创业团队来说算是经验和技术能力都不错的一个团队,每个成员都有多年的游戏开发经验。因此每一个同事都负责一些比较底层的模块,也会负责各个玩法系统的开发,是一个纵向的结构。总结起来,让所有同事都参与优化的好处主要有:

让团队中的每个人建立优化意识;
每个人作为自己负责模块的优化负责人;
组织专门的优化周期,横向对比,互相学习。

请输入图片描述
请输入图片描述
组织专门进行优化周期的Evernote记录


3. 团队开发效率优化

终于来到第三部分,也就是之前说的看上去和性能优化并没有直接关系的团队开发效率优化。
请输入图片描述

在聊这部分之前,我想让读者思考一个问题——我们为什么要做优化?
请输入图片描述

是为了让游戏的运行更加流程?让游戏更加省流量?更省电?让游戏包体更小?这些都是我们进行优化的目标,但归根结底,我们做这些优化的目标都是——为玩家提供更好的游戏体验。
请输入图片描述

所以在我看来,如果一个优化,无论使用多么高超的技巧,如果它的优化结果无法直接或者间接地被玩家感受到,那这个优化可能就只是一个程序员的“自嗨”,无法为游戏提供真正的价值。反过来说,如果我们可以优化团队的开发效率,让团队有更多的时间来开发新的功能、制作更多的游戏细节,那对于游戏来说也是一种优化。

因此在我看来,进行团队效率的优化是一件非常重要的事情,也是程序的职责之一。我主要想从这样三个方面来聊一下如何进行团队开发效率的优化:工作流的构建、程序团队、策划团队。
请输入图片描述

3.1 工作流的构建

我觉得在项目中构建更好、更顺畅的工作流可以很大地提升整体团队的工作效率。我以我们团队现在一个功能的完成流程为例来分享一下我们团队使用的工作流。
请输入图片描述

策划提前和程序、美术沟通需求的可行性,在可行性确定之后,通过Redmine这样的管理软件提单,将需求详细地描述在任务单里;
我们在Redmine中集成了Webhook的功能,当有任务提出的时候,Redmine会通过钉钉的接口通知到对应的程序;
程序根据自己手头的工作安排进行排期和功能实现,当任务单完成并进行自测之后,会将代码提交到svn上,同时将Redmine上的单子修改为“已完成”的状态,状态的变更会同样通知到相应的策划和QA;
SVN通过SVN hook的方式,自动触发Jenkins的Lua代码编译指令,Jenkins调用我们部署在公司内网的一套分布式打包服务,进行脚本编译。我们团队中只有程序有Lua代码的svn访问权限,其他职位统一使用编译好的Lua bytes code。 当打包完成之后,分布式的打包服务会调用钉钉接口将完成消息通知到特定的群里。
策划需要进行导表、更新服务器,或者QA同事需要进行安卓/iOS打包的时候,都是通过Jenkins进行请求,Jenkins继续调用分布式打包服务进行打包,并将结果通知到群里。

请输入图片描述

Jenkins上的部分服务

对于Jenkins部分,提醒一下要做好权限控制,对于其他职位可能需要的,尽量避免参数式的执行方式,而是以多个任务的方式提供。而程序部分则可以尽量灵活地使用参数进行构建。对于发布版本打包、分支创建等功能,通过权限控制不要让策划/美术/QA误操作点击到。

我们的分布式打包服务是基于Python构建的,通过简单的RPC服务进行内网跨机器的互联,通过argparse模块进行参数化的提供,方便扩展:

def ParseArgs(args):
    parser = argparse.ArgumentParser(description = 'Build App')
    parser.add_argument('-p', '--platform', choices=('android', 'ios',), required = True)
    parser.add_argument('-c', '--channel')   #all, xiaomi
    parser.add_argument('-b', '--build-type', choices=('dev', 'pub',), default="dev")
    parser.add_argument('-sp', '--spmark')
    parser.add_argument('-hm', '--headmark')
    parser.add_argument('--non-sdo-server', action = 'store_true')
    parser.add_argument('--nopatch', action = 'store_true')
    parser.add_argument('--onlyab', action = 'store_true')  
    parser.add_argument('--uwashipping', action = 'store_true')     #单独为uwa测试准备的发布参数,临时添加
    parser.add_argument('--make-base', action = 'store_true')
    parser.add_argument('--il2cpp', action = 'store_true')
    parser.add_argument('-xp', '--xcode-profile-type', choices=('development', 'addhoc', 'appstore',), default="development") 

    buildArgs = parser.parse_args(args)

我觉得这样的工作流的好处主要有:

程序将更多的事情推出去,交给工具,自己可以更加专注在程序开发的工作上;
其他职位拥有更多的自主权,在不需要程序参与的情况下可以完成自己的很多工作;
通过钉钉这样的IM的通知功能,将轮询的消息变成通知,不再需要等待和关注Jenkins任务的完成进度,完成之后自然就会收到通知。

3.2 提高程序开发效率

这块基本都在PPT里了,不赘述了,其中调试工具部分再次推荐一下:Hdg Remote Debug这样的设备调试工具,关于Lua的部分在3月份的博客中已经说得非常详细了,也不再重复。
请输入图片描述

3.3 策划工作效率优化

策划工作效率的优化部分想讲两个切身经历的事情。一个是非常小的一个优化,帮助策划实现NPC坐标从Unity中拷贝到Excel中。
请输入图片描述

我们因为开发周期比较紧,而且服务器需要一些NPC的位置数据做验证,因此没有在Unity内部为策划实现NPC编辑器,而是需要策划手动去Excel表里填写。这里就有一个填写坐标的过程,最初的时候策划手动填写非常费时间,而且容易出错,后来帮助他们实现了一个点击GameObject节点拷贝坐标到粘贴板的功能,策划使用后表示极大地提升了填写NPC表格的工作效率。

有时候程序只需要通过很简单的代码就可以帮助其他职位的同事解决一些工作中的痛点,提高工作效率。
请输入图片描述

第二件事情是之前在大公司工作的时候的一个亲身经历。当时在带新人做mini项目,一个新人策划就在公司的KM知识分享平台上提了一个问题——他表示现在的策划填表的工作效率很低,需要经历这样几个复杂的步骤:
请输入图片描述

在Excel中编辑数据,然后提交到SVN上,通过导表将数据转换成程序代码读取的资源,然后更新服务器,更新客户端,启动客户端连接服务器才能查看结果,这些步骤要花费大约10-20分钟的时间。他问能否编写完数据之后就可以直接在游戏内看到结果?

当时的我作为自以为在游戏行业已经有几年工作经验的“过来人”,看到新人策划有这样的疑问,心里其实是有一些嘲讽的。所以去“耐心”地回复他:对于客户端来说,可以做到本地导表然后不重启客户端就可以直接Reload数据查看结果,但是如果你不把数据上传到svn上,服务器如何知道你本地修改的结果?这就像那样一个笑话:

“是这样的,张总,
您在家里的电脑上按了ctrl+c,然后在公司的电脑上再按ctrl+v是肯定不行的。即使同一篇文章也不行。不不,多贵的电脑都不行。”

这个笑话后来的结果是自己成了一个笑话,因为虽然时代的发展,网络硬盘等云服务的普及,也有了跨电脑进行粘贴拷贝的功能……张总不再需要很贵的电脑就可以实现自己的操作。

这个故事的发展和这个笑话有些相似,在大约半年之后,我和工作室的另外一个同事将rpyc这样一套中间件引入公司并基于它实现了跨进程的外挂式编辑框架。基于这套框架就实现了策划在编辑器内编辑完数据,只需要点击重载数据的按钮,就可以自动更新本地的客户端和指定ip的一台服务器中的数据,不再需要提交到svn,甚至不需要重启客户端就可以看到修改之后的结果。
请输入图片描述

经过样的改进之后,之前需要10-20分钟左右时间的操作,现在只需要2-5秒就可以实现,极大地提升了策划的工作效率。我和那位同事也因此拿了当年公司内部的技术分享奖。

这个故事对于我的触动还蛮大的,因为最初我所嘲讽的一个新人的想法,最终由我和另外的一个同事一起进行了实现,这对于我来说也是一种讽刺。因此在之后我再听到策划或者其他职位的一些看上去“异想天开”的想法的时候,不会急于反驳或者指出其中的漏洞,而是先想想是否自己的思路被自己了解的技术所禁锢,是否有别的方式可以真的实现这些想法。

通过这两个故事我想表达一个观点,对于程序在团队效率优化方面应当承担什么样的角色?借用《蜘蛛侠1》里非常有名的那句话来说——能力越大,责任越大。
请输入图片描述

因为程序是整个团队中最了解技术和开发的人,也最有能力开发一些工具或者引入一些方法让整个团队的工作效率得到提升,因此也应该肩负起相应的责任。


4. 总结

最后,我们聊了这么多,进行一些总结。

请输入图片描述
我在游戏行业里也做了五六年,特别是自己在创业的这一年多的时间,让我更加深切地感受到游戏开发非常符合这样的冰山理论。
请输入图片描述

浮在冰面上的这一部分是玩家可以感受到的游戏内容,比如精致的美术资源、有趣的玩法,而在这之下,有更多无法被玩家直接感受到的内容,比如被迭代掉的玩法。而今天我们所聊的这些优化的内容,比如美术规范、代码质量、团队的工作流构建,它们大都是水面以下,无法被玩家切身感受到的部分。但它们又是如此地重要,是整座冰山不可或缺的一部分。

就像我之前所说,通过刚才的分享大家应该也可以感受到,这些优化的内容非常的琐碎繁杂,就像散布在各个地方的一个又一个点,是团队的协作让这些点可以连接成线,形成类似于美术资源的规范制定、规范执行和规范检查这样的闭环,而在整个游戏的开发周期过程中通过团队持之以恒地去做这些事情,让这些线连接成一张大网,将水聚拢在周围凝结成冰,托起了整座冰山,使得海面上可以被玩家感受到的内容越来越多,这就是我眼中基于团队的持续优化之道。

最后的最后,我还是想把我在上次分享中也说过的一句话送给大家。

这一年多的创业经历让我更加深刻地体会到游戏开发是一件艰难而且辛苦的事情,有一些朋友或者同事也找我聊作为一个程序进行游戏开发的迷茫,我自己内心也曾有过彷徨和纠结。因为很多事情太过琐碎,带给我们的成就感可能也会偏低。但是我也发现,现在做过几年游戏行业之后,依然留在游戏行业中的人心中都有着对于游戏发自肺腑的热爱和激情。它们或从小就喜欢游戏,或曾经被游戏感动过心中最为柔软的那个部分,在游戏行业内坚持做这些着看似平凡的工作。

所以,我想把这句话送给所有依然在坚持的游戏开发者们——不忘初心,不愧平凡,相信通过团队的协作和坚持不懈的努力,可以给这份平凡以不凡!

请输入图片描述
谢谢!