Skip to content

Repository files navigation

Python Bindings for EntityX (α Alpha)

This system adds the ability to extend entity logic with Python scripts. The goal is to allow ad-hoc behaviour to be assigned to entities through scripts, in contrast to the more strictly pure entity-component system approach.

Example

fromentityximportEntity, Component, emitfrommygameimportPosition, Health, DeadclassPlayer(Entity):
position=Component(Position, 0, 0)
health=Component(Health, 100)
defon_collision(self, event):
self.health.health-=10ifself.health.health<=0:
emit(Dead(self))

Building and installing

EntityX Python has the following build and runtime requirements:

CMake Options:

  • ENTITYX_PYTHON_BUILD_TESTING : Enable building of tests
  • BOOST_ROOT : Set path to boost root if CMake did not find it
  • ENTITYX_ROOT : Set path to EntityX root if CMake did not find it
  • PYTHON_ROOT : Set path to Python root if CMake did not find it

Check out the source to entityx_python, and run:

mkdir build &&cd build
cmake ..
make
make install

Design

  • Python scripts are attached to entities via PythonScript.
  • Systems and components can not be created from Python, primarily for performance reasons.
  • Events are proxied directly to Python entities via PythonEventProxy objects.
    • Each event to be handled in Python must have an associated PythonEventProxyimplementation.
    • As a convenience BroadcastPythonEventProxy<Event>(handler_method) can be used. It will broadcast events to all PythonScript entities with a <handler_method>.
  • PythonSystem manages scripted entity lifecycle and event delivery.

Summary

To add scripting support to your system, something like the following steps should be followed:

  1. Expose C++ Component and Event classes to Python with BOOST_PYTHON_MODULE.
  2. Initialize the module with PyImport_AppendInittab.
  3. Create a Python package.
  4. Add classes to the package, inheriting from entityx.Entity and using the entityx.Component descriptor to assign components.
  5. Create a PythonSystem, passing in the list of paths to add to Python's import search path.
  6. Optionally attach any event proxies.
  7. Create an entity and associate it with a Python script by assigning PythonScript, passing it the package name, class name, and any constructor arguments.
  8. When finished, call EntityManager::destroy_all().

Interfacing with Python

entityx::python primarily uses standard boost::python to interface with Python, with some helper classes and functions.

Exposing C++ Components to Python

In most cases, this should be pretty simple. Given a component, provide a boost::python class definition with two extra methods defined with EntityX::Python helper functions assign_to<Component> and get_component<Component>. These are used from Python to assign Python-created components to an entity and to retrieve existing components from an entity, respectively.

Here's an example:

namespacepy= boost::python;
structPosition : publicentityx::Component<Position> {
Position(float x = 0.0, float y = 0.0) : x(x), y(y) {}
float x, y;
};
voidexport_position_to_python() {
py::class_<Position>("Position", py::init<py::optional<float, float>>())
// Allows this component to be assigned to an entity
.def("assign_to", &entityx::python::assign_to<Position>)
// Allows this component to be retrieved from an entity.// Set return_value_policy to reference raw component pointer
.def("get_component", &entityx::python::get_component<Position>,
py::return_value_policy<py::reference_existing_object>() )
.staticmethod("get_component")
.def_readwrite("x", &Position::x)
.def_readwrite("y", &Position::y);
}
BOOST_PYTHON_MODULE(mygame) {
export_position_to_python();
}

Using C++ Components from Python

Use the entityx.Component class descriptor to associate components and provide default constructor arguments:

importentityxfrommygameimportPosition# C++ ComponentclassMyEntity(entityx.Entity):
# Ensures MyEntity has an associated Position component,# constructed with the given arguments.position=entityx.Component(Position, 1, 2)
def__init__(self):
assertself.position.x==1assertself.position.y==2

Delivering events to Python entities

Unlike in C++, where events are typically handled by systems, EntityX::Python explicitly provides support for sending events to entities. To bridge this gap use the PythonEventProxy class to receive C++ events and proxy them to Python entities.

The class takes a single parameter, which is the name of the attribute on a Python entity. If this attribute exists, the entity will be added to PythonEventProxy::entities (std::list<Entity>), so that matching entities will be accessible from any event handlers.

This checking is performed in PythonEventProxy::can_send(), and can be overridden, but further checking can also be done in the event receive() method.

A helper template class called BroadcastPythonEventProxy<Event> is provided that will broadcast events to any entity with the corresponding handler method.

To implement more refined logic, subclass PythonEventProxy and operate on the protected member entities. Here's a collision example, where the proxy only delivers collision events to the colliding entities themselves:

structCollisionEvent : publicentityx::Event<CollisionEvent> {
CollisionEvent(Entity a, Entity b) : a(a), b(b) {}
// NOTE: See note below in export_collision_event_to_python().
Entity a, b;
};
structCollisionEventProxy : publicentityx::python::PythonEventProxy, public entityx::Receiver<CollisionEvent> {
CollisionEventProxy() : entityx::python::PythonEventProxy("on_collision") {}
voidreceive(const CollisionEvent &event) {
// "entities" is a protected data member, populated by// PythonSystem, with Python entities that pass can_send().for (auto entity : entities) {
auto py_entity = entity.templatecomponent<entityx::python::PythonComponent>();
if (entity == event.a || entity == event.b) {
py_entity->object.attr(handler_name.c_str())(event);
}
}
}
};
voidexport_collision_event_to_python() {
py::class_<CollisionEvent>("Collision", py::init<Entity, Entity>())
// NOTE: Normally, def_readonly() would be used to expose attributes,// but you must use the following construct in order for Entity// objects to be automatically converted into their Python instances.
.add_property("a", py::make_getter(&CollisionEvent::a, py::return_value_policy<py::return_by_value>()))
.add_property("b", py::make_getter(&CollisionEvent::b, py::return_value_policy<py::return_by_value>()));
// Register event manager emit so signal handlers will trigger properlyvoid (EventManager::*emit)(const CollisionEvent &) = &EventManager::emit;
py::class_<EventManager, boost::noncopyable>("EventManager", py::no_init)
.def("emit", emit);
}
BOOST_PYTHON_MODULE(mygame) {
export_position_to_python();
export_collision_event_to_python();
}

Sending events from Python

This is relatively straight forward. Once you have exported a C++ event to Python:

fromentityximportEntity, emitfrommygameimportCollisionclassAnEntity(Entity): passemit(Collision(AnEntity(), AnEntity()))

Initialization

Finally, initialize the mygame module once, before using PythonSystem, with something like this:

// This should only be performed once, at application initialization time.CHECK(PyImport_AppendInittab("mygame", initmygame) != -1)
<< "Failed to initialize mygame Python module";

Then create a PythonSystem as necessary:

// Initialize the PythonSystem.
vector<string> paths;
// Ensure that MYGAME_PYTHON_PATH includes entityx.py from this distribution.
paths.push_back(MYGAME_PYTHON_PATH);
// +any other Python paths...
entityx::python::PythonSystem python(paths);
// Add any Event proxies.
python->add_event_proxy<CollisionEvent>(ev, std::make_shared<CollisionEventProxy>());

About

Python bindings for EntityX

Resources

Stars

14 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages