Making an RPG Maker VX esque database using ScriptableObject, with custom editor

Hello!

I’ve been prototyping an RPG in RPG Maker VX Ace for a few months, but I feel I need a more versatile engine, so I’m trying my hand at learning Unity. I was looking into ways to create a database containing stuff (characters, jobs, items etc) akin to the database in RMVX. Someone told me to try using ScriptableObject and then write a custom editor for it. My Database class looks like this:

[System.Serializable]
public class Database : ScriptableObject {
	public List<Character> characters;
	public List<Job> jobs;
	public List<Skill> skills;
}

Character class looks like this, and the other database items look similar:

[System.Serializable]
public class Character {
	public string name = "Character";
	public int experience = 0;
	public Job job;
}

My first question is: can I even have a reference to a Job object inside a Character object? What I want is basically to reference a Job stored in the list of jobs in the database. If this doesn’t work, is using an ID, or maybe even just the index in the list, the best option?

My next question is about going about creating a custom editor and property drawers for my different database items. This is my custom database editor:

[CanEditMultipleObjects]
[CustomEditor(typeof(Database))]
public class DatabaseEditor : Editor {

	public override void OnInspectorGUI() {
		serializedObject.Update();
		DrawGroup("characters");
		DrawGroup("jobs");
		DrawGroup("skills");
		serializedObject.ApplyModifiedProperties();
	}

    // Draw the full list of items of a category in the database
	private void DrawGroup(string groupName) {
		SerializedProperty list = serializedObject.FindProperty(groupName);
		EditorGUILayout.PropertyField(list);

		if (list.isExpanded) {
			// Draw array size field
			EditorGUILayout.PropertyField(list.FindPropertyRelative("Array.size"));

			EditorGUI.indentLevel++;

			// Draw all the list elements
			for (int i = 0; i < list.arraySize; i++) {
				SerializedProperty element = list.GetArrayElementAtIndex(i);
				EditorGUILayout.PropertyField(element);
			}

			EditorGUI.indentLevel--;
		}
	}
}

I have also written a quick property drawer for Character objects:

[CustomPropertyDrawer(typeof(Character))]
public class CharacterDrawer : PropertyDrawer {
	public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
		property.isExpanded = EditorGUILayout.Foldout(property.isExpanded, label);

		if (property.isExpanded) {
			EditorGUILayout.PropertyField(property.FindPropertyRelative("name"));
			EditorGUILayout.PropertyField(property.FindPropertyRelative("experience"));

			// Some way to reference a Job object in the database?
		}
	}
}

OK, so first off, how do I reference a Job object? The Job objects are stored in the jobs field in the Database. Is there any way a Character object can get a straight reference to a Job object? What type of EditorGUI field do I use for that? I’m also getting line-high gaps in between every element in the list, why is that? Picture:

alt text

Thanks in advance for any help!

@Mmmpies Thanks a bunch for the link! I ended up doing it like they did, having a separate ScriptableObject asset for each item in the database. It’s a bit messier, but it’s way easier to reference other database items from one item. Cheers!

Edit: I also managed to make proper custom property drawers, so now everything looks nice and clean in the inspector.

For future reference, if anyone stumbles across this having the same problems as I did, here are a few of my classes:

[System.Serializable]
public class Job : ScriptableObject {
	public new string name = "Job";
	public Sprite icon = null;
	public List<SkillIntPair> learnableSkills = new List<SkillIntPair>();
	public List<WeaponTypeIntPair> weaponMastery = new List<WeaponTypeIntPair>();
	public List<EquipmentTypeIntPair> equipmentMastery = new List<EquipmentTypeIntPair>();
	public Range strGrowth = new Range();
	public Range vitGrowth = new Range();
	public Range intGrowth = new Range();
	public Range wisGrowth = new Range();
	public Range dexGrowth = new Range();
	public Range agiGrowth = new Range();
	public List<Attribute> attributes = new List<Attribute>();
}

You need to make Lists of custom pair classes, because Unity doesn’t serialize generic types (e.g. KeyValuePair doesn’t work). Here is my SkillIntPair class and its custom property drawer:

[System.Serializable]
public class SkillIntPair {
	public Skill skill = null;
	public int level = 0;
}

[CustomPropertyDrawer(typeof(SkillIntPair))]
public class SkillIntPairPropertyDrawer : PropertyDrawer {
	public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
		EditorGUI.BeginProperty(position, label, property);
		SerializedProperty key = property.FindPropertyRelative("skill");
		SerializedProperty value = property.FindPropertyRelative("level");

		// Get the name of the skill, and use that as the label for the property
		// (otherwise the label is simply "Element n" when stored in lists)
		Skill skillObject = (Skill)key.objectReferenceValue;
		string header = label.text;

		if (skillObject != null)
			header = skillObject.name;

		// Disable indention for this property
		position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), new GUIContent(header));
		int indent = EditorGUI.indentLevel;
		EditorGUI.indentLevel = 0;

		// Decide where the property fields are to be drawn
		Rect keyRect = new Rect(position.x, position.y, position.width / 2, position.height);
		Rect valueRect = new Rect(position.x + position.width / 2, position.y, position.width / 2, position.height);

		// GUIContent.none removes the separate label from the field
		// If you want a label you have to use custom EditorGUI.LabelField I think,
		// because the inherent labels of these PropertyFields are buggy for some reason
		EditorGUI.PropertyField(keyRect, key, GUIContent.none);
		EditorGUI.PropertyField(valueRect, value, GUIContent.none);

		EditorGUI.indentLevel = indent;

		EditorGUI.EndProperty();
	}
	
	public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
	{
		return 18;
	}
}