开发心得
这里的每一点心得都是遇到的坑……
游戏设计
别限制玩家要怎么玩
解决方案技术栈
- 美术资源管理svn/git-fls
也可以建2个库,美术资源是Fork的库。这样。美术库只包含基本代码。代码库只包含基本允许场景。 - 其他资源管理使用git
- 文档使用 dokuwiki/ amwiki + git /Confluence或代替品
- 项目管理 (禅道/Redmine) + QQ邮箱 + webqq(调研中)
- 服务器使用docker Kitematic
- mysql mongodb
- Jenkins
- 使用.vsTemplate规范代码,创建VSIX
版本控制
代码中完成Version类,对应版本配置文件。配置文件必须是可以差分的。
配置文件3个为默认配置。重写配置,指定版本重写配置。后面的配置文件盖前面的。
设计之初,任何功能都要根据版本可配置开启和关闭。每个功能条目配置上下限两个版本。
例如一个功能按钮是否显示,是否禁用,以应对功能随版本开闭。
工作区
创建工作区概念,客户端服务器各种自动化脚本放在一起,所有路径以配置的工作区路径+相对路径为准
配置表
- ID、数字等不能有隐含信息,所有约束必须明文指出。
- 错误示例:41,42,十位代表一个类别,个位代表子类别。
- 元编程,为配置表每个字段生成一个接口,表结构类型是接口的组合、
- 配置表支持的类型应该为proto3的类型,兼容性更好
数据结构
- 使用byte代替bool
少用bool 表示一个属性,尽量用byte。比如一个单位的一个属性时可不可见。由可见不可见改为可见级别。bool改为byte。这样可以设置不同级别的战争迷雾。
都是一个字节,开销差不多,判断用是否大于0。 - 使用int代替bool
int更通用,可以有负值,也方便内存对齐
权衡之后,更倾向使用int表达一个属性
代码细节 架构相关
水平的叫层,垂直的叫模块。比如Megumin.Net网络模块,位于传输层和应用层。
宗旨:尽量多分库,dll不怕多
大约是编程心得和杂项
基本4层:- 4 软件层,程序入口。最开始向 2 注入IoC容器,或IoC配置。所有模块依赖的1中其他模块都需要延迟注入,避免循环引用问题。
- 3 所有模块接口实现,通过依赖2实现延迟注入。
- 2 Ioc容器接口,包含所有模块接口(如果使用反射可以省略4)
- 1 最底层模块接口
- 0 公共API接口,所有模块共用
1
2
3
4
5
6
7
8
94 ---------------`软件层 IoC容器实现层`
/|\
3 | 3 ---------------`模块实现层`(这里可以分为多个子层)
|\|/|
| 2 | ---------------`IoC容器接口层`(这里可以分为多个子层)
|/|\|
1 | 1 ---------------`模块接口层`
\|/
0 ---------------`通用接口层`代码
- 不应该跨模块使用enum,除非enum声明在基础层。
- 定义枚举一定要考虑是否会扩展。枚举扩展非常麻烦。
- 也要考虑是否有FlagAttribute。
- 当你负责的模块使用了基础层的枚举,请在enum源码处注明你当前的模块,不能在源码处注明的,在文档中注明。(防止枚举变化带来大量改动)
- 不应该跨模块使用enum,除非enum声明在基础层。
业务逻辑代码操作的MODEL,应该是接口或者虚类
接口设计
- 为了兼容AOT,接口定义的方法最好不要带泛型,可以改为泛型接口。IL2CPP或者其他静态生成器生成代码时,无法分析
接口中泛型方法
的实现类的泛型方法
的实际调用填充类型
。
类设计
普通类
- 如果一个类只为另一个类服务,那么他们应该写在一起,或者声明为内部类。
- 例如:如果只有一个Book类,那么BookManager类则没有必要,应该写成Book的静态函数。
- 声明 和 new 对象之前想想它的生命周期。
- 动态的还是静态的
- 是不是唯一的
- 如果类的逻辑只使用到类的部分属性,那么应该可以考虑抽象出接口
- 逻辑只关心部分数据,不意味着要把对象的数据拆开。应该通过接口区分,而不是拆成各个sub子类。
不要用dynamic做形参类型
- 如果一个类只为另一个类服务,那么他们应该写在一起,或者声明为内部类。
通讯协议类
开发阶段协议都是用Json,通过string传递。减少迭代。对协议类接口化。
发布阶段,将部分协议具体化,提高性能。通讯类命名
不建议在类名中加入前缀后缀,例如reqest,response,S2C,C2S,在实际使用中,类名会变得越来长,一个类也未必具有单一功能,S2C的类也可能S2S,失去前缀的意义反而带来歧义。应答类
必须包含2个内容:
谁做了什么,即请求上下文;Rpc机制解决了这个问题。- 导致了什么结果,连锁结果应该放在同一个包中,避免时序错误 类似Race Condition。
应答类中嵌套它对应的请求类是合理的。必须在 更小的包体 和 准确性 之间做出取舍。
如果特殊必要,不要使用增量的方式发送数据,每个消息包一定要独立完整。低频消息不在乎这点数据量。增量发送会导致接收端数据准确性问题。
业务逻辑应不应该直接使用协议类型?
究竟应该继承
还是组合
?
- 继承还是组合应该由两个类的
相似程度
决定,功能相差的多应该用耦合.
具体到百分比就是,如果类相似程度大于80%,用继承,小于80%用组合. - 决定类A要不要继承B,看A与B相同功能占A总功能的百分比决定.
大于75%就继承. - 组合时子元素不应该持有父元素引用,否则就是设计不合理,违反了高内聚低耦合.如果用到父,应该通过传参的方式.
- 继承和组合 父子关系是反过来的.
代码规范
方法宽度长度不应该大于一个屏幕
///依据:大写开头是安全的,小写开头意味着你可能绕过某些Get/Set逻辑 https://docs.microsoft.com/zh-cn/dotnet/standard/design-guidelines/capitalization-conventions
Pascal 类 结构 属性 字段 元组参数(本质为字段) 方法 (static readonly) const event
camel 参数缩进 4空格
大括号换行
= 后换行(代码过长时)
=>后换行(代码过长时)
不要使用#region,严重影响代码导航和滚动条。
团队协作时,如果不能统一格式。就
不允许
使用自动格式页面所有代码。这将导致代码无法追溯。
ID
- ID 类唯一
- UID 进程内唯一
- GUID 全局唯一
- 对于玩家ID NPCID 怪物ID等实体,共用一个域使用long类型,向前兼容。
这里不能在乎int/long的性能损失。
时间模块
游戏功能系统设计之初就要将游戏暂停,后台恢复,掉线等情况考虑进去,比如移动,设计成根据时刻采样而不是位置的累加。
使用自己的update而不是unity的update,使用参数UpdateContent 包含 time realtimeSinceStartup frameCount updatemode等,可以指向unity的时间值,也可以重新实现这几个值。
自己实现时间模块可以根据功能自由的进行快进慢放操作,还可以提供可控制的相对时间戳,unity的time存在严重的设计足。
为游戏设计几个状态:
- offline
- normal
- pause
- pause2normal //暂停到恢复正常的过程,用于处理网络中阻塞的数据。
- willnormal //进入normal的前一个时刻,通常为一帧
- connect //连接过程中。
- reconnect //重连过程中。区分连接和重连十分重要。
通常处理功能模块有多种模式。如移动逻辑:
平滑移动,强制位置对齐,每次移动固定长度等。根据游戏进程的状态选用不同的移动模式,可以获得很好的断线重连效果。
UI设计
UI大小(横屏)
- 画布大小 1280 * 720 (或者) 1334 * 750(iphone6尺寸)
- 切图大小尽量8的倍数,至少4的倍数。
- 对于移动端,一以红米Note4x 5.5英寸 1920*1080 像素为例:
- 大Button,键盘按键的[3-4]/2,6-7个为宜。
- 小Button,键盘按键的宽度,7-9为宜。
- 可选物品选单 水平排列半屏 4 个为宜,垂直排列 2.5-3.5 。
真实项目中可以把UI设计稿截个全屏图放入手机中感受一下。
UnityUI代码结构
基于多层继承的代码结构。(设想)
继承层级 N+2 N+1 N N-1 预制体层 嵌套层 泛型层 总基类 实际挂在prefab上的脚本。 仅用于其他功能的嵌套引用。如果unity支持泛型序列化,那么这层完全可以取消 依赖于Model数据的逻辑(名字,堆叠) 不依赖于实际数据的逻辑 例: ItemIconView1 第一版的物品图标脚本 (abstract)ItemTIconBase ItemIcon<T>
(abstract)ItemIconCore Monobehaviour ItemIconView2 第二版的物品图标脚本 ShopItemView1 +(abstractItemTIconBase 商店物品脚本,包含了一个普通物品的图标,当由于需求变化ItemIconView出现大幅度修改时,由于这里引用的父类所以无需改动。 基于接口的代码结构。
做到你的所有逻辑写在不继承于MonoBehavior的类里即可。
对于文字,图片等资源的ID表示法问题
由于需要文字国际化,我们通常在逻辑中使用一个Key(int/string/enum)表示一句话。那么在内存中我们什么时候将Key转化为字符串Value:
A. 在配置表加载完毕后,Model构建时将Key转化为Value。表现为Model中字段为Value(string)。
B. 在逻辑层面保持Key,在显示到屏幕的最后一刻,也就是View层将Key转化为String。表现为Model中字段为NameKey(Key)。
C. 在逻辑层面保持Key,在显示到屏幕的最后一刻,也就是View层将Key转化为图片等大型资源。表现为Model中字段为AssetKey(Key)。
各个方面优劣对比 | A:Name(string) | B:NameKey(Key) | C:AssetKey(Key) |
---|---|---|---|
内存占用 | - | - | ○ |
Debug难易 | ○ | × | ○ |
参数传递 | - | - | ○ |
跨模块API设计 | ○ | × | × |
结论:对于String使用A方式,对于图片,预制体等可延迟加载的大型资源,使用C方式。
MVC
- Q:游戏架构中MVC中M到底是什么?
- A:网络远端传来的数据;配置表;数据库。
游戏设计中,数据来源总是从这三者之一出发,最终表现在屏幕上。
Input操作应该转化为指令,然后进入游戏逻辑,构成网络指令和操作指令的统一。 - 数据+逻辑应该做到可以脱离View运行。
- A:网络远端传来的数据;配置表;数据库。
M-V-VM
Q: 为什么要有ViewModel?
A: 比如有一组物品,有个是否是当前正在被选中的属性。如果这个属性放在Model上,那么关闭View,再打开View,这个物品仍然是被选中的。
所以是否被选中应该是ViewModel的一个属性。打开View时,根据Model初始化ViewModel。
所以对于View层,Model不可见,只关心ViewModel即可。ViewModel是Model加其他View需要的属性的组合。
Q:一个model可以对应多个ViewModel么?比如用于3D空间和用于UI?
Q:ViewModel可以Clone/Fork么?
Q: 当model数据发生改变使用dirty轮询机制还是事件机制?
- dirty机制
- 只需要在ui界面需要刷新的去看看是否有脏标志 有脏标志就刷新下界面
- 可能改变多次,但是只需要最新的
- 轮询可以控制频率和间隔,事件如果短时间巨量变动可能导致大量运算
开发流程
IDEDA -> 确定所属功能 -> 拆分功能明确依赖 -> 明确依赖上下游并标准化接口 -> IDEA.
事务句柄/事务锁/ET6.0协程锁
对于任何需要异步执行/分段/防重入/互斥
的事务,应该采用先获取句柄在执行的设计,句柄在(多段)执行过程中传递,在执行结束后复位。句柄可以设计成泛型的,保存事务中间信息,尽量每次使用新句柄,而不是复用句柄,防止脏数据。
对象池
要有全局任务控制。
对于各个功能,使用的频率决定相关功能对象池的权重,以此决定对象池长期保持对象的数量。
把需要加载的东西加入任务队列。控制好内存和帧率的平衡。
良好的设计应该是游戏应该知道什么时间段是性能占用低的时间。
比如打开全屏UI,通常性能非常充足。这个时候全局如果认为内存可用。就会分帧实例化对象池的对象。
值类型在哪里分配内存
取决于声明位置,声明在方法内部,就在栈上. 声明在类的字段上,就在堆上.
不要盲目解耦合
- 调用方式与耦合程度:
类型强引用直接调用 > 接口或多态调用 > 事件委托调用 > 字符串反射调用 - 以下几个情况适合解耦合:
- 两个类型不在同一个程序集,被引用的程序集中希望调用引用它的程序集中的函数
- 两个类型不是由同一个程序员实现,互相不能有效沟通
- 类型在编码时未知,引用在编码时未知,由运行时动态生成
编程三个核心技巧
,理解每种技巧都是编程路上的分水岭.
- 回调函数->委托事件
- 泛型
- 多线程->异步
编程境界
- 世间万物皆对象
- 世间万物皆数据
解释:分得清什么是数据什么是逻辑,知道数据和逻辑可以互相转化,进而复用。
例子:各种配置表。 - 标准化
解释:任何事物都需要不停的迭代发展。标准化是快速迭代的必要条件。 - 复杂度不会消失,只会转移。
同时也意味着可以转移
。- 标准化是一个明确的边界,将复杂度分离到各个子模块中。
- 需求的最小复杂度是存在的,实现的复杂度总是大于最小复杂度。错误的设计会增加不必要的复杂度。
- 消灭重复与不必要复杂度的引入,比如Util函数中的十多个传参。
- 数据是存在,逻辑是变化。
以前以为发明二进制的人很牛逼,用0和1就能表达整个世界,洞悉了世界的本质。但其实不是的,二进制也好,十进制也好,只是表达方式的不同。计算机使用二级制,是因为物理材料容易实现,如果材料合适,使用三进制,十进制都是可以的。
数据与逻辑不因表达方式不同而改变。
花言巧语不能改变事物本质。
数据与逻辑才是万物的基石,二进制不是。程序的本质就是数据和逻辑。要写出更通用和准确的逻辑,就需要更高级的数据描述方式。interface和class具有局限性。
- 还没有悟出来……
工具链
经验法则: 美术素材只能用工具生成中间素材,不能生成最终素材,因为对于最终素材90%的情况需要手动调整,挂载脚本。一旦由工具重新生成,手动调整部分会丢失,如果工具要保留手动调整部分,要做大量工作。
unity客户端可以开启HTTP监听,然后远程发送调试指令,也可以嵌入到控制台
1 | private void StartHttpDebug() |