Introduction to Modding Unity Games With Addressables

Use Unity Addressables to make it easy to let users create mods, enhancing the user experience and expressing their creativity through your game. By Ajay Venkat.

4.9 (13) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

Instantiating Assets Into the Scene

Once you bring an asset into memory, deleting the object from the scene won’t remove it from memory again. You must save the AsyncOperationHandle of that object and manually release it. You’ll retrieve this handle when you instantiate an object by using ReferenceLookupManager.

To handle instantiating new objects, add the following within the class:

// 1
public AsyncOperationHandle<GameObject> Instantiate(string key, Vector3 position, Vector3 facing, Transform parent)
{
    // 2
    if (!instances.ContainsKey(key))
    {
        Debug.LogError("The object you are looking for doesn't exist in this mod pack.");
    }

    // 3
    InstantiationParameters instParams = 
        new InstantiationParameters(position, Quaternion.LookRotation(facing), parent);

    // 4
    AsyncOperationHandle<GameObject> handle = 
        Addressables.InstantiateAsync(instances[key], instParams, true);

    // 5
    loadedGameObjects.Add(handle);

    return handle;
}

This method allows you to instantiate a GameObject by simply using the key, also known as the address of the asset. Here’s a breakdown:

  1. Allows you to instantiate a GameObject by using its key at a certain position, facing direction and with a parent.
  2. Ensures that the provided key exists. Otherwise, Addressables won’t be able to find the asset.
  3. Uses InstantiationParameters to tell Unity how to spawn an object.
  4. Addressables.InstantiateAsync works like the normal instantiate method with the exception that it takes in an IResourceLocation as the object to spawn. It knows which key to use thanks to instances[key].
  5. Adds the handle to the list of loadedGameObjects so you can remove it from memory in the future.

Since the operation is asynchronous, Unity won’t return the instantiated GameObject immediately. Therefore, you return the handle, which you can use to get the GameObject once the operation completes.

Removing Assets From Memory

You need to add one final method to the ReferenceLookupManager to clear the instantiated GameObjects when switching mods:

public void ClearLoadedGameObjects()
{
    foreach (AsyncOperationHandle handle in loadedGameObjects)
    {
        if (handle.Result != null)
        {
            Addressables.ReleaseInstance(handle);
        }
    }

    loadedGameObjects.Clear();
}

This method loops through all the handles acquired in the instantiation phase and uses Addressables.ReleaseInstance to remove the GameObject and its asset from memory.

Save the script, then open RW/Scripts/InstanceHolder.cs to make one minor change. Within ModUpdated(), add the following line:

manager.lookupManager.Instantiate(nameReference, transform.position, transform.forward, this.transform);

The script will call ModUpdated() when you load a new mod. This means that each time the mod changes, InstanceHolder will spawn a GameObject using ReferenceLookupManager at the correct location.

Overall, ReferenceLookupManager takes care of instantiation and the InstanceHolder tells ReferenceLookupManager where and what to instantiate.

Modifying Player Scripts

You still have a few problems to tackle. For example, you have to call all the instantiations in the game through ReferenceLookupManager, including:

  1. Bullet spawning
  2. Debris spawning

You’ll find a better way to handle this now.

Handling Bullet Spawning

Open RW/Scripts/PlayerController.cs and, at the bottom of the file, find the following lines:


instanceQueue.Enqueue
(
    GameObject.Instantiate(gameObjectReference, position, 
        Quaternion.LookRotation(faceDirection), null)
);

This simply instantiates the bullet GameObject and adds it to an instanceQueue that supports some basic object pooling. Now, remove those lines and replace them with the following:


ReferenceLookupManager.instance.Instantiate(
    "Bullet",
    position,
    faceDirection,
    null
    ).Completed += reference =>
    {
        instanceQueue.Enqueue(reference.Result);
    };

Here, you’re telling the ReferenceLookupManager to spawn an asset named Bullet with the given parameters.

Once Addressables complete the AsyncOperation, you can use the Completed callback to get the reference.Result. This is the GameObject that’s then added to the instanceQueue.

Note the reversal in the code structure. That’s because you have to wait until AsyncOperation completes to get the GameObject, then add it to the instanceQueue. Unity has to actually read a file from your disk into the game, which is the trade-off when using Addressables.

Handling Debris Spawning

The same concept applies for the Debris that appear when the bullet hits a surface. Open RW/Scripts/Bullet.cs and find the following lines:

GameObject particleSystem = GameObject.Instantiate(particleSystemInstance, 
    other.GetContact(0).point, 
    Quaternion.LookRotation(other.GetContact(0).normal), 
    null);
Destroy(particleSystem, 1);

This code spawns the particle system at the collision point and then destroys it after one second. Remove this code and replace it with the following:

ReferenceLookupManager.instance.Instantiate(
    "Debris",
    other.GetContact(0).point,
    other.GetContact(0).normal,
    this.transform
    ).Completed += go =>
    {
        go.Result.GetComponent<ParticleSystem>().Play();
        GameObject.Destroy(go.Result, 1);
    };

Here, you’re telling the ReferenceLookupManager to spawn the Debris asset. How it will change depends on the mod. Once you instantiate Debris, you can use its reference to Play() the particle system and then Destroy it after one second.

You’re almost there. Now, you just need to find the mod files and set up the ReferenceLookupManager‘s variables and everything will work perfectly!

Creating the Mod Manager

The ModManager script is responsible for finding and loading the mods.

Open RW/Scripts/ModManager.cs and locate ModInfo at the bottom of the file. That struct contains all the information required to find and store a mod. Here’s what each variable within the struct does:

  • modName: The name of the mod.
  • modAbsolutePath: The file path to the mod.
  • modFile: Class containing file information about the mod.
  • locator: The mod’s IResourceLocator.
  • isDefault: Whether this is the default mod that comes with the game.

There are two important things to note here:

  1. IResourceLocator allows Addressables to find an IResourceLocation, given the address of the asset.
  2. The default mod contains the assets that come with the default game. You’ll set this up soon.

Now, look at the variables at the top of the class scope. path is where you must load the mods. It’s nested within the Assets folder.

To set up Start(), add the following lines:

// 1
Addressables.InitializeAsync().Completed += handle =>
{
    // 2
    ModInfo defaultContent = new ModInfo
    {
        isDefault = true,
        locator = handle.Result,
        modAbsolutePath = "",
        modFile = null,
        modName = "Default"
    };

    // 3
    mods.Add(defaultContent);
    ReloadDictionary();
    activatedMod = "Default";
    LoadCurrentMod();
};

LoadMods();

This can look overwhelming, so here’s the breakdown:

  1. You need this line to initialize the Addressables system, but you can also use it to gather the IResourceLocator of the currently loaded Addressables assets.
  2. Sets up ModInfo with the information provided. handle.Result contains the IResourceLocator of the default mod.
  3. Adds the default mod to the list of mods and makes it the currently activated mod.

This code isn’t fully functional because you haven’t implemented ReloadDictionary(), LoadCurrentMod() or LoadMods() yet. You’ll do that next