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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Introduction to Modding Unity Games With Addressables
35 mins
- Getting Started
- Understanding Addressables
- Advantages of Addressables
- Looking Under Addressables’ Hood
- Exploring the Project
- Understanding the Addressables’ Settings
- Setting up the Mod
- Renaming and Building Addressable Assets
- Loading Mods Using Addressables
- Creating the Reference Lookup Manager
- Setting up the Reference Lookup Manager
- Instantiating Assets Into the Scene
- Removing Assets From Memory
- Modifying Player Scripts
- Handling Bullet Spawning
- Handling Debris Spawning
- Creating the Mod Manager
- Loading Mods From the Mod Directory
- Loading New Catalogs Into Addressables
- Loading Mods With a Button
- Changing the Currently Loaded Mod
- Finding Assets Within the Mod
- Creating the Default Mod
- Testing and Profiling the Game
- Using the Addressables Event Viewer
- Where to Go From Here?
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:
- Allows you to instantiate a GameObject by using its key at a certain position, facing direction and with a parent.
- Ensures that the provided key exists. Otherwise, Addressables won’t be able to find the asset.
- Uses
InstantiationParameters
to tell Unity how to spawn an object. -
Addressables.InstantiateAsync
works like the normal instantiate method with the exception that it takes in anIResourceLocation
as the object to spawn. It knows which key to use thanks toinstances[key]
. - 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:
- Bullet spawning
- 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:
-
IResourceLocator
allows Addressables to find anIResourceLocation
, given the address of the asset. - 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:
- 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. - Sets up
ModInfo
with the information provided.handle.Result
contains theIResourceLocator
of the default mod. - 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