Scalable C++ crafting system

C++ | SOLID principles | Scalable | C++20

Why did I build it?

This is a Crafting System built on C++, while keeping compatibility with Unreal Engine in mind. It started out as an Idea for a game, then quickly translated into an project to learn some best practices used in programming and new c++ features.

How it works

The system takes a Recipe and performs a validation based on inputs like Character skills, inventory and proximity to crafting stations and then proceeds to craft the item if it passes. That's the basic gist of it. Let me list out the components in detail below.

Under the hood

Craftable items in the game can be of a specific rarity and a category.

Code Samples
Rarity, Category and Crafting Stations

#pragma once

#include <cstdint>

namespace CraftingSystem {

    enum class Rarity : uint32_t {
        Common = 0,
        Rare = 1,
        Epic = 2,
        Legendary = 3
    };

    enum class Category : uint32_t {
        Weapon = 0,
        Armor = 1,
        Potion = 2,
        Ammo = 3,
        Accessory = 4
    };

    enum class Station : uint32_t {
        StellarForge = 0, // Weapons Crafting Station
        NaniteSynthetizer = 1, // Potions Crafting Station
        Anywhere = 2,
        RelicFoundry = 3 // Accessory Crafting Station
    };

}
								

Craftable items in the game can be of a specific rarity and a category.

Ingredients, Recipe and RequiredSkill

#pragma once

#include "Enums.h"
#include <cstdint>
#include <string>
#include <vector>

namespace CraftingSystem {

    using ItemId = uint32_t;

    struct Ingredient {
        ItemId id;
        uint32_t quantity; 
    };

    struct RequiredSkill {
        std::string skill;  // e.g., "Blacksmithing" (GAS: map to FGameplayAttribute).
        uint32_t threshold; // Min level.
    };

    struct Recipe {
        ItemId resultId;
        Category category;
        Station requiredStation;
        std::vector<Ingredient>> ingredients;  // 3-7 base; scale qty based on rarity.
        std::vector<RequiredSkill> requiredSkills;  
        Rarity minRarity;  // Output rarity; inputs must match or upscale.

     
        [[nodiscard]] std::vector<Ingredient> GetScaledIngredients(Rarity targetRarity) const;
    };

}
								

Various Recipes can be loaded from a json file or UE DataTables. The RecipeLoader class handles this. The RecipeRegistry class then manages the loaded recipes.

RecipeLoader and Recipe Registry

#pragma once

#include "Types.h"
#include "IRecipeLoader.h"
#include <memory>
#include <vector>
#include <unordered_map>

namespace CraftingSystem {

    class IRecipeLoader {
    public:
        virtual ~IRecipeLoader() = default;
        [[nodiscard]] virtual std::vector<Recipe> Load() = 0;  // e.g., from JSON or UE DataTables.
    };
								
	class RecipeRegistry {
        std::unordered_map<Category, std::unordered_map<ItemId, Recipe>> recipes_;  // Nested for O(1) category+result lookup.
        std::unique_ptr<IRecipeLoader> loader_;

    public:
        explicit RecipeRegistry(std::unique_ptr<IRecipeLoader> loader);
        void Initialize();  // Loads and populates maps.
        [[nodiscard]] std::vector<const Recipe*> GetByCategory(Category cat) const;
        [[nodiscard]] const Recipe* FindByResult(Category cat, ItemId id) const;
    };
								
	RecipeRegistry::RecipeRegistry(std::unique_ptr<IRecipeLoader> loader)
        : loader_(std::move(loader)) {}

    void RecipeRegistry::Initialize() {
        auto recipes = loader_->Load();
        for (const auto& recipe : recipes) {
            recipes_[recipe.category][recipe.resultId] = recipe;
        }
    }

    std::vector<const Recipe*> RecipeRegistry::GetByCategory(Category cat) const {
        auto it = recipes_.find(cat);
        if (it == recipes_.end()) {
            return {};  
        }
        std::vector<const Recipe*> result;
        result.reserve(it->second.size());
        std::ranges::transform(it->second, std::back_inserter(result),
                               [](const auto& p) { return &p.second ; });
        return result;
    }

    const Recipe* RecipeRegistry::FindByResult(Category cat, ItemId id) const {
        auto catIt = recipes_.find(cat);
        if (catIt == recipes_.end()) return nullptr;
        auto idIt = catIt->second.find(id);
        return (idIt != catIt->second.end()) ? &idIt->second : nullptr;
    }

}
								

Before crafting the Validators validate the various requirements. This part is scalable so you can easily add or remove requirements.

Recipe Validators

#pragma once

#include "Types.h"
#include <memory>
#include <vector>

namespace CraftingSystem {
	
								
	class InventoryValidator : public IRecipeValidator {
    public:
        [[nodiscard]] bool CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const override;
    };
	
	bool InventoryValidator::CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const {
        auto scaled = recipe.GetScaledIngredients(targetRarity);
        for (const auto& [id, quantity] : scaled) {
            auto it = player.inventory.find(id);
            uint32_t total = (it != player.inventory.end()) ? it->second : 0u;
            if (total < quantity) return false;
        }
        return true;
    }
	
	class InventoryValidator : public IRecipeValidator {
    public:
        [[nodiscard]] bool CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const override;
    };
								
	bool InventoryValidator::CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const {
        auto scaled = recipe.GetScaledIngredients(targetRarity);
        for (const auto& [id, quantity] : scaled) {
            auto it = player.inventory.find(id);
            uint32_t total = (it != player.inventory.end()) ? it->second : 0u;
            if (total < quantity) return false;
        }
        return true;
    }
	
								
	class IStationChecker {
    public:
        virtual ~IStationChecker() = default;
        [[nodiscard]] virtual bool IsAtStation(Station station) const = 0;  // e.g., raycast/overlap in UE.
    };
								
	class StationValidator : public IRecipeValidator {
        const IStationChecker& checker_;

    public:
        explicit StationValidator(const IStationChecker& checker);
        [[nodiscard]] bool CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const override;
    };
								
	StationValidator::StationValidator(const IStationChecker& checker)
        : checker_(checker) {}

    bool StationValidator::CanCraft(const Recipe& recipe, const PlayerState& player, Rarity) const {
        return checker_.IsAtStation(recipe.requiredStation);
    }
	
								
	class ISkillProvider {
    public:
        virtual ~ISkillProvider() = default;
        [[nodiscard]] virtual uint32_t GetSkill(const std::string& skill) const = 0;
        // For GAS: bool HasStat(FGameplayAttribute stat, float threshold);
    };
								
	class SkillValidator : public IRecipeValidator {
        const ISkillProvider& skills_;

    public:
        explicit SkillValidator(const ISkillProvider& skills);
        [[nodiscard]] bool CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const override;
    };
								
	SkillValidator::SkillValidator(const ISkillProvider& skills)
        : skills_(skills) {}

    bool SkillValidator::CanCraft(const Recipe& recipe, const PlayerState&, Rarity) const {
        for (const auto& [skill, threshold] : recipe.requiredSkills) {
            if (skills_.GetSkill(skill) < threshold) return false;
        }
        return true;
    }
								
								
	class CompositeValidator : public IRecipeValidator {
        std::vector<std::unique_ptr<IRecipeValidator>> validators_;

    public:
        template<typename... Validators>
        explicit CompositeValidator(Validators&&... vals);

        [[nodiscard]] bool CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const override;
    };
								
	template<typename... Validators>
    CompositeValidator::CompositeValidator(Validators&&... vals) {
        (validators_.emplace_back(std::forward<Validators>(vals)), ...);  // C++20 fold expression.
    }

    bool CompositeValidator::CanCraft(const Recipe& recipe, const PlayerState& player, Rarity targetRarity) const {
        return std::all_of(validators_.begin(), validators_.end(),
                           [&](const auto& v) { return v->CanCraft(recipe, player, targetRarity); });
    }

}
								

Crafting service class puts it all together.

CraftingService

#pragma once

#include "RecipeRegistry.h"
#include "IRecipeValidator.h"
#include "PlayerState.h"
#include "Types.h"
#include <vector>
#include <algorithm>
#include <iterator>

namespace CraftingSystem {

    class CraftingService {
        const RecipeRegistry& registry_;
        std::unique_ptr<IRecipeValidator> validator_;

    public:
        explicit CraftingService(const RecipeRegistry& reg, std::unique_ptr<IRecipeValidator> val);
        bool TryCraft(ItemId resultId, Rarity targetRarity, PlayerState& player);
        [[nodiscard]] std::vector<const Recipe*> GetAvailable(const PlayerState& player, Category cat, Rarity targetRarity) const;
    };
                                        
    // Helper to derive category from resultId TODO: Stub. Update later.
    Category DeriveCategoryFromId(ItemId id) {
        return static_cast<Category>(id % 5);
    }

    bool CraftingService::TryCraft(ItemId resultId, Rarity targetRarity, PlayerState& player) {
        auto cat = DeriveCategoryFromId(resultId);
        const auto* recipe = registry_.FindByResult(cat, resultId);
        if (!recipe || !validator_->CanCraft(*recipe, player, targetRarity)) return false;

        auto scaled = recipe->GetScaledIngredients(targetRarity);

        // Consume ingredients (O(k) map lookups/updates).
        for (const auto& [id, quantity] : scaled) {
            auto it = player.inventory.find(id);
            if (it == player.inventory.end() || it->second < quantity) {
                return false;  // Paranoia check. CanCraft should prevent this.
            }
            it->second -= quantity;  
            if (it->second == 0) {
                player.inventory.erase(it);  // Clean up
            }
        }

        // Add result O(1).
        ++player.inventory[resultId];  // Merges if exists.

        // Upgrades
        // Multiplayer: Broadcast event.
        // GAS: Grant ability/effect on craft.
        return true;
    }

    std::vector<const Recipe*> CraftingService::GetAvailable(const PlayerState& player, Category cat, Rarity targetRarity) const {
        auto recipes = registry_.GetByCategory(cat);
        std::vector<const Recipe*> available;
        available.reserve(recipes.size());
        std::ranges::copy_if(recipes, std::back_inserter(available),
                             [&](const auto* r) { return validator_->CanCraft(*r, player, targetRarity); });
        return available; 
    }

}
								

Note: This is still a work in progress. There are a lot of stubs that has to be implemented. But, the outline of the system is done.