diff options
author | Alec Thomas <alec@swapoff.org> | 2013-12-20 20:57:02 -0500 |
---|---|---|
committer | Alec Thomas <alec@swapoff.org> | 2013-12-20 20:57:02 -0500 |
commit | 4a97a96a01b5acb75709587ac8ac5bf3d5d693ca (patch) | |
tree | cf4538693be4f4eb15689befe5be64b245880f73 | |
parent | d74c47d8a9c3acbdc8fdd6ff3712c66f4f90b7d5 (diff) |
Move Python support to https://github.com/alecthomas/entityx_python
-rw-r--r-- | CMakeLists.txt | 41 | ||||
-rw-r--r-- | entityx/python/PythonSystem.cc | 295 | ||||
-rw-r--r-- | entityx/python/PythonSystem.h | 262 | ||||
-rw-r--r-- | entityx/python/PythonSystem_test.cc | 277 | ||||
-rw-r--r-- | entityx/python/README.md | 165 | ||||
-rw-r--r-- | entityx/python/entityx/__init__.py | 111 | ||||
-rw-r--r-- | entityx/python/entityx/tests/__init__.py | 0 | ||||
-rw-r--r-- | entityx/python/entityx/tests/assign_test.py | 18 | ||||
-rw-r--r-- | entityx/python/entityx/tests/constructor_test.py | 10 | ||||
-rw-r--r-- | entityx/python/entityx/tests/create_entities_from_python_test.py | 33 | ||||
-rw-r--r-- | entityx/python/entityx/tests/deep_subclass_test.py | 26 | ||||
-rw-r--r-- | entityx/python/entityx/tests/event_emit_test.py | 13 | ||||
-rw-r--r-- | entityx/python/entityx/tests/event_test.py | 11 | ||||
-rw-r--r-- | entityx/python/entityx/tests/update_test.py | 8 | ||||
-rw-r--r-- | entityx/python/setup.py | 12 |
15 files changed, 0 insertions, 1282 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f49c75f..af6d17c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,6 @@ set(ENTITYX_RUN_BENCHMARKS false CACHE BOOL "Run benchmarks (in conjunction with set(ENTITYX_MAX_COMPONENTS 64 CACHE STRING "Set the maximum number of components.") set(ENTITYX_USE_CPP11_STDLIB true CACHE BOOL "For Clang, specify whether to use libc++ (-stdlib=libc++).") set(ENTITYX_BUILD_SHARED false CACHE BOOL "Build shared libraries?") -set(ENTITYX_BUILD_PYTHON false CACHE BOOL "Build entityx::python?") include(${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake) include(CheckCXXSourceCompiles) @@ -91,13 +90,6 @@ require(HAS_CXX11_LONG_LONG "C++11 lambdas") message("-- Checking misc features") require(HAVE_STDINT_H "stdint.h") -if (ENTITYX_BUILD_PYTHON) - message("-- Building with Python support (-DENTITYX_BUILD_PYTHON=0 to disable)") - find_package(Boost 1.48.0 COMPONENTS python) -else (ENTITYX_BUILD_PYTHON) - message("-- Python support disabled") -endif (ENTITYX_BUILD_PYTHON) - set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g") set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG") @@ -119,35 +111,6 @@ if (ENTITYX_BUILD_SHARED) list(APPEND install_libs entityx_shared) endif (ENTITYX_BUILD_SHARED) -if (ENTITYX_BUILD_PYTHON AND 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) - set(CMAKE_REQUIRED_FLAGS ${CMAKE_CXX_FLAGS}) -endif (ENTITYX_BUILD_PYTHON AND Boost_PYTHON_LIBRARY) - if (ENTITYX_BUILD_TESTING) #find_package(Boost 1.48.0 REQUIRED COMPONENTS timer system) add_subdirectory(gtest-1.6.0) @@ -158,10 +121,6 @@ if (ENTITYX_BUILD_TESTING) create_test(system_test entityx/System_test.cc) create_test(tags_component_test entityx/tags/TagsComponent_test.cc) create_test(dependencies_test entityx/deps/Dependencies_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) diff --git a/entityx/python/PythonSystem.cc b/entityx/python/PythonSystem.cc deleted file mode 100644 index 373948d..0000000 --- a/entityx/python/PythonSystem.cc +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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 <boost/noncopyable.hpp> -#include <Python.h> -#include <cassert> -#include <string> -#include <iostream> -#include <sstream> -#include "entityx/python/PythonSystem.h" - -namespace py = boost::python; - -namespace entityx { -namespace python { - - -static const py::object None; - - -class PythonEntityXLogger { -public: - PythonEntityXLogger() {} - explicit PythonEntityXLogger(PythonSystem::LoggerFunction logger) : logger_(logger) {} - ~PythonEntityXLogger() { flush(true); } - - void write(const std::string &text) { - line_ += text; - flush(); - } - -private: - void flush(bool force = false) { - size_t offset; - while ((offset = line_.find('\n')) != std::string::npos) { - std::string text = line_.substr(0, offset); - logger_(text); - line_ = line_.substr(offset + 1); - } - if (force && line_.size()) { - logger_(line_); - line_ = ""; - } - } - - PythonSystem::LoggerFunction logger_; - std::string line_; -}; - - -/** - * Base class for Python entities. - */ -struct PythonEntity { - explicit PythonEntity(ptr<EntityManager> entity_manager, Entity::Id id) : _entity(Entity(entity_manager, id)) {} // NOLINT - virtual ~PythonEntity() {} - - void destroy() { - _entity.destroy(); - } - - operator Entity () const { return _entity; } - - virtual void update(float dt, int frame) {} - - Entity::Id _entity_id() const { - return _entity.id(); - } - - Entity _entity; -}; - -static std::string PythonEntity_repr(const PythonEntity &entity) { - std::stringstream repr; - repr << "<Entity " << entity._entity.id().index() << "." << entity._entity.id().version() << ">"; - return repr.str(); -} - - -static std::string Entity_Id_repr(Entity::Id id) { - std::stringstream repr; - repr << "<Entity::Id " << id.index() << "." << id.version() << ">"; - return repr.str(); -} - - -// static std::string entity_repr(Entity entity) { -// std::stringstream repr; -// repr << "<Entity::Id " << entity.id().index() << "." << entity.id().version() << ">"; -// return repr.str(); -// } - -// static bool entity_eq(Entity left, Entity right) { -// return left.id() == right.id(); -// } - - -// A to-Python converter from Entity to PythonEntity. -struct EntityToPythonEntity { - static PyObject *convert(Entity entity) { - auto python = entity.component<PythonComponent>(); - assert(python && "Entity does not have a PythonComponent"); - return py::incref(python->object.ptr()); - } -}; - - -Entity::Id EntityManager_configure(ptr<EntityManager> entity_manager, py::object self) { - Entity entity = entity_manager->create(); - entity.assign<PythonComponent>(self); - return entity.id(); -} - -BOOST_PYTHON_MODULE(_entityx) { - py::to_python_converter<Entity, EntityToPythonEntity>(); - - py::class_<PythonEntityXLogger>("Logger", py::no_init) - .def("write", &PythonEntityXLogger::write); - - py::class_<BaseEvent, ptr<BaseEvent>, boost::noncopyable>("BaseEvent", py::no_init); - - py::class_<PythonEntity>("Entity", py::init<ptr<EntityManager>, Entity::Id>()) - .def_readonly("_entity_id", &PythonEntity::_entity_id) - .def("update", &PythonEntity::update) - .def("destroy", &PythonEntity::destroy) - .def("__repr__", &PythonEntity_repr); - - py::class_<Entity::Id>("EntityId", py::no_init) - .def_readonly("id", &Entity::Id::id) - .def_readonly("index", &Entity::Id::index) - .def_readonly("version", &Entity::Id::version) - .def("__repr__", &Entity_Id_repr); - - py::class_<PythonComponent, 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, ptr<EntityManager>, boost::noncopyable>("EntityManager", py::no_init) - .def("configure", &EntityManager_configure); - - void (EventManager::*emit)(const BaseEvent &) = &EventManager::emit; - - py::class_<EventManager, ptr<EventManager>, boost::noncopyable>("EventManager", py::no_init) - .def("emit", emit); - - py::implicitly_convertible<PythonEntity, Entity>(); -} - - -static void log_to_stderr(const std::string &text) { - std::cerr << "python stderr: " << text << std::endl; -} - -static void log_to_stdout(const std::string &text) { - std::cout << "python stdout: " << text << std::endl; -} - -// PythonSystem below here - -bool PythonSystem::initialized_ = false; - -PythonSystem::PythonSystem(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() { - try { - py::object entityx = py::import("_entityx"); - entityx.attr("_entity_manager").del(); - entityx.attr("_event_manager").del(); - py::object sys = py::import("sys"); - sys.attr("stdout").del(); - sys.attr("stderr").del(); - py::object gc = py::import("gc"); - gc.attr("collect")(); - } catch(...) { - PyErr_Print(); - PyErr_Clear(); - throw; - } - // 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(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_.lock(); - entityx.attr("_event_manager") = event_manager; - } catch(...) { - PyErr_Print(); - PyErr_Clear(); - throw; - } -} - -void PythonSystem::update(ptr<EntityManager> entity_manager, ptr<EventManager> event_manager, double dt) { - for (auto entity : entity_manager->entities_with_components<PythonComponent>()) { - ptr<PythonComponent> python = entity.component<PythonComponent>(); - - try { - python->object.attr("update")(dt, frame_); - } catch(...) { - PyErr_Print(); - PyErr_Clear(); - throw; - } - } - frame_++; -} - -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 cls = module.attr(event.component->cls.c_str()); - py::object from_raw_entity = cls.attr("_from_raw_entity"); - if (py::len(event.component->args) == 0) { - event.component->object = from_raw_entity(event.entity.id()); - } else { - py::list args; - args.append(event.entity.id()); - 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); - } - } -} - -} // namespace python -} // namespace entityx diff --git a/entityx/python/PythonSystem.h b/entityx/python/PythonSystem.h deleted file mode 100644 index d63fa38..0000000 --- a/entityx/python/PythonSystem.h +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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 <boost/python.hpp> -#include <boost/function.hpp> -#include <list> -#include <vector> -#include <string> -#include "entityx/System.h" -#include "entityx/Entity.h" -#include "entityx/Event.h" -#include "entityx/config.h" - -// boost::python smart pointer adapter for std::shared_ptr<T> -#ifdef ENTITYX_NEED_GET_POINTER_SHARED_PTR_SPECIALIZATION - -#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 std - - -#endif - - -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 Entity class. - * - * @param module The Python module where the Entity subclass resides. - * @param cls The Class within the module. Must inherit from entityx.Entity. - * @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. - */ - explicit 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. - */ - explicit 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. - * - * @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(ptr<Component> component, ptr<EntityManager> entity_manager, Entity::Id id) { - entity_manager->assign<Component>(id, component); -} - - -/** - * A helper function for retrieving an existing component associated with an - * entity. - */ -template <typename Component> -ptr<Component> get_component(ptr<EntityManager> entity_manager, Entity::Id id) { - return entity_manager->component<Component>(id); -} - - -/** - * 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); - } - } -}; - -/** - * An entityx::System that bridges EntityX and Python. - * - * This system handles exposing entityx functionality to Python. The Python - * support differs in design from the C++ design in the following ways: - * - * - Entities contain logic and can receive events. - * - Systems and Components can not be implemented in Python. - */ -class PythonSystem : public entityx::System<PythonSystem>, public entityx::Receiver<PythonSystem> { -public: - typedef boost::function<void (const std::string &)> LoggerFunction; - - PythonSystem(ptr<EntityManager> entity_manager); // NOLINT - 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); - } - } - - /// Return the Python paths the system is configured with. - const std::vector<std::string> &python_paths() const { - return python_paths_; - } - - virtual void configure(ptr<EventManager> event_manager) override; - virtual void update(ptr<EntityManager> entities, ptr<EventManager> event_manager, double dt) override; - - /** - * Set line-based (not including \n) logger for stdout and stderr. - */ - void log_to(LoggerFunction stdout, LoggerFunction stderr); - - /** - * Proxy events of type Event to any Python entity with a handler_name method. - */ - template <typename Event> - void add_event_proxy(ptr<EventManager> event_manager, const std::string &handler_name) { - ptr<BroadcastPythonEventProxy<Event>> proxy(new BroadcastPythonEventProxy<Event>(handler_name)); - event_manager->subscribe<Event>(*proxy.get()); - event_proxies_.push_back(static_pointer_cast<PythonEventProxy>(proxy)); - } - - /** - * Proxy events of type Event using the given PythonEventProxy implementation. - */ - template <typename Event, typename Proxy> - void add_event_proxy(ptr<EventManager> event_manager, ptr<Proxy> proxy) { - event_manager->subscribe<Event>(*proxy); - event_proxies_.push_back(static_pointer_cast<PythonEventProxy>(proxy)); - } - - void receive(const EntityDestroyedEvent &event); - void receive(const ComponentAddedEvent<PythonComponent> &event); - -private: - void initialize_python_module(); - - weak_ptr<EntityManager> entity_manager_; - std::vector<std::string> python_paths_; - LoggerFunction stdout_, stderr_; - static bool initialized_; - std::vector<ptr<PythonEventProxy>> event_proxies_; - int frame_ = 0; -}; - -} // namespace python -} // namespace entityx diff --git a/entityx/python/PythonSystem_test.cc b/entityx/python/PythonSystem_test.cc deleted file mode 100644 index d4d531c..0000000 --- a/entityx/python/PythonSystem_test.cc +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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> - */ - -// NOTE: MUST be first include. See http://docs.python.org/2/extending/extending.html -#include <Python.h> -#include <gtest/gtest.h> -#include <boost/python.hpp> -#include <cassert> -#include <vector> -#include <string> -#include <iostream> -#include "entityx/Entity.h" -#include "entityx/Event.h" -#include "entityx/python/PythonSystem.h" - -namespace py = boost::python; -using std::cerr; -using std::endl; -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) { - if (entity == event.a || entity == event.b) { - auto py_entity = entity.component<PythonComponent>(); - py_entity->object.attr("on_collision")(event); - } - } - } -}; - - -BOOST_PYTHON_MODULE(entityx_python_test) { - py::class_<Position, 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, 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, ptr<CollisionEvent>, py::bases<BaseEvent>>("Collision", py::init<Entity, Entity>()) - .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>())); -} - - -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.reset(new EventManager()); - em.reset(new EntityManager(ev)); - system.reset(new PythonSystem(em)); - system->add_path(ENTITYX_PYTHON_TEST_DATA); - if (!initialized) { - initentityx_python_test(); - initialized = true; - } - system->add_event_proxy<CollisionEvent>(ev, ptr<CollisionEventProxy>(new CollisionEventProxy())); - system->configure(ev); - } - - void TearDown() { - weak_ptr<EventManager> v = ev; - weak_ptr<EntityManager> e = em; - em->destroy_all(); - system.reset(); - em.reset(); - ev.reset(); - } - - ptr<PythonSystem> system; - ptr<EventManager> ev; - 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(static_cast<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(static_cast<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(static_cast<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(static_cast<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(static_cast<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"); - auto scriptg = g.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."; - } -} - - -TEST_F(PythonSystemTest, TestEventEmissionFromPython) { - try { - struct CollisionReceiver : public Receiver<CollisionReceiver> { - void receive(const CollisionEvent &event) { - a = event.a; - b = event.b; - } - - Entity a, b; - }; - - CollisionReceiver receiver; - ev->subscribe<CollisionEvent>(receiver); - - ASSERT_FALSE(receiver.a); - ASSERT_FALSE(receiver.b); - - py::object test = py::import("entityx.tests.event_emit_test"); - test.attr("emit_collision_from_python")(); - - ASSERT_TRUE(receiver.a); - ASSERT_TRUE(receiver.b); - } catch(...) { - PyErr_Print(); - PyErr_Clear(); - ASSERT_FALSE(true) << "Python exception."; - } -} diff --git a/entityx/python/README.md b/entityx/python/README.md deleted file mode 100644 index cf83ab5..0000000 --- a/entityx/python/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# Python Scripting System 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, in contract to the more pure entity-component system approach. - -## Limitations - -Planned features that are currently unimplemented: - -- Emitting events from Python. - -## Design - -- Python scripts are attached to entities via `PythonComponent`. -- 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 all `PythonComponent` entities with a `<handler_method>`. -- `PythonSystem` manages scripted entity lifecycle and event delivery. - -## Summary - -To add scripting support to your system, something like the following steps should be followed: - -1. Expose C++ `Component` and `Event` classes to Python with `BOOST_PYTHON_MODULE`. -2. Initialize the module with `PyImport_AppendInittab`. -3. Create a Python package. -4. Add classes to the package, inheriting from `entityx.Entity` and using the `entityx.Component` descriptor to assign components. -5. Create a `PythonSystem`, passing in the list of paths to add to Python's import search path. -6. Optionally attach any event proxies. -7. Create an entity and associate it with a Python script by assigning `PythonComponent`, passing it the package name, class name, and any constructor arguments. -8. When finished, call `EntityManager::destroy_all()`. - -## Interfacing with Python - -`entityx::python` primarily uses standard `boost::python` to interface with Python, with some helper classes and functions. - -### Exposing C++ Components to Python - -In most cases, this should be pretty simple. Given a component, provide a `boost::python` class definition wrapped in `entityx::ptr<T>`, 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: - -```c++ -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; -}; - -void export_position_to_python() { - py::class_<PythonPosition, ptr<PythonPosition>>("Position", py::init<py::optional<float, float>>()) - .def("assign_to", &assign_to<Position>) // Allows this component to be assigned to an entity - .def("get_component", &get_component<Position>) // Allows this component to be retrieved from an entity. - .staticmethod("get_component") // (as above) - .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: - -```c++ -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, ptr<CollisionEvent>, py::bases<BaseEvent>>("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(); -} -``` - - -### Sending events from Python - -This is relatively straight forward. Once you have exported a C++ event to Python: - -```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: - -```c++ -// 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: - -```c++ -// Initialize the PythonSystem. -vector<string> paths; -paths.push_back(MYGAME_PYTHON_PATH); -// +any other Python paths... -ptr<PythonSystem> python(new PythonSystem(paths)); - -// Add any Event proxies. -python->add_event_proxy<CollisionEvent>(ev, ptr<CollisionEventProxy>(new CollisionEventProxy())); -``` diff --git a/entityx/python/entityx/__init__.py b/entityx/python/entityx/__init__.py deleted file mode 100644 index 339c90b..0000000 --- a/entityx/python/entityx/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -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, frame): - 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_id): - component = self._cls.get_component(_entityx._entity_manager, entity_id) - if not component: - component = self._cls(*self._args, **self._kwargs) - component.assign_to(_entityx._entity_manager, entity_id) - 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_id = kwargs.pop('entity_id', None) - self = _entityx.Entity.__new__(cls) - if entity_id is None: - entity_id = _entityx._entity_manager.configure(self) - _entityx.Entity.__init__(self, _entityx._entity_manager, entity_id) - for k, v in self._components.items(): - setattr(self, k, v._build(self._entity_id)) - return self - - def __init__(self): - """Default constructor.""" - - def __repr__(self): - return '<%s.%s %d.%d>' % (self.__class__.__module__, self.__class__.__name__, self._entity_id.index, self._entity_id.version) - - @classmethod - def _from_raw_entity(cls, entity_id, *args, **kwargs): - """Create a new Entity from a raw entity. - - This is called from C++. - """ - self = Entity.__new__(cls, entity_id=entity_id) - cls.__init__(self, *args, **kwargs) - return self - - -def emit(event): - """Emit an event. - - :param event: A Python-exposed C++ subclass of entityx::BaseEvent. - """ - return _entityx._event_manager.emit(event) diff --git a/entityx/python/entityx/tests/__init__.py b/entityx/python/entityx/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/entityx/python/entityx/tests/__init__.py +++ /dev/null diff --git a/entityx/python/entityx/tests/assign_test.py b/entityx/python/entityx/tests/assign_test.py deleted file mode 100644 index 670d9b9..0000000 --- a/entityx/python/entityx/tests/assign_test.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 6dd62de..0000000 --- a/entityx/python/entityx/tests/constructor_test.py +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 8f8f3a4..0000000 --- a/entityx/python/entityx/tests/create_entities_from_python_test.py +++ /dev/null @@ -1,33 +0,0 @@ -import entityx -from entityx_python_test import Position - - -class EntityA(entityx.Entity): - position = entityx.Component(Position, 1, 2) - - def __init__(self, x=None, y=None): - if x is not None: - self.position.x = x - if y is not None: - self.position.y = y - - -def create_entities_from_python_test(): - a = EntityA() - assert a._entity_id.index == 0 - assert a.position.x == 1.0 - assert a.position.y == 2.0 - - b = EntityA() - assert b._entity_id.index == 1 - - a.destroy() - c = EntityA() - # Reuse destroyed index of "a". - assert c._entity_id.index == 0 - # However, version is different - assert a._entity_id.id != c._entity_id.id and c._entity_id.id > a._entity_id.id - - d = EntityA(2.0, 3.0) - assert d.position.x == 2.0 - assert d.position.y == 3.0 diff --git a/entityx/python/entityx/tests/deep_subclass_test.py b/entityx/python/entityx/tests/deep_subclass_test.py deleted file mode 100644 index 801b487..0000000 --- a/entityx/python/entityx/tests/deep_subclass_test.py +++ /dev/null @@ -1,26 +0,0 @@ -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.x == self.position2.x and self.position.y == self.position2.y - self.position.x += 1 - assert self.position.x == self.position2.x and self.position.y == self.position2.y diff --git a/entityx/python/entityx/tests/event_emit_test.py b/entityx/python/entityx/tests/event_emit_test.py deleted file mode 100644 index 4696e26..0000000 --- a/entityx/python/entityx/tests/event_emit_test.py +++ /dev/null @@ -1,13 +0,0 @@ -from entityx import Entity, emit -from entityx_python_test import Collision - - -class AnEntity(Entity): - pass - - -def emit_collision_from_python(): - a = AnEntity() - b = AnEntity() - collision = Collision(a, b) - emit(collision) diff --git a/entityx/python/entityx/tests/event_test.py b/entityx/python/entityx/tests/event_test.py deleted file mode 100644 index 8edf8c5..0000000 --- a/entityx/python/entityx/tests/event_test.py +++ /dev/null @@ -1,11 +0,0 @@ -from entityx import Entity - - -class EventTest(Entity): - collided = False - - def on_collision(self, event): - assert event.a - assert event.b - assert event.a == self or event.b == self - self.collided = True diff --git a/entityx/python/entityx/tests/update_test.py b/entityx/python/entityx/tests/update_test.py deleted file mode 100644 index 7585aa6..0000000 --- a/entityx/python/entityx/tests/update_test.py +++ /dev/null @@ -1,8 +0,0 @@ -import entityx - - -class UpdateTest(entityx.Entity): - updated = False - - def update(self, dt, frame): - self.updated = True diff --git a/entityx/python/setup.py b/entityx/python/setup.py deleted file mode 100644 index 50e2b03..0000000 --- a/entityx/python/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -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', - ) |