1.Unity中什么是预制体Prefab
在Unity中,预制体(Prefab)是一种可重复使用的游戏对象(GameObject)的模板或蓝图。它允许开发者定义和配置游戏对象的属性、组件、子对象等,并在场景中多次实例化这个模板,创建多个相同或类似的对象。
预制体具有以下特点和用途:
- 重用性:预制体允许开发者创建一个可重复使用的对象模板。通过实例化预制体,可以在场景中创建多个相同的对象,从而提高开发效率和减少重复工作。
- 组件和属性:预制体不仅可以包含游戏对象,还可以包含其它组件、属性和子对象。这意味着可以在预制体中定义和配置游戏对象的外观、行为和功能,使其具有所需的特性。
- 动态更新:一旦在场景中实例化了预制体,对预制体进行的修改会自动应用到所有已实例化的对象上。这意味着可以通过修改预制体来一次性更新所有相关对象,而不需要手动修改每个对象。
- 层级管理:在Unity编辑器中,预制体以独立的资源存在,并且可以在场景层级视图中组织和管理。这使得开发者可以方便地编辑、实例化、替换和删除预制体,并在整个项目中重复使用它们。
使用预制体的常见场景包括但不限于:
- 创建重复的敌人、道具或障碍物。
- 定义玩家角色、NPC或可交互物体的模板。
- 制作可重复使用的UI元素,如按钮、面板或图标。
- 创建特效和粒子系统的模板。
- 定义关卡的布局和组成元素。
预制体在Unity中是一个非常有用的工具,可以加速开发过程,简化对象的创建和管理,同时提高项目的可维护性和扩展性。
2.预制体的应用
2.1预制体的制作
在Unity中,可以使用以下方法来创建预制体:
- 拖拽创建:你可以从场景层次结构面板中选择一个或多个游戏对象,并将它们直接拖放到Assets文件夹中,从而在该文件夹中创建一个新的预制体文件。
- 实例化创建:你可以在脚本中使用代码动态创建游戏对象,并将其保存为预制体。例如,你可以使用Instantiate函数在运行时创建一个游戏对象,并使用
PrefabUtility.SaveAsPrefabAsset
函数将其保存为预制体文件。
无论使用哪种方法,一旦创建了预制体,你可以将其拖放到场景中的其他位置,或者在脚本中动态实例化它们。预制体使你可以在多个地方重用相同的游戏对象,节省了重复创建的时间和努力,并且可以方便地进行更新和修改。
第一种方法比较常用,今天主要介绍第一种方法,就是选中在Hierarchy层级视图中选择对象,拖拽到你想要保存的Assets文件中,即可创建成功。
2.2预制体的修改
在Unity中,可以使用以下方法对预制体进行修改:
- 直接修改预制体:可以在Assets文件夹中找到预制体文件,双击打开它。这将在场景中打开预制体,你可以直接对其进行修改,例如调整游戏对象的位置、旋转、缩放等。任何对预制体的修改都会影响到使用该预制体的所有实例。
- 实例化预制体并修改实例:你可以将预制体拖放到场景中创建实例。然后,你可以在场景中选择该实例,并对其进行修改,例如移动、旋转、缩放等。这种修改只会影响到该实例,不会影响到其他使用相同预制体的实例。
- 使用脚本动态修改:你可以在脚本中使用代码对预制体进行动态修改。通过获取预制体实例的引用,你可以访问和修改其组件属性、变换信息等。
请注意,对预制体进行修改时需要小心。直接修改预制体会影响所有实例,可能导致不希望的结果。而通过实例化预制体并修改实例,你可以更加精确地控制修改的范围。如果你希望对预制体进行全局修改,可以通过修改预制体文件本身。在修改预制体之前,最好备份一下,以免意外破坏预制体的原始设置。
2.3预制体的加载
在Unity中,常用加载预制体的方法如下:
- Instantiate(实例化):使用Instantiate函数可以在运行时加载并实例化预制体。你可以将预制体作为参数传递给Instantiate函数,并指定实例化后的位置、旋转和父对象等属性。该方法返回一个对新实例的引用,你可以对其进行操作和修改。实例代码:
GameObject prefab = Resources.Load<GameObject>("PrefabName");
GameObject instance = Instantiate(prefab, position, rotation);
- 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);
- 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预制体的应用
知道预制体的制作、修改及加载后,我们通过一个实例加深理解,并运用。弹窗是项目开发过程中常用的一个功能,复用性也非常高,本次实例我们就通过加载预制体,通过脚本动态加载预制体,并在想要实现弹窗的位置进行弹窗及相应的后续操作。
- 首选,制作一个用于弹窗的游戏对象,设置一个消息文本框,三个按钮。
- 将游戏对象通过拖拽的方式拖到Assets/MessageBox/Resources文件夹下,生成预制体MessageBox。
- 新建一个脚本,控制预制体的动态加载及逻辑实现,主要注意
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,所以脚本加载到预制体上时会显示这几个属性,我们需要将属性赋值,直接拖拽相应的对象到属性值中即可。
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);
}
弹窗效果如下:
2.5预制体的销毁
预制体的销毁有两种方法,分别是UnityEngine.Object.Destroy方法和UnityEngine.Object.DestroyImmediate方法。它们之间有一些区别:
- Destroy:
Destroy
函数是异步销毁对象的方法。当你调用Destroy
函数时,Unity会在当前帧结束后,在下一帧中销毁该对象。这意味着在调用Destroy
后,对象可能仍然可以在当前帧中访问。它将在下一帧被销毁并从场景中移除。 - 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.扩展:项目中对游戏对象进行实例化
项目开发中,会遇到这样的问题,想要实现一个表格,又或者想要一个模板进行重复使用,如果该对象较小(比如是一行表格,一个输入框等等),制作预制体再去通过代码加载达到复用的目的,虽然可行,但是有更好的实现方法。
比如下图,我想实现点击一次新增按钮,增加一行参数(红框中的为一行参数,包括标题,输入框及单位)。
我们查看下它的层级结构:
其实用到的还是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写一个“健壮”的贪吃蛇游戏”。若有错误之处,欢迎大家批评指正,谢谢。