aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlec Thomas <alec@swapoff.org>2013-08-16 23:53:44 -0400
committerAlec Thomas <alec@swapoff.org>2013-08-16 23:53:44 -0400
commit451b2f0e1ebeea6e4ffb7b24e208d2b56b6a1e9b (patch)
treee81afff8067e1ab1a2a9466965e1bda374bc08d3
parent70d2aef8ea1edd50df3050d503eda029fbc4d706 (diff)
Add Python based scripting system (still experimental).
The build system has also been revamped considerably, with the ability to select between std::shared_ptr and boost::shared_ptr, as well as other improvements.
-rw-r--r--.travis.yml2
-rw-r--r--CMakeLists.txt78
-rw-r--r--CheckCXX11SharedPtr.cmake6
-rw-r--r--entityx/Entity.h3
-rw-r--r--entityx/config.h.in8
-rw-r--r--entityx/python/PythonSystem.cc206
-rw-r--r--entityx/python/PythonSystem.h256
-rw-r--r--entityx/python/PythonSystem_test.cc242
-rw-r--r--entityx/python/README.md136
-rw-r--r--entityx/python/entityx/__init__.py102
-rw-r--r--entityx/python/entityx/tests/__init__.py0
-rw-r--r--entityx/python/entityx/tests/assign_test.py18
-rw-r--r--entityx/python/entityx/tests/constructor_test.py10
-rw-r--r--entityx/python/entityx/tests/create_entities_from_python_test.py21
-rw-r--r--entityx/python/entityx/tests/deep_subclass_test.py24
-rw-r--r--entityx/python/entityx/tests/event_test.py10
-rw-r--r--entityx/python/entityx/tests/update_test.py8
-rw-r--r--entityx/python/setup.py12
-rwxr-xr-xscripts/travis.sh2
19 files changed, 1124 insertions, 20 deletions
diff --git a/.travis.yml b/.travis.yml
index 1f5acba..3bd2e8e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,7 @@ before_install:
- if test $CC = gcc; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test; fi
- sudo apt-get update -qq
- sudo apt-get upgrade -y
- - sudo apt-get install -y boost1.48
+ - sudo apt-get install -y boost1.48 python-dev
- if test $CC = gcc; then sudo apt-get install gcc-4.7 g++-4.7; fi
- if test $CC = gcc; then sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.7 20; fi
- if test $CC = gcc; then sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.7 20; fi
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3d9b999..fce2a50 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,6 +8,7 @@ set(ENTITYX_MAX_COMPONENTS 64 CACHE STRING "Set the maximum number of components
set(ENTITYX_USE_CPP11_STDLIB false CACHE BOOL "Use the C++11 stdlib (-stdlib=libc++).")
# Check for which shared_ptr implementation to use.
set(ENTITYX_USE_STD_SHARED_PTR false CACHE BOOL "Use std::shared_ptr<T> rather than boost::shared_ptr<T>?")
+set(ENTITYX_BUILD_SHARED true CACHE BOOL "Build shared libraries?")
include(${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake)
include(CheckCXXSourceCompiles)
@@ -40,9 +41,10 @@ macro(create_test TARGET_NAME SOURCE)
entityx
gtest
gtest_main
- ${Boost_LIBRARIES}
+ ${Boost_SYSTEM_LIBRARY}
${Boost_TIMER_LIBRARY}
${Boost_SIGNALS_LIBRARY}
+ ${ARGN}
)
add_test(${TARGET_NAME} ${TARGET_NAME})
endmacro()
@@ -69,19 +71,62 @@ set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)
find_package(Boost 1.48.0 REQUIRED COMPONENTS signals)
+find_package(Boost 1.48.0 COMPONENTS python)
+
include_directories(${Boost_INCLUDE_DIR})
+set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
+set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")
+set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
+set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
+
+# Things to install
+set(install_libs entityx)
include(CheckCXX11SharedPtr.cmake)
-set(sources entityx/System.cc entityx/Event.cc entityx/Entity.cc entityx/Manager.cc)
+set(sources entityx/tags/TagsComponent.cc entityx/System.cc entityx/Event.cc entityx/Entity.cc entityx/Manager.cc)
add_library(entityx STATIC ${sources})
-add_library(entityx_shared SHARED ${sources})
-target_link_libraries(
- entityx_shared
- ${Boost_SIGNALS_LIBRARY}
-)
-set_target_properties(entityx_shared PROPERTIES OUTPUT_NAME entityx)
+if (ENTITYX_BUILD_SHARED)
+ message("-- Building shared libraries (-DENTITYX_BUILD_SHARED=0 to only build static librarires)")
+ add_library(entityx_shared SHARED ${sources})
+ target_link_libraries(
+ entityx_shared
+ ${Boost_SIGNALS_LIBRARY}
+ )
+ set_target_properties(entityx_shared PROPERTIES OUTPUT_NAME entityx)
+ list(APPEND install_libs entityx_shared)
+endif (ENTITYX_BUILD_SHARED)
+
+include_directories(${Boost_INCLUDE_DIR})
+
+if (Boost_PYTHON_LIBRARY)
+ message("-- Found boost::python, building entityx/python")
+ find_package(PythonLibs REQUIRED)
+ include_directories(${PYTHON_INCLUDE_DIRS})
+ set(ENTITYX_HAVE_BOOST_PYTHON 1)
+ set(python_sources entityx/python/PythonSystem.cc)
+ add_library(entityx_python STATIC ${python_sources})
+ list(APPEND install_libs entityx_python)
+ install(
+ FILES ${CMAKE_CURRENT_SOURCE_DIR}/entityx/python/entityx/__init__.py
+ DESTINATION share/entityx/python/
+ RENAME entityx.py
+ )
+ message("-- Installing entityx Python package to ${CMAKE_INSTALL_PREFIX}/share/entityx/python")
+ set(ENTITYX_INSTALLED_PYTHON_PACKAGE_DIR ${CMAKE_INSTALL_PREFIX}/share/entityx/python/)
+ if (ENTITYX_BUILD_SHARED)
+ add_library(entityx_python_shared SHARED ${python_sources})
+ target_link_libraries(
+ entityx_python_shared
+ entityx_shared
+ ${Boost_PYTHON_LIBRARY}
+ ${PYTHON_LIBRARIES}
+ )
+ set_target_properties(entityx_python_shared PROPERTIES OUTPUT_NAME entityx_python)
+ list(APPEND install_libs entityx_python_shared)
+ endif (ENTITYX_BUILD_SHARED)
+endif (Boost_PYTHON_LIBRARY)
if (ENTITYX_BUILD_TESTING)
find_package(Boost 1.48.0 REQUIRED COMPONENTS signals timer system)
@@ -92,6 +137,10 @@ if (ENTITYX_BUILD_TESTING)
create_test(event_test entityx/Event_test.cc)
create_test(system_test entityx/System_test.cc)
create_test(tags_component_test entityx/tags/TagsComponent_test.cc)
+ if (Boost_PYTHON_LIBRARY)
+ add_definitions(-DENTITYX_PYTHON_TEST_DATA=\"${CMAKE_CURRENT_SOURCE_DIR}/entityx/python\")
+ create_test(python_test entityx/python/PythonSystem_test.cc entityx_python ${Boost_PYTHON_LIBRARY} ${PYTHON_LIBRARIES})
+ endif (Boost_PYTHON_LIBRARY)
if (ENTITYX_RUN_BENCHMARKS)
message("-- Running benchmarks")
add_definitions(-DGTEST_USE_OWN_TR1_TUPLE=1 -DBOOST_NO_CXX11_NUMERIC_LIMITS=1)
@@ -101,15 +150,20 @@ if (ENTITYX_BUILD_TESTING)
endif ()
endif (ENTITYX_BUILD_TESTING)
-file(GLOB headers "${CMAKE_CURRENT_SOURCE_DIR}/entityx/*.h")
+
+configure_file(
+ ${CMAKE_CURRENT_SOURCE_DIR}/entityx/config.h.in
+ ${CMAKE_CURRENT_SOURCE_DIR}/entityx/config.h
+)
install(
- FILES ${headers}
- DESTINATION "include/entityx"
+ DIRECTORY "entityx"
+ DESTINATION "include"
+ FILES_MATCHING PATTERN "*.h"
)
install(
- TARGETS entityx entityx_shared
+ TARGETS ${install_libs}
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
diff --git a/CheckCXX11SharedPtr.cmake b/CheckCXX11SharedPtr.cmake
index 24dc871..0dc3281 100644
--- a/CheckCXX11SharedPtr.cmake
+++ b/CheckCXX11SharedPtr.cmake
@@ -52,9 +52,3 @@ else()
message("-- Using boost::shared_ptr<T> (try -DENTITYX_USE_STD_SHARED_PTR=1 to use std::shared_ptr<T>)")
endif()
endif()
-
-configure_file(
- ${CMAKE_CURRENT_SOURCE_DIR}/entityx/config.h.in
- ${CMAKE_CURRENT_SOURCE_DIR}/entityx/config.h
-)
-
diff --git a/entityx/Entity.h b/entityx/Entity.h
index a4c7dec..0a8f3e7 100644
--- a/entityx/Entity.h
+++ b/entityx/Entity.h
@@ -170,6 +170,9 @@ struct EntityCreatedEvent : public Event<EntityCreatedEvent> {
};
+/**
+ * Called just prior to an entity being destroyed.
+ */
struct EntityDestroyedEvent : public Event<EntityDestroyedEvent> {
EntityDestroyedEvent(Entity entity) : entity(entity) {}
diff --git a/entityx/config.h.in b/entityx/config.h.in
index 346eb80..e1e4d67 100644
--- a/entityx/config.h.in
+++ b/entityx/config.h.in
@@ -4,10 +4,14 @@
#cmakedefine ENTITYX_HAVE_STD_SHARED_PTR 1
#cmakedefine ENTITYX_USE_STD_SHARED_PTR 1
#cmakedefine ENTITYX_MAX_COMPONENTS @ENTITYX_MAX_COMPONENTS@
+#cmakedefine ENTITYX_HAVE_BOOST_PYTHON 1
+#cmakedefine ENTITYX_INSTALLED_PYTHON_PACKAGE_DIR "@ENTITYX_INSTALLED_PYTHON_PACKAGE_DIR@"
// Which shared_ptr implementation should we use?
#if (ENTITYX_HAVE_STD_SHARED_PTR && ENTITYX_USE_STD_SHARED_PTR)
+
#include <memory>
+
namespace entityx {
using std::make_shared;
using std::shared_ptr;
@@ -15,10 +19,13 @@ using std::static_pointer_cast;
using std::weak_ptr;
using std::enable_shared_from_this;
}
+
#elif ENTITYX_HAVE_BOOST_SHARED_PTR
+
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/make_shared.hpp>
+
namespace entityx {
using boost::shared_ptr;
using boost::make_shared;
@@ -26,6 +33,7 @@ using boost::static_pointer_cast;
using boost::weak_ptr;
using boost::enable_shared_from_this;
}
+
#endif
namespace entityx {
diff --git a/entityx/python/PythonSystem.cc b/entityx/python/PythonSystem.cc
new file mode 100644
index 0000000..028ab50
--- /dev/null
+++ b/entityx/python/PythonSystem.cc
@@ -0,0 +1,206 @@
+/**
+ * Copyright (C) 2013 Alec Thomas <alec@swapoff.org>
+ * All rights reserved.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution.
+ *
+ * Author: Alec Thomas <alec@swapoff.org>
+ */
+
+// http://docs.python.org/2/extending/extending.html
+#include <Python.h>
+#include <cassert>
+#include <string>
+#include <iostream>
+#include <sstream>
+#include "entityx/python/PythonSystem.h"
+
+using namespace std;
+using namespace boost;
+namespace py = boost::python;
+
+namespace entityx {
+namespace python {
+
+
+static const py::object None;
+
+
+class PythonEntityXLogger {
+public:
+ PythonEntityXLogger() {}
+ PythonEntityXLogger(PythonSystem::LoggerFunction logger) : logger_(logger) {}
+
+ void write(const std::string &text) {
+ logger_(text);
+ }
+private:
+ PythonSystem::LoggerFunction logger_;
+};
+
+
+struct PythonEntity {
+ PythonEntity(Entity entity) : _entity(entity) {}
+
+ void destroy() {
+ _entity.destroy();
+ }
+
+ void update(float dt) {}
+
+ Entity _entity;
+};
+
+
+static std::string entity_repr(Entity entity) {
+ stringstream repr;
+ repr << "<Entity::Id " << entity.id() << ">";
+ return repr.str();
+}
+
+
+BOOST_PYTHON_MODULE(_entityx) {
+ py::class_<PythonEntityXLogger>("Logger", py::no_init)
+ .def("write", &PythonEntityXLogger::write);
+
+ py::class_<PythonEntity>("Entity", py::init<Entity>())
+ .def_readonly("_entity", &PythonEntity::_entity)
+ .def("update", &PythonEntity::update)
+ .def("destroy", &PythonEntity::destroy);
+
+ py::class_<Entity>("RawEntity", py::no_init)
+ .add_property("id", &Entity::id)
+ .def("__repr__", &entity_repr);
+
+ py::class_<PythonComponent, entityx::shared_ptr<PythonComponent>>("PythonComponent", py::init<py::object>())
+ .def("assign_to", &assign_to<PythonComponent>)
+ .def("get_component", &get_component<PythonComponent>)
+ .staticmethod("get_component");
+
+ py::class_<EntityManager, entityx::shared_ptr<EntityManager>, boost::noncopyable>("EntityManager", py::no_init)
+ .def("create", &EntityManager::create);
+}
+
+
+static void log_to_stderr(const std::string &text) {
+ cerr << "python stderr: " << text << endl;
+}
+
+static void log_to_stdout(const std::string &text) {
+ cout << "python stdout: " << text << endl;
+}
+
+// PythonSystem below here
+
+bool PythonSystem::initialized_ = false;
+
+PythonSystem::PythonSystem(entityx::shared_ptr<EntityManager> entity_manager)
+ : entity_manager_(entity_manager), stdout_(log_to_stdout), stderr_(log_to_stderr) {
+ if (!initialized_) {
+ initialize_python_module();
+ }
+ Py_Initialize();
+ if (!initialized_) {
+ init_entityx();
+ initialized_ = true;
+ }
+}
+
+PythonSystem::~PythonSystem() {
+ // FIXME: It would be good to do this, but it is not supported by boost::python:
+ // http://www.boost.org/doc/libs/1_53_0/libs/python/todo.html#pyfinalize-safety
+ // Py_Finalize();
+}
+
+void PythonSystem::add_installed_library_path() {
+ add_path(ENTITYX_INSTALLED_PYTHON_PACKAGE_DIR);
+}
+
+void PythonSystem::add_path(const std::string &path) {
+ python_paths_.push_back(path);
+}
+
+void PythonSystem::initialize_python_module() {
+ assert(PyImport_AppendInittab("_entityx", init_entityx) != -1 && "Failed to initialize _entityx Python module");
+}
+
+void PythonSystem::configure(entityx::shared_ptr<EventManager> event_manager) {
+ event_manager->subscribe<EntityDestroyedEvent>(*this);
+ event_manager->subscribe<ComponentAddedEvent<PythonComponent>>(*this);
+
+ try {
+ py::object main_module = py::import("__main__");
+ py::object main_namespace = main_module.attr("__dict__");
+
+ // Initialize logging.
+ py::object sys = py::import("sys");
+ sys.attr("stdout") = PythonEntityXLogger(stdout_);
+ sys.attr("stderr") = PythonEntityXLogger(stderr_);
+
+ // Add paths to interpreter sys.path
+ for (auto path : python_paths_) {
+ py::str dir = path.c_str();
+ sys.attr("path").attr("insert")(0, dir);
+ }
+
+ py::object entityx = py::import("_entityx");
+ entityx.attr("_entity_manager") = entity_manager_;
+ // entityx.attr("event_manager") = boost::ref(event_manager);
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ throw;
+ }
+}
+
+void PythonSystem::update(entityx::shared_ptr<EntityManager> entity_manager, entityx::shared_ptr<EventManager> event_manager, double dt) {
+ for (auto entity : entity_manager->entities_with_components<PythonComponent>()) {
+ shared_ptr<PythonComponent> python = entity.component<PythonComponent>();
+
+ try {
+ python->object.attr("update")(dt);
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ throw;
+ }
+ }
+}
+
+void PythonSystem::log_to(LoggerFunction stdout, LoggerFunction stderr) {
+ stdout_ = stdout;
+ stderr_ = stderr;
+}
+
+void PythonSystem::receive(const EntityDestroyedEvent &event) {
+ for (auto proxy : event_proxies_) {
+ proxy->delete_receiver(event.entity);
+ }
+}
+
+void PythonSystem::receive(const ComponentAddedEvent<PythonComponent> &event) {
+ // If the component was created in C++ it won't have a Python object
+ // associated with it. Create one.
+ if (!event.component->object) {
+ py::object module = py::import(event.component->module.c_str());
+ py::object from_raw_entity = module.attr(event.component->cls.c_str()).attr("_from_raw_entity");
+ if (py::len(event.component->args) == 0) {
+ event.component->object = from_raw_entity(event.entity);
+ } else {
+ py::list args;
+ args.append(event.entity);
+ args.extend(event.component->args);
+ event.component->object = from_raw_entity(*py::tuple(args));
+ }
+ }
+
+ for (auto proxy : event_proxies_) {
+ if (proxy->can_send(event.component->object)) {
+ proxy->add_receiver(event.entity);
+ }
+ }
+}
+
+}
+}
diff --git a/entityx/python/PythonSystem.h b/entityx/python/PythonSystem.h
new file mode 100644
index 0000000..8377f99
--- /dev/null
+++ b/entityx/python/PythonSystem.h
@@ -0,0 +1,256 @@
+/**
+ * Copyright (C) 2013 Alec Thomas <alec@swapoff.org>
+ * All rights reserved.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution.
+ *
+ * Author: Alec Thomas <alec@swapoff.org>
+ */
+
+#pragma once
+
+// http://docs.python.org/2/extending/extending.html
+#include <Python.h>
+#include "entityx/config.h"
+
+// boost::python smart pointer adapter for std::shared_ptr<T>
+#if (ENTITYX_HAVE_STD_SHARED_PTR && ENTITYX_USE_STD_SHARED_PTR)
+
+#include <boost/python.hpp>
+#include <memory>
+
+namespace std {
+
+// This may or may not work... it definitely does not work on OSX.
+template <class T> inline T * get_pointer(const std::shared_ptr<T> &p) {
+ return p.get();
+}
+
+}
+
+namespace boost {
+namespace python {
+
+template <typename T> struct pointee<std::shared_ptr<T> > {
+ typedef T type;
+};
+
+}
+}
+
+#endif
+
+#include <vector>
+#include <string>
+#include <boost/python.hpp>
+#include <boost/function.hpp>
+#include "entityx/System.h"
+#include "entityx/Entity.h"
+#include "entityx/Event.h"
+
+
+namespace entityx {
+namespace python {
+
+
+/**
+ * An EntityX component that represents a Python script.
+ */
+class PythonComponent : public entityx::Component<PythonComponent> {
+public:
+ /**
+ * Create a new PythonComponent from a Python class.
+ *
+ * @param module The Python module where the Entity subclass resides.
+ * @param cls The Class within the module.
+ * @param args The *args to pass to the Python constructor.
+ */
+ template <typename ...Args>
+ PythonComponent(const std::string &module, const std::string &cls, Args ... args) : module(module), cls(cls) {
+ unpack_args(args...);
+ }
+
+ /**
+ * Create a new PythonComponent from an existing Python instance.
+ */
+ PythonComponent(boost::python::object object) : object(object) {}
+
+ boost::python::object object;
+ boost::python::list args;
+ const std::string module, cls;
+
+private:
+ template <typename A, typename ...Args>
+ void unpack_args(A &arg, Args ... remainder) {
+ args.append(arg);
+ unpack_args(remainder...);
+ }
+
+ void unpack_args() {}
+};
+
+
+class PythonSystem;
+
+
+/**
+ * Proxies C++ EntityX events to Python entities.
+ */
+class PythonEventProxy {
+public:
+ friend class PythonSystem;
+
+ /**
+ * Construct a new event proxy.
+ *
+ * @param handler_name The default implementation of can_send() tests for
+ * the existence of this attribute on an Entity.
+ */
+ PythonEventProxy(const std::string &handler_name) : handler_name(handler_name) {}
+ virtual ~PythonEventProxy() {}
+
+ /**
+ * Return true if this event can be sent to the provided Python entity.
+ *
+ * @param object The Python entity to test for event delivery.
+ */
+ virtual bool can_send(const boost::python::object &object) const {
+ return PyObject_HasAttrString(object.ptr(), handler_name.c_str());
+ }
+
+protected:
+ std::list<Entity> entities;
+ const std::string handler_name;
+
+private:
+ /**
+ * Add an Entity receiver to this proxy. This is called automatically by PythonSystem.
+ *
+ * @param entity The entity that will receive events.
+ */
+ void add_receiver(Entity entity) {
+ entities.push_back(entity);
+ }
+
+ /**
+ * Delete an Entity receiver. This is called automatically by PythonSystem
+ * after testing with can_send().
+ *
+ * @param entity The entity that was receiving events.
+ */
+ void delete_receiver(Entity entity) {
+ for (auto i = entities.begin(); i != entities.end(); ++i) {
+ if (entity == *i) {
+ entities.erase(i);
+ break;
+ }
+ }
+ }
+};
+
+
+/**
+ * A helper function for class_ to assign a component to an entity.
+ */
+template <typename Component>
+void assign_to(entityx::shared_ptr<Component> component, Entity &entity) {
+ entity.assign<Component>(component);
+}
+
+
+/**
+ * A helper function for retrieving an existing component associated with an
+ * entity.
+ */
+template <typename Component>
+entityx::shared_ptr<Component> get_component(Entity &entity) {
+ return entity.component<Component>();
+}
+
+
+/**
+ * A PythonEventProxy that broadcasts events to all entities with a matching
+ * handler method.
+ */
+template <typename Event>
+class BroadcastPythonEventProxy : public PythonEventProxy, public Receiver<BroadcastPythonEventProxy<Event>> {
+public:
+ BroadcastPythonEventProxy(const std::string &handler_name) : PythonEventProxy(handler_name) {}
+ virtual ~BroadcastPythonEventProxy() {}
+
+ void receive(const Event &event) {
+ for (auto entity : entities) {
+ auto py_entity = entity.template component<PythonComponent>();
+ py_entity->object.attr(handler_name.c_str())(event);
+ }
+ }
+};
+
+
+class PythonSystem : public entityx::System<PythonSystem>, public entityx::Receiver<PythonSystem> {
+public:
+ typedef boost::function<void (const std::string &)> LoggerFunction;
+
+ PythonSystem(entityx::shared_ptr<EntityManager> entity_manager);
+ virtual ~PythonSystem();
+
+ /**
+ * Add system-installed entityx Python path to the interpreter.
+ */
+ void add_installed_library_path();
+
+ /**
+ * Add a Python path to the interpreter.
+ */
+ void add_path(const std::string &path);
+
+ /**
+ * Add a sequence of paths to the interpreter.
+ */
+ template <typename T>
+ void add_paths(const T &paths) {
+ for (auto path : paths) {
+ add_path(path);
+ }
+ }
+
+ const std::vector<std::string> &python_paths() const {
+ return python_paths_;
+ }
+
+ virtual void configure(entityx::shared_ptr<EventManager> event_manager) override;
+ virtual void update(entityx::shared_ptr<EntityManager> entities, entityx::shared_ptr<EventManager> event_manager, double dt) override;
+
+ /**
+ * Set functions that writes to sys.stdout/sys.stderr will be passed to.
+ */
+ void log_to(LoggerFunction stdout, LoggerFunction stderr);
+
+ template <typename Event>
+ void add_event_proxy(entityx::shared_ptr<EventManager> event_manager, const std::string &handler_name) {
+ auto proxy = entityx::make_shared<BroadcastPythonEventProxy<Event>>(handler_name);
+ event_manager->subscribe<Event>(*proxy);
+ event_proxies_.push_back(entityx::static_pointer_cast<PythonEventProxy>(proxy));
+ }
+
+ template <typename Event, typename Proxy>
+ void add_event_proxy(entityx::shared_ptr<EventManager> event_manager, entityx::shared_ptr<Proxy> proxy) {
+ event_manager->subscribe<Event>(*proxy);
+ event_proxies_.push_back(entityx::static_pointer_cast<PythonEventProxy>(proxy));
+ }
+
+ void receive(const EntityDestroyedEvent &event);
+ void receive(const ComponentAddedEvent<PythonComponent> &event);
+private:
+ void initialize_python_module();
+
+ entityx::shared_ptr<EntityManager> entity_manager_;
+ std::vector<std::string> python_paths_;
+ LoggerFunction stdout_, stderr_;
+ static bool initialized_;
+ std::vector<entityx::shared_ptr<PythonEventProxy>> event_proxies_;
+};
+
+}
+}
diff --git a/entityx/python/PythonSystem_test.cc b/entityx/python/PythonSystem_test.cc
new file mode 100644
index 0000000..bb8b2b0
--- /dev/null
+++ b/entityx/python/PythonSystem_test.cc
@@ -0,0 +1,242 @@
+/**
+ * Copyright (C) 2013 Alec Thomas <alec@swapoff.org>
+ * All rights reserved.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution.
+ *
+ * Author: Alec Thomas <alec@swapoff.org>
+ */
+
+// http://docs.python.org/2/extending/extending.html
+#include <Python.h>
+#include <cassert>
+#include <vector>
+#include <string>
+#include <gtest/gtest.h>
+#include <boost/python.hpp>
+#include "entityx/Entity.h"
+#include "entityx/Event.h"
+#include "entityx/python/PythonSystem.h"
+
+using namespace std;
+using namespace boost;
+namespace py = boost::python;
+using namespace entityx;
+using namespace entityx::python;
+
+
+struct Position : public Component<Position> {
+ Position(float x = 0.0, float y = 0.0) : x(x), y(y) {}
+
+ float x, y;
+};
+
+
+struct Direction : public Component<Direction> {
+ Direction(float x = 0.0, float y = 0.0) : x(x), y(y) {}
+
+ float x, y;
+};
+
+
+struct CollisionEvent : public Event<CollisionEvent> {
+ CollisionEvent(Entity a, Entity b) : a(a), b(b) {}
+
+ Entity a, b;
+};
+
+
+struct CollisionEventProxy : public PythonEventProxy, public Receiver<CollisionEvent> {
+ CollisionEventProxy() : PythonEventProxy("on_collision") {}
+
+ void receive(const CollisionEvent &event) {
+ for (auto entity : entities) {
+ auto py_entity = entity.component<PythonComponent>();
+ if (entity == event.a || entity == event.b) {
+ py_entity->object.attr("on_collision")(event);
+ }
+ }
+ }
+};
+
+
+BOOST_PYTHON_MODULE(entityx_python_test) {
+ py::class_<Position, entityx::shared_ptr<Position>>("Position", py::init<py::optional<float, float>>())
+ .def("assign_to", &assign_to<Position>)
+ .def("get_component", &get_component<Position>)
+ .staticmethod("get_component")
+ .def_readwrite("x", &Position::x)
+ .def_readwrite("y", &Position::y);
+ py::class_<Direction, entityx::shared_ptr<Direction>>("Direction", py::init<py::optional<float, float>>())
+ .def("assign_to", &assign_to<Direction>)
+ .def("get_component", &get_component<Direction>)
+ .staticmethod("get_component")
+ .def_readwrite("x", &Direction::x)
+ .def_readwrite("y", &Direction::y);
+ py::class_<CollisionEvent>("Collision", py::init<Entity, Entity>())
+ .def_readonly("a", &CollisionEvent::a)
+ .def_readonly("b", &CollisionEvent::b);
+}
+
+
+class PythonSystemTest : public ::testing::Test {
+protected:
+ PythonSystemTest() {
+ assert(PyImport_AppendInittab("entityx_python_test", initentityx_python_test) != -1 && "Failed to initialize entityx_python_test Python module");
+ }
+
+ void SetUp() {
+ ev = EventManager::make();
+ em = EntityManager::make(ev);
+ vector<string> paths;
+ paths.push_back(ENTITYX_PYTHON_TEST_DATA);
+ system = entityx::make_shared<PythonSystem>(em);
+ system->add_paths(paths);
+ if (!initialized) {
+ initentityx_python_test();
+ initialized = true;
+ }
+ system->add_event_proxy<CollisionEvent>(ev, entityx::make_shared<CollisionEventProxy>());
+ system->configure(ev);
+ }
+
+ void TearDown() {
+ system.reset();
+ em.reset();
+ ev.reset();
+ }
+
+ entityx::shared_ptr<PythonSystem> system;
+ entityx::shared_ptr<EventManager> ev;
+ entityx::shared_ptr<EntityManager> em;
+ static bool initialized;
+};
+
+bool PythonSystemTest::initialized = false;
+
+
+TEST_F(PythonSystemTest, TestSystemUpdateCallsEntityUpdate) {
+ try {
+ Entity e = em->create();
+ auto script = e.assign<PythonComponent>("entityx.tests.update_test", "UpdateTest");
+ ASSERT_FALSE(py::extract<bool>(script->object.attr("updated")));
+ system->update(em, ev, 0.1);
+ ASSERT_TRUE(py::extract<bool>(script->object.attr("updated")));
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestComponentAssignmentCreationInPython) {
+ try {
+ Entity e = em->create();
+ auto script = e.assign<PythonComponent>("entityx.tests.assign_test", "AssignTest");
+ ASSERT_TRUE(bool(e.component<Position>()));
+ ASSERT_TRUE(script->object);
+ ASSERT_TRUE(script->object.attr("test_assign_create"));
+ script->object.attr("test_assign_create")();
+ auto position = e.component<Position>();
+ ASSERT_TRUE(bool(position));
+ ASSERT_EQ(position->x, 1.0);
+ ASSERT_EQ(position->y, 2.0);
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestComponentAssignmentCreationInCpp) {
+ try {
+ Entity e = em->create();
+ e.assign<Position>(2, 3);
+ auto script = e.assign<PythonComponent>("entityx.tests.assign_test", "AssignTest");
+ ASSERT_TRUE(bool(e.component<Position>()));
+ ASSERT_TRUE(script->object);
+ ASSERT_TRUE(script->object.attr("test_assign_existing"));
+ script->object.attr("test_assign_existing")();
+ auto position = e.component<Position>();
+ ASSERT_TRUE(bool(position));
+ ASSERT_EQ(position->x, 3.0);
+ ASSERT_EQ(position->y, 4.0);
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestEntityConstructorArgs) {
+ try {
+ Entity e = em->create();
+ auto script = e.assign<PythonComponent>("entityx.tests.constructor_test", "ConstructorTest", 4.0, 5.0);
+ auto position = e.component<Position>();
+ ASSERT_TRUE(bool(position));
+ ASSERT_EQ(position->x, 4.0);
+ ASSERT_EQ(position->y, 5.0);
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestEventDelivery) {
+ try {
+ Entity f = em->create();
+ Entity e = em->create();
+ Entity g = em->create();
+ auto scripte = e.assign<PythonComponent>("entityx.tests.event_test", "EventTest");
+ auto scriptf = f.assign<PythonComponent>("entityx.tests.event_test", "EventTest");
+ ASSERT_FALSE(scripte->object.attr("collided"));
+ ASSERT_FALSE(scriptf->object.attr("collided"));
+ ev->emit<CollisionEvent>(f, g);
+ ASSERT_TRUE(scriptf->object.attr("collided"));
+ ASSERT_FALSE(scripte->object.attr("collided"));
+ ev->emit<CollisionEvent>(e, f);
+ ASSERT_TRUE(scriptf->object.attr("collided"));
+ ASSERT_TRUE(scripte->object.attr("collided"));
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestDeepEntitySubclass) {
+ try {
+ Entity e = em->create();
+ auto script = e.assign<PythonComponent>("entityx.tests.deep_subclass_test", "DeepSubclassTest");
+ ASSERT_TRUE(script->object.attr("test_deep_subclass"));
+ script->object.attr("test_deep_subclass")();
+
+ Entity e2 = em->create();
+ auto script2 = e2.assign<PythonComponent>("entityx.tests.deep_subclass_test", "DeepSubclassTest2");
+ ASSERT_TRUE(script2->object.attr("test_deeper_subclass"));
+ script2->object.attr("test_deeper_subclass")();
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
+
+
+TEST_F(PythonSystemTest, TestEntityCreationFromPython) {
+ try {
+ py::object test = py::import("entityx.tests.create_entities_from_python_test");
+ test.attr("create_entities_from_python_test")();
+ } catch (...) {
+ PyErr_Print();
+ PyErr_Clear();
+ ASSERT_FALSE(true) << "Python exception.";
+ }
+}
diff --git a/entityx/python/README.md b/entityx/python/README.md
new file mode 100644
index 0000000..17743b9
--- /dev/null
+++ b/entityx/python/README.md
@@ -0,0 +1,136 @@
+# Python Scripting System for EntityX
+
+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, in contract to the more pure entity-component system approach.
+
+## Concepts
+
+- Python scripts are attached to entities with `PythonComponent`.
+- Events are proxied directly to Python entities via `PythonEventProxy` objects.
+- `PythonSystem` manages scripted entity lifecycle and event delivery.
+
+## Overview
+
+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 `PythonComponent`, passing it the package name, class name, and any constructor arguments.
+
+## 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 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_<PythonPosition, entityx::shared_ptr<PythonPosition>>("Position", py::init<py::optional<float, float>>())
+ .def("assign_to", &entityx::python::assign_to<Position>)
+ .def("get_component", &entityx::python::get_component<Position>)
+ .staticmethod("get_component")
+ .def_readwrite("x", &PythonPosition::x)
+ .def_readwrite("y", &PythonPosition::y);
+}
+
+BOOST_PYTHON_MODULE(mygame) {
+ export_position_to_python();
+}
+```
+
+### 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 Event<CollisionEvent> {
+ CollisionEvent(Entity a, Entity b) : a(a), b(b) {}
+
+ Entity a, b;
+};
+
+struct CollisionEventProxy : public PythonEventProxy, public Receiver<CollisionEvent> {
+ CollisionEventProxy() : 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<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>())
+ .def_readonly("a", &CollisionEvent::a)
+ .def_readonly("b", &CollisionEvent::b);
+}
+
+
+BOOST_PYTHON_MODULE(mygame) {
+ export_position_to_python();
+ export_collision_event_to_python();
+}
+```
+
+### 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 and destroy `PythonSystem` as necessary:
+
+```
+// Initialize the PythonSystem.
+vector<string> paths;
+paths.push_back(MYGAME_PYTHON_PATH);
+// +any other Python paths...
+auto script_system = make_shared<PythonSystem>(paths);
+
+// Add any Event proxies.
+script_system->add_event_proxy<CollisionEvent>(
+ ev, entityx::make_shared<CollisionEventProxy>());
+```
diff --git a/entityx/python/entityx/__init__.py b/entityx/python/entityx/__init__.py
new file mode 100644
index 0000000..cdac6a6
--- /dev/null
+++ b/entityx/python/entityx/__init__.py
@@ -0,0 +1,102 @@
+import _entityx
+
+
+"""These classes provide a convenience layer on top of the raw entityx::python
+primitives.
+
+They allow you to declare your entities and components in an intuitive way:
+
+ class Player(Entity):
+ position = Component(Position)
+ direction = Component(Direction)
+ sprite = Component(Sprite, 'player.png') # Sprite component with constructor argument
+
+ def update(self, dt):
+ self.position.x += self.direction.x * dt
+ self.position.x += self.direction.y * dt
+
+Note that components assigned from C++ must be assigned prior to assigning
+PythonComponent, otherwise they will be created by the Entity constructor.
+"""
+
+
+__all__ = ['Entity', 'Component']
+
+
+class Component(object):
+ """A field that manages Component creation/retrieval.
+
+ Use like so:
+
+ class Player(Entity):
+ position = Component(Position)
+
+ def move_to(self, x, y):
+ self.position.x = x
+ self.position.y = y
+ """
+ def __init__(self, cls, *args, **kwargs):
+ self._cls = cls
+ self._args = args
+ self._kwargs = kwargs
+
+ def _build(self, entity):
+ component = self._cls.get_component(entity)
+ if not component:
+ component = self._cls(*self._args, **self._kwargs)
+ component.assign_to(entity)
+ return component
+
+
+class EntityMetaClass(_entityx.Entity.__class__):
+ """Collect registered components from class attributes.
+
+ This is done at class creation time to reduce entity creation overhead.
+ """
+
+ def __new__(cls, name, bases, dct):
+ dct['_components'] = components = {}
+ # Collect components from base classes
+ for base in bases:
+ if '_components' in base.__dict__:
+ components.update(base.__dict__['_components'])
+ # Collect components
+ for key, value in dct.items():
+ if isinstance(value, Component):
+ components[key] = value
+ return type.__new__(cls, name, bases, dct)
+
+
+class Entity(_entityx.Entity):
+ """Base Entity class.
+
+ Python Enitities differ in semantics from C++ components, in that they
+ contain logic, receive events, and so on.
+ """
+
+ __metaclass__ = EntityMetaClass
+
+ def __new__(cls, *args, **kwargs):
+ entity = kwargs.pop('raw_entity', None)
+ self = _entityx.Entity.__new__(cls)
+ if entity is None:
+ entity = _entityx._entity_manager.create()
+ component = _entityx.PythonComponent(self)
+ component.assign_to(entity)
+ _entityx.Entity.__init__(self, entity)
+ for k, v in self._components.items():
+ setattr(self, k, v._build(self._entity))
+ return self
+
+ def __init__(self):
+ """Default constructor."""
+
+ @classmethod
+ def _from_raw_entity(cls, raw_entity, *args, **kwargs):
+ """Create a new Entity from a raw entity.
+
+ This is called from C++.
+ """
+ self = Entity.__new__(cls, raw_entity=raw_entity)
+ cls.__init__(self, *args, **kwargs)
+ return self
diff --git a/entityx/python/entityx/tests/__init__.py b/entityx/python/entityx/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/entityx/python/entityx/tests/__init__.py
diff --git a/entityx/python/entityx/tests/assign_test.py b/entityx/python/entityx/tests/assign_test.py
new file mode 100644
index 0000000..670d9b9
--- /dev/null
+++ b/entityx/python/entityx/tests/assign_test.py
@@ -0,0 +1,18 @@
+from entityx import Entity, Component
+from entityx_python_test import Position
+
+
+class AssignTest(Entity):
+ position = Component(Position)
+
+ def test_assign_create(self):
+ assert self.position.x == 0.0, self.position.x
+ assert self.position.y == 0.0, self.position.y
+ self.position.x = 1
+ self.position.y = 2
+
+ def test_assign_existing(self):
+ assert self.position.x == 2, self.position.x
+ assert self.position.y == 3, self.position.y
+ self.position.x += 1
+ self.position.y += 1
diff --git a/entityx/python/entityx/tests/constructor_test.py b/entityx/python/entityx/tests/constructor_test.py
new file mode 100644
index 0000000..6dd62de
--- /dev/null
+++ b/entityx/python/entityx/tests/constructor_test.py
@@ -0,0 +1,10 @@
+from entityx import Entity, Component
+from entityx_python_test import Position
+
+
+class ConstructorTest(Entity):
+ position = Component(Position)
+
+ def __init__(self, x, y):
+ self.position.x = x
+ self.position.y = y
diff --git a/entityx/python/entityx/tests/create_entities_from_python_test.py b/entityx/python/entityx/tests/create_entities_from_python_test.py
new file mode 100644
index 0000000..41e8a9a
--- /dev/null
+++ b/entityx/python/entityx/tests/create_entities_from_python_test.py
@@ -0,0 +1,21 @@
+import entityx
+from entityx_python_test import Position
+
+
+class EntityA(entityx.Entity):
+ position = entityx.Component(Position, 1, 2)
+
+
+def create_entities_from_python_test():
+ a = EntityA()
+ assert a._entity.id == 0
+ assert a.position.x == 1.0
+ assert a.position.y == 2.0
+
+ b = EntityA()
+ assert b._entity.id == 1
+
+ a.destroy()
+ c = EntityA()
+ # Reuse destroyed entitys ID.
+ assert c._entity.id == 0
diff --git a/entityx/python/entityx/tests/deep_subclass_test.py b/entityx/python/entityx/tests/deep_subclass_test.py
new file mode 100644
index 0000000..7dc7ca5
--- /dev/null
+++ b/entityx/python/entityx/tests/deep_subclass_test.py
@@ -0,0 +1,24 @@
+from entityx import Entity, Component
+from entityx_python_test import Position, Direction
+
+
+class BaseEntity(Entity):
+ direction = Component(Direction)
+
+
+class DeepSubclassTest(BaseEntity):
+ position = Component(Position)
+
+ def test_deep_subclass(self):
+ assert self.direction
+ assert self.position
+
+
+class DeepSubclassTest2(DeepSubclassTest):
+ position2 = Component(Position)
+
+ def test_deeper_subclass(self):
+ assert self.direction
+ assert self.position
+ assert self.position2
+ assert self.position is self.position
diff --git a/entityx/python/entityx/tests/event_test.py b/entityx/python/entityx/tests/event_test.py
new file mode 100644
index 0000000..f73cf88
--- /dev/null
+++ b/entityx/python/entityx/tests/event_test.py
@@ -0,0 +1,10 @@
+from entityx import Entity
+
+
+class EventTest(Entity):
+ collided = False
+
+ def on_collision(self, event):
+ assert event.a
+ assert event.b
+ self.collided = True
diff --git a/entityx/python/entityx/tests/update_test.py b/entityx/python/entityx/tests/update_test.py
new file mode 100644
index 0000000..3c05d6a
--- /dev/null
+++ b/entityx/python/entityx/tests/update_test.py
@@ -0,0 +1,8 @@
+import entityx
+
+
+class UpdateTest(entityx.Entity):
+ updated = False
+
+ def update(self, dt):
+ self.updated = True
diff --git a/entityx/python/setup.py b/entityx/python/setup.py
new file mode 100644
index 0000000..50e2b03
--- /dev/null
+++ b/entityx/python/setup.py
@@ -0,0 +1,12 @@
+try:
+ from distribute import setup
+except ImportError:
+ from setuptools import setup
+
+setup(
+ name='entityx',
+ version='0.0.1',
+ packages=['entityx'],
+ license='MIT',
+ description='EntityX Entity-Component Framework Python bindings',
+ )
diff --git a/scripts/travis.sh b/scripts/travis.sh
index 0a502ca..3244b82 100755
--- a/scripts/travis.sh
+++ b/scripts/travis.sh
@@ -11,5 +11,5 @@ if [ "$USE_STD_SHARED_PTR" = "1" ]; then
fi
cmake ${CMAKE_ARGS}
-make
+make VERBOSE=1
make test