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
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))
Building and installing
EntityX Python has the following build and runtime requirements:
CMake Options:
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
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
PythonEventProxyobjects.- 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 allPythonScriptentities with a<handler_method>.
- Each event to be handled in Python must have an associated
PythonSystemmanages scripted entity lifecycle and event delivery.
Summary
To add scripting support to your system, something like the following steps should be followed:
- Expose C++
ComponentandEventclasses to Python withBOOST_PYTHON_MODULE. - Initialize the module with
PyImport_AppendInittab. - Create a Python package.
- Add classes to the package, inheriting from
entityx.Entityand using theentityx.Componentdescriptor 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().
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:
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(); }
Using C++ Components from 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
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:
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(); }
Sending events from 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()))
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>());