I'm building a card game and I'd like to have a clean architecture for card abilities. I've got a CardData ScriptableObject with properties for a card. I want card abilities to compose together to describe what a card does, like a card called DrawAndHealCard that draws 2 cards and heals 5 health when played.
I realized right away that this means I'll need a concrete asset for each variation of a CardAbility. So DrawAndHealCard has a reference to two assets: DrawCards2 and HealPlayer5. That's ridiculous, I want all the data to feel like it's on a single DrawAndHealCard.
So I learned about AssetDatabase.AddObjectToAsset()
, this seems like the right idea, I can have the abilities as sub-assets of a CardData asset and not have deal with the organization of all these separate assets. So now I'm trying to build an Editor
to manage this and it is painful.
I've read so much stuff about Unity serialization, SOs, Editor scripts, ... seriously hitting a wall with this and about to downgrade to something that feels less elegant architecturally. If there's a better way to do this, I'm open to suggestions about totally different routes too.
Code below is stripped down, but it's the gist of what I'm trying to figure out.
Where I'm at right now is onAddCallback
seems to be adding a sub-asset correctly, but onRemoveCallback
does not remove it. My Clear All Abilities button does work however. I can't find any good docs or guides about this stuff, so I'm pretty lost currently.
// CardData.cs
[CreateAssetMenu(fileName = "CardData", menuName = "Card Game/CardData", order = 1)]
public class CardData : ScriptableObject
{
public Sprite image;
public string description;
public CardAbility[] onPlayed;
}
// CardAbility.cs
public class CardAbility : ScriptableObject
{
public abstract void Resolve();
}
// DrawCards.cs
public class DrawCards : CardAbility
{
public int numCards = 1;
public override void Resolve()
{
Deck.instance.DrawCards(numCards);
}
}
// HealPlayer.cs
public class HealPlayer : CardAbility
{
public int healAmt = 10;
public override void Resolve()
{
Player.instance.Heal(healAmt);
}
}
// CardDataEditor.cs
[CustomEditor(typeof(CardData))]
public class CardDataEditor : Editor
{
private ReorderableList abilityList;
public void OnEnable()
{
abilityList = new ReorderableList(
serializedObject,
serializedObject.FindProperty("onPlayed"),
draggable: true,
displayHeader: true,
displayAddButton: true,
displayRemoveButton: true);
abilityList.onRemoveCallback = (ReorderableList l) => {
l.serializedProperty.serializedObject.Update();
var obj = l.serializedProperty.GetArrayElementAtIndex(l.index).objectReferenceValue;
DestroyImmediate(obj, true);
AssetDatabase.SaveAssets();
l.serializedProperty.DeleteArrayElementAtIndex(l.index);
l.serializedProperty.serializedObject.ApplyModifiedProperties();
};
abilityList.onAddCallback = (ReorderableList l) => {
var index = l.serializedProperty.arraySize;
l.serializedProperty.arraySize++;
l.index = index;
var element = l.serializedProperty.GetArrayElementAtIndex(index);
// Hard coding a specific ability for now
var cardData = (CardData)target;
var newAbility = ScriptableObject.CreateInstance<DrawCards>();
newAbility.name = "test";
newAbility.numCards = 22;
element.objectReferenceValue = newAbility;
AssetDatabase.AddObjectToAsset(newAbility, cardData);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
serializedObject.ApplyModifiedProperties();
};
// Will use this to provide a menu of abilities to choose from.
/*
abilityList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
var menu = new GenericMenu();
var guids = AssetDatabase.FindAssets("", new[]{"Assets/CardAbility"});
foreach (var guid in guids) {
var path = AssetDatabase.GUIDToAssetPath(guid);
menu.AddItem(new GUIContent("Mobs/" + Path.GetFileNameWithoutExtension(path)), false, clickHandler, new WaveCreationParams() {Type = MobWave.WaveType.Mobs, Path = path});
}
menu.ShowAsContext();
};
*/
// Will use this to render CardAbility properties
/*
abilityList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
};
*/
}
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
abilityList.DoLayoutList();
// XXX: Ultimately don't expect to use these, experimenting with
// other ways of adding/deleting.
if (GUILayout.Button("Add Ability")) {
var cardData = (CardData)target;
var newAbility = ScriptableObject.CreateInstance<CardAbility>();
AssetDatabase.AddObjectToAsset(newAbility, cardData);
AssetDatabase.SaveAssets();
}
if (GUILayout.Button("Clear All Abilities")) {
var path = AssetDatabase.GetAssetPath(target);
Object[] assets = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
for (int i = 0; i < assets.Length; i++) {
if (assets[i] is CardAbility) {
Object.DestroyImmediate(assets[i], true);
}
}
AssetDatabase.SaveAssets();
}
serializedObject.ApplyModifiedProperties();
}
}