-
Notifications
You must be signed in to change notification settings - Fork 5
Tutorial
Ginseng is an entity-component framework that boasts extreme usability and compile-time safety. This tutorial will cover all of its functionality.
Ginseng is a single-file, header-only library. The only thing you need to do is copy include/ginseng/ginseng.hpp to your project, preferably keeping it inside the ginseng/ directory.
Our project structure might look like:
src/
ginseng/
ginseng.hpp
main.cpp
And main.cpp will include ginseng/ginseng.hpp:
#include "ginseng/ginseng.hpp"
int main() {}Now, simply make sure to compile with C++11 enabled (-std=c++11 on many compilers, though most current compilers have it enabled by default), and everything should work.
The core functionality of Ginseng is contained within Ginseng::Database<>. This class is what manages all of your entities and components, and provides lightweight query methods.
Let's create a database:
#include "ginseng/ginseng.hpp"
using DB = Ginseng::Database<>;
int main() {
DB db;
}Yep, it's that easy. Notice that Database is a class template; it accepts a custom allocator, using std::allocator by default.
Let's take a look at the types and methods provided by Database<>. Don't worry, we'll take a look at all of this in more detail later.
Types:
-
struct EntID- Entity ID, similar to a pointer. -
struct ComID- Component ID, similar to a pointer. -
struct ComInfo<T>- Handle to a component of known type.
Basic methods:
-
EntID makeEntity()- Creates an empty entity. -
void eraseEntity()- Erases an entity and all of its components. -
ComInfo<T> makeComponent<T>(EntID,T)- Adds the given component to the entity. -
void eraseComponent(ComID)- Erases a single component. -
size_type size() const- Returns the number of entities.
Cross-database methods:
-
? displaceEntity(EntID)- Moves the entity out of the database. -
EntID emplaceEntity(?)- Moves the entity into the database. -
? displaceComponent(ComID)- Moves the component out of the database. -
ComID emplaceComponent(?)- Moves the component into the database.
Query methods:
-
void visit<F>(F)- Takes a function, loops over all entities, applying the function where possible. -
std::vector<?> query<...>()- Returns a vector of data for entities that match the given constraints.
Entities are represented by the type Database::EntID. This type is much like a pointer. If the entity that it points to is erased or displaced, the EntID becomes invalid.
EntID provides one method (along with operator== and operator<):
-
ComInfo<T> get<T>()- Accesses a component of the entity.
Let's add an entity to our database:
#include "ginseng/ginseng.hpp"
using DB = Ginseng::Database<>;
int main() {
DB db;
DB::EntID ent = db.makeEntity();
}Now, we can't do much with an empty entity, so let's take a look at...
When dealing with components, there are two classes you need to know about.
First, we have ComID. This class is very similar to EntID. ComID does not store any information about the type of the component it points to. Along with comparison operators, ComID provides:
-
EntID getEID() const- Returns theEntIDof the containing entity. -
T& cast<T>()- Returns a reference to the component. Undefined ifTdoes not match the component's type.
Next we have ComInfo<T>. This class is similar to ComID, but it actually knows what type the component is. It provides the following methods:
-
explicit operator bool() const- Returnstrueif this handle points to a component. -
T& data() const- Returns a reference to the component. -
ComID id() const- Returns the underlyingComID.
Now, we can actually add some components to our entity.
Components in Ginseng can be any type. Literally anything. However, it is recommended that you use standard-layout class types. This means any class with no inheritance or virtual functions.
Let's give our entity a name and job, and we'll add a couple of other entities as well:
#include "ginseng/ginseng.hpp"
#include <string>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
int main() {
DB db;
{
auto ent = db.makeEntity();
auto name_info = db.makeComponent(ent, Name{"Alice"});
auto job_info = db.makeComponent(ent, Job{"Programmer", "Umbrella Corp."});
// .data() can be used to access the component directly.
job_info.data().title = "Software Engineer";
}
{
auto ent = db.makeEntity();
auto name_info = db.makeComponent(ent, Name{"Bob"});
auto job_info = db.makeComponent(ent, Job{"Test Subject", "Aperture"});
// .id() returns the ComID.
db.eraseComponent(job_info.id());
}
{
auto ent_c = db.makeEntity();
db.makeComponent(ent_c, Job{"Secret Agent", "Government"});
}
}Now that we have some data floating around, we need a way to access it.
Visiting databases is extremely straightforward. All you need is a function (or functor or lambda or whatever) that accepts component types as parameters. The database will call your visitor on each entity that has the proper components. For the sake of simplicity, Database::EntID is considered to be a component that all entities have by default (it points to the containing entity, of course).
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
// Component parameters may be taken by value or reference.
void print_jobs(DB::EntID eid, Job& job) {
std::string name = "Somebody";
// This is a very useful pattern for checking for optional components.
if (auto name_info = eid.get<Name>()) {
name = name_info.data().value;
}
std::cout << name << " is a " << job.title << " for " << job.company << std::endl;
}
int main() {
DB db;
auto ent = db.makeEntity();
db.makeComponent(ent, Name{"Alice"});
db.makeComponent(ent, Job{"Software Engineer", "Umbrella Corp."});
ent = db.makeEntity();
db.makeComponent(ent, Name{"Bob"});
ent = db.makeEntity();
db.makeComponent(ent, Job{"Secret Agent", "Government"});
// No special syntax, just pass the visitor function to visit().
db.visit(print_jobs);
}Databases provide a Database::ComInfo<T> class template that contains a ComID and a direct reference to the component of type T.
When visiting or querying, using ComInfo<T> instead of just T will give the visitor a ComInfo<T>, as expected.
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
int main() {
DB db;
auto ent = db.makeEntity();
db.makeComponent(ent, Name{"Alice"});
db.makeComponent(ent, Job{"Software Engineer", "Umbrella Corp."});
ent = db.makeEntity();
db.makeComponent(ent, Name{"Bob"});
ent = db.makeEntity();
db.makeComponent(ent, Job{"Secret Agent", "Government"});
db.visit([]( DB::ComInfo<Job> job_info ){
std::cout << "firing someone from job " << job_info.data().title << std::endl;
db.eraseComponent(job_info.id());
});
}Ginseng provides a Ginseng::Not class template that is used to "invert" visitor parameters. When a visitor takes a Not<T> parameter, it will visit entities that do not have the component T. These Not objects are empty objects, so take them by value.
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
int main() {
DB db;
auto ent = db.makeEntity();
db.makeComponent(ent, Name{"Alice"});
db.makeComponent(ent, Job{"Software Engineer", "Umbrella Corp."});
ent = db.makeEntity();
db.makeComponent(ent, Name{"Bob"});
ent = db.makeEntity();
db.makeComponent(ent, Job{"Secret Agent", "Government"});
std::cout << "These people have names, but no jobs:" << std::endl;
// Naturally, lambdas and other such classes can be used as visitors.
db.visit([](Name& name, Ginseng::Not<Job>){
std::cout << "- " << name.value << std::endl;
});
}Tag components are components that have no value. They are usually just empty structs that are used to tag an entity as having a certain property. Ginseng provides the Ginseng::Tag class template that is used to create tag component types.
If you do not use Ginseng::Tag, even if your component is empty, Ginseng will treat it just like a normal component, which could result in unnecessary memory allocations.
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
// This is the recommended pattern for tag types.
struct IsFemale_tag {};
using IsFemale = Ginseng::Tag<IsFemale_tag>;
int main() {
DB db;
auto ent = db.makeEntity();
db.makeComponent(ent, Name{"Alice"});
db.makeComponent(ent, IsFemale{});
ent = db.makeEntity();
db.makeComponent(ent, Name{"Bob"});
// Tag types are added just like any other component.
db.visit([](Name& name, IsFemale){
std::cout << name.value << " is female." << std::endl;
});
}Sometimes it may be useful to create multiple Database objects, and transfer entities and components between them. Fortunately, this is actually very simple.
To move and entity or component between databases, simply displace the item from the current database, and emplace it into the new database.
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
int main() {
DB db;
auto ent = db.makeEntity();
db.makeComponent(ent, Name{"Alice"});
db.makeComponent(ent, Job{"Software Engineer", "Umbrella Corp."});
ent = db.makeEntity();
db.makeComponent(ent, Name{"Bob"});
ent = db.makeEntity();
db.makeComponent(ent, Job{"Secret Agent", "Government"});
DB people_with_jobs;
DB people_without_jobs;
db.visit([](DB::EntID eid){
// The currently visited entity and its components can be safely erased or displaced while visiting.
// However, note that any EntIDs, ComIDs, and references to components will become invalid.
if (auto job_info = eid.get<Job>()) {
// The act of displacement returns a value type containing the whole entity or component.
// Don't lose it!
auto person = db.displaceEntity(eid);
people_with_jobs.emplaceEntity(std::move(person));
} else {
auto person = db.displaceEntity(eid);
people_without_jobs.emplaceEntity(std::move(person));
}
});
std::cout << "There are " << people_with_jobs.size() << " people who have jobs." << std::endl;
}Querying is similar to visiting, but instead of applying a function automatically, query() simply returns a vector containing all of the matching entities and components.
The result vector type is std:vector<std::tuple<EntID,Ts...>>, where Ts... is a sequence of references to component value types, and values of any other types (ComInfo<T>, Not<T>, and Tag<T>).
So, an example:
#include "ginseng/ginseng.hpp"
#include <string>
#include <iostream>
using DB = Ginseng::Database<>;
struct Name {
std::string value;
};
struct Job {
std::string title;
std::string company;
};
struct TeamColor {
std::string color_name;
};
struct IsFemale_tag {};
using IsFemale = Ginseng::Tag<IsFemale_tag>;
int main() {
DB db;
/* ... create some entities ... */
std::cout << "These people have no names, but do have jobs, team colors, and are female:" << std::endl;
for (auto& result : db.query<Ginseng::Not<Name>, Job, IsFemale, DB::ComInfo<TeamColor>>()) {
auto& eid = std::get<0>(result);
auto& job = std::get<2>(result);
auto& team_info = std::get<4>(result);
std::cout << "A " << job.title << " is on team " << team_info.data().color_name << std::endl;
}
}