3d RPG clothing (and inventory) system using Godot 4 and Blender

Copyright Pablo Ibarz 2025. CC BY-NC 4.0

Ever wanted to make a character that can wear different things they find on the ground? I played Disco Elysium and thought the clothing system was really cool. I tried to make it in Godot and ended up with something much more general. The result could be adapted to behave like clothing systems from almost any 3d game that has them.

All project files and resources used in this guide can be found here: GitHub - Roboticy3/body-groups: 3d equipment and inventory system in Godot

Final product (advanced):

Final product (introductory):

This guide is presented in two levels:

Introductory - You just want the chestplate to show up on the guy. You have a basic understanding of the Godot editor. If you don’t, please watch a tutorial on the different editor panels first (the one I linked is very old, but holds up well. GDQuest is the best!). You may not understand exactly how the code below works, but you should still be able to use it if you follow along.

Software Requirements (introductory):

  • Godot (I’m using version 4.4.1 stable, but this guide should work on any 4.x version.)

Advanced - You want granular control- and I mean really granular control -of how different pieces of clothing occlude each other. You have some general programming knowledge. You know what the word occlude means. You are willing to try some spicier techniques for the sake of workflow… You also have a basic understanding of the Godot editor.

Software Requirements (advanced)

  • Godot
  • Blender (I’m using 4.4 stable, but this guide should go as far back as Godot is compatible)
    • (optional) Mixamo Root addon or alternative import-mixamo_root_motion addon, they both do the same thing more or less, but neither of them are maintained very well. That’s why I made their section optional.

Both levels assume you will use the same inventory system, so we’ll start with that.

I was going to upload this as one big thing. But the character limit for a post is 32000 characters and I was a bit over 100000. Hopefully I can figure out how to chain these together in some meaningful way.

Edit: I updated the code to be better and rewrote the last section to be shorter. It’s become obvious to me that this guide is pretty advanced no matter how you cut it, but the beginner section does still do a good job being easier to follow.

Part 1: An inventory

Without an inventory to keep our clothing, we’ll never be able to equip it! We’ll start by designing the data structure for our items.

Create a new folder called “inventory” in the file dock by right clicking “res://”, then clicking Create New, then clicking Folder, then typing “inventory” into the bottom of the file popup and clicking “Create”. This will keep all scripts and scenes and whatever other things you come up with related to the inventory system.

Items

Items shouldn’t do anything on their own. They just store data for other systems in the game to use. They are also shared, in the fact that multiple parts of the game may want to know what the deal is with the same item. These are perfect use cases for Godot’s Resource class.

Create a subfolder of inventory called “scripts” (right click the inventory folder, then create a new folder from there), then create a script (right click the scripts folder, click Create New, then click Script). A popup should appear. You can set the script to “inherit” from the Resource class by typing “Resource” in the “Inherits:” box.

Then change the path to “res://inventory/scripts/inventory.gd” and click Create. The script editor may appear in the center panel automatically. If not, you can double click the script in the file dock to edit it, or press Ctrl+F3 to view the script editor. You should end up with something like this on screen:

extends Resource

Scripts are really just text files. The script editor will let us edit the text files, while also giving the text nice colors and telling us if the scripts have any obvious errors that will prevent Godot from being able to interpret them as code.

So, what should items store? Well, it depends on your project. Let’s look at what I came up with and see why each field is there.

inventory_item.gd

## An inventory item.
## Equipment should be saved to a file and named the body group that it will 
##	be displayed on.

extends Resource
class_name InventoryItem

#The display texture of this item in the inventory
@export var texture:Texture2D

#Name and description
@export var display_name:String
@export_multiline var description:String

#A list of places this item can be stored. For example:
#	Gear - stored in the equipment panel
#	Hat - goes in the hat slot in equipment
#	Key - stored in the key panel
#	Shirt - goes in the shirt slot in equipment
# and so on.
@export var types:Array[StringName]

#Set to false to allow for multiple of this item to be added to inventory lists.
@export var single:=true

func _to_string() -> String:
	return "InventoryItem<{}, {}>".format([display_name, types], "{}")

Script Explanation

class_name will register this script as a “type” (depending on how much you care about object oriented programming, please cut out additional quotation marks: “”“”“”“”“”), which will let us reference the structure you define here in other scripts (yes, I know there are other ways to do this, do it your way if you want, I’m not your dad).

texture: An image or texture representing the item. When you display the item on our inventory screen, this texture might give us a better visual representation.

display_name: The plain text name with which the item will appear on screen, or a translation key if you’re using translations.

description: A longer, flavorful description of the item. I believe item descriptions are a very cheap way to effectively contribute to worldbuilding and tone, but this is technically optional.

types: A list of places the item can sit in the inventory. If your rpg has multiple characters, it would be good to include in this list the names of characters that can equip this item, then add a check later to tell the player if the article of clothing “doesn’t fit”. We’re going to use it to tag items with which screens of the inventory they can move between, such as the equipment panel versus individual equipment slots.

Note that all the properties have the @export tag. @export tags each field to be persistent, meaning you can access them in the Inspector panel of the editor and save copies of this resource when you start using it in the editor, which you will do shortly.

It is also important to understand that this script does not include the item’s actual shape and materials, as in how they will appear on your character when equipped. You will import/create 3d models for the equipment later. This is more like an attachment onto those models, telling the game how the item can be used, and how it should appear in menus.

Creating an Item

Let’s make an item and save it to the project. Choose a place in the filesystem dock you want to keep your items. Mine are character-specific, so I have a folder for the character and a folder for their associated items inside. Once you’ve chosen a location, right click to “Create New” then select “Resource”. A popup with a search bar should appear, containing all Resource types. Type in “InventoryItem” into the search bar, a result should come up, then click “Create” at the bottom of the popup. Now, a second popup appears! This is a regular file dialogue, where you can set the file name for your resource and save it to a file.

Give the file a memorable name! We’re going to leverage the file name later.

Required items (introductory)

If you’re following the introductory guide, you will need at least one item named Helmet.tres and one item called Chestplate.tres. The helmet should have the types “Helmet” and “Gear” and the Chestplate should have the types “Chestplate” and “Gear”. The “Gear” type indicates that these are both equippable items, and the “Helmet” and “Chestplate” types indicate what equipment slots they will fit into.

I’m 90% sure .tres is short for “text resource”.

Modifying Items

Now, double click the resource. This should open its exported properties in the Inspector panel. Fill in the details however you please.

Here’s an item I made called OfficeShirt. I’m leaving the textures blank for now. The “Gear” type is the part of the inventory where the shirt will display, and the “Shirt” type is the equipment slot that it will fit into.

Make a couple items. Some equipment, some not. Save them all to files. And remember which ones you want to be visible equipment later.

In the future, this script is a great place to add information about effects your item should have, such as buffs and debuffs, enchantments, etc.

Inventory

If you’re using a plugin for your inventory system, you can integrate the item structure with whatever you’re using and skip to the next section, or you can follow me on a simple inventory implementation.

Call me data-oriented but dagnabbit I think you can do this with a Resource as well. Since resources can be saved to files, so making the inventory itself a resource will make it easier to save the player’s progress later.

inventory_item_set.gd

extends Resource
class_name InventoryItemSet

@export var items:Array[InventoryItem] = []

By employing the One True Data Structure, you have successfully finished all the resources you need to get this inventory up and running. Now lets implement that user interface.

Create an InventoryItemSet resource, add the InventoryItems you made to its list, and save it to your project. You will load it in later.

Inventory Interface (20 minutes)

Over the next two sections, we’ll be making an inventory screen that may look something like this:

And, here’s a more complex version with more equipment types and side tangents filled out:

ItemList

Godot provides a powerful built-in node type to do more or less what you want out of the box. The ItemList node contains… a list of items. Kind of like our inventory set. We’ll need a script to “manage” our inventory set: tell it when to add and remove items. Then, the item list will give the user powerful actions they can perform on the item set, reminiscent of an inventory.

One such built in feature is “activating” items. Selected items in an ItemList can be “activated” in game by pressing Enter or double-clicking. This doesn’t do anything by default, but you can hijack it as our equip/unequip action. You can also interface with the selection of the ItemList. We’ll use this to give the player feedback, based on an item’s types, on what equipment slots the item can be moved into.

Scene Setup

If you haven’t yet, create a 3D scene to hold your character and save it anywhere in your project (maybe not in the inventory folder, lol). Add a CanvasLayer node to the scene. The CanvasLayer usually describes a layer of the user interface, and you will use it to display the inventory. You can select a node in the scene tree, then right click it and click “Add New Node”, or press Ctrl+A. A popup will appear with all the different kinds of nodes you can add here and a search bar. Search “CanvasLayer” and click “Create” or press Enter.

Add an ItemList under the CanvasLayer in the same way.

Once the control is added, the central panel of the editor should appear grey, kind of like this:

If you are still looking at a 3d editor, or a script editor, press Ctrl+F1 to switch to this view.

Ctrl+F1, Ctrl+F2, and Ctrl+F3 allow you to switch between 2d, 3d, and script editing respectively at lighting speed. Try it out! Check out other cool shortcuts here.

This will be our primary items screen. I want to place it in the top-left of the screen. In the 2d editor, click on Items in the scene tree to select it. Green and orange points should appear in the editor. Click the little final smash ball in the top-center of the 2d editor. This should open a menu called “Anchor preset.” Hover over anchor presets to see their names. Click the one labelled “Top Right” to lock the ItemList to the top right of the screen.

The green “anchor” points will move proportionately to the screen in game, including changes in aspect ratio. The orange “offset” or pixel points are constant pixel offsets from their corresponding anchor points. The full implications of this are out of the scope of this guide. But, if you’re curious, create a new scene and add nodes of type Panel to it and play with their anchors and offsets. Then run the scene with F6 and change the size and aspect of the screen to see how they move.

Now, adjust the orange controls until you like where the inventory is sitting on screen.

ItemList scene setup

Rename the ItemList (F2) based on what type of item you want to appear there. For example, if you gave your equipment items the “Gear” type, name the ItemList “Gear.”

In the same Anchor Preset menu as before, set it’s Anchor Preset to “Full Rect” so it covers the whole Items node. It should appear in the 2d editor as a dark grey rectangle.

Add a Node beneath the ItemList of type Node. Name the Node “InventoryManager” (F2).
NodeLayout5

Right click the ItemList, click “Save Branch as Scene”, and save it to the inventory folder as “inventory_list_view.tscn.” Scenes don’t have to be whole levels, they can be something as simple as single interface elements, like in here. Saving the list to a scene will make it easier to add more of them to your game if you want to later. See TabContainer tangent for an example.

Optional Tangent: TabContainer

Replace Items with a node type called TabContainer. Right click Items → click “Change Type”, then search “TabContainer” and click “Change.”

TabContainer is another powerful primitive. Duplicate the ItemList beneath it by clicking it, then pressing Ctrl+D. Try changing the visibility of the lists by clicking the eyes on the right of the scene tree. TabContainer enforces that only one of its direct descendants in the scene tree will be visible at a time, and allows the player to switch between them by clicking the different tabs.

Recall some of the types you gave to your items. Maybe “Gear” or “Clothing”, “Key” or “Money”. Pick out a few types that you want to belong to different tabs of the TabContainer. Set the names of the ItemLists beneath the TabContainer to match the item types you want to end up there.

inventory_list_view.tscn scene tree:

Inventory scene tree with multiple tabs:

Opening the Inventory

Our new scene inventory_list_view.tscn will contain two scripts. Create a script in the scripts folder of your inventory folder called “inventory_list_view.gd” and set the “Inherits” field to “ItemList”. Then, create a script in the same place called “inventory_manager.gd”, set to inherit “Node”.

Editing inventory_list_view.tscn (if editing a different scene, select a scene tab in the center top of the editor, or double-click the scene in the FileSystem panel), attach inventory_list_view.gd to the ItemList by clicking and dragging the script from the filesystem to the scene tree. Then, do the same for inventory_manager.gd and the InventoryManager node.

inventory_manager.gd’s job is to hold an InventoryItemSet. More specifically the InventoryItemSet resource you saved to disk earlier. The one you don’t have to double back to do because you’re so good at following instructions. inventory_manager.gd then translate this set into signals that the ItemList can use to add and display inventory items.

## Store a list of inventory items, emit events when items are added and removed
extends Node

#To reference the same item list as this manager, copy this resource in the editor
@export var items:InventoryItemSet = InventoryItemSet.new()

func _ready():
	for i in items.items:
		item_added.emit(i)
	
signal item_added(item:InventoryItem)
signal item_removed(item:InventoryItem)

# When adding an item, the default slot is after the end of the array, or zero
#	if new slots cannot be added.
func add_item(item:InventoryItem) -> bool:
	if item.single:
		for i in items.items:
			if i == item: return false
	
	items.items.append(item)
	item_added.emit(item)
	return true

func remove_item(item:InventoryItem):
	for i in items.items.size():
		if items.items[i] == item: 
			items.items.remove_at(i)
			item_removed.emit(item)
			return true
	return false

Select InventoryManager in the scene tree and open the Node panel, you should see the item_added and item_removed signals there. Signals are basically messages or tunnels. Nodes can emit them and you can hook them up to actions on other nodes in the editor, as we’re about to do. When the game starts, the _ready method above will be called, scanning through all the items in the inventory and emitting an item_added signal for each one. Once you connect the signal up with the ItemList properly, our items should appear visually.

Double-click item_added to bring up the signal connector popup. Click ItemList and name the receiver method _on_add_item, then press “Connect”. This will create a new func in inventory_list_view.gd called _on_add_item. Don’t worry, we’ll populate it later.

Double-click item_removed, click ItemList and name the receiver method _on_remove_item. Click Connect.

Here is what the Node panel should look like when everything’s connected:

In the Inspector panel, set the items property in the inspector panel to the InventoryItemSet you saved earlier. You can do this easily by right-clicking the items property, clicking “Quick Load”, and you should see the resource you saved earlier come up.

Displaying the Inventory

inventory_list_view.gd’s job is slightly more complicated. It maintains a “view” of the inventory. It’s a little abstract to explain upfront without seeing it in action, but this design will actually allow us to reuse this script for both our inventory screen and our equipment slots!

## Grab a set of items from an inventory manager child and display them as an
## ItemList. The type of items that can be stored here are determined by the 
## name of the node, which should be found in each item's type list.

## Items can be moved between two lists as long as they both match its type in 
## this way. 
extends ItemList

#Keep track of the actual item list.
var items:Array[InventoryItem] = []
@export var max_size := 0

#Call this to pick up an item. Also used by `_on_item_activated` to test if an 
#	item can be transfered, thus the boolean return value.
func _on_add_item(item:InventoryItem) -> bool:
	
	if max_size > 0 and item_count > max_size: return false
	
	#Check type of the item against types that can be stored here.
	if !(name in item.types): return false
	
	#Check if the item cannot be added because it is single and already present
	if item.single and item in items: return false
	
	add_item(item.display_name, item.texture)
	items.append(item)
	inventory_item_added.emit(item)
	return true
#Emitted when _on_add_item succeeds. No arguments passed because this is meant 
#	to be used to effect the actual visibility of the body groups.
signal inventory_item_added(item:InventoryItem)

func _on_remove_item(item:InventoryItem) -> bool:
	for i in items.size():
		if items[i] == item:
			remove_item(i)
			items.remove_at(i)
			inventory_item_removed.emit(item)
			return true
	return false
signal inventory_item_removed(item:InventoryItem)

func _init() -> void:
	#Remove any items added through the editor.
	clear()

Add this script and run the game with F6. You should see the display names and textures of the items from your InventoryItemSet appearing in the panel. Did nothing show up? Remember that the name of the ItemList corresponds to the type of items that can be displayed here. In the main scene, make sure the name of the ItemList matches one of the types from the items you made earlier. Close the game and return to the editor with F8 to make changes.

Here’s what my inventory panel looks like in game:

For example, I made equipment items have the “Gear” type, and named my ItemList “Gear”. But there were also keys, and coins. The “Gear” items show up in the list, but the others do not. If you’re interested in organizing multiple item types, see the optional tangent on TabContainers above. Now, onto the breakdown:

extends ItemList : This script can only be attached to ItemList nodes and has all the functions available to ItemLists. You will use add_item, remove_item, clear, and deselect_all.

items : The list of items currently being displayed. _on_add_item and _on_remove_item synchronize this with the ItemList’s displayed list.

max_size : If set to something other than 0, this viewer will fail to display more than max_size items.

_on_add_item : Add an item to this ItemList. If successful, returns true and emits the inventory_item_added signal. When an equipment slot gains an item, it will emit this signal, causing the actual clothing to be displayed on your character.

_on_remove_item : Remove an item from this ItemList. If successful, returns true and emits the inventory_item_removed signal. Equipment slots will emit that signal to hide clothing items from your character.

_init : In any node script, _init runs when the Node is first created, before it actually makes its way into the scene tree. All you do here is clear any items that were added in the editor, so they don’t overlap with the items given to the ItemList by InventoryManager.

If you’re taking more than 10 or 20 minutes on any one section of this guide, step back for a second. Experiment with something you were curious about earlier, take a break, just do anything other than reading this guide.

Seriously, if you struggle with stepping back from problems like I do, time yourself. I personally have hit my head against problems for more time than I am willing to admit without taking a break, and timers just make that go away for me. Checklists too.

I gave myself 1 hour to write the this section, ran out of time, and now I’ve taken a step back to edit it! I fixed 3 spelling mistakes and added 2 jokes! Now my morale has improved, I’ve finished everything up to the next section, and I’m ready to set a new timer before continuing.

Optional Tangent: collectable items and save states (advanced)

To implement save states, remove the InventoryItemSet field and add create a singleton “SaveData,” which stores a path to the active InventoryItemSet being used for the current save file. Then replace all instances of items in this script with SaveData.items. This will allow you to have a default inventory, and a path that you can write to as the user collects items.

Also note that any node in your game can have an InventoryManager attached, and thus you can program any node to give the player items, or take them away! Try adding a node that gives the player one of the items you made earlier, and exclude that item from the initial InventoryItemSet. If you’re not seeing these changes reflected in the ItemList, it may be good to move the logic from inventory_manager.gd straight into inventory_item_set.gd, and then just directly giving the InventoryItemSet resource to your ItemList via an exported property. That way, different users of the InventoryItemSet can pass signals through it.

Equipping and Unequipping items (10 minutes)

Groups are a built-in feature of Godot that group nodes into lists. They will allow different inventory views to communicate with each other when passing items between them, without having to explicitly connect a Signal. This is great for when you’re not sure how your user interfaces and whatnot are going to be structured ahead of time, or when you just have to act on multiple nodes at once and don’t want to connect individual signals. Here, we’ll use groups to add equipping and unequipping to our inventory_list_view.gd script.

Scene setup


Add a new Control to our CanvasLayer and name it something like “Equipment” or “Gear”. The name here does not matter at a technical level, just nice to stay organized. This is where you will view our equipped items. I want to put mine below the existing inventory screen. To do this, set the anchor point to the bottom right corner, then move the orange points to fill out the shape.

For the equipment slots, add ItemLists beneath the Control. One for each type of equipment you defined when you were making items earlier.

Required Slots (Introductory)

Make one slot named “Helmet” and another named “Chestplate”. These will match the types of your required items from earlier.

Scene Setup Part 2

Give each new ItemList an inventory_list_view.gd script and make its name match the type of equipment that will fit there. Set max_items in the inspector panel to 1. This indicates that only one item should be equipped in a slot at any given time. There should also be a default ItemList property called “Auto Width”, shown as a checkbox. Make this checked if you want the names of your items to be legible while equipped.

When using the Inspector panel on multiple nodes, you can Shift+click to select multiple adjacent nodes in the scene tree. If they have the same properties, like our ItemLists all with the same script attached, the inspector panel will let you change all of their properties at once! Try to do this with max_items. Once all the scripts are attached, select all of the ItemLists you just created and change the max_items property.

It may be good to put an image in the background of this part of the screen, and to place the lists on this image in a way that visually conveys what part of the body each equipment slot covers. To do this, add a TextureRect node beneath the Control, and set its Anchor Preset to Full Rect. I just drew a stick figure in mspaint for my image, but you can get much more elaborate with yours if you like. Once finished, save the image to your project. Then select the TextureRect, right click the “Texture” field in the Inspector panel, and click Quick Load to bring up all the images in your project.

You may notice that, even if the TextureRect is anchored in a particular way, the actual image doesn’t automatically fit. If this happens, change the “Expand Mode” in the Inspector to “Ignore Size” and experiment with the “Stretch Mode” until you find a value that works for you.

Improving inventory_list_view.gd

Add this code to inventory_list_view.gd. _ready connects signals, to the rest of the functions, but also adds whatever list it is attached to to a group. This group is used in all the other functions to turn signals into effects over the whole interface.

@export var ui_group := "Inventory"

func _ready():
	
	#Register this ui component so it is activated and deactivated when
	#	necessary. 
	add_to_group(ui_group)
	
	#Selecting an item will make all of its groups `available`, indicating the 
	#	item can be moved there, and vice-versa for the other lists.
	item_selected.connect(_on_item_selected)
	
	#Changing visibility will effectively "clear" the selection, making the type
	#	available. This does not clean up all groups effected by previous
	#	selections, just enough to make a new selection.
	visibility_changed.connect(_on_clear_selection)
	#Clearing the selection... also clears the selection.
	empty_clicked.connect(_on_clear_selection.unbind(2))
	
	#When an item is activated, try to move it to another list by looking
	#	through the ui group for a valid drop candidate.
	item_activated.connect(_on_item_activated)

#Make lists available or unavailable based on if their type matches the last 
#	selected item.
func _on_item_selected(i:int):
	var item := items[i]
	
	for n in get_tree().get_nodes_in_group(ui_group):
		#print("making node ", n, " available ", should_be_available)
		n.toggle_available(n.name in item.types)

#Clear the selection, and make all lists available again.
signal selection_cleared
func _on_clear_selection():
	deselect_all()
	for n in get_tree().get_nodes_in_group(ui_group):
		n.toggle_available(true)
	selection_cleared.emit()

#Change the visibility and interactivity of this list.
@export var unavailable_modulate := Color(1.0,1.0,1.0,0.7)
func toggle_available(to:bool):
	if to:
		modulate = Color.WHITE
		mouse_filter = Control.MOUSE_FILTER_STOP
	else:
		modulate = unavailable_modulate
		mouse_filter = Control.MOUSE_FILTER_IGNORE

func _on_item_activated(i:int):
	var item := items[i]
	
	#Try to move the item to another node in the group.
	for n in get_tree().get_nodes_in_group(ui_group):
		if n == self: continue
		if n._on_add_item(item): 
			_on_remove_item(item)
			_on_clear_selection()
			return

ui_group : The group this list is added to. If you want different lists to behave independently, change this property. Otherwise it’s safe to leave it as default.

_ready : Runs when the list is loaded into the scene tree, after all of its children have run _ready and before its parent. What’s important here is that this part of the script connects ItemList’s existing signals to our custom functions. These signals are emitted as the user acts on ItemList, and connecting to them is what enables our custom behavior.

This is also a good example of how to connect signals in code, instead of through the editor like you did before. I’m pretty sure editor signals are connected right before _ready is called anyways, so this shouldn’t behave any differently. But I don’t have a source on that.

_on_item_selected : Connected during _ready, runs whenever the user’s selection changes. Looks at the selected item, and highlights all equipment slots that the item can go into. This is just a nice visual thing, for the most part.

_on_clear_selection : Connected during _ready, removes highlighting effects from the selection and tells the ItemList to deselect all of its items. It’s good to clean up these effects after a user has equipped an item to show that, after their previous selection, they are not limited in what they can pick next.

_toggle_available : Called by _on_item_selected and _on_clear_selection to change if a list appears usable. In this example, it also toggles whether the list receives input from the mouse, so it does have some functionality.

_on_item_activated : Here’s the real meat. When the user double-clicks and item or presses enter from the list, this function tries to move the item to another view in the group. Recall that this list is only a view of the inventory, so removing the item from the list, and adding it to another list in the group, can all be done without compromising the set of items that are actually in the player’s possession.

This function also leverages the true/false return value of _on_add_item from the first half of the script. To “check” if an equipment slot will accept the item, it just tries to add the item to whatever it sees, and quits either when it runs out of options or finds a willing list. Pretty cool, right?

Ok. That was a lot of code. But, that should be everything! Launch with F6. You should be able to select items, and double-click them to pass them into equipment slots. And, since the equipment slot code works the same way, you can double-click filled equipment slots to put the items back in the main list. That’s equipping and unequipping.

Oh, you’re telling me it’s not doing anything? But you worked so hard! Make sure the names of the equipment slots match the types of the items that you’re trying to put in them. Double check the type lists of the items by looking at their saved resources in the inspector.

Optional Tangent: Description box

you worked so hard to put descriptions in our item resources. Really got into the lore. And now it’s just gone? I’m brushing over this for time sake. But, if you make a RichTextLabel in your inventory screen, give it a script that connects to the selection_changed signal of every node in the ui_group, and give it an inventory manager, you have all the data you need to have a little info window on the current selected item.

Optional Tangent: Doing things right (advanced)

If you want to have more exotic inventory systems, with things like multiselect or multiple, context-aware actions, you will probably need some kind of context object to consolidate the selection of items over the whole ui_group, on top of access to the current selection in any individual list. This idea of a context object is actually how multi-selection is implemented in the Godot editor itself! To act on multiple nodes in the inspector, the editor attempts to cast the selection context to a multiselect context, then the multiselect context wraps whatever action was performed in a single action on the undo stack. Pretty cool right?

Equipping items in 3d (introductory)

Godot also has powerful tools for quickly setting up simple 3d games. These tools will greatly speed up the process of getting our equipment system over the finish line!

Quick 3d scene setup

Make a new scene- actually, make a new folder. Name the folder “Character” or something. This folder will contain all the resources for your character. So, if your character has a name, the name of this folder would be a good place to put it.

Now, inside your character folder, create a new scene (right click folder, click Create New, click Scene). Now, a setting you may not have touched much in this popup is the “Root Type”. The last option should be “Node” with an icon on the right, click the icon and you’ll enter the same search bar that you used to place nodes of a particular type earlier. Search CharacterBody3D and select it.

The “Root” of the scene is the highest node in the Scene Tree viewer. The “type” of this node will determine its behavior. In this case, our new scene will be our 3d character: their controls, physics, animations, and most importantly the appearance of their equipment. You will then add this character, this scene, to a larger scene containing a level, and see that our character can move around! CharacterBody3D is at the highest level because it will provide the physics and controls, the main inputs from the outside world for how our character should act.

If you’re new to this, try not to freak out about the idea of scenes within scenes. It’s totally normal, and thew faster you get used to it the faster you will be able to use what is possibly Godot’s most powerful feature for your own machinations. If you want to get more comfortable with it, I recommend the optional tangent about TabContainers from earlier.

Character motion

In the character folder, create a new folder called “scripts” for all the scripts our character will use. Don’t worry, these ones are relatively short.

Create a new script in the new folder. In the script creation popup make it inherit from CharacterBody3D by clicking the icon across from “Inherits” and searching for CharacterBody3D. This will give our script access to the functionality that you want to get out of the root of our character scene. But, most importantly…

Look at the “Template” field. When you set the “Inherits” field to CharacterBody3D, what happened? In Godot 4.4 (I’m not sure how long this feature’s been around), you should see text pop in saying “CharacterBody3D: Basic Movement.” This is a template for our script that will automatically make some of the movement basics for us. And, honestly, even though I myself am capable of making my own character motion scripts, I use this feature all the time! It’s not just good for prototyping, it’s a simple, well made script in its own right. In fact, you don’t need to modify it at all in this section! (Though, if you know about input actions I would recommend trying to switch out arrow keys for WASD controls, since this is recommended by the script’s comments) Click create and attach the script to the CharacterBody3D in your character scene.

Now, there are three nodes you have to add to our character scene before it will work: A CollisionShape3D for the CharacterBody3D to touch with, a Camera3D for the player to see with, and a MeshInstance for the player to look at.

Add a CollisionShape3D under the CharacterBody3D. Select it, then right click the “Shape” field in the Inspector and select “New CapsuleShape3D”. If the 3d editor is not open in the center panel, open it with Ctrl+F2. Set the shape’s height to 2 and radius to 0.5. Then, under the “Transform” panel, change the position to x: 0.0, y: 1.0, z: 0.0.

You can think of the y position as the “vertical position” of the CollisionShape3D. The shape’s height is 2, but, to place the player nicely on the ground, you want the bottom of the capsule to touch position x: 0.0, y: 0.0, z: 0.0. i.e. “no left/right offset, no up/down offset, and no backward/forward offset”.

Now, add a MeshInstance3D and set it’s mesh field in the inspector to CapsuleMesh3D. Change the capsule’s properties to match the collision shape and move the MeshInstance3D to y: 1.0 to match the collision shape’s position.

Finally, add a Camera3D. Use the 3D editor to move it to a nice position. I want my camera to be “over the shoulder”, so I move it off to the side. The inspector panel for the camera shows a preview of what the camera sees. Make sure the capsule mesh is in frame.

Level scene

Create a new 3d scene (Select “Root Type” as “3d scene”) in your project. Maybe have a folder called “level” or something. Add a node of type CSGBox. Set it’s dimensions to be wide and flat, with high values in the x and z dimensions and a small value in the y dimension. Then, check “Use Collision” in Inspector.

CSG stands for “Constructive Solid Geometry” It’s a good tool for blocking out and prototyping levels since you can quickly create simple shapes, but I don’t recommend including them in your final project, since they are less performant. The main advantage here is that, when “Use Collision” is checked, they merge the functionality of a collision shape and a mesh instance into one: both displaying a shape on screen and giving the player something to collide with.

Then, click and drag the character scene from your file system, and over the CSGBox, your character’s capsule should appear over the csg box. End the drag to place the character in your scene tree.

Finally, you need some lights. Click the 3 dots in the top of the 3d editor to reveal the sky “Environment” and “Sun” being used by the editor to add light to your scene. These let us see what we’re doing in the editor, but they won’t appear in-game until you click “Add Sun to Scene” and “Add Environment to Scene”. These will add a DirectionalLight3D node and a WorldEnvironment node to our scene tree. How these work is out of the scope of the guide. Just know they add light, and maybe experiment a little with their properties in the inspector.

Now, launch your scene with F6. You should be able to move a character with the arrow keys (or WASD if you set it up), and jump with space, all thanks to that template script from earlier. If all works well, close the session with F8 to return to the editor.

If you right click the level scene in your file system, you can click “Set as main scene”. Then, you can launch it with F5, regardless of what scene you are currently editing.

Camera motion

Last step, camera motion. Open your character scene and add a Node3D. Name it “Eyes”, and place it towards the top of your character. Then click and drag the Camera node onto Eyes in the scene tree.

Create a new script in the scripts folder of your character’s folder. Name it eyes.gd.

extends Node3D

func _unhandled_input(event:InputEvent):
	if event is InputEventMouseMotion:
		rotation.x -= event.relative.y * 0.002
		get_parent().rotation.y -= event.relative.x * 0.002

This script will rotate the character, and the eyes, to look left, right, up and down depending on mouse motion. The _unhandled_input stack is out of the scope of this guide. Just know that event.relative contains mouse motion in pixels, and rotation.x is the vertical rotation of the eyes and get_parent().rotation.y is the horizontal rotation of the character. Effecting these variables with mouse motion means that moving the mouse left and right turns the whole character, while moving it up and down only controls the eyes. Run the level scene again to test this.

If you can’t see your character, the camera may be inside of it! Move the camera. Note that since it’s the “Eyes” node that rotates the camera will rotate around it.

Importing models (2 minutes)

Alright, now lets model some armor. I’ve provided some free-to-use simple armor models for testing purposes below. Each comes in a .blend file. Add these to your project in your character folder. This may freeze the editor or cause a loading bar to appear. You can double click the .blend files in the FileSystem panel to view their contents.

Helmet file: https://github.com/Roboticy3/body-groups/blob/main/introductory/capsule_man/Helmet.blend

Chestplate file: https://github.com/Roboticy3/body-groups/blob/main/introductory/capsule_man/Chestplate.blend

You don’t actually have to know any Blender to use these.

In GitHub, press the three dots in the top right to get the option to download the individual files.

Showing/hiding models based on equipment (10 minutes)

Open the character scene. Add 2 Node3Ds and name them “HelmetLoader” and “ChestplateLoader”. Create a new script in the character’s scripts folder called “armor_loader.gd” and attach it to both nodes.

extends Node

@export var load_list:Array[PackedScene] = []
var load_map:Dictionary[StringName, PackedScene] = {}

@export var equip_group := "Equip"

func _ready():
	add_to_group(equip_group)
	
	#sort the load_list into a mapping for faster access.
	for l in load_list:
		var key := get_saved_resource_name(l)
		load_map[key] = l

#cut off the .tres or .blend at the end of the file name
var re := RegEx.create_from_string(r"\..*")
func get_saved_resource_name(r:Resource) -> StringName:
	return re.sub(r.resource_path.get_file(), "")

func _on_equip(item:InventoryItem):
	
	var i_name := get_saved_resource_name(item)
	var l = load_map.get(i_name)
	if l is PackedScene:
		var n:Node = l.instantiate()
		n.name = i_name
		add_child(n)

func _on_unequip(item:InventoryItem):
	var i_name := get_saved_resource_name(item)
	for c in get_children():
		if c.name == i_name:
			remove_child(c)

This script will load and unload armor pieces as they are equipped in the inventory.

The difference between loading and unloading and revealing and hiding the items is that the armor parts are simply not in memory at all when they are unequipped. This is cool because you can make custom scenes to represent the armor, then add as many things as you like to it. Particle effects, screen effects, damage boxes. The sky is the limit. Of course, if the scene gets to complicated, the game may lag when equipping it.

Speaking of inventory, lets grab the inventory from the first scene you worked on. Open the scene containing the inventory. Select the CanvasLayer. Press Ctrl+C to copy. Now, open the character scene. Select the root CharacterBody3D. Press Ctrl+V to paste. This may also freeze the editor for a moment while it resolves all the properties of the pasted nodes.

Unfortunately, our equipment slots are not quite powerful enough as they are to signal the armor loaders. Ok, technically they can do it with some modifications, but that’s not the responsibility of inventory_list_view.gd, the script attached to the equipment slots. It’s job is just to display and move items around the inventory. To actually manage the equip action, we’re going to add a Node beneath our equip slots with a new script, and connect the equip slot signals to those:

Bare with me

First, delete one of the two equipment slots. Bare with me. Right click the remaining slot and select “Save Branch as Scene”, and save the scene to your inventory folder as equipment_slot.tscn. This will create a tiny scene containing only the equipment slot node.

Now, and make sure you’re still editing the character scene, remake the missing equipment slot. Duplicate the original with Ctrl+D, move the duplicate using the 2d editor back into place, and rename it with F2 to match the old slot. Remember that the renaming is crucial for the items to know what slots they can go into.

equipment_slot.tscn:

Improving equipment slots

You can now edit equipment_slot.tscn, and your changes will be reflected across both equipment slots. If you choose to add any more equipment slots later, they will also benefit from any changes you make here.

Editing equipment_slot.tscn, add a node called “ArmorNotifier”. Create a script in character/scripts called armor_notifier.gd.

extends Node

@export var equip_group := "Equip"

func equip(item:InventoryItem):
	for n in get_tree().get_nodes_in_group(equip_group):
		n._on_add_item(item)

func unequip(item:InventoryItem):
	for n in get_tree().get_nodes_in_group(equip_group):
		n._on_remove_item(item)

Connect the inventory_item_added signal from the parent ItemList to equip, and the inventory_item_removed signal to unequip. Then, if equip_group in this script has the same default value as equip_group in armor_loader.gd, moving an item to the equipment slot should trigger the corresponding armor to be loaded, and vice-versa. Moving an item out of the equipment slot should trigger the corresponding armor to not be loaded.

This works by looking through all members of the equip_group group. armor_loader.gd adds itself to this group, so that includes the HelmetLoader and ChestplateLoader nodes. It then tries to give each of them whatever item the equipment slot receives. Test the level with F6, or F5 if you set it as the main scene. You should be able to move around, see your inventory, equip and unequip items, and equipping and unequipping items should be reflected on your character.

In theory, this is less performant than a direct line from each equipment slot to their corresponding loader, and you could connect the signals by hand, but this way the inventory system is much easier to expand with more equipment slots and loaders. All you have to do is add them with the right scripts and everything should work automatically.

Optional Tangent: Power to the artist! (assumes basic Blender knowledge)

It’s less than ideal that, in these Blender files, you can see the armor, but can’t see the character. It would be better if the character as a whole had 1 blender file. That way the character artist could more easily work with the armor, and give the character a more advanced model.

Make a folder in your character folder called “External” and add a text file to it call “.gdignore”. Godot will now not try to import from this folder or sub-folders. Merge the armor pieces into one blender file by creating a new blender file in the External folder, then copying the objects from each of the armor blender files. (Yes you can copy object between different instances of Blender.) Delete the original blender files.

To model a capsule, Shift+A → Mesh → UV sphere, set its radius in the popup on the bottom left of the viewport to 0.5, and the number of rings to an odd number. Then Tab to enter edit mode. A to select all, G, Z, 0.5 to move everything up to the ground plane. Enter top view by clicking the blue “z” in the top right of the viewport or pressing Numpad 7. Then, drag to select all the upward-facing faces of the sphere. Then G, Z, 1 to move them up 1 unit. The total height of the object should now be 2, matching the proportions of your original capsule.

Now you have a capsule that you can see your armor sitting on in Blender. Convenient!

Select one of the armor pieces. Click File in the way top left of the window, Export → Export as gltf/glb. glb is a scene format that Godot understands natively, much like .blend. Export the file to the character folder, and set the export settings to Include only selected objects. Do the same for the other armor piece.

Then, in Godot, set the scenes loaded by the HelmetLoader and ChestplateLoader to these new .glb files. Everything should work as expected.

You could even change the character model. Select the capsule you created in Blender and export it as .glb to the character folder. Then delete the original capsule and replace it with the imported glb. A couple tutorials on top of that and you could even add animations.

I would recommend putting objects that export to different files in Blender in separate collections. Then, set the export preset to include visible object as opposed to selected. So, when you’re exporting, you just have to check/uncheck the desired collections, and you’re free to add more objects to an export if you please. This will be explored more thoroughly in the advanced section below.

And of course, keep in mind that Blender won’t remember your custom export settings between sessions unless you save it as an operator preset. Blender is a little more quirky than Godot about saving your progress.

Part 3.1: Equipping items in 3d (advanced)

This part is probably, definitely overkill. With this system, you can create- let me check -infinite layers of clothing, that block other layers of clothing in- put this in your abacus -infinitely many ways. We’ll go over simplifications of different parts of this as I go, but let’s start by showing off what it empowers the final product to do.

This is Geno. He ran away from his job.

I was going to make an rpg centering on him and his friends until I realized how hard it is to make rpgs. After about 200 hours of working on dialogue systems, rpg mechanics, a poorly justified multiplayer system, and this guide, I have very little to show for it (recall the habit of hitting my head against problems without taking a step back to reflect).

But… look at the flap on his shirt! It gets hidden when he puts pants on. Isn’t that remarkable? Basically, it shows that the shirt is broken up into at least 2 parts. The legs part, and the torso part. The legs part is occluded by the pants. Not by the gpu, not by the programmer (well, you are going to write code to establish this relation, but it won’t just be static connections), but by the artist. Whoever makes your character models, whether it’s yourself or somebody else, they can control what clothing shows or hides other clothing completely from their 3d art suite of choice.

Coat on:


Coat off and shirt on:

Coat and shirt sleeves forcefully enabled using the remote debugger:

Here, you can see the same effect with the coat and the shirt sleeves. The shirt sleeves are actually much baggier than the coat sleeves. But the coat hides the shirt sleeves.

What, doesn’t sound like much? Well, uh, let’s you do it pal! Seriously! Finish this guide and implement the system yourself! Here’s the plan:

First, we’re going to set up a root motion controller character in Godot. This is just so you can see our character moving and make sure clothing parts aren’t clipping into each other. This is also a remarkably esoteric task when using animations from Mixamo, which do not have a game-ready root bone by default. The addons required for this are not actively supported, and I encourage you to try to come up with custom root motion solutions, even though I failed to do that.

Next, you will establish relations between the clothing items based on a naming scheme you give them in Blender: Each object’s name will be a comma separated list of groups it belongs to, and if it belongs to a group starting with “-” (“-Shirt”, “-Legs” etc), showing this object will hide all the objects in that group.

If you’re not doing the next section, you at least need a .glb file in your project with a character and a skeleton in it. If you’re not sure how to do that, please read at least the “Importing into Godot” section from the Mega Optional Tangent below.

Mega Optional Tangent: “Quick” and Dirty Mixamo Setup with Root Motion

Just a heads up that animations are always super finicky. I’m really sorry if this doesn’t work for you, but I simply can’t account for all the things that may go wrong on another person’s setup. If you’re really committed to following this section, just try to be patient, and don’t be scared of coming up with your own solutions to certain parts.

Lets start our Blender project with an armature from mixamo. Make a folder in your Godot project for your character, if you haven’t already, and make a subfolder called “External”. To mark this folder as something Godot shouldn’t care about, add a blank file to it called “.gdignore”. If you save our Blender project for the character here, Godot won’t try to import it, but you can still include it in our project for version tracking, which I believe is the main motive for the .gdignore functionality in the first place.

Open Blender and create a new file in this External folder. Then, go to Mixamo and download one of their locomotion packs to the External folder. Install a Mixamo root motion addon for blender. These addons combine the motion of the hip bone from Mixamo rigs, the bounding boxes of the rigs at different points in time, and other data to build a “game ready” root motion bone.

Root motion will allow the animations to control how our character moves, fitting the theme of our clothing system of deferring power over the character to the artist. Typically, the “root motion bone” is on the ground plane and only rotate on the vertical axis. These bones describe the “root” of the character. Godot can then “consume” the motion, then you can reapply it as physical motion of a character through the game world.
The primary weakness of the mixamo root motion addons I found were that neither allowed for proper rotation on the vertical axis, likely since it’s hard to source this from the hip bone alone. This limits our character controller to strafe movement.

Once you have the addon installed, use it to import the locomotion pack you downloaded in batch. All the animations should have root motion baked into them properly. The addon should support importing all the animations as actions on one armature instead of multiple armatures. Make sure this is the case.

Optional Optional Tangent: Retargetting Mixamo animations with root motion

If you want to use the same animations on multiple characters, without ending up with multiple copies of the same animation file, import these animations into a separate Blender project using the same addon, then save that blender file to your Godot project in a place where Godot will import it. Then, to make animations from this “shared” rig work with individual characters, go to the import settings of the blend file with the shared animations, select the Skeleton3D in the import panel and create a new BoneMap with a SkeletonProfileHumanoid. If all goes well, all the points on the skeleton profile should appear green. This means they both have a valid match and are attached to the rest of the bone hierarchy. Save the BoneMap resource as a file, and apply it to whatever characters you end up importing in the same way. Finally, export all the animations from the shared blend file as files and add them to an animation library.

Now, you can load the animation library into any animation player or animation tree you use for any of the characters that have that BoneMap applied in their import settings.

Importing into Godot

Design your character. Depending on your blender experience, this can be as simple as parenting objects to the armature:

Separate Objects/Retro style: Shift+A to add a mesh. Move it into place on the armature/edit it to your liking. Then select it, shift-click the armature, Ctrl+Tab to enter Pose Mode, click to select a bone, then Ctrl+P to parent the object, and select “parent to bone”. Repeat this process for all the objects you want to create. Make sure to have some objects that partially cover other objects up, such that you want them to fully cover them in-game. This will add an easy way to check if the armor system is working later.

Or, you can sculpt your character as a single solid mesh (clothing items should still be as separate meshes), then parent it to the armature with weights:

Scupled body and clothes/Modern style: Sculpt your mesh. Click it and shift-click the armature, Ctrl+P, the click “parent with automatic weights”. Click the armature. Go into Pose Mode with Ctrl+Tab and test that the weights are good. If they aren’t, click the mesh, press Ctrl+Tab, then 7 to enter Weight Paint mode. Go through the bones and look for anomalies in their influence on the mesh and fix them with the weight brushes. You will run into partial coverage relations more often, but it helps to make a clothing item that is intended to cover another clothing item or body part that is actually larger. Since then, if the system doesn’t work, you will see obvious clipping on that part of your character.

Put the block-out character and their prototype clothing items into a collection, and the armature into another collection. You can do this by going to the Outliner panel in Blender (in the top left by default), right clicking the top collection, clicking “new collection”, and moving desired objects into it by clicking and dragging their names in the Outliner.

Once everything is ready, go to File → Export → Export as glb/gltf. In the popup, check “Visible Objects”. You probably also want to open the export properties Data → Meshes and check “Apply Modifiers”.

you may make changes to the character later, but the armature object should stay the same. By splitting them into different collections, you can disable our original character’s collection in favor of a new one, by hiding the original collection and showing another, while keeping the armature. Including visible objects will make sure only the objects on screen find their way into the export, which I find pretty intuitive by Blender standards.

One more thing. In my experience working with animations, I really think animations in Godot should loop by default. But they don’t! Animation actions from Blender will only loop if you rename them to end in “-loop”. Luckily, this is easy to do. Press Ctrl+F2 to bring up the batch rename menu. Set the object type to Actions, set the selection to All, set the rename type to Set Name and click “Suffix”, then set the rename to “-loop”, this will add the -loop suffix to all actions. You can then re-export the .glb to get looping animations.

Quick character controller

Now that you have a .glb in Godot. It should import as a scene. Double click it to view the export settings. You should see all the animations without the -loop suffix in their name. And if you click on them, you should see their loop type is set to “Linear.”

Create a new scene in your character folder to contain the .glb and other behaviors you create. Make the scene root a CharacterBody3D and add the .glb as a child.

Give the CharacterBody3D a CollisionShape and a copy of the inventory ui. Then create a 3d scene with a source of light and some ground collision (if confused, refer to the introductory section called “Level Scene”), set it as your main scene and add the character to it. Also, add a camera. For this demo, a static camera is more than enough.

Now, here’s the plan:

Set up an AnimationTree to take input from the player and control animations accordingly.

Then, read root motion back from the AnimationTree into this script to apply motion to the player.

AnimationTree

First, check that the root motion is working. Right click the imported Blender character and check “Editable Children”. This should reveal an AnimationPlayer. Select any animation that moves the character and see that the character moves through the world. (make sure to have the 3d editor open to see the character move).

However, if you set the Root Motion Track property of the AnimationPlayer in the inspector:

Then set the track to the “Root” bone of the armature:

The character should now play animations without moving. Note that, when the walking or running animations loop, the character no longer teleports. The loop should appear seamless.

If this is the case, add an AnimationTree to your character scene. Set the “Anim Player” field to the AnimationPlayer. Then set the Root Motion Track the same way. Finally, set the Tree Root property to a new AnimationNodeStateMachine. An “AnimationTree” tab should open in the bottom dock. You can open it manually by double-clicking the AnimationNodeStateMachine resource.

In the AnimationNodeStateMachine, add a node of type BlendSpace2D using the add tool. Use the link tool to connect the BlendSpace2D to the Start node.


To the BlendSpace2D, Add a walking, running, and strafing animations from the locomotion pack by selecting the add animations tool, then clicking in the blend space. For walking backwards, add a walking node, but set it to play backwards by clicking on it with the select/edit tool, which open’s its properties in the inspector.

The blend space is basically an input for the player. I laid mine out so that “up” (1.0 on the y axis) is forward, “left” (-1.0 on the x axis) is left, right is right, and down is back. Some more dogmatic Godot users may point out that -1 on the y axis is typically forward in other parts of the engine. So it’s totally sensible to flip this shape around.

Make sure to use the preview different positions in the AnimationTree by clicking the preview position button, then clicking in the blend space and seeing how the character moves in the 3d editor. Make sure the looping is seamless and the character moves in place, as this indicates the root motion is being consumed properly.

Controlling animations

Now, we’re ready to send inputs to the animation tree. Add a new Node to the character scene and name it Controller. Controller will record input from the player, and emit a Signal when the input changes. You can hook up this signal to the AnimationTree in a way that changes the position in the blend space you created.

Bit of a mouthful, I know. Here’s the script to attach to our controller:


extends Node

@export var target_property := ""

signal direction_changed(property:StringName, to:Vector2)

var interpolated_input := Vector2.ZERO
const INTERPOLATION_SPEED := 5.0
#emit local_target_direction_changed with the current input direction
func _process(delta:float):
	interpolated_input = interpolated_input.move_toward(
		Input.get_vector("m_left","m_right","m_down","m_up"),
		delta * INTERPOLATION_SPEED
	)
	
	direction_changed.emit(target_property, interpolated_input)

Replace “m_left”, “m_right” etc. with whatever your preferred WASD actions are, or replace them with “ui_left”, “ui_right” etc if you have no idea what I just said to you.

As you can see, the script is actually pretty short. You have a target property to set wherever you send the input, and you compute the input each frame. The INTERPOLATION_SPEED constant and it’s logic gives us something like Unity’s “GetAxis” method (where Godot’s Input is more like Unity’s “GetAxisRaw”), where the player input is smoothed over, mostly to smooth out the experience for keyboard users.

I recently played a Metroidvania Month entry with a 3d character that, on WASD, very clearly did not use this smoothing. That gave the animation a fun, snappy vibe in it’s own right, so you could simplify this script even further: just passing the raw input to the signal, maybe even on an _input callback instead of a _process callback, if you’re ok with that.

Once this script is attached to the Controller in the editor, connect the direction_changed signal to the set method on the AnimationTree. Then, with the AnimationTree selected, look in the inspector panel. You should see a drop down box called “Parameters”. This contains all the parameters of the AnimationNodeStateMachine you created. Open it and you should see another drop down with the same name as the blend space you created earlier. Mine is called “Strafe”. Open this drop down and you should see a property storing a 2d position. This is the position of the blend space that determines the animation. Copy the path to this property:

Then, select the Controller again. The script exports a field called target_property. Select it’s value in the inspector and Ctrl+V to paste the path you copied from the AnimationTree.

To review, the direction_changed signal will call the set method on the animation tree with the arguments target_property and interpolated_input, this will set the position of your blend space to whatever interpolated_input is. This will then trigger the animations of the character to change according to how you set up the blend space.

Launch the main scene to test this. Use WASD to move if you set up the actions, or arrow keys if you used the ui_ actions. The character’s animation should change as you move around, but it may not match how the character is actually moving in the world. Maybe they’re not moving at all. Time to fix that.

Applying Root Motion

Add a script to the CharacterBody3D using the default CharacterBody3D template provided by Godot. This will appear if you try to create a script on a CharacterBody3D in the scene tree, or create a script inheriting from CharacterBody3D in the filesystem. The template should look like this:

extends CharacterBody3D

const SPEED = 5.0
const JUMP_VELOCITY = 4.5

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta
	
	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		velocity.x = direction.x * SPEED
		velocity.z = direction.z * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)
		velocity.z = move_toward(velocity.z, 0, SPEED)

	move_and_slide()

We’re going to gut this thing like a fish! Check this out:

## CharacterBody3D controlled exclusively by the root motion track of an
## AnimationMixer, which is generally expected to use physics processing to sync
## properly with this script.

extends CharacterBody3D

@export_node_path("AnimationMixer") var mixer_path := NodePath("")
@onready var mixer:AnimationMixer = get_node(mixer_path)

#velocity due to acceleration stored separately so it can be computed into the 
#	actual velocity alongside root motion.
var sub_velocity := Vector3.ZERO
func _physics_process(delta: float) -> void:
	
	if is_on_floor():
		#flatten acceleration velocity to the ground
		sub_velocity -= sub_velocity.project(get_gravity())
	else:
		sub_velocity += get_gravity() * delta
	
	velocity = get_root_motion_velocity(delta) + sub_velocity
	move_and_slide()
	
	quaternion *= mixer.get_root_motion_rotation()

func get_root_motion_velocity(delta:float) -> Vector3:
		return mixer.get_root_motion_position() / delta

You may still see the bones of our original movement template. If not.. again I said we’d gut it like a fish. Who leaves the bones in a fish?

Script Explanation

mixer_path : a path to another Node relative to the CharacterBody3D in the scene tree. You can set it from the editor by clicking on the property in the inspector. Then, a view of the scene tree will appear with valid nodes highlighted.

mixer : An AnimationMixer. AnimationMixer is the base class for AnimationTree and AnimationPlayer. So you can use this script with either if you want. The mixer will tell us exactly how much root motion has been consumed.

sub_velocity : velocity due to external forces is separate from velocity due to root motion. We have the velocity due to root motion in the mixer, but need to store this velocity separately.

_physics_process : moves and rotates the character based on root motion. This is done in physics process so the motions are all caught by the physics engine. Works best if the “Callback” field of the AnimationTree is set to “Physics”.

_get_root_motion_velocity(delta:float) : A small wrapper so I don’t have to write this awkward “divide by delta” line more than once. The mixer doesn’t actually store velocity, it stores a change in position. And since velocity is position over time, we need to divide by delta to get the velocity.

The final value given to velocity on each physics step is always _get_root_motion_velocity(delta) + sub_velocity, creating a character that moves on root motion and is effected by external forces. In this case, the only external force implemented is gravity.

Testing

Attach the script, set the mixer_path to the AnimationTree, and run the main scene. The character should move according to how the root bone moved in Blender.

Holes (sculpted, solid characters only)

This section only applies if you sculpted a character as a single mesh. You can skip it if you haven’t, or if you find a way on your own to break your character into parts without compromising their design. The latter is a great step to take if you’re going for retro graphics, since older hardware often didn’t have support for meshes that actually bend with on a skeleton, and broke up their characters into solid objects to make up for it.

That being said, how many holes does a human have?

Here’s Geno’s original model in Blender:

He has zero holes. No, not even there. He’s not quite manifold, if you know what I mean, but he’s totally waterproof. There’s no holes in the base mesh. This is sub-optimal for gaming.

After all, I don’t want to hide the entire man if he just puts on a shirt. I only want to hide the torso and arms. To do that, we’re going to need to get out our buzzsaw and make some cuts.

Gah! Now he looks like a robot! This is terrible! (I’ve actually already fixed this, but I’m displaying the model in solid view, which always draws edges between objects regardless of if seams would actually appear there in-game.)

Take your character’s mesh, make a copy of them and add it to a new collection. Do this for clothing items as well. DO NOT BREAK UP THE MESH YET.

The Data Transfer Modifier

This section is brought to you by a 2022 forum post on Blender stackexchange that works way better than it has any right to.

For each of the copies, open the modifier stack in the Properties panel (typically in the lower right of the screen, there’s a vertical list of icons, the modifier stack is the one that looks like a wrench). Add a modifier called “Data Transfer” and give it these settings:

Of course, replace “Body.Main.002” with whatever the original copy of the mesh was. Also, click “Generate Data Layers”. Make this modifier come before the Armature modifier in the stack by clicking and dragging the 8 dots in the top left of the modifier or clicking the drop down and clicking “Move to Top”

Now, go to the original mesh. In the same panel, it should have an “Armature” modifier. Remove or disable this modifier.

That’s more like it. If you are still seeing seams in your character, it might be because you are viewing the scene in “solid” mode, which displays edges of objects no matter what. Change the viewport to Material Preview or Render by pressing Z and selecting an option from the radial menu to see the smoothed out character.

What you just did was target the copied mesh’s normals, the data that will determine how it is lit, and it’s vertex weights, the data that will determine how it bends with the mesh, to copy directly from the original. Now, when you split this object up, its modifiers will also be duplicated.

Normally, when you split and object in two, any chance at smooth normals along the seams are destroyed, and you may end up recalculating the weights of the separate meshes, which might cause holes to appear between them when the character’s rig bends. Now, however, any meshes cut from the copies will have exactly the normals and weights of the original, in the exact same spots, as long as you don’t modify the cut parts too much. If you’re not sure about your character’s base mesh, it might be good to put off setting all of this up, since changing the base mesh in a way that propagates down is really hard. In exchange, the split objects will retain any smooth lighting and articulation from the base mesh.

If you want the same thing to happen for clothing items, repeat the process for clothing items. If the lighting on a copy or split object suddenly looks weird, make sure to remove the armature modifier from the original.

Once ready, export over the old glb with the new collection of split objects, and the collection containing the rig enabled, instead of the rig and the original meshes.

Occlusion

To hide and reveal these clothing items, it makes sense to use Godot’s groups, since the items may be broken into multiple parts.

Naming scheme

Now that you have your character’s body parts and clothing parts as separate objects in Blender, we need a way for the artist to tell Godot that some parts cover other parts. The naming scheme I came up with was:

Each object’s name is a comma-separated list of groups it should be in. For example, the torso of Geno’s shirt is called:

OfficeShirt,Shirt,-Torso

OfficeShirt is the filename of the item corresponding to this object in game. Shirt is a group that I added in to be occluded by higher layers of clothing, though nothing actually covers it currently. -Torso indicates that, if this node is shown, it should cover the Torso group.

Objects can occlude multiple layers:

WornPants,Pants,-Legs,-Pelvis

And they can also belong to multiple groups that are occluded by other objects, but I don’t have any examples of that in my project.

Rename all the parts of your character’s body and clothing items to fit this naming scheme, then re-export the Blender project.

Sometimes re-importing the .glb can erroneously clear import setting in Godot like the BoneMap (which is why we saved it to a file, so it’s easy to add back in). It’s also a common mistake to not show and hide the correct collections while exporting, or to not save the export preset in Blender. Here is a checklist:

  • Did I select the right collections on exporting?
  • Did I use the right export preset?
  • Did Godot import the file with the right settings?

Adding objects to groups

You could do this with a tool script: Attach a script to the skeleton that iterates through its children and adds them to the correct groups (with the persistent argument set to true). But that would give you another thing to do every time you update the import with new objects. Honestly, it’s probably fine, but it’s not the solution I went for.

After all, adding a node to a group in the editor is basically just an instruction for Godot to add the node to a group at runtime. So it’s not much less performant* to have a script parse them at runtime, instead.

*yes, I’m aware that gdscript is slower than C++. But, within a reasonable number of parts, and by reasonable I mean anything less than, like, 3000, it’s going to be a negligible difference on modern hardware. If this bothers you enough, you can just use the tool script anyways.

There’s also an issue of, if there are multiple characters on screen, how to make the groups their body parts are added to “local” to their instance. There are a couple of ways to do this, but I chose to implement a custom “NodeGroupSet” resource, that replicates the properties of the SceneTree’s groups, but in resource form, allowing it to be shared around only with the nodes who need to use it.

Godot, at least until I figure out the source code, does not support scene-local groups or something similar. I may look like it does, it may feel like it should, but that simply isn’t the case. I personally think this is a great candidate for a GDExtension plugin. Since it’s a feature that doesn’t quite fit into the design of the engine, but would be useful for a lot of people.

The custom solution involves two scripts. Make a new folder in the project’s scripts folder called “custom_local_groups” for the following:

node_group.gd:

## Local, nonserializable shared node group.

## Not being serializable is a tradeoff for working with Node references 
## directly.

extends Resource
class_name NodeGroup

var nodes:Array[Node] = []

func add_node(n:Node):
	nodes.push_back(n)
	n.tree_exiting.connect(
		remove_node.bind(n)
	)

func remove_node(n:Node):
	for i in nodes.size():
		if nodes[i] == n: 
			nodes.remove_at(i)

node_group_set.gd:

## Approximate the experience of if Godot had local groups.

extends Resource
class_name NodeGroupSet

#map between groups and nodes
@export var groups:Dictionary[StringName,NodeGroup]

#map between nodes and groups
var total:Dictionary[Node,Array]

func add_node_to_group(n:Node, g:StringName) -> bool:
	var group = groups.get(g)
	var found = true
	if !(group is NodeGroup):
		var new_group := NodeGroup.new()
		print("created group ", new_group)
		groups[g] = new_group
		group = new_group
		found = false
	
	group.add_node(n)
	
	if total.has(n):
		total[n].append(g)
	else:
		total[n] = [g]
	return found

func get_nodes_in_group(g:StringName) -> Array[Node]:
	if !groups.has(g): return []
	return groups[g].nodes

func get_groups() -> Array[StringName]:
	return groups.keys()

func get_nodes():
	return total.keys()

func get_groups_of(n:Node):
	return total[n]

signal set_group_visible(g:StringName, v:bool)
func signal_set_group_visible(g:StringName, v:bool) -> void:
	set_group_visible.emit(g,v)

NodeGroupSet is now a resource we can attach to any script that wants to access a set of groups being used locally. Great.

Note the signal_set_group_visible method. This will let us send a signal through the resource, from one user (the inventory) to another (the script that will actually show and hide our nodes) that one group should be hidden or revealed.

We also cache the groups that each node is a member of here, replicating the behavior of Node’s get_groups method. This is crucial for determining which groups affect which other groups.

Ok! Let’s write that script and close out the tutorial once and for all.

Part 3.2: Watch for Rolling Cloths in 0 x _process calls

Now, you’re probably wondering what could be so difficult about occluding clothing as described before. After all, I did spend 6 hours figuring out the code for this section. But to answer that, we need to talk about parallel univer- sorry, directed acyclic graphs.

For the uninitiated, a graph is a set of nodes and connections between nodes. Like this:

The connections are called “edges”. A list of unique, adjacent edges is called a “trail.” Example of a trail:

A graph can also have a “direction” applied on it. The direction transforms an edge from a line into an arrow, giving it a direction. These are called “directed” graphs.

A “cycle” is a trail that starts and ends at the same node. On a directed graph, the cycle must follow the direction of the edges. Below, the blue path is a cycle in the un-directed graph, but not in the directed graph, because one edge goes clockwise while the others go counterclockwise. The red path, on the other hand, is a cycle in the directed graph because all the edges move clockwise.

A directed, acyclic graph is a directed graph with no directed cycles. See that you cannot draw a directed cycle in the graph below, making it acyclic:

Sorry, that’s a lot. But directed acyclic graphs, or DAGs, are an incredibly useful structure in programming, especially in game design.

Think of the arrows as signals, or just descriptions, of a relationship between two objects: If some node A does this, B does that. Note that there is a direction from A to B. Critically, as long as there are no cycles, these relationships will never cause an infinite loop or undefined behavior.

Say a Sprite A will emit a signal when it changes color. This signal is connected to a sprite B that will change position, emitting a signal connected to a sprite C that will rotate. If the graph were cyclic, C might have a signal connected to A to tell it to change color again. So, if you told A to change color, the game would freeze because the computer would see:

Change A's color
emit signal
Change B's position
emit signal
Change C's scale
emit signal
Change A's color
.
.
.

The program would just do this forever. Sometimes cycles are useful. For example, maybe you only process one “step” of a network of relationships each frame. For example, if these signals were marked as “deferred” in the editor, they would not freeze the game.

Seriously, if you’re new to what I’m talking about, try this! I know it seems a little off topic, but I really want to hammer in how common DAGs are in game development.
First, create the system that freezes the game as described above by connecting signals between 3 Sprite2Ds. Give them scripts with methods to make a simple changes to their color, position and scale (flipping color between two values, adding to position, and adding to scale are the best for this example). Then, mark each signal as deferred by clicking “Advanced” in the Signal connection window and checking the new box called “call deferred”. The game will no longer freeze because each step is being processed on a new tick.

We’re going to want an acyclic relation between our clothing/equipment groups, as well.

One group contains a list of nodes, which each may cover a list of groups. In that way we have a set of “arrows” pointing from one group to others, describing the coverage.

Putting it all together

Thinking of our clothing system as a graph of clothing groups also raises some interesting questions. If a clothing item A covered a clothing item B, and B covered a clothing item C, should equipping A hide C? I say yes. But this also means we have to be careful about cycles. Because if A covers B, and B covers A, then A covers.. itself? Equipping it would then do nothing, or crash the game, or some other nonsense if you used deferred calls.

I think I’ve done enough explaining to walk through the body groups controller. I’m rewriting this section after opening up my own project a couple weeks after first posting this because my old code didn’t work. The new code is up on the github. It’s less than half as long! It should be a little easier to understand at a slight performance cost, which we will address later.

Create a new node in the player scene called “BodyGroupsController” and a new script in the “body_groups” folder called “body_groups_controller.gd”. Attach the script to the node. This script will manage the visibility of our character’s parts:

extends Node

@export var body_groups:NodeGroupSet

@export var equipped:Dictionary[StringName,bool] = {}

func _ready():
	body_groups.set_group_visible.connect(_on_set_group_visible)

func _on_set_group_visible(g:StringName, v:bool):
	if v:
		show_group(g)
	else:
		hide_group(g)

func show_group(g:StringName):
	for n in body_groups.get_nodes_in_group(g):
		n.set("visible", true)
	equipped[g] = true
	
	resolve()

func hide_group(g:StringName):
	for n in body_groups.get_nodes_in_group(g):
		n.set("visible", false)
	equipped[g] = false
	
	resolve()

func get_groups_covered_by(g:StringName) -> Array[StringName]:
	var covered_groups:Array[StringName] = []
	
	#`i` iterates through all groups directly covered by `g`
	for n in body_groups.get_nodes_in_group(g):
		for h in body_groups.get_groups_of(n):
			var i = get_covered_group(h)
			if !i: continue
			covered_groups.append(i)
			covered_groups.append_array(get_groups_covered_by(i))
	
	return covered_groups


func get_covered_group(g:StringName):
	if g.begins_with("-"): return g.substr(1)
	return null

func resolve():
	
	#Show all nodes in equipped groups being covered by unequipped groups
	for g in body_groups.get_groups():
		var g_invisible := false if equipped.get(g) else true
		
		if g_invisible:
			for h in get_groups_covered_by(g):
				for n in body_groups.get_nodes_in_group(h):
					for i in body_groups.get_groups_of(n):
						if equipped.get(i):
							n.set("visible", true)
							print("show ", n)
	
	#Hide all nodes in groups covered by equipped groups
	for g in body_groups.get_groups():
		var g_visible := true if equipped.get(g) else false
		
		if g_visible: 
			for h in get_groups_covered_by(g):
				for n in body_groups.get_nodes_in_group(h):
					n.set("visible", false)
					print("hide ", n)

Script explanation

_ready : We dropped a sneaky signal in the NodeGroupSet resource earlier for communicating between the equipment slots and this controller. Here we hook it up.

_on_set_group_visible : This is the method the signal gets connected to. The signal passes a group name and a boolean, v. v is true if the group is getting “equipped”, and false if the group is getting “unequipped”. We simply use that to switch between two different methods, show_group and hide_group.

You may wonder why we don’t just perform the logic in this method. It made more sense in the old version of the script, where show_group and hide_group were more different. You can change this if you want. You can change anything, if you want.

show_group and hide_group : These methods show/hide all nodes in the target group, mark it as equipped/unequipped, then call resolve, which will repair the relationships defined by the NodeGroupSet.

get_groups_covered_by : Iterate through all nodes in a given group. Each node may cover some other groups. This function is recursive, meaning it will call itself on groups that it finds to get the whole tree of groups that may be affected by equipping/unequipping the input group. In the demo project, calling this method on the “NiceCoat” group will return the following list:

Recursion, when a function makes a call to itself, is a common way to represent a “search” through a directed acyclic digraph. If there happens to be a cycle, the recursion will continue until the computer or abstraction layer decides you’ve had enough fun and issues a stack overflow error. There are some subtleties with this method that made it harder to implement with a more professional iterative search, but I encourage you to try.

get_covered_group : Takes a group name starting with “-” and returns a String with the “-” trimmed off the front, generating the name of the “covered” group. If the group name does not start with “-”, this method returns null.

resolve : The real meat and potatoes. For my money, the easiest way to verify and repair the relationship between the groups is to, first, show all nodes covered by unequipped groups, then, hide all nodes covered by equipped groups. Think of it like sculpting “up” with clay, the chiseling down to get the final product.

What I like about this version of the script is that, while the DAG stuff is very conceptually important, it doesn’t end up adding a lot of weight to the actual code. Just a little bit of recursion, which might be a little unfamiliar to some readers. Oh, who am I kidding. This guide takes a lot of patience to get through.

Common issues

You should have a flexible clothing system up and running. There are two very common issues that I ran into while working on the advanced section, so I thought I would discuss those here.

For the beginner section, just go back a few steps if something goes wrong and try again from there. If undoing with Ctrl+Z isn’t enough, I recommend using something like git to track your progress. GitHub Desktop and VSCode have good tooling for going back and forth between multiple versions of a project intuitively.

Mixamo isn’t working like I want.

Yeah… It’s sketchy. It took a lot of tinkering to get it right and part of the workflow relies on an unmaintained blender addon. I might see your comment if you leave one here describing what’s going wrong. Here’s a quick list of things to check though:

  1. Does the BoneMap in the import settings for your character appear with all green points. If not, have you saved a valid bone map to a resource?
  2. Double check the root motion. Use a RootMotionView node (see Godot docs) to visualize the root motion is working.
  3. Are the data transfer modifiers configured exactly as I described? These can be very tedious to fix. It may be cheaper for your time just to go with detached objects that don’t need to be seamless (i.e. the “retro” style described earlier)
  4. Does your blender file have:
    1. A collection for the original character model
    2. A collection a split objects
    3. A collection for the armature
    4. The collection for the split objects and armature enabled
    5. A glb export configuration to export visible objects with “Apply Modifiers” checked
  5. Are your mixamo animations imported from a separate blender file using the mixamo root motion blender addon? This is sometimes easier than using the animations with your main blender file.

Some groups are covered when I don’t want them to be covered.

For me, this happened when I split one clothing item into two. The “Coat” was a later addition to the example project. I split the shirt and torso into two objects because the Coat occluded a large part of both of them, but not all of either. This introduced a bug where equipping the coat hid the whole torso! But why? Well, the shirt hides the torso. Splitting it into two objects made two parts that both hid the torso. One of which was hidden by the coat. Therefore, the coat hid the torso.

If you’re having a lot of issues like this, it may be good to add a maximum recursion depth to get_groups_covered_by that you can adjust to see where longer relations are causing unexpected behavior.

I appreciate your work on this. This won’t work for me because I’m doing something different with existing models that I’m importing and chunking differently, but this still might have some things for me to learn. Do you have a blog post that might be a little easier to consume than these forum posts?

1 Like

No, sorry. I mostly just wanted a record that I wasn’t just sitting around while I was actually working on this project. How exactly does your chunking work? You may still be able to get something out of the last section if you’re able to map your groups onto whatever you’re using.

I may make an edit later where the sections are less attached to each other. For example, making the last section not rely on the naming conventions of the previous section.

1 Like

I’m using KayKit’s models from itch.io and Patreon. I suck in the whole model and then chop it up into parts, then make those parts available in the character selector. The point of my process is I don’t want to take anything into Blender to edit it. I just store one version of every head, hat, body, arm and leg. The colors are handled by swapping materials. That way, I drop the models I want to choose from in and use them. They load from a selected directory.

Right now I’m cleaning up a 2D game, so I’ll come back to this later when I have time to read through it.

1 Like

Well, if you have them as separate objects, the only thing that could be complicated about it is assigning groups to each object in a way that will work with the tutorial. If one part comes in multiple meshes, that would be following the advanced section. If one part comes in one part, the introductory system should be more than enough. That part of the tutorial doesn’t use blender at all. I just uses .blend files as an example.

All the inventory system really provides for the armor system is a signal to show/load equipment and a signal to hide/unload it.

Yeah I already have a character creator that loads them, displays them and creates a character. But like I said, I’ll take a look. Always good to see what other people are doing.

1 Like