Skip to main content

examples/game_mechanics/
inventory_system.rs

1//! This example shows one possible way to implement an inventory system
2//!  using ECS relationships.
3
4use flecs_ecs::prelude::*;
5
6//MARK: Components
7
8// Inventory tags, relationships
9
10/// Base item type
11#[derive(Component, Debug)]
12struct Item;
13
14/// Container tag
15#[derive(Component, Debug)]
16struct Container;
17
18/// Inventory tag
19#[derive(Component, Debug)]
20struct Inventory;
21
22#[derive(Component, Debug)]
23struct ContainedBy;
24
25// Item / unit properties
26
27/// Item is active/worn
28#[derive(Component, Debug)]
29struct Active;
30
31/// Number of items the instance represents
32#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
33struct Amount {
34    amount: i32,
35}
36
37/// Health of the item
38#[derive(Component, Debug, Clone)]
39struct Health {
40    value: i32,
41}
42
43/// Amount of damage an item deals per use
44#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
45struct Attack {
46    value: i32,
47}
48
49// Items
50#[derive(Component, Debug)]
51struct Sword;
52#[derive(Component, Debug)]
53struct Armor;
54#[derive(Component, Debug)]
55struct Coin;
56
57// Item prefab types
58
59#[derive(Component, Debug)]
60struct WoodenSword;
61#[derive(Component, Debug)]
62struct IronSword;
63#[derive(Component, Debug)]
64struct WoodenArmor;
65#[derive(Component, Debug)]
66struct IronArmor;
67
68//MARK: Utility Functions
69
70/// Find the "kind" of an item (e.g., Sword, Armor, Coin) by looking for something
71/// that inherits from `Item`.
72fn item_kind(item: EntityView<'_>) -> Option<Entity> {
73    let world = item.world();
74    let mut result_entity: Option<Entity> = None;
75
76    item.each_component(|comp| {
77        if comp.is_entity() {
78            // If id is a plain entity (component), check if component inherits
79            // from Item
80            if comp.entity_view().has((id::<flecs::IsA>(), Item)) {
81                result_entity = Some(comp.entity_view().id());
82            }
83        } else if comp.is_pair() {
84            // If item has a base entity, check if the base has an attribute
85            // that is an Item.
86            if comp.first_id() == flecs::IsA::ID
87                && let Some(base_kind) = item_kind(comp.second_id())
88            {
89                result_entity = Some(base_kind);
90            }
91        }
92    });
93
94    result_entity
95}
96
97/// Return the "name of the prefab" (e.g., `WoodenSword`) if found,
98/// otherwise the more generic kind (e.g., `Sword`).
99fn item_name(item: EntityView<'_>) -> Option<String> {
100    let world = item.world();
101    let mut result_name: Option<String> = None;
102
103    item.each_component(|comp| {
104        if comp.is_entity() {
105            if comp.entity_view().has((id::<flecs::IsA>(), Item)) {
106                result_name = comp.entity_view().get_name();
107            }
108        } else if comp.is_pair()
109            && comp.first_id() == flecs::IsA::ID
110            && let Some(base_kind) = item_kind(comp.second_id())
111        {
112            result_name = comp.second_id().get_name();
113        }
114    });
115
116    result_name
117}
118
119/// If entity is not a Container, get its Inventory target (the actual container).
120fn get_container(container: EntityView<'_>) -> Entity {
121    let world = container.world();
122    if container.has(Container) {
123        return container.id();
124    }
125    container.target(Inventory, 0).unwrap().id()
126}
127
128/// Iterate all items in an inventory
129fn for_each_item<F>(container: EntityView<'_>, mut func: F)
130where
131    F: FnMut(flecs_ecs::core::EntityView<'_>, ()),
132{
133    let world = container.world();
134    world
135        .query::<()>()
136        .with((ContainedBy, container))
137        .build()
138        .each_entity(func);
139}
140
141/// Find item in the inventory of the specified "kind".
142/// If `active_required == true`, only return items that have `Active`.
143fn find_item_w_kind(
144    container: EntityView<'_>,
145    kind: Entity,
146    active_required: bool,
147) -> Option<Entity> {
148    let world = container.world();
149
150    let mut result: Option<Entity> = None;
151
152    let container = world.entity_from_id(get_container(container));
153
154    for_each_item(container, |item, _| {
155        // Check if we should only return active items. This is useful when
156        // searching for an item that needs to be equipped.
157        if active_required && !item.has(Active) {
158            return;
159        }
160
161        if let Some(ik) = item_kind(item)
162            && ik == kind
163        {
164            result = Some(item.id());
165        }
166    });
167
168    result
169}
170
171/// Transfer a single item to a different container.
172fn transfer_item(container: EntityView<'_>, item: EntityView<'_>) {
173    let world = container.world();
174
175    let amt = item.try_cloned::<&Amount>().unwrap_or(Amount { amount: 1 });
176
177    #[allow(clippy::redundant_else)]
178    if amt.amount > 0 {
179        // If item has amount we need to check if the container already has an
180        // item of this kind, and increase the value.
181        let ik = item_kind(item).unwrap();
182        let dst_item = find_item_w_kind(container, ik, false);
183
184        if let Some(dst_item) = dst_item {
185            // If a matching item was found, increase its amount
186            world
187                .entity_from_id(dst_item)
188                .get::<&mut Amount>(|dst_amt| {
189                    dst_amt.amount += amt.amount;
190                });
191            item.destruct();
192            return;
193        } else {
194            // If no matching item was found, fallthrough which will move the
195            // item from the src container to the dst container
196        }
197    }
198
199    // Move item to target container (replaces previous ContainedBy, if any)
200    item.add((ContainedBy, container));
201}
202
203/// Move all items from `src` container to `dst` container.
204fn transfer_items(dst: EntityView<'_>, src: EntityView<'_>) {
205    let world = dst.world();
206    println!(">> Transfer items from {} to {}\n", src.name(), dst.name());
207
208    // Defer is recommended in Flecs so you can safely modify ECS while iterating.
209    // In flecs-rust you can do `world.defer(|w| { ... })`.
210    world.defer(|| {
211        let dst_container = world.entity_from_id(get_container(dst));
212        let src_container = world.entity_from_id(get_container(src));
213
214        for_each_item(src_container, |item, _| {
215            transfer_item(dst_container, item);
216        });
217    });
218}
219
220/// Attack `player` with `weapon`.
221fn attack(player: EntityView<'_>, weapon: EntityView<'_>) {
222    let world = player.world();
223
224    println!(
225        ">> {} is attacked with a {}!",
226        player.name(),
227        item_name(weapon).unwrap_or("UnknownItem".to_string())
228    );
229
230    let atk = weapon.try_cloned::<&Attack>();
231
232    if atk.is_none() {
233        // A weapon without Attack power? Odd.
234        println!(" - the weapon is a dud");
235        return;
236    }
237
238    let mut att_value = atk.unwrap().value;
239
240    // Get armor item, if player has equipped any
241    if let Some(armor_e) = find_item_w_kind(player, world.component_id::<Armor>(), true) {
242        let armor_e = world.entity_from_id(armor_e);
243        armor_e.get::<Option<&mut Health>>(|health| {
244            if let Some(armor_health) = health {
245                println!(
246                    " - {} defends with {} ({} health)",
247                    player.name(),
248                    item_name(armor_e).unwrap(),
249                    armor_health.value
250                );
251
252                // Subtract attack from armor health. If armor health goes below
253                // zero, delete the armor and carry over remaining attack points.
254                armor_health.value -= att_value;
255
256                if armor_health.value <= 0 {
257                    att_value = -armor_health.value;
258                    armor_e.destruct();
259                    println!(" - {} is destroyed!", item_name(armor_e).unwrap());
260                } else {
261                    println!(
262                        " - {} has {} health left after taking {} damage",
263                        item_name(armor_e).unwrap(),
264                        armor_health.value,
265                        att_value
266                    );
267                    att_value = 0;
268                }
269            } else {
270                // Armor without Defense power? Odd.
271                println!(" - the {} armor is a dud", item_name(armor_e).unwrap());
272            }
273        });
274    } else {
275        // Brave but stupid
276        println!(" - {} fights without armor!", player.name());
277    }
278
279    // For each usage of the weapon, subtract one from its health
280    weapon.get::<&mut Health>(|weapon_health| {
281        if weapon_health.value > 0 {
282            weapon_health.value -= 1;
283            if weapon_health.value == 0 {
284                println!(" - {} is destroyed!", item_name(weapon).unwrap());
285                weapon.destruct();
286            } else {
287                println!(
288                    " - {} has {} uses left",
289                    item_name(weapon).unwrap(),
290                    weapon_health.value
291                );
292            }
293        }
294    });
295
296    // If armor didn't counter the whole attack, subtract from the player health
297    if att_value > 0 {
298        player.get::<&mut Health>(|player_health| {
299            player_health.value -= att_value;
300            if player_health.value <= 0 {
301                println!(" - {} died!", player.name());
302                player.destruct();
303            } else {
304                println!(
305                    " - {} has {} health left after taking {} damage",
306                    player.name(),
307                    player_health.value,
308                    att_value
309                );
310            }
311        });
312    }
313
314    println!();
315}
316
317/// Print items in a container / inventory.
318fn print_items(container: EntityView<'_>) {
319    println!("-- {}'s inventory:", container.name());
320
321    let world = container.world();
322    let mut count = 0;
323
324    // In case the player entity was provided, make sure we're working
325    // with its inventory entity.
326    let container = world.entity_from_id(get_container(container));
327
328    for_each_item(container, |item, _| {
329        // Items with an Amount component fill up a single inventory slot but
330        // represent multiple instances, like coins.
331        let amount = item
332            .try_cloned::<&Amount>()
333            .unwrap_or(Amount { amount: 1 })
334            .amount;
335        println!(
336            " - {} {} ({})",
337            amount,
338            item_name(item).unwrap_or("UnknownItem".to_string()),
339            world.entity_from_id(item_kind(item).unwrap()).name()
340        );
341
342        count += 1;
343    });
344
345    if count == 0 {
346        println!(" - << empty >>");
347    }
348
349    println!();
350}
351
352//MARK: ECS Modules
353
354#[derive(Component)]
355pub struct InventoryComponentsModule;
356
357#[derive(Component)]
358pub struct InventoryModule;
359
360impl Module for InventoryComponentsModule {
361    fn module(world: &World) {
362        world.module::<InventoryComponentsModule>("inventory::components");
363
364        world.component::<Item>();
365        world.component::<Container>();
366        world.component::<Inventory>();
367        // Item can only be contained by one container
368        world
369            .component::<ContainedBy>()
370            .add_trait::<flecs::Exclusive>();
371    }
372}
373
374impl Module for InventoryModule {
375    fn module(world: &World) {
376        world.module::<InventoryModule>("inventory::systems");
377        world.import::<InventoryComponentsModule>();
378    }
379}
380
381#[derive(Component)]
382pub struct ItemComponentsModule;
383
384impl Module for ItemComponentsModule {
385    fn module(world: &World) {
386        world.module::<ItemComponentsModule>("item::components");
387
388        world.component::<Active>();
389        world.component::<Amount>();
390
391        //health gets copied to instance, don't share, defaults to Onstantiate Copy,
392        world.component::<Health>();
393
394        world
395            .component::<Attack>()
396            .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>();
397
398        world
399            .component::<Sword>()
400            .is_a(Item)
401            .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>();
402
403        world
404            .component::<Armor>()
405            .is_a(Item)
406            .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>();
407        world
408            .component::<Coin>()
409            .is_a(Item)
410            .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>();
411
412        //register item prefabs
413        world
414            .prefab_type::<WoodenSword>()
415            .add(Sword)
416            .set(Attack { value: 1 })
417            // copy to instance, don't share
418            .set(Health { value: 5 });
419
420        world
421            .prefab_type::<IronSword>()
422            .add(Sword)
423            .set(Attack { value: 4 })
424            .set(Health { value: 10 });
425
426        world
427            .prefab_type::<WoodenArmor>()
428            .add(Armor)
429            .set(Health { value: 10 });
430
431        world
432            .prefab_type::<IronArmor>()
433            .add(Armor)
434            .set(Health { value: 20 });
435    }
436}
437
438#[derive(Component)]
439pub struct ItemModule;
440
441impl Module for ItemModule {
442    fn module(world: &World) {
443        world.module::<ItemModule>("item::systems");
444        world.import::<ItemComponentsModule>();
445    }
446}
447
448//MARK: Main
449
450fn main() {
451    let mut world = World::new();
452
453    world.import::<InventoryModule>();
454    world.import::<ItemModule>();
455
456    // Create a loot box with items
457    let loot_box = world
458        .entity_named("Chest")
459        .add(Container)
460        .with_first(ContainedBy, || {
461            world.entity().is_a(IronSword);
462            world.entity().is_a(WoodenArmor);
463            world.entity().add(Coin).set(Amount { amount: 30 });
464        });
465
466    // Create a player entity with an inventory
467    let player = world.entity_named("Player").set(Health { value: 10 }).add((
468        Inventory,
469        world.entity().add(Container).with_first(ContainedBy, || {
470            world.entity().add(Coin).set(Amount { amount: 20 });
471        }),
472    ));
473
474    // Print items in loot box
475    print_items(loot_box);
476
477    // Print items in player inventory
478    print_items(player);
479
480    // Copy items from loot box to player inventory
481    transfer_items(player, loot_box);
482
483    // Print items in player inventory after transfer
484    print_items(player);
485
486    // Print items in loot box after transfer
487    print_items(loot_box);
488
489    // Find armor entity & equip it
490    if let Some(armor) = find_item_w_kind(player, world.component_id::<Armor>(), false) {
491        world.entity_from_id(armor).add(Active);
492    }
493
494    // Create a weapon to attack the player with
495    let my_sword = world.entity().is_a(IronSword);
496
497    // Attack player
498    attack(player, my_sword);
499    attack(player, my_sword);
500    attack(player, my_sword);
501    attack(player, my_sword);
502    attack(player, my_sword);
503
504    // Output:
505    // -- Chest's inventory:
506    //  - 1 IronSword (Sword)
507    //  - 1 WoodenArmor (Armor)
508    //  - 30 Coin (Coin)
509
510    // -- Player's inventory:
511    //  - 20 Coin (Coin)
512
513    // >> Transfer items from Chest to Player
514
515    // -- Player's inventory:
516    //  - 50 Coin (Coin)
517    //  - 1 IronSword (Sword)
518    //  - 1 WoodenArmor (Armor)
519
520    // -- Chest's inventory:
521    //  - << empty >>
522
523    // >> Player is attacked with a IronSword!
524    //  - Player defends with WoodenArmor (10 health)
525    //  - WoodenArmor has 6 health left after taking 4 damage
526    //  - IronSword has 9 uses left
527
528    // >> Player is attacked with a IronSword!
529    //  - Player defends with WoodenArmor (6 health)
530    //  - WoodenArmor has 2 health left after taking 4 damage
531    //  - IronSword has 8 uses left
532
533    // >> Player is attacked with a IronSword!
534    //  - Player defends with WoodenArmor (2 health)
535    //  - WoodenArmor is destroyed!
536    //  - IronSword has 7 uses left
537    //  - Player has 8 health left after taking 2 damage
538
539    // >> Player is attacked with a IronSword!
540    //  - Player fights without armor!
541    //  - IronSword has 6 uses left
542    //  - Player has 4 health left after taking 4 damage
543
544    // >> Player is attacked with a IronSword!
545    //  - Player fights without armor!
546    //  - IronSword has 5 uses left
547    //  - Player died!
548}