原文:http://zijan.iteye.com/blog/911102

用Unity3D制作基于web的网络游戏,不可避免的会用到一个技术-资源动态加载。比如想加载一个大场景的资源,不应该在游戏的开始让用户长时间等待全部资源的加载完毕。应该优先加载用户附近的场景资源,在游戏的过程中,不影响操作的情况下,后台加载剩余的资源,直到所有加载完毕。

本文包含一些代码片段讲述实现这个技术的一种方法。本方法不一定是最好的,希望能抛砖引玉。代码是C#写的,用到了Json,还有C#的事件机制。

在讲述代码之前,先想象这样一个网络游戏的开发流程。首先美工制作场景资源的3D建模,游戏设计人员把3D建模导进Unity3D,托托拽拽编辑场景,完成后把每个gameobject导出成XXX.unity3d格式的资源文件(参看BuildPipeline),并且把整个场景的信息生成一个配置文件,xml或者Json格式(本文使用Json)。最后还要把资源文件和场景配置文件上传到服务器,最好使用CMS管理。客户端运行游戏时,先读取服务器的场景配置文件,再根据玩家的位置从服务器下载相应的资源文件并加载,然后开始游戏,注意这里并不是下载所有的场景资源。在游戏的过程中,后台继续加载资源直到所有加载完毕。

一个简单的场景配置文件的例子: 
MyDemoSence.txt

  1. {
  2. "AssetList" : [{
  3. "Name" : "Chair 1",
  4. "Source" : "Prefabs/Chair001.unity3d",
  5. "Position" : [2,0,-5],
  6. "Rotation" : [0.0,60.0,0.0]
  7. },
  8. {
  9. "Name" : "Chair 2",
  10. "Source" : "Prefabs/Chair001.unity3d",
  11. "Position" : [1,0,-5],
  12. "Rotation" : [0.0,0.0,0.0]
  13. },
  14. {
  15. "Name" : "Vanity",
  16. "Source" : "Prefabs/vanity001.unity3d",
  17. "Position" : [0,0,-4],
  18. "Rotation" : [0.0,0.0,0.0]
  19. },
  20. {
  21. "Name" : "Writing Table",
  22. "Source" : "Prefabs/writingTable001.unity3d",
  23. "Position" : [0,0,-7],
  24. "Rotation" : [0.0,0.0,0.0],
  25. "AssetList" : [{
  26. "Name" : "Lamp",
  27. "Source" : "Prefabs/lamp001.unity3d",
  28. "Position" : [-0.5,0.7,-7],
  29. "Rotation" : [0.0,0.0,0.0]
  30. }]
  31. }]
  32. }

AssetList:场景中资源的列表,每一个资源都对应一个unity3D的gameobject 
Name:gameobject的名字,一个场景中不应该重名 
Source:资源的物理路径及文件名 
Position:gameobject的坐标 
Rotation:gameobject的旋转角度 
你会注意到Writing Table里面包含了Lamp,这两个对象是父子的关系。配置文件应该是由程序生成的,手工也可以修改。另外在游戏上线后,客户端接收到的配置文件应该是加密并压缩过的。

主程序:

  1. 。。。
  2. public class MainMonoBehavior : MonoBehaviour {
  3. public delegate void MainEventHandler(GameObject dispatcher);
  4. public event MainEventHandler StartEvent;
  5. public event MainEventHandler UpdateEvent;
  6. public void Start() {
  7. ResourceManager.getInstance().LoadSence("Scenes/MyDemoSence.txt");
  8. if(StartEvent != null){
  9. StartEvent(this.gameObject);
  10. }
  11. }
  12. public void Update() {
  13. if (UpdateEvent != null) {
  14. UpdateEvent(this.gameObject);
  15. }
  16. }
  17. }
  18. 。。。
  19. }

这里面用到了C#的事件机制,大家可以看看我以前翻译过的国外一个牛人的文章。C# 事件和Unity3D 
在start方法里调用ResourceManager,先加载配置文件。每一次调用update方法,MainMonoBehavior会把update事件分发给ResourceManager,因为ResourceManager注册了MainMonoBehavior的update事件。

ResourceManager.cs

  1. 。。。
  2. private MainMonoBehavior mainMonoBehavior;
  3. private string mResourcePath;
  4. private Scene mScene;
  5. private Asset mSceneAsset;
  6. private ResourceManager() {
  7. mainMonoBehavior = GameObject.Find("Main Camera").GetComponent<MainMonoBehavior>();
  8. mResourcePath = PathUtil.getResourcePath();
  9. }
  10. public void LoadSence(string fileName) {
  11. mSceneAsset = new Asset();
  12. mSceneAsset.Type = Asset.TYPE_JSON;
  13. mSceneAsset.Source = fileName;
  14. mainMonoBehavior.UpdateEvent += OnUpdate;
  15. }
  16. 。。。

在LoadSence方法里先创建一个Asset的对象,这个对象是对应于配置文件的,设置type是Json,source是传进来的“Scenes/MyDemoSence.txt”。然后注册MainMonoBehavior的update事件。

  1. public void OnUpdate(GameObject dispatcher) {
  2. if (mSceneAsset != null) {
  3. LoadAsset(mSceneAsset);
  4. if (!mSceneAsset.isLoadFinished) {
  5. return;
  6. }
  7. //clear mScene and mSceneAsset for next LoadSence call
  8. mScene = null;
  9. mSceneAsset = null;
  10. }
  11. mainMonoBehavior.UpdateEvent -= OnUpdate;
  12. }

OnUpdate方法里调用LoadAsset加载配置文件对象及所有资源对象。每一帧都要判断是否加载结束,如果结束清空mScene和mSceneAsset对象为下一次加载做准备,并且取消update事件的注册。

最核心的LoadAsset方法:

  1. private Asset LoadAsset(Asset asset) {
  2. string fullFileName = mResourcePath + "/" + asset.Source;
  3. //if www resource is new, set into www cache
  4. if (!wwwCacheMap.ContainsKey(fullFileName)) {
  5. if (asset.www == null) {
  6. asset.www = new WWW(fullFileName);
  7. return null;
  8. }
  9. if (!asset.www.isDone) {
  10. return null;
  11. }
  12. wwwCacheMap.Add(fullFileName, asset.www);
  13. }
  14. 。。。

传进来的是要加载的资源对象,先得到它的物理地址,mResourcePath是个全局变量保存资源服务器的网址,得到fullFileName类似http://www.mydemogame.com/asset/Prefabs/xxx.unity3d。然后通过wwwCacheMap判断资源是否已经加载完毕,如果加载完毕把加载好的www对象放到Map里缓存起来。看看前面Json配置文件,Chair 1和Chair 2用到了同一个资源Chair001.unity3d,加载Chair 2的时候就不需要下载了。如果当前帧没有加载完毕,返回null等到下一帧再做判断。这就是WWW类的特点,刚开始用WWW下载资源的时候是不能马上使用的,要等待诺干帧下载完成以后才可以使用。可以用yield返回www,这样代码简单,但是C#要求调用yield的方法返回IEnumerator类型,这样限制太多不灵活。

继续LoadAsset方法:

  1. 。。。
  2. if (asset.Type == Asset.TYPE_JSON) { //Json
  3. if (mScene == null) {
  4. string jsonTxt = mSceneAsset.www.text;
  5. mScene = JsonMapper.ToObject<Scene>(jsonTxt);
  6. }
  7. //load scene
  8. foreach (Asset sceneAsset in mScene.AssetList) {
  9. if (sceneAsset.isLoadFinished) {
  10. continue;
  11. } else {
  12. LoadAsset(sceneAsset);
  13. if (!sceneAsset.isLoadFinished) {
  14. return null;
  15. }
  16. }
  17. }
  18. }
  19. 。。。

代码能够运行到这里,说明资源都已经下载完毕了。现在开始加载处理资源了。第一次肯定是先加载配置文件,因为是Json格式,用JsonMapper类把它转换成C#对象,我用的是LitJson开源类库。然后循环递归处理场景中的每一个资源。如果没有完成,返回null,等待下一帧处理。

继续LoadAsset方法:

  1. 。。。
  2. else if (asset.Type == Asset.TYPE_GAMEOBJECT) { //Gameobject
  3. if (asset.gameObject == null) {
  4. wwwCacheMap[fullFileName].assetBundle.LoadAll();
  5. GameObject go = (GameObject)GameObject.Instantiate(wwwCacheMap[fullFileName].assetBundle.mainAsset);
  6. UpdateGameObject(go, asset);
  7. asset.gameObject = go;
  8. }
  9. if (asset.AssetList != null) {
  10. foreach (Asset assetChild in asset.AssetList) {
  11. if (assetChild.isLoadFinished) {
  12. continue;
  13. } else {
  14. Asset assetRet = LoadAsset(assetChild);
  15. if (assetRet != null) {
  16. assetRet.gameObject.transform.parent = asset.gameObject.transform;
  17. } else {
  18. return null;
  19. }
  20. }
  21. }
  22. }
  23. }
  24. asset.isLoadFinished = true;
  25. return asset;
  26. }

终于开始处理真正的资源了,从缓存中找到www对象,调用Instantiate方法实例化成Unity3D的gameobject。UpdateGameObject方法设置gameobject各个属性,如位置和旋转角度。然后又是一个循环递归为了加载子对象,处理gameobject的父子关系。注意如果LoadAsset返回null,说明www没有下载完毕,等到下一帧处理。最后设置加载完成标志返回asset对象。

UpdateGameObject方法:

  1. private void UpdateGameObject(GameObject go, Asset asset) {
  2. //name
  3. go.name = asset.Name;
  4. //position
  5. Vector3 vector3 = new Vector3((float)asset.Position[0], (float)asset.Position[1], (float)asset.Position[2]);
  6. go.transform.position = vector3;
  7. //rotation
  8. vector3 = new Vector3((float)asset.Rotation[0], (float)asset.Rotation[1], (float)asset.Rotation[2]);
  9. go.transform.eulerAngles = vector3;
  10. }

这里只设置了gameobject的3个属性,眼力好的同学一定会发现这些对象都是“死的”,因为少了脚本属性,它们不会和玩家交互。设置脚本属性要复杂的多,编译好的脚本随着主程序下载到本地,它们也应该通过配置文件加载,再通过C#的反射创建脚本对象,赋给相应的gameobject。

最后是Scene和asset代码:

  1. public class Scene {
  2. public List<Asset> AssetList {
  3. get;
  4. set;
  5. }
  6. }
  7. public class Asset {
  8. public const byte TYPE_JSON = 1;
  9. public const byte TYPE_GAMEOBJECT = 2;
  10. public Asset() {
  11. //default type is gameobject for json load
  12. Type = TYPE_GAMEOBJECT;
  13. }
  14. public byte Type {
  15. get;
  16. set;
  17. }
  18. public string Name {
  19. get;
  20. set;
  21. }
  22. public string Source {
  23. get;
  24. set;
  25. }
  26. public double[] Bounds {
  27. get;
  28. set;
  29. }
  30. public double[] Position {
  31. get;
  32. set;
  33. }
  34. public double[] Rotation {
  35. get;
  36. set;
  37. }
  38. public List<Asset> AssetList {
  39. get;
  40. set;
  41. }
  42. public bool isLoadFinished {
  43. get;
  44. set;
  45. }
  46. public WWW www {
  47. get;
  48. set;
  49. }
  50. public GameObject gameObject {
  51. get;
  52. set;
  53. }
  54. }

代码就讲完了,在我实际测试中,会看到gameobject一个个加载并显示在屏幕中,并不会影响到游戏操作。代码还需要进一步完善适合更多的资源类型,如动画资源,文本,字体,图片和声音资源。

动态加载资源除了网络游戏必需,对于大公司的游戏开发也是必须的。它可以让游戏策划(负责场景设计),美工和程序3个角色独立出来,极大提高开发效率。试想如果策划改变了什么NPC的位置,美工改变了某个动画,或者改变了某个程序,大家都要重新倒入一遍资源是多么低效和麻烦的一件事。

======================================================================

生成配置文件代码

using UnityEngine;
using UnityEditor;
using System.IO;
using System;
using System.Text;
using System.Collections.Generic;
using LitJson;
public class BuildAssetBundlesFromDirectory
{
static List<JsonResource> config=new List<JsonResource>();
static Dictionary<string, List<JsonResource>> assetList=new Dictionary<string, List<JsonResource>>();
[@MenuItem("Asset/Build AssetBundles From Directory of Files")]//这里不知道为什么用"@",添加菜单
static void ExportAssetBundles ()
{//该函数表示通过上面的点击响应的函数
assetList.Clear();
string path = AssetDatabase.GetAssetPath(Selection.activeObject);//Selection表示你鼠标选择激活的对象
Debug.Log("Selected Folder: " + path); if (path.Length != )
{
path = path.Replace("Assets/", "");//因为AssetDatabase.GetAssetPath得到的是型如Assets/文件夹名称,且看下面一句,所以才有这一句。
Debug.Log("Selected Folder: " + path);
string [] fileEntries = Directory.GetFiles(Application.dataPath+"/"+path);//因为Application.dataPath得到的是型如 "工程名称/Assets" string[] div_line = new string[] { "Assets/" };
foreach(string fileName in fileEntries)
{
j++;
Debug.Log("fileName="+fileName);
string[] sTemp = fileName.Split(div_line, StringSplitOptions.RemoveEmptyEntries);
string filePath = sTemp[];
Debug.Log(filePath);
filePath = "Assets/" + filePath;
Debug.Log(filePath);
string localPath = filePath;
UnityEngine.Object t = AssetDatabase.LoadMainAssetAtPath(localPath); //Debug.Log(t.name);
if (t != null)
{
Debug.Log(t.name);
JsonResource jr=new JsonResource();
jr.Name=t.name;
jr.Source=path+"/"+t.name+".unity3d";
Debug.Log( t.name);
config.Add(jr);//实例化json对象
string bundlePath = Application.dataPath+"/../"+path;
if(!File.Exists(bundlePath))
{
Directory.CreateDirectory(bundlePath);//在Asset同级目录下相应文件夹
}
bundlePath+="/" + t.name + ".unity3d";
Debug.Log("Building bundle at: " + bundlePath);
BuildPipeline.BuildAssetBundle(t, null, bundlePath, BuildAssetBundleOptions.CompleteAssets);//在对应的文件夹下生成.unity3d文件
}
}
assetList.Add("AssetList",config);
for(int i=;i<config.Count;i++)
{
Debug.Log(config[i].Source);
}
}
string data=JsonMapper.ToJson(assetList);//序列化数据
Debug.Log(data);
string jsonInfoFold=Application.dataPath+"/../Scenes";
if(!Directory.Exists(jsonInfoFold))
{
Directory.CreateDirectory(jsonInfoFold);//创建Scenes文件夹
}
string fileName1=jsonInfoFold+"/json.txt";
if(File.Exists(fileName1))
{
Debug.Log(fileName1 +"already exists");
return;
}
UnicodeEncoding uni=new UnicodeEncoding(); using( FileStream fs=File.Create(fileName1))//向创建的文件写入数据
{
fs.Write(uni.GetBytes(data),,uni.GetByteCount(data));
fs.Close();
}
}
}

(转)在Unity3D的网络游戏中实现资源动态加载的更多相关文章

  1. 在Unity3D的网络游戏中实现资源动态加载

    用Unity3D制作基于web的网络游戏,不可避免的会用到一个技术-资源动态加载.比如想加载一个大场景的资源,不应该在游戏的开始让用户长时间等待全部资源的加载完毕.应该优先加载用户附近的场景资源,在游 ...

  2. Unity实现精灵资源动态加载

    private Sprite LoadSourceSprite(string relativePath) {         //把资源加载到内存中         UnityEngine.Objec ...

  3. html中的图像动态加载问题

    首先要说明下文档加载完成是什么概念 一个页面http请求访问时,浏览器会将它的html文件内容请求到本地解析,从窗口打开时开始解析这个document,页面初始的html结构和里面的文字等内容加载完成 ...

  4. 非常郁闷的 .NET中程序集的动态加载

    记载这篇文章的原因是我自己遇到了动态加载程序集的问题,而困扰了一天之久. 最终看到了这篇博客:http://www.cnblogs.com/brucebi/archive/2013/05/22/Ass ...

  5. Java之——Web项目中DLL文件动态加载方法

    本文转自:https://blog.csdn.net/l1028386804/article/details/53903557 在Java Web项目中,我们经常会用到通过JNI调用dll动态库文件来 ...

  6. Java中的资源文件加载方式

    文件加载方式有两种: 使用文件系统自带的路径机制,一个应用程序只能有一个当前目录,但可以有Path变量来访问多个目录 使用ClassPath路径机制,类路径跟Path全局变量一样也是有多个值 在Jav ...

  7. Unity中资源动态加载的几种方式比较

    http://blog.csdn.net/leonwei/article/details/18406103 初学Unity的过程中,会发现打包发布程序后,unity会自动将场景需要引用到的资源打包到安 ...

  8. angularJS ng-repeat中的directive 动态加载template

    有个需求,想实现一个html组件,传入不同的typeId,渲染出不同的表单元素. <div ng-repeat="field in vm.data"> <magi ...

  9. java 中能否使用 动态加载的类(Class.forName) 来做类型转换?

    今天同事提出了一个问题: 将对象a 转化为类型b,b 的classpath 是在配置文件中配置的,需要在运行中使用Class.forName 动态load进来,因为之前从来没有想过类似的问题,所以懵掉 ...

随机推荐

  1. Python全栈 MySQL 数据库 (表字段增、删、改、查、函数)

    ParisGabriel              每天坚持手写  一天一篇  决定坚持几年 为了梦想为了信仰    开局一张图         查询SQL变量 show variables 1.表字 ...

  2. 【Python】print 方法的参数

    当在IDEL或者命令行中执行 help(print) 命令时,就可以看到 print 方法的详细说明: print(value, ..., sep=' ', end='\n', file=sys.st ...

  3. lowercase calligraphic letters

    本系列文章由 @yhl_leo 出品,转载请注明出处. 文章链接: http://blog.csdn.net/yhl_leo/article/details/53454402 LaTeX公式表达中,经 ...

  4. excel模板解析—桥接模式:分离解析模板和业务校验

    在做excel模板解析的时候,其实会有两个部分,第一,将模板读取出来,校验一些必录项等. 但除了这些,在数据真正被业务线使用的时候,还会有一些其他的校验,比如说:根据业务,年龄是不能超过多少岁的,包括 ...

  5. C# 命名管道

    有些场合需要高效率,进行线程间通信,可以使用 C#命名管道.

  6. Angular & RxJS & Typesc­ript

    Angular & RxJS & Typesc­ript https://www.wmnetwork.cc/d/?mid=75627 杭州经开区国际创博中心 https://www.w ...

  7. easyUI layout

    layout是一个容器,它有5个区域:north(北丐),south(南帝),east(东邪),west(西毒),center(中神通),像不像金庸的天龙八部,中间区域的panel是必须的, 周边区域 ...

  8. 网络战争 [KD-Tree+最小割树]

    题面 思路 首先吐槽一下: 这题是什么东西啊??出题人啊,故意拼题很有意思吗??还拼两个这么毒瘤的东西???? 10K代码了解一下???? 然后是正经东西 首先,本题可以理解为这样: 给定$n$个块, ...

  9. RocketMQ 源码分析 RouteInfoManager(四)

    在上一章分析了NamesrvController的构造函数时,会生成一个RouteInfoManager对象,该对象存放着整个消息集群的相关消息,所以这里单独拿出来分析.其实试想一下namesrv的功 ...

  10. gulp技巧总结

    1. gulp.dest 会自动创建目录 gulp.dest(dir),若dir不存在,gulp会自动创建它 2. gulp.src copy具名路径(即不子目录**的路径)的文件,不会保留文件夹路径 ...