This document describes the Vessel API, a C++ system designed for managing and tracking various resources and units within containers and packages as streams. It utilizes a flexible, templated approach, making it adaptable for different resource types and units. That library has a possibility to be used in Game Development, in Math Modeling, or in Liquid Artificial Intellegence Architercture. The API also supports robust testing, demonstrated by the provided unit test examples. This is just a demo of my code style and used tools of C++17/20. The version used in my code possible has too many changes ahead during a development of the full-functional editor. It's only on me the decision to publicate new changes or not.
The Vessel API revolves around the following fundamental ideas:
-
Tags (
PackTestTag): These are empty struct used as template parameters to specialize the behavior ofVesselcomponents. This allows for distinct configurations for different "types" of packages or resources. ForPackTestTag, theTagSelectorspecialization dictates thatUnitsarefloatandResourceIdisTestResource. -
Resources (
TestResource): Anenum class(e.g.,TestResource::Test1,TestResource::Test2) that identifies specific types of resources being managed. -
Units (
float): The quantitative measure of the resources. In thePackTestTagspecialization, units are represented asfloatorint(e.g.,kEmptyAmountKg,kCapacityAmountKg,kEmptyPoints,kCapacityPoints). -
Containers (
Vessel::Container<Tag>): These represent individual storage units for a specific resource type. They typically trackRequestUnits(how much has been taken or requested) andAvailableUnits(how much is remaining or can be supplied). A container is essentially a buffer for a single resource type. -
Packages (
Vessel::Package<Tag>): These act as collections of containers. APackagemanages multipleContainerinstances, each corresponding to aResourceId. They provide mechanisms for loading, saving, and exchanging resource states among themselves or with other containers. -
PackageInterface (
Vessel::PackageInterface<Tag>): This is an interface (likely an abstract base class or concept) that bothPackageandPackageAdapteradhere to. It enables polymorphic interaction, meaning you can write code that operates on any object implementing this interface without needing to know its concrete type. -
PackageAdapter (
Vessel::PackageAdapter<Tag>): This powerful class allows an individualContainerto be treated as aPackageInterface. This is particularly useful for applying operations designed for packages (like resource transfers) to single containers by specifying which resource type that container represents for the operation.
The provided code examples and tests demonstrate key API interactions.
-
Creating a Package: You initialize a
Packageby providing anstd::unordered_mapthat defines the initial capacity for eachResourceIdits internal containers will hold.// Define initial capacities for resources static const std::unordered_map<ResourceId, Package::Units> kContainerProperties { { ResourceId::Test1, kCapacityAmountKg}, // Test1 has kCapacityAmountKg { ResourceId::Test2, kCapacityAmountKg}, // Test2 has kCapacityAmountKg }; // Create a package. By default, its internal containers are full upon creation. Package consumerPackage{ kContainerProperties };
-
Loading State: You can set the state of multiple containers within a package using
LoadState. This method takes aContainerStateTable(a map ofResourceIdto the desired requested amount of units). TheLoadStateeffectively updates theRequestUnitsfor each specified container.const Package::ContainerStateTable emptyStateTable { {ResourceId::Test1, { kEmptyAmountKg }}, // Set Test1 to empty (all requested) {ResourceId::Test2, { kEmptyAmountKg }}, // Set Test2 to empty (all requested) }; consumerPackage.LoadState(emptyStateTable); // After this, consumerPackage's containers for Test1 and Test2 will be in an "empty" state.
-
Saving State: To retrieve the current "requested" state of a package, use
SaveState. This populates aContainerStateTablewith the currentRequestUnitsfor each resource.Package::ContainerStateTable currentPackageState; consumerPackage.SaveState(currentPackageState); // currentPackageState now holds the requested amounts for all resources in consumerPackage.
You can obtain a mutable reference to a specific container within a package using GetContainer and then directly modify its Amount.
// Get the container for Test1 and set its current amount to 0 (empty)
consumerPackage.GetContainer(ResourceId::Test1).SetAmount(kEmptyAmountKg);
// This directly manipulates the available units within the container.The API provides convenient overloaded operators (>> and <<) for transferring resources between PackageInterface objects.
-
Transfer from Provider to Consumer (
>>): TheproviderPackage >> consumerPackage;operation transfers all available resources fromproviderPackagetoconsumerPackage. After this,providerPackage's containers will be empty, andconsumerPackage's containers will be full.providerPackage >> consumerPackage; // consumerPackage's containers are now full, providerPackage's are empty. -
Transfer from Consumer to Provider (
<<): TheproviderPackage << consumerPackage;operation transfers all available resources fromconsumerPackagetoproviderPackage. After this,consumerPackage's containers will be empty, andproviderPackage's containers will be full.providerPackage << consumerPackage; // consumerPackage's containers are now empty, providerPackage's are full.
The PackageAdapter is a crucial component that allows a standalone Container to participate in Package-level operations by making it conform to the PackageInterface. This is especially useful for targeted resource transfers.
Consider this example:
-
Initialize a provider package with capacities
Package providerPackage { std::unordered_map<EResource, Package::Units> { { EResource::Type1, 255.f }, // Container for EResource::Type1 with 255 units capacity { EResource::Type2, 255.f }, // Container for EResource::Type2 with 255 units capacity } }; -
Ensure provider's containers are full
(This is redundant if the constructor fills them, but good for clarity)providerPackage.GetContainer(EResource::Type1).SetAmount(255.f); providerPackage.GetContainer(EResource::Type2).SetAmount(255.f);
-
Create a standalone consumer container with its own capacity
Package::Container consumerContainer { 255.f }; // This container is currently empty by default (or needs explicit SetAmount(0.f)) -
Perform a targeted transfer using
PackageAdapter
This line transfers onlyEResource::Type1fromproviderPackageintoconsumerContainer.
ThePackageAdapteracts as a temporary bridge, mappingconsumerContainerto theType1resource.providerPackage >> PackageAdapter(EResource::Type1, consumerContainer);
Breakdown of the PackageAdapter transfer:
providerPackage >> ...: Initiates a transfer fromproviderPackage.PackageAdapter(EResource::Type1, consumerContainer):
A temporaryPackageAdapteris created. It wrapsconsumerContainerand declares that this container should be treated as the target forEResource::Type1.- Result:
providerPackage's internal container forEResource::Type1will become empty (0 units available).providerPackage's internal container forEResource::Type2remains unaffected (still full with 255 units).consumerContainerwill become full (255 units available), having received theType1resource.
This demonstrates how PackageAdapter enables a single, general-purpose Container to interact with a multi-resource Package for a specific resource type, acting as a flexible conduit for resource flow.
The Vessel API's structure makes it suitable for various resource management scenarios, including:
- Game Development: Managing player inventories, in-game resources (health, mana, ammunition), or crafting materials.
- Simulation: Tracking resource consumption and production in complex simulated environments.
- Logistics & Manufacturing: Modeling stock levels, material flow, or contents of storage units.
Your proposed plans aim to significantly upgrade the Vessel API by leveraging advanced C++ features for better performance, compile-time safety, and developer experience.
Using the Curiously Recurring Template Pattern (CRTP) will enable static polymorphism. This means that function calls can be resolved at compile-time rather than runtime, eliminating the overhead of virtual function calls associated with dynamic polymorphism. Using Entity Component System (ECS) will re-organize all the object into the consistent Array of Structures and optimize overheads for navigating through OOP patterns as they can be put into cache by lines (space availability).
- How it helps:
- Performance: Reduces runtime overhead, leading to faster execution, especially for frequent operations on
PackageInterface. - Optimization: Allows compilers to perform more aggressive optimizations like inlining, as the exact type is known during compilation.
- Performance: Reduces runtime overhead, leading to faster execution, especially for frequent operations on
Small Buffer Optimization (SBO) is a technique typically used for containers (like std::string or std::vector) to avoid dynamic memory allocation for small objects. If the size of the object (or its contained data) fits within a pre-allocated small buffer on the stack, it prevents a heap allocation, which can be a significant performance win.
- How it helps:
- Performance: Reduces memory allocation overhead, particularly for
ContainerorPackageinstances that manage a small number of resources or when their internal data structures are small. This can decrease cache misses and improve overall speed. - Memory Efficiency: Avoids heap fragmentation for frequently created small objects.
- Performance: Reduces memory allocation overhead, particularly for
C++ Concepts allow you to define compile-time constraints on template parameters. Applying them as contracts will ensure that any types used with your Vessel templates (e.g., for TagSelector, Units, ResourceId) adhere to specific requirements.
- How it helps:
- Type Safety: Guarantees that only valid types are used with your templates, catching errors at compile-time rather than runtime.
- Improved Error Messages: Provides much clearer and more concise error messages to users when they violate type requirements, significantly improving the developer experience.
- User Guidance: Clearly communicates the expected interfaces and properties of template arguments, serving as built-in documentation.
Enabling compile-time usage means allowing certain operations or resource definitions to be evaluated and fixed during compilation. This often involves using constexpr functions and variables, and potentially std::integral_constant or similar techniques.
- How it helps:
- Performance: Moves computations from runtime to compile-time, resulting in zero-cost abstractions and faster program startup.
- Resource Definition: Allows resource properties, capacities, or initial states to be immutable and known at compile time, which can simplify some logic and provide stronger guarantees.
- Metaprogramming: Opens up possibilities for advanced template metaprogramming techniques, enabling highly specialized and optimized code generation.