游戏优化
AOI
概念:Area Of Interest,兴趣范围。
广播范围:全服广播、地图广播、社交关系、交互目标、玩家自身。其中地图广播使用频率最频繁,需要优化空间最大。
例子:场景中有100个人,同步的时候每个人每秒要发10条数据到服务端,同步的时候,服务端就需要发送十万条,如果每次发送的数据是30字节(一个字节8位),那么每秒发3MB的数据,3MB=24Mb。需要24M带宽,带宽是按位算的不是按字节算的。还有一个参数是交换机或服务器的交换率,表示每秒发多少个包,这个例子中转发率是100k。
方案一
方案:
- 数值改变慢的时候,发送信息数量减少。对不关心该数据的人,转发的信息频率或数量变少。
- 只对兴趣范围内的人做广播,例如距离远不广播,这极大降低信息处理压力和网络负载。设计数据结构为数组,每个角色维护一个身边人的数组,每一帧遍历一下周围的人,丢到数据中,然后只给数组中的人转发。
- 兴趣范围需要分等级,例如距离最近的时候是Lv1,最远是lv3。lv3只关心这个人存不存在,lv2关注移动等,lv1才关注战斗等所有具体信息。但是如果有人和你发发生了战斗(关联),无论多远都需要在兴趣范围内。
伪代码:
void OnEntityMove(who)
{
foreach(var entity in entities){
if(who == entity) continue;
bool nowInAOI = who.Distance(entity) < who.AOIRange;
bool alreadyInAOI = who.AOI.contains(entity)
if((alreadyInAOI && !nowInAOI)){
who.onLeaveAOI(entity);
entity.onLeaveAOI(who);
}
if((!alreadyInAOI && nowInAOI)){
who.onEnterAOI(entity);
eneity.OnEnterAOI(who);
}
}
}
缺点:计算成本较高,cpu压力大,每次移动都需要计算距离的次数为(1+N)n/2,100人需要计算5050次。
改善方案:多线程并行计算,根据移动速度减少每秒计算次数,分批计算每帧只计算固定的次数以分散压力。
方案二
方案:
对场景进行区域(格子)划分,只对区域内的人同步。注意的是区域划分合理
void OnEntityMove(who){
int new_x = (int)(who.position.x / size); // 格子索引
int new_y = (int)(who.position.y / size);
if(new_x != who.grid_x || new_y != who.grid_y){
who.LeaveGrid(who.grid_x,who.grid_y){
who.LeaveGrid(who.grid_x,who.grid_y);
who.EnterGrid(new_x,new_y);
this.grid_x = new_x;
this.grid_y = new_y;
}
}
}
优点:相比于上面的计算距离,这里的运算量显著降低,计算速度快
缺点:需要额外的数据结构存储格子信息,实现复杂度高,且存在格子边界问题
改善方案:兴趣范围扩散到九宫格,区域大小可以用2的N次方表示,计算所在区域时可以用位运算。
方案三
目标:不惜一切代价优化到极致,是最难啃的任务。
方案:
- 优化数据结构,例如用树结构优化变量
- 降低运算消耗
- ECS架构,采用面向数据概念的优化架构,提升运算性能。
- 采用并行运算与GPU加速
注意:适可而止,避免过度优化,要考虑性价比。
资源优化
性能优化
方向:内存占用、大小容量、运行效率
资源优化是首要优化任务
资源精细程度适可而止,不要占用太大空间,满载需求即可。
基础类型:模型、动作、纹理、声音、字体
综合资源类型:场景(地形、光影)、UI(图集)、粒子系统
其中基础资源前三个是优化重点
方法:
制作前要指定合理规范:例如规定模型最大面数、UV、LOD,规定贴图最大尺寸和格式,动作时长帧率。其中一些基本规范为美术风格(写实、卡通、像素),视角(2D、3D、2.5D、固定、自由),场景(无缝大地图、超大规模)、玩法。使用九宫格、图集等。
使用规范:模型导入优化(模型压缩、网格优化、可读写、Lightmap UV、动作导入)、动作导入优化(动画压缩、Rig-优化游戏对象)、纹理导入优化(纹理格式、POT、可读写、Mipmap、纹理大小、压缩选项)、场景优化(资源组织、引用和依赖、资源复用)、多平台优化(Standalone、IOS、Android)
优化流程自动化:使用代码进行检查
模型资源
检查参数
Model选项卡中:
MeshCompression:网格压缩,减少模型精度来降低大小,启动压缩后每个模型看看有没有问题,没有问题可以打开
Read/Write Enable:可以读写,如果代码没有对模型进行读写可以关闭。
Optimze Mesh:优化网格
Rig选项卡:
Optimize Game Objects:可以优化模型中的子物体数量,可以变成一个,大大减小遍历时间。
Animation选项卡:
Import Animation:不带动画不要选,否则产生冗余消耗。
Anim Compression:动画压缩,可能会使动画出现问题,出现问题就关掉
纹理资源
导入前图片格式要对:Png、TGA等,否则不支持转换成UI资源,导致没有办法压缩。
Max Size:设置图片最大尺寸
Compression:可以压缩图片,一般选High Quality
Compressor Quality:如果没问题就往小的调
Generate MipMaps:没有特殊情况保持打开,这样在距离较远时图片的像素会变小
注意:NPOT和POT的占用大小完全不同
代码处理
Unity有一个AssetPostprocessor类,可以继承它进行处理,具体查资料。
using UnityEngine;
using UnityEditor;
using System.IO;
namespace PostProcessor{
public class UTexturePreprecessor: AssetPostprocessor{
public static bool IsHD = false;
void OnPreprocessTexture(){
TextureImporter textureImporter = (TextureImporter0)assetImporter;
texureImporter.isReadable = false;
if(assetPath.StartsWith("Assets/UI")){
textureImporter.mipmapEnabled = false;
textureImporter.textureType = TextureImporterType.Sprite;
}
else if(assetPath.StartWith("Assets/Units")){
textureImporter.textureFormat = TextureImporterFormat.AutomaticCompressed;
}
else if(assetPath.StartsWith("Assets/FX/Textures")){
if(textureImporter.textureFormat == TextureImporterFormat.AutomaticTruecolor
|| textureImporter.textureFormat = TextureImporterFormat.Automatic16bit
|| textureImporter.textureFormat = TextureImporterFormat.ARGB16
|| textureImporter.textureFormat = TextureImporterFormat.RGB24
|| textureImporter.textureFormat = TextureImporterFormat.RGBA32
|| textureImporter.textureFormat = TextureImporterFormat.ARGB32
|| textureImporter.textureFormat = TextureImporterFormat.RGB16
|| textureImporter.textureFormat = TextureImporterFormat.RGBA16){
textureImporter.textureFormat = TextureImporterFormat.AutomaticCompressed;
}
}
}
}
}
只有这个脚本在项目中(例如Editor文件夹中),这样导入的时候,就会自动执行,不用手动改了。
CPU优化
性能分析
干扰:需要排除干扰,有可能是干扰问题,排除干扰后性能没有问题。
外部干扰:CPU、内存、IO
内部干扰:工具导致的消耗,例如Profiler(准确评估其消耗),Vertical Sync(分析时关掉),Log output(正式发布需要关日志)。
工具:Unity Profiler、Custom Profiler、Timer&Log
代码分析
void Update(){
CustomProfile();
}
void CustomProfile(){
UnityEngine.Profileing.Profiler.BeginSample("CustomProfile");
for(int i = 0;i < 100; i++){
CustomFunction();
}
UnityEngine.Profiling.Profiler.EndSample();
}
void CustomFunciton(){
UnityEngine.Profileing.Profiler.BeginSample("CustomFunction");
for(int i = 0;i < 100; i++){
CustomCalc();
}
UnityEngine.Profiling.Profiler.EndSample();
}
void CustomCalc(){
UnityEngine.Profileing.Profiler.BeginSample("CustomCale");
float t = 100, f = 0f;
for(int i = 0;i < 100; i++){
f += Mathf.Pow(Mathf.Sin(i), t)
}
UnityEngine.Profiling.Profiler.EndSample();
}
这种方式相比Unity Profile对性能没有影响
对于打包之后的文件,需要用Timer&Log
using System;
using System.Diagnostics;
public class ProTimer: IDisposable{
private string name;
private int times;
private Stopwatch watch;
public ProTimer(string name, int times){
}
public ProTimer(string name,int times){
this.name = name;
this.times = times;
if(this.times <= 0)
this.times = 1;
watch = Stopwatch.StartNew();
}
public void Dispose(){
watch.Stop();
float ms = watch.ElapsedMillseconds; // 通过构造和析构来确定经过多少时间
if(times > 1){
UnityEngine.Debug.Log(string.Format("ProTimer : [{0}] finished:[{1:0.00}ms] total, [{2:0.000000}ms] per peroid for [{3}] times", name, ms, ms / times, times));
}
else
UnityEngine.Debug.Log(string.Format("ProTimer : [{0}] finished:[{1:0.00}ms] total", name, ms));
}
}
使用:
public class ProTest : MonoBehaviour{
void Start(){
int n = 10000000;
using(ProTimer p = new ProTimer("Function")){
Function(n);
}
using(ProTimer p = new ProTimer("Function",n)){
Function(n);
}
}
void Function(int count){
int t = 0;
for(int i = 0;i < count; i++>{
t++;
})
}
}
这样就可以在日志中捕获信息了,通过日志分析耗时,但是当正式发布的时候要去掉这个功能。
要点
该做法是不是Unity运行效率最高的最佳做法,需要:
- Component的缓冲和获取,能缓存就不要实时读取
- 移除空声明,用不到的Start和Update删掉
- 避免Find和SendMessage,大概比直接调用函数慢2000倍
- 禁用未使用或画面中不可见的的游戏脚本和对象
- 对象池(Pool和PoolManager)
- 数据结构优化:Array(遍历效率最高)、List(能动态增加容量、性能消耗在这里,遍历效率略低于数组)、Dictionary(遍历效率底,查询效率高)、Stack、Queue
- 算法优化:使用时间复杂度低的算法、密集计算分布式(把一帧中需要执行的高强度逻辑分散到多帧中,可以利用Queue,多线程等)、缓存(监听变化事件)
- 注意:分析第一,优化第二,避免一知半解的盲目优化,避免增加过多的临时代码,增加的日志最优要删除,尽量通过debug发现问题。
GPU优化
GPU的问题表现往往是帧率低,解决问题的时候要对渲染的基本原理有所了解。
渲染步骤:
- CPU执行游戏逻辑
- CPU调用渲染接口(OpenGL,DirectX)
- 渲染接口将数据提供给硬件驱动
- 硬件驱动把数据提供给GPU的顶点渲染器,通过图源编译进行栅格化,变成像素数据
- 像素数据通过像素渲染器处理,最后丢到帧缓存中等待显示
工具:Unity Profiler、Frame Debug、GPU Profiler(显卡厂商提供)
要点:Drawcall、Batching、图集、移动设备优化
Drawcall
一次Drawcall相当于一次渲染状态的改变
渲染对象关系:Shader可以对应多个Material、一个Material可以对应多个Renderer
Batching
分为Dynamic Batching和Static Batching
Dynamic Batching:
- 所有Mesh实例需要具有相同的材质引用
- 只有Particle System和MeshRenderer可以动态批(随着Unity版本有所不同)
- Vertex Attibute总数必须小于900
- 所有实例必须采用Uniform Scale或所有Mesh都采用Nonuniform Scale,不能混合使用
- 必须引用相同的Lightmap
- 材质Shader不用使用Multiple passes
- Mesh实例不能接受实时阴影
- 隐藏限制:每个Batch最大300Mesh,最多32000Mesh可以Batch
Static Batching:
- 所有Mesh实例具有相同的材质引用
- 所有Mesh必须标记为Static
- 额外的内存占用
- 静态对象无法通过原始的Transform移动
- 任何一点可见,全部渲染 (目前Unity优化过这个点)
图集:Sprite Altas
较复杂,待续。。。
移动设备优化要点
- 最小的Drawcall
- 最小的材质数量
- 最小的纹理尺寸
- 方形&POT纹理
- Shader中使用尽可能低的数据类型
- 避免Alpha测试
内存优化
问题:发现是什么多占了内存
工具:Unity Profiler、Memory Profiler(需要下载额外的插件)
内存占用组成:Unity、Mono、GfxDriver、FMOD
Unity
Unity引擎中占用的内存,包括Texture、Mesh、Animation、Shader、Font
- Texture的大小、纹理格式、Mipmap(选择会造成额外的一倍内存消耗)、压缩、图集
- Mesh的Vertex、Bone、Compression、Batching
- Animation的Length,Keyframe,Compression
- Dynamic Batching、Static Batching
- GC优化:Garbage Collection,逐一定位分析,减少频繁的对象创建,避免装箱,善用缓存,Memory Pool,GameObject Pool。
Mono
C#代码占用的内存,可回收
GfxDriver
显卡驱动占用的内存,渲染数据占的内存
FMOD
声音资源占用的内存
多线程与线程安全
多线程可以解决多任务和提升计算性能。但要注意线程同步的问题,不要同时对一个文件进行增删等操作,最好是一个业务只由一个线程处理。
用于同步的对象:lock(锁定对象)、Monitor(lock的底层,灵活一些)、Mutex(互斥,可以脱离线程做索引)、ReaderWriterLockSlim(读写锁,读写互斥,写写互斥,读读不互斥)、Semaphore(信号量)、Interlocked(联锁)。可以分为对象锁,操作锁、变量锁。
注意:只在必要的时候加锁,加锁力度越小越好。
游戏安全
常见威胁:外挂、破解、数据篡改。
外挂类型:加速挂(单机加速、网络加速),协议挂(功能丰富,支持脱机),内存挂(基于Hook和注入技术,功能强大,主流),脚本辅助(键盘鼠标模拟,不易检测)。
破解:直接影响收入,资源盗用,山寨。
数据篡改:内存数据,存档数据,通讯数据。
通用方案:尽可能使用第三方服务
手段:进程检测,窗口检测,HOOK检测,黑名单
防范三板斧:加密、混淆、加壳
数据保护:内存保护(单机必做)、存档保护(使用复杂数据结构,加密)
推荐书目:
《Intel汇编语言程序设计》
《Windows PE权威指南》
《加密与解密(第三版)》
《游戏安全————手游安全技术入门》
《游戏外挂攻防艺术》
协议分析工具:Wireshark
内存数据扫描更改工具:Cheat Engine
一种简单的防内存扫描方法
int magic = 0x23432455;
public int hp;
public int HP{
get{return hp ^ magic;}
set{ hp = value ^ magic;}
}
public long mp;
public float MP{
get{return System.BitConverter.Int64BitsToDouble(mp ^ magic);}
set{mp = System.BitConverter.DoubleToInt64Bits(value) ^ magic;}
}
发布准备
AssetBundle
AssetBundle可以把Prefabs,Models,Materials,Textures,Fonts,Shaders,Audio Clips,Scenes打包到一个文件中,可以作为DLC(Downloadable Content),减少初始包大小,可以进行版本更新,可以加载为用户平台优化的资源,减少运行时的内存压力。
打包:直接在点击资源,在Inspector的小预览窗口就能选择是否以AsetBundle打包,new一个就是一个新的包,然后直接build即可。
加载代码:
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
public class BundleTest : MonoBehaviour
{
void Start()
{
StartCoroutine(LoadCharacter("Warrior"));
}
IEnumerator LoadCharacter(string assetBundleName)
{
string uri = @"F:\UnityCourse\mmowork\Src\Client\Assets\AssetBundles\character" + assetBundleName.ToLower() + ".asset";
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(uri);
yield return request;
GameObject gameObject = request.assetBundle.LoadAsset<GameObject>(assetBundleName);
Instantiate(gameObject);
}
}
我们需要理解AssetBundle依赖关系以及分配策略。
依赖关系:打包依赖、加载依赖
- 避免多次打包材质纹理等共用的bundle(模型打包时会自动检测依赖关系并打包材质纹理)
- 将材质纹理等共用资源打包成一个bundle,这样其他模型bundle打包时会自动引用这个bundle,不会额外产生空间。
- 当材质纹理打包成另外一个bundle时,加载模型时必须保证先加载材质纹理。
public class BundleTest : MonoBehaviour { void Start() { StartCoroutine(LoadCharacter("Warrior")); } IEnumerator LoadCharacter(string assetBundleName) { string uri = @"F:\UnityCourse\mmowork\Src\Client\Assets\AssetBundles\character" + assetBundleName.ToLower() + ".asset"; AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(uri); yield return request; // 这里需要加上材质bundle的加载,需要保证LoadAsset之前材质都在内存中 GameObject gameObject = request.assetBundle.LoadAsset<GameObject>(assetBundleName); Instantiate(gameObject); } }
bundle分配策略:
按类型分类
强类型关联、平台相关、本地化相关、无交叉依赖
按并发分类
加载时机一致、无交叉依赖
按逻辑单元分组
逻辑功能、逻辑对象、共享对象
其他建议
- 频繁更新的对象进行拆分
- 同时加载的对象加载到一起
- 拆分加载时机不一致的Bundle
- 合并频繁加载的小粒度Bundle
资源统一加载
加载方式:Resources、AssetBundles、Raw(Image\Movie)
Resources
var textFile = Resources.Load<TextAsset>("Text/textFile01");
var sprite = Resources.Load<Sprite>("Sprite/sprite01");
var audioClip = Resources.Load<AudioClip>("Audio/audioClip01");
AssetBundle
AssetBundle.LoadFromMemory
AssetBundle.LoadFromMemoryAsync
AssetBundle.LoadFromFile
AssetBundle.LoadFromFileAsync
AssetBundle.LoadFromStream
AssetBundle.LoadFromStreamAsync
资源加载需求分析
发布平台、动态更新\流式加载、压缩加密、自定义数据包
资源路径:Application.dataPath、Application.streamingAssetsPath(包内)、Application.persistentDataPath(补丁(默认))、Application.temporaryCachePath(下载缓存、补丁(IOS))
流程设计:

自动更新(建议只在必要的大型项目中做)
版本信息管理:
版本号、版本记录、版本清单、版本内容
更新策略:
发布途径:应用商店更新、自主更新
更新内容:二进制更新、资源更新、热更新
环境:
持续集成CI(Jenkins、Bamboo、Gitlab CI)、DevOps、更新服务器(IIS、Apache、Nginx)
更新系统组成:
版本构建系统、版本生成工具(DIY)、更新服务器(Version.txt,UpdateList.txt,Updates)、更新逻辑
更新方案:
服务器为主:动态生成实际需要的版本清单、一次更新多个版本、动态更新策略(渠道、区域、比例)、开发成本和运行成本高
客户端为主:服务器压力小,无服务器逻辑的开发成本,难以支持动态策略,一次更新到最新成本高