Unity3d 周分享(18期 2019.6.1 )

选自过去1~2周 自己所看到外文内容:https://twitter.com/unity3d 和各种其他博客来源吧 

 

1、

1)Unity x Android Studio混用經驗分享---Laird / 果思設計 資深工程師 【台湾】

2)關於Unity快速搭建3D場景的最最最入門小技巧---Wei J / CyberTail Games Co-founder

3)如何處理出好看的卡通渲染---Kuro / 火炬 共同創辦人

https://www.youtube.com/watch?v=ZazQ9MiGEXk

常见两种方式, Android 导出 jar, .aar 为文件到Unity中作为Plugins用, 或者Unity Export Project 工程然后 安卓开发。 作者分享的是第二种。

Unity3d 周分享(18期 2019.6.1 )_第1张图片

Unity3d 周分享(18期 2019.6.1 )_第2张图片

Unity3d 周分享(18期 2019.6.1 )_第3张图片

Unity3d 周分享(18期 2019.6.1 )_第4张图片

Unity3d 周分享(18期 2019.6.1 )_第5张图片

 

 

 

2、SRP ScriptableRenderPipeline

https://github.com/Unity-Technologies/ScriptableRenderPipeline

 

 

 

 

https://unity.com/srp

利用Unity的功能自己写一个 :

https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/

https://github.com/keijiro/Retro3DPipeline

 

 

3、 在Unity中阴影的实现方式:

https://luumoon.github.io/2018/02/28/Planar-Shadow-%E5%AE%9E%E7%8E%B0%E5%8F%8A%E5%BA%94%E7%94%A8/

1) Unity自带阴影

2)Projector Shadow方式

其中有插件,如Dynamic Shadow Projector (免费)。

https://zhuanlan.zhihu.com/p/42433900 看评论好像这种方案的性能并没好出太多~~

3)Planar Shadow

听说是镇魔曲游戏使用这个方案

  1. https://forum.unity.com/threads/shader-sharing-the-most-simple-shadow-shader-planar-projection-shadowing.319830/
  2. https://github.com/ozlael/PlannarShadowForUnity
  3. http://qiankanglai.me/2016/12/23/planar-shadow/index.html

4)Unity 2017 Tutorial - Blob Shadow Projector - Fake Shadow - YouTube

就是使用Unity提供的 Projector 组件, 做假的阴影, 正常如果游戏地面平台 可以使用一个假的阴影模型, 但是效果很差。 但是都说Projector有些性能问题。 比如Unity Standard Assets 中有 Effects.unitypackage -》 Projectors -》 BlobShadowProjector.prefab

5) 直接使用一个假的模型片 (不动的npc, 小范围移动的怪(地要平),这个模型片也要高于地面一些, 简单粗暴,可以考虑使用, Unity动态合批,这种阴影都可以合并为一个DC) Unlit/Transparent shader就行

Unity3d 周分享(18期 2019.6.1 )_第6张图片

 

 

4、 Unity Chan的卡通渲染Shader, UTS已經到2.0.6版了,作者小林信行也把它和Aura 2, 一套製作環境光源和霧氣的套件整合,重新賦予2014年所發佈的演唱會專案新的感覺。

想做VTuber類型專案,一定要研究這個Shader。

https://www.youtube.com/watch?v=UqHwICpl5ps&feature=share

UTS2 v.2.0.6 : https://github.com/unity3d-jp/UnityChanToonShaderVer2_Project

Aura 2 - Volumetric Lighting & Fog :https://assetstore.unity.com/packages/tools/particles-effects/aura-2-volumetric-lighting-fog-137148

 

 

 

 

 

Unite Beijing 2018的一段Unity烘焙光照演說,仔細說明了參數設定與光照烘焙注意事項,非常值得一聽。

https://www.youtube.com/watch?v=5s2V_E125co&fbclid=IwAR1oZ3AtCq8t-3cP5QrKTXNY1UCLmTFIlbpCTGuPJFjWIV12lV6Mus7Vsfw

Unity3d 周分享(18期 2019.6.1 )_第7张图片

众多技术大牛在Unite Beijing 2018中为开发者带来了精彩的技术演讲,内容涵盖游戏开发、技术美术、工业等多个领域。错过本次大会的开发者也无需担心,Connect为大家整理了文字版演讲实录,让我们看看都有哪些内容吧!

  1. 浅谈伽玛和线性颜色空间
  2. Unity Shader着色器优化
  3. Lightmap烘焙最佳实践
  4. 解析AssetBundle https://www.youtube.com/watch?v=mMjcDjM8Fm8
  5. 玩转移动端大型世界开发 刘伟贤 ─【Unity 議程】Unity MMORPG 遊戲優化https://www.youtube.com/watch?v=Z1zE-AWgYZ4
  6. 基于照片建模的游戏制作流程
  7. Playable API:定制你的动画系统 成亮 ─ 【Unity 議程】利用 Cinemachine 實作遊戲運鏡系統 https://www.youtube.com/watch?v=3hDvBcQZxKQ
  8. Unity工业之路
  9. 使用自定义渲染纹理实现炫酷特效
  10. 未来影像,影向未来
  11. 《崩坏3》:在Unity中实现高品质的卡通渲染(上)
  12. 《崩坏3》:在Unity中实现高品质的卡通渲染(下)
  13. 育碧-《Eagle Flight》背后的研发
  14. 《旅行青蛙》小规模开发的匠心独运
  15. 江毅冰-《从AAA游戏到实时渲染的动画电影》
  16. ProBuilder快速关卡建模实践
  17. 利用Cinemachine快速创建游戏中的相机系统

 

5、

https://connect.unity.com/p/fake-nulle-a-operator-i

运算符 "??" 和 "?." 是假的空

对象存储在内存中的哪个位置?

在我更多地谈论假空值之前,我需要解释一下内存管理的方式以及特定对象存储在内存中的位置。

类的实例instancje (zwykłych)存储在哪里?

下面是我们可以在C#的帮助下创建的典型类的示例。

 class ExampleClass
    {
    }
只要我们引用它们,这些类的实例将保留在内存中。 
  ExampleClass instancja = new ExampleClass();
如果我们没有引用它们,它们将被垃圾收集器删除。这些类的实例将存在于托管内存中
  ExampleClass instancja = new ExampleClass();
        // 我赋值null因此我失去了对ExampleClass实例的引用
        instancja = null;

我赋值null因此我 对ExampleClass类实例的引用

继承自UnityEngine.Object类的类的实例存储在何处?

从UnityEngine.Object 类开始,它们继承:

  • gameObject
  • 所有组件(例如Transform,Rigidbody等)
  • 所有Asset(例如,Texture,ScriptableObject,AudioClip等)

这些对象同时在本机和托管内存中。

它是由什么产生的?

Unity编辑器是用C ++编写的。在这种语言中,已经实现了各个类和编辑器功能的操作。我们无法直接获得各个类的实现。

我们在C#中编写与我们的游戏相关的代码,因为它的简单性(相对于C ++语言)。

我们在脚本中使用的与资产,组件和游戏对象相关的类是包装器,它允许您处理实际位于本机端的实例实例。

下面是GameObject 类的示例。该类不实现gameObjects本身。它允许您管理本机端的gameObject实例并与之通信。

Unity3d 周分享(18期 2019.6.1 )_第8张图片

概括

从UnityEngine.Object 类继承的对象实例同时存在于本机和托管内存中,其他对象的实例仅存在于托管内存中。

Unity3d 周分享(18期 2019.6.1 )_第9张图片

检查对象是否存在的方法

首先让我讨论UnityEngine.Object 类中的方法。

UnityEngine.Object 上的转换 bool运算符

UnityEngine.Object 类有一个特殊的运算符,允许您检查对象是否存在。

https://docs.unity3d.com/ScriptReference/Object-operator_Object.html

using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
    void Start()
    {
        GameObject gameObject = new GameObject();
        //我们可以直接将对象分配给bool值以检查它是否存在
        bool obiektIstnieje = gameObject;
        // 我们也可以直接否认对象的存在
        bool obiektNieIstnieje = !gameObject;
        // 或者将对象 放在if 中
        if (gameObject)
        {
            Debug.Log(gameObject);
        }
        //而不用直接判空
        //if (gameObject!=null)
        //{
        //}
    }
}

操作符 == 和 !=

要检查对象是否存在,我们还可以使用operator ==或!=

using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
    void Start()
    {
        GameObject gameObject = new GameObject();
      // 我们检查对象是否为null
        if (gameObject!=null)
        {
            Debug.Log(gameObject);
        }
    }
}

方法函数 System.Object.ReferenceEquals() 和 System.Object.Equals()

这是另外两种检查对象是否存在的方法。

但是,正如您将立即看到的那样,对于从UnityEngine.Object 继承的对象,它们并不总是在Unity编辑器中正常工作。以下示例检查gameObject在尚未初始化时是否为null。

我们在编辑器中执行测试。

using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
    public GameObject myGameObject;
    void Update()
    {
        Debug.Log("对象是null吗?");  // Czy obiekt jest nullem?
        bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null);
        Debug.Log("referenceEquals: " + referenceEquals);
        bool equals = System.Object.Equals(myGameObject, null);
        Debug.Log("equals: " + equals);
        // 为了比较,我们还使用operator ==
        bool operatorPorównania = myGameObject == null;
        Debug.Log("operatorPorównania: " + operatorPorównania);      // 比较运算符   
    }
}

请注意前两种方法的值不正确。这些方法返回对象仍然存在的信息。为什么会这样?

Unity3d 周分享(18期 2019.6.1 )_第10张图片

上面的运算符如何检查UnityEngine.Object 是否为空?

在最开始,我提到继承自UnityEngine.Object 类的对象实例存在于本机内存中,并且托管内存中有一个包装器,用于在本机内存中进行通信和实例管理。

见下图。

Unity3d 周分享(18期 2019.6.1 )_第11张图片

方法System.Object.ReferenceEquals()和System.Object.Equals()比较存在于对象实例管理的内存。

 

但是,运算符==,!=和对象转换为bool 引用本机内存中的对象实例,忽略托管部分中实例的存在。因此,我们有时可能会遇到这样的情况:即使对象仍然存在于托管页面上,这些操作符将“假装为空对象”。编辑器的这种机制称为"fake null"。

Unity3d 周分享(18期 2019.6.1 )_第12张图片

什么是"fake null"?

Unity编辑器有一个特殊的机制,用于处理从UnityEngine.Object 类继承的对象的null 。

当我们在Destroy()方法的帮助下加载新场景或销毁对象时,我们会销毁本机端的对象。然后,垃圾收集器将删除托管页面上对象的包装。

当一个对象在本机和托管端不存在时,它是一个“ 真正的空”。

如果对象在本机端不存在并且仍然存在于管理端,则该对象将是"fake null"。

在什么情况下设施将是“假nullem”?

第一种情况 - “ 假nulle ”仅存在于编辑器中。编译的应用程序中没有“ 假nulle ” - 对象将存在与否。

该物体可能是“ 假空字符串”,例如,在对象从方法去除的情况下销毁(方法破坏本机端对象),并在旁边还有个管理垃圾收集器,因为它尚未删除。(下面是一个示例代码)

using System.Collections;
using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
    public GameObject myGameObject;
    private void Start()
    {
        myGameObject = new GameObject();
    }
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
            StartCoroutine(DestroyAndLogInfo());
    }
    IEnumerator DestroyAndLogInfo()
    {
        // 我们摧毁了这个物体
        Destroy(myGameObject);
        // 只有在调用Update()完成后才会销毁对象
        // 这就是我们等待一帧写出日志的原因
        yield return null; 
        Debug.Log("Czy obiekt jest nullem?");   // 对象是null吗?
        bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null);
        Debug.Log("referenceEquals: " + referenceEquals);
        bool equals = System.Object.Equals(myGameObject, null);
        Debug.Log("equals: " + equals);
        bool operatorPorównania = myGameObject == null;
        Debug.Log("operatorPorównania: " + operatorPorównania);
    }
}

为什么System.Object.ReferenceEquals()和System.Object.Equals()方法不能正确地用于UnityEngine.Object?

这两种方法对于"fake null"不起作用,因为这些方法比较了管理方的引用。在"fake null" 的情况下,该对象仍将存在于托管内存中,即使本机不再存在。

Unity3d 周分享(18期 2019.6.1 )_第13张图片

System.Object.ReferenceEquals()和System.Object.Equals()方法何时适用于UnityEngine.Object?

这些方法在编译应用程序后将正常工作,因为"fake null"仅存在于Unity编辑器中。

我们还可以在代码中指定空值。这样,"fake null"将变为“ 真正的空”。

 using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
    public GameObject myGameObject;
    void Update()
    {
        //我们分配一个空值来获得“真正的空”
        myGameObject = null;
        Debug.Log("Czy obiekt jest nullem?");
        bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null);
        Debug.Log("referenceEquals: " + referenceEquals);
        bool equals = System.Object.Equals(myGameObject, null);
        Debug.Log("equals: " + equals);
        bool operatorPorównania = myGameObject == null;
        Debug.Log("operatorPorównania: " + operatorPorównania);
    }
}

现在这些方法返回与== 运算符相同的值。

Unity3d 周分享(18期 2019.6.1 )_第14张图片

使用"fake null"有什么好处?

由于"fake null"机制,Unity编辑器可以获得有关不存在的对象的更多详细信息。

下面您可以看到每个空值显示的错误比较。

在"fake null" 的情况下错误更详细包含我没有引用的信息。“ 真的空”的错误显示有关缺失引用的更差的信息。

Unity3d 周分享(18期 2019.6.1 )_第15张图片

using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
	// 未初始化的对象
	public GameObject myGameObject;
	void Update ()
	{
		//如果我们分配一个空值,那么我们得到一个“真正的空”
		// 如果我们评论这一行,我们将得到“fake null”
		myGameObject = null;
		// 我指的是activeSelf大小来触发错误
		bool a = myGameObject.activeSelf;
	}
}

有关"fake null"的更多详细信息,请参阅此帖子:

https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/

 

Fake null a operator "??" 和 "?:".

首先,几句话会提醒您这些操作符是如何运作的。

操作员“??”如何工作?

Null-coalescing运算符或“ ?? ”以这样的方式工作:当对象存在时,它返回此对象,如果对象为null,则返回运算符“ ?? ” 右侧的值。

https://docs.microsoft.com/pl-pl/dotnet/csharp/language-reference/operators/null-coalescing-operator

下面是一个示例代码:

using UnityEngine;
public class NullCoalescingOperator : MonoBehaviour
{
    void Start()
    {
        // 对象为null时的情况
        string obiektNieIstnieje = null;
        string wynik = obiektNieIstnieje ?? "Wartość z prawej strony operatora ??";  // 操作员右侧的价值?
        Debug.Log("Gdy mamy referencje do nulla: "+ wynik);   // 当我们引用null时:
        // 对象存在时的情况
        string obiektIstnieje = "Obiekt!";   // 对象!
        string wynik2 = obiektIstnieje ?? "To zostanie zwrócone jeśli obiekt przed operatorem ?? jest nullem";
        Debug.Log("Gdy mamy referencje do obiektu: " + wynik2);
    }    
}

控制台窗口中将显示以下日志:

Unity3d 周分享(18期 2019.6.1 )_第16张图片

 

“?.” 如何工作?

空条件运算符 "?." 或 "?[]".

?. 是与null比较的缩短版本。如果对象为null,则不会调用 ?. 后面的代码。

下面是一个示例代码,可帮助您了解此运算符:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NullConditionalOperator : MonoBehaviour {
     void Start()
    {
        string wynik = "";
        string obiektNieIstnieje = null;
		wynik = obiektNieIstnieje?.Replace(oldChar: 'A', newChar: 'Z');
		Debug.Log("Gdy mamy referencje do nulla: "+ wynik);
        // 使用运算符相当于上面的代码?.
        //if (obiektNieIstnieje != null)
        //{
        //    wynik = obiektNieIstnieje.Replace(oldChar: 'A', newChar: 'Z');
        //}
        //Debug.Log("Gdy mamy referencje do nulla: " + wynik);
        string obiektIstnieje = "AAAAAAA!";
		wynik = obiektIstnieje.Replace(oldChar: 'A', newChar: 'Z');
		Debug.Log("Gdy mamy referencje do obiektu: " + wynik);
	
    }
}

控制台窗口中将显示以下日志:

 

使用运算符“??”和“?.” 时为什么要小心

上述运算符与检查对象是否为空密切相关。他们将对象与管理方面的null进行比较,因此它们无法正常用于"fake null"。

下面是在“ 真的空” 的情况下这些运算符的操作差异的示例。

所有资产,组件和游戏对象都继承自UnityEngine.Object 类。

Unity3d 周分享(18期 2019.6.1 )_第17张图片

using UnityEngine;
public class FakeNullTest : MonoBehaviour
{
	// 未初始化的对象
	public GameObject myGameObject;
	void Update ()
	{
        // 如果我们分配一个空值,那么我们得到一个“真正的空”
        // 如果我们注释这一行,我们将得到“fake null”
          myGameObject = null;      
        string name = "AAAA";
        // 如果myGameObject存在,则返回其名称
        // 否则没有任何返回
        name = myGameObject?.name;
        Debug.Log("?. "+ name);
        // 如果myGameObject存在,则返回它
        // 如果你还没有  就创建一个新的gameObject
        GameObject temp = myGameObject ?? new GameObject("Nowo utworzony GameObject");
        Debug.Log("?? " + temp.name);
    }
}

因此,对于从UnityEngine.Object 类继承的对象,请不要使用这些运算符。用作运算符!= ,== 和转换为bool 的替代品,因为它们支持“ Fake null ”。

在这里,您可以找到有关如何替换这两个运算符的信息:

https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object

关于“fake null ”主题的参考书目和其他材料。

https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/

https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object

https://answers.unity.com/questions/1465702/getcomponent-does-never-return-null-possible-bug.html?childToView=1466149#comment-1466149

https://answers.unity.com/questions/1378330/-operator-not-working-as-expected.html?childToView=1378682#comment-1378682

https://answers.unity.com/questions/1398006/2d-array-initialized-but-send-null.html

https://answers.unity.com/questions/1236014/-operator-not-working.html

https://www.youtube.com/watch?v=feRt_q9wRz0&list=PLfurnpjsyGQItrxGMNLzphKIemfXWhVmg

 

 

4、达哥 讲 Unity优化: https://www.youtube.com/watch?v=Cjh5naX8UU8

Unity台北場優化分享 II PPT : https://www.slideshare.net/KelvinLo5/unity-ii?fbclid=IwAR1CRnFkMxj_oblBnVXfDGaBkZ9eBpTRtRykXUy8DezbAk-DgpqyLZEBL9g

https://blog.csdn.net/u010019717/article/details/90729425

 

 

5、 关于: Draw Call 和 SetPass Call 两个指标哪个更重要?

SetPass Calls: 56 Draw Calls: 90 Total Batches: 73 Tris: 6.1k Verts: 10.2k

(Dynamic Batching) Batched Draw Calls: 41 Batches: 24 Tris: 0 Verts: 1.0k

(Static Batching) Batched Draw Calls: 0 Batches: 0 Tris: 0 Verts: 0

(Instancing) Batched Draw Calls: 0 Batches: 0 Tris: 0 Verts: 0

 

Profiler 中: Draw Calls【未合批之前的数量】 = Total Batches【Stats 窗口中的Batches】 + Saved by batching 【Stats 窗口中的】

上面的数据只有 动态批处理, 所以 : Batched Draw Calls = Batches + Saved by batching

还是要弄懂这几个值:

FrameDebug 窗口中显示的是实际的DrawCall 数, 也就是Total Batches【Stats 窗口中的Batches】。

Save by batching是 Unity靠Batching为我们节省了多少个Draw Call

 

https://unity3d.com/cn/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games

UWA 上说 SetPass Call 更重要 !

https://blog.uwa4d.com/archives/QA_Rendering.html

https://answer.uwa4d.com/question/58d29b8b5a5050b366a6b6ae

https://answer.uwa4d.com/question/59ddc3fa43cf099e2d2295be

简单来说,Unity引擎中Total Batches是我们建议最需要关注的指标。

1个Setpass Call或者Batch,相当于是一次Render State的切换,而1个Draw Call则是CPU让GPU去进行渲染某一个Object的1次操作。在当前的移动设备中,1次Render State的切换要1个Draw Call本身要耗时。所以,Total Batches是我们较为建议的关注指标,也是UWA性能报告中所提供的Draw Call查看指标。而Frame Debugger中,其数量是与Total Batches相一致的,即查看的是每一个Batch的渲染物体。

更为详细一些的说明,可以看 这个帖子 。

而Unity Profiler中的Draw Call,其理论上对应的则是glDrawElements的调用次数,其与高通或其他第三方工具所返回的Gl Trace信息操作数量不太一致,但应该与其中的glDrawElements API的调用次数基本一致,题主可以自行检测看看。

SetPass calls:在Unity 4.x和3.x原来的Stats面板的第一项是“Draw calls”,然而到了Unity5.X版本,Stats上没有了“Draw calls”,却多出来一项”SetPass calls“。

比如说场景中有100个gameobject,它们拥有完全一样的Material,那么这100个物体很可能会被Unity里的Batching机制结合成一个Batch。所以用“Batches”来描述Unity的渲染性能是不太合适的,它只能反映出场景中需要批处理物体的数量。那么可否用“Draw calls”来描述呢?答案同样是不适合。每一个“Draw calls”是CPU发送个GPU的一个渲染请求,请求中包括渲染对象所有的顶点参数、三角面、索引值、图元个数等,这个请求并不会占用过多的消耗,真正消耗渲染资源的是在GPU得到请求指令后,把指令发送给对应物体的Shader,让Shader读取指令并通知相应的渲染通道(Pass)进行渲染操作。

假设场景中有1个gameobject,希望能显示很酷炫的效果,它的Material上带有许多特定的Shader。为了实现相应的效果,Shader里或许会包含很多的Pass,每当GPU即将去运行一个Pass之前,就会产生一个“SetPass call”,因此在描述渲染性能开销上,“SetPass calls”更加有说服力。

向GPU发送命令时发生的最昂贵的操作是SetPass调用。如果我们的游戏由于向GPU发送命令而受CPU限制,减少SetPass调用的数量可能是提高性能的最佳方法。

 

https://forum.unity.com/threads/setpass-calls-vs-drawcalls.508979/

SetPass refers to the material setup, and DrawCalls refer to each item submitted for rendering.

When batching is enabled, Unity can perform a single SetPass call, and then submit multiple draw calls, re-using the same material state.

SetPass指的是材质设置,DrawCalls指的是提交渲染的每个项目。

启用批处理后,Unity可以执行单个SetPass调用,然后提交多个绘制调用,重新使用相同的材​​质状态。

 

渲染简介

简单地看一下Unity渲染帧时会发生什么。

注意:在本文中,我们将使用术语“object对象”来表示可以在我们的游戏中呈现的对象。任何带有Renderer组件的GameObject都将被称为对象。

在最基本的层面上,渲染可以描述如下:

  • 中央处理单元,被称为CPU,找出必须绘制的内容以及如何绘制。。
  • CPU将指令发送到图形处理单元,称为GPU。
  • GPU根据CPU的指令绘制内容。

对于每个渲染帧,CPU执行以下工作:

  • CPU检查场景中的每个对象以确定是否应该渲染它。只有满足某些条件才会呈现对象; 例如,其边界框的某些部分必须位于相机的视锥体内。据说将无法渲染的对象被剔除。
  • CPU收集有关将要呈现的每个对象的信息,并将此数据排序为称为draw calls绘图调用命令。绘图调用包含有关单个网格的数据以及应如何呈现该网格; 例如,应该使用哪些纹理。在某些情况下,共享设置的对象可以组合到同一个绘图调用中。将不同对象的数据组合到同一个绘制调用中称为批处理batching。
  • CPU 为每个绘制调用创建一个称为batch的数据包。批量有时可能包含绘制调用以外的数据,但这些情况不太可能导致常见的性能问题,因此我们不会在本文中考虑这些。

 

对于包含绘制调用的每个批处理batch,CPU现在必须执行以下操作:

  • CPU可以向GPU发送命令以将多个已知的变量统一地改变为 render state。此命令称为SetPass call。SetPass调用告诉GPU用于渲染下一个网格的设置。仅当要渲染的下一个网格需要从前一个网格更改渲染状态时,才会发送SetPass call。
  • CPU将绘图调用发送到GPU。绘图调用指示GPU使用最近的SetPass调用中定义的设置呈现指定的网格。
  • 在某些情况下,批次batch可能需要多次pass。pass是着色器代码的一部分,新pass需要更改渲染状态。对于批处理中的每个pass,CPU必须发送新的SetPass call,然后必须再次发送draw call。

 

同时,GPU执行以下工作:

  • GPU按照发送顺序处理来自CPU的任务。
  • 如果当前任务是SetPass call,则GPU更新渲染状态。
  • 如果当前任务是draw call,则GPU渲染网格。这是分阶段发生的,由着色器代码的不同部分定义。渲染的这一部分很复杂,我们不会详细介绍它,但是我们理解一段称为顶点着色器的代码告诉GPU如何处理网格的顶点,然后是一段代码称为片段着色器告诉GPU如何绘制单个像素。
  • 重复此过程,直到GPU处理完所有从CPU发送的任务为止。

 

从广义上讲,CPU为了渲染帧而必须执行的工作分为三类:

  • 确定必须绘制的内容
  • 准备GPU的命令
  • 将命令发送到GPU

将命令发送到GPU

将命令发送到GPU所花费的时间是游戏受CPU限制的最常见原因。

向GPU发送命令时发生的最昂贵的操作是SetPass调用。如果我们的游戏由于向GPU发送命令而受CPU限制,减少SetPass调用的数量可能是提高性能的最佳方法。

我们可以看到在Unity的Profiler窗口的渲染分析器中发送了多少个 SetPass calls和batches。在性能受损之前可以发送的SetPass调用的数量在很大程度上取决于目标硬件。

SetPass calls的数量及其与batches数量的关系取决于几个因素,我们将在本文后面更详细地介绍这些主题。但是,通常情况是:

  • 在大多数情况下,减少批次数和/或使更多对象共享相同的渲染状态将减少SetPass调用的数量。
  • 在大多数情况下,减少SetPass调用的数量将提高CPU性能。

如果减少批量数量并不会减少SetPass调用的数量,那么它仍然可以带来性能提升。这是因 大致有三种减少批次和SetPass调用的方法。具体看文档!:

  • 减少要渲染的对象数量可能会减少批量和SetPass调用。 (减少角色数量,相机视锥体剔除距离,用距离隐藏对象Layer Cull Distances, 遮挡剔除 )
  • 减少每个对象必须呈现的次数通常会减少SetPass调用的次数。 (实时照明,阴影和反射,确切的说取决于我们为游戏选择的渲染路径)
  • 将必须渲染的对象数据组合成较少的批次将减少批次数。 (静态批处理,动态批处理,GPU Instancing , 图集, 网格合并, 小心Renderer.material产生副本 )

 

 

 

 

优化不能盲目的进行优化。 https://unity3d.com/learn/tutorials/temas/performance-optimization/diagnosing-performance-problems-using-profiler-window?playlist=44069

首先要确定是CPU 还是GPU 导致的性能问题更为严重:

Profiler , GPU Usage 中, 在中间有一个 CPU:xx ms GPU: xx ms 的显示, 通过比较这两个值就可以确认。

Unity3d 周分享(18期 2019.6.1 )_第18张图片

  • 确定我们的游戏是否受CPU限制 及其解决办法
  • GC分析及其 解决办法
  • 物理分析 及其解决办法
  • 慢脚本分析 及其解决办法

 

 

关注时间比关注帧率可能更重要:

虽然帧速率是谈论游戏性能的常用方式,但当我们尝试提高游戏性能时,我们更有用的是考虑以毫秒为单位渲染帧所需的时间。这有两个原因。首先,这是一个更精确的措施。当我们努力提高游戏性能时,每毫秒都可以计入我们的目标。其次,帧率的相对变化意味着在不同尺度上的不同事物。从60到50 FPS的变化表示额外的3.3 ms的处理时间,但是从30到20 FPS的变化表示额外的16.6 ms的处理时间。这两个示例都是10 FPS下降,但渲染帧所花费的时间差异很大。

我们有必要了解帧必须渲染多少毫秒才能满足常见的帧速率。要找到这个数字,我们应该遵循公式1000 / [所需的帧速率]。使用这个公式,我们可以看到,对于每秒渲染30帧的游戏,它必须在33.3毫秒内渲染每个帧。对于以60 FPS运行的游戏,它必须在16.6毫秒内渲染每个帧。

对于渲染的每个帧,Unity必须执行许多不同的任务。简单来说,Unity必须更新游戏状态,拍摄游戏快照,然后将快照绘制到屏幕上。每帧期间必须执行的任务包括读取用户输入,执行脚本和执行照明计算等操作。除此之外,还有一些操作可以在单帧期间多次发生,例如物理计算。当所有这些任务都足够快地执行时,我们的游戏将具有一致且可接受的帧速率。当所有这些任务都无法足够快地执行时,帧渲染时间太长,帧速率将下降。

了解哪些任务执行时间过长对于了解如何解决我们的性能问题至关重要。一旦我们知道哪些任务正在降低我们的帧速率,我们就可以尝试优化游戏的那一部分。这就是分析如此重要的原因:分析工具向我们展示了每个任务在任何给定帧中所花费的时间。

 

 

 

 

 

 

6、 介绍“Unity Delayed Asset”,它可以从Inspector上的Reference而不是文件路径读取Resources文件夹的资源

https://github.com/Trisibo/Unity-delayed-asset

using UnityEngine;
public class Example : MonoBehaviour
{
    private void Update()
    {
        if ( Input.GetKeyDown( KeyCode.Space ) )
        {
            var texture = Resources.Load( "hoge" );
        }
    }
}

通常,在使用Resources.Load时,会编写如上所示的代码。

using Trisibo;
using UnityEngine;
public class Example : MonoBehaviour
{
    [SerializeField, DelayedAssetType( typeof( Texture ) )]
    private DelayedAsset m_textureReference;
    private void Update()
    {
        if ( Input.GetKeyDown( KeyCode.Space ) )
        {
            var texture = m_textureReference.Load() as Texture;
        }
    }
}

Unity3d 周分享(18期 2019.6.1 )_第19张图片

如果您使用“Unity Delayed Asset”,您将能够编写这样的代码。

与通常的书写风格相比,源代码将是多余的。

您可以在Inspector中的Resources.Load中设置要加载的资产

加载场景时,不会加载Inspector中设置的资源,但会在调用Load函数时加载

注: 思想挺好的,内部封装只是引用Resources的路径,为Editor提供扩展预览。 给人感觉好像直接引用资源一样方便使用。

 

 

 

 

7、 如果使用第三方的音频库播放音频, 就要禁用Unity自带的Audio模块。

脚本检查是否处理了。

using NUnit.Framework;
using System.Linq;
using UnityEditor;
namespace UniCommonTestRunner
{
    /// 
    /// 测试音频是否无效
    /// 
    public partial class UniCommonTestRunner
    {
        [Test]
        public void CheckAudioDisable()
        {
            var path    = "ProjectSettings/AudioManager.asset";
            var manager = AssetDatabase.LoadAllAssetsAtPath( path ).FirstOrDefault();
            var obj     = new SerializedObject( manager );
            var prop    = obj.FindProperty( "m_DisableAudio" );
            Assert.IsTrue( prop.boolValue );
        }
    }
}

 

 

 

8、 正常添加这种菜单都是在Mono 脚本内部添加 :

Unity3d 周分享(18期 2019.6.1 )_第20张图片

public class Test : MonoBehaviour
{
    [ContextMenu("Test")]
    public void TestTest()
    {
    }

但是图片中还有一个我添加的    菜单命令 “Set Random Rotation”。   这种是怎么添加的呢?
    [MenuItem("CONTEXT/Transform/Set Random Rotation")]
    private static void RandomRotation(MenuCommand command)
    {
        var transform = command.context as Transform;

        Undo.RecordObject(transform, "Set Random Rotation");
        transform.rotation = Random.rotation;
    }

如果需要所有组件都有的话就:
    [MenuItem("CONTEXT/Component/Set Random Rotation")]
    [MenuItem("CONTEXT/MonoBehaviour/Set Random Rotation")]    

 

 

9、 在不改变比率的情况下将Unity图片适合屏幕尺寸

Unity3d 周分享(18期 2019.6.1 )_第21张图片

  • 相机:位置(0, 0,-5)
  • board: 位置(0, 0, 0) scale(1, 1, 1)


 

// //指定的尺寸比
float targetRatio= targetWidth / targetHeight;
// 屏幕比
float screenRatio = (float) Screen.width / (float) Screen.height;
//  Camera.orthographicSize是高度的一半,计算高度 要乘2
var height = _camera.orthographicSize * 2f;
float width = 0;
if (screenRatio > targetRatio)
{
    // 因为屏幕长度超过指定大小,所以将指定的大小宽度适合屏幕宽度 
    width = screenRatio * height;
    height = width / targetRatio;
}
else
{
    //由于屏幕是垂直的,因此垂直于屏幕高度
    width = height * targetRatio;
}
board.localScale = new Vector3(width, height, 1f);

Unity3d 周分享(18期 2019.6.1 )_第22张图片

https://www.shibuya24.info/entry/screen_fit

Unity3d 周分享(18期 2019.6.1 )_第23张图片

 

 

10、 [Unity]介绍可以测量编译时间的“CompileTime.cs”

# // Originally found here: https://answers.unity.com/questions/1131497/how-to-measure-the-amount-of-time-it-takes-for-uni.html

using UnityEngine;
using UnityEditor;

[InitializeOnLoad]
class CompileTime : EditorWindow
{
    static bool isTrackingTime;
    static double startTime;

    static CompileTime()
    {
        EditorApplication.update += Update;
        startTime = PlayerPrefs.GetFloat("CompileStartTime", 0);
        if (startTime > 0)
        {
            isTrackingTime = true;
        }
    }


    static void Update()
    {
        if (EditorApplication.isCompiling && !isTrackingTime)
        {
            startTime = EditorApplication.timeSinceStartup;
            PlayerPrefs.SetFloat("CompileStartTime", (float)startTime);
            isTrackingTime = true;
        }
        else if (!EditorApplication.isCompiling && isTrackingTime)
        {
            var finishTime = EditorApplication.timeSinceStartup;
            isTrackingTime = false;
            var compileTime = finishTime - startTime;
            PlayerPrefs.DeleteKey("CompileStartTime");
            Debug.Log("Script compilation time: \n" + compileTime.ToString("0.000") + "s");
        }
    }
}


将上述脚本添加到Unity项目的“Editor”文件夹中 , 译时间将在控制台窗口中显示

 

 

 

11、 可视化Unity游戏中每个资产占用的空间,并快速优化游戏的文件大小

Unity3d 周分享(18期 2019.6.1 )_第24张图片

通常,您可以在构建之后查看Unity Editor的日志,以查看游戏文件大小的某些统计信息。这就是它的样子:

Textures 33.1 mb 54.1%

Meshes 0.0 kb 0.0%

Animations 0.0 kb 0.0%

Sounds 8.3 mb 13.6%

Shaders 172.8 kb 0.3%

Other Assets 8.2 mb 13.4%

Levels 82.1 kb 0.1%

Scripts 4.7 mb 7.7%

Included DLLs 6.4 mb 10.5%

File headers 201.5 kb 0.3%

Complete size 61.3 mb 100.0%

资源文件夹中使用的资产和文件,按未压缩大小排序。

如何运行

双击UnitySizeExplorer.exe

转到File>Open

导航到Unity Editor的日志文件。 (通常在$ HOME \ AppData \ Local \ Unity \ Editor下)

选中或取消选中树视图中的项目,以从饼图和估计的文件大小添加/删除它们。 展开文件夹以直接使用其子项。

如果您首先过滤掉非常小的文件,因为它们会混淆UI并使工具变得迟缓,这会有所帮助。 转到过滤器>小。 这些项目仍将以最终大小计算,但将从树视图和饼图中隐藏。

请注意,在Unity中构建之前必须清除日志文件的内容,以确保只有一个大小条目。

https://github.com/aschearer/unitysizeexplorer

 

 

 

 

12、[Unity]禁用iOS上的加速度计以提高性能

Unity3d 周分享(18期 2019.6.1 )_第25张图片

如果您不在iOS中使用加速计,请在Unity菜单中的“文件>构建设置...”中

打开“播放器设置...” ,

在“其他设置”中“加速计频率”“禁用”通过使

您可以提高性能只有一点点

PlayerSettings.accelerometerFrequency

如果要从脚本更改,请参阅上面的属性

https://docs.unity3d.com/2018.3/Documentation/Manual/iphone-iOS-Optimization.html

为什么Android没有这个选项?

 

 

13、 The relationship between AddForce () and physics. #unitytips

https://twitter.com/UnityBerserkers/status/1128421790685048832

我觉得评论中的 “为什么不是4个不同的功能; AddForce,Accelerate,SetMomentum和AddVelocity?” 也值得思考 ~~

Unity3d 周分享(18期 2019.6.1 )_第26张图片

Unity3d 周分享(18期 2019.6.1 )_第27张图片

Unity3d 周分享(18期 2019.6.1 )_第28张图片

Unity3d 周分享(18期 2019.6.1 )_第29张图片

类似讲到Unity物理方法的 : https://connect.unity.com/p/why-we-add-forces-in-fixedupdate-not-in-update-eng Update和FixedUpdate 调用的区别分析

方法如AddForce()/ AddTorque()应在FixedUpdate()中被调用 (或与之相关的物理循环的其它方法)。

如果任何方法会导致主体以恒定加速度移动较长时间,则应在FixedUpdate()中调用它。

在我们仅向主体添加一次力的情况下,我们也可以在Update()中使用AddForce()/ AddTorque()方法。这种情况的一个例子可以是发射射弹,其中仅在运动开始时我们在射弹上增加力。

Unity3d 周分享(18期 2019.6.1 )_第30张图片

Unity3d 周分享(18期 2019.6.1 )_第31张图片

其实还有些位置同步是需要放到LateUpdate 中的, 否则显示上会出现抖动。

Unity3d 周分享(18期 2019.6.1 )_第32张图片

 

 

 

 

14、

一个网站 http://alexanderameye.github.io ,其中有几个Unity教程。 主题范围从ocean waves到toon shading。 一定要看看!

 “收集一些整洁的数学和物理技巧”

https://twitter.com/unitycoder_com/status/1128044764912390144

https://github.com/zalo/MathUtilities

Unity3d 周分享(18期 2019.6.1 )_第33张图片

 

 

 

 

15、 哦,看看我在暂存UPM存储库中找到了什么,这是UIElements可视化编辑器的早期预览版! “UI Builder允许您使用UIElements,UXML和USS直观地创建和编辑UI” - 这对于使用UIElements制作编辑器工具非常有用?

 

此外,在2019.2β中他们添加了一个超级有用的示例工具,让您可以看到默认样式和代码示例,了解如何使用所有控件和内容(Window➡UI➡UIElementsSamples)在我试图弄清楚如何帮助时帮了很多忙 将类型分配给枚举下拉列表

Unity3d 周分享(18期 2019.6.1 )_第34张图片

Unity3d 周分享(18期 2019.6.1 )_第35张图片

Unity3d 周分享(18期 2019.6.1 )_第36张图片

 

https://twitter.com/LotteMakesStuff/status/1127389073369391109

很多人都在期待 他的运行时支持。

 

 

 

 

 

16、方便#unity3d着色器提示:

如果您想要那么酷的“立即修复”"Fix now"按钮来更改属性上法线贴图的纹理类型,请在纹理采样器之前添加“[Normal]”标签!

 

并且不要忘记将“白色” "white" 改为“凹凸”"bump" 作为默认值!

https://twitter.com/HarryAlisavakis/status/1128211169741869056

Unity3d 周分享(18期 2019.6.1 )_第37张图片

Unity3d 周分享(18期 2019.6.1 )_第38张图片

Unity3d 周分享(18期 2019.6.1 )_第39张图片

 

 

 

 

17、 Project 选中资源有一个命令, “Find References in scene” 可以查看一个资源的引用。

ref:ty_public_resource/vegetation/Textures/ty_xiaocao04.tga

今天我试了一下, 把这个命令拷贝到 Project中进行搜搜, 果然可以使用。

t:prefab ref:ty_public_resource/vegetation/Textures/ty_xiaocao04.tga

两个一起使用,让它在 Prefab中搜索引用这个图片的Prefab。

Unity3d 周分享(18期 2019.6.1 )_第40张图片

 

 

18、 关于New Memory Profiler https://forum.unity.com/threads/new-memory-profiler-preview-package-available-for-unity-2018-3.597271/

Unity3d 周分享(18期 2019.6.1 )_第41张图片

Unity3d 周分享(18期 2019.6.1 )_第42张图片

内存分析很强大, 但是2018.3 以下版本只能 只能使用旧的了~~~~

Unity3d 周分享(18期 2019.6.1 )_第43张图片

Unity3d 周分享(18期 2019.6.1 )_第44张图片

Unity3d 周分享(18期 2019.6.1 )_第45张图片

 

19、 [Unity]关于制作编辑器扩展插件时的平台判断

依赖于平台的编译:

using UnityEngine;
using System.Collections;
public class PlatformDefines : MonoBehaviour {
  void Start () {
    #if UNITY_EDITOR
      Debug.Log("Unity Editor");
    #endif
    #if UNITY_IOS
      Debug.Log("Iphone");
    #endif
    #if UNITY_STANDALONE_OSX
    Debug.Log("Stand Alone OSX");
    #endif
    #if UNITY_STANDALONE_WIN
      Debug.Log("Stand Alone Windows");
    #endif
  }          
}

使用Application.platform:

if (Application.platform == RuntimePlatform.OSXEditor) {
  // MacOS上的编辑器 
}
else if (Application.platform == RuntimePlatform.WindowsEditor) {
  // Windows上的编辑器 
}
else if (Application.platform == RuntimePlatform.LinuxEditor) {
  // Linux上的编辑器 
}
else  {
  //其他
}

 

 

 

20、 [Unity] 当粒子回收时设置处理(回调)

OnParticleSystemStopped

https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnParticleSystemStopped.html

Unity3d 周分享(18期 2019.6.1 )_第46张图片

因为如果启用了循环,则循环不会停止。不会执行OnParticleSystemStopped,

但是,如果您使用Stop停止ParticleSystem,将执行OnParticleSystemStopped。

 

 

21、 今天看到一个 靠拆包赚钱的工具 : 一直在更新~

https://devxdevelopment.com/UnityUnpacker

https://devxdevelopment.com/DevXUnityUnpackerChangeLog

可以直接打开包:

Unity3d 周分享(18期 2019.6.1 )_第47张图片

这个工具也是一样, il2cpp 没办法完成看到实现部分, 只能看到类和函数签名。

它这个AssetBundle 查看也不错。 依赖关系全都出来了。

Unity3d 周分享(18期 2019.6.1 )_第48张图片

免费开源的工具 Il2CppDumper 反编译的 il2cpp 只能看到类和函数的签名,具体实现细节看不到。

 

 

针对于一幅大小为 4M,像素分辨率为 1024x1024,格式为 RGBA(每通道8 位)的纹理而言,

Unity 中所有可用的 ASTC 块大小所对应的压缩比率。

Unity3d 周分享(18期 2019.6.1 )_第49张图片

 

 

 

合并网格以减少绘制调用数量

为减少渲染所需的绘制调用数量,您可以使用Mesh.CombineMeshes() 方法将多个网格合并为一 个网格。如果所有网格的材质相同,请将 mergeSubMeshes 参数设置为 true,以便它可以根据合 并组中的每个网格生成单一子网格。

把多个网格合并为单个较大的网格将帮助您:

 创建更高效的遮挡器。

 将多个基于图块的资源转变为大型、无缝、实心的单一资源。

网格合并脚本对性能优化很有帮助,但是这取决于您的场景构成。较大网格在视图中的停留时间长于 较小网格,请进行试验,以获取正确的网格大小。 应用此技术的一种方法是在层级中创建空的游戏对象,使其成为您想要合并的所有网格的父网格,然 后为其附加一个脚本。

避免读取/写入网格

如果运行时修改了模型,则 Unity 在保留原始数据的同时会在内存中另外保存一份网格数据副本。 如果运行时未修改模型(即使准备缩放),也请在导入设置的模型选项卡中禁用读取/写入已启用选 项。这样便无需另外保存一份副本,因而可节省内存。

使用纹理贴图集

您可以使用纹理贴图集减少一组对象所需的绘制调用数量。 纹理贴图集是指合并成一个大型纹理的纹理组。多个对象可通过一组合适的坐标重复使用此纹理。这 有助于 Unity 对共享相同材质的对象采用自动批处理。

设置对象的 UV 纹理坐标时,请避免更改其材质的 mainTextureScale 和 mainTextureOffset 属性。这会创建新的独特材质,该材质无法与批处理一同运行。请通过 MeshFilter 组件获取网格 数据并使用 Mesh.uv 属性更改每个像素元的坐标。 下图显示了纹理贴图集:

Unity3d 周分享(18期 2019.6.1 )_第50张图片

 

使用 Early-z Mali

GPU 包含了执行 Early-Z 算法的功能。Early-Z 可通过去除过度绘制的片段来提升性能。 Mali GPU 通常对大部分内容执行 Eaxly-Z 算法,但在一些情形中并不执行,以达到确保正确度的目的。这可 能比较难以从 Unity 内部加以控制,因为它依赖于 Unity 引擎以及编译器生成的代码。不过,您可以查看一些 迹象。

针对移动平台编译您的着色器,再查看其代码。确保您的着色器不会落入下列类别之一:

着色器有副作用

这意味着着色器线程在执行期间修改了全局状态,因此二次执行该着色器可能会产生不同的结果。通 常,这表示您的着色器写入到共享的读取/写入内存缓冲区,如着色器存储缓冲区对象或图像。例如, 如果您创建通过递增计数器来测量性能的着色器,它就有副作用。 如下所列不归类为副作用:

 只读内存访问。

 写入到仅写入缓冲区。

 纯粹的局部内存访问。

着色器调用 discard()

如果片段着色器在执行期间可以调用 discard(),那么 Mali GPU 无法启用Early-Z。这是因为,片 段着色器可以丢弃当前的片段,但深度值之前被 Early-Z 测试修改,而这是无法逆转的。 启用了 Alpha-to-coverage 如果启用了 Alpha-to-coverage,则片段着色器会计算稍后为定义 alpha 而要访问的数据。 例如,在渲染一棵树的树叶时,它们通常会表示为平面,其纹理定义树叶的哪个区域是透明还是不 透明。如果启用了Early-Z,您会获得不正确的结果,因为场景的一部分可能会被该平面的透明部分 遮挡。

深度源不固定的函数

用于深度测试的深度值不来自顶点着色器。如果您的片段着色器写入到 gl_FragDepth,Mali GPU 无法执行 Early-Z 测试。

 

 

 

 

 

个人主页挺有创意: 可以跟3d场景物体交互 。

https://www.lettier.com/

https://github.com/lettier/3d-game-shaders-for-beginners 3d-game-shaders-for-beginners

Table Of Contents

  • Setup
  • Building The Example Code
  • Running The Demo
  • Reference Frames
  • GLSL
  • Render To Texture
  • Texturing
  • Lighting
  • Normal Mapping
  • Outlining
  • Fog
  • Bloom
  • SSAO
  • Depth Of Field
  • Posterization
  • Cel Shading
  • Pixelization
  • Sharpen
  • Film Grain

 

 

一个游戏: https://logicworld.net/

https://habr.com/ru/post/453564/#Content

https://play.google.com/store/apps/details?id=com.ViacheslavRud.Circuit

Unity中实现类似于 数字电路 使用标准逻辑门(AND,NAND,OR,NOR,XOR,XNOR,NOR,NOT)

Unity3d 周分享(18期 2019.6.1 )_第51张图片

Unity3d 周分享(18期 2019.6.1 )_第52张图片

 

 

 

 

 

这篇文章有太多关于水。

Unity3d 周分享(18期 2019.6.1 )_第53张图片

 

 

 

类似RPGMaker的国产游戏开发工具,但是能开发的游戏类型不止RPG https://www.evkworld.com/

 

你可能感兴趣的:(学unity涨知识,unity3d,周分享)