Try โ€‚โ€‰HackMD

Resolving Script References

You're working on the most amazing map you've ever made. It has it all, floors, walls, even ceilings. But you've decided it's missing something important, traps!

This isn't the first time you've used Unity, so you open up your code editor and write a quick script for a simple trapdoor. When the trigger is hit, it disables all the floor objects, and hopefully your naive player will fall in.

public class Trapdoor : MonoBehaviour { public GameObject[] thingsToToggle; void OnTriggerEnter(Collider other) { // Simply disable all of the listed gameobjects foreach (GameObject gameObj in thingsToToggle) { gameObj.SetActive(false); } } }

You add your script to the scene and give it a quick in-editor test to be sure it's working, and then you build your assets and run the game.

Unfortunately, your player finds that they're just fine. Your script doesn't seem to be working, and a new error has appeared in the console:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

The Problem

We see custom scripts exported with our assets all the time. Items have scripts for handles, whoosh points, damagers, and more. These scripts seem to work fine in the game, so what's going on here?

Lets go ahead and dump the contents of our trapdoor component and see what the addressables system is bundling:

{ "m_GameObject": { "m_FileID": 0, "m_PathID": -8710228835305402403 }, "m_Enabled": 1, "m_Script": { "m_FileID": 0, "m_PathID": 6447319093052299756 }, "m_Name": "", "thingsToToggle": [ { "m_FileID": 0, "m_PathID": -3648254178535100691 }, { "m_FileID": 0, "m_PathID": -153881632763166575 }, { "m_FileID": 0, "m_PathID": -3198262604198698770 }, { "m_FileID": 0, "m_PathID": -6041985994401417334 } ] }

This might look like nonsense at first, but it actually makes sense. We can see our thingsToToggle field is included here, with a list of the gameobjects it was referencing. But it doesn't look like our code was included in the bundle at all.

The other fields in this dump are fairly self explanatory, but what is m_Script? It seems to point to another asset in our bundle, so lets take a look:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

We can see the name of our class here, and the name of the assembly it is contained inside.

This makes sense! Instead of packing the code in our bundle, addressables instead packs a reference to where the code should be in the built version of the game.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
The other fields in our bundle come from the MonoBehaviour class that our script inherits from.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
The default assembly Unity places your scripts in is called Assembly-CSharp.dll. This is why we see a reference to this assembly in our bundle.

What We Learned

Script Reference

Code is not included in our asset bundles. Instead, a reference for where to find our code is packed inside. The information it uses for this is as follows:

  • The class name
  • The namespace our class is contained inside
  • The assembly our namespace is contained inside

Serialized Fields

Any serializable fields that our class had were preserved. Looking inside our bundle, we found that the thingsToPreserve variable was packed and the references it had were included.

"thingsToToggle": [ { "m_FileID": 0, "m_PathID": -3648254178535100691 }, { "m_FileID": 0, "m_PathID": -153881632763166575 }, { "m_FileID": 0, "m_PathID": -3198262604198698770 }, { "m_FileID": 0, "m_PathID": -6041985994401417334 } ]

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’
All public fields are serializable by default, but you can explicitly mark private fields as serializable using the [SerializeField] attribute.

You can read more about serialization here:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/serialization/

The Solution

To solve our problem we need to do two things.

The First Thing

We need to actually include our code with our mod. Thankfully this part isn't too bad! You can follow this guide here to learn how to build assemblies that you can include in your mod folder:
https://hackmd.io/@lyneca/bas-script-modding#Writing-Code

We're going to include an exact copy of our Trapdoor class in this assembly, and include this assembly in our mod folder. The name of this assembly file is important!

The Second Thing

We need to find a way to change the location our script reference points to. Currently it tells Unity to check Assembly-CSharp.dll for our class. Unfortunately we can't modify this assembly to include our code, so we need to redirect this reference elsewhere.

Assembly Definitions

Thankfully, Unity provides a built-in solution to this problem! Assembly Defintions are a special asset that will cause all scripts it shares a folder with to build to a different assembly.

We can create an assembly definition by right clicking in the project window and selecting it under the Create> dropdown. The name of this file is important, we want to make sure it matches the name of the assembly we built to our mod folder (excluding the .dll)

The last step is to make sure your scripts are placed in the same folder as your assembly defintion, and then simply rebuild your mod!

Inspecting our bundle again, we can see it now looks for our custom assembly instead!

Bonus

Minifying

Since only our fields were included in the bundle, we can actually exclude our methods in the Unity version.

Script in our Unity project:

public class Trapdoor : MonoBehaviour { public GameObject[] thingsToToggle; }

Script in our mod folder assembly:

public class Trapdoor : MonoBehaviour { public GameObject[] thingsToToggle; void OnTriggerEnter(Collider other) { // Simply disable all of the listed gameobjects foreach (GameObject gameObj in thingsToToggle) { gameObj.SetActive(false); } } }

UnityEvents

We can still make use of UnityEvents without breaking references. Take this simple component for example:

public class GraphicColorChanger : MonoBehaviour { public Graphic graphic; public void RandomizeColor() { Color randomColor = new Color( Random.value, // R Random.value, // G Random.value, // B 1); // A graphic.color = randomColor; } }

We can create a button and reference this method in the Unity Inspector

And so long as a matching script exists in our mod assembly, this event will still work! You can also minify method code in your Unity version, since you're only looking to reference the method:

public class GraphicColorChanger : MonoBehaviour { public Graphic graphic; public void RandomizeColor() {} }

And that's it! I hope you can find this helpful. If you have any questions, feel free to contact me on Discord.