Jump to content
RPG

Creating an InventoryItem Editor Part 4

We’ll be enabling it in code for the Editor, and to enable it for the tooltip, find the tooltip prefab, navigate to the TextMeshPro component for the description, go into more options and check the “Rich Text” box. This is so the UI can tell if the user is in a state to use the item. x = rect. 33f, rect.

readGameDev Team

This is the fourth post in my series on creating an InventoryItem Editor in Unity that will edit any child of InventoryItem.  If you're not caught up, you can read the first post here, the second post here, and the third post here.

Creating Actions

It’s time to start adding some ActionItems to our RPG. Most of the groundwork has been laid in the Inventory course, but there isn’t a very practical example of an action. We’re going to create a simple healing spell Action that doubles as a spell and a potion.

First, we’re going to make some small modifications to ActionItem.cs

We’ll start by getting rid of the [CreateAssetMenu()] item. There is no real point in creating an ActionItem out of the box, as it doesn’t do anything. The CreateAssetMenu() is so that we can test the basic “does this work” with the UI and nothing more.

We’re also going to add a virtual bool CanUse(GameObject user). This is so the UI can tell if the user is in a state to use the item. It’s an aweful shame to use an item only to have it do nothing because the conditions weren’t met.

For now, this is just

public virtual bool CanUse(GameObject user) { return true; }

Now you can add code in the ActionStore and ActionSlotUI to determine if the action is useable. While I’m not going to set that up for you, I’ll demonstrate how this works internally in this lesson.

By now, you’ve probably cleverly figured out that we need setters and a DrawCustomInspector() for the ActionItem data, which for now is just a consumable bool. In a future tutorial, I’ll be going through adding cooldown timers to the actions, which will be handled in the ActionItem.

#if UNITY_EDITOR void SetIsConsumable(bool value) { if (consumable == value) return; SetUndo(value?"Set Consumable":"Set Not Consumable"); consumable = value; Dirty(); } bool drawActionItem = true; public override void DrawCustomInspector() { base.DrawCustomInspector(); drawActionItem = EditorGUILayout.Foldout(drawActionItem, "Action Item Data"); if (!drawActionItem) return; EditorGUILayout.BeginVertical(contentStyle); SetIsConsumable(EditorGUILayout.Toggle("Is Consumable", consumable)); EditorGUILayout.EndVertical(); } #endif

That’s it for our ActionItem setup. Now it’s time to create our Healing Spell. I make my action scripts in a subfolder of Scripts called Actions. The actual Action Items I create, I put under Game/Actions/Resources
Create a folder Actions and a script HealingSpell.
Here’s what mine looks like:

using GameDevTV.Inventories; using RPG.Attributes; using UnityEditor; using UnityEngine; namespace RPG.Actions { [CreateAssetMenu(fileName="New Healing Spell", menuName = "RPG/Actions/HealingSpell")] public class HealingSpell : ActionItem { [SerializeField] float amountToHeal; [SerializeField] bool isPercentage; public override bool CanUse(GameObject user) { if (!user.TryGetComponent(out Health health)) return false; if (health.IsDead() || health.GetPercentage() >= 100.0f) return false; return true; } public override void Use(GameObject user) { if (!user.TryGetComponent(out Health health)) return; if (health.IsDead()) return; health.Heal(isPercentage ? health.GetMaxHealthPoints() * amountToHeal / 100.0f : amountToHeal); } #if UNITY_EDITOR void SetAmountToHeal(float value) { if (FloatEquals(amountToHeal, value)) return; SetUndo("Change Amount To Heal"); amountToHeal = value; Dirty(); } void SetIsPercentage(bool value) { if (isPercentage == value) return; SetUndo(value?"Set as Percentage Heal":"Set as Absolute Heal"); isPercentage = value; } bool drawHealingData = true; public override void DrawCustomInspector() { base.DrawCustomInspector(); drawHealingData = EditorGUILayout.Foldout(drawHealingData, "HealingSpell Data"); if (!drawHealingData) return; EditorGUILayout.BeginVertical(contentStyle); SetAmountToHeal(EditorGUILayout.IntSlider("Amount to Heal", (int)amountToHeal, 1, 100)); SetIsPercentage(EditorGUILayout.Toggle("Is Percentage", isPercentage)); EditorGUILayout.EndVertical(); } #endif } }

Our CanUse function tests to make sure that the user has a Health component, and that the User is not dead, or has full health. (Healing when Dead… leads to Zombies, we don’t want Zombies!).

The Use function checks again that the User has Health and is not Dead. It then calls Health.Heal() with a value of either the AmountToHeal in points, or a percentage of the user’s Max Health depending on if IsPercentage is checked.

With the editor code I’ve included in the script, it will draw properly in the CustomEditor

An example of a healing potionAn example of a healing spell.

The Healing Potion will heal the user for 10 points of damage. The spell which doesn’t dissappear on use, will heal for 10% of the user’s max health.

The Tooltip Preview

Believe it or not, we are almost done with our InventoryItem Editor Window. The next stop is adding the tooltip preview, which will entail dividing the window into two panes.
In the first section of Quests and Dialogues, we explore using BeginArea(rect) and EndArea to create moveable nodes on the screen. We’re actually going to use a variation of this, but just creating two fixed nodes. One pane will be 2/3rds the width of the inspector, the second pane will occupy the remaining third of the inspector.

There is a little housekeeping to take care of first. The idea behind this inspector is that we’re going to be simulating the tooltip that users will see in the game. One of the things I felt was important in a tooltip when I did my project is that if an item has bonuses, these should be reflected in the tooltip. This led me to make change to GetDescription() in InventoryItem. Rather than simply leaving it as public string GetDescription(), I changed it to a virtual method.

public virtual string GetDescription() { return description; }

Because there might be some time when a class needs the raw description without additions from other classes, I also included another getter to get the raw description.

public string GetRawDescription() { return description; }

We can now override GetDescription() to suit our needs in child classes.

We’ll start with the WeaponConfig. We want to let the user know if this is a ranged or melee weapon, what the weapon’s range is, what the base damage is, and what bonuses to damage it may have. We probably also want to include the snarky description (at least if you’re like me, item and class descriptions are snarky and meant to bring a chuckle to the player).

public override string GetDescription() { string result = projectile ? "Ranged Weapon" : "Melee Weapon"; result += $"\n\n{GetRawDescription()}\n"; result += $"\nRange {weaponRange} meters"; result += $"\nBase Damage {weaponDamage} points"; if ((int)percentageBonus != 0) { string bonus = percentageBonus > 0 ? "<color=#8888ff>bonus</color>" : "<color=#ff8888>penalty</color>"; result += $"\n{(int) percentageBonus} percent {bonus} to attack."; } return result; }

You’ll note that I’m using “\n” tags and color tags within the string. Both TextMeshPro, and the Editor labels support RichText if you enable it. We’ll be enabling it in code for the Editor, and to enable it for the tooltip, find the tooltip prefab, navigate to the TextMeshPro component for the description, go into more options and check the “Rich Text” box.

So what we’re doing with this method is starting by identifying the weapon type. If there is a projectile assigned, then by definition it is a ranged weapon.
It then adds in the raw description, and adds the weapon’s range, damage, and percentage bonus.
Now when a tooltip or the Editor asks for GetDescription() it gets a nicely formatted description with all the information.

Next up is the StatsEquipableItem:

string FormatAttribute(Modifier mod, bool percent) { if ((int)mod.value == 0.0f) return ""; string percentString = percent ? "percent" : "point"; string bonus = mod.value > 0.0f ? "<color=#8888ff>bonus</color>" : "<color=#ff8888>penalty</color>"; return $"{Mathf.Abs((int) mod.value)} {percentString} {bonus} to {mod.stat}\n"; } public override string GetDescription() { string result = GetRawDescription()+"\n"; foreach (Modifier mod in additiveModifiers) { result += FormatAttribute(mod, false); } foreach (Modifier mod in percentageModifiers) { result += FormatAttribute(mod, true); } return result; }

The first method is a helper… it takes a Modifier and returns a formatted string… an example would be a Modifier with a stat of Stat.Damage and a value of 10, in the percentageModifier list would read:

10 percent bonus to Damage

With this helper method, we can now simply cycle through each additiveModifier and percentageModifier and add the string to the result.

And finally, we have the HealthPotion… this one you’ll need to do custom for each class you have… so if you make an Ice spell, you’ll need a custom description modifier for it… Here’s mine for HealthPotion

public override string GetDescription() { string result = GetRawDescription()+"\n"; string spell = isConsumable() ? "potion" : "spell"; string percent = isPercentage ? "percent of your Max Health" : "Health Points."; result += $"This {spell} will restore {(int)amountToHeal} {percent}"; return result; }

Now that our Descriptions are out of the way, it’s time to get to the business of making Panes…
I’ve separated out the code to draw the Inspector into it’s own method, and created a method for drawing the tooltip. We’ll start with what our OnGui looks like, because this is how we’re going to create two panes…

GUIStyle previewStyle; GUIStyle descriptionStyle; GUIStyle headerStyle; void OnEnable() { previewStyle = new GUIStyle(); previewStyle.normal.background = EditorGUIUtility.Load("Assets/Asset Packs/Fantasy RPG UI Sample/UI/Parts/Background_06.png") as Texture2D; previewStyle.padding = new RectOffset(40, 40, 40, 40); previewStyle.border = new RectOffset(0, 0, 0, 0); } bool stylesInitialized = false; void OnGUI() { if (selected == null) { EditorGUILayout.HelpBox("No Item Selected", MessageType.Error); return; } if (!stylesInitialized) { descriptionStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, stretchHeight = true, fontSize = 14, alignment = TextAnchor.MiddleCenter }; headerStyle = new GUIStyle(descriptionStyle) { fontSize = 24 }; stylesInitialized = true; } Rect rect = new Rect(0, 0, position.width * .65f, position.height); DrawInspector(rect); rect.x = rect.width; rect.width /= 2.0f; DrawPreviewTooltip(rect); }

I’m actually throwing a few things in here… We need some styles to properly draw the Tooltip, and there are specific places where they should be initialized…
You can initialize the previewStyle (which defines the background of the rect) in OnEnable. That’s useful because you only want to load the background once, not every frame.
The other styles MUST be in the OnGUI thread (either in OnGUI or called by OnGUI because they are copying an existing style, and Unity gets upset if you put it in OnEnable.

DescriptionStyle defines the text. If you want to change the font, you can add it in the initialization block.
HeaderStyle copies the DescriptionStyle, but increases the font size, so the name is bigger in the tooltip.

I use the bool stylesInitialized to prevent the styles from being defined over and over again. This is very important if you load a font, as you don’t want to load a font each frame.

Next, we create a Rect based on the EditorWindow’s dimensions…
The EditorWindow has a property Rect position which holds the current location, width and height of the window. Our BeginArea, however, needs to start at 0,0, since the dimensions you give to BeginArea are relative to position, not relative to the editor…
We’ll set the rect’s width to position.width*.66 (2/3rds of the window) and the height to position.height to take the whole window.
This rect will be passed to DrawInspector.

Vector2 scrollPosition; void DrawInspector(Rect rect) { GUILayout.BeginArea(rect); scrollPosition = GUILayout.BeginScrollView(scrollPosition); selected.DrawCustomInspector(); GUILayout.EndScrollView(); GUILayout.EndArea(); }

DrawInspector creates a drawing area, and also a scrolling area in case the inspector overflows horizontally.
Then it’s back to the selected.DrawCustomInspector();

This leaves the moment I teased you with in the first post of this tutorial, the Preview Tooltip…

If you look again at OnGUI, we’re setting the x of rect to the width of the rect, then dividing the width by 2 before passing it to DrawPreviewTooltip… This is just a trick to make a rect of the other 1/3rd of the Editor Window. You can change the ratios if you want, just be careful not to accidentally overlap the panes.

void DrawPreviewTooltip(Rect rect) { GUILayout.BeginArea(rect, previewStyle); if (selected.GetIcon() != null) { float iconSize = Mathf.Min(rect.width * .33f, rect.height * .33f); Rect texRect = GUILayoutUtility.GetRect(iconSize, iconSize); GUI.DrawTexture(texRect, selected.GetIcon().texture, ScaleMode.ScaleToFit); } EditorGUILayout.LabelField(selected.GetDisplayName(), headerStyle); EditorGUILayout.LabelField(selected.GetDescription(), descriptionStyle); GUILayout.EndArea(); }

First, we begin a new area with our Rect…
Then, if there is an Icon, we draw the icon with it’s size based on the size of the pane.
GUILayoutUtility.GetRect(iconSize, iconSize) is a handy function. It instructs the layout to set aside the area requested, and returns a rect of the location that it has set aside…
We then use that rect as the coordinates for GUI.DrawTexture to draw our sprite.

After that, we just draw Labelfields for the displayName and description. Because we overrode the description, it will automatically have the correct information based on the class…

This pretty much concludes the tutorial on creating an InventoryItem EditorWindow.

Here are a couple of challenges if you want to extend your adventure in EditorWindow coding…

  • It’s fairly easy to make the regular inspectors use our DrawCustomInspectors(). Research regular Editors and create an Editor that will edit InventoryItems and it’s children (there is an optional boolean on the [directive] you’ll be putting on the Editor to edit the children as well). While the tutorials out there have you drawing each property with specialized property commands and creating positions, etc, all you really need to do is get an instance of the selected object, and call DrawCustomInspector(). I’ll leave this challenge unsolved in the repo for now.
  • Add a Rarity to the InventoryItem’s list of properties… Most games have rarities like Common, Uncommon, Rare, Epic, and Legendary… perhaps an Enum? Change the color of the title in the tooltip to match the rarity…

Check us out on social media