Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question How to implement exchangeable GameObjects?

Discussion in 'General Graphics' started by XRay3, Aug 4, 2023.

  1. XRay3

    XRay3

    Joined:
    Aug 9, 2021
    Posts:
    5
    Hey,

    I've got a question about a general workflow and would like to ask for some comments and recommendations about options and their pros and cons.

    Situation: I'd like to change my assets at runtime. Let's say may game uses assets in different flavours, each asset is modular and can be exchanged one by one (flipping materials won't work here). I like to build the small world/levels with one base asset in the editor but want to allow:
    a) me to change to flavour during design phase
    b) the user to change the flavour during runtime

    The very first idea was to swap scenes (same world in different flavours) or having a parent gameobject with the flavours and enabling/disabling them. However, both ways would lead to unmaintainable situations as I'm pretty sure that more flavours will follow. Each structural change would lead to 3 to x changes in fact.

    My current approach is to create prefabs with all flavours attached like this structure:
    - Fence [Script: ExchangeableAsset)
    --- Default (enabled by default)
    ----- a low poly fence asset
    --- HighRes (disabled by default)
    ----- a high res fence asset
    --- Pixelated (disabled by default)
    ----- a pixelated fence asset

    I'd then create two actions (one for editor, one for game mode) that cycle through all "ExchangeableAsset"s and disable the "default" ones while enabling the highRes or Pixelated ones.

    I'm pretty sure there must be better ways. How did you or would you implement such a feature?
     
  2. JijiruJiru

    JijiruJiru

    Joined:
    Aug 3, 2023
    Posts:
    6
    hmmmm I am a newbie myself but I was just playing with AssetBundles and I think they are made for such scenarions (among other things)

    you could load the asset bundles during startup into memory and load them during runtime from memory, my idea would be to create asset bundles for each of your flavors and then load them during runtime as you wish

    On a second thought you'd still have to go through the game objects an respawn them or whatever (maybe through reloading the scene, although I am unsure if that is what you want)

    just my incompetent 2cents
     
  3. c0d3_m0nk3y

    c0d3_m0nk3y

    Joined:
    Oct 21, 2021
    Posts:
    568
    Question is, does the exchange have to be instantaneous or is it ok to have a short loading time?

    If it needs to be instantaneous, you will need to keep all the flavors in memory and just disable the renderers of the inactive ones (basically what you are doing). If a short loading time is fine, you can write some code which destroys the game object of the old flavor and instantiates the prefab of the new flavor.

    To avoid a hitch with the second approach, you will have to load the data asynchronously. You will also have to call Resources.UnloadUnusedAssets() to make sure that the previous flavor gets purged from memory.

    Both solutions require a little bit of coding, but not much.
     
  4. XRay3

    XRay3

    Joined:
    Aug 9, 2021
    Posts:
    5
    Thanks for your answers! Ok, as I see there is no built in solution or best approach.

    As it doesn't need to be instantaneous I will most probably change the behaviour and use the resource information script to replace the prefabs. So I will only add the default one to the prefab as a gameobject and reference the others as prefabs, then destroy/unload old ones and instantiate the new ones accordingly.

    I'm not afraid of the code (as a professional software engineer), just wandering if there was a better solution that supports such process out of the box. If we check professional games out there this seems to be quiet common feature (at least for level of details). Btw: could I use the LoD mechanism in some way?
     
  5. JijiruJiru

    JijiruJiru

    Joined:
    Aug 3, 2023
    Posts:
    6
    without knowing the LoD system in detail, I know one thing: Abusing systems for purposes they are not built for may work in a limited fashion but in general is never a good idea

    ______________________________________________________

    HOWEVER I find the problem you are having very interesting and as I want to learn stuff and gain a bit experience with the system, I will experiment a bit and try to come up with a PoC, I'll let you know if I come up with something, that seems feasable
     
  6. XRay3

    XRay3

    Joined:
    Aug 9, 2021
    Posts:
    5
    That's absolutely true but a misunderstanding :) I don't want to abuse it, just asking if it is intended to use it for such a requirement? It seems that it can do exactly what I'm looking for, but is (obviously) only used for details with distance calculation.

    Thanks. If you find a feasible idea let me know :) (I'm not looking for code, only a concept).
     
  7. JijiruJiru

    JijiruJiru

    Joined:
    Aug 3, 2023
    Posts:
    6
    Ok I have to admit, I am a bit proud b/c I made it and even in the way I initially proposed

    Yeah I admit its only in 2D and so on but the idea is the same

    Here in my Crappy Bird tutorial thingy (I am especially proud of the bird rotation which somewhat follows its trajectory):
    For demonstration purposes there are two different versions of the pipes and the red ones come without collision which is demonstrated here:
    assetSwap.gif

    I stuck to my initial idea:
    • create different asset bundles with different versions of the asset in question
    • load them as needed (i can do this with a push of a button during runtime) - in this case i just keep them in memory, which is probably a bad idea for larger bundles
    • change references to assets
    • replace the objects

    Further considerations to make:
    • loading the asset bundles from disk (or potentially remotely) is rather heavy so a more sophisticated mechanism is needed, like a central manager instance that loads them async as the need arises
    • keeping them in memory might be to expensive... again something pointing to some central manager so that it can unload them, when not needed
    • obviously this is only going to work for objects where you have some code managing them, this should not be much of a problem, just load them via a tag or something and you are fine although I am not sure ATM how one would map them to assets... there should be a way
    • you'll need prefabs for that but you are doing this anyway
    • I have no idea how this going to handle with a large amount of objects... loading and unloading can be done async but replacing the objects... phew
    Code that does that, there are unnecessary sections removed:
    Code (CSharp):
    1. public class PipeSpawner : MonoBehaviour
    2. {
    3.     private const string GREEN = "assets", RED = "otherassets";
    4.  
    5.     //blah
    6.  
    7.     private GameObject pipe;
    8.     private List<GameObject> pipes = new List<GameObject>();
    9.     private AssetBundle ab;
    10.  
    11.     // Start is called before the first frame update
    12.     void Start()
    13.     {
    14.         loadAssetsFromBundle(GREEN);
    15.  
    16.  
    17.         //other init
    18.     }
    19.  
    20.     // Update is called once per frame
    21.     void Update()
    22.     {
    23.         if (timer <=  0)
    24.         {
    25.             pipes.Add(Instantiate(pipe,new Vector3(transform.position.x, UnityEngine.Random.Range(heightMin, heightMax), 0), transform.rotation));
    26.          
    27.             timer = SpawnRate;
    28.             //remove pipes, we dont need anymore
    29.             var newPipes = new List<GameObject>(pipes);
    30.             foreach(var p in pipes)
    31.             {
    32.                 if (p.transform.position.x < DESTROYPIPES)
    33.                 {
    34.                         newPipes.Remove(p);
    35.                         Destroy(p);
    36.                 }
    37.             }
    38.             pipes = newPipes;
    39.         }
    40.         else //blah
    41.  
    42.         if ( Input.GetKeyDown(KeyCode.S) )
    43.         {
    44.             switch( ab.name )
    45.             {
    46.                 case GREEN: loadAssetsFromBundle(RED); break;
    47.                 default: loadAssetsFromBundle(GREEN); break;
    48.             }
    49.         }
    50.     }
    51.  
    52.     private void loadAssetsFromBundle(string abName)
    53.     {
    54.         if (loadAB(abName) < 0)
    55.         {
    56.             return;
    57.         }
    58.  
    59.         //load assets
    60.         pipe = ab.LoadAsset<GameObject>("pipe");
    61.  
    62.         //switch all pipes to the new asset
    63.         var newPipes = new List<GameObject>();
    64.         foreach (var p in pipes)
    65.         {
    66.             Debug.Log("updating objects");
    67.             var pos = p.transform;
    68.             Destroy(p);
    69.  
    70.             var newP = Instantiate(pipe, pos.position, pos.rotation);
    71.             newPipes.Add(newP);
    72.         }
    73.         pipes = newPipes;
    74.     }
    75.  
    76.     private int loadAB(string abName)
    77.     {
    78.         // check if we loaded the bundle already
    79.         var abs = AssetBundle.GetAllLoadedAssetBundles();
    80.         foreach (var bundle in abs)
    81.         {
    82.             if (bundle.name == abName)
    83.             {
    84.                 ab = bundle;
    85.                 return 0;
    86.             }
    87.         }
    88.  
    89.         // if not, load it
    90.         var abPath = Path.Combine(Application.dataPath, "AssetBundles/" + abName);
    91.         ab = AssetBundle.LoadFromFile(abPath);
    92.  
    93.         // if we were not able to load, we have a serious problem
    94.         if (ab == null)
    95.         {
    96.             Debug.Log("unable to load asset bundle " + abPath);
    97.             return -1;
    98.         }
    99.  
    100.         return 0;
    101.     }
    102. }
    103.  
     
    Last edited: Aug 4, 2023
    XRay3 likes this.
  8. XRay3

    XRay3

    Joined:
    Aug 9, 2021
    Posts:
    5
    Thanks for your solution! I really appreciate it. I will take a deep dive into AssetBundles and check out how to transfer this to my project.