A powerful 3D/2.5D game controller plugin for Bevy Engine.
The Inventory and Economy System provides a robust framework for managing items, equipment, currency, and trading in Bevy All-in-One. This unified documentation covers the interconnected subsystems that drive the game’s economy and player progression through item acquisition.
Inventory - The main container logicInventoryItem - Data structure for individual itemsEquipment - Player gear slotsCurrency - Money managementVendor - Shop configurationVendorInventory - Shop stock managementThe Inventory system is designed to be modular and entity-component based. Any entity in the world can have an Inventory, not just the player. This allows for chests, loot boxes, and even NPCs to carry items.
The system is split into three main modules:
These modules communicate primarily through Events, ensuring that logic remains decoupled. For example, a “Buy” action triggers a PurchaseItemEvent, which is then processed by the vendor system to check funds, deduct currency, and transfer items.
Inventory uses a fixed-size slot array (Vec<Option<InventoryItem>>). This classic approach allows for:
None, allowing for “holes” in the inventory.Items have a max_stack property.
Inventory component tracks current_weight vs weight_limit.The economy relies on Currency components attached to entities.
buy_multiplier and sell_multiplier.
infinite supplies (good for basics like arrows or potions).InventoryThe primary component for any entity that stores items.
#[derive(Component, Debug, Reflect)]
pub struct Inventory {
pub items: Vec<Option<InventoryItem>>,
pub max_slots: usize,
pub weight_limit: f32,
pub current_weight: f32,
}
items: The dynamic list of slots. Size is usually initialized to max_slots.max_slots: Hard limit on direct inventory size (default: 24).weight_limit: Maximum carry weight before penalties (logic for penalties handled by gameplay systems).current_weight: Cached weight sum, updated via recalculate_weight().InventoryItemThe data object for an item. Note that this is cloned into the Inventory slots.
#[derive(Debug, Clone, Reflect)]
pub struct InventoryItem {
pub item_id: String, // Unique identifier (e.g., "sword_iron")
pub name: String, // Display name (e.g., "Iron Sword")
pub quantity: i32, // Current stack count
pub max_stack: i32, // Max per slot
pub weight: f32, // Weight per unit
pub item_type: ItemType, // Enum: Weapon, Ammo, Consumable, etc.
pub icon_path: String, // Asset path for UI
pub value: f32, // Base gold value
pub category: String, // Sorting category
pub min_level: u32, // Usage requirement
pub info: String, // Description/Lore
}
EquipmentSeparate from the main grid, this component handles active gear.
#[derive(Component, Debug, Default, Reflect)]
pub struct Equipment {
pub main_hand: Option<InventoryItem>,
pub armor: Option<InventoryItem>,
}
PhysicalItemAttached to 3D world entities representing loot on the ground.
#[derive(Component, Debug, Reflect)]
pub struct PhysicalItem {
pub item: InventoryItem,
}
Interaction Logic: When a player interacts with a PhysicalItem entity (via InteractionType::Pickup), the item data is copied to the player’s Inventory, and the world entity is despawned.
CurrencyTracks the wallet of a player or NPC.
#[derive(Component, Debug, Clone)]
pub struct Currency {
pub amount: f32,
pub currency_type: CurrencyType, // Default: Gold
}
VendorConfiguration for a shop NPC.
#[derive(Component, Debug, Clone)]
pub struct Vendor {
pub name: String,
pub buy_multiplier: f32, // Price modifier when player BUYS
pub sell_multiplier: f32, // Price modifier when player SELLS
pub infinite_stock: bool, // If true, new items added are infinite by default
pub add_sold_items: bool, // If true, items player sells are added to stock
pub min_level_to_buy: u32, // Global level requirement
pub currency_type: CurrencyType,
}
VendorInventoryThe actual stock held by the vendor. Distinct from Inventory to support shop-specific features like “Infinite Stock” flags per item.
#[derive(Component, Debug, Clone)]
pub struct VendorInventory {
pub items: Vec<ShopItem>,
pub categories: Vec<VendorCategory>,
}
inventory.rs)The Pickup System listens for InteractionEvent where type is Pickup.
InteractionEventQueue.Inventory.PhysicalItem.add_item):
item_id matches and quantity < max_stack, fills the stack.None slot and fills it.inventory.rs)The UI uses Bevy’s UI (Flexbox) system.
InventoryUIRoot: Main container, absolute positioning.FlexWrap::Wrap.60px), spawned in a loop (0..24).update_inventory_ui):
Inventory and InventoryUISlot children.vendor.rs)Buying an item involves a multi-step check:
PurchaseItemEvent is fired (usually by UI interaction).handle_purchase_events processes the queue.amount > 0 or is it infinite?Currency.amount cover cost? (Cost = base_value * vendor_multiplier).Currency.ShopItem stock (unless infinite).Inventory (Requires separate logic call or event trigger).Inventory::add_item would happen here.The system relies heavily on the “Queue Resource” pattern to handle events, a workaround for certain Bevy event limitations or to ensure persistence across specific frame steps.
| Event | Resource Queue | Description |
| :— | :— | :— |
| InteractionEvent | InteractionEventQueue | Triggered by interacting with world objects. Used for Pickups. |
| Event | Resource Queue | Description |
| :— | :— | :— |
| AddCurrencyEvent | AddCurrencyEventQueue | Safe way to inject money (rewards, loot). |
| RemoveCurrencyEvent | RemoveCurrencyEventQueue | Safe debit. Fails if insufficient funds. |
| Event | Resource Queue | Description |
| :— | :— | :— |
| PurchaseItemEvent | PurchaseItemEventQueue | Player attempts to buy from Vendor. |
| SellItemEvent | SellItemEventQueue | Player attempts to sell to Vendor. |
| PurchaseFailedEvent | PurchaseFailedEventQueue | Feedback for UI (e.g., “Not enough gold”). |
fn spawn_player(mut commands: Commands) {
commands.spawn((
Player, // Player Tag
// 1. Add Inventory
Inventory {
max_slots: 30,
weight_limit: 150.0,
..default()
},
// 2. Add Equipment Slots
Equipment::default(),
// 3. Add Wallet
Currency {
amount: 500.0,
currency_type: CurrencyType::Gold,
},
));
}
To drop an item in the world that can be picked up:
fn spawn_loot_sword(mut commands: Commands, asset_server: Res<AssetServer>) {
// 1. Define the Item Data
let sword_item = InventoryItem {
item_id: "steel_sword".into(),
name: "Steel Sword".into(),
quantity: 1,
max_stack: 1,
weight: 5.0,
item_type: ItemType::Weapon,
icon_path: "icons/weapon_sword.png".into(),
value: 150.0,
category: "Weapons".into(),
min_level: 5,
info: "A standard adventurer's blade.".into(),
};
// 2. Spawn Entity with Visuals + PhysicalItem component
commands.spawn((
// Visuals (Mesh, Material, Transform)
SceneBundle {
scene: asset_server.load("models/sword.glb#Scene0"),
transform: Transform::from_xyz(0.0, 1.0, 0.0),
..default()
},
// Logic Component
PhysicalItem {
item: sword_item,
},
// Make it interactable
InteractionTarget {
interaction_type: InteractionType::Pickup,
label: "Pick up Steel Sword".into(),
..default()
}
));
}
fn setup_blacksmith(mut commands: Commands) {
let mut inventory = VendorInventory::default();
// Add an infinite supply of repair hammers
inventory.add_item(ShopItem {
item: InventoryItem {
name: "Repair Hammer".into(),
value: 10.0,
..default()
},
amount: 1,
buy_price: 15.0, // Markup
sell_price: 5.0,
infinite: true, // Never runs out
min_level: 0,
use_vendor_min_level: true,
});
commands.spawn((
// Vendor Identity
Vendor {
name: "Village Blacksmith".into(),
buy_multiplier: 1.2, // Expensive to buy from
sell_multiplier: 0.8, // Good place to sell
infinite_stock: false,
add_sold_items: true, // You can buy back what you sold
..default()
},
inventory, // Attach the stock
));
}
The add_item function implements a smart stacking algorithm to maximize inventory efficiency.
Weight is not calculated every frame to save performance.
recalculate_weight() is called only when:
(item.weight * item.quantity) for all Some(item) slots.The vendor system includes an auto-categorization feature:
update_vendor_categories: This system iterates through all items in a VendorInventory.category string field.VendorCategory structs, which can be used by the UI to create tabs (e.g., “Weapons”, “Potions”, “Materials”) without manual configuration.The currency system prevents atomic errors (like negative balance) via the RemoveCurrencyEvent.
amount_to_remove > strict balance, the transaction is aborted.CurrencyRemovalFailedEvent is fired instead.recalculate_weight update? If weight limit is exceeded, logic might prevent pickup (depending on implementation flags).handle_pickup_events is running in the schedule.SaleFailedEvent.ItemNotFound: The item data you sent doesn’t match the vendor’s expected format?Vendor configuration: Does the vendor have currency_type matching the player’s wallet?Inventory component changes or when toggle_inventory_ui sets visibility.Changed<Inventory> for performance, or running every frame if optimization is not yet a concern.InventoryItem.ShopItem amounts.buy_multiplier based on Charisma stats.