- Home /
Using ScriptableObjects as Types/Enums
I'm having trouble setting up a simple Attribute system with ScriptableObjects. The design goals/components are:
An extensible system for managing RPG attribute types like "Health" or "Mana". Instead of using Enums or subclassing, I want to create types and edit their properties (display name, min & max values, progression table etc.) from the Unity Editor. This is the purpose of the
ScriptableObject
classAttributeType
.A very generic, serializable
Attribute
class that holds a specific value and is injected with a specificAttributeType
to determine the actual type of attribute. This way an instance ofAttribute
can be generic and delegate it's behavior to its concreteAttributeType
reference.A host of
ScriptableObject
classes that represent many different entities like characters, items, modifiers, etc. These need to be able to instantiate Attributes of specificAttributeTypes
like a health attribute or a mana attribute. These objects too, shall be Assets created and edited in the Unity Editor.
A simplified example with a Character
class as user of Attribute
:
[CreateAssetMenu(fileName = "New Attribute Type", menuName ="Data/Attribute Type")]
public class AttributeType : ScriptableObject
{
public string key = "undefined";
public string displayName = "Unnamed attribute";
public string displayFormat = "";
public string PrettyPrint(float value)
{
return this.displayName + ":\t" + value.ToString(this.displayFormat);
}
}
[Serializable]
public class Attribute
{
[SerializeField]
private AttributeType attributeType;
[SerializeField]
public float value;
public override string ToString()
{
if(this.attributeType == null) return this.value.ToString();
return this.attributeType.PrettyPrint(this.value);
}
public Attribute() : this(null) { }
public Attribute(AttributeType type)
{
this.attributeType = type;
}
}
[CreateAssetMenu(fileName ="New Character", menuName = "Data/Character")]
public class Character : ScriptableObject
{
public Attribute health;
public Attribute mana;
// wild speculation, does not work at all
private void Awake()
{
health = new Attribute(Resources.Load<AttributeType>("Assets/Data/Health"));
mana = new Attribute(Resources.Load<AttributeType>("Assets/Data/Mana"));
}
}
In the Assets folder there is one asset for Health and Mana each at Assets/Data/Health
and Assets/Data/Mana
respectively.
What causes me headaches is the initialization of a new Character
asset. Whenever a new Instance of Character
is created (either through the Editor's context menu or ScriptableObject<Character>.CreateInstance()
), the attributes should be injected with the appropriate references to the Health and Mana assets.
I'm having serious problems getting any reference to the ScriptableObject
assets through code. Neither Resources.Load()
, AssetDatabase.LoadAssetAtPath()
, not ScriptableObject.FindObjectsOfType()
returns anything (null or empty array).
Even if I did manage to fetch the reference to the AttributeType
assets, the question remains, where/when to perform the initialization in the Character
class. Constructors are a no-go, OnEnable()
runs every time the object is touched in the editor. The most sane place seems to be Awake()
, but again I'm uncertain regarding unintended side-effects.
Any advise on how to architect a working solution and best practice in this situation is highly welcomed.
Answer by nx_alpha · Aug 10, 2019 at 08:12 AM
After playing around a bit with Dankey_Kang & Blubberfisch's suggestions I found that their solution has one drawback: When you change the structure of your user's class (e.g. add another Attribute
to Character
), existing assets will break, because the initialization only runs at creation time in the editor.
The solution is to set all members to null in their declaration and perform the initialisation lazily in the OnEnable
method:
[CreateAssetMenu(fileName ="New Character", menuName = "Data/Character")]
public class Character : ScriptableObject
{
public Attribute health = null;
public Attribute mana = null;
private void OnEnable()
{
if (health == null) health = new Attribute(Resources.Load<AttributeType>("Data/Health"));
if (mana == null) mana = new Attribute(Resources.Load<AttributeType>("Data/Mana"));
}
}
This way, whenever you add a new member to your Character
class these new members will be initialized properly when the object is used by either the Editor or the game.
Answer by Dankey_Kang · Aug 08, 2019 at 07:08 PM
To initialize a ScriptableObject you need to create an initialization function and call it sometime after you create it using ScriptableObject.CreateInstance. That means you will have to create your own function to instantiate your ScriptableObject and save it which is annoying but here is a link that you may find useful: https://wiki.unity3d.com/index.php/CreateScriptableObjectAsset, I will sometimes use this kind of structure to create them using a context menu and call my initialization function right after I call CreateInstance. Not sure if I understood your problem in its entirety but I hope this helps.
Yes, that should work. In the static CreateAsset() function seen in the wiki code, you would add the stuff currently in your Awake method. Resources.Load() should be able to load your objects if they are located in a folder named "Resources", see https://docs.unity3d.com/ScriptReference/Resources.Load.html.
The following loads the attributes from folder Assets\Resources\Attributes:
[$$anonymous$$enuItem("Assets/Create/Data/Character")]
public static void CreateAsset()
{
Character c = ScriptableObjectUtility.CreateAsset<Character>();
c.health = new Attribute(Resources.Load<AttributeType>("Attributes/Health"));
c.mana = new Attribute(Resources.Load<AttributeType>("Attributes/$$anonymous$$ana"));
}
Thanks to both Dankey_$$anonymous$$ang & Blubberfisch! The static factory method was what I was trying to avoid in the hopes that Unity would have some other mechanism in place. Since both of you seem to agree that this is the way to go and not under$$anonymous$$ing any best practice, I can certainly live with it. Also thanks for the friendly re$$anonymous$$der regarding the assets folder structure. Once I put everything in the magic "Resources" folder, the references to the existing assets are resolved as intended.
Your answer
Follow this Question
Related Questions
Custom assets give Missing (Mono Script) 0 Answers
How to change an object's name inside an asset file? 0 Answers
Get Current Project Window Folder 2 Answers