- Home /
Custom Inspector's Values Reset in Play Mode, But Only Sometimes
I've written my first custom inspector for my game's Stats classes.
I have a base Stats class, then subclasses EnemyStats and PlayerStats. I have a custom inspector for both subclasses and they're both pretty much identical. However, my PlayerStats values always reset when I enter play mode, but my EnemyStats values persist.
Why is this happening?
PlayerStatsEditor:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
/*
* Creates a custom inspector for PlayerStats.
* Allows stats to be updated and for their related stats to automatically update accordingly.
* Also grants the ability to restore either health or mana at run time with the click of a button.
* */
//TODO: Experience Bar and levelling -- Add a level up button.
//Show status effects, add a clear status effects button. -- Maybe status effects should be shown in a custom SpellManager inspector?
[CustomEditor(typeof(PlayerStats))]
public class PlayerStatsEditor : Editor {
PlayerStats playerStats;
public override void OnInspectorGUI() {
playerStats = (PlayerStats)target;
DrawHeading("Primary/Governing Stats");
DrawStatHeadings();
playerStats.BaseStrength = DrawStat(playerStats.BaseStrength, "STR", Stat.Strength);
playerStats.BaseEndurance = DrawStat(playerStats.BaseEndurance, "END", Stat.Endurance);
playerStats.BaseAgility = DrawStat(playerStats.BaseAgility, "AGI", Stat.Agility);
playerStats.BaseIntelligence = DrawStat(playerStats.BaseIntelligence, "INT", Stat.Intelligence);
playerStats.BaseLuck = DrawStat(playerStats.BaseLuck, "LCK", Stat.Luck);
playerStats.BaseCharisma = DrawStat(playerStats.BaseCharisma, "CHA", Stat.Charisma);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Secondary Stats");
DrawStatHeadings();
playerStats.BaseMaxHealth = DrawStat(playerStats.BaseMaxHealth, "MaxHP", Stat.MaxHealth);
playerStats.CurrentHealth = DrawFilledBar(playerStats.CurrentHealth, Stat.MaxHealth, Color.red);
if (GUILayout.Button("Restore Health", GUILayout.Width(Screen.width - 40f))) {
playerStats.CurrentHealth = playerStats.GetCalculatedStat(Stat.MaxHealth);
}
playerStats.BaseMaxMana = DrawStat(playerStats.BaseMaxMana, "MaxMP", Stat.MaxMana);
playerStats.CurrentMana = DrawFilledBar(playerStats.CurrentMana, Stat.MaxMana, Color.cyan);
if (GUILayout.Button("Restore Mana", GUILayout.Width(Screen.width - 40f))) {
playerStats.CurrentMana = playerStats.GetCalculatedStat(Stat.MaxMana);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
playerStats.BaseXPModifier = DrawStat(playerStats.BaseXPModifier, "XPMod", Stat.XPModifier);
playerStats.BaseDamage = DrawStat(playerStats.BaseDamage, "Damage", Stat.Damage);
playerStats.BaseDefense = DrawStat(playerStats.BaseDefense, "Defense", Stat.Defense);
playerStats.BaseToHit = DrawStat(playerStats.BaseToHit, "ToHit", Stat.ToHit);
playerStats.BaseDodge = DrawStat(playerStats.BaseDodge, "Dodge", Stat.Dodge);
playerStats.BaseSpeed = DrawStat(playerStats.BaseSpeed, "Speed", Stat.Speed);
playerStats.BaseCarryingCapacity = DrawStat(playerStats.BaseCarryingCapacity, "Weight Cap.", Stat.CarryingCapacity);
playerStats.BaseBarter = DrawStat(playerStats.BaseBarter, "Barter", Stat.Barter);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Tertiary/Magick");
DrawStatHeadings();
playerStats.BaseEarth = DrawStat(playerStats.BaseEarth, "Earth", Stat.Earth);
playerStats.BaseEarthDef = DrawStat(playerStats.BaseEarthDef, "Earth Def.", Stat.EarthDef);
playerStats.BaseFire = DrawStat(playerStats.BaseFire, "Fire", Stat.Fire);
playerStats.BaseFireDef = DrawStat(playerStats.BaseFireDef, "Fire Def.", Stat.FireDef);
playerStats.BaseWind = DrawStat(playerStats.BaseWind, "Wind", Stat.Wind);
playerStats.BaseWindDef = DrawStat(playerStats.BaseWindDef, "Wind Def.", Stat.WindDef);
playerStats.BaseWater = DrawStat(playerStats.BaseWater, "Water", Stat.Water);
playerStats.BaseWaterDef = DrawStat(playerStats.BaseWaterDef, "Water Def.", Stat.WaterDef);
playerStats.BaseLightning = DrawStat(playerStats.BaseLightning, "Lightning", Stat.Lightning);
playerStats.BaseLightningDef = DrawStat(playerStats.BaseLightningDef, "Lightning Def.", Stat.LightningDef);
playerStats.BaseHealing = DrawStat(playerStats.BaseHealing, "Healing", Stat.Healing);
playerStats.BaseDeath = DrawStat(playerStats.BaseDeath, "Death", Stat.Death);
playerStats.BaseDeathDef = DrawStat(playerStats.BaseDeathDef, "Death Def.", Stat.DeathDef);
playerStats.BaseBlood = DrawStat(playerStats.BaseBlood, "Blood", Stat.Blood);
playerStats.BaseBloodDef = DrawStat(playerStats.BaseBloodDef, "Blood Def.", Stat.BloodDef);
playerStats.BaseBattle = DrawStat(playerStats.BaseBattle, "Battle", Stat.Battle);
playerStats.BaseBattleDef = DrawStat(playerStats.BaseBattleDef, "Battle Def.", Stat.BattleDef);
playerStats.BaseAlteration = DrawStat(playerStats.BaseAlteration, "Alteration", Stat.Alteration);
playerStats.BaseAlterationDef = DrawStat(playerStats.BaseAlterationDef, "Alteration Def.", Stat.AlterationDef);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Tertiary/Weapons & Armor");
DrawStatHeadings();
playerStats.BaseLightArmor = DrawStat(playerStats.BaseLightArmor, "LightArmor", Stat.LightArmor);
playerStats.BaseHeavyArmor = DrawStat(playerStats.BaseHeavyArmor, "HeavyArmor", Stat.HeavyArmor);
playerStats.BaseRangedWeapons = DrawStat(playerStats.BaseRangedWeapons, "RangedWeapons", Stat.RangedWeapons);
playerStats.BaseLightWeapons = DrawStat(playerStats.BaseLightWeapons, "LightWeapons", Stat.LightWeapons);
playerStats.BaseHeavyWeapons = DrawStat(playerStats.BaseHeavyWeapons, "HeavyWeapons", Stat.HeavyWeapons);
playerStats.CalculateSecondaryStats();
serializedObject.Update();
serializedObject.ApplyModifiedProperties();
}
private float DrawFilledBar(float statValue, Stat stat, Color fillColor) {
float lineOffset = 18;
EditorGUI.DrawRect(new Rect(20, GUILayoutUtility.GetLastRect().position.y + lineOffset, Screen.width - 40, 16), Color.gray);
EditorGUI.DrawRect(new Rect(21, GUILayoutUtility.GetLastRect().position.y + lineOffset + 1, Screen.width * (statValue / playerStats.GetCalculatedStat(stat)) - (41 * (statValue / playerStats.GetCalculatedStat(stat))), 14), fillColor);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("", EditorStyles.boldLabel, GUILayout.Width(Screen.width * .4f - 30f));
statValue = EditorGUILayout.FloatField(statValue, GUILayout.Width(30));
EditorGUILayout.LabelField(" / " + playerStats.GetCalculatedStat(stat).ToString() + " (" + statValue / playerStats.GetCalculatedStat(stat) * 100 + "%)", EditorStyles.boldLabel, GUILayout.Width(Screen.width * .6f));
EditorGUILayout.EndHorizontal();
return statValue;
}
private static void DrawHeading(string heading) {
int originalFontSize = EditorStyles.boldLabel.fontSize;
EditorStyles.boldLabel.fontSize = 14;
EditorGUILayout.LabelField(heading, EditorStyles.boldLabel, GUILayout.Height(20f));
EditorStyles.boldLabel.fontSize = originalFontSize;
}
private static void DrawStatHeadings() {
float maxWidth = Screen.width / 6 - 20f;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Stat", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Base", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Buff", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Equipment", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Bonus", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Calculated", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.EndHorizontal();
}
private float DrawStat(float statValue, string label, Stat stat) {
float maxWidth = Screen.width / 6 - 20f;
string statString = stat.ToString();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(label, EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
statValue = EditorGUILayout.FloatField(statValue, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(playerStats.BuffStats[statString].ToString(), GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(playerStats.EquipmentStats[statString].ToString(), GUILayout.MaxWidth(maxWidth));
playerStats.BonusStats[statString] = EditorGUILayout.FloatField(playerStats.BonusStats[statString], GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(playerStats.GetCalculatedStat(stat).ToString(), GUILayout.MaxWidth(maxWidth));
EditorGUILayout.EndHorizontal();
return statValue;
}
}
EnemyStatsEditor:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
/*
* Creates a custom inspector for EnemyStats.
* Allows stats to be updated and for their related stats to automatically update accordingly.
* Also grants the ability to restore either health or mana at run time with the click of a button.
* */
//TODO: Experience Bar and levelling -- Add a level up button.
//Show status effects, add a clear status effects button. -- Maybe status effects should be shown in a custom SpellManager inspector?
[CustomEditor(typeof(EnemyStats))]
public class EnemyStatsEditor : Editor {
EnemyStats enemyStats;
public override void OnInspectorGUI() {
enemyStats = (EnemyStats)target;
DrawHeading("Primary/Governing Stats");
DrawStatHeadings();
enemyStats.BaseStrength = DrawStat(enemyStats.BaseStrength, "STR", Stat.Strength);
enemyStats.BaseEndurance = DrawStat(enemyStats.BaseEndurance, "END", Stat.Endurance);
enemyStats.BaseAgility = DrawStat(enemyStats.BaseAgility, "AGI", Stat.Agility);
enemyStats.BaseIntelligence = DrawStat(enemyStats.BaseIntelligence, "INT", Stat.Intelligence);
enemyStats.BaseLuck = DrawStat(enemyStats.BaseLuck, "LCK", Stat.Luck);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Secondary Stats");
DrawStatHeadings();
enemyStats.BaseMaxHealth = DrawStat(enemyStats.BaseMaxHealth, "MaxHP", Stat.MaxHealth);
enemyStats.CurrentHealth = DrawFilledBar(enemyStats.CurrentHealth, Stat.MaxHealth, Color.red);
if (GUILayout.Button("Restore Health", GUILayout.Width(Screen.width - 40f))) {
enemyStats.CurrentHealth = enemyStats.GetCalculatedStat(Stat.MaxHealth);
}
enemyStats.BaseMaxMana = DrawStat(enemyStats.BaseMaxMana, "MaxMP", Stat.MaxMana);
enemyStats.CurrentMana = DrawFilledBar(enemyStats.CurrentMana, Stat.MaxMana, Color.cyan);
if (GUILayout.Button("Restore Mana", GUILayout.Width(Screen.width - 40f))) {
enemyStats.CurrentMana = enemyStats.GetCalculatedStat(Stat.MaxMana);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
enemyStats.BaseDamage = DrawStat(enemyStats.BaseDamage, "Damage", Stat.Damage);
enemyStats.BaseDefense = DrawStat(enemyStats.BaseDefense, "Defense", Stat.Defense);
enemyStats.BaseToHit = DrawStat(enemyStats.BaseToHit, "ToHit", Stat.ToHit);
enemyStats.BaseDodge = DrawStat(enemyStats.BaseDodge, "Dodge", Stat.Dodge);
enemyStats.BaseSpeed = DrawStat(enemyStats.BaseSpeed, "Speed", Stat.Speed);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Tertiary/Magick");
DrawStatHeadings();
enemyStats.BaseEarth = DrawStat(enemyStats.BaseEarth, "Earth", Stat.Earth);
enemyStats.BaseEarthDef = DrawStat(enemyStats.BaseEarthDef, "Earth Def.", Stat.EarthDef);
enemyStats.BaseFire = DrawStat(enemyStats.BaseFire, "Fire", Stat.Fire);
enemyStats.BaseFireDef = DrawStat(enemyStats.BaseFireDef, "Fire Def.", Stat.FireDef);
enemyStats.BaseWind = DrawStat(enemyStats.BaseWind, "Wind", Stat.Wind);
enemyStats.BaseWindDef = DrawStat(enemyStats.BaseWindDef, "Wind Def.", Stat.WindDef);
enemyStats.BaseWater = DrawStat(enemyStats.BaseWater, "Water", Stat.Water);
enemyStats.BaseWaterDef = DrawStat(enemyStats.BaseWaterDef, "Water Def.", Stat.WaterDef);
enemyStats.BaseLightning = DrawStat(enemyStats.BaseLightning, "Lightning", Stat.Lightning);
enemyStats.BaseLightningDef = DrawStat(enemyStats.BaseLightningDef, "Lightning Def.", Stat.LightningDef);
enemyStats.BaseHealing = DrawStat(enemyStats.BaseHealing, "Healing", Stat.Healing);
enemyStats.BaseDeath = DrawStat(enemyStats.BaseDeath, "Death", Stat.Death);
enemyStats.BaseDeathDef = DrawStat(enemyStats.BaseDeathDef, "Death Def.", Stat.DeathDef);
enemyStats.BaseBlood = DrawStat(enemyStats.BaseBlood, "Blood", Stat.Blood);
enemyStats.BaseBloodDef = DrawStat(enemyStats.BaseBloodDef, "Blood Def.", Stat.BloodDef);
enemyStats.BaseBattle = DrawStat(enemyStats.BaseBattle, "Battle", Stat.Battle);
enemyStats.BaseBattleDef = DrawStat(enemyStats.BaseBattleDef, "Battle Def.", Stat.BattleDef);
enemyStats.BaseAlteration = DrawStat(enemyStats.BaseAlteration, "Alteration", Stat.Alteration);
enemyStats.BaseAlterationDef = DrawStat(enemyStats.BaseAlterationDef, "Alteration Def.", Stat.AlterationDef);
EditorGUILayout.Space();
EditorGUILayout.Space();
DrawHeading("Tertiary/Weapons & Armor");
DrawStatHeadings();
enemyStats.BaseLightArmor = DrawStat(enemyStats.BaseLightArmor, "LightArmor", Stat.LightArmor);
enemyStats.BaseHeavyArmor = DrawStat(enemyStats.BaseHeavyArmor, "HeavyArmor", Stat.HeavyArmor);
enemyStats.BaseRangedWeapons = DrawStat(enemyStats.BaseRangedWeapons, "RangedWeapons", Stat.RangedWeapons);
enemyStats.BaseLightWeapons = DrawStat(enemyStats.BaseLightWeapons, "LightWeapons", Stat.LightWeapons);
enemyStats.BaseHeavyWeapons = DrawStat(enemyStats.BaseHeavyWeapons, "HeavyWeapons", Stat.HeavyWeapons);
enemyStats.CalculateSecondaryStats();
serializedObject.ApplyModifiedProperties();
}
private float DrawFilledBar(float statValue, Stat stat, Color fillColor) {
float lineOffset = 18;
EditorGUI.DrawRect(new Rect(20, GUILayoutUtility.GetLastRect().position.y + lineOffset, Screen.width - 40, 16), Color.gray);
EditorGUI.DrawRect(new Rect(21, GUILayoutUtility.GetLastRect().position.y + lineOffset + 1, Screen.width * (statValue / enemyStats.GetCalculatedStat(stat)) - (41 * (statValue / enemyStats.GetCalculatedStat(stat))), 14), fillColor);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("", EditorStyles.boldLabel, GUILayout.Width(Screen.width * .4f - 30f));
statValue = EditorGUILayout.FloatField(statValue, GUILayout.Width(30));
EditorGUILayout.LabelField(" / " + enemyStats.GetCalculatedStat(stat).ToString() + " (" + statValue / enemyStats.GetCalculatedStat(stat) * 100 + "%)", EditorStyles.boldLabel, GUILayout.Width(Screen.width * .6f));
EditorGUILayout.EndHorizontal();
return statValue;
}
private static void DrawHeading(string heading) {
int originalFontSize = EditorStyles.boldLabel.fontSize;
EditorStyles.boldLabel.fontSize = 14;
EditorGUILayout.LabelField(heading, EditorStyles.boldLabel, GUILayout.Height(20f));
EditorStyles.boldLabel.fontSize = originalFontSize;
}
private static void DrawStatHeadings() {
float maxWidth = Screen.width / 6 - 20f;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Stat", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Base", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Buff", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Equipment", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Bonus", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField("Calculated", EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.EndHorizontal();
}
private float DrawStat(float statValue, string label, Stat stat) {
float maxWidth = Screen.width / 6 - 20f;
string statString = stat.ToString();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(label, EditorStyles.boldLabel, GUILayout.MaxWidth(maxWidth));
statValue = EditorGUILayout.FloatField(statValue, GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(enemyStats.BuffStats[statString].ToString(), GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(enemyStats.EquipmentStats[statString].ToString(), GUILayout.MaxWidth(maxWidth));
enemyStats.BonusStats[statString] = EditorGUILayout.FloatField(enemyStats.BonusStats[statString], GUILayout.MaxWidth(maxWidth));
EditorGUILayout.LabelField(enemyStats.GetCalculatedStat(stat).ToString(), GUILayout.MaxWidth(maxWidth));
EditorGUILayout.EndHorizontal();
return statValue;
}
}
Answer by Bunny83 · Nov 14, 2017 at 04:40 PM
You have several problems in your code. First and formost you should not mix SerializedObject / SerializedProperties with the "target" variable. Those a two very different approaches to the same thing.
Second you don't seem to understand how SerializedObject should be used or how it works. Also you don't properly "communicate" your changes to Unity's serialization system.
As i said there are two ways to edit the serialized data of an object.
SerializedObject / SerializedProperty
The SerializedObject is the preferred way of manipulating the serialized data. It supports multi object editing out of the box and does handle undo registration automatically.
The SerializedObject is actually a "copy" of the serialized data. Note that the serialized data is completely abstracted from the actual classes and instances. It's just the plain data. When you call Update on the SerializedObject it will pull the current data from disk into the SerializedObject / SerializedProperties. Now you could change / edit / modify the various SerializedProperties. This is done be reading / writing one of the following values:
animationCurveValue
arraySize
boolValue
boundsIntValue
boundsValue
colorValue
doubleValue
enumValueIndex
exposedReferenceValue
floatValue
intValue
longValue
objectReferenceValue
quaternionValue
rectIntValue
rectValue
stringValue
vector2IntValue
vector2Value
vector3IntValue
vector3Value
vector4Value
You can either bring up your own UI for each property by using FindProperty or by iterating through all properties and use a PropertyField. A SerializedProperty is actually an "iterator object". That means you can simply call Next on it and the serializedProperty will represent the next serialized value as they would appear in the normal inspector. If you roll your own UI you may want to check the propertyType. Keep in mind that you have to use the "correct" value from the above mentioned list. So you can't read / write "stringValue" from a SerializedProperty that actually represents a Vector3 value. PropertyField handles all those types automatically. Also PropertyField will use any PropertyDrawers that might be declared for this field / fieldtype.
After you modified any of those "properties" you have to call "ApplyModifiedProperties" on the SerializedObject to actually apply all the changes to the underlying serialized data on disk.
target / targets
Another way to edit the serialized data is to directly access the target class instance / instances. Unlike the SerializedObject here you have to handle multi object editing yourself. So when more than one object is selected you would need to use targets instead. Note that multi object editing is only possible when your Editor has the CanEditMultipleObjects attribute.
Using target is actually the legacy method of editing objects but it can still be used. However you have to manually tell Unity that you edit this object. Since we now talk about "normal" class instances Unity doesn't know when you modified something. To do this you would need to use the Undo class. You usually have to call Undo.RecordObject before you do any changes to the object. This method registers the given object to be checked / serialized after the editor has finished the current execution.
Note that when you mix SerializedObject and target access you can get undefined results. When you call ApplyModifiedProperties you most likely erase all changes you've done manually
Keep in mind that Unity can only "truly" serialize classes which are derived from UnityEngine.Object. Custom "Serialized" classes are not serialized on their own but are simple "sub data" of the UnityEngine.Object derived type that references this class.
I highly recommend to read the Script Serialization documentation carefully.
Ps: In many cases implementing a PropertyDrawer is usually easier when you only want to change how certain serialized values are displayed. In addition you can use DecoratorDrawers to split the different values into sections or add a header text. Unity has some built-in decorators (Tooltip, Header, Space, ...) and even PropertyDrawers (Range, TextArea, Delayed, ...) for some special cases.
I was hoping for a quick and easy answer, but alas, it looks like I have some learning to do. Thanks for the detailed response!
Answer by Tourist · Nov 14, 2017 at 04:11 PM
You call for serializedObject.Update just before applying modified properties.
serializedObject.Update() must be at the beginning of your OnInspectorGUI because it may reset modified values.