Option 1: ScriptableObject
The simplest way to do this is if Behaviour
and Entity
inherit from ScriptableObject
; then you can do something like this:
public abstract class Behaviour : ScriptableObject {
// Stuff common to all behaviours goes here.
}
[CreateAssetMenu(fileName = "someBehaviour.Asset", menuName = "Behaviours/Some Behaviour")]
public class SomeBehaviour : Behaviour {
// Customized behaviour stuff goes here.
}
[CreateAssetMenu(fileName = "newEntity.Asset", menuName = "Entity")]
public class Entity : ScriptableObject {
// No need for a "name" - it inherits that already from ScriptableObject.
public List<Behaviour> behaviours;
}
This adds options to create entities and behaviours into the Asset > Create menu in the Unity editor, and the Create menu you get when right-clicking in the Project tab.
Your designers can then create a .asset
file for each version of a behaviour, and each entity, using the existing Inspector, before you write any custom editor code. Because this is using native Unity serialization, references between the assets are visible to Unity, so it can keep them updated, cull unused assets from builds, and pack the data efficiently using binary serialization. You get handy features like multi-object editing basically "for free", and when writing your own editor code you can use the infrastructure provided by Unity's SerializedObject
/ SerializedProperty
to process changes field-by-field, support undo, etc.
ScriptableObjects are also eligible to receive certain message calls automatically, like Awake
, OnEnable
, OnValidate
, OnDisable
, OnDestroy
, which can help when you need to do some dynamic initialization or cleanup before/after using them.
To instantiate a copy of one of these entities at runtime, without altering the source asset file, you can just say Instantiate(myEntity)
like you would with a prefab. I haven't tested whether this performs a deep copy (instantiating copies of each behaviour in the list) or a shallow copy (instantiating a copy of the list whose entries all still point to the original behaviours), so you might have to implement your own deep copy method if the behaviours contain mutable data.
By default, you'd have to create each customized behaviour instance as its own asset file, then link references to those assets in the entity's list. So if I have variants on a "flee" behaviour that each use a different speed, I'd need to create FleeSlow.asset
, FleeMedium.asset
, FleeFast.asset
, even if they're all just instances of the same FleeBehaviour
script with different parameter values.
That's useful if you have shared behaviours that should use the same values for every entity that uses them - saving file size and memory by de-duplicating the data, and avoiding the need to manually keep many separate instances in sync - but cumbersome if every entity is using bespoke behaviour instances. The Entity inspector will also just show a list of references to Behaviours, not nested inspectors so you can tune their parameters all in one place.
Both these frustrations can be improved by writing a custom inspector or custom editor window that acts on this data. The big win is that you don't have to finish that editor window before you begin working with this solution, since the default inspector handles a basic workflow out of the box.
The downside of course is that this is no longer a plain old C# class, but carries some overhead due to inheriting ScriptableObject
. If getting this type as lean as possible is important to you, you can instead handle your own polymorphic serialization via XML.
Option 2: XML Serialization
The built-in .Net XML Serialization library lets you use attributes to customize how a class should be represented in XML form, including options to serialize lists of polymorphic types.
using System.Collections.Generic;
using System.Xml.Serialization;
public class Entity {
// This makes the "name field show up as an attribute inside the <Entity> tag
[XmlAttribute]
public string name;
// This parses tags for any of the specified class names as items in this list.
[XmlElement(typeof(SomeBehaviour)), XmlElement(typeof(SomeOtherBehaviour)) /*...*/]
public List<Behaviour> behaviours;
}
This will allow you to read/write entities from XML that looks like this:
<?xml version="1.0"?>
<Entity xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="My Awesome Entity">
<SomeBehaviour someAttribute="5">
<direction>
<x>1</x>
<y>0</y>
</direction>
</SomeBehaviour>
</Entity>
You can store this text in a .xml
file, and read/write it with the XmlSerializer class.
The downside here is that you're responsible for doing all the heavy lifting yourself. Any deserialize-edit-serialize-save operations need to be handled in your custom editor window code. The assets on disc will tend to be larger since XML is such a verbose text format, and reading/writing them is likely to be slower. As far as Unity is concerned, this is just a text file, so if any references in your project that this entity depends on change, it won't know how to update the text to match unless you expressly handle that change yourself.
On the plus side though, you're now completely independent of Unity serialization, so you could stream new entity definitions from a server at runtime, or allow modders to create and distribute their own entities, without needing the Unity editor installed. I've used this in the past to support games with user generated content.