Table of Contents
- Introduction
- Video
- Let's Examine
- Character Details Section
- Inventories Section
- UI Creation
- Stat Icon Scene
- Attribute Item Scene
- Character Equipment
- Inventory Grid
- Data Driven Items
- Itemization Overview
- Item Class
- Gear Class
- Affix Class
- Armor Class
- Weapon Class
- Jewelry Class
- Socketable and Gem Classes
- Data Driven UI
- UI Item Icon
- UI Tooltip
- Equipping Gems
- Recap
Introduction
As Game Developers we may be inspired to create based on something we see that excites us, or provides us with certain emotions. These inspirations may come from other games that we've played, but they may also come from other mediums that we enjoy. If we spend some time and study that medium, we enable ourselves to create something that is similar in nature or that shares in creativity.
In this Feature Study, we'll be taking a look at Diablo IV's Character Panel and Inventory including aspects of it's Itemization. Our goal is to better understand how the latest installation of Diablo in the franchise accomplishes it's implementation of Items, Inventories, and the way that information is presented to a player via the UI components. We'll recreate these elements in Godot 4 to demonstrate how we can get the creative likeness by studying how Diablo IV is structured.
This began as purely a UI study, but the more time I spent with the code and was building things out, the more I was inspired to replicate some of Diablo's Itemization. I really love what I ended up with, even though it's not fully functional and isn't something I would toss directly into a game.
I ended up in quite the deep dive in the way Diablo's systems work and really wanted to get the same feel with the items in this example. Not only did that deep dive teach me a ton about Diablo, I learned a few things wtihin Godot that will be helpful for me moving forward. Hopefully as I review what I've done, you learn a few things new yourself!
This is snapshot of some of my reference images I used and compiled within PureRef .
Video
As always, feel free to follow along with the Video or read at your own pace here!
Let's Examine
Diablo IV takes an approach at simplifying the Character screen by combining the Character's stats, equipment, and current Inventory into a single UI panel.
This Character Panel can quickly be opened to provide information about our Characters Stats, which Gear is equipped on our Character and what new Loot we've picked up.
We'll begin by taking the Character Panel and dividing it into smaller pieces that better outline the interface's building blocks:
Character Details Section
Our Character Details section contains a small panel for stats one one side, and on the other we have our equipment section.
The Stat Panel has an overview of our Character and allows us to get a quick glance at some important information:
Our Character's Level, Name and Title:
Button's for accessing our Profile and additional Materials and Detailed Stats:
Our Character's Primary Stats:
Our Character's Attributes:
On the right side, we have the Equipment by slot for our Character, along with a preview of what our Character looks like.
We have the Equipment slots for Armor and Jewelry:
Lastly we have our Weapon slots:
Inventories Section
The UI's Inventory section contains a tab for each category of Inventories, the Inventory Grid, and our various Currencies:
The Tabs allow us to switch between a specific Inventory Category (Equipment, Consumables, Quests, and Aspects)
The Inventory Grid for the current tab has a set number of slots based on the maximum amount of items our Inventory can store. Our Equipment inventory has 33 slots of storage that we can store on our character at a time. To give us more room, we must go to our Character's Stash to unload from any one of these inventories.
Then breaking down our Currencies, we have a section for our Gold, Red Dust, and our Obols:
UI Creation
Now that we have studied the different components of Diablo's UI, we have a direction we can attempt to recreate it within Godot.
We start with our UI scene and layout some Control nodes to break up our UI into the proper sections. The way we are dividing we can easily use the MarginContainer
for padding and the VBoxContainer
to distribute our sections between the top Character Details section and our Inventory section. We'll wrap each of those in their own parent control nodes as well and distribute them as we need to using the layout > container sizing > stretch ratio. We'll use 3 and 2 respectively, which means our Character Details will take up 3/5ths of the container and the Inventory will use the remaining 2/5ths of the container.
Note that we also have a ColorRect
and TextureRect
that we are using to define the background color and overlaying a texture for some detail. This is the technique I use a few times over to get a grainy feel on the background, without needing to create it external.
To do this I set the ColorRect
to the color I want to use, and then for the TextureRect
I use a NoiseTexture2D
and let that set the scale for the type of texture I'm going for. I use FastNoiseLite with a type of Perlin and tinker with the width and height to get the style I want.
Next we have our Character Details nodes; a MarginContainer
that contains a HBoxContainer
for the Stats and Character Equipment sections. We also have a Control
container for our Level display which in itself contains the same ColorRect
and TextureRect
configuration described previously. We again distribute the Stats and Character Equipment sections using the Stretch Ratio 1/3 and 2/3 respectively.
For our Stats section, we have a fair bit going on. Our Stats Control is a type of VBoxContainer
to distribute our Nameplate, Materials and Extra Stats, Primary Stats, and Attributes.
Each Node use a layout of MarginContainer
s and VBoxContainer
for alignments, but our Primary Stats and Attributes are both using some branched scenes that we've composed to make reusable.
Stat Icon Scene
Our first custom scene is for our Stat's which include an icon, under our Primary Stats section. This contains an icon to highlight it's stat type, and contains a stat name and value. We will use our scene to export these values and create a reusable scene to simplify our Stats node tree.
The Node Tree is small and the script is short with only a few behaviors.
# stat_icon_item.gd
@tool
extends HBoxContainer
@onready var stat_label: Label = %StatLabel
@onready var value_label: Label = %ValueLabel
@onready var icon_texture_rect: TextureRect = %IconTextureRect
@export var stat_name: String = "Stat":
set(value):
stat_name = value
_update_labels()
@export var stat_value: int = 10:
set(value):
stat_value = value
_update_labels()
@export var stat_icon: Texture:
set(value):
stat_icon = value
_update_labels()
func _ready():
_update_labels()
func _update_labels():
if stat_label:
stat_label.text = stat_name
if value_label:
value_label.text = str(stat_value)
if icon_texture_rect:
icon_texture_rect.texture = stat_icon
Our StatIconItem has bindings to the labels for the Stat's Name and Value as well as the Icon TextureRect
that we will modify. We'll export the stat_name
, stat_value
, and stat_icon
variables and have setter methods that allow us to _update_labels
and see them immediately in our editor thanks to the @tool
definition at the top of our script defining this as a tool script that will run in editor. Our _update_labels
function simply updates the correct nodes' values.
Attribute Item Scene
Our next custom scene is for the Attribute items. This is a Name string and Value that we export and handle with a tool script:
# attribute_item.gd
@tool
extends MarginContainer
@onready var attribute_label: Label = %AttributeLabel
@onready var value_label: Label = %ValueLabel
@export var attribute_name: String = "Attribute":
set(value):
attribute_name = value
_update_labels()
@export var stat_value: int = 10:
set(value):
stat_value = value
_update_labels()
func _ready():
_update_labels()
func _update_labels():
if attribute_label:
attribute_label.text = attribute_name
if value_label:
value_label.text = str(stat_value)
Character Equipment
Now let's look at our Character Equipment section. We have a ColorRect
, then we contain the rest within a MarginContainer
. Inside here we have our Paper Doll for our Character, the background gradient shadow and the VboxContainer
for our Equipment: Armor, Jewelry, and Weapons. These Slots are for our custom UIItemIcon which we will cover more in our UI Item Icon section within our Inventory.
Again note the layout is making use of MarginContainer
for padding and VBoxContainer
for list like alignment.
Put this all together and so far it looks like this:
Inventory Grid
Our Inventory Section has a Node Tree containing the BG setup and a VBoxContainer
for our Inventory Tabs, Inventory Grid, and our Currencies. The Inventory stuff is contained within our MarginContainer
and another VboxContainer
thus spacing and stacking our Tabs above our InventoryGridContainer Control
node. This Grid has a ColorRect
bg with our Slot placeholders and the InventoryGrid using the GridContainer
node and a custom script for generating the Inventory and it's Items.
With our Node Tree setup with all our Control
nodes, we should have an inventory that looks like this:
Our InventoryGrid will populate our Inventory instance and create the UI elements for each Item.
Let's look at the script from a high level perspective, then take a step back and review what we are going to implement for our Items and the Item's data structure.
# inventory_grid.gd
extends GridContainer
var ui_item: PackedScene = preload("res://ui/inventory/ui_item_icon.tscn")
var prefixes: Array = [...]
var suffixes: Array = [...]
var inventory: Inventory
func _ready() -> void:
...
func _on_item_added(item: Item, _qty, _total) -> void:
...
func _on_item_removed(item: Item, _qty, _total) -> void:
...
func generate_item(category: Item.Category) -> Item:
...
func generate_name(item: Item) -> String:
...
Our Grid holds a reference to an Inventory class, a PackedScene
of our UIItemIcon, and our functions deal with an Item class.
Data Driven Items
This is the point in which this project was no longer only a UI study of Diablo IV and recreating that UI layout within Godot 4.
I wanted to populate items both in the Character's Equipment slots, as well as within my fancy Inventory Grid. At first I started with the idea of mocking up a template of a single item or maybe a few icons of armor and weapons and then call it a day.
I couldn't settle there, I had to have the full dopamine hit of random item Rarities, random rolled Stats, Sockets, Gems, and Sacred and Ancestral tiers!
Not only did I want the Inventory icons to be randomized, I wanted to be able to hover each item icon and get the tooltip showing all my random stats, item name, the fancy colored borders and backgrounds to let me know how sweet my loot is, and all the extra details we may want to pack into our item descriptions.
I wanted to see if I could not only mimic the UI, but also some of that fancy data structure that Diablo IV's itemization uses.
Itemization Overview
Let's begin by examining Items in Diablo IV.
If we look at the different Categories of Items, we have Gear (Armor, Weapons, and Jewelry) and Socketable Items such as a Gem.
All Items at their core are going to have some common attributes:
- Name
- Rarity | Tier | Category
- Image Icon
- Description
- Level Requirements
- Tradable?
- Class Restrictions
- Account Bound?
- Salvageable?
- Value
More information is required for our Jewelry/Armor/Weapon/Socketable.
Here we can define Gear to extend from an Item containing additional details such as:
- Item Power
- Upgrades (Current/Max)
- Affix (Count/Rolled)
- Socket (Count/Rolled)
- Durability
Even further we can extend Gear into Armor with it's own details such as the Type of Armor:
- Chest
- Helm
- Pants
- Footwear
- Gloves
- Shield
For the Weapons, we'll also extend Gear and add it's Types:
- Axe
- Axe2H
- Bow
- Crossbow
- Dagger
- Offhand
- Mace
- Mace2H
- Staff
- Sword
- Sword2H
- Scythe
- Scythe2H
- Wand
- Polearm
Jewelry is either a Ring or Amulet Type, but we'll still be extending Gear.
Another category of Item is Socketables and from that we can extend it into a Gem.
Gem has a Type and Quality Types:
- Diamond
- Saphire
- Ruby
- Emerald
- Amethyst
- Topaz
Qualities:
- Crude
- Chipped
- Normal
- Flawless
- Royal
We've got ourselves the elements for what we need to create some new Custom Resources!
Item Class
We will start with our Base class for Item -> item.gd
:
# item.gd
class_name Item extends Resource
@export var name: String = "Item Name"
@export var description: String
@export var image: Texture
@export var quantity: int = 1
@export var stackable: bool = false
@export var value: int = 1
@export var rarity: Rarities = Rarities.Common
@export var tier: Tiers = Tiers.Normal
@export var level_requirement: int = 1
@export var is_account_bound: bool = false
@export var salvageable: bool = true
@export var tradable: bool = true
@export_flags(
"Barbarian",
"Druid",
"Necromancer",
"Rogue",
"Sorcerer",) var class_restrictions = 0
@export var inventory: Inventory
var class_strings = "Classes: ":
get:
var class_string = []
for _id in Classes.size():
var _class = Classes.values()[_id]
if _class and (class_restrictions & _class) == _class:
var key = Classes.keys()[_id]
class_string.append(str(key))
return ", ".join(class_string)
var compound_category = "":
get:
return _get_compound_category()
enum Classes {
None = 0x0,
Barbarian = 0x1,
Druid = 0x2,
Necromancer = 0x4,
Rogue = 0x8,
Sorcerer = 0x16,
}
enum Category {
Item,
Armor,
Weapon,
Jewelry,
Gem
}
enum Tiers {
Normal,
Sacred,
Ancestral
}
enum Rarities {
Common,
Magic,
Rare,
Legendary,
Unique
}
static var RARITY_COLORS: Array[Color] = [
Color(.3, .3, .3, 1),
Color(.18, .23, .63, 1),
Color(.58, .5, .0, 1),
Color(.56, .21, .0, 1),
Color(.54, .34, .12, 1),
]
func _get_compound_category() -> String:
return "Item"
func _get_type() -> String:
return "Item"
For our base Item, we setup @export
s for our variables that we want to be able to modify on the resource. We also setup some helper variables that use a getter
function to generate things like our class_strings and returning our compound_category from a function we can define on later class definitions. We also have some enumerators for our Character Classes, Categories of Items, Tiers, and Rarities. We also setup the array of RARITY_COLORS to help setup the branding of our backgrounds and borders of our item icons in our UI.
Gear Class
Next we'll extend our Item and define our Gear -> gear.gd
:
# gear.gd
class_name Gear extends Item
@export var power: int = 0
@export var affix_count: int = 0
@export var equipped: bool = false
static var AFFIX_COUNTS: Dictionary = {
Rarities.Common: {
"min": 0,
"max": 0
},
Rarities.Magic: {
"min": 1,
"max": 2
},
Rarities.Rare: {
"min": 2,
"max": 4
},
Rarities.Legendary: {
"min": 4,
"max": 4
},
Rarities.Unique: {
"min": 4,
"max": 4
}
}
var upgrades: int = 0
var max_upgrades: int = 0:
get:
return _get_max_upgrades()
var sockets: int = 0
var max_sockets: int = 1:
get:
return _get_max_sockets()
var socketed: Array[Socketable] = []
var durability: int = 100
var max_durability: int = 100
var affixes: Array[Affix] = [
]
var breakpoint_tier: int:
get:
if power < 150:
return 1
elif power < 340:
return 2
elif power < 460:
return 3
elif power < 625:
return 4
elif power < 725:
return 5
return 6
var min_ranges: Dictionary = {
Tiers.Normal: 0,
Tiers.Sacred: 600,
Tiers.Ancestral: 685
}
var max_ranges: Dictionary = {
Tiers.Normal: 620,
Tiers.Sacred: 720,
Tiers.Ancestral: 820
}
var rolled = false
func _get_max_sockets():
return 1
func _get_max_upgrades():
match(tier):
Tiers.Normal:
match(rarity):
Rarities.Common:
return 0
Rarities.Magic:
return 2
Rarities.Rare:
return 3
Rarities.Legendary:
return 4
Rarities.Unique:
return 5
_:
return 5
func _get_compound_category() -> String:
var _tier = Tiers.keys()[tier]
var _rarity = Rarities.keys()[rarity]
return "{tier}{rarity}{type}".format({
"tier": "%s " % _tier if tier != Tiers.Normal else "",
"rarity": "%s " % _rarity if rarity != Rarities.Common else "",
"type": _get_type()
})
func roll_affixes() -> void:
affixes.clear()
var affix_tier = AFFIX_COUNTS[rarity]
for _i in randi_range(affix_tier.min, affix_tier.max):
var has_affix = true
var affix
while(has_affix):
affix = Affix.roll_affix()
has_affix = affixes.any(func(a: Affix):
return a.stat == affix.stat
)
affixes.append(affix)
func roll() -> void:
sockets = randi_range(0, max_sockets)
upgrades = randi_range(0, max_upgrades)
roll_affixes()
rolled = true
func socket(item: Socketable) -> void:
if socketed.size() < sockets:
socketed.append(item)
return
Our Gear has quite a bit going on, but this is where our Itemization really takes off. We are defining the structure for Item power, Affixes, and other Gear specific variables. We setup some lower and upper limits to determine things like our AFFIX_COUNTS, max_upgrades, max_sockets, breakpoint_tier, Tier Ranges. We have some functions for setting up and rolling our item's stats: roll
and roll_affixes
; when we roll()
we setup our number of sockets and upgrades for this item based on our ranges, then we slap on some Affixes by running our roll_affixes()
function. This will leverage our Affix class to populate some strings in our description, no real behavior in this example, but fits in the UI nicely.
Affix Class
# affix.gd
class_name Affix extends RefCounted
@export var stat: String
@export var type: Type
@export var min_amount: float
@export var max_amount: float
@export var amount: float
@export var template_string: String:
get:
match(type):
Type.Percent:
return "+{amount}% {stat}"
Type.Stat:
return "+{amount} {stat}"
return "Affix Modifier"
var label: String:
get:
return template_string.format({
"amount": ("%.2f" % amount) if (type == Type.Percent) else ("%d" % amount),
"stat": stat.replace("_", " ")
})
enum Type {
Percent,
Stat
}
enum Stat {
Dexterity,
Intelligence,
Strength,
Willpower,
All_Stats
}
static var PERCENT_STATS: Dictionary = {
"Total Armor": {
"min": 2.0,
"max": 4.8
},
"Damage": {
"min": 4.4,
"max": 10.0
},
"Attack Speed": {
"min": 4.4,
"max": 10.0
},
"Basic Skill Attack Speed": {
"min": 4.4,
"max": 10.0
},
}
static func roll_affix() -> Affix:
var affix = Affix.new()
var affix_type = Type.values().pick_random()
affix.type = affix_type
var _stat
var data
match(affix_type):
Type.Percent:
_stat = PERCENT_STATS.keys().pick_random()
data = PERCENT_STATS.get(_stat)
affix.min_amount = data.min
affix.max_amount = data.max
affix.amount = randf_range(affix.min_amount, affix.max_amount)
Type.Stat:
_stat = Stat.keys().pick_random()
affix.stat = _stat
data = [
{
"min": 28,
"max": 42
},
{
"min": 38,
"max": 52
}
].pick_random()
affix.min_amount = data.min
affix.max_amount = data.max
affix.amount = randi_range(int(affix.min_amount), int(affix.max_amount))
affix.min_amount = data.min
affix.max_amount = data.max
affix.amount = randf_range(affix.min_amount, affix.max_amount)
affix.stat = _stat
return affix
Our Affix code is a bit more involved so we can randomize based on flat percentages and stat based. The end result should be an Affix with a string that can be passed through a template. This structure can be extended much further to provide a mechanical benefit to our stat system if we wanted to flesh it out further for our needs!
Armor Class
Luckily our Gear class handles the bulk of the heavy lifting for data and functions for what our equipment may need, but our Armor will have some unique distinctions:
# armor.gd
class_name Armor extends Gear
@export var type: Type = Type.Chest
enum Type {
Chest,
Helm,
Pants,
Footwear,
Gloves,
Shield
}
static var ARMOR_TEXTURE: Texture = preload("res://assets/armor/armory.png")
static var ARMOR_ICONS = {
Type.Chest: [0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88],
Type.Helm: [1, 9, 17, 25, 33, 41, 49],
Type.Pants: [2, 10, 18, 26, 34],
Type.Footwear: [3, 11, 19, 27, 35],
Type.Gloves: [4, 12, 20, 28, 36, 44, 52],
Type.Shield: [7, 15, 23, 31, 39, 47],
}
var category: Category = Category.Armor
func _get_max_sockets() -> int:
match(type):
Type.Chest, Type.Pants:
return 2
_:
return 1
func _get_type() -> String:
return Type.keys()[type]
Notice that we can define our Armor's type based on our Type enumerator.
We also have a nice ARMOR_TEXTURE that we reference, along with the ARMOR_ICONS mapping frame indices to the specific Armor Type.
We also set the category as being the Armor Category. our _get_max_sockets
function is defined as part of overriding our Gear implementation, which allows us to vary based on our Armor Type.
Lastly we override our _get_type
function to be based on our Type enumerator.
Weapon Class
Our Weapon class is almost identical in structure as our Armor class, instead we define our Weapon Types and use a new WEAPON_TEXTURE and it's mapping for WEAPON_ICONS.
The functions and category are all the same approach as our Armor implementation.
# weapon.gd
class_name Weapon extends Gear
@export var type: Type = Type.Axe
var category: Category = Category.Weapon
enum Type {
Axe,
Axe2H,
Bow,
Crossbow,
Dagger,
Offhand,
Mace,
Mace2H,
Staff,
Sword,
Sword2H,
Scythe,
Scythe2H,
Wand,
Polearm
}
static var WEAPON_TEXTURE: Texture = preload("res://assets/armor/weapons.png")
static var WEAPON_ICONS: Dictionary = {
Type.Axe: [44,46,47,48,49],
Type.Axe2H: [44,46,47,48,49],
Type.Bow: [90,91,92,93,94,95,96,97,98,99],
Type.Crossbow: [80,81,82,83,84,85,86,87,88,89],
Type.Dagger: [30,31,32,34,36,37,38,39],
Type.Offhand: [70,71,72,73,74,75,76,77,78,79],
Type.Mace: [43,45,50,51,52,53,54,55,56,59],
Type.Mace2H: [43,45,52,53,54,55,56,59],
Type.Staff: [0,1,2,3,4,5,6,7,8,9],
Type.Sword: [60,61,62,63,64,65,66,67,68,69],
Type.Sword2H: [40,41,60,61,62,63,64,65,66,67,68,69,],
Type.Scythe: [33,35],
Type.Scythe2H: [33,35],
Type.Wand: [10,11,12,13,14,15,16,17,18,19],
Type.Polearm: [20,21,22,23,24,25,26,27,28,29],
}
func _get_max_sockets() -> int:
match(type):
Type.Axe2H, Type.Bow, Type.Crossbow, Type.Mace2H, Type.Staff, Type.Sword2H, Type.Scythe2H, Type.Polearm:
return 2
_:
return 1
func _get_type() -> String:
match(type):
Type.Axe2H:
return "Two-Handed Axe"
Type.Mace2H:
return "Two-Handed Mace"
Type.Sword2H:
return "Two-Handed Sword"
Type.Scythe2H:
return "Two-Handed Scythe"
return Type.keys()[type]
Jewelry Class
Big surprise, the Jewelry class is like the rest of our Gear, with only a Ring and Amulet type, the implementation is even easier!
# jewelry.gd
class_name Jewelry extends Gear
@export var type: Type = Type.Ring
enum Type {
Ring,
Amulet
}
static var JEWELRY_TEXTURE: Texture = preload("res://assets/armor/armory.png")
static var JEWELRY_ICONS = {
Type.Ring: [5, 13, 21, 29],
Type.Amulet: [6, 14, 22, 30],
}
var category: Category = Category.Jewelry
func _get_max_sockets() -> int:
return 2
func _get_type() -> String:
return Type.keys()[type]
Socketable and Gem Classes
Let's look at our other Item types; Socketable/Gem class.
First our Socketable is simply a new base class for future socketable Items: Gems and Hearts for Season 1, who know's what else we could add (Runes maybe?).
# socketable.gd
class_name Socketable extends Item
func _get_compound_category() -> String:
return "Socketable"
Woah, so little code, what about our Gem?:
# gem.gd
class_name Gem extends Socketable
@export var type: Type = Type.Diamond
@export var quality: Quality = Quality.Crude:
set(qual):
quality = qual
level_requirement = LEVEL_REQUIREMENTS[quality]
enum Type {
Diamond,
Saphire,
Ruby,
Emerald,
Amethyst,
Topaz
}
enum Quality {
Crude,
Chipped,
Normal,
Flawless,
Royal
}
var LEVEL_REQUIREMENTS = [
15,
20,
40,
50,
60
]
var texture = preload("res://assets/armor/gem.png")
var COLORS: Array[Color] = [
Color(.68, .66, .62),
Color(0, .23, .96),
Color(.8, .12, .16),
Color(.07, .58, .05),
Color(.51, 0, .81),
Color(.83, .57, .15)
]
var category: Category = Category.Gem
var color: Color:
get:
return COLORS[type]
func _init() -> void:
stackable = true
value = 4
image = texture
func _get_compound_category() -> String:
return "Gem"
func _get_type() -> String:
return Type.keys()[type]
Looks like our Gem class is doing a lot of the heavy lifting for socketables right now, but that's how things are for this example. If I had implemented more Socketable items, I'd likely extract those shared behaviors into our common class, but for now, Gem is all we have.
A good consideration here is if the Socketable class is required then? I would say go with your gut, don't over complicate things if you don't need to, in my original run of this code, I didn't have a Socketable class, as only Gems were on my mind, so everything was based purely on Gem in the code, but I refactored that out to be based on Socketable instead when looking at a Gear's socket structures.
As always, use your best judgements!
Data Driven UI
Now that we've created our data structures for Items, let's look back our Inventory Grid and see how we leverage that data to build out our Inventory and drive our UI with it all.
# inventory_grid.gd
extends GridContainer
var ui_item: PackedScene = preload("res://ui/inventory/ui_item_icon.tscn")
var prefixes: Array = [...]
var suffixes: Array = [...]
var inventory: Inventory
func _ready() -> void:
inventory = Inventory.new()
...
func _on_item_added(item: Item, _qty, _total) -> void:
...
func _on_item_removed(item: Item, _qty, _total) -> void:
...
func generate_item(category: Item.Category) -> Item:
...
func generate_name(item: Item) -> String:
...
First we've got our Inventory class -> inventory.gd
# inventory.gd
class_name Inventory extends Resource
signal item_added(item, qty, total)
signal item_removed(item, qty, total)
@export var max_capcity: int = 1
@export var items: Array[Item] = []
func _init(cap: int = 1):
max_capcity = cap
func find(item: Item) -> Item:
var _id: int
if item is Gem:
var _items = items.filter(
func(_item: Item):
return _item is Gem and _item.type == item.type and _item.quality == item.quality
)
if _items.size():
_id = items.find(_items.front())
else:
return
else:
_id = items.find(item)
if _id != -1:
return items[_id]
return
func add(item: Item, qty: int = 1) -> void:
var i = find(item)
if !i:
items.append(item)
item.inventory = self
i = item
i.quantity = qty
item_added.emit(i, qty, i.quantity)
elif i.stackable:
i.quantity += qty
item_added.emit(i, qty, i.quantity)
func remove(item: Item, qty: int = 1) -> void:
var i = find(item)
if !i:
return
if i.quantity >= qty:
i.quantity -= qty
item_removed.emit(item, qty, i.quantity)
else:
items.erase(item)
item_removed.emit(item, qty, 0)
func has(item: Item, minimum: int = 1) -> bool:
return count(item) >= minimum
func count(item: Item) -> int:
var i = find(item)
if i:
return i.quantity
return 0
func sort() -> void:
items.sort_custom(func(a: Item, b: Item):
if a.category < b.category:
return true
elif a.category == b.category:
if a.type < b.type:
return true
elif a.type == b.type:
if a is Gear and b is Gear:
return a.power > b.power
if a is Gem and b is Gem:
return a.quality > b.quality
return false
)
Here we have a data structure for storing our Item's and keeping track of when we add/remove 1 or more Item. We have signals that we emit when an item is added or removed form our Inventory. We also have a convenient sort
function that will organize our Inventory based on our custom function.
Back in our Inventory Grid
# inventory_grid.gd
extends GridContainer
var ui_item: PackedScene = preload("res://ui/inventory/ui_item_icon.tscn")
...
func _ready() -> void:
inventory = Inventory.new()
for i in range(randi_range(30, 33)):
var item_data = generate_item([
Item.Category.Weapon,
Item.Category.Weapon,
Item.Category.Armor,
Item.Category.Armor,
Item.Category.Jewelry,
Item.Category.Jewelry,
Item.Category.Gem,
].pick_random())
inventory.add(item_data, item_data.quantity)
inventory.sort()
for item in inventory.items:
_on_item_added(item, item.quantity, item.quantity)
inventory.item_added.connect(_on_item_added)
inventory.item_removed.connect(_on_item_removed)
func _on_item_added(item: Item, _qty, _total) -> void:
...
func _on_item_removed(item: Item, _qty, _total) -> void:
...
func generate_item(category: Item.Category) -> Item:
...
func generate_name(item: Item) -> String:
...
When our node is ready, we loop over a range of 30-33 items and call our generate_item
function passing in a random item from our list.
Then we take that generated item and add it to our Inventory then call sort
once we've added all our items. Once we've sorted, we'll loop over the items and call our _on_item_added
function, passing the item and it's quantity.
We also want to connect our signals to the inventory so when we add or remove a new item, we can properly update our UI grid.
Let's look at our generate_*
functions:
# inventory_grid.gd
...
func generate_item(category: Item.Category) -> Item:
var item: Item
var atlas: AtlasTexture = AtlasTexture.new()
var type: int
match(category):
Item.Category.Gem:
item = Gem.new()
item.type = Gem.Type.values().pick_random()
item.quality = Gem.Quality.values().pick_random()
item.salvageable = false
item.name = "{quality}{spacer}{type}".format({
"quality": "" if item.quality == Gem.Quality.Normal else Gem.Quality.keys()[item.quality],
"spacer": "" if item.quality == Gem.Quality.Normal else " ",
"type": Gem.Type.keys()[item.type]
})
item.quantity = randi_range(1, 3)
return item
Item.Category.Armor:
item = Armor.new()
type = Armor.Type.values().pick_random()
atlas.atlas = Armor.ARMOR_TEXTURE
var i = Armor.ARMOR_ICONS[type].pick_random()
atlas.region = Rect2((i % 8) * 32, (i / 8) * 32, 32, 32)
Item.Category.Weapon:
item = Weapon.new()
type = Weapon.Type.values().pick_random()
atlas.atlas = Weapon.WEAPON_TEXTURE
var i = Weapon.WEAPON_ICONS[type].pick_random()
atlas.region = Rect2((i % 10) * 24, (i / 10) * 24, 24, 24)
Item.Category.Jewelry:
item = Jewelry.new()
type = Jewelry.Type.values().pick_random()
atlas.atlas = Jewelry.JEWELRY_TEXTURE
var i = Jewelry.JEWELRY_ICONS[type].pick_random()
atlas.region = Rect2((i % 8) * 32, (i / 8) * 32, 32, 32)
...
return item
func generate_name(item: Item) -> String:
var prefix = prefixes.pick_random()
var suffix = suffixes.pick_random()
return "{pre}{prefix_spacer}{type}{suffix_spacer}{suff}".format({
"pre": prefix,
"prefix_spacer": "" if prefix == "" else " ",
"type": item._get_type(),
"suff": suffix,
"suffix_spacer": "" if suffix == "" else " ",
})
Our function will take the category of Item we want to generate and match on that to populate the Gem and return, or if it's a type of Gear, we'll create the proper Item class, set the type, create the AtlasTexture
from the proper Texture
and region, then the remaining common variables and return our generated item.
match(category):
...
item.image = atlas
item.type = type
item.name = generate_name(item)
item.power = randi_range(0, 820)
item.value = randi_range(10000, 200000)
item.rarity = Item.Rarities.values().pick_random()
item.tier = Item.Tiers.values().pick_random()
item.level_requirement = randi_range(0, 100)
item.is_account_bound = randi_range(0,1) == 0
item.salvageable = true
item.tradable = (item.rarity != Item.Rarities.Unique)
item.roll()
for socket in randi_range(0, item.sockets):
var gem = generate_item(Item.Category.Gem)
item.socket(gem)
return item
We also are calling our generate_name
function to get the proper string name of our item and set it to our item's details.
Now let's look at those function's we've connected to our signals:
# inventory_grid.gd
...
func _on_item_added(item: Item, _qty, _total) -> void:
var ui_item_icon = ui_item.instantiate() as UIItemIcon
ui_item_icon.item = item
add_child(ui_item_icon)
func _on_item_removed(item: Item, _qty, _total) -> void:
for ui_icon in get_children() as Array[UIItemIcon]:
if ui_icon.item == item:
if _total == 0:
remove_child(ui_icon)
else:
ui_icon._update_item_icon()
These functions help to create instances of our UIItemIcon and track those children and update when we remove the items from the Inventory.
UI Item Icon
Our UIItemIcon is our connection between data and the UI, we'll assign the Item to the item variable of our UI element, and it'll do the heavy lifting for mapping the data to the proper components, and delegating how we view our Tooltip as well.
Our Node Tree consists of our Root Control node, Our Item/Highlight/ItemIcon Textures, a QuantityLabel, and a container for our Sockets. These will all be mapped in code for binding data within our script -> ui_item_icon.gd
.
# ui_item_icon.gd
class_name UIItemIcon extends Control
@export var item: Item:
set(_item):
item = _item
item.changed.connect(_update_item_icon)
@onready var item_texture: TextureRect = $ItemTexture
@onready var item_icon_texture: TextureRect = $ItemIconTexture
@onready var highlight_texture: TextureRect = $HighlightTexture
@onready var quantity_label: Label = %QuantityLabel
@onready var socket_controls: VBoxContainer = %Sockets
var overlay_shader: ShaderMaterial = preload("res://ui/shaders/color_overlay_shader.tres")
var SOCKET_SCENE = preload("res://ui/inventory/ui_socket.tscn")
func _ready() -> void:
_update_item_icon()
func _update_item_icon() -> void:
...
func _get_drag_data(_at_position: Vector2) -> Variant:
...
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
...
func _drop_data(_position, data) -> void:
...
We @export
out our Item variable with a setter function so we can connect to the Item's changed
signal, letting us know the item's data may have changed, such as quantity. When this happens we call our _update_item_icon
function, which is the staple of this script.
After our @onready
binds, we have our variables for the overlay_shader which is what we use to add some color shader to our bg texture. Our color_overlay_shader.tres
ShaderMaterial
has a shader -> ui_item_icon.gdshader
# ui_item_icon.gdshader
shader_type canvas_item;
uniform vec3 color = vec3(1.0, 1.0, 1.0);
void fragment() {
vec4 base = texture(TEXTURE, UV);
vec4 blend = vec4(color, base.a);
vec4 limit = step(0.5, base);
COLOR = mix(2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend), limit);
}
Our Shader is a Color mix of our base texture and our defined color shader parameter.
We also have our SOCKET_SCENE to instance for each socket of this item -> ui_socket.gd
. This NodeTree is a Control
root, with a TextureRect
for the Socket Background as well as the Gem Texture
; which we'll bind a reference in code.
# ui_socket.gd
class_name UISocket extends Control
@export var item: Gem:
set(_item):
item = _item
_update_gem()
@export var gem_colors: Array[Color] = [
Color(.68, .66, .62),
Color(0, .23, .96),
Color(.8, .12, .16),
Color(.07, .58, .05),
Color(.51, 0, .81),
Color(.83, .57, .15)
]
@onready var gem: TextureRect = $Gem
var overlay_shader: ShaderMaterial = preload("res://ui/shaders/color_overlay_shader.tres" )
func _ready() -> void:
_update_gem()
func _update_gem() -> void:
if item:
if gem and overlay_shader:
gem.show()
var shader_material = overlay_shader.duplicate()
shader_material.set_shader_parameter("color", item.color)
gem.material = shader_material
else:
gem.hide()
Our Socket will either show the gem with the shader applied based on our Item's color, or it'll hide the Gem Texture
.
In our ui_item_icon.gd
's _ready
function, we're calling our _update_item_icon
function.
# ui_item_icon.gd
...
func _update_item_icon() -> void:
var color: Color
if item:
if item is Gear:
if !item.rolled:
item.roll()
var mat = overlay_shader.duplicate() as ShaderMaterial
match(item.category):
Item.Category.Gem:
item_icon_texture.texture = Gem.GEM_TEXTURE
mat.set_shader_parameter("color", item.color)
item_icon_texture.material = mat
color = Item.RARITY_COLORS[Item.Rarities.Common]
var bg_mat = overlay_shader.duplicate() as ShaderMaterial
bg_mat.set_shader_parameter("color", color)
item_texture.material = bg_mat
highlight_texture.hide()
Item.Category.Weapon, Item.Category.Armor, Item.Category.Jewelry:
color = Item.RARITY_COLORS[item.rarity]
mat.set_shader_parameter("color", color)
item_texture.material = mat
item_icon_texture.texture = item.image
item_icon_texture.material = null
highlight_texture.visible = !(item.tier == Item.Tiers.Normal)
highlight_texture.self_modulate = Color.PEACH_PUFF if item.tier == Item.Tiers.Sacred else Color.WHITE
if item is Gear and item.sockets:
for socket in socket_controls.get_children():
socket_controls.remove_child(socket)
for i in item.sockets:
var socket = SOCKET_SCENE.instantiate() as UISocket
if item.socketed.size() > i:
var gem: Gem = item.socketed[i] as Gem
if gem:
socket.item = gem
socket_controls.add_child(socket)
if item.stackable:
if item.quantity > 1:
quantity_label.show()
else:
quantity_label.hide()
quantity_label.text = str(item.quantity)
else:
quantity_label.hide()
if !mouse_entered.is_connected(UI.on_hover_item.bind(self)):
mouse_entered.connect(UI.on_hover_item.bind(self))
if !mouse_exited.is_connected(UI.on_hover_leave.bind(self)):
mouse_exited.connect(UI.on_hover_leave.bind(self))
We ensure we have an item defined, and then if it's Gear we want to ensure we've rolled it's stats. Then we get a duplicate of our ShaderMaterial
which for the Gem icon we'll apply the shader as we previously did for our UISocket scene.
For the Icon's Background Texture
we'll also set the material based on the item's rarity.
For Gear we also have the highlight Texture
which we show for Sacred and Ancestral Tiers.
We loop over the Gear's sockets and create an instance of our UISocket and ensure we add it to the scene.
Stackable Items will show a quantity label when the value is over 1.
Last we ensure we have our Mouse events connected for our UI's functions!
UI Tooltip
# ui.gd
class_name UI extends Control
var tooltip: Tooltip
var TOOLTIP_SCENE: PackedScene = preload("res://ui/tooltip/tooltip.tscn")
static var _ui: UI
func _ready() -> void:
_ui = self if !_ui else _ui
func _delete_tooltip() -> void:
if _ui.tooltip and !_ui.tooltip.is_queued_for_deletion():
_ui.remove_child(_ui.tooltip)
if _ui.tooltip.mouse_entered.is_connected(_ui.on_hover_item):
_ui.tooltip.mouse_entered.disconnect(_ui.on_hover_item)
if _ui.tooltip.mouse_exited.is_connected(_ui.on_hover_leave):
_ui.tooltip.mouse_exited.disconnect(_ui.on_hover_leave)
_ui.tooltip.queue_free()
_ui.tooltip = null
static func on_hover_item(item: UIItemIcon) -> void:
if _ui:
_ui._delete_tooltip()
var tt: Tooltip = _ui.TOOLTIP_SCENE.instantiate() as Tooltip
tt.item = item.item
_ui.tooltip = tt
tt.global_position.x = item.global_position.x - tt.size.x - 40
_ui.add_child(tt)
static func on_hover_leave(_item: UIItemIcon) -> void:
if _ui:
_ui._delete_tooltip()
We setup a static reference to our ui, making this a singleton. There are many ways to tackle this piece, here I'm experimenting with Godot's static variables for our _ui variable.
Note our on_hover_*
functions; they delete and recreate our UI's tooltip scene and ensure it's position and item is properly defined based on which UIItemIcon Item we hover from our Inventory Grid. These functions call over to our _delete_tooltip
function for the cleanup and remove it from memory.
Our Tooltip is also doing some data binding similar to the UIItemIcon and has a more involved Node SceneTree and accompanied script.
The Tooltip's Root is a Control
node, and has a VBoxContainer
to stack the EquippedContainer
Control
node for showing when our Item is equipped. This is done by a referenced EquippedPlate
Control
node containing a ColorRect
background and MarginContainer
for the Texture
and Label
. We'll toggle this visibility within our script.
Our InfoPanel
Control
node contains the bulk of bound data. We have a ColorRect
BG and InfoContainer
MarginContainer
, nesting it's own ColorRect
BG and MarginContainer
. This sets up the layers of Border, padding, and Background Graident Texture
s: GradientRect
and BorderRect
which are each NinePatchRect
s and will be referenced in code to apply our shader to it based on our Item Rarity. Inside we have our MarginContainer
for the IconTexture
and the VBoxContainer
for stacking the individual labels for our data binding to the Item properties; each of which is bound with a unique name to be referenced from within our script.
# tooltip.gd
@tool
class_name Tooltip extends Control
@export var color: Color
@export var item: Item
@onready var equipped_plate: Control = %EquippedPlate
@onready var info_container: MarginContainer = %InfoContainer
@onready var bg: ColorRect = %BG
@onready var icon_texture: TextureRect = %IconTexture
@onready var item_name: RichTextLabel = %ItemName
@onready var category: RichTextLabel = %Category
@onready var power: RichTextLabel = %Power
@onready var upgrades: RichTextLabel = %Upgrades
@onready var stat_separator: HSeparator = %StatSeparator
@onready var description: RichTextLabel = %Description
@onready var stat: RichTextLabel = %Stat
@onready var affix_separator: HSeparator = %AffixSeparator
@onready var affix: RichTextLabel = %Affix
@onready var level_requirement: RichTextLabel = %LevelRequirement
@onready var account_bound: RichTextLabel = %AccountBound
@onready var salvageable: RichTextLabel = %Salvageable
@onready var tradable: RichTextLabel = %Tradable
@onready var class_identifier: RichTextLabel = %ClassName
@onready var price: HBoxContainer = %Price
@onready var sell_value: RichTextLabel = %SellValue
@onready var coin_icon: TextureRect = %CoinIcon
@onready var durability: RichTextLabel = %Durability
@onready var gradient_rect: NinePatchRect = %GradientRect
@onready var border_rect: NinePatchRect = %BorderRect
@onready var node_map: Dictionary = { ... }
var SOCKET_CONTAINER: PackedScene = preload("res://ui/tooltip/socket_container.tscn")
var overlay_shader: ShaderMaterial = preload("res://ui/shaders/color_overlay_shader.tres")
var string_templates: Dictionary:
get:
...
var item_tokens: Dictionary:
get:
...
var hidden_nodes: Array:
get:
...
func _ready() -> void:
_update_tooltip_values()
func _update_tooltip_values() -> void:
...
func _on_info_container_resized() -> void:
...
The bulk of our work for setting up our Tooltip happens on our _update_tooltip_values
function, which we call when our node is ready:
# tooltip.gd
...
func _update_tooltip_values() -> void:
if !item:
return
icon_texture.texture = item.image
var mat = overlay_shader.duplicate() as ShaderMaterial
var col = Item.RARITY_COLORS[item.rarity]
mat.set_shader_parameter("color", col)
gradient_rect.material = mat
border_rect.material = mat
item_name.add_theme_color_override("default_color", col.lightened(.6))
category.add_theme_color_override("default_color", col.lightened(.6))
upgrades.add_theme_color_override("default_color", col.lightened(.6))
for key in node_map.keys():
var node = node_map.get(key) as RichTextLabel
if node:
var template: String = string_templates.get(key, "")
node.text = template.format(item_tokens)
if item:
if hidden_nodes.has(key):
node.visible = false
else:
match(key):
"upgrades":
node.visible = false if !(item is Gear) else item.upgrades > 0
"account_bound":
node.visible = item.is_account_bound
"power":
if item is Gem:
node.text = ""
"salvageable":
node.visible = !item.salvageable
"tradable":
node.visible = !item.tradable
"class_identifier":
node.visible = item.class_restrictions != 0
var last_node = stat
if item is Gear:
for _affix in item.affixes:
var affix_node = affix.duplicate()
affix_node.text = _affix.label
last_node.add_sibling(affix_node)
last_node = affix_node
match(item.category):
Item.Category.Gem:
var gem_mat = overlay_shader.duplicate() as ShaderMaterial
gem_mat.set_shader_parameter("color", item.color)
icon_texture.material = gem_mat
Item.Category.Weapon, Item.Category.Armor, Item.Category.Jewelry:
for i in item.sockets:
var socket = SOCKET_CONTAINER.instantiate() as SocketContainer
if item.socketed.size() > i:
var gem: Gem = item.socketed[i] as Gem
if gem:
socket.gem = gem
last_node.add_sibling(socket)
last_node = socket
equipped_plate.visible = item is Gear and item.equipped
info_container.force_update_transform()
bg.set_deferred("size", Vector2(bg.size.x, info_container.get_rect().size.y))
We are setting our icon's Texture
, we setup our ShaderMaterial
based on our Item Rarity, this is applied to our gradient_rect and border_rect Texture
s. We also set the color_override
for the item_name, category, and upgrades Label
.
We map over our node_map and match any potential string_templates and toggle any visibility based on boolean states:
# tooltip.gd
...
@onready var node_map: Dictionary = {
"item_name": item_name,
"category": category,
"power": power,
"upgrades": upgrades,
"description": description,
"stat": stat,
"affix": affix,
"level_requirement": level_requirement,
"account_bound": account_bound,
"salvageable": salvageable,
"tradable": tradable,
"class_identifier": class_identifier,
"sell_value": sell_value,
"durability": durability,
}
...
var string_templates: Dictionary:
get:
return {
"item_name": "{item_name}{qty_string}",
"category": "{compound_category}",
"power": "{base_power}{bonus_power_string} Item Power",
"upgrades": "[b]Upgrades[/b]: {current_upgrades}/{max_upgrades}",
"description": "{description}",
"stat": "Stat Modifier",
"socket_label": "Empty Socket",
"level_requirement": "Requires Level {level_requirement}",
"account_bound": "Account Bound",
"salvageable": "Cannot Salvage",
"tradable": "Not Tradable",
"class_identifier": "{class_identifier}",
"sell_value": "[color=#d5af88]Sell Value[/color]: {sell_value}",
"durability": "[color=#d5af88]Durability[/color]: {durability}/{max_durability}"
}
var item_tokens: Dictionary:
get:
if !item:
return {
"item_name": "Test Item 2000",
"compound_category": "Sacred Legendary Gloves",
"base_power": "700",
"bonus_power_string": "+25" if true else "",
"current_upgrades": 4,
"max_upgrades": 5,
"level_requirement": 80,
"class_identifier": "Rogue",
"sell_value": 14000
}
else:
var details: Dictionary = {
"item_name": item.name,
"qty_string": (" (%d)" % item.quantity) if (item.stackable and item.quantity > 1) else "",
"compound_category": item.compound_category,
"level_requirement": item.level_requirement,
"class_identifier": str(item.class_strings),
"sell_value": StringHelper.comma_sep(item.value),
}
if item is Gear:
var extras: Dictionary = {
"base_power": item.power,
"bonus_power_string": "+{power}".format({"power": item.upgrades * 5}) if item.upgrades else "",
"current_upgrades": item.upgrades if item is Gear else 0,
"max_upgrades": item.max_upgrades if item is Gear else 0,
"durability": item.durability,
"max_durability": item.max_durability
}
details.merge(extras)
if item is Gem:
var extras: Dictionary = {
"description": "Can be inserted into equipment with sockets."
}
details.merge(extras)
return details
var hidden_nodes: Array:
get:
var nodes = []
match(item.category):
Item.Category.Gem:
nodes.append_array([
"class_identifier",
"stat",
"durability"
])
_:
nodes.append_array([
"description"
])
return nodes
...
Note how we are using a getter function to define some of our Dictionaries and their corresponding templates.
We also loop over any Gear Affixs and add the corresponding labels as we need inside our Stat list.
Similar to our UIItemIcon, we look at our Item Category and for Gem we setup the shader for the icon, for Gear, we map our Sockets using our UISocket scene and add it to our scene tree.
Last we setup our Equipped Plate
visibility and update our container's sizing.
# tooltip.gd
...
func _on_info_container_resized() -> void:
if info_container and bg:
bg.set_deferred("size", Vector2(bg.size.x, info_container.get_rect().size.y))
We'll also ensure our signal is bound so when our info container is resized for any reason, we'll update the sizing again.
Because our Tooltip is a tool script -> @tool
, we can see a realtime preview in our Editor!
And now that we have everything setup for Tooltips and UIItemIcons we can see it in action when we run our demo. Our Inventory Grid generates a full inventory, and when we hover over an Item within our grid, we can see our Tooltip showing that Item's details:
Be sure to check out the great pixel art I've used in this example: IdylWild's Armory and DARA90's RPG WEAPONS .
Equipping Gems
What about all those drag
related functions within our UIItemIcon? Well that allows us to drag and drop Items around and depending on our logic allow behavior to occur, like say slapping a Gem into an empty socket of an Item.
# ui_item_icon.gd
...
func _get_drag_data(_at_position: Vector2) -> Variant:
var preview = Control.new()
var icon = item_icon_texture.duplicate()
icon.self_modulate.a = .5
preview.add_child(icon)
set_drag_preview(preview)
return { item_id = "item", item = item}
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if data.item is Socketable:
if item is Gear:
if item.socketed.size() < item.sockets:
return true
return false
func _drop_data(_position, data) -> void:
if data.item is Socketable:
if item is Gear:
if item.socketed.size() < item.sockets:
item.socket(data.item)
if data.item.inventory:
data.item.inventory.remove(data.item, 1)
_update_item_icon()
UI.on_hover_leave(self)
UI.on_hover_item(self)
Our _get_drag_data
function defines what happens when we start dragging an Item. We'll set our dragging icon to our Item icon at half alpha. Then we return a data object that contains our item_id and item variable. This data object is passed between our _can_drop_data
function and our _drop_data
function.
These test what we can do with the data object, in our case our Item we chose to drag around, and where we drag to. If we test if the Item we are dragging is of type Socketable and the current UIItemIcon's item we are dragging over is a type of Gear with free sockets, then we can drop.
When we can and do drop our drag even onto a valid UIItemIcon, we tell our Gem to remove from the inventory, and the Gear to apply the socket item, and update it's UIItemIcon to reflect the change.
Recap
To wrap it up, I threw together a fun dungeon scene using Kenney's Dungeon Asset Pack and rendered it out as a static background. Added a TextureRect
with this render as a background under our UI scene, and added a gradient to transition between the UI Character Details panel and our "Game" rendered image. This gives it a nice look and give's it a nice atmosphere to match our UI!
With all that completed we can see that we have an exciting working prototype of Diablo IV's Itemization and UI likeness within our Godot 4 scene.
Our implementation for Itemization lets us randomize the items based on the categories and types of items we may want, fancy UI Icons that update based on the data, as well as showing a Tooltip upon hovering over the icon. We can drag and drop icons around for a socket mechanism, and this could be used to drag and drop to organize the inventory, or swap gear in equipment slots, or whatever else you wanted to implement for your game; you could even reintroduce the grid based mechanic from older Diablo games and have fun with the tetris style approach!
This project was much more invovled than our previous topics, but was really fun to work on. I hope you were able to take away some ideas on how to take an existing idea and break it down into understandable components and implement them for your games.
Join us over on our Discord if you want to discuss the project in more detail!
If you want to support my work, consider sharing with others, becoming a member or leaving a one time donation on Ko-fi !
Thanks for joining me on this adventure!
Happy Coding.