Unity终极实战之MMORPG-Part4


游戏优化

AOI

概念:Area Of Interest,兴趣范围。
广播范围:全服广播、地图广播、社交关系、交互目标、玩家自身。其中地图广播使用频率最频繁,需要优化空间最大。
例子:场景中有100个人,同步的时候每个人每秒要发10条数据到服务端,同步的时候,服务端就需要发送十万条,如果每次发送的数据是30字节(一个字节8位),那么每秒发3MB的数据,3MB=24Mb。需要24M带宽,带宽是按位算的不是按字节算的。还有一个参数是交换机或服务器的交换率,表示每秒发多少个包,这个例子中转发率是100k。

方案一

方案:

  1. 数值改变慢的时候,发送信息数量减少。对不关心该数据的人,转发的信息频率或数量变少。
  2. 只对兴趣范围内的人做广播,例如距离远不广播,这极大降低信息处理压力和网络负载。设计数据结构为数组,每个角色维护一个身边人的数组,每一帧遍历一下周围的人,丢到数据中,然后只给数组中的人转发。
  3. 兴趣范围需要分等级,例如距离最近的时候是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次方表示,计算所在区域时可以用位运算。

方案三

目标:不惜一切代价优化到极致,是最难啃的任务。
方案:

  1. 优化数据结构,例如用树结构优化变量
  2. 降低运算消耗
  3. ECS架构,采用面向数据概念的优化架构,提升运算性能。
  4. 采用并行运算与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的问题表现往往是帧率低,解决问题的时候要对渲染的基本原理有所了解。
渲染步骤:

  1. CPU执行游戏逻辑
  2. CPU调用渲染接口(OpenGL,DirectX)
  3. 渲染接口将数据提供给硬件驱动
  4. 硬件驱动把数据提供给GPU的顶点渲染器,通过图源编译进行栅格化,变成像素数据
  5. 像素数据通过像素渲染器处理,最后丢到帧缓存中等待显示

工具: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)、更新逻辑
更新方案:
服务器为主:动态生成实际需要的版本清单、一次更新多个版本、动态更新策略(渠道、区域、比例)、开发成本和运行成本高
客户端为主:服务器压力小,无服务器逻辑的开发成本,难以支持动态策略,一次更新到最新成本高


文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
评论
  目录