Asset Selector and Modding – BB – Update 12

close upAsset Selector and Modding – BB – Update 12close up

Posted: February 19, 2025

Last Updated: February 26, 2025

Asset Selector and Modding – BB – Update 12

Timeframe: February 1, 2024 – February 18, 2024

!!! Jump to Visual Demos !!!

Long time no see! It’s been almost 3 weeks since the last update. Setting aside computer problems, it took so long because I also just… did a lot of stuff. While some of it was spent fleshing out the builder inspector, much of it was focused on adding MODDING capabilities to the game. Isn’t that cool? Let’s get into it.

EVERYTHING:

Asset Selectors:

Assets were not integrated into the InGameBuilders very well until now. Up to this point, I had a single exposed field having to do with assets: the key for a sound effect to play upon firing bullets. Since this was (mostly) just interpreted as a string, I just left it as an input text box the user could type into. The problem with this is that it’s annoying as hell. You have to make sure the key you type exists or else nothing happens. I’ve fixed that with asset selectors.

Basically, these asset fields are now buttons. When you click the button, it pulls up a file browser where you can locate the relevant asset types in your project folder and just select the file itself. This then automatically transforms the file path into a relevant key that can be used to query that asset in whatever system uses it.

  • AssetSelectorField: A MonoBehaviour like InteractiveSlider or InteractiveDropdown that exposes a string value (the asset key) but changes through a custom file selection process.
  • AssetMapManager: A static class designed to easily retrieve an AssetMap of a given asset type. Current works for Sprite, Font, and AudioClip assets.
  • AssetMapHelpers: Provides simple conversion from type names to Types. Helpful for converting information in the builder inspector config file.
  • AudioAssetManager: Changed the AudioAssetManager (pretty old) to be an IAssetKeyMap<AudioClip>, which I had put off on doing for a long time. It has an additional check in GetAsset which allows users to pass in either an asset path or an asset key.
  • AssetSelectorWidget, AssetSelectorWidgetDisplay, AssetSelectorWidgetButton: Altered usage to allow Types to be passed in to correctly retrieve the relevant AssetMap from AssetMapManager.
  • Binder: Altered to accept AssetSelectorField as an interactive element.
  • BuilderBindingGroup: Altered to accept AssetSelectorField as an interactive element.
  • BuilderInspectorField: Altered to allow creation of AssetSelectorFields.

The Entity Prefab Fields Corollary:

Similar to the asset selector fields, I also spent a little bit of time on the prefab fields. These are fields where you have to select an existing creation upon which your current creation relies. For example, in the FiringPointBuilder, you may select a bullet you have created to be fired from your new firing point. Before this change, this was completely customized for each builder; i.e. each builder had custom logic to properly set, pack, and unpack these entity prefabs in your subject. Now, most of the logic has been offloaded from individual builders back onto the Binder, BuilderBindings, and BuilderInspectorFields.

Here is a snippet of old code used to change the bullet creation being used by the subject in the FiringPointBuilder:

// **** OLD FiringPointBuilder code********************************************
// Call ChangeBulletPrefab by manually creating a UI button
// and hooking up the event to this.
public void ChangeBulletPrefab() {
    // Hook up prefab selection widget
    string[] categories = new string[] {
        "Bullet",
    };
    // This filters, organizes and displays a list of prefabs
    // matching category "Bullet"
    // Performs callback to SetBulletPrefab with the GUID and potential index
    prefabSelectionWidget.ShowDisplayRegistry(SetBulletPrefab, categories);
}
// This is responsible for manually taking the returnd Guid
// and placing it into the proper field.
// We want to bypass this by simply treating Entities as
// another transformable data type that we can get/set
// using reflection and our Binder.
public void SetBulletPrefab(Guid guid, int idx) {
    Debug.Log($"SetBulletPrefab called w/guid {guid} for idx {idx}");
    if (!this.TryGetSubjectComponents(false)) { return; }
    if (firingPointBuffer.Length > 0 && idx < firingPointBuffer.Length && idx >= 0) {
        FiringPointElement fpe = firingPointBuffer[idx];
        // Set prefab based on found info
        EntityIDPair info = GetInfoByID(guid);
        if (InfoNotFound(info)) { return; }
        // other junk here...
        // ...
        fpe.OriginalPattern.projectilePrefabID = info.ID;
        firingPointBuffer[idx] = fpe;
    }
}

Similar logic would have to be copy-pasted for each prefab field you wanted to edit, and you’d have to make a button and manually hook it up for each prefab. With, the RegistryPrefabField, though, it is treated just like any other basic data type in the Binder, with some waiting/user-selection logic built right into the interactive element.

The BuilderInspector now instantiates a GameObject with a RegistryPrefabField for entity prefab fields, just like how it would instantiate an object with an InteractiveSlider or InteractiveDropdown for fields of other data types. This RegistryPrefabField can now also be used as a bindingObject in a BuilderBinding, so when the RegistryPrefabField changes its ‘value’ (i.e. when the user finishes selecting an entity prefab), it automatically performs the associated Binder callbacks.

  • RegistryPrefabSelectionWidget, RegistryPrefabsDisplay: Altered usage to allow int type categories instead of only EntityPrefabMapper name categories.
  • Binder: Altered to accept RegistryPrefabField as an interactive element.
  • BuilderBindingGroup: Altered to accept RegistryPrefabField as an interactive element.
  • BuilderInspectorField: Altered to allow creation of RegistryPrefabFields.
  • InGameBuilder: Added utilities to help check for potential reference cycle in used prefabs to prevent cyclic dependencies.

Modding:

Modding is what actually took me the plurality of the past few weeks (coupled with a failing hard drive). More details can be found on this long-form post on modding in ECS and at this example git repo. As I mention in that post, I will never have the time nor creativity to make all the kinds of stuff that modders can make, so adding modding support seemed like my best bet on making this truly expandable.

The long and short of it is that I had to mess around with loading DLLs at runtime from a specific mods folder with a LOT of finicky, ECS-specific quirks that made my life hell while I tried to figure out why certain errors decided to (or decided not to) show up in the editor’s play mode vs. the build. Here’s a bulleted summary:

  • ModLoaderBaseEntities: A mod loader that checks a specific folder for DLLs, reads their status from a metadata file, and decides whether to load each DLL.
  • AttachToEntityClonerInjection: A class in Unity.Entities that I was forced to modify because its static constructor always seemed to initialize before anything else. I had to change it to call the mod loader before its own actual initialization logic (just because it calls TypeManager.Initialize()).
  • Moved the Entities package to a modifiable Packages folder. This turned out to cause strange problems in the build until I patched it.
  • Other basic GameObject-based mod tools, but I haven’t done much with those.
  • Created an SDK for bullet hell-maker modding. It requires Unity, but you get access to the namespaces/DLL from the bullet hell project. Also, you just need to drag the assembly produced by the project to the mod folder and it should work.

Other:

  • Add Element button added to BuilderInspectorBufferDisplay.
  • Fixed error caused my mounting a null Entity in the BaseMountSystem.
  • Altered InGameBuilder’s GetPrefabFieldInfo to optionally return null Entities for fields that are empty instead of the default behavior of not including the “not found” info.

Video Demos

Gravity mod example 1. Shooting 5 bullets each with increasing speed. The gravity system automatically applies a fake gravity to each spawned bullet.

Gravity mod example 2. Shooting bullets in a spray upwards. The aim has a sine-based functional sweep with an amplitude of 20 degrees and frequency of 0.2Hz.

Gravity mod example 3. Two firing point elements shooting radially and with a constant turn rate in opposite directions.

Add element button. Now creates a default structure in the binding object and updates the buffer display, causing a new element to show in the display.

Asset selector demonstration. Here, we change the audio clip played when the FP fires (turn audio on to hear it).