Search Unity

Creating a package to config packages

Discussion in 'Package Manager' started by ShawnFeatherly, May 18, 2020.

  1. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    Goal

    I'm trying to create a config package for a scoped registry environment that can be used to add configuration values to other child packages that have a dependency on the config package. When the child package is included in a project it'd have modifiable fields in two locations:
    1. In Builds, by a *.json file that gets dumped by the child package into the projects StreamingAssets folder.
    2. In the Unity editor, by modifying a field in the inspector or an UnityEditor window.

    Using ScriptableObjects

    I have this working as a ScriptableObject in an embedded environment. Adding a new configurable value to a child package is done by inheriting a ScriptableObject based class from the config package and adding public fields to that class. This inheritance allows an *.asset file of the ScriptableObject in the child package to have the new fields in the UnityEditor without any additional work and is in a structure exportable to *.json. That *.asset file can be modified in projects that use the child package.

    The *.json file is the source of truth. The ScriptableObject loads values in from the *.json file when the Unity editor starts. If values are modified in the ScriptableObject's inspector, they trigger OnValidate and overwrite the *.json file.

    This method does now work when the package is in a scoped registry as that makes the package read only. This means the ScriptableObject in the child package can't be modified in the Unity Editor. Modifying the *.json file in builds still works fine.

    Using Samples

    Using the samples~ folder would lift the read-only issue. However, the package wouldn't be configurable if the sample isn't installed. Is there a way to force a sample of a package to be installed?

    Using PlayerPrefs

    I considered playerprefs, but didn't implement due to the work involved. I believe I'd have to replicate the way the inspector UI is generated for ScriptableObjects in a custom UnityEditor window. I'd also likely need a way for the config package to handle finding fields that were added by child packages. Despite the extra work, maybe player prefs is the only way?

    Question

    Maybe there are other ways I'm not thinking of. Is there a suggested way to store configuration values in a UPM package that can be modified in both the Unity editor and a build? Preferably in a way that the configuration logic can be a separate package that other package can have a dependency on.
     
  2. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    I ended up using the package to generate a ScriptableObject into the project. This removes the ability to reference that ScriptableObject for any examples included in the package. Instead that ScriptableObject had to be duplicated for any examples. Otherwise this solution worked well.

    I also investigated:
    1. https://docs.unity3d.com/ScriptReference/PackageManager.Client.Embed.html turned out to do nothing in Unity 2019.3.12 when run from a scopedRegistry. At least it was different behavior than an error in the console when run from an embedded package.
    2. Re-jigger Remote Config to be used for Local Config. This would have required too much re-jiggering. Got some feedback on the idea: https://forum.unity.com/threads/local-config.893419/
    3. Saw that samples may be able to be force installed with https://docs.unity3d.com/ScriptReference/PackageManager.UI.Sample.Import.html
     
    Last edited: Jun 9, 2020
  3. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    Hi @ShawnFeatherly,

    You seem to have really done a thorough investigation! I think you probably found the most straight-forward solution. It sounds fairly close to what I would have suggested, detailed below.

    Your configuration package can provide classes intended to be serialized to save the config options (whether to YAML or JSON), using ScriptableObject for "documents" (root objects to serialize) and "plain old" C# objects marked with the Serializable attribute. You can also have some code that treats those scriptable objects as singletons to be automatically discovered in the project, generated with defaults when none are present, be tweakable from playmode, etc. Then you can have your other packages go through this code to read the config objects before using/changing the values.

    Compared to embedding the config package, this has the advantage of not moving the logic from your config package to the project, and instead have a more structured way to load, save, and validate your configuration settings (not to mention dealing with configuration upgrades potentially more gracefully, and handling build events to copy the config files to StreamAssets or asset bundles as needed), and projects just need to track the configuration assets rather than what would basically be a "fork" of the config package. On the other hand, the config package ends up being a bit more complicated because it's not just straight out config classes.

    Let me throw another idea in here: Addressables. Have you looked at this package? I'm not sure if it's really applicable to your use case, but it might be one more tool in your toolbox.
     
  4. maximeb_unity

    maximeb_unity

    Unity Technologies

    Joined:
    Mar 20, 2018
    Posts:
    556
    I'm actually surprised about this. Did you try to embed a package that was already in your project? Embed only works on packages in the project, so if you were trying to add + embed a package from your scoped registry, you would need to add it first using https://docs.unity3d.com/ScriptReference/PackageManager.Client.Add.html.

    I'm also not quite sure what you mean with the last sentence. You should be able to call the PackageManager.Client APIs from code in any package; it shouldn't behave differently based on the origin of the calls. On the other hand, there are restrictions on the target packages; for instance, you cannot embed a package that's already embedded.
     
  5. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    Thanks @maximeb_unity for the response!
    Your suggestion is very close to what I ended up with. The config package provided an abstract class for a configurable package to extend and name its default ScriptableObject that would be generated. The only slight difference is I used SessionState strings to act as a singleton. The SessionState key was the ScriptableObject class name and the string value was set to the asset path after it searches for the type in the AssetDatabase on [InitializeOnLoadMethod]. Since, by default, rebuild is done often and searching the project for the ScriptableObject type seemed intensive, I also used SessionState to track if it's the first rebuild of the session. Only populating the "singleton" on the start of the session.

    Addressable's is a great idea. I didn't think to look into that. I definitely will when/if I revisit this! Addressable could be a great way to force load in a part of the package this is editable.

    Regarding `PackageManager.Client.Embed` I tried:
    1. To embed a package that was already in my project. The entire package folder was copied/embedded into the Packages folder. The package was not referenced in manifest.json. When this setup ran `Client.Embed` it produced an error message correctly, as the package was already embedded.
    2. Hooking manifest.json up to an internal Verdaccio npm registry. Then installing the package through the UPM UI. Running the `Client.Embed` code that was inside the package via a [UnityEditor.MenuItem("Edit/Remove UPM Readonly")]. This is the one that no longer returned an error (also correctly). However, it didn't seem to do anything else. The package was still read-only and didn't seem to be copied into the project.
    I didn't try adding the package through `Client.Add` in script.