Unity程序基础框架学习

一.常见的资源管理方式

  在Unity中,Project窗口中的Assets目录下保存了项目中使用的脚本、模型、预制体、贴图等等资源,资源的种类多,需要管理起来。一般地,我们将不同的资源放在不同的文件夹下,按照模块分类,方便管理。常见的文件夹:Scripts脚本文件夹、Scenes场景文件夹、Resources在脚本中使用的资源文件夹、Prefabs预制件文件夹等。

二.搭建简单的程序框架

1.单例模式基类模块

游戏中常用的管理类或者其他只能有一个实例的物体(如一些UI面板)会使用单例模式,因此我们创建一个单例模式基类,只要某个类继承这个基类,就能实现单例模式。

/// <summary>
/// 单例模式基类
/// </summary>
public class BaseManager<T> where T:new()
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
                instance = new T();
            return instance;
        }
    }
}

最基本的单例模式基类实现,但是在Unity中脚本基本都是继承了MonoBehaviour的。

/// <summary>
/// 继承mono的单例模式基类
/// 保证这个脚本只挂载一次,否则会破坏单例模式
/// </summary>
public class SingleMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            return instance;
        }
    }

    /// <summary>
    /// awake函数必须声明为保护的虚函数,这样子类可以看到并且子类书写awake函数不会覆盖这个函数
    /// </summary>
    protected virtual void Awake()
    {
        instance = this as T;
    }
}

这是继承了MonoBehaviour的单例模式基类,但是需要手动将这个脚本挂载到某个物体上,才会调用awake函数为单例赋值。

/// <summary>
/// 改进版继承monobehaviour的单例模式基类
/// 这种方式创建的单例模式不需要再挂载脚本了,自动挂载这个脚本,不用手动添加脚本防止不小心添加了多个脚本导致单例被破坏
/// </summary>
public class SingleAutoMono<T> : MonoBehaviour where T:MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            //如果没有单例,自动创建一个空物体并挂载脚本,这样会调用awake函数为单例赋值
            if (instance == null) 
            {
                GameObject obj = new GameObject();
                obj.name = typeof(T).ToString();
                instance = obj.AddComponent<T>();
            }
            return instance;
        }
    }

    protected virtual void Awake()
    {
        instance = this as T;
    }
}

改进版继承monobehaviour的单例模式基类,不用挂载这个脚本就能实现单例模式,避免手动挂载时出错。

2.缓存池模块基础

对于子弹等物体,在游戏中如果不断实例化再销毁会加速内存消耗,缩短两次GC的间隔时间,导致游戏卡顿,因此对于一些会重复使用的对象,我们可以不销毁使用完的对象而是将其保存下来,下次需要使用时先看一看有没有保存好的对象,有的话取出对象,没有对象了再继续实例化对象并在使用完成后保存下来,这样就能实现内存的循环使用,节约内存,这个保存对象的地方就是缓存池。

public class PoolData
{
    public GameObject rootObj;
    public List<GameObject> poolList;

    public PoolData(GameObject obj,GameObject poolObj)
    {
        rootObj = new GameObject(obj.name);
        rootObj.transform.parent = poolObj.transform;
        poolList = new List<GameObject>();
    }

    public void PushObj(GameObject obj)
    {
        poolList.Add(obj);
        obj.transform.parent = rootObj.transform;
        obj.SetActive(false);
    }

    public GameObject GetObject()
    {
        GameObject obj = poolList[0];
        poolList.RemoveAt(0);
        obj.SetActive(true);
        return obj;
    }
}
public class PoolMgr : BaseManager<PoolMgr>
{
    //缓存池容器
    public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
    //作为根节点的空物体
    public GameObject poolRoot;

    public GameObject GetObject(string name)
    {
        GameObject obj = null;
        if(poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
        {
            obj = poolDic[name].GetObject();
        }
        else
        {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
        }
        //断开父子关系
        obj.transform.parent = null;
        return obj;
    }
    public void PushObj(string name,GameObject obj)
    {
        if (poolRoot == null) poolRoot = new GameObject("Pool");

        if (!poolDic.ContainsKey(name))
            poolDic.Add(name, new PoolData(obj,poolRoot));
        poolDic[name].PushObj(obj);
    }

    public void Clear()
    {
        poolDic.Clear();
        poolDic = null;
    }
}

3.事件中心模块

在游戏中,常常出现一个特定的事件发生会导致其他很多事件执行的情况,如怪物死亡后需要分数记录、怪物的继续创建等,这些被调用的方法并不是在同一个脚本中的,那么表现出来的游戏的耦合性就非常高,而且因为被调用的方法往往需要先找到游戏物体再找到其上面的脚本,这样对性能也有浪费,因此我们往往使用委托来记录需要执行的方法,在特定事件发生后执行相应的委托即可,而添加功能或者删除功能时只需要将相应的想要被调用的方法添加入委托或从委托中删去即可。这样的委托往往不止一个,这些委托的管理类就可以称为我们的事件中心。

public class EventCenter : BaseManager<EventCenter>
{
    //存储所有委托的字典
    private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>();

    /// <summary>
    /// 添加方法到事件中
    /// </summary>
    /// <param name="name">事件名称</param>
    /// <param name="action">要存储的函数</param>
    public void AddEventListener(string name,UnityAction<object> action)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] += action;
        }
        else
        {
            eventDic.Add(name, action);
        }
    }

    /// <summary>
    /// 清除事件中的某个方法
    /// </summary>
    /// <param name="name">事件名称</param>
    /// <param name="action">要清除的函数</param>
    public void RemoveEventListener(string name,UnityAction<object> action)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] -= action;
        }
    }

    public void Clear()
    {
        eventDic.Clear();
        eventDic = null;
    }

    /// <summary>
    /// 触发事件
    /// </summary>
    /// <param name="name">事件名称</param>
    public void EventTrigger(string name,object param)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name](param);
        }
    }
}

4.公共Mono模块

这个模块的作用是使没有继承MonoBehaviour的函数也可以实现mono的update函数的效果,具体来说还是利用委托,一个继承了monoBehaviour的类的update函数不断执行委托,然后利用另一个类管理委托,提供注册委托和将方法从委托中移除的函数,然后在外部将想要不断调用的函数注册到委托中即可。这个类中继承MonoBehaviour的脚本在start函数中调用了DontDestroyOnLoad方法,使所挂载的游戏物体不随场景移除,那么这个委托就不会随着场景的加载而被销毁,实现了多场景共用的目的。通过封装,可以使任意函数实现MonoBehaviour的声明周期函数的调用效果,只需要在相应的函数中调用委托,再将方法注册到委托中即可,包括协程也可以使用MonoMgr类进行封装,在外部函数中开启协程。

使用公共Mono模块的意义在于节约性能的开销。在Unity执行update函数时,会遍历所有的继承了MonoBehaviour的类,将所有定义好的update函数先存储在一个list中,存储完成后再遍历list进行调用,所以过多的update会拉长遍历所有的继承MonoBehaviour的类的时间,对性能造成影响,使用公共Mono模块就可以使用一个update函数完成所有的update功能,节约性能。关于这方面的内容,可以参考:https://blogs.unity3d.com/cn/2015/12/23/1k-update-calls/

/// <summary>
/// 继承了mono的类
/// </summary>
public class MonoController : MonoBehaviour
{
    private event UnityAction updateEvent;

    private void Start()
    {
        DontDestroyOnLoad(gameObject);
    }

    private void Update()
    {
        if(updateEvent != null)
        {
            updateEvent();
        }
    }

    /// <summary>
    /// 添加帧更新的函数
    /// </summary>
    /// <param name="fun">添加的函数</param>
    public void AddUpdateListener(UnityAction fun)
    {
        updateEvent += fun;
    }
    /// <summary>
    /// 移除帧更新的函数
    /// </summary>
    /// <param name="fun">移除的函数</param>
    public void RemoveUpdateListener(UnityAction fun)
    {
        updateEvent -= fun;
    }
}
/// <summary>
/// 管理类,对添加和移除委托函数的方法进一步封装,实现单例
/// </summary>
public class MonoMgr : BaseManager<MonoMgr>
{
    private MonoController controller;

    public MonoMgr()
    {
        GameObject obj = new GameObject("MonoController");
        controller = obj.AddComponent<MonoController>();
    }

    /// <summary>
    /// 添加帧更新的函数
    /// </summary>
    /// <param name="fun">添加的函数</param>
    public void AddUpdateListener(UnityAction fun)
    {
        controller.AddUpdateListener(fun);
    }
    /// <summary>
    /// 移除帧更新的函数
    /// </summary>
    /// <param name="fun">移除的函数</param>
    public void RemoveUpdateListener(UnityAction fun)
    {
        controller.RemoveUpdateListener(fun);
    }
}

5.场景切换模块

在游戏中经常要切换场景,如果场景资源很多,在场景切换完成后再进行人物等其他资源加载会有卡顿的感觉,所以我们可以使用异步的方式进行场景切换,在切换的过程中可以执行场景加载进度条更新、人物信息加载等行为,所以可以对场景的切换方法进行进一步封装,达到异步加载场景时执行其他行为的目的。

public class SceneMgr : BaseManager<SceneMgr>
{
    /// <summary>
    /// 切换场景
    /// </summary>
    /// <param name="name">场景名</param>
    public void LoadScene(string name,UnityAction fun)
    {
        //场景同步加载
        SceneManager.LoadScene(name);
        //加载完成后执行fun
        fun();
    }

    /// <summary>
    /// 使用协程异步加载
    /// </summary>
    /// <param name="name"></param>
    /// <param name="fun"></param>
    public void LoadSceneAsyn(string name,UnityAction fun)
    {
        //协程加载场景
        MonoMgr.Instance.StartCoroutine(ReallyLoadSceneAsyn(name, fun));
    }
    private IEnumerator ReallyLoadSceneAsyn(string name,UnityAction fun)
    {
        AsyncOperation ao = SceneManager.LoadSceneAsync(name);
        while (!ao.isDone)
        {
            //事件中心,向外分发进度情况
            EventCenter.Instance.EventTrigger("Loading", ao.progress);
            //更新进度条
            yield return ao.progress;
        }

        fun();
    }
}

6.资源加载模块

 在游戏中经常需要加载资源,无论是从AB包加载还是从Resources文件夹加载等,加载的过程使用同步加载都需要消耗一定的时间,导致游戏卡顿,我们提供一个资源加载的框架,将同步和异步加载作进一步封装,常用的一些操作如游戏物体的实例化、异步加载过程中的进度条显示等也可以在加载的过程中进行。

public class ResMgr:BaseManager<ResMgr>
{
    /// <summary>
    /// 同步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="name">资源名</param>
    /// <returns></returns>
    public T Load<T>(string name) where T:Object
    {
        T res = Resources.Load<T>(name);
        //如果对象是GameObject类型,直接实例化好外界直接使用
        if (res is GameObject)
            return GameObject.Instantiate(res);
        return res;
    }

    /// <summary>
    /// 异步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="name">资源名称</param>
    /// <param name="callback">回调函数</param>
    public void LoadAysnc<T>(string name,UnityAction<T> callback) where T : Object
    {
        MonoMgr.Instance.StartCoroutine(ReallyLoadAsync(name,callback));
    }
    private IEnumerator ReallyLoadAsync<T>(string name, UnityAction<T> callback) where T:Object
    {
        ResourceRequest rr = Resources.LoadAsync<T>(name);
        yield return rr;

        if (rr.asset is GameObject)
            callback(GameObject.Instantiate(rr.asset) as T);
        else
            callback(rr.asset as T);
    }
}

7.输入管理模块

在游戏中,有很多地方需要根据输入进行响应,如玩家需要根据输入进行移动等,我们可以将想要检测的按键通过事件中心进行分发,如果响应的键按下,就分发事件,需要检测按键的地方只需要将按下键后的执行的事情注册到事件中心即可,这样就实现了解耦。同时可以使用公共mono模块优化,将检测按键输入的事件封装为函数注册到公共mono中。

public class InputMgr : BaseManager<InputMgr>
{
    //是否开启所有按键监听的开关
    private bool isOpenInput = false;

    public InputMgr()
    {
        MonoMgr.Instance.AddUpdateListener(Update);
    }

    /// <summary>
    /// 这个update函数是供注册到公共Mono使用的,在公共Mono中调用,这个类并没有继承MonoBehaviour,Unity不会自动调用它
    /// </summary>
    private void Update()
    {
        if (!isOpenInput) return;
        //按键按下,通过事件中心分发事件,如果要监听某事件,将事件注册到事件中心即可
        /*CheckKeyCode(KeyCode.W);
        CheckKeyCode(KeyCode.A);
        CheckKeyCode(KeyCode.S);
        CheckKeyCode(KeyCode.D);*/
        foreach(KeyCode key in Enum.GetValues(typeof(KeyCode)))
        {
            CheckKeyCode(key);
        }
    }
    /// <summary>
    /// 分发事件的函数封装
    /// </summary>
    /// <param name="key">分发事件的具体按键</param>
    private void CheckKeyCode(KeyCode key)
    {
        if (Input.GetKeyDown(key))
        {
            EventCenter.Instance.EventTrigger("Key " + key.ToString() + " Down", key);
        }
        if (Input.GetKeyUp(key))
        {
            EventCenter.Instance.EventTrigger("Key " + key.ToString() + " Up", key);
        }
    }
}

8.音频管理模块

同样的,我们可以在游戏中统一管理杂乱的音效。

public class MusicMgr : BaseManager<MusicMgr>
{
    private AudioSource bkMusic = null;
    private float bkValue = 1;
    private GameObject soundObj;
    private List<AudioSource> soundList = new List<AudioSource>();
    private float soundValue = 1;

    public MusicMgr()
    {
        MonoMgr.Instance.AddUpdateListener(Update);
    }
    private void Update()
    {
        for (int i = soundList.Count - 1; i > -1; i--)
        {
            if (!soundList[i].isPlaying)
            {
                GameObject.Destroy(soundList[i]);
                soundList.RemoveAt(i);
            }
        }
    }
    /// <summary>
    /// 改变音量大小
    /// </summary>
    /// <param name="v"></param>
    public void ChangeBkValue(float v)
    {
        bkValue = v;
        if (bkMusic == null)
            return;
        bkMusic.volume = v;
    }

    /// <summary>
    /// 播放背景音乐
    /// </summary>
    /// <param name="name"></param>
    public void PlayBkMusic(string name)
    {
        if(bkMusic == null)
        {
            GameObject obj = new GameObject("BkMusic");
            bkMusic = obj.AddComponent<AudioSource>();
        }
        ResMgr.Instance.LoadAysnc<AudioClip>("Music/BK/" + name, (clip) =>
         {
             bkMusic.clip = clip;
             bkMusic.volume = bkValue;
             bkMusic.Play();
         });
    }
    /// <summary>
    /// 暂停背景音乐
    /// </summary>
    public void PauseBkMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Pause();
    }
    /// <summary>
    /// 停止背景音乐
    /// </summary>
    public void StopBkMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Stop();
    }
    /// <summary>
    /// 播放音效
    /// </summary>
    public void PlaySound(string name, bool isLoop, UnityAction<AudioSource> callBack = null)
    {
        if(soundObj == null)
        {
            soundObj = new GameObject("Sound");
        }

        ResMgr.Instance.LoadAysnc<AudioClip>("Sound/" + name, (clip) =>
         {
             AudioSource source = soundObj.AddComponent<AudioSource>();
             source.clip = clip;
             source.volume = soundValue;
             source.Play();
             soundList.Add(source);
             if (callBack != null)
                 callBack(source);
         });
    }
    /// <summary>
    /// 停止音效
    /// </summary>
    /// <param name="source"></param>
    public void StopSound(AudioSource source)
    {
        if (soundList.Contains(source))
        {
            soundList.Remove(source);
            source.Stop();
            GameObject.Destroy(source);
        }
    }
    /// <summary>
    /// 改变所有音效的音量
    /// </summary>
    /// <param name="value"></param>
    public void ChangeSoundValue(float value)
    {
        soundValue = value;
        for (int i = 0; i < soundList.Count; i++)
        {
            soundList[i].volume = value;
        }
    }
}

9.UI管理模块

游戏中的UI常常做成一个个面板预制体,但是面板之间存在很多的点击事件或者相互调用,所以为了避免千头万绪的调用,也就是实现解耦合,方便修改维护,我们需要使用一个管理模块统一管理所有的面板。

public class BasePanel : MonoBehaviour
{
    private Dictionary<string, List<UIBehaviour>> controlDic = new Dictionary<string, List<UIBehaviour>>();

    protected virtual void OnClick(string name)
    {

    }

    private void Awake()
    {
        FindChildrenControl<Button>();
        FindChildrenControl<Image>();
        FindChildrenControl<Text>();
        FindChildrenControl<Toggle>();
        FindChildrenControl<Slider>();
        FindChildrenControl<ScrollRect>();
    }
    /// <summary>
    /// 得到面板中的某个组件
    /// </summary>
    /// <typeparam name="T">组件类型</typeparam>
    /// <param name="controlName">组件名称</param>
    /// <returns></returns>
    protected T GetControl<T>(string controlName) where T : UIBehaviour
    {
        if (controlDic.ContainsKey(controlName))
        {
            for (int i = 0; i < controlDic[controlName].Count; i++)
            {
                if (controlDic[controlName][i] is T)
                    return controlDic[controlName][i] as T;
            }
        }
        return null;
    }
    /// <summary>
    /// 找到子对象的对应控件
    /// </summary>
    /// <typeparam name="T">控件类型</typeparam>
    private void FindChildrenControl<T>() where T:UIBehaviour
    {
        T[] controls = this.GetComponentsInChildren<T>();
        string objName;
        for (int i = 0; i < controls.Length; i++)
        {
            objName = controls[i].gameObject.name;
            if (controlDic.ContainsKey(objName))
                controlDic[objName].Add(controls[i]);
            else
                controlDic.Add(objName, new List<UIBehaviour>() { controls[i] });

            if(controls[i] is Button)
            {
                (controls[i] as Button).onClick.AddListener(() =>
                {
                    OnClick(objName);
                });
            }
        }
    }
}
public enum E_UI_Layer
{
    bot,
    mid,
    top,
    system,
}

public class UIManager : BaseManager<UIManager>
{
    public Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();

    private Transform canvas;
    private Transform bot;
    private Transform mid;
    private Transform top;
    private Transform system;

    public UIManager()
    {
        GameObject obj = ResMgr.Instance.Load<GameObject>("UI/Canvas");
        canvas = obj.transform;
        GameObject.DontDestroyOnLoad(obj);

        bot = canvas.Find("Bot");
        mid = canvas.Find("mid");
        top = canvas.Find("Top");
        system = canvas.Find("System");

        obj = ResMgr.Instance.Load<GameObject>("UI/EventSystem");
        GameObject.DontDestroyOnLoad(obj);
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板脚本类型</typeparam>
    /// <param name="panelName">面板名</param>
    /// <param name="layer">面板层级</param>
    /// <param name="callBack">回调函数,面板创建完成后完成的行为</param>
    public void ShowPanel<T>(string panelName,E_UI_Layer layer,UnityAction<T> callBack=null) where T : BasePanel
    {
        //有面板直接加载面板,没有异步加载面板
        if (panelDic.ContainsKey(panelName))
        {
            BasePanel panel = panelDic[panelName];
            if(panel is T)
            {
                panel.gameObject.SetActive(true);
                if (callBack != null)
                    callBack(panel as T);
            }
        }
        else
            ResMgr.Instance.LoadAysnc<BasePanel>("UI/" + panelName, (obj) =>
             {
                 Transform father = bot;
                 switch (layer)
                 {
                     case E_UI_Layer.system:
                         father = system;
                         break;
                     case E_UI_Layer.mid:
                         father = mid;
                         break;
                     case E_UI_Layer.top:
                         father = top;
                         break;
                 }
                 obj.transform.SetParent(father);
                 obj.transform.localPosition = Vector3.zero;
                 obj.transform.localScale = Vector3.one;

                 (obj.transform as RectTransform).offsetMax = Vector2.zero;
                 (obj.transform as RectTransform).offsetMin = Vector2.zero;

                 T panel = obj.GetComponent<T>();
                 if (callBack != null)
                     callBack(panel);

                 panelDic.Add(panelName, panel);
             });
    }
    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <param name="panelName">面板名称</param>
    public void HidePanel(string panelName)
    {
        if (panelDic.ContainsKey(panelName))
        {
            panelDic[panelName].gameObject.SetActive(false);
        }
    }

    public T GetPanel<T>(string name) where T : BasePanel
    {
        if (panelDic.ContainsKey(name))
        {
            BasePanel panel = panelDic[name];
            if (panel is T)
                return panelDic[name] as T;
        }
        return null;
    }
}

 

原文地址:https://www.cnblogs.com/movin2333/p/14480790.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


这篇文章主要介绍了Unity游戏开发中外观模式是什么意思,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章主要介绍Unity中地面检测方案的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1.普通射线在角色坐标(一般是脚底)...
这篇文章主要介绍了Unity游戏开发中如何消除不想要的黄色警告,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带...
这篇文章主要介绍了Unity中有多少种渲染队列,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解
这篇文章主要介绍Unity中如何实现Texture,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!了解Texture2D 如上图,Texture2D是一张
小编给大家分享一下Unity中DOTS要实现的特点有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让...
这篇文章给大家分享的是有关unity中如何实现UGUI遮罩流光特效的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。下面是核心shader:Sh...
这篇文章主要为大家展示了“Unity中如何实现3D坐标转换UGUI坐标”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下...
这篇文章主要介绍了Unity游戏开发中设计模式的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章主要介绍了Unity中如何实现仿真丝袜渲染,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了...
这篇文章给大家分享的是有关Unity插件OVRLipSync有什么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。项目需要接入对话口型动...
这篇文章主要介绍了Unity性能优化之DrawCall的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家...
这篇文章给大家分享的是有关Unity给力插件之Final IK怎么用的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。这插件有什么用:一般游...
这篇文章给大家分享的是有关Unity中如何内嵌网页插件UniWebView的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。一、常见Unity中内...
小编给大家分享一下Unity如何做流体物理的几个轮子,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让...
小编给大家分享一下Unity中Lod和Occlusion Culling的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收...
这篇文章将为大家详细讲解有关Unity中LineRenderer与TrailRenderer有什么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获...
这篇文章主要介绍了Unity中coroutine问题的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起...
这篇文章将为大家详细讲解有关unity中spine怎么用,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。骨骼动画首先我们来看到...
这篇文章主要为大家展示了“Unity Shader后处理中如何实现简单均值模糊”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学...