在上一篇文章中,我们大约是开始接触到资源加载的事情了,场景资源则是一个比较特殊的资源,我们只要添加到Build Settings里面,那么我们就可以通过API直接加载。
但是其他类型的资源怎么办呢?比如我们制作一个网络游戏,接收到后台的返回数据要求给人物装上一把枪,但是我们也不可能把所有枪都作为成员数据赋值上去,肯定是希望用到哪个就加载哪个,所以这个时候就需要支持通过类似名字或者路径的方式加载资源。
因此本章我们将会讲解Unity中资源到底是什么,如何进行加载的,以及几种加载方式适用的场景。
引用形式的加载
之前的篇章里面我们要使用某个资源,都是在代码中加入成员变量,借助Unity的序列化能力显示在编辑器面板上,然后我们通过拖拽等方式,将场景内的物体或者Project窗口里面的资源赋值到对应的变量上去,这种加载资源的方法是通过引用来进行加载的。
之前分析实际场景文件的时候我们有分析过,我们的GameObject也好,组件类也罢,在序列化的时候都会生成一个唯一的id,这个id就是用来存储引用关系,在场景启动的时候就会加载场景里面已有的GameObject,然后根据文件内的id数据来索引所有的引用关系。
但是有一个问题就是,我们之前其实有使用Prefab赋值给场景内物体的组件变量上,那这个引用关系又是如何呢?
我们不妨以文本方式打开Demo.unity文件,找到我们赋值bullet的FireController这个物体的FireController组件:
可以看到这里我们又见到了fileID,但是很显然这个fileID我们是无法在场景文件内搜索到第二个使用的地方,毕竟这里赋值的Prefab是存在于Project窗口里面而非场景里面,那么我们注意到后面还有一个guid,擅长全局搜索的朋友可能会尝试把整个Assets目录都搜索一遍,很快就能找到其实这个guid存在于Bullet.prefab.meta文件中:
打开看内容就是这样:
可以看到这个meta文件里面记录的guid跟我们场景里面引用Prefab的变量上存的guid一致,并且如果你再打开Bullet.prefab就能找到fileID是对应的Prefab上的那个组件。
这个时候其实也就理解了,对于非场景内直接可以引用的资源,场景外的资源文件,都会带有一个.meta文件,这个文件内存储了这个资源的guid,而引用场景外资源的方法就是通过这个guid进行检索。
可以仔细观察,Assets文件夹下所有文件都会带上一个.meta文件,也就是说其实所有文件包括文件夹,在Unity中都是可以被引用的。
这也顺带解答了如果我们的游戏工程要上传git或者svn,p4这样的版本控制系统,.meta文件是一定需要上传的,如果没有上传,其他人打开工程时,Unity会自动生成新的.meta文件,那个时候你的所有通过guid引用的资源都将失效。
当然.meta文件不仅仅存了guid这个信息,还有很多其他的信息,这些我们会在后面每种资源类型进阶教程讲到的时候再详细说明。
在理解清楚Unity针对场景内和场景外资源的引用如何处理之后,其实也就能理解为什么我们的东西打包也能拿到,因为Unity打包是从Build Settings里面加入的场景查找所有资源的引用,所有用到的资源都会打包到最终游戏包中。
Resources文件夹
当我们没有在任何代码成员里面直接引用组件或者GameObject或者Prefab资源的时候,我们如何动态的加载一个Prefab或者什么的资源呢?Unity提供了一个特殊的潜规则,一个名字叫做Resources的文件夹,在你的Assets目录下,任何一个叫Resources的文件夹都可以,可以同时存在多个在不同目录下的Resources文件夹,这些文件夹会被Unity统一识别到,然后你就可以用Resources.Load 这个API加载里面的资源:
Unity - Scripting API: Resources.Loaddocs.unity3d.com/ScriptReference/Resources.Load.html
有点类似我们加载新的场景,我们可以通过相对路径来加载Resources文件夹下的内容,注意路径一定需要使用/而不是\来作为路径的分隔符,
例如:
- Assets/Test1/Resources/a.prefab,那么你加载的时候用的是a,不带文件后缀名
- Assets/Test1/Resources/BB/a.prefab,那么你加载的时候用的是BB/a
- Assets/Test2/Resources/BB/a.prefab,你会和2冲突,Unity会告诉你所有Resources文件夹里面的文件不能有一样的相对路径
OK,那我们来试一下,之前我们的Bullet是通过直接赋值给成员变量,我们现在把Prefab资源放到Resources文件夹里面,我们目前没有,所以我们新建一个Resources文件夹,还是Project窗口右键Create->Folder,然后把我们的Bullet prefab文件直接拖进去,相当于剪切。
注意如果你是直接在windows资源管理器里面来做这个操作的话,应该把对应的.meta文件也一起剪切走,之前也说了这个.meta文件带有这个资源的guid信息,如果你不剪切走,那么就会导致原有的引用关系找不到了(虽然现在是直接用Resources来加载,可能不会有影响)。在Unity的Project窗口中看不到.meta文件,因为你剪切资源的时候会自动帮你处理。
然后我们需要修改一下之前创建子弹的FireController的代码:
using UnityEngine;
using Object = UnityEngine.Object;public class FireController : MonoBehaviour
{private bool isMouseDown = false;private float lastFireTime = 0f;private Vector3 fireDirection;private AddVelocity bullet;public string bulletResourcesPath;public float fireInterval = 0.1f;public Transform fireBeginPosition;private void Start(){bullet = Resources.Load<AddVelocity>(bulletResourcesPath);}void Update(){if (Input.GetButton("Fire1")){if (!isMouseDown){isMouseDown = true;lastFireTime = Time.time;Fire();}else if (Time.time - lastFireTime > fireInterval){lastFireTime = Time.time;Fire();}}else{isMouseDown = false;}}void Fire(){// 在这里实现每次触发的逻辑// 创建新的子弹,每次都是从模板bullet复制一个出来AddVelocity newBullet = Object.Instantiate(bullet);newBullet.transform.position = fireBeginPosition.position;newBullet.SetDirection(fireDirection);}public void SetDirection(Vector3 direction){fireDirection = direction;}
}
我们的修改主要是将之前的public成员bullet换成了private成员,我们不再需要序列化这个成员,取而代之的是用了一个string成员来让编辑器上可以填入资源的路径。
然后我们在Start方法里面通过Resources.Load来加载一次这个资源,后续就和之前的使用方法一样。Resources.Load方法是带有泛型,或者用C++的话来讲就是模板类型,填入<>里面的类型将会用类似GetComponent的方法从加载上来的GameObject中获取,当然你想加载上来就是GameObject也完全可以在<>里面填GameObject这个类型。
OK,我们直接在编辑器里面填入我们子弹的资源路径Bullet:
可以再跑起来看看,我们也一样可以创建子弹,获得和之前一样的效果。
这样的加载方法有个缺点:放在Resources文件夹下的资源不会管有没有引用,在打包游戏的时候会全部打包进去,毕竟Unity也不知道你到底要用哪个,所以这个办法比较适合小体量的资源。
AssetDatabase
编辑器模式下专用的加载资源方法,主要用于编辑器下的扩展和功能操作,跟正常游戏的运行逻辑是独立开的,由于涉及到了编辑器的游戏运行时的概念,本章不会讲解,会留到专门讲解编辑器扩展开发的时候再讲。
AssetBundle
Unity提供的正统的游戏打包发布后的正规资源加载方式,但是很多概念理解起来相当麻烦,本大章系列文章还只是初步入门的阶段,暂时不讲这部分,会留到后面详细讲解游戏打包时的资源管理再讲。
思考题
- Resources.Load会同步加载资源,阻塞代码执行,显然官方的API有异步版本,如果改成异步应该怎么写?
下一章
上面我们讲解了两个资源加载的方式,一个是通过引用自动加载,一个是通过路径来动态加载。虽然Resources.Load比较挫,但是现阶段足够我们初步学习Unity的资源加载。
下一章我们将会简单入门一下编辑器扩展,Unity编辑器并非一尘不变,而是可以通过我们的代码随心所欲的调整,这也是现代商业游戏引擎提供的编辑器扩展能力,通过扩展能力,我们能更好的让引擎服务于我们的定制化需求。当然了,在其中也会学习到编辑器所跑的逻辑和游戏跑的逻辑这两个概念的区别。