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 (12) · 3 Reviews

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

Loading Mods From the Mod Directory

Find LoadMods() and add the following code to it:

DirectoryInfo modDirectory = new DirectoryInfo(path);

foreach (FileInfo file in modDirectory.GetFiles())
{
    if (file.Extension == ".json")
    {
       // 1
    }
}

Here, you make LoadMods async so it works with the asynchronous nature of Addressables.

This segment is responsible for first getting the DirectoryInfo to locate the mods, then looping over the FileInfo of each file inside the given directory. When it approaches a file that ends in the .json format, it knows that it’s an Addressables catalog file.

Now, at // 1, add the following:

// 1
string modName = file.Name;
modName = modName.Replace(".json", "");
modName = modName.Replace("_", " ");
modName = System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(modName.ToLower());

// 2
IResourceLocator modLocator = await LoadCatalog(file.FullName);

// 3
ModInfo mod = new ModInfo
{
    modFile = file,
    modAbsolutePath = file.FullName,
    modName = modName,
    isDefault = false,
    locator = modLocator
};
mods.Add(mod);
ReloadDictionary();

Here’s what’s going on in the code above:

  1. Format the name of the mod from: mod_name.json to Mod Name.
  2. Use LoadCatalog to return the IResourceLocator of the current mod. You can only use the await keyword within an async method, it essentially hangs the thread while the result loads.
  3. Store the information in ModInfo and add it to the list of current mods.

For your next step, you need to implement loading the catalogs.

Loading New Catalogs Into Addressables

Below LoadMods, find LoadCatalog. Remove any existing code within the body of the method and replace it with the following:


AsyncOperationHandle<IResourceLocator> operation = 
    Addressables.LoadContentCatalogAsync(path);
// Wait until the catalog file is loaded 
// then retrieve the IResourceLocator for this mod
IResourceLocator modLocator = await operation.Task; 
return modLocator;

This method takes in the path of the catalog file and returns an IResourceLocator. The key method is the Addressables.LoadContentCatalogAsync method. Addressables provides this method to load external catalog files.

So how is the user going to load a new mod into your game? In your next step, you’ll create a button that they can click to add a mod.

Loading Mods With a Button

To load a mod with the click of a button, you’ll use the dictionary. Find ReloadDictionary and add the following to it:

modDictionary.Clear();

for (int i = 0; i < mods.Count; i++)
{
    modDictionary.Add(mods[i].modName, mods[i]);
}

for (int i = 0; i < buttons.Count; i++)
{
    GameObject.Destroy(buttons[i].gameObject);
}

buttons.Clear();

foreach (ModInfo info in mods)
{
    Button newButton = Instantiate(buttonPrefab, buttonParent);
    buttons.Add(newButton);

    newButton.onClick.AddListener(() =>
    {
        ChangeMod(info.modName);
    });

    newButton.GetComponentInChildren<Text>().text = info.modName;
}

So what’s going on in this code?

First, you loop through each mod and add it to the dictionary. The dictionary maps the name to ModInfo. You use this to get the IResourceLocator.

Then, you delete the existing buttons on-screen and replace them with the new buttons. The buttons let the user select a new mod.

By looping through each loaded ModInfo, you instantiate a newButton. You add a click listener to each button to call ChangeMod with the parameter of the current mod’s name. This makes each button load a new mod.

For your next step, you’ll give the user a way to load new mods.

Changing the Currently Loaded Mod

Now, find the ChangeMod method and add the following to its body:

lookupManager.ClearLoadedGameObjects();
activatedMod = newModName;
LoadCurrentMod();

As mentioned before, ClearLoadedGameObjects() removes all the instantiated GameObjects and removes the current mod’s assets from memory to make way for the new mod.

Now, you just need to load the current mod. To do so, find LoadCurrentMod and add the following:

if (modDictionary.ContainsKey(activatedMod))
{
    lookupManager.instances.Clear();
    for (int i = 0; i < lookupManager.requiredAssets.Count; i++)
    {
        lookupManager.instances.Add(
            lookupManager.requiredAssets[i],
            FindAssetInMod(
                lookupManager.requiredAssets[i],
                modDictionary[activatedMod]
                )
            );
    }

    for (int i = 0; i < modUpdateListeners.Count; i++)
    {
        modUpdateListeners[i]();
    }
}

Here's what's going on above:

You need to initialize the ReferenceLookupManager for each mod, and this is where you set it up. The list of requiredAssets is in the ReferenceLookupManager within the activated mod.

First, you loop through all the required assets within the lookupManager. Then, for each requiredAsset[i], you find the IResourceLocation for the asset with that key by using FindAssetInMod().

Finally, you loop through all the modUpdateListeners and inform them of the mod change. modUpdateListeners come from the InstanceHolder components.

The last thing you need to do is make it possible to find specific assets inside the mod.

Finding Assets Within the Mod

To do this, find FindAssetInMod and replace its existing lines with:

IList<IResourceLocation> locs;
if (mod.locator.Locate(key, typeof(object), out locs))
{
    return locs[0];
}

if (mod.modName != mods[0].modName)
{
    return FindAssetInMod(key, mods[0]);
}

Debug.LogError("This asset could not be found, ensure you are using the right key, or that the key exists in this mod");

return null;

Here, you use Locate inside IResourceLocator to get the IResourceLocation of all the assets with the address of key.

If the script can't find the asset within the mod, then you search for it within the default mod. This is where Addressables shine: They give you the ability to mix and match different asset bundles without dependency and loading issues.

Now, let out a sigh of relief because the coding is over! You just created your very own modding pipeline from scratch. To recap, ModManager works like this:

  1. You initialize the default mod and store it in a struct called ModInfo.
  2. The script searches the given mod directory for catalog files. Then, for every catalog file, the script stores the mod's IResourceLocator.
  3. You update the UI, then map the buttons to each of the loaded mods.
  4. When you click a button, the script removes the current mod from memory and ReferenceLookupManager updates the instances dictionary with the new mod's assets.
  5. When a script uses Instantiate from ReferenceLookupManager, it gathers the asset from the currently loaded mod.

There's just one more thing to do: You have to package the existing assets within the game into the Addressables system within the Game Starter project.

Creating the Default Mod

Open Window ▸ Asset Management ▸ Addressables ▸ Groups. Note that the prefabs within the project have already been marked as Addressable and have the correct addresses attached. If you had to do this yourself, you'd follow the same process as in the Content Starter project.

Before you build the default mod, it's important to change the Play Mode Script to Use Existing Build. This ensures that the Addressables system uses fully built catalog files rather than simulating catalog files.

Changing the Play Mode Script to Use Existing Build

Finally, select Build ▸ New Build ▸ Default Build Script.