文章目錄
  1. 1. 前言
    1. 1.1. Prefab
      1. 1.1.1. 嵌套预制件
      2. 1.1.2. 预制件变体
      3. 1.1.3. 新版预制件工作流
      4. 1.1.4. 进阶
    2. 1.2. 结论
  2. 2. 引用

前言

本篇文章是为了记录学习Unity官方最新推出的嵌套预制件工作流的相关知识,深入理解新版嵌套预制件的设计思路与使用工作流。

达到以下几个目标:

  1. 理解新版嵌套预制件带来的好处
  2. 如何避免Unity版本升级过程中预制件相关操作工具不兼容等问题。
  3. 设计新的预制件相关工具时考虑新老版本预制件工作流问题。

Prefab

首先我们先来了解下什么是预制件(Prefab)?
**Unity’s Prefab system allows you to create, configure, and store a GameObject complete with all its components, property values, and child GameObjects
as a reusable Asset. The Prefab Asset acts as a template from which you can create new Prefab instances in the SceneA Scene contains the environments and menus of your game. Think of each unique Scene file as a unique level. In each Scene, you place your environments, obstacles, and decorations, essentially designing and building your game in pieces. More info
See in Glossary.

根据官网的介绍我们可以得知,在Unity中Prefab和纹理,材质,音效一样都是一种资源格式,预制件(Prefab)其实是一个资源载体(e.g. GameObject包含层级以及多种Component脚本做成的重用实体对象),为了方便重复使用做好的资源模版。

老版本预制件系统问题:

  1. 不支持嵌套预制件
  2. 不支持变体预制件(后续会介绍)

新版本预制件很好的解决了上面说到的两个问题。

新版预制件在Unity 2018.3推出
新版预制件主要由以下三个部分组成:

  1. 嵌套预制件
  2. 预制件变体
  3. 预制件模式

接下来挨个学习上面三个部分。

Note:
新版Prefab是一种Asset,但打开处理的时候更多的是一种对Content的封装的概念,打开Prefab Mode是指打开Prefab浏览编辑Prefab Content

嵌套预制件

嵌套预制件顾名思义是指预制件之间是允许引用使用的关系而不是完全独立复制使用的概念。
老版本预制件系统就是采用复制实体引用的方式。
而新版嵌套预制件是采用真实预制件引用的方式。

嵌套预制件举例说明:
预制件A里用到了预制件B,如果我们修改预制件B,那么预制件A应该是自动更新变化。

嵌套预制件实战:
如下所示,我们制作了三个预制件,分别是Prefab_Cube_A,Prefab_Sphere_B以及Prefab_A_And_B,其中前两者是独立的预制件,最后一个是使用前两个预制件拼接而成的新的预制件。
NestedPrefabExample

可以看到嵌套的预制件在新的预制件下是蓝色的(意味着是关联了特定预制件的意思),为了验证嵌套预制件,我简单的修改Prefab_Cube_A颜色来达到实现我们预期的嵌套预制件是引用关系的验证:
NestedPrefabChangeMat
可以看到我们只修改了预制件Prefab_Cube_A的材质,但嵌套使用该预制件的Prefab_A_And_B里的Prefab_A也跟着变化了,这就是前面所提到的嵌套预制件的概念(嵌套引用关系而非实例化新个体)

预制件变体

预制件变体的概念类似于编程里继承的概念,继承属性的同时,父预制件的修改也会影响到子预制件变体。

预制件变体不仅继承了父预制件的状态数据,还跟父预制件是强制关联的。
举例说明:
通过预制件A创建预制件变体A2,当我们改变A时,A2继承的相同属性会跟着变化
NestedPrefabPrefabVariant
NestedPrefabChangeVariantParent

适用情况:

  1. 需要在个别Prefab基础上做少许做成预制件使用,同时需要同步父预制件修改的情况下。

Note:

  1. 预制件变体可以理解成封装了一层Prefab的Prefab Asset
  2. 预制件变体的Content根节点时Prefab而Prefab的Content根节点时GameObject

新版预制件工作流

Prefab有两种状态:

  1. Prefab Asset(Prefab资源)
  2. Prefab Instance(Prefab实例对象—场景里是蓝色的)
    Prefab Instance和Prefab Asset之间是关联着的,跟老版预制件里的概念是一致的。我们可以通过修改Prefab Instance然后把改变Apply到Prefab Asset上。新版预制件有区别的是支持Component级别的Apply和Revert。
    NestedPrefabComponentApply

还有一种独立于预制件的状态:实例对象
UnpackPrefabInstance
于预制件无关的实体对象是黑色的而非蓝色:
NestedPrefabUnpackPrefabColor
这里的Unpack相当于解除了Prefab Asset关联直接访问预制件的Content(Prefab Content后续会说到,在新版预制件里很重要的一个概念)

新版预制件有一个新的概念叫做预制件模式,修改并Apply修改到Prefab Asset上有两种方式:

  1. 直接打开Prefab Mode编辑
  2. 修改Prefab Instance后Apply到Prefab Asset上

这里重点讲解学习Prefab Mode:
Prefab Mode类似单独打开了一个修改预制件的场景,进入新的预制件场景才能看到预制件里的内容,这一点概念很重要
NestedPrefabOpenPrefabMode
NestedPrefabMode
在Prefab Mode下的修改可以通过保存和Undo操作来实现保存到Prefab Asset和撤销操作的效果。

了解了新版Prefab的Prefab Mode概念后,让我们结合相关API来制作一些新版Prefab的处理操作工具来深入理解:
需求:
检查指定Prefab里是否有无效的脚本挂在(Missing Script)并清除所有无效挂在脚本后保存Prefab Asset
NestedPrefabMissingScriptMono

相关API:

  1. AssetDatabase(处理Asset的工具类接口)
  2. Selection(处理Asset资源选中工具类接口)

步骤:

  1. 选中Prefab Asset
  2. 判定是否是选中Prefab Asset
  3. 打开Prefab Mode
  4. 查找并删除所有Missing Script
  5. 保存Prefab Asset并退出Prefab Mode
    实现:
    RemovePrefabMissingScriptTool.cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;

    /// <summary>
    /// 删除预制件无效脚本引用工具
    /// </summary>
    public class RemovePrefabMissingScriptTool
    {
    [MenuItem("Tools/Assets/移除Missing脚本 #&s", false)]
    public static void RemoveAllMissingScripts()
    {

    // 1. 选中Prefab Asset
    var selections = Selection.GetFiltered(typeof(Object), SelectionMode.Assets | SelectionMode.DeepAssets);
    Debug.Log($"选中有效数量:{selections.Length}");
    foreach (var selection in selections)
    {
    if(IsPrefabAsset(selection))
    {
    RemovePrefabAllMissingScripts(selection as GameObject);
    }
    else
    {
    Debug.LogError($"不符合预制件要求的Asset:{selection.name}");
    }
    }
    AssetDatabase.SaveAssets();
    }

    /// <summary>
    /// 是否是预制件资源
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    private static bool IsPrefabAsset(Object obj)
    {

    if(obj is GameObject)
    {
    var assetpath = AssetDatabase.GetAssetPath(obj);
    return !string.IsNullOrEmpty(assetpath);
    }
    else
    {
    return false;
    }
    }

    /// <summary>
    /// 移除预制件Asset所有无效的脚本
    /// </summary>
    private static void RemovePrefabAllMissingScripts(GameObject go)
    {

    // 3. 打开Prefab Mode
    // 4. 查找并删除所有Missing Script
    // 5. 保存Prefab Asset并推出Prefab Mode)
    var assetpath = AssetDatabase.GetAssetPath(go);
    var contentsRoot = PrefabUtility.LoadPrefabContents(assetpath);
    RemoveAllMissingScripts(contentsRoot);
    PrefabUtility.SaveAsPrefabAsset(contentsRoot, assetpath);
    PrefabUtility.UnloadPrefabContents(contentsRoot);
    }

    /// <summary>
    /// 移除无效的脚本
    /// </summary>
    private static void RemoveAllMissingScripts(GameObject go)
    {

    Component[] components = go.GetComponents<Component>();
    var r = 0;
    var serializedObject = new SerializedObject(go);
    var prop = serializedObject.FindProperty("m_Component");
    for (int i = 0; i < components.Length; i++)
    {
    if (components[i] == null)
    {
    string s = go.name;
    Transform t = go.transform;
    while (t.parent != null)
    {
    s = t.parent.name +"/"+s;
    t = t.parent;
    }
    Debug.Log (s + " has an empty script attached in position: " + i, go);

    prop.DeleteArrayElementAtIndex(i - r);
    r++;
    }
    }
    serializedObject.ApplyModifiedProperties();
    foreach (Transform childT in go.transform)
    {
    RemoveAllMissingScripts(childT.gameObject);
    }
    }
    }

核心思想是因为Missing Component本来就为Null无法向常规Component那样DestroyImmediate,需要通过SerializedObject去获取Component的方式删除指定位置的Component。

此脚本只针对单个预制件内容进行修改,嵌套预制件需要通过批量多选的方式实现各自移除各自身上的Missing Script来实现移除所有Missing脚本。

了解了Prefab Mode的存在,那么如何监听Prefab Mode的打开关闭生命周期了?
这里我们需要用到PrefabStage API(新版Prefab Mode下打开保存相关生命周期工具类接口)
这里只是简单的做个API实验,看看PrefabStage是否起作用。
PrefabStageListener.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.SceneManagement;
using UnityEngine;

/// <summary>
/// 新版预制件生命周期监听
/// </summary>
[ExecuteInEditMode]
public class PrefabStageListener : MonoBehaviour
{
private void Start()
{

PrefabStage.prefabSaved += OnPrefabSaved;
PrefabStage.prefabSaving += OnPrefabSaving;
PrefabStage.prefabStageOpened += OnPrefabOpened;
PrefabStage.prefabStageClosing += OnPrefabClosing;
}

private void OnDestroy()
{

PrefabStage.prefabSaved -= OnPrefabSaved;
PrefabStage.prefabSaving -= OnPrefabSaving;
PrefabStage.prefabStageOpened -= OnPrefabOpened;
PrefabStage.prefabStageClosing -= OnPrefabClosing;
}

/// <summary>
/// 响应预制件内容保存完毕
/// </summary>
/// <param name="go"></param>
private void OnPrefabSaved(GameObject go)
{

Debug.Log("预制件内容保存完毕!");
}

/// <summary>
/// 响应预制件内容保存
/// </summary>
/// <param name="go"></param>
private void OnPrefabSaving(GameObject go)
{

Debug.Log("预制件内容保存!");
}

/// <summary>
/// 响应预制件内容打开
/// </summary>
/// <param name="prefabstage"></param>
private void OnPrefabOpened(PrefabStage prefabstage)
{

Debug.Log("打开预制件内容!");
Debug.Log($"预制件:{prefabstage.prefabAssetPath} 内容阶段:{prefabstage.stageHandle}");
}

/// <summary>
/// 响应预制件内容关闭
/// </summary>
/// <param name="prefabstage"></param>
private void OnPrefabClosing(PrefabStage prefabstage)
{

Debug.Log("关闭预制件内容!");
Debug.Log($"预制件:{prefabstage.prefabAssetPath} 内容阶段:{prefabstage.stageHandle}");
}
}

结果:
PrefabStageListener
PrefabStage.prefabStageOpened全局监听不起作用(原因未知,忘知道的朋友告知)。PrefabStage.prefabStageClosing能正确工作。

Note:
Prefab Mode模式下修改叫做Save而非Apply,因为已经处于Prefab Content模式下了

进阶

PrefabUtility(操作处理Prefab的工具类接口)

更多学习,待续……

结论

  1. 支持嵌套预制件,能更高效的利用预制件,节约预制件的资源大小
  2. 支持预制件变体,能更高效的制作只有细微变化且有关联的相关预制件,增加开发效率
  3. 新版预制件最大区别就是提出了Prefab Mode的概念,让预制件在一个独立的环境进行编辑,进入Prefab Mode编辑的是Prefab Content而非Prefab本身。

引用

Prefabs Manual
详解新版Unity中可嵌套的Prefab系统
Introducing the new prefab workflow - Unity at GDC 2019
Unite LA 2018 - Introducing the New Prefab Workflow

文章目錄
  1. 1. 前言
    1. 1.1. Prefab
      1. 1.1.1. 嵌套预制件
      2. 1.1.2. 预制件变体
      3. 1.1.3. 新版预制件工作流
      4. 1.1.4. 进阶
    2. 1.2. 结论
  2. 2. 引用