Dynamic loading of resources in Unity3D online games

Posted by George Botley on Tue, 04 Jun 2019 04:53:04 +0200

Original: http://www.cnblogs.com/hisiqi/p/3202709.html

Making web-based online games with Unity3D inevitably involves the dynamic loading of a technology-resource.For example, if you want to load a resource for a large scene, you shouldn't have users wait a long time for all the resources to be loaded at the beginning of the game.Scene resources near users should be loaded first, and the remaining resources should be loaded in the background during the game, without affecting the operation, until all loads are completed.(

This article contains code snippets that describe a way to implement this technology.This method is not necessarily the best, and I hope it can be used as a starting point.The code is written in C#, Json is used, and C# has an event mechanism.(

Before you tell the code, imagine the development process for such an online game.First, the 3D modeling of scene resources is created by artists. Game designers import the 3D modeling into Unity3D, drag and drop to edit the scene, export each gameobject to a resource file in XXX.unity3d format (see BuildPipeline), and generate a configuration file with the information of the entire scene in xml or Json format (this article uses Json).Finally, upload the resource files and scene configuration files to the server, preferably using CMS management.When the client runs the game, it first reads the server's scene configuration file, then downloads the corresponding resource file from the server according to the player's location, and then starts the game. Note that not all scene resources are downloaded here.During the game, the background continues to load resources until all loads are completed.(

A simple scenario configuration file example:
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: A list of resources in the scene, each corresponding to a gameobject in unity3D
Name:gameobject name, should not be renamed in a scene
Source: The physical path and file name of the resource
Position:gameobject coordinates
Rotation:gameobject rotation angle
You will notice that the Writing Table contains Lamp, which is a parent-child relationship.The configuration file should be programmatically generated or manually modified.In addition, after the game is online, the configuration file received by the client should be encrypted and compressed.(

Main program:

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

C# event mechanism is used here, so you can see an article I have translated previously from a Cowman abroad. C#Events and Unity3D 
Call ResourceManager in the start method and load the configuration file first.Each time the update method is called, MainMonoBehavior distributes the update event to the ResourceManager because the ResourceManager registers the update event for MainMonoBehavior.(

ResourceManager.cs 

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

In the LoadSence method, first create an Asset object, which corresponds to the configuration file, setting the type to Json, and source to the "Scenes/MyDemoSence.txt" passed in.Then register the update event for MainMonoBehavior.(

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

LoadAsset is called in the OnUpdate method to load the profile object and all resource objects.Each frame determines whether the load is complete, if the end empties the mScene and mSceneAsset objects in preparation for the next load, and unregisters the update event.(

Core LoadAsset methods:

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

The incoming resource object is loaded, its physical address is obtained first. mResourcePath is the global variable that holds the address of the resource server and gets fullFileName similar to http://www.mydemogame.com/asset/Prefabs/xxx.unity3d.The wwCacheMap is then used to determine if the resource has been loaded and, if so, to cache the loaded WW objects in the Map.Looking at the previous Json configuration files, Chair 1 and Chair 2 use the same resource, Chair001.unity3d, and there is no need to download Chair 2 when it is loaded.If the current frame is not loaded, return null until the next frame.This is the feature of the WWW class, which cannot be used immediately when you start downloading resources with WWW. Wait until the Norgan frame download is complete before you can use it.You can return to WWW with yield, which is a simple code, but C# requires the method calling yield to return the IEnumerator type, which is too restrictive and inflexible.(

Continue with the LoadAsset method:

 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.          
8.        //load scene  
9.        foreach (Asset sceneAsset in mScene.AssetList) {  
10.            if (sceneAsset.isLoadFinished) {  
11.                continue;  
12.            } else {  
13.                LoadAsset(sceneAsset);  
14.                if (!sceneAsset.isLoadFinished) {  
15.                    return null;  
16.                }  
17.            }  
18.        }  
19.    }   

The code runs here, indicating that the resources have been downloaded.The processing resources are now loaded.The first time must be to load the configuration file first, because it's in Json format, converting it into C#objects using the JsonMapper class, and I'm using the LitJson open source class library.Then loop through each resource in the scene recursively.If not, return null and wait for the next frame to be processed.(

Continue with the LoadAsset method:


 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.  
10.        if (asset.AssetList != null) {  
11.            foreach (Asset assetChild in asset.AssetList) {  
12.                if (assetChild.isLoadFinished) {  
13.                    continue;  
14.                } else {  
15.                    Asset assetRet = LoadAsset(assetChild);  
16.                    if (assetRet != null) {  
17.                        assetRet.gameObject.transform.parent = asset.gameObject.transform;  
18.                    } else {  
19.                        return null;  
20.                    }  
21.                }  
22.            }  
23.        }  
24.    }  
25.  
26.    asset.isLoadFinished = true;  
27.    return asset;  
28.}  

Finally, the real resources are processed, the WW object is found in the cache, and the Instantiate method is called to instantiate the gameobject into Unity3D.The UpdateGameObject method sets gameobject properties, such as position and rotation angle.Then there is a loop that recursively processes the parent-child relationship of a gameobject in order to load child objects.Note that if LoadAsset returns null, the WW is not downloaded until the next frame is processed.Finally, set the load completion flag to return the asset object.(

UpdateGameObject method:

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

Only three properties of GameObject are set here. Good-eyed students will surely find that these objects are "dead" because they will not interact with players without scripting properties.Setting script properties is more complicated. As the main program downloads locally, the compiled scripts should also be loaded through a configuration file, then created by C#reflection, and assigned to the corresponding gameobject.(

Finally, the Scene and asset codes:

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

The code is finished, and in my actual tests, I see GameObjects loaded and displayed on the screen one by one, without affecting the game.The code also needs to be further refined for more resource types, such as animation resources, text, fonts, pictures, and sound resources.(

In addition to online games, dynamic loading of resources is also necessary for game development in large companies.It allows game planning (responsible for scene design), artist and program roles to be independent, greatly improving development efficiency.Imagine how inefficient and cumbersome it would be if you planned to change the position of any NPC, changed an animation, or changed a program.(

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

Generate Profile Code

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")]//I don't know why'@'is used to add menus here
        static void ExportAssetBundles ()
        {//This function represents a function that responds with the click above
                   assetList.Clear();
                   string path = AssetDatabase.GetAssetPath(Selection.activeObject);//Selection indicates that your mouse selects the active object
                  Debug.Log("Selected Folder: " + path);
       
       
                  if (path.Length != 0)
                  {
                           path = path.Replace("Assets/", "");//This is because AssetDatabase.GetAssetPath gets types such as Assets/folder names and looks at the next sentence.
                            Debug.Log("Selected Folder: " + path);
                           string [] fileEntries = Directory.GetFiles(Application.dataPath+"/"+path);//Because Application.dataPath gets types such as Project Name/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[1];
                                     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);//Instantiate a json object
                                              string bundlePath = Application.dataPath+"/../"+path;
                                               if(!File.Exists(bundlePath))
                                                {
                                                   Directory.CreateDirectory(bundlePath);//Folder in Asset sibling directory
                                                }
                                              bundlePath+="/"  + t.name + ".unity3d";
                                             Debug.Log("Building bundle at: " + bundlePath);
                                             BuildPipeline.BuildAssetBundle(t, null, bundlePath, BuildAssetBundleOptions.CompleteAssets);//Generate.unity3d file under corresponding folder
                                    } 
                           }
                            assetList.Add("AssetList",config);
                            for(int i=0;i<config.Count;i++)
                            {
                                    Debug.Log(config[i].Source);
                            }
                  }
                string data=JsonMapper.ToJson(assetList);//Serialized data
                Debug.Log(data);
        string jsonInfoFold=Application.dataPath+"/../Scenes";
        if(!Directory.Exists(jsonInfoFold))
        {
            Directory.CreateDirectory(jsonInfoFold);//Create Scenes Folder
        }
                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))//Write data to the created file
                {
                        fs.Write(uni.GetBytes(data),0,uni.GetByteCount(data));
                        fs.Close();
                }
      }
}

Topics: JSON xml