大型Moba项目开发记录1

单例类的创建

https://www.cnblogs.com/zhaoqingqing/p/3894061.html 相关知识:泛型类的约束条件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace TKGame
{
    public abstract class Singleton<T> where T : new()
    {
        private static T _instance;
        static object _lock = new object();
        public static T Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new T();
                        }
                    }
                }
                return _instance;
            }
        }
    }

    public class UnitySingleton<T> : MonoBehaviour where T : Component
    {
        private static T _instance;
        public static T Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = FindObjectOfType(typeof(T)) as T;  //找到T类型的对象返回
                    if (_instance == null)
                    {
                        GameObject obj = new GameObject();
                        obj.hideFlags = HideFlags.HideAndDontSave;
                        _instance = (T)obj.AddComponent(typeof(T));
                    }
                }
                return _instance;
            }
        }

        public virtual void Awake()
        {
            //不跟随场景销毁
            DontDestroyOnLoad(this.gameObject);
            if (_instance == null)
            {
                _instance = this as T;
            }
            else
            {
                Destroy(gameObject);
            }
        }
    }
}

//where表明了对类型变量T的约束关系。where T:new()指明了创建T的实例时应该具有构造函数。
一般情况下,无法创建一个泛型类型参数的实例。
然而,new()约束改变了这种情况,要求类型参数必须提供一个无参数的构造函数。

资源管理单元

知识点补充:
1. 被 internal 修饰的东西只能在本程序集(当前项目)内被使用。
2. 被 protected internal 修饰的属性/方法 可以在其他项目中, 被派生类使用.
3. IDisposable接口的主要用途是释放非托管的资源。 垃圾回收器自动释放不再使用该对象时分配给托管对象的内存。 但是,不可能预测将发生垃圾回收。 此外,垃圾回收器具有不知道如窗口句柄的非托管资源,或打开文件和流。
4. 使用Dispose的此接口可显式释放垃圾回收器结合使用的非托管的资源的方法。 不再需要该对象时,对象的使用者可以调用此方法。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Game.Resource
{
    //资源加载类型
    public enum ResourceType
    {
        ASSET,
        PREFAB,
        LEVELASSET,
        LEVEL
    }

    public class ResourecesUnit : IDisposable
    {
        private string mPath;
        private Object mAsset;
        private ResourceType mResourceType;
        private List<ResourecesUnit> mNextLevelAssets;  //下一层级Asset
        private AssetBundle mAssetBundle;
        private int mAssetBundleSize;  //尺寸
        private int mReferenceCount;  //资源引用计数

        internal ResourecesUnit(AssetBundle assetBundle, int asssetBundleSize, Object asset, string path, ResourceType resourceType)
        {
            mPath = path;
            mAsset = asset;
            mResourceType = resourceType;
            mAssetBundle = assetBundle;
            mAssetBundleSize = asssetBundleSize;
            mReferenceCount = 0;
        }

        public Object Asset
        {
            get { return mAsset; }
            internal set { mAsset = value; }
        }

        public ResourceType resourceType
        {
            get { return mResourceType; }
        }

        public List<ResourecesUnit> NextLeveAssets
        {
            get { return mNextLevelAssets; }
            internal set
            {
                foreach (ResourecesUnit asset in value)
                {
                    mNextLevelAssets.Add(asset);
                }
            }
        }

        public AssetBundle AssetBundle
        {
            get { return mAssetBundle; }
            set { mAssetBundle = value; }
        }

        public int AssetBundleSize
        {
            get { return mAssetBundleSize; }
        }

        public int ReferenceCount
        {
            get { return mReferenceCount; }
        }

        public void addReferenceCount()
        {
            ++mReferenceCount;
            foreach (ResourecesUnit asset in mNextLevelAssets)
            {
                asset.addReferenceCount();
            }
        }

        public void redeceReferenceCount()
        {
            --mReferenceCount;
            foreach (ResourecesUnit asset in mNextLevelAssets)
            {
                asset.redeceReferenceCount();
            }
            if (isCanDestroy())
            {
                Dispose();
            }
        }

        /// <summary>
        /// 是否可以销毁资源
        /// </summary>
        /// <returns></returns>
        public bool isCanDestroy()
        {
            return mReferenceCount == 0;
        }

        public void Dispose()
        {
            if (mAssetBundle != null)
            {
                mAssetBundle = null;
            }
            mNextLevelAssets.Clear();
            mAsset = null;
        }

        //#region IDisposable Support
        //private bool disposedValue = false; // 要检测冗余调用

        //protected virtual void Dispose(bool disposing)
        //{
        //    if (!disposedValue)
        //    {
        //        if (disposing)
        //        {
        //            // TODO: 释放托管状态(托管对象)。
        //        }

        //        // TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
        //        // TODO: 将大型字段设置为 null。

        //        disposedValue = true;
        //    }
        //}

        //// TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
        //// ~ResourecesUnit() {
        ////   // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        ////   Dispose(false);
        //// }

        //// 添加此代码以正确实现可处置模式。
        //public void Dispose()
        //{
        //    // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        //    Dispose(true);
        //    // TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
        //    // GC.SuppressFinalize(this);
        //}
        //#endregion
    }
}

//被 internal 修饰的东西只能在本程序集(当前项目)内被使用。
//被 protected internal 修饰的属性/方法 可以在其他项目中, 被派生类使用

//此接口的主要用途是释放非托管的资源。 垃圾回收器自动释放不再使用该对象时分配给托管对象的内存。 但是,不可能预测将发生垃圾回收。 此外,垃圾回收器具有不知道如窗口句柄的非托管资源,或打开文件和流。
//使用Dispose的此接口可显式释放垃圾回收器结合使用的非托管的资源的方法。 不再需要该对象时,对象的使用者可以调用此方法。

资源管理

Xml在unity内存储文件路径,脚本调用此路径来加载游戏物体。

操作Xml文档:

        XmlDocument doc = new XmlDocument();
        string xmlStr = "<book genre='novel' ISBN='1-861001-57-5'>" +
                "<title>Pride And Prejudice</title>" +
                "</book>";
        Debug.Log("加载的xml文件:" + xmlStr);
        doc.LoadXml(xmlStr);

        XmlElement root = doc.DocumentElement;  //获取根节点
        IEnumerator iter = root.GetEnumerator();
        if (iter.MoveNext())
        {
            string genre = root.GetAttribute("genre");
            Debug.Log("查找的属性值:" + genre);
        }

进一步完善:

using System.Collections.Generic;
using System.Collections;
using System.IO;
using System;
using System.Xml;
using UnityEngine;
using Game;
/// <summary>
/// 操作xml文本
/// </summary>
namespace Game.Resource
{
    public class ArchiveManager : Singleton<ArchiveManager>
    {
        internal Dictionary<string, Archive> mAllArchives;

        public ArchiveManager()
        {
            mAllArchives = new Dictionary<string, Archive>();
        }

        public void Init()
        {
            StreamReader sr = ResourceManager.OpenText("Resource");
            XmlDocument doc = new XmlDocument();  //文件操作类
            doc.LoadXml(sr.ReadToEnd());
            XmlElement root = doc.DocumentElement;  //查找根节点
            IEnumerator iter = root.GetEnumerator();  //迭代器
            while (iter.MoveNext())
            {
                XmlElement child_root = iter.Current as XmlElement;
                IEnumerator child_iter = child_root.GetEnumerator();
                if (!mAllArchives.ContainsKey(child_root.Name))
                {
                    Archive arh = new Archive();
                    mAllArchives.Add(child_root.Name, arh);
                }
                while(child_iter.MoveNext())
                {
                    XmlElement file = child_iter.Current as XmlElement;
                    string name = file.GetAttribute("name");  //返回指定属性的数值
                    string type = file.GetAttribute("type");
                    mAllArchives[child_root.Name].add(name, type);
                }
            }
            sr.Close();
        }
    }
}

知识点补充:
1. 栈Stack和队列Queue的区别:
吃多了拉是队列,吃多了吐是栈。
相关博客推荐:https://www.cnblogs.com/JiYF/p/6281667.html
2. AssetBundle
AssetBundles是Unity编辑器中在编辑过程中创建的一些文件,这些文件可以在项目运行环境中使用。AssetBundles可以包含的资源文件比如模型,材质,贴图和场景等等,注意:AssetBundles不能包含脚本!

具体来说,一个AssetBundle就是把资源或者场景以某种方式紧密集合在一起的一个文件。这个AssetBundle文件可以被单独加载到unity应用程序中。这允许模型、贴图、音效设置很大的场景这些资源进行流式加载或者异步加载。当然AssetBundle文件也可以本地缓存这样能够在程序启动的时候立马被加载出来。但AssetBundle技术主要目的就是需要的时候从远程服务器下载需要的资源。AssetBundle可以包含任何unity可识别的资源文件,甚至包括二进制文件。但就是不能包含脚本文件。
https://blog.csdn.net/swj524152416/article/details/73348296

资源加载进度:

using System;
using System.Collections.Generic;
using System.Collections;
using UnityEngine;

namespace Game.Resource
{
    public class ResourceAsyncOperation
    {
        internal RequestType mRequestType;
        internal int mAllDependencesAssetSize;  //依赖尺寸
        internal int mLoadDependencesAssetSize;
        internal bool mComplete;
        public AsyncOperation asyncOperation;

        internal ResourceUnit mResource;

        internal ResourceAsyncOperation(RequestType requestType)
        {
            mRequestType = requestType;
            mAllDependencesAssetSize = 0;
            mLoadDependencesAssetSize = 0;
            mComplete = false;
            asyncOperation = null;
            mResource = null;
        }

        public bool Complete
        {
            get { return mComplete; }
        }

        //资源加载进度
        public int Prograss
        {
            get
            {
                if (mComplete)
                    return 100;
                else if (mLoadDependencesAssetSize == 0)
                    return 0;
                else
                {
                    if (ResourcesManager.Instance.UsedAssetBundle)
                    {
                        //使用assetbundle
                        if (mRequestType == RequestType.LOADLEVEL)
                        {
                            int depsPrograss = (int)(((float)mLoadDependencesAssetSize / mAllDependencesAssetSize) * 100);
                            int levelPrograss = asyncOperation != null ? (int)((float)asyncOperation.progress * 100.0f) : 0;
                            return (int)(depsPrograss * 0.8) + (int)(levelPrograss * 0.2);
                        }
                        else
                        {
                            return (int)(((float)mLoadDependencesAssetSize / mAllDependencesAssetSize) * 100);
                        }
                    }
                    //不使用
                    else
                    {
                        if (mRequestType == RequestType.LOADLEVEL)
                        {
                            int levelPrograss = asyncOperation != null ? (int)((float)asyncOperation.progress * 100.0f) : 0;
                            return levelPrograss;
                        }
                        else
                        {
                            return 0;
                        }
                    }
                }
            }
        }
    }
}

测试demo
加载一个Cube到游戏场景

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using System.Linq;
using Game.Resource;

public class LoadUiResource
{
    //建立实例化物体的对应关系,方便下次使用
    public static Dictionary<string, GameObject> LoadResDic = new Dictionary<string, GameObject>();

    //加载UI资源
    public static GameObject LoadRes(Transform parent, string path)
    {
        if (CheckResInDic(path))  //检查路径是否正确
        {
            if (GetResInDic(path) != null)  //检查路径下是否存在的物体
                return GetResInDic(path);
            else
                LoadResDic.Remove(path);
        }

        //资源加载到场景
        GameObject objLoad = null;
        ResourceUnit objUnit = ResourcesManager.Instance.loadImmediate(path, ResourceType.PREFAB);  //加载资源到内存
        if (objUnit == null || objUnit.Asset == null)
        {
            Debug.LogError("load unit failed" + path);
            return null;
        }
        objLoad = GameObject.Instantiate(objUnit.Asset) as GameObject;
        objLoad.transform.parent = parent;
        objLoad.transform.localScale = Vector3.one;
        objLoad.transform.localPosition = Vector3.zero;
        LoadResDic.Add(path, objLoad);
        return objLoad;
    }


    //检查资源路径是否存在
    public static bool CheckResInDic(string path)
    {
        if (LoadResDic == null || LoadResDic.Count == 0)
            return false;
        return LoadResDic.ContainsKey(path);
    }

    public static GameObject GetResInDic(string path)
    {
        if (LoadResDic == null || LoadResDic.Count == 0)
            return null;
        GameObject obj = null;
        if (LoadResDic.TryGetValue(path, out obj))
            return obj;
        else
            return null;
    }
}
        string path = "Test/Cube";
        LoadUiResource.LoadRes(transform.root, path);

知识补给站:
DestroyImmediate和Destroy区别
DestroyImmediate立即对对像进行销毁;
1,DestroyImmediate立即销毁目标,并将目标置为null,且将目标的所有上层引用置空,如用DestroyImmediate销毁OBJ下的所子物体后 OBJ.childcount将变为0,见代码//22222222222
2,Destroy则在本帧结束前,渲染之前销毁目标和上层引用。不会立即销毁,Destroy调用后,目标数据仍然存在,不为null,上层引用也正常。见代码//111111111处,因为销毁物体后,childcount
仍然保持不变,这常会造成其它地方的逻辑出错,子结点已经销毁了却还能取到,因此,我们应该将子结点从父物体摘除,rootTrans.DetachChildren();这样其它地方代码再获取子结点及其数量时就不导致逻辑出错了。

3,基于上述原理,测试Destroy和DestroyImmediate的时间占用,发现后者几乎是前者的10倍。
https://www.cnblogs.com/timeObjserver/p/6679632.html

    public static void ClearAllChild(Transform transform)
    {
        while (transform.childCount > 0)
        {
            GameObject.DestroyImmediate(transform.GetChild(0).gameObject);
        }
        transform.DetachChildren();  //所有子物体解除父子关系。
    }

客户端知识点总结

GamePlay
1. 战斗逻辑: 人物同步细节,插值运算
2. 碰撞检测效率:球体>立方体>Mesh
可见性检测–战争迷雾
3. 渲染
GPU渲染关线:Shader(DrictX OpenGL) 光照
角色动画系统: 角色动画状态机,动画融合(模型尽量和动画分开,一个动画一个文件)
Avatar换装
游戏特效–粒子编辑器
4. 游戏优化

运行效率优化
1. Profiler工具 Windows菜单下
2. 反复实例化实体,利用 对象池
3. 贴图优化
4. 顶点优化 优化模型面数
5. Batch
6. 网络优化
Https弱联网 适合通信效率低的场景
Tcp适合网络通讯但对延迟不敏感的场景
Udp用冗余流量来保证延时。

研发效率
1. 定义游戏内常用概念的命名,项目执行过程中的各种资源名,配置名都使用相同的命名。
2. 数据驱动
首先考虑没有数据代码的处理
Excel是最佳的配置文件结构
3. 所有策划干涉到的游戏逻辑都要做到数据驱动
4. 网络通讯抽象架构优于ProtoBuffer,ProtoBuffer优于Json和XML
5. 美术
沟通资源规范,同屏最多可以显示角色个数,还有最大面数,场景最大面数,贴图最大尺寸

游戏外挂与反外挂
1. MD5加密,压缩gap格式,设置解压密码
2. 校验最高成绩,如果大于最高成绩,服务器丢弃数据
3. 客户端和服务器共享逻辑实现,避免维护两套文件

其他
1. 资源管理
本地文件管理,本地缓存管理
2. 热更新
游戏代码更新
游戏资源更新
ulua tolua
3. 版本控制
文件的md5做版本好最好
用文件的svn版本号做版本号最方便

猜你喜欢

转载自blog.csdn.net/gsm958708323/article/details/80143684