写在前面1:
部分是我没接触过但是听说的,绝大部分是我个人遇到的并且解决的,还有一部分是看的其他人写的感觉之后能用到的.如果有看到错误的地方,希望能指明,感激不尽
写在前面2
这篇是个人在工作了两年多之后的一些开发感悟。有关于工具使用的,有关于引擎使用的,有关于
每个人都有各自不同的开发特点,但是我这篇所说的能囊括大部分的通用技巧。希望大家在开发过程中能去粗取精,合理应用。
写在前面3:
上一家的公司虽然只有两三个开发,但是无论是在工具的使用上,还是代码的开发整洁程度抑或是办事效率以及解决问题的效率都是我迄今为止遇到的最先进的。而新进的公司是一家大公司,虽然开发人员很多,但是和上一家小公司比起来对代码框架的使用以及业务的开发还有办事效率上,都是一坨狗屎。也因此产生了写这篇的冲动。
1. 代码风格:
(1)如果是刚毕业的实习生,最好有事没事读一下代码命名规则方面的书。记住了,代码是给人看的,然后才是给机器看的(当然使用汇编语言或者更底层语言的可以忽略),所以首先要保证你的函数
、成员变量
、类型名称
、命名空间
要能简介易看懂,如果无法做到简洁明了,那么就选择写注释,要去写明这个名称所代表的意义以及方法所要做的事情的简述。
(2)如果使用C++、C#等使用强类型语言开发程序,那么大家肯定使用的是Virsual Studio(装逼的也不会看这种文章),所以在写完之后最好 Ctrl+K,D
整理一下代码,使其符合VS的排版规则。用什么语言就要像什么语言(比如在Java/JavaScript/TypeScript中,方法后面的左括号不用换行,这是人家Java的排版规则),但是一旦你使用C系列语言进行开发,那么请一定换行,不要觉着无所谓,Python的创始人嫌弃人家的括号才让Python面世,但是我不觉得大部分资深程序员觉着这是个好的想法。括号的作用是为了让代码块状化,也就是为了更清晰,Python 虽然有 def
和空格配合,但是谁用谁知道。当然现在的VS Code很方便。
(3)在字符串的使用上,如果是常量,能不能将其归到单独的静态类或者写成静态变量,最好再加上限定符(比如const[编译时常量C++/C#中都有]
, readonly [运行时常量仅在C#中]
)。不要再代码中到处留种,看上去像大姨妈一样。我是一个有超级强迫症的人,绝对看不惯一片红的这种代码存在(当然有人改变了编辑器的主题就另说)。
(4)不要在代码中使用各种魔术数,所谓魔术数就是指不知道上下文还不知道这个数值代表的啥意思的数值,如果有大量的代码中用到同一个意思的数值,最好创建个成员变量或者局部变量,创建方式同字符串的方式。如果代码从头到尾都是你一人开发一人维护,那你也要考虑将来你不维护了别人也要能看得到。做人留一线,免得被鞭尸。
(5)设计层面一定要遵循一个类只保留与这个类相关的方法和变量(这个也决定于你使用的什么设计模式,以及开发方式,所以仁者见仁智者见智吧),一个方法只做一件事,这个是慢慢培养起来的习惯,这个习惯养成之后,你会发现写代码,查BUG真的很爽。
2. 其他杂七杂八整合:
Unity
-- Unity运行时库是定制版的Mono,只和.net framework在一定程度上兼容,参考Unity3D引擎简介
-- Unity3D 不要多人同时修改Unity中的场景文件,一旦冲突将很难合并
-- Unity3D中的资源引用是靠和文件名相同的meta文件来实现的,只有用到的文件才会打包到最终的产品中。但所有Resources目录中的资源(包括子目录中的Resources目录)都会不分青红皂白的全部打包到最终产品,以供Resouces.Load这样的接口通过路径来在运行时动态加载。所有没有运行时动态加载需求的资源都不要放到Resouces目录中
-- Unity中的Invoke不能调用自己所在的方法
void test()
{
Invoke("test",1);
}
替换为:
void test()
{
Invoke("test1",1);
}
void test1()
{
test();
}
-- Unity修改好脚本之后最好用Ctrl+Shift+B编译一下,有时候就算是save但运行的还是原来的逻辑
-- Unity工程里面文件路径的'/'和''有区别,加载文件的路径是/,创建文件最后是用'',用Path.Combine拼接创建文件的路径是''
-- Unity 在Windows环境下大小写不敏感,截取字符串要注意
-- Unity里面Resources.Load加载prefab的时候不能包含文件的扩展名
-- Unity中不要用Debug.LogError打印普通日志
-- 采用PrefabLoader加载预设,设置父节点时,预设中所有子节点的Depth会自动适配,但是active=false的节点,不会自适应。
-- Unity重命名脚本导致挂载其预设脚本丢失问题,将脚本从预设上卸载保存,重命名脚本保存后再挂上去,再从预设上卸载该脚本保存,再将该脚本重命名回去,再重新挂到预设上去;
-- Unity5的运行时在解析utf-8编码的xml时会把前面的BOM直接当文本解析而报错,此时应采用无BOM版本的utf-8编码
-- Unity3D性能优化:DrawCall优化
-- Unity设置自定义RBG颜色是0-1
的float
值
-- Unity粒子中的IsAlive(withChildren = true)
会对下级gameObject
进行遍历并且递归结束之后又对返回列表做一次元素复制List.ToArray()
,然后再以transform.gameObject.GetComponent<>()
,而不是transform.GetComponent<>()
来获取子级组件,在Update
中使用极其消耗性能,有实验对一个带2个子ParticleSystem
共3个ParticleSystem
组件的游戏对象进行IsAlive(true)
判断810241024次用时为3900ms,而单独取出判断仅为65ms.好的解决办法是对所有的particlesystem
建一个链表进行统一的处理;次一些的方法是对该粒子的带particlesystem
的祖先对象建一个链表进行处理,然后以IsAlive(false)
进行判断,当它死亡时认为其次级粒子也应全部死亡
-- NGUI中多个Grid或Table组件嵌套着并且动态更新节点时,不能在同一个渲染帧更新,这会引起位置错误,应该从子组件往父组件逐帧更新
-- JSON .NET For Unity具有比JsonFx For Unity3D更好的平台兼容性
-- Unity Native2D中的Collider点选射线是从屏幕点击点开始的一条直线,而3D Collider中的点选射线是从摄像机发出的射线
-- Unity在Animator里面只有一个动画的情况,如果想要重复播放这个动画需要添加一个空白动画与该动画衔接起来,这样每次调用播放动画的时候可以从头开始播放,因为Animator会记录当前动画的状态,如果不切换动画状态就会出现动画一直停留在该状态的最后一帧
-- Unity GetComponent是一个比较消耗时间的操作,对于频繁调用的地方,应该在Start方法中获取并缓存,否则可能会造成严重的性能损伤
Lua/Cocos
-- lua中table.sort要求table的下标从1开始且连续,否则会报错
-- cocos lua中的json解析器对"/"这样的字符串转义无法正常识别。
-- cocos android编译报错时,可考虑删除proj.android/obj目录修复
Android NDK: WARNING:D:\gxtime\code4\appbuilder\gcmd\cocos2d-x/cocos2dx/Android.mk:cocos2dx_static: LOCAL_LDLIBS is always ignored for static libraries
make.exe: Entering directory `D:/gxtime/code4/appbuilder/gcmd/cocos2d-x/projects/zlsg/proj.android'
make.exe: *** No rule to make target `D:\gxtime\code4\gamecmd\gcmd\cocos2d-x/projects/zlsg/Classes/AppDelegate.cpp', needed by `obj/local/armeabi/objs-debug/zlsg_logic/Classes/AppDelegate.o'. Stop.
-- cocos Android编译如果出现如下报错,请确保Resources目录下没有中文目录或文件:
BUILD FAILED
C:\android\sdk\tools\ant\build.xml:932: The following error occurred while executing this line:
C:\android\sdk\tools\ant\build.xml:950: null returned: 1
-- cocos中可采用CCDirector:sharedDirector():getScheduler():setTimeScale(2)
整体加快游戏时钟速度
-- cocos的音量控制,在Android系统上正常。在pc和wp上,只有0/1的区别,没有音量大小的区别。音量函数:引擎底层,setBackgroundMusicVolume();需要自己封装。
-- Android平台不支持路径中的""
-- cocos中的FreeType字体如果加载不正常,可以先用其他字体替代检测,有可能是字体文件本身和cocos不兼容
-- cocos不支持按钮长按时间,需要通过定时器手工检测触发
-- cocos.splitUTF8返回utf8字串的字(英文或者中文)个数,并返回每个字列表,返回相对英文的个数(中文算两个英文)
-- 实现cocos用的富文本排版算法/控件cocos.formatOneLine
-- cocos在Windows和Android上会启用luajit,如果发现一些边缘性的lua特性有差异可能与此相关
-- 使用cocos制作时,界面显示的CCLayer和界面上堆积的CCNode最好是建立父子关系,这样不管是在模拟器还是在手机上都不会出现位置偏移
-- cocos跨平台游戏需要注意cocos线程
和主UI线程
以及平台线程
之间的冲突,否则会出现莫名其妙的Crash
-- cocos中ScrollView是以左下角为原点排版的
-- cocos多个Action同时运行最好能保证相互正交
-- cocos使用replaceScene()
替换场景后,在同一帧内使用getRunningScene()
获取到的还是老场景。 解决方法: 1. 将新场景传出去。 2. 使用CCDirector::mainLoop()
强制刷新一帧
-- lua里面的require一定要写全路径(获取路径+相对路径),否则在非devmode下加载会出问题
-- lua中创建cocos中的结构体后,需要手工释放,否则会有内存泄漏。比如ccp
-- lua中谨慎使用new
,否则必须手工采用release
以避免内存泄漏;推荐使用new_local
,lua会自动处理回收。
-- lua中谨慎使用retain,否则很难保证C++对象被正确的release,会造成内存泄漏
-- lua是为单线程设计的,多线程访问lua虚拟机需要加锁。开多个lua虚拟机会造成过多的资源损耗,另外不同虚拟机之间如何通信也是一个复杂的问题。
-- tolua++使用时,尽量将C++类的析构函数导出给lua,以避免被作为原生内存暴力free,而未调用析构函数。传送门
-- lua的变量插入符。在字符串中加入,可以将其后的一个英文单词解释成变量。使用string.gsub()函数替换变量,并返回替换后的字符串。 如:
local from = "我想做一个$goodman,请$leave我。"
local t =
{
goodman = "好人",
leave = "放过"
}
local to = string.gsub(from,"%$(%w+)",t)
print(to)
-> 我想做一个好人,请放过我
-- lua中除法运算时除数为0,运算结果的值为NaN
-- lua数组索引的时候注意存的时候存的是数字还是字符串数字
-- lua require一个脚本文件script.lua
后package.loaded["script"]
会被设置一个非nil
值,如果script.lua
以module
封装过,package.loaded["ScriptModule"]
也会被设置一个非nil
值,那么脚本文件script.lua
已经require过了想重新require,就需要设置package.loaded["script"] = nil
以及package.loaded["ScriptModule"] = nil
-- lua遍历,如果很清楚目标容器是致密数组,就用ipairs,而不是pairs。一方面是性能问题,另外也是在明示所用容器的类型。而且这个在后期调整会比较难,需要根据代码去仔细推敲类型,建议编码时就明确搞定
-- lua里#(T)
和table.getn(T)
等价,得到的是数组中元素的数量,所谓数组意味着key值从1开始的连续性数据结构,而T={[2]=1}
这样的结构在使用table.getn(T)
时,得到的值只能是0
-- lua math.random(0)
会crash
,math.random(float)
的用法也会有问题
-- lua中一定要注意标识符的正确拼写
-- lua代码要比C++更严格的控制编码风格和代码质量
-- lua中local函数调用应该在定义之后,可通过提前声明一个变量解决,module函数不受此限制。lua的解析上是完全的流模型。所谓的解释型语言的称呼在这儿得到了很好的诠释
-- lua中的函数可以通过闭包传递额外的参数
-- utf-8的中文字占三个字节,luastring.sub截断中文时要注意,截取不对时Windows上和Android上都会报错,但是由于BMFont(cocos)在两个平台上的实现不一样,所以错误的表现会不一样
-- HttpRequest:new之后应该及时进行release以避免内存泄漏,参考
-- Visual Leak Detector是VC下非常不错的内存泄漏检测工具
Python
-- Python对空白敏感。缩进时,空格和tab不能混用。
C#(读Sharp)
-- C# string.Format格式化的时需要对{和}进行转义
-- C#中的容器都是引用类型,和C++ STL中的值类型容器是不一样的,传参的时候不会有拷贝性能损伤
-- C++/Unity中的C#采用Pascal的命名规则,js/TypeScript采用camel的命名规则
-- C#中enum不要当成int的用,enum本身是专有的类型,和C/C++中的区别很大
-- if (npc == null) return;
类似的判空不要放在一行,视觉不敏感,并且无法打断点
-- 《The Swift Programming Language》中文版;和C#语言很像
C++
-- C++中的对象,不能直接用memcpy的方式进行序列化。只有C struct的POD类型才可采用memcpy的方式“序列化”
-- C#会默认对所有字段进行初始化,这个和C/C++有重大区别
-- 不要在C++头文件中采用using namespace ...
,这会引起全局namespace
污染
-- nullptr可以作为std::function的默认值,operator== as default value for std::function
-- C++中的const修饰对复杂代码的理解和维护很有帮助
Type Script/Egret
-- Egret中为了避免resource.json资源文件冲突,请务必在修改资源前pull,修改之后立刻push
-- TS类的成员函数中使用this指针并不总是能够取到类本身,有时取到的是该成员函数本身
-- Egret模板定义的json中,ts文件的顺序标明依赖关系
-- Egret添加新资源(图,特效,字体,声音)后,都要清除浏览器缓存,否则资源无法正常加载
-- Egret中很多情况下确认程序没有问题但运行总报错,就清理一下IE的缓存,特别是涉及渲染的时候
-- 在Egret/egretProperties.json中增加新的模块时,注意文件路径不能直接从windows目录直接拷贝否则本应是"/"粘过来的目录是""会导致引擎编译失败
-- 新导入字体文件时,要更改Egret/web.config文件增加<mimeMap fileExtension=".fnt" mimeType="text/plain" />否则引擎无法正常识别font类型文件
-- Egret引擎的TextureMerger默认勾选切除透明边界,这个可能导致合图后的帧动画中心点偏移
-- Egret性能优化
-- TypeScript得到类名称的方法在代码混淆后会将方法作为字符串返回,而不是方法执行后的结果。 方法:
private getClassName():string {
var funcNameRegex = /function (.{1,})\(/;
var results = (funcNameRegex).exec(this["constructor"].toString());
return (results && results.length > 1) ? results[1] : "";
}
-- js的pretty-print工具: js-beautify
Git (管理工具)使用总结
-- git一定要把autocrlf和safecrlf都设置为false,坑爹的设计会让同一个文件在不同的机器上有不同的MD5码
-- git一次commit/push最好不要超过G的级别,否则工具支持可能会有问题
-- git的子树合并
-- git merge时如果有冲突,不要在解决冲突后只提交“属于自己的部分“,否则其他人的代码就会被覆盖丢失
-- git配置必须用Git Bash生成ssh的key;必须采用ssh的终端而不是默认的认证方式; 需要设置环境变量HOME
=用户文件夹根目录
-- 在cmd命令行中用生成ssh key,发现git的用户文件目录和命令行开头显示的不一样时,就用git bash操作
-- git/svn提交日志,请确保足够的人类可读性
-- 游戏发布后如果有多个分支。改bug的时候如果只在分支改就是给自己埋坑
-- 在代码/提交记录中最好采用真实的身份标识,包括:姓名、email等
工作总结和感悟
-- 代码表格在提交前自己测一下,不要出现太明显的bug。
-- 要善用电话,紧急
的事情不要靠打字解决,节奏、沟通质量和对应对此事的重视程度都会打折扣。
-- 对同一概念、同一物品,沟通时叫法要统一。如果是大型项目一定一定一定要写文档,并且随时维护,当然维护这个文档可能会很费时间,也可能会忘掉,但是想起来的时候一定要去同步相关的修改,否则等项目开发开发到一定程度之后,你会发现末日降临了。
-- 对待变量命名、编码风格、代码组织一定要严肃严谨,这样才能在做重构、设计、架构时也保持严肃和严谨的态度
-- 有意义的消息是最终把游戏逻辑串起来的关键。建议消息定义时、客户端/服务器消息处理时都加上必要的注释,方便维护。
-- 复现步骤复杂的bug,要尝试搭建能快速复现的脚手架。bug的快速稳定反复复现对于解决问题非常关键
-- 开发过程中,主干负责游戏本身相关的功能线。所有和游戏主体无关的,非平台中立的内容均不应该在主干中开发,比如支付、广告或者某具体平台/运营商相关的功能玩法特化。分支开发的过程中,发现游戏本身的bug,修复后需要及时将该条修改合并(cherry pick)到主干。这样后面再有新产品的发布时就不会踩同样的坑了。
-- 文件名是一件非常严肃的事情,包括空格、大小写及其命名规则
-- 复杂的操作流程最好有记录,只是看别人操作一遍很容易随后忘记
-- 对于按照文件名和路径作为资源引用的引擎,第一次把资源加入到游戏中时,一定要特别注意整理好文件名
-- 注释在写的时候需要注重实用性,不要讲废话
-- 尽量不要敲代码到很晚,睡前敲代码影响睡眠
-- 问题想不明白就先缓缓,比如喝个茶或者咖啡或者开个小差
-- 使用移位操作描述状态时,要注意移位操作的可扩展性,避免溢出
-- 如非必要,谨慎采用bit位去表示数据,复杂的移位计算容易引起混乱
-- 所有的文件扩展名均采用小写,从美术接手资源时需要留意
-- 建议在每个文件/类前面添加简单的描述信息或关键字,这对于通过搜索来维护非常重要,特别是应用层的逻辑模块或者界面
-- 在接手/介入新功能开发点时,切记不要从第一行代码开始通读,要以要解决的问题为主线去看。只了解应该了解的信息量,但同时要能有效的解决问题。
-- 考虑“短路”写法,把大量的前件预先处理,可以让代码实现逻辑更清晰,并能有效缩减代码嵌套深度.例如:
void Func()
{
if(a == true)
if(b== true)
ExcuteFunc()
}
可简化为
void Func()
{
if(a == false)
return;
if(b == false)
return;
ExcuteFunc();
}
-- 函数大量的传出参数可能在暗示设计问题,除非必须这么用,否则违反直觉和思维习惯。函数的返回值是很好的通用数据返回渠道,如果有多个返回值,可以考虑Tuple (元组) 的方式。
-- 在复杂度适当的情况下,尽量采用字符串格式化而不是拼接,有利于维护和后期的国际化,无论哪种语言.例如:
var dialogInfo = "是否花费" + info.price + "银币购买" + info.ownername + "的" + tableInfo.strName + "?";
更好的写法:
var dialogInfo = string.Format("是否花费{0}银币购买{1}的{2}?", info.price, info.ownername, tableInfo.strName);
-- 函数之间、类之间最好添加一行空白,单行可实现的函数和属性除外
-- 在开发过程中尽量避免错别字,尤其是客户端要显示的文字,这会给玩家带来很不好的印象
-- 主干永远只有游戏本体相关的东西,任何于此无关的都不应该出现在主干。特定平台的功能剪裁和定制,特殊的优化,如纹理压缩等都应该在对应的分支
-- 不要因为当前只有一个平台就觉得在主干上直接嵌入方便,随后当第二个平台惊喜出现的时候的时候大家就笑了,又开始各种屏蔽剪裁了
-- 除非特定语言要求必须用空格缩进,否则一般采用tab4缩进
-- 任何一条不必要的输出信息都可能让更有效的信息淹没
-- 日志中不要有废话,过期并且无实际意义的日志请及时删除
;仅仅是个人相关的临时调试日志,不要提交
-- 关于技术的讨论,我们应该秉持求同存异的原则,目的只是寻求一个合适的解决方案而不是征服一个人的观点
-- 解决问题应该从问题直观来源切入,不要妄自揣测,用代码和逻辑说话,显示上面的问题就先从UI显示层切入逻辑数据问题就从服务器端切入,这样可以大大提高解决问题的效率以及减少很多不必要的工作;什么是对什么是错一定要去证明,否则就是给自的己埋坑。到真正的问题出现的时候,你就要哭了。或者会导致按下葫芦浮起瓢
-- 工具、脚本等研发辅助系统,应该保持默认状态就是可用的,而不需要先进行大量的配置才能跑起来。合理精细的默认值设置,会改善系统的友好度
-- 应该仔细规划开发步骤和提交内容,必须保证仓库上的版本是可用的,即使是很大的改动,否则团队配合会有问题
-- 代码中尽量不要使用o, l
这样容易和数字0, 1
混淆的标识符
-- 不要在代码中卖萌,尤其是可能展示给用户的提示文字、日志等,迟早会尴尬的
-- 一定要严格的尊重美术的效果图和设计,在UI用户体验细节上要高度还原。这里的细节问题,更多的说明了我们程序对待自己,对待产品,对待客户的专业态度
-- 代码是团队共同所有的。不要忌讳别人改自己的代码,也要勇敢的去改别人的代码。随着时间的推移和需求的改变,代码的调整是客观的
-- 程序一定要尊重策划/美术的设计,要尽最大可能去还原原始设计,关于产品设计方面,程序不能太强势
-- 确定过期的代码请及时删除,不要在源码文件中留下大量注释掉的代码,要合理利用版本管理工具
-- 重要的问题在群里沟通,尽量避免私聊。防止重复沟通和可能的误解
-- 事不过三,过三则重构。重复的流程性的工作,要善用工具来解决
-- 界面和界面逻辑之间的耦合尽量少用字符串,编译器的强类型限制是很好的安全保障
-- 开发过程中应该及时删除废弃的代码和资源,资源可以备份在其他位置
-- GooglePlay上有侵权嫌疑的产品,最好是每个账户单独上,这些账户之间不要有任何可察觉的关联,比如收款的银行卡号,注册的信用卡号,甚至注册时候的IP地址……,否则查封的时候是群封。账号封停后,未结余款到期后会正常结算的。GooglePlay一般违规三次账号是肯定会被封的。上有内购的产品,最好接第三方支付,一旦账号被封,至少还有些渣渣能捡。当然接第三方支付本身就是违反GooglePlay规定的,在产品的FaceBook主页引导玩家游戏外重置的也是违规的。换账号后,新游戏包传上去后,原账号下的产品用户是可以强更到新版本的。GooglePlay总体在审核上比苹果松。
-- 尽量不在代码中写死IP,尽量使用域名,并用单独的配置文件以方便部署和运维
-- 注释的正确写法://
后面一定要加一个空格
-- 程序观点下的线性代数
-- C的数学库函数
-- 变量、函数、文件、类的命名要么就直接用英文单词,要么汉语全拼音,千万不要用简写或者少写一个字母这种失误出现,一旦在开发过程中发现了就立马纠正,不要得过且过
性能优化/代码整理/重构
-- 调整资源加载策略,只加载必须加载的
-- 调整算法,时间换空间,空间换时间。比如缓存和延迟计算等技术的使用
-- 同步操作可以优化为异步的以提高响应
-- 用完后立即释放短期内不会被重复加载的图片以优化内存使用
-- 采用合适的atlas拼图提升图形渲染效率
-- 尽量减少高级渲染技术的使用,或用简单的技术替代,可提升帧率
-- 帧率稳定比高帧率但不稳定要好的多,人类会很快适应固定长度的延迟。网络响应和操作反馈也是相同的原则
-- 实在无法优化性能的地方,就优化响应,让感觉起来很快。比如添加进度条来进行对部分资源的预加载
-- 要通过剖析明确瓶颈所在,直觉实在是个糟糕的向导,即使特别熟悉可以代码的人也不例外
-- 在开发出可以把全局复杂度降至最低程度的干净体系之前,关注性能问题便是过早优化——万恶之源
-- 不要优化系统,而是让其设计尽可能的灵活面对改进和扩展,仅在系统完成之后,再关注纯粹的优化
数据库
-- SQL应尽量采用参数化查询的方式,string.foramt会被恶意注入
PS: 有空了接着整理
网友评论