游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2387|回复: 0

基于团队的持续优化之道(二)

[复制链接]

8717

主题

8783

帖子

1万

积分

版主

Rank: 7Rank: 7Rank: 7

积分
11952
发表于 2018-5-28 10:18:53 | 显示全部楼层 |阅读模式
文/侑虎科技

基于团队的持续优化之道(一)

2. 程序代码优化

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

259.png

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个资深的同学主导进行,而在我们的团队里,优化的工作是一件全员参与的事情。

2665.png

2.1 底层模块

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


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

2.1.1 Lua与C#的职责划分

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

2827.png

这里涉及到的语言有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对我们项目进行深度测试的时候给出的测试报告结论如下:

21850.png

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

21935.png

同事增练开发的调用次数统计工具

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

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

2.1.2 资源管理模块

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

22211.png

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

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

22463.png

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

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

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

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

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

2.2 代码质量

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

23107.png

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

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

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

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

23535.png

针对性的培训和定期的技术分享。技术分享可能会花费挺多的时间,但是在时间相对宽松的研发期坚持进行技术分享还是会给团队带来有多正向的收益。我们一年多的创业时间内,技术分享大约做了十几场,虽然和大厂的分享相比不算很多,但是在促进团队技术进步、提升代码和设计质量等方面还是起到了很好的作用。

代码Review。也有不少人和我讨论过在团队内进行大范围的Code Review的可行性。首先Code Review对于提升代码质量肯定是有很大帮助的,但是从我个人的项目经验来说,要在手游这样一个需要快速开发迭代的团队里推行严格的Code Review代价还是非常大的。比如工作压力比较大的情况下,我们一个同事可能会在一天产出上千行的Lua代码,如果想要另外一个同样有这样大工作压力的同事抽出时间来进行完整的Review,几乎是一件不可能的事情。因此我们选择只在关键节点进行Review,包括核心代码和线上Bug修复代码,以及新同事入职的第一个月提交的代码。我们有过一次集体Review和迭代的过程,对于项目中会由多人共同维护的一段逻辑,大家都花时间进行迭代,然后分享自己迭代的思路。这种方式虽然会花费团队挺多时间,但是偶尔针对特定代码进行还是比较有效果的,可以统一大家对于关键部分代码的设计理念和使用方式。

静态分析工具。这块我们在使用的有LuaChecker和UnityEngineAnalyzer,针对代码进行检查,可以发现一些优化的点。

2.3 全员参与的优化(补)

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

让团队中的每个人建立优化意识;

每个人作为自己负责模块的优化负责人;

组织专门的优化周期,横向对比,互相学习。

24399.png


组织专门进行优化周期的Evernote记录

3. 团队开发效率优化

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

24481.png

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

24515.png

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

24600.png

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

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

24833.png

3.1 工作流的构建

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

24918.png

策划提前和程序、美术沟通需求的可行性,在可行性确定之后,通过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模块进行参数化的提供,方便扩展:

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

  15.     buildArgs = parser.parse_args(args)
复制代码

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

程序将更多的事情推出去,交给工具,自己可以更加专注在程序开发的工作上;

其他职位拥有更多的自主权,在不需要程序参与的情况下可以完成自己的很多工作;

通过钉钉这样的IM的通知功能,将轮询的消息变成通知,不再需要等待和关注Jenkins任务的完成进度,完成之后自然就会收到通知。

3.2 提高程序开发效率

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

26836.png

3.3 策划工作效率优化

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

26915.png

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

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

27142.png

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

27244.png

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

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

“是这样的,张总,

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

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

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

27847.png

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

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

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

28163.png

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

4. 总结

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

28262.png

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

28322.png

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

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

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

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

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

28999.png

谢谢!

关于UWA:

由侑虎科技开发的游戏/VR应用性能优化平台,目前提供 1)性能诊断与优化 2)资源检测与分析 3)UWA GOT 三大工具,帮助开发者在短时间内大幅度提升性能表现;同时其搭建的知识分享的博客和答疑解惑的互动平台使广大开发者收益。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-2-24 12:25

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表