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.
from entityx import Entity, Component, emit
from mygame import Position, Health, Dead
class Player(Entity):
position = Component(Position, 0, 0)
health = Component(Health, 100)
def on_collision(self, event):
self.health.health -= 10
if self.health.health <= 0:
emit(Dead(self))
EntityX Python has the following build and runtime requirements:
ENTITYX_PYTHON_BUILD_TESTING
: Enable building of testsBOOST_ROOT
: Set path to boost root if CMake did not find itENTITYX_ROOT
: Set path to EntityX root if CMake did not find itPYTHON_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
- 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
PythonEventProxy
implementation. - As a convenience
BroadcastPythonEventProxy<Event>(handler_method)
can be used. It will broadcast events to allPythonScript
entities with a<handler_method>
.
- Each event to be handled in Python must have an associated
PythonSystem
manages scripted entity lifecycle and event delivery.
To add scripting support to your system, something like the following steps should be followed:
- Expose C++
Component
andEvent
classes to Python withBOOST_PYTHON_MODULE
. - Initialize the module with
PyImport_AppendInittab
. - Create a Python package.
- Add classes to the package, inheriting from
entityx.Entity
and using theentityx.Component
descriptor to assign components. - Create a
PythonSystem
, passing in the list of paths to add to Python's import search path. - Optionally attach any event proxies.
- Create an entity and associate it with a Python script by assigning
PythonScript
, passing it the package name, class name, and any constructor arguments. - When finished, call
EntityManager::destroy_all()
.
entityx::python
primarily uses standard boost::python
to interface with Python, with some helper classes and functions.
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:
namespace py = boost::python;
struct Position : public entityx::Component<Position> {
Position(float x = 0.0, float y = 0.0) : x(x), y(y) {}
float x, y;
};
void export_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();
}
Use the entityx.Component
class descriptor to associate components and provide default constructor arguments:
import entityx
from mygame import Position # C++ Component
class MyEntity(entityx.Entity):
# Ensures MyEntity has an associated Position component,
# constructed with the given arguments.
position = entityx.Component(Position, 1, 2)
def __init__(self):
assert self.position.x == 1
assert self.position.y == 2
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:
struct CollisionEvent : public entityx::Event<CollisionEvent> {
CollisionEvent(Entity a, Entity b) : a(a), b(b) {}
// NOTE: See note below in export_collision_event_to_python().
Entity a, b;
};
struct CollisionEventProxy : public entityx::python::PythonEventProxy, public entityx::Receiver<CollisionEvent> {
CollisionEventProxy() : entityx::python::PythonEventProxy("on_collision") {}
void receive(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.template component<entityx::python::PythonComponent>();
if (entity == event.a || entity == event.b) {
py_entity->object.attr(handler_name.c_str())(event);
}
}
}
};
void export_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 properly
void (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();
}
This is relatively straight forward. Once you have exported a C++ event to Python:
from entityx import Entity, emit
from mygame import Collision
class AnEntity(Entity): pass
emit(Collision(AnEntity(), AnEntity()))
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>());