]> code.bitgloo.com Git - clyne/entityx.git/commitdiff
Add Python based scripting system (still experimental).
authorAlec Thomas <alec@swapoff.org>
Sat, 17 Aug 2013 03:53:44 +0000 (23:53 -0400)
committerAlec Thomas <alec@swapoff.org>
Sat, 17 Aug 2013 03:53:44 +0000 (23:53 -0400)
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.

19 files changed:
.travis.yml
CMakeLists.txt
CheckCXX11SharedPtr.cmake
entityx/Entity.h
entityx/config.h.in
entityx/python/PythonSystem.cc [new file with mode: 0644]
entityx/python/PythonSystem.h [new file with mode: 0644]
entityx/python/PythonSystem_test.cc [new file with mode: 0644]
entityx/python/README.md [new file with mode: 0644]
entityx/python/entityx/__init__.py [new file with mode: 0644]
entityx/python/entityx/tests/__init__.py [new file with mode: 0644]
entityx/python/entityx/tests/assign_test.py [new file with mode: 0644]
entityx/python/entityx/tests/constructor_test.py [new file with mode: 0644]
entityx/python/entityx/tests/create_entities_from_python_test.py [new file with mode: 0644]
entityx/python/entityx/tests/deep_subclass_test.py [new file with mode: 0644]
entityx/python/entityx/tests/event_test.py [new file with mode: 0644]
entityx/python/entityx/tests/update_test.py [new file with mode: 0644]
entityx/python/setup.py [new file with mode: 0644]
scripts/travis.sh

index 1f5acba0dd9ba0227a97b7c207935eee89ff0005..3bd2e8ed23ce21d0a918f174cee499df75e73f21 100644 (file)
@@ -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
index 3d9b9990a42260c31d1980a8f0837544b8e03166..fce2a507394ccb933926533f5d0968ff1cb2ad1c 100644 (file)
@@ -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
     )
index 24dc8716d8b82df4ea9e820851dc8fb92b946e33..0dc328190663f949a564f6a3b000e1553cb0f07c 100644 (file)
@@ -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
-)
-
index a4c7deca61ffd50a7dcc4730f846fe91453ebeb5..0a8f3e7c7cfd68fce20917d80263350e03505ec2 100644 (file)
@@ -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) {}
 
index 346eb80710ceb4219886f33c489e84471463842d..e1e4d6732969b09060e946e210c8fbeb44f15958 100644 (file)
@@ -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 (file)
index 0000000..028ab50
--- /dev/null
@@ -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 (file)
index 0000000..8377f99
--- /dev/null
@@ -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 (file)
index 0000000..bb8b2b0
--- /dev/null
@@ -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 (file)
index 0000000..17743b9
--- /dev/null
@@ -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 (file)
index 0000000..cdac6a6
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/entityx/python/entityx/tests/assign_test.py b/entityx/python/entityx/tests/assign_test.py
new file mode 100644 (file)
index 0000000..670d9b9
--- /dev/null
@@ -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 (file)
index 0000000..6dd62de
--- /dev/null
@@ -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 (file)
index 0000000..41e8a9a
--- /dev/null
@@ -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 (file)
index 0000000..7dc7ca5
--- /dev/null
@@ -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 (file)
index 0000000..f73cf88
--- /dev/null
@@ -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 (file)
index 0000000..3c05d6a
--- /dev/null
@@ -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 (file)
index 0000000..50e2b03
--- /dev/null
@@ -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',
+    )
index 0a502cafdb811f147796d41714b6b0946d1e005c..3244b82c5f7263ab4997f03a4304fd1c6bb319f3 100755 (executable)
@@ -11,5 +11,5 @@ if [ "$USE_STD_SHARED_PTR" = "1" ]; then
 fi
 
 cmake ${CMAKE_ARGS}
-make
+make VERBOSE=1
 make test