今天聊聊如何使用Unity预制体Prefab提高工作效率

1.Unity中什么是预制体Prefab

image.png

在Unity中,预制体(Prefab)是一种可重复使用的游戏对象(GameObject)的模板或蓝图。它允许开发者定义和配置游戏对象的属性、组件、子对象等,并在场景中多次实例化这个模板,创建多个相同或类似的对象。

预制体具有以下特点和用途:

  1. 重用性:预制体允许开发者创建一个可重复使用的对象模板。通过实例化预制体,可以在场景中创建多个相同的对象,从而提高开发效率和减少重复工作。
  2. 组件和属性:预制体不仅可以包含游戏对象,还可以包含其它组件、属性和子对象。这意味着可以在预制体中定义和配置游戏对象的外观、行为和功能,使其具有所需的特性。
  3. 动态更新:一旦在场景中实例化了预制体,对预制体进行的修改会自动应用到所有已实例化的对象上。这意味着可以通过修改预制体来一次性更新所有相关对象,而不需要手动修改每个对象。
  4. 层级管理:在Unity编辑器中,预制体以独立的资源存在,并且可以在场景层级视图中组织和管理。这使得开发者可以方便地编辑、实例化、替换和删除预制体,并在整个项目中重复使用它们。

使用预制体的常见场景包括但不限于:

  • 创建重复的敌人、道具或障碍物。
  • 定义玩家角色、NPC或可交互物体的模板。
  • 制作可重复使用的UI元素,如按钮、面板或图标。
  • 创建特效和粒子系统的模板。
  • 定义关卡的布局和组成元素。

预制体在Unity中是一个非常有用的工具,可以加速开发过程,简化对象的创建和管理,同时提高项目的可维护性和扩展性。

2.预制体的应用

2.1预制体的制作

在Unity中,可以使用以下方法来创建预制体:

  1. 拖拽创建:你可以从场景层次结构面板中选择一个或多个游戏对象,并将它们直接拖放到Assets文件夹中,从而在该文件夹中创建一个新的预制体文件。
  2. 实例化创建:你可以在脚本中使用代码动态创建游戏对象,并将其保存为预制体。例如,你可以使用Instantiate函数在运行时创建一个游戏对象,并使用PrefabUtility.SaveAsPrefabAsset函数将其保存为预制体文件。

无论使用哪种方法,一旦创建了预制体,你可以将其拖放到场景中的其他位置,或者在脚本中动态实例化它们。预制体使你可以在多个地方重用相同的游戏对象,节省了重复创建的时间和努力,并且可以方便地进行更新和修改。

第一种方法比较常用,今天主要介绍第一种方法,就是选中在Hierarchy层级视图中选择对象,拖拽到你想要保存的Assets文件中,即可创建成功。
image.png

2.2预制体的修改

在Unity中,可以使用以下方法对预制体进行修改:

  1. 直接修改预制体:可以在Assets文件夹中找到预制体文件,双击打开它。这将在场景中打开预制体,你可以直接对其进行修改,例如调整游戏对象的位置、旋转、缩放等。任何对预制体的修改都会影响到使用该预制体的所有实例
  2. 实例化预制体并修改实例:你可以将预制体拖放到场景中创建实例。然后,你可以在场景中选择该实例,并对其进行修改,例如移动、旋转、缩放等。这种修改只会影响到该实例,不会影响到其他使用相同预制体的实例
  3. 使用脚本动态修改:你可以在脚本中使用代码对预制体进行动态修改。通过获取预制体实例的引用,你可以访问和修改其组件属性、变换信息等。

请注意,对预制体进行修改时需要小心。直接修改预制体会影响所有实例,可能导致不希望的结果。而通过实例化预制体并修改实例,你可以更加精确地控制修改的范围。如果你希望对预制体进行全局修改,可以通过修改预制体文件本身。在修改预制体之前,最好备份一下,以免意外破坏预制体的原始设置。

2.3预制体的加载

在Unity中,常用加载预制体的方法如下:

  1. Instantiate(实例化):使用Instantiate函数可以在运行时加载并实例化预制体。你可以将预制体作为参数传递给Instantiate函数,并指定实例化后的位置、旋转和父对象等属性。该方法返回一个对新实例的引用,你可以对其进行操作和修改。实例代码:
GameObject prefab = Resources.Load<GameObject>("PrefabName");
GameObject instance = Instantiate(prefab, position, rotation);
  1. AssetBundle(资源包):AssetBundle是一种打包和加载资源的方式,包括预制体。你可以在编辑器中创建AssetBundle,然后在运行时通过加载AssetBundle并实例化其中的预制体。使用AssetBundle.LoadAsset或AssetBundle.LoadAssetAsync函数加载预制体。实例代码:
AssetBundle assetBundle = AssetBundle.LoadFromFile("Path/To/AssetBundle");
GameObject prefab = assetBundle.LoadAsset<GameObject>("PrefabName");
GameObject instance = Instantiate(prefab, position, rotation);
  1. Addressable Assets(可寻址资源):Unity的Addressable Assets系统提供了一种灵活的方式来管理和加载资源,包括预制体。你可以使用Addressables.LoadAsset或Addressables.Instantiate函数加载预制体。实例代码:
Addressables.LoadAssetAsync<GameObject>("PrefabName").Completed += handle =>
{

    GameObject prefab = handle.Result;
    GameObject instance = Instantiate(prefab, position, rotation);
};

上述方法都可以根据需求来选择合适的加载方式。Instantiate函数是日常开发中最常用的方法,而AssetBundle和Addressable Assets则提供了更灵活的资源管理和加载选项,在大型项目中比较常用,今天主要介绍第一种方法。

若使用第一种方法加载,需要注意的是,我们需要将预制体放在命名为Resources的文件夹中,因为代码加载预制体是通过UnityEngine.Resources类中的方法实现的,在初始化阶段会遍历所有Assets文件夹下的所有Resources文件夹去加载对应的资源,方法如下:

        public static T Load<T>(string path) where T : Object
        {
            return (T)Load(path, typeof(T));
        }

如果是加载预制体,那么泛型类型T就是GameObject,如果是加载图集,那么类型就是Sprite,还可以使用该方法加载其他资源,比如说音频等。

2.4预制体的应用

知道预制体的制作、修改及加载后,我们通过一个实例加深理解,并运用。弹窗是项目开发过程中常用的一个功能,复用性也非常高,本次实例我们就通过加载预制体,通过脚本动态加载预制体,并在想要实现弹窗的位置进行弹窗及相应的后续操作。

  1. 首选,制作一个用于弹窗的游戏对象,设置一个消息文本框,三个按钮。
    image.png
  2. 将游戏对象通过拖拽的方式拖到Assets/MessageBox/Resources文件夹下,生成预制体MessageBox
  3. 新建一个脚本,控制预制体的动态加载及逻辑实现,主要注意show方法的使用及参数传入,代码如下:
namespace Assets.MessageBox.Scripts
{

    using System;

    using UnityEngine;
    using UnityEngine.UI;

    /// <summary>
    /// 消息弹窗类
    /// Implements the <see cref="UnityEngine.MonoBehaviour" />
    /// </summary>
    /// <seealso cref="UnityEngine.MonoBehaviour" />
    public class MessageBox : MonoBehaviour
    {
        /// <summary>
        /// 预制体
        /// </summary>
        public static MessageBox instance;

        /// <summary>
        /// 按钮1
        /// </summary>
        public Button btn1;

        /// <summary>
        /// 按钮2
        /// </summary>
        public Button btn2;

        /// <summary>
        /// 按钮3
        /// </summary>
        public Button btn3;

        /// <summary>
        /// 遮罩
        /// </summary>
        public RectTransform mask;

        /// <summary>
        /// 弹窗消息
        /// </summary>
        public Text textInfo;

        /// <summary>
        /// 按钮1点击事件
        /// </summary>
        private Action m_Action1;

        /// <summary>
        /// 按钮2点击事件
        /// </summary>
        private Action m_Action2;

        /// <summary>
        /// 按钮3点击事件
        /// </summary>
        private Action m_Action3;

        /// <summary>
        /// 打开事件
        /// </summary>
        public static event Action OpenedCallback;

        /// <summary>
        /// 关闭事件
        /// </summary>
        public static event Action ClosedCallback;

        /// <summary>
        /// 关闭弹窗预制体
        /// </summary>
        public static void Close()
        {
            if (instance == null)
            {
                return;
            }

            ClosedCallback?.Invoke();
            Destroy(instance.gameObject);
            instance = null;
        }

        /// <summary>
        /// 隐藏弹窗
        /// </summary>
        public static void Hide()
        {
            if (instance == null)
            {
                return;
            }

            ClosedCallback?.Invoke();
            instance.gameObject.SetActive(false);
        }

        /// <summary>
        /// 打开弹窗预制体
        /// </summary>
        /// <returns>MessageBox.</returns>
        public static MessageBox Open()
        {
            if (instance != null)
            {
                return instance;
            }

            instance = Instantiate(Resources.Load<GameObject>("MessageBox")).GetComponent<MessageBox>();
            var objs = GameObject.FindGameObjectsWithTag("Root");
            var currentCanvas = objs[objs.Length - 1];
            instance.transform.SetParent(currentCanvas.transform);
            instance.transform.localPosition = Vector3.zero;
            instance.transform.localScale = Vector3.one;
            instance.transform.localRotation = Quaternion.identity;
            return instance;
        }

        /// <summary>
        /// 显示消息窗口
        /// </summary>
        /// <param name="info">The information.</param>
        /// <param name="btnName">Name of the BTN.</param>
        /// <param name="btn1">The BTN1.</param>
        /// <param name="btn2">The BTN2.</param>
        /// <param name="btn3">The BTN3.</param>
        public static void Show(
            string info,
            string[] btnName = null,
            Action btn1 = null,
            Action btn2 = null,
            Action btn3 = null)
        {
            Open();
            if (btnName != null)
            {
                // Debug.Log(btnName[0]);
            }

            instance.gameObject.SetActive(true);
            instance.mask.localScale = Vector3.one;
            instance.textInfo.text = info;
            if (btnName == null || btnName.Length == 1)
            {
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName == null ? "OK" : btnName[0];
                instance.btn1.gameObject.transform.localPosition = new Vector3(0, -70, 0);
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(false);
                instance.btn3.gameObject.SetActive(false);
            }

            if (btnName != null && btnName.Length == 2)
            {
                instance.btn1.transform.localPosition = new Vector3(-120, -70, 0);
                instance.btn2.transform.localPosition = new Vector3(120, -70, 0);
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName[0];
                instance.btn2.transform.Find("Text").GetComponent<Text>().text = btnName[1];
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(true);
                instance.btn3.gameObject.SetActive(false);
            }

            if (btnName != null && btnName.Length == 3)
            {
                instance.btn1.transform.localPosition = new Vector3(-120, -70, 0);
                instance.btn2.transform.localPosition = new Vector3(0, -70, 0);
                instance.btn3.transform.localPosition = new Vector3(120, -70, 0);
                instance.btn1.transform.Find("Text").GetComponent<Text>().text = btnName[0];
                instance.btn2.transform.Find("Text").GetComponent<Text>().text = btnName[1];
                instance.btn3.transform.Find("Text").GetComponent<Text>().text = btnName[2];
                instance.btn1.gameObject.SetActive(true);
                instance.btn2.gameObject.SetActive(true);
                instance.btn3.gameObject.SetActive(true);
            }

            instance.m_Action1 = btn1;
            instance.m_Action2 = btn2;
            instance.m_Action3 = btn3;

            OpenedCallback?.Invoke();
        }

        /// <summary>
        /// 按钮1点击
        /// </summary>
        private void OnBtn1Click()
        {
            Hide();
            this.m_Action1?.Invoke();
            this.m_Action1 = null;
        }

        /// <summary>
        /// 按钮2点击
        /// </summary>
        private void OnBtn2Click()
        {
            Hide();
            this.m_Action2?.Invoke();
            this.m_Action2 = null;
        }

        /// <summary>
        /// 按钮3点击
        /// </summary>
        private void OnBtnMClick()
        {
            Hide();
            this.m_Action3?.Invoke();
            this.m_Action3 = null;
        }

        /// <summary>
        /// 销毁
        /// </summary>
        private void OnDestroy()
        {
            instance = null;
        }

        /// <summary>
        /// 开始
        /// </summary>
        private void Start()
        {
            this.btn1.onClick.AddListener(this.OnBtn1Click);
            this.btn2.onClick.AddListener(this.OnBtn2Click);
            this.btn3.onClick.AddListener(this.OnBtnMClick);
        }
    }
}

4.将脚本拖拽到预制体MessageBox的下方,挂载在预制体上,因为我们定义了公开的属性btn1、btn2、btn3、msak、textInfo,所以脚本加载到预制体上时会显示这几个属性,我们需要将属性赋值,直接拖拽相应的对象到属性值中即可。
image.png

5.通过上述方法一个预制体、脚本已经完成,接下来我们进行调用。在贪吃蛇项目中,当蛇撞到自己身体时,弹窗告诉我们已经死亡,文本框是否重新开始?实现两个按钮,一个按钮是确认,重新开始,一个按钮是返回主界面,截取代码如下:

else if (collision.tag == "body")
            {
                //撞到自己身体
                m_IsReward = false;
                m_DieAudioSource.Play();
                TimeStop();
                OnGamePauseChang();
                Record();

                var msg = LanguageModel.instance.GetCurLanguageValue(LanguageKey.kRestart);
                var btnArray = new[]
                              {
                                  LanguageModel.instance.GetCurLanguageValue(LanguageKey.kConfirm), 
                                  LanguageModel.instance.GetCurLanguageValue(LanguageKey.kReturn)
                              };
                //由消息盒子实现弹窗
                MessageBox.Show(
                    msg,
                    btnArray,
                    RestartItem.Restart,
                    RestartItem.ReturnHome);
            }

弹窗效果如下:
image.png

2.5预制体的销毁

预制体的销毁有两种方法,分别是UnityEngine.Object.Destroy方法和UnityEngine.Object.DestroyImmediate方法。它们之间有一些区别:

  1. Destroy: Destroy函数是异步销毁对象的方法。当你调用Destroy函数时,Unity会在当前帧结束后,在下一帧中销毁该对象。这意味着在调用Destroy后,对象可能仍然可以在当前帧中访问。它将在下一帧被销毁并从场景中移除。
  2. DestroyImmediate: DestroyImmediate函数是同步销毁对象的方法。与Destroy不同,调用DestroyImmediate函数后,对象会立即被销毁,而不需要等到下一帧。这意味着在调用DestroyImmediate后,对象将立即从场景中移除,不再可用。

需要注意的是,使用DestroyImmediate函数可能会在性能上造成一些开销,特别是当销毁大量对象时。这是因为立即销毁对象可能导致场景中的其他对象需要调整和重排,以填补被销毁对象的空缺。

因此,一般情况下,建议使用Destroy函数进行对象的销毁,除非你确实需要立即从场景中移除对象并且已经权衡了性能开销。

接着上面消息弹窗, 使用MessageBox.Close()进行预制体销毁的实例代码:

        /// <summary>
        /// Closes this instance.
        /// </summary>
        public static void Close()
        {
            if (instance == null)
            {
                return;
            }

            ClosedCallback?.Invoke();
            Destroy(instance.gameObject);
            instance = null;
        }

3.扩展:项目中对游戏对象进行实例化

项目开发中,会遇到这样的问题,想要实现一个表格,又或者想要一个模板进行重复使用,如果该对象较小(比如是一行表格,一个输入框等等),制作预制体再去通过代码加载达到复用的目的,虽然可行,但是有更好的实现方法。
比如下图,我想实现点击一次新增按钮,增加一行参数(红框中的为一行参数,包括标题,输入框及单位)。
image.png

我们查看下它的层级结构:

image.png

其实用到的还是UnityEngine.Object.Instantiate(m_ParamItem)方法,可以实例化预制体,也可以实例化一个游戏对象,实例化该对象后会克隆一个一模一样的对象,你可以对其进行需要的操作,实例代码:

//实例化游戏对象m_ParamItem
 var paramObject = UnityEngine.Object.Instantiate(m_ParamItem);
 //实例化后的物体指定父级节点
 paramObject.SetParent(m_ParamItem.parent);
 //定义位置,缩放、是否激活等
 paramObject.localScale = Vector3.one;
 paramObject.localPosition = Vector3.zero;
 paramObject.gameObject.SetActive(true);
 //获取相关组件并赋值
 paramObject.Find("Text").GetComponent<Text>().text = standardDatas[i].parameterName;
 paramObject.Find("InputField").GetComponent<InputField>().text =standardDatas[i].parameterValue.ToString("F3");
 paramObject.Find("Unit").GetComponent<Text>().text = standardDatas[i].parameterUnit;

4.结语

今天主要分享了预制体的制作、修改、加载以及简单应用,正确的使用预制体可以减少大量重复性构建工作,更加高级的用法还在探索中,具体使用可以参考我的文章“从0开始,用Unity写一个“健壮”的贪吃蛇游戏”。若有错误之处,欢迎大家批评指正,谢谢。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYBbsNVX' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片