In many games, chests serve as important game mechanics for storing items, rewarding players, and adding a layer of interactivity. Designing a chest/container system in Unity can be straightforward if when we already have an item system in place ofcourse. By extending our existing item and inventory system, I could easily create a robust container system without starting from scratch.
Leveraging Our Existing Item System
The plan was like below:
Create a Container Item: This serves as a base class for items that can store other items (e.g., chests, bags).
Extend to a Chest Item: Specialize the container to behave like a chest with unique properties (e.g., locking/unlocking).
Add Chest Behavior: Implement the interaction logic for the chest in the game world (opening, retrieving, and storing items).
By using this approach, I avoided duplicating code and took full advantage of our existing item framework.
Step 1: Base Container Class
A chest is essentially a container which has addiitional fields next to the BaseItem class. So, our first step is to create a container item class.
namespace DTWorldz.Items.Models.Containers
{
public abstract class BaseContainerItem : BaseItem
{
public AudioClip OpenSound { get; private set; }
public AudioClip CloseSound { get; private set; }
// Constructor
protected BaseContainerItem(string name) : base(name)
{
if (ItemData == null)
{
return;
}
if (ItemData is ContainerData containerData)
{
OpenSound = containerData.OpenSound;
CloseSound = containerData.CloseSound;
}
}
}
}
Why is this useful?
The BaseContainerItem
class extends BaseItem
, meaning it inherits all the properties and behaviors of a regular item (like name, description, and icons), but also includes a built-in open close sounds. This flexibility allows us to create any container-like item in the game.
Step 2: Specializing the Chest
Now that we have a base container, we can easily create a ChestItem
by inheriting from BaseContainerItem
. We can also add chest-specific features, such as whether the chest is locked and methods for locking or unlocking it.
using DTWorldz.Items.Data.Scriptables;
using UnityEngine;
namespace DTWorldz.Items.Models.Containers
{
public class ChestItem : BaseContainerItem
{
public AudioClip UnlockSound { get; private set; }
public bool IsLocked { get; private set; }
public AudioClip LockSound { get; private set; }
// Constructor
public ChestItem() : base("ChestItem")
{
if (ItemData == null)
{
return;
}
if (ItemData is ContainerData equipmentData)
{
IsLocked = equipmentData.IsLocked;
LockSound = equipmentData.LockSound;
UnlockSound = equipmentData.UnlockSound;
}
}
public void UnlockChest()
{
IsLocked = false;
}
public void LockChest()
{
IsLocked = true;
}
}
}
Why is this useful?
The ChestItem
class is now a specialized container. You can control whether the chest is locked, and the system will know how to handle its inventory just like any other container. This avoids redundant code while keeping the design modular and extendable.
Step 3: Chest Behavior in the World
To interact with the chest in the game, you’ll need to add behavior. Just like items have behaviors (for example, equipping or consuming), a chest will have its own behavior that allows players to interact with it (such as opening it to reveal the contents). But first thing is to also create BaseContainerItemBehaviour to allow to have generic interactions for all the container types.
using DTWorldz.Behaviours.Mobiles;
using DTWorldz.Inventory.Behaviours;
using DTWorldz.Inventory.Behaviours.UI;
using DTWorldz.Items.Models.Containers;
using UnityEngine;
namespace DTWorldz.Items.Behaviours.Containers
{
public class BaseContainerItemBehaviour<T> : BaseItemBehaviour<T> where T : BaseContainerItem
{
public GameObject ChestVisuals;
private ContainerItemUIBehaviour inventoryUI;
public ContainerItemUIBehaviour InventoryUI
{
get
{
return inventoryUI;
}
}
private InventoryBehaviour inventoryBehaviour;
public InventoryBehaviour InventoryBehaviour
{
get
{
return inventoryBehaviour;
}
}
public override void Awake()
{
base.Awake();
inventoryBehaviour = GetComponent<InventoryBehaviour>();
if (inventoryBehaviour == null)
{
Debug.LogError($"{gameObject.name}: InventoryBehaviour is null.");
}
}
public override void Interact(MobileBehaviour mobileBehaviour)
{
// Implement interaction logic for containers (e.g., opening container UI)
if (BaseItem == null)
{
Debug.LogError("BaseItem is null");
return;
}
if(mobileBehaviour == null)
{
Debug.LogError("MobileBehaviour is null");
return;
}
mobileBehaviour.Mobile.InvokeItemUsed(BaseItem);
if (BaseItem is ChestItem chest && chest.IsLocked)
{
Debug.Log("Chest is locked.");
return;
}
// check if mobile is close enough to the container
if (Vector3.Distance(mobileBehaviour.transform.position, transform.position) > 1.5f)
{
Debug.Log("Mobile is too far from the container.");
return;
}
// Open the container's inventory
OpenContainer(mobileBehaviour);
}
protected virtual void OpenContainer(MobileBehaviour mobileBehaviour)
{
Debug.Log($"Opening container {BaseItem.ItemName}.");
// Instantiate the chest's visuals
var containerVisuals = Instantiate(ChestVisuals, transform.position, Quaternion.identity);
inventoryUI = containerVisuals.GetComponent<ContainerItemUIBehaviour>();
inventoryUI.Initialize(inventoryBehaviour, BaseItem);
}
}
}
Please check Interact() method.
In this way regular items will be picked up from world on interaction but container items will be stay in the world and opens the container to show some visuals to the player.
Now we can make a specilized ChestItemBehaviour and put some loot it in it.
using DTWorldz.Behaviours.Mobiles;
using DTWorldz.Inventory.Behaviours;
using DTWorldz.Inventory.Behaviours.UI;
using DTWorldz.Items.Models.Containers;
using Unity.VisualScripting;
using UnityEngine;
namespace DTWorldz.Items.Behaviours.Containers
{
public class ChestItemBehaviour : BaseContainerItemBehaviour<ChestItem>
{
public override void Awake()
{
base.Awake();
// Additional initialization for the chest (e.g., visuals, sounds, etc.)
}
void Start(){
Invoke("PutSomeLoot", 0.1f);
}
void PutSomeLoot(){
var katana = new Models.Equipments.MainHand.Weapons.Melee.Swords.Katana();
InventoryBehaviour.AddItem(katana, 1, 0);
var longPants = new Models.Equipments.Leggings.LongPants();
InventoryBehaviour.AddItem(longPants, 1, 1);
var longShirt = new Models.Equipments.Torso.LongShirt();
InventoryBehaviour.AddItem(longShirt, 1, 2);
var shoes = new Models.Equipments.Footwear.Shoes();
InventoryBehaviour.AddItem(shoes, 1, 3);
var healthPotion = new Models.Consumables.Potions.HealthPotion();
InventoryBehaviour.AddItem(healthPotion, 5, 4);
}
}
}
Why is this useful?
The ChestItemBehaviour
handles how the chest interacts with the game world, such as when the player opens it or tries to access the items inside. It extends the BaseContainerItemBehaviour
, allowing the same code for interaction, drag-and-drop, and inventory management to be reused, further minimizing redundant code.
Step 4: Integrating with the UI
When the player interacts with the chest, you’ll want to show the chest’s inventory on the screen, allowing them to add or remove items from it. This can be done by leveraging your existing inventory UI system. Since a chest is just another type of container, you can use the same UI elements you would for your player’s inventory. It’s just another component on the item itself.
using DTWorldz.Behaviours;
using DTWorldz.Behaviours.Mobiles.Humanoid;
using DTWorldz.Items.Interfaces;
using DTWorldz.Items.Models;
using DTWorldz.Items.Models.Containers;
using DTWorldz.Utils;
using TMPro;
using UnityEngine;
namespace DTWorldz.Inventory.Behaviours.UI
{
public class ContainerItemUIBehaviour : InventoryUIBehaviour
{
public TMP_Text Name;
private PlayerBehaviour player;
private PlayerMovementBehaviour playerMovementBehaviour;
private bool isContainerOpen = false;
private BaseContainerItem item;
public override void Start()
{
base.Start();
isContainerOpen = true;
player = FindAnyObjectByType<PlayerBehaviour>();
playerMovementBehaviour = player.GetComponent<PlayerMovementBehaviour>();
SoundUtils.PlayDetachedSound(item.OpenSound);
}
public void Initialize(InventoryBehaviour inventoryBehaviour, BaseContainerItem item = null)
{
base.Initialize(inventoryBehaviour);
if (item != null)
{
this.item = item;
SetName(item.ItemName);
}
}
public override void CreateActionButtons(BaseItem item, int slotIndex)
{
// base.CreateActionButtons(item, slotIndex);
// // Clear existing buttons
foreach (Transform child in ActionButtonContainer)
{
Destroy(child.gameObject);
}
if (item == null)
{
return;
}
if (InventoryBehaviour.GetQuantity(slotIndex) == 0)
{
return;
}
CreateActionButton("Loot", () =>
{
var suceeded = player.InventoryBehaviour.AddItem(item, 1);
if (suceeded)
{
InventoryBehaviour.RemoveItem(item, 1, slotIndex);
SoundUtils.PlayDetachedSound(item.PickupSound);
}
});
// Add a "Drop" button for every item
CreateActionButton("Drop", () =>
{
InventoryBehaviour.DropItem(item, slotIndex);
});
}
public override void SortInventoryItems()
{
base.SortInventoryItems();
}
void Update()
{
// if character is moving then close the container
if (player != null && (player.Mobile.IsDead || playerMovementBehaviour.IsMoving))
{
Close();
}
}
public void Close()
{
if (isContainerOpen)
{
SoundUtils.PlayDetachedSound(item.CloseSound);
isContainerOpen = false;
Destroy(gameObject);
}
}
public void SetName(string name)
{
Name.text = name;
}
}
}
Wrapping It Up
By building a chest system on top of existing item classes, I could have not only implemented a powerful feature but also maintained clean, reusable, and extendable code. Here are the key takeaways:
Reuse Existing Structures: By extending
BaseItem
, I’ve avoid duplicating logic for items, while adding specific functionality to containers and chests.Leverage Inheritance: The use of inheritance makes it easy to add other container types in the future (such as bags, crates, or special loot chests) with minimal effort.
Interaction and UI: The chest system integrates seamlessly with the inventory system, leveraging the same UI and interaction logic, making it easier for players to interact with containers in a consistent way.
With this approach, I could now built a simple and effective chest system that can be easily expanded to add more features, such as locking mechanisms, unique loot, and more!
And of course if you want to give it a try, this project is on Unity’s Asset Store
2D Accelerator Project - > https://u3d.as/3nFD
Happy coding! Let me know if you have any thoughts or questions in the comments below.