From 1200b2840de140fb0710dbe6cad78baeb68d3264 Mon Sep 17 00:00:00 2001 From: Billy Everyteen Date: Sun, 16 Jul 2017 02:38:56 +0200 Subject: [PATCH] Added AttachmentSystems + docs + example --- .gitignore | 1 + examples/17_local_coordinates/LICENSE | 20 + examples/17_local_coordinates/main.py | 361 ++++++++++ examples/17_local_coordinates/yourappname.kv | 214 ++++++ .../kivent_core/systems/attachment_system.pxd | 56 ++ .../kivent_core/systems/attachment_system.pyx | 635 ++++++++++++++++++ modules/core/setup.py | 5 +- modules/docs/source/gamesystems.rst | 27 + 8 files changed, 1317 insertions(+), 2 deletions(-) create mode 100644 examples/17_local_coordinates/LICENSE create mode 100644 examples/17_local_coordinates/main.py create mode 100644 examples/17_local_coordinates/yourappname.kv create mode 100644 modules/core/kivent_core/systems/attachment_system.pxd create mode 100644 modules/core/kivent_core/systems/attachment_system.pyx diff --git a/.gitignore b/.gitignore index c891fed1..1ade3205 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.pyc .idea *.c +*.cpp *.pyd *.swp *.egg-info diff --git a/examples/17_local_coordinates/LICENSE b/examples/17_local_coordinates/LICENSE new file mode 100644 index 00000000..0f3803d2 --- /dev/null +++ b/examples/17_local_coordinates/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013-2015 Jacob Kovac + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/17_local_coordinates/main.py b/examples/17_local_coordinates/main.py new file mode 100644 index 00000000..79f4418b --- /dev/null +++ b/examples/17_local_coordinates/main.py @@ -0,0 +1,361 @@ +from kivy.app import App +from kivy.uix.widget import Widget +from kivy.uix.treeview import TreeView, TreeViewLabel +from math import radians, degrees +from kivent_core.gameworld import GameWorld, ObjectProperty +from kivent_core.managers.resource_managers import texture_manager +from kivent_core.systems.renderers import RotateRenderer +from kivent_core.systems.position_systems import PositionSystem2D +from kivent_core.systems.rotate_systems import RotateSystem2D +from kivy.properties import StringProperty, NumericProperty, ListProperty, BooleanProperty +from kivent_core.systems.attachment_system import LocalPositionRotateSystem2D +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.dropdown import DropDown +from kivy.uix.button import Button +from kivy.graphics import Color, Rectangle +from os.path import dirname, join, abspath + +class AttachmentSystemDemoAPI(): + """ + This class represents a simple API for the LocalPositionRotateSystem2D system. + It is used here to give a better overview over the relevant demo code. + You do NOT need to wrap the AttachmentSystems in your code. + + The concept is the same for all other "local systems" like LocalPositionSystem2D. + """ + def __init__(self, gameworld, + local_position_system="local_position", + local_rotation_system="local_rotate", + attachment_system="attachment"): + self.local_position_system = local_position_system + self.local_rotation_system = local_rotation_system + self.attachment_system = gameworld.system_manager[attachment_system] + self.entities = gameworld.entities + self.gameworld = gameworld + + def attach_entity(self, child_id, parent_id): + """ + Attach one entity to another. + Both entities need to be part of the LocalPositionRotateSystem2D. + Local values in root entities (entities without parent) are ignored. + + You can safely attach entities already attached to an other entity. + It will be automatically detached from the previous parent. + """ + + # If you create cycles in the children parent relations (a->b->a) + # the whole subtree (a and b in this case) won't be updated anymore. + # Normally you'd want to avoid cycles. + attachment = getattr(self.entities[parent_id], + self.attachment_system.system_id) + if attachment.has_ancestor(child_id): + raise ValueError("Cycle in relationtree detected.") + # Alternative: + #if self.attachment_system.has_ancestor_by_id(parent_id, child_id): + self.attachment_system.attach_child(parent_id, child_id) + + def detach_entity(self, entity_id): + """ + Detach a child entity from the parent. + This will convert this entity to a root entity. + The global systems (position, rotate) will still hold the old values. + In other words the global position of this entity will not + change after detaching. + """ + attachment = getattr(self.entities[entity_id], + self.attachment_system.system_id) + if not attachment.is_root: + self.attachment_system.detach_child(entity_id) + + def remove_entity(self, entity_id): + """ + Remove an entity. + If an entity with children is removed all children will + be detached and become root entities. + """ + self.gameworld.remove_entity(entity_id) + + def remove_tree(self, entity_id): + """ + Removes an entity and its complete children tree. + """ + self.attachment_system.remove_subtree(entity_id) + + def set_local_coordinates(self, entity_id, x, y): + """ + Accessing the global or local system components is simple. + Just access the local or global system components + via dot lookup. + """ + position = getattr(self.entities[entitiy_id], + self.local_position_system) + position.x = x + position.y = y + + def get_local_coordinates(self, entity_id): + position = getattr(self.entities[entity_id], + self.local_position_system) + return (position.x, position.y) + + def set_local_rotation(self, entity_id, r): + rotation = getattr(self.entities[entity_id], + self.local_rotation_system) + rotation.r = radians(r) + + def get_local_rotation(self, entity_id): + rotation = getattr(self.entities[entity_id], + self.local_rotation_system) + return degrees(rotation.r) + + +class SimpleDropDown(BoxLayout): + """ + Simple DropDown wrapper to make handling a bit easier. + """ + text = StringProperty("Select one") + row_height = NumericProperty(44) + main_button = ObjectProperty(None) + selected = ObjectProperty(None) + background = ListProperty((1,1,1,1)) + + def __init__(self, *args, **kwargs): + super(SimpleDropDown, self).__init__() + self._dropdown = DropDown(size_hint_x=1) + self.orientation = "vertical" + self._dropdown.bind(on_select=self._selected) + with self._dropdown.canvas.before: + self._background_rect = Color(*self.background) + self.rect = Rectangle(size=self._dropdown.size, + pos=self._dropdown.pos) + self._dropdown.bind(pos=self.on_update_rect, size=self.on_update_rect) + + def on_update_rect(self, instance, value): + self.rect.pos = instance.pos + self.rect.size = instance.size + + def _selected(self, source, widget): + if widget != self.selected: + self.selected = widget + self.main_button.text = widget.text + + def on_main_button(self, _, value): + if self.main_button: + self.remove_widget(self.main_button) + self.main_button = value + self.main_button.bind(on_release=self._dropdown.open) + + def add_option(self, text, user_data=None): + button = Button(text=text, size_hint_y=None, height=self.row_height) + button._user_data = user_data + button.bind(on_release=self._dropdown.select) + self._dropdown.add_widget(button) + + def find_option(self, value, comparator=None): + children = self._dropdown.container.children + child = None + for child in children: + if comparator: + if comparator(child, value): + break + elif child.text == value: + break + else: + return None + return child + + def remove_option(self, value, comparator=None): + child = self.find_option(value, comparator) + if child: + self._dropdown.remove_widget(child) + + def select_option(self, value, comparator=None): + child = self.find_option(value, comparator) + if child: + self._dropdown.select(child) + + +def _create_treeview_item(text, user_data=None): + item = TreeViewLabel( + text=text, + is_open=True, + size_hint = (None, None) + ) + item._user_data = user_data + item.size=item.texture_size + item.text_size = item.size + item.width = 300 # this is a bit dirty, but how else ? + return item + + +texture_manager.load_image(join(dirname(dirname(abspath(__file__))), 'assets', + 'star3-blue.png')) + + +class TestGame(Widget): + def __init__(self, **kwargs): + super(TestGame, self).__init__(**kwargs) + self.entities = dict() + self._ent_default_color = (255,255,255,255) + self._ent_selected_color = (255,0,0,255) + self._selected = None + self.gameworld.init_gameworld( + ['attachment', 'local_position', 'local_rotate', 'rotate_color_renderer', 'rotate', 'color', 'position'], + callback=self.init_game) + + def init_game(self): + self.setup_states() + self.set_state() + gamescreen = self.ids.gamescreenmanager.ids.main_screen + self.entity_tree = gamescreen.ids.tree_view + self.entity_dropdown = gamescreen.ids.ent_dropdown + self.txt_local_x = gamescreen.ids.txt_local_x + self.txt_local_y = gamescreen.ids.txt_local_y + self.slider_rotate = gamescreen.ids.rotation_slider + self.entity_dropdown.add_option('None', -1) + + self.demoApi = AttachmentSystemDemoAPI( + self.gameworld, + "local_position", "local_rotate", "attachment") + + self.entity_tree.bind(selected_node=self.on_tree_node_selected) + self.txt_local_x.bind(focus=self.on_position_change) + self.txt_local_y.bind(focus=self.on_position_change) + self.slider_rotate.bind( + value=self.on_rotation_change) + + def create_entity(self, parent_id, local_position): + camera = self.gameworld.system_manager['camera1'] + create_component_dict = { + 'rotate_color_renderer': { + 'texture': 'star3-blue', + 'size': (50, 50), + 'render': True + }, + 'color': self._ent_default_color, + 'position': camera.get_camera_center(), + 'rotate': 0, + # Create root entities with 'parent':-1 or simply use an empty dict. + # Like 'attachment': {} + 'attachment': {'parent': parent_id}, + 'local_position': local_position, + 'local_rotate': 0, + } + component_order = ['rotate', 'color', + 'position', 'local_position', 'local_rotate', 'rotate_color_renderer', 'attachment'] + entity_id = self.gameworld.init_entity(create_component_dict, component_order) + return entity_id + + def on_add_entity(self): + parent = self._selected + if parent is None: + parent = -1 + tree_parent = None + else: + parent = parent.entity_id + tree_parent = self.entities[parent][1] + entity_id = self.create_entity(parent, (25,0), ) + entity = self.gameworld.entities[entity_id] + ent_name = 'Item_%i' % entity_id + tree_entry = _create_treeview_item(ent_name, entity_id) + self.entity_tree.add_node(tree_entry, tree_parent) + drop_entity = self.entity_dropdown.add_option(ent_name, entity_id) + self.entities[entity_id] = (entity, tree_entry, drop_entity) + self.entity_tree.select_node(tree_entry) + + def on_select_parent(self): + entity_id = self._selected + if entity_id is None: + return + entity_id = entity_id.entity_id + parent_id = self.entity_dropdown.selected + self.demoApi.detach_entity(entity_id) + tree_parent = None + if not parent_id is None: + parent_id = parent_id._user_data + if parent_id != -1: + self.demoApi.attach_entity(entity_id, parent_id) + tree_parent = self.entities[parent_id][1] + tree_entry = self.entities[entity_id][1] + self.entity_tree.remove_node(tree_entry) + self.entity_tree.add_node(tree_entry, tree_parent) + self.entity_tree.select_node(tree_entry) + + def on_position_change(self, instance, value): + if value: # skip got focus + return + if self._selected is None: + return + entity = self._selected + if instance == self.txt_local_x: + entity.local_position.x = int(self.txt_local_x.text) + else: + entity.local_position.y = int(self.txt_local_y.text) + + def on_rotation_change(self, instance, value): + if self._selected is None: + return + entity = self._selected + self.demoApi.set_local_rotation(entity.entity_id, value) + + def on_tree_node_selected(self, _, node): + if node is None: return + entity_id = node._user_data + entity = self.entities[entity_id][0] + if entity == self._selected: + return + # Restore old highlights + if not self._selected is None: + self._selected.color.rgb = self._ent_default_color + entity.color.rgb = self._ent_selected_color + # and update labels + self._selected = entity + x, y = self.demoApi.get_local_coordinates(entity_id) + self.txt_local_x.text = "%i" % x + self.txt_local_y.text = "%i" % y + self.slider_rotate.value = self.demoApi.get_local_rotation(entity_id) + parent_id = entity.attachment.parent + self.entity_dropdown.select_option( + parent_id, comparator=lambda n,v: n._user_data == v) + self.entity_tree.select_node(node) # Restore selected state on tree + + def on_remove_entity(self): + entity = self._selected + if entity is None: return + for child in entity.attachment.children: + tree_entry = self.entities[child][1] + self.entity_tree.remove_node(tree_entry) + self.entity_tree.add_node(tree_entry) + self.entity_tree.remove_node(self.entities[entity.entity_id][1]) + self._selected = None + del self.entities[entity.entity_id] + self.demoApi.remove_entity(entity.entity_id) + + def on_remove_entity_tree(self): + entity = self._selected + if entity is None: return + for child in entity.attachment.children: + tree_entry = self.entities[child][1] + self.entity_tree.remove_node(tree_entry) + del self.entities[child] + self.entity_tree.remove_node(self.entities[entity.entity_id][1]) + self._selected = None + del self.entities[entity.entity_id] + self.demoApi.remove_tree(entity.entity_id) + + def setup_states(self): + self.gameworld.add_state(state_name='main', + systems_added=['rotate_color_renderer'], + systems_removed=[], systems_paused=[], + systems_unpaused=['rotate_color_renderer'], + screenmanager_screen='main') + + def set_state(self): + self.gameworld.state = 'main' + + +class YourAppNameApp(App): + pass + + +if __name__ == '__main__': + YourAppNameApp().run() \ No newline at end of file diff --git a/examples/17_local_coordinates/yourappname.kv b/examples/17_local_coordinates/yourappname.kv new file mode 100644 index 00000000..e31c6533 --- /dev/null +++ b/examples/17_local_coordinates/yourappname.kv @@ -0,0 +1,214 @@ +#:kivy 1.9.0 +#:import path os.path +#:import dirname os.path.dirname +#:import re re +#:import main __main__ + + +TestGame: + +: + gameworld: gameworld + app: app + size: root.size + GameWorld: + z_index: 1 + id: gameworld + gamescreenmanager: gamescreenmanager + size_of_gameworld: 200*1024 + size_of_entity_block: 128 + zones: {'general': 2000} + PositionSystem2D: + system_id: 'local_position' + gameworld: gameworld + zones: ['general'] + RotateSystem2D: + system_id: 'local_rotate' + gameworld: gameworld + zones: ['general'] + LocalPositionRotateSystem2D: + system_id: 'attachment' + gameworld: root.gameworld + zones: ['general'] + PositionSystem2D: + system_id: 'position' + gameworld: gameworld + zones: ['general'] + RotateSystem2D: + system_id: 'rotate' + gameworld: gameworld + zones: ['general'] + ColorSystem: + system_id: 'color' + gameworld: gameworld + zones: ['general'] + RotateColorRenderer: + gameworld: gameworld + zones: ['general'] + max_batches: 100 + frame_count: 3 + updateable: True + size_of_batches: 256 + size_of_component_block: 128 + shader_source: path.join(dirname(dirname(path.abspath(main.__file__))), 'assets', 'glsl', 'positioncolorrotateshader.glsl') + gameview: 'camera1' + GameView: + system_id: 'camera1' + gameworld: gameworld + size: root.size + window_size: root.size #root.size + pos: root.pos + do_scroll: True + do_scroll_lock: True + GameScreenManager: + id: gamescreenmanager + size: root.size + pos: root.pos + gameworld: gameworld + +: + MainScreen: + id: main_screen + + +: + name: 'main' + z_index: 1 + AnchorLayout: + size: root.size + pos: root.pos + anchor_x: 'right' + + GridLayout: + cols: 1 + rows: 2 + pos: self.parent.pos + size_hint: (0.2, 1) + size_hint_min: (150, None) + size_hint_max: (300, None) + orientation: 'vertical' + canvas.before: + Color: + rgba: 0.0, 0.2, 0.2, 0.85 + Rectangle: + pos: self.pos + size: self.size + + ScrollView: + pos: (0, 0) + canvas.before: + Color: + rgba: 0.0, 0.2, 0.0, 0.85 + Rectangle: + pos: self.pos + size: self.size + TreeView: + size_hint: (None, None) + height: self.minimum_height + width: self.minimum_width + hide_root: True + id: tree_view + root_options: dict(text='Entities:') + + BoxLayout: + orientation: 'vertical' + Button: + text: "Add entity" + size_hint_min: (None, 20) + size_hint_max: (None, 50) + on_release: app.root.on_add_entity() + Button: + text: "Remove entity" + size_hint_min: (None, 20) + size_hint_max: (None, 50) + on_release: app.root.on_remove_entity() + Button: + text: "Remove subtree" + size_hint_min: (None, 20) + size_hint_max: (None, 50) + on_release: app.root.on_remove_entity_tree() + BoxLayout: # Spacer + size_hint: (None, 1) + + SimpleSectionLabel: + text: 'Local position:' + BoxLayout: + canvas.before: + Color: + rgba: 1.0, 0.2, 0.0, 0.85 + Rectangle: + pos: self.pos + size: self.size + orientation: 'horizontal' + size_hint: (1, None) + size: (0, 30) + # TODO. only allow integer + RegexInput: + id: txt_local_x + text: '0' + multiline: False + input_type: 'number' + input_filter: 'int' + RegexInput: + id: txt_local_y + text: '0' + multiline: False + input_type: 'number' + SimpleSectionLabel: + text: 'Local rotation:' + BoxLayout: + canvas.before: + Color: + rgba: 1.0, 0.2, 0.0, 0.85 + Rectangle: + pos: self.pos + size: self.size + orientation: 'horizontal' + size_hint: (1, None) + size: (0, 50) + Slider: + id: rotation_slider + value: 360 + range: (-360, 360) + step: 1 + Label: + id: slider_label + text: "%i" % rotation_slider.value + multiline: False + size_hint: (None, 1) + size: (self.font_size * 3, self.texture_size[1]) + valign: 'center' + canvas.before: + Color: + rgba: 1.0, 0.4, 0.0, 0.85 + Rectangle: + pos: self.pos + size: self.size + + SimpleSectionLabel: + text: 'Select parent:' + Button: + id: parent_dropdown_select + text: "Select parent" + size_hint_min: (None, 20) + size_hint_max: (None, 50) + SimpleDropDown: + id: ent_dropdown + main_button: parent_dropdown_select + on_selected: app.root.on_select_parent() + + + + _regex: re.compile('^\-?[0-9]*$') + input_filter: lambda s,undo: s if self._regex.match(self.text+s) else "" + + + + + halign: 'left' + size_hint: (1, None) + size: self.texture_size + text_size: self.size + bold: True + padding_y: 5 + \ No newline at end of file diff --git a/modules/core/kivent_core/systems/attachment_system.pxd b/modules/core/kivent_core/systems/attachment_system.pxd new file mode 100644 index 00000000..07f79145 --- /dev/null +++ b/modules/core/kivent_core/systems/attachment_system.pxd @@ -0,0 +1,56 @@ +# distutils: language = c++ +from kivent_core.systems.staticmemgamesystem cimport StaticMemGameSystem, MemComponent +from kivent_core.systems.position_systems cimport PositionStruct2D +from kivent_core.systems.rotate_systems cimport RotateStruct2D + +from libcpp.set cimport set as cpp_set +from libcpp.unordered_set cimport unordered_set +from libcpp.queue cimport queue as cpp_queue +from libcpp.stack cimport stack +from libcpp.vector cimport vector +from cython cimport bint +from libc.stdint cimport uintptr_t + +ctypedef struct RelationStruct: + unsigned int entity_id + cpp_set[RelationStruct*] *children + RelationStruct *parent + unsigned int components_index + uintptr_t user_data + +cdef class RelationComponent(MemComponent): + cdef void* get_descendants(self, vector[RelationStruct*] *output) except NULL + +cdef class RelationTreeSystem(StaticMemGameSystem): + cdef unordered_set[RelationStruct*] root_nodes + cdef unsigned int _state + + cdef RelationStruct* _attach_child(self, RelationStruct* parent_socket, + RelationStruct *child_socket) except NULL + cdef RelationStruct* _attach_child_by_id(self, unsigned int parent_id, + unsigned int child_id) except NULL + + cdef unsigned int _detach_child(self, RelationStruct* parent_socket) except 0 + cdef unsigned int _detach_child_by_id(self, unsigned int child_id) except 0 + + cdef void* get_descendants(self, RelationStruct *parent, + vector[RelationStruct*] *output) except NULL + cdef void* get_topdown_iterator(self, vector[RelationStruct*] *output) except NULL + cdef bint has_ancestor(self, RelationStruct* entity, unsigned int ancestor) + cpdef bint has_ancestor_by_id(self, unsigned int entity_id, unsigned int ancestor) + +cdef class LocalPositionSystem2D(RelationTreeSystem): + cdef bint _allocated + cdef vector[RelationStruct*] _work_queue + cdef unsigned int _parent_offset + cdef unsigned int _last_socket_state + cdef unsigned int _update(self, float dt, + vector[RelationStruct*] *work_queue) except 0 + cdef unsigned int _init_component(self, unsigned entity_id, + unsigned int component_index, + unsigned int components_index, + RelationStruct *relation_struct) except 0 + +cdef class LocalPositionRotateSystem2D(LocalPositionSystem2D): + pass + \ No newline at end of file diff --git a/modules/core/kivent_core/systems/attachment_system.pyx b/modules/core/kivent_core/systems/attachment_system.pyx new file mode 100644 index 00000000..0b374cb1 --- /dev/null +++ b/modules/core/kivent_core/systems/attachment_system.pyx @@ -0,0 +1,635 @@ +# distutils: language = c++ +# cython: embedsignature=True + +from kivy.properties import ( + BooleanProperty, StringProperty, NumericProperty, ListProperty, ObjectProperty + ) +from kivent_core.managers.entity_manager cimport EntityManager +from kivent_core.managers.system_manager cimport SystemManager +from kivent_core.systems.staticmemgamesystem cimport StaticMemGameSystem, MemComponent +from kivent_core.memory_handlers.zone cimport MemoryZone +from kivent_core.memory_handlers.membuffer cimport Buffer +from kivent_core.memory_handlers.indexing cimport IndexedMemoryZone +from kivent_core.systems.position_systems cimport PositionStruct2D +from kivent_core.systems.rotate_systems cimport RotateStruct2D +from kivy.factory import Factory +from kivent_core.systems.position_systems cimport PositionComponent2D, PositionSystem2D +from kivent_core.systems.rotate_systems cimport RotateComponent2D, RotateSystem2D +from kivent_core.systems.gamesystem cimport GameSystem +from libc.math cimport sin, cos + + +cdef class RelationComponent(MemComponent): + ''' + The RelationComponent holds a list of all entities attached to the + entity holding this component. + + Its main use is to iterate over RelationTrees. + + **Attributes:** + **entity_id** (unsigned int): The entity_id this component is currently + associated with. Will be -1 if the component is + unattached. + + **parent** (unsigned int): The id of the parent or -1 + if this element doesn't have a parent. + + **children** (list): A list of all entity_ids of the child entities. + + **is_root** (bool): True if this component has no parent. + + **descendants** (list): A list of all descendants entity_ids. + ''' + property entity_id: + def __get__(self): + cdef RelationStruct* data = self.pointer + return data.entity_id + + property parent: + def __get__(self): + cdef RelationStruct* data = self.pointer + if data.parent == NULL: + return -1 + return data.parent.entity_id + + property children: + def __get__(self): + cdef RelationStruct* data = self.pointer + if data.children == NULL: + return [] + return list(x.entity_id for x in data.children[0]) + + property is_root: + def __get__(self): + cdef RelationStruct* data = self.pointer + return data.parent == NULL + + property descendants: + def __get__(self): + cdef vector[RelationStruct*] tree + self.get_descendants(&tree) + cdef RelationStruct *x + return [ x.entity_id for x in tree ] + + def has_ancestor(self, unsigned int ancestor): + ''' + Tests if the given entity_id is an ancestor of this entity. + Usefull to prevent cycles when attaching entities. + + **Args:** + **ancestor** (unsigned int): The entity_id to test for. + ''' + cdef RelationStruct* entity = self.pointer + if entity.entity_id == ancestor: + return True + while entity.parent: + if entity.parent.entity_id == ancestor: + return True + entity = entity.parent + return False + + cdef void* get_descendants(self, + vector[RelationStruct*] *output) except NULL: + cdef RelationStruct *parent = self.pointer + if parent.children == NULL: + return parent + cdef RelationStruct *child + cdef RelationStruct *current + cdef unsigned int pos = 0 + for child in parent.children[0]: + output[0].push_back(child) + cdef unsigned int size = output.size() + while pos < size: + current = output[0][pos] + if current.children and current.children[0].size(): + for child in current.children[0]: + output[0].push_back(child) + size += 1 + pos += 1 + return parent + + + +cdef class RelationTreeSystem(StaticMemGameSystem): + ''' + Processing Depends only on itself. + + A flexible Relationship system which can be used by different GameSystems + which need to attach entities to others in one way or another. + Currently its main use is create local coordinate systems. + + When removing a parent component all childs are detached + and changed to root nodes. + If you want to remove a parent and its childs use the **remove_subtree method**. + + **Attributes: (Cython Access Only):** + **root_nodes** (unordered_set[RelationStruct*]): All sockets which + aren't itself children of other sockets. + It is usefull if you want to iterate over the attachment tree from top + to bottom (see the **get_topdown_iterator** function for an example). + + **_state** (unsigned int): A simple (wrap around) counter which is + increased after every change to the relationsship tree. + Usefull to check if the tree has changed since the last update tick + when the iteration order is cached. + ''' + type_size = NumericProperty(sizeof(RelationStruct)) + component_type = ObjectProperty(RelationComponent) + updateable = BooleanProperty(False) + processor = BooleanProperty(False) + system_names = ListProperty(['relations']) + system_id = StringProperty('relations') + + def __init__(self, **kwargs): + super(RelationTreeSystem, self).__init__(**kwargs) + self._state = 0 + + def init_component(self, unsigned int component_index, + unsigned int entity_id, str zone, args): + """ + A **RelationComponent** is initialized with an args dict containing + a 'parent' key holding the parents entity_id. + + If there is no 'parent' key or the value is -1 the entity will become + a root entity. + """ + cdef MemoryZone memory_zone = self.imz_components.memory_zone + cdef RelationStruct* pointer = memory_zone.get_pointer( + component_index) + pointer.entity_id = entity_id + self.root_nodes.insert(pointer) + + cdef RelationStruct* _attach_child(self, RelationStruct* parent, RelationStruct* child) except NULL: + ''' + Register a child as attachment of a parent. + + **Parameter: + **parent** (RelationStruct*) + + **child** (RelationStruct*) + ''' + if child.parent != NULL: + self._detach_child(child) + child.parent = parent + if parent.children == NULL: + parent.children = new cpp_set[RelationStruct*]() + parent.children[0].insert(child) + self.root_nodes.erase(child) + self._state = (self._state + 1) % -1 + return child + + cdef RelationStruct* _attach_child_by_id(self, unsigned int parent_id, unsigned int child_id) except NULL: + cdef MemoryZone my_memory = self.imz_components.memory_zone + cdef IndexedMemoryZone entities = self.gameworld.entities + cdef unsigned int system_index = self.system_index + 1 + cdef unsigned int* entity = entities.get_pointer(child_id) + cdef RelationStruct *child_struct = my_memory.get_pointer( + entity[system_index]) + entity = entities.get_pointer(parent_id) + cdef RelationStruct *parent_struct = my_memory.get_pointer( + entity[system_index]) + return self._attach_child(parent_struct, child_struct) + + cdef unsigned int _detach_child(self, RelationStruct* child) except 0: + ''' + Deregister an attachment. + The child will be changed to a root node. + + **Parameter: + **child** (RelationStruct*) + ''' + if child.parent == NULL or child.parent.children == NULL: + raise ValueError("Can't detach entities without parent.") # TODO: correct exception + child.parent.children[0].erase(child) + self.root_nodes.insert(child) + child.parent = NULL + self._state = (self._state + 1) % -1 + return 1 + + cdef unsigned int _detach_child_by_id(self, unsigned int child_id) except 0: + cdef MemoryZone my_memory = self.imz_components.memory_zone + cdef IndexedMemoryZone entities = self.gameworld.entities + cdef unsigned int system_index = self.system_index + 1 + cdef unsigned int* entity = entities.get_pointer(child_id) + cdef RelationStruct *child_struct = my_memory.get_pointer( + entity[system_index]) + return self._detach_child(child_struct) + + def remove_component(self, unsigned int component_index): + ''' + Typically this will be called automatically by GameWorld. + If you want to remove a component without destroying the entity call this function directly. + + **Args:** + **component_index** (unsigned int): the component_id to be removed. + ''' + cdef MemoryZone socket_memory = self.imz_components.memory_zone + cdef RelationStruct* pointer = socket_memory.get_pointer( + component_index) + cdef RelationStruct* child + if pointer.parent: + pointer.parent.children[0].erase(pointer) + if pointer.children: + for child in pointer.children[0]: + self._detach_child(child) + pointer.children[0].clear() + del pointer.children + self.root_nodes.erase(pointer) + self._state = (self._state + 1) % -1 + super(RelationTreeSystem, self).remove_component(component_index) + + def clear_component(self, unsigned int component_index): + cdef MemoryZone memory_zone = self.imz_components.memory_zone + cdef RelationStruct* pointer = memory_zone.get_pointer( + component_index) + pointer.entity_id = -1 + pointer.parent = NULL + pointer.children = NULL + pointer.components_index = -1 + + cdef void* get_descendants(self, RelationStruct *parent, + vector[RelationStruct*] *output) except NULL: + ''' + Append all descendants of a given entity in a top down fashion + to the output vector. + It is guaranteed that every entity in the list is + located after its parent. + ''' + if parent.children == NULL: + return parent + cdef RelationStruct *child + cdef RelationStruct *current + cdef unsigned int pos = output.size() + for child in parent.children[0]: + output[0].push_back(child) + cdef unsigned int size = output.size() + while pos < size: + current = output[0][pos] + if current.children and current.children[0].size(): + for child in current.children[0]: + output[0].push_back(child) + size += 1 + pos += 1 + return parent + + cdef void* get_topdown_iterator(self, vector[RelationStruct*] *output) except NULL: + ''' + Fills the output vector with all non root nodes in a top down fashion. + ''' + cdef RelationStruct *parent + output[0].clear() + for parent in self.root_nodes: + self.get_descendants(parent, output) + return output + + cdef bint has_ancestor(self, RelationStruct* entity, unsigned int ancestor): + if entity.entity_id == ancestor: + return 1 + while entity.parent: + if entity.parent.entity_id == ancestor: + return 1 + entity = entity.parent + return 0 + + cpdef bint has_ancestor_by_id(self, unsigned int entity_id, unsigned int ancestor): + """ + Tests if the entity has the given ancestor.. + Usefull to prevent cycles when attaching entities. + + **Args:** + **entity_id** (unsigned int): The entities id. + + **ancestor** (unsigned int): The ancestors id. + """ + cdef MemoryZone my_memory = self.imz_components.memory_zone + cdef IndexedMemoryZone entities = self.gameworld.entities + cdef unsigned int system_index = self.system_index + 1 + cdef unsigned int* entity = entities.get_pointer(entity_id) + cdef RelationStruct *ent_struct = my_memory.get_pointer( + entity[system_index]) + return self.has_ancestor(ent_struct, ancestor) + + def attach_child(self, unsigned int parent_id, unsigned int child_id): + """ + Register a child as attachment of a parent. + If the child entity is alreay attached to another parent + it will be detached before. + + **Args:** + **parent_id** (unsigned int) + + **child_id** (unsigned int) + """ + self._attach_child_by_id(parent_id, child_id) + + def detach_child(self, unsigned int child_id): + """ + Deregister an attachment. + The child will become a a root entity. + + **Args:** + **child_id** (unsigned int) + """ + self._detach_child_by_id(child_id) + + def remove_subtree(self, unsigned int entity_id): + """ + Remove the given entity and all its descendants from the gameworld. + + **Args:** + **entity_id** (unsigned int) + """ + gameworld = self.gameworld + remove_entity = gameworld.remove_entity + cdef MemoryZone my_memory = self.imz_components.memory_zone + cdef IndexedMemoryZone entities = gameworld.entities + cdef unsigned int system_index = self.system_index + 1 + cdef unsigned int* entity = entities.get_pointer(entity_id) + if entity[system_index] == -1: + raise ValueError('Entity has no %s component' % self.system_name) + cdef RelationStruct *pointer = my_memory.get_pointer( + entity[system_index]) + cdef RelationStruct* cur_parent + cdef RelationStruct* cur_child + # We need to remove all children recursive, but due to recursion limit + # we replace the recursion with a stack based approach. + cdef stack[RelationStruct*] child_stack + child_stack.push(pointer) + while not child_stack.empty(): + cur_parent = child_stack.top() + if cur_parent.children == NULL or cur_parent.children[0].size() == 0: + child_stack.pop() + if cur_parent != pointer: + remove_entity(cur_parent.entity_id) + continue + if cur_parent.children != NULL: + for cur_child in cur_parent.children[0]: + child_stack.push(cur_child) + remove_entity(pointer.entity_id) + self._state = (self._state + 1) % -1 + +Factory.register('RelationTreeSystem', cls=RelationTreeSystem) + + +class ChangeAfterAllocationException(): + pass + + +cdef class LocalPositionSystem2D(RelationTreeSystem): + ''' + Processing Depends On: LocalPositionSystem2D, PositionSystem2D + + The **LocalPositionSystem2D** allows to attach entities to other entities to + construct local coordinate systems. + Local coordinates (offset) are available. + + **Attributes:** + **system_names** (list): Shall contain the system id of the **PositionSystem2D** + which will be used to store the **global** position. + + **local_systems** (list): Shall contain the system id of the **PositionSystem2D** + which will be used for the **local** position. + + **parent_systems** (list): Shall contain the system id of the **PositionSystem2D** + which will be used for the **parents** position. + In most cases this will be the same value as used for the global system. + + .. note:: Root nodes also need to own the local position component \ + even if they aren't used. + + .. note:: This system can be used to implement other local systems in cython. \ + Extend it, select the required components in the **system_names**, **local_systems** \ + and **parent_systems** properties and overwrite the **update** method. \ + For more information see **LocalPositionRotateSystem2D** source code. + ''' + updateable = BooleanProperty(True) + processor = BooleanProperty(True) + # own components global position + system_names = ListProperty([ + 'position' + ]) + local_systems = ListProperty([ + 'local_position' + ]) + parent_systems = ListProperty([ + 'position' + ]) + system_id = StringProperty('local_position_system') + + def __init__(self, **kwargs): + self._parent_offset = len(self.system_names) + len(self.local_systems) + self._last_socket_state = 0 + self._allocated = 0 + super(LocalPositionSystem2D, self).__init__(**kwargs) + + def allocate(self, Buffer master_buffer, dict reserve_spec): + # We use our own component as placeholder for the parent components + # because we might not have the related parent components. + cdef str my_component = self.system_names[0] + self.system_names = list(self.system_names + self.local_systems + + list(my_component for _ in self.parent_systems)) + self._allocated = 1 + return super(LocalPositionSystem2D, self).allocate(master_buffer, reserve_spec) + + def on_parent_systems(self, _, v): + if self._allocated: + raise ChangeAfterAllocationException( + "Can't change 'parent_systems' after system allocation.") + self.parent_systems = list(v) + def on_local_systems(self, _, v): + if self._allocated: + raise ChangeAfterAllocationException( + "Can't change 'local_systems' after system allocation.") + self.local_systems = list(v) + def on_system_names(self, _, v): + if self._allocated: + raise ChangeAfterAllocationException( + "Can't change 'system_names' after system allocation.") + self.system_names = list(v) + + cdef unsigned int _init_component(self, unsigned entity_id, + unsigned int component_index, + unsigned int components_index, + RelationStruct *relation_struct) except 0: + ''' + Overwrite this method if you want to set the user_data. + You can safely store pointer in the user_data field. + ''' + return 1 + + def init_component(self, unsigned int component_index, + unsigned int entity_id, str zone, dict args): + """ + A **RelationComponent** is initialized with an args dict containing + a 'parent' key holding the parents entity_id. + + If there is no 'parent' key or the value is -1 the entity will become + a root entity and its local coordinates are ignored. + """ + super(LocalPositionSystem2D, self).init_component( + component_index, entity_id, zone, args) + cdef MemoryZone memory_zone = self.imz_components.memory_zone + cdef RelationStruct *relation_struct = memory_zone.get_pointer( + component_index) + cdef unsigned int ent_comps_ind = self.entity_components.add_entity( + entity_id, zone) + relation_struct.components_index = ent_comps_ind + if not 'parent' in args or args['parent'] == -1: + return + cdef unsigned int parent_id = args['parent'] + self._attach_child_by_id(parent_id, entity_id) + self._init_component(entity_id, component_index, + ent_comps_ind, relation_struct) + + def remove_component(self, unsigned int component_index): + """ + Typically this will be called automatically by GameWorld. + If you want to remove a component without destroying the entity + call this function directly. + + If you want to override the behavior of component cleanup override + clear_component instead. + Only override this function if you are working directly with the + storage of components for your system. + + **Args:** + **component_index** (unsigned int): the component_id to be removed. + """ + cdef MemoryZone memory_zone = self.imz_components.memory_zone + cdef RelationStruct *pointer = memory_zone.get_pointer( + component_index) + self.entity_components.remove_entity(pointer.components_index) + super(LocalPositionSystem2D, self).remove_component(component_index) + + cdef RelationStruct* _attach_child(self, RelationStruct* parent, RelationStruct* child) except NULL: + RelationTreeSystem._attach_child(self, parent, child) + # We need to set or update the parent component pointer + cdef SystemManager system_manager = self.gameworld.system_manager + cdef IndexedMemoryZone entities = self.gameworld.entities + cdef unsigned int *parent_entity = entities.get_pointer( + parent.entity_id) + cdef unsigned int ent_comp_index = child.components_index + cdef str system_name + cdef unsigned int system_index + cdef StaticMemGameSystem system + cdef MemoryZone system_memory + cdef unsigned int comp_index + cdef void *pointer + cdef unsigned int component_count = self.entity_components.count + cdef void** component_data = ( + self.entity_components.memory_block.data) + cdef real_index = ent_comp_index * component_count + cdef unsigned int i = self._parent_offset + for system_name in self.parent_systems: + system = system_manager[system_name] + system_index = system.system_index + 1 + comp_index = parent_entity[system_index] + if comp_index == -1: + raise ValueError("Attachments parent has no '%s' component." % system_name) + system_memory = system.imz_components.memory_zone + pointer = system_memory.get_pointer(comp_index) + component_data[real_index + i] = pointer + i += 1 + return child + + cdef unsigned int _update(self, float dt, vector[RelationStruct*] *work_queue) except 0: + cdef void** component_data = ( + self.entity_components.memory_block.data) + cdef unsigned int component_count = self.entity_components.count + cdef PositionStruct2D *global_pos + cdef PositionStruct2D *local_pos + cdef PositionStruct2D *parent_pos + cdef RelationStruct *parent + cdef unsigned int real_index + for parent in work_queue[0]: + if parent == NULL: + continue + real_index = parent.components_index * component_count + global_pos = component_data[real_index + 0] + local_pos = component_data[real_index + 1] + parent_pos = component_data[real_index + 2] + global_pos.x = parent_pos.x + local_pos.x + global_pos.y = parent_pos.y + local_pos.y + return 1 + + def update(self, dt): + # We need to update the values in the correct order. + # TODO: switch to static memory ? + cdef vector[RelationStruct*] *work_queue = &self._work_queue + if self._state != self._last_socket_state: + self.get_topdown_iterator(work_queue) + self._last_socket_state = self._state + self._update(dt, work_queue) + +Factory.register('LocalPositionSystem2D', cls=LocalPositionSystem2D) + + +cdef class LocalPositionRotateSystem2D(LocalPositionSystem2D): + """ + Processing Depends On: LocalPositionRotateSystem2D, PositionSystem2D, + RotateSystem2D + + The **LocalPositionRotateSystem2D** allows to attach entities to other entities to + construct local coordinate systems. + Local coordinates and local rotation are available. + + **Attributes:** + **system_names** (list): Shall contain the system id of the **PositionSystem2D** + and **RotateSystem2D** which will be used to store the **global** position + and rotation. + + **local_systems** (list): Shall contain the system id of the **PositionSystem2D** + and **RotateSystem2D** which will be used for the **local** position and rotation. + + **parent_systems** (list): Shall contain the system id of the **PositionSystem2D** + and **RotateSystem2D** which will be used for the **parents** position and rotation. + In most cases this will be the same values as used for the global system. + + .. note:: Root nodes also need to own the local position and local rotation component \ + even if they aren't used. + """ + # own components global position + system_names = ListProperty([ + 'position', 'rotate' + ]) + local_systems = ListProperty([ + 'local_position', 'local_rotate' + ]) + parent_systems = ListProperty([ + 'position', 'rotate' + ]) + system_id = StringProperty('local_position_rotate_system') + + cdef unsigned int _update(self, float dt, vector[RelationStruct*] *work_queue) except 0: + cdef void** component_data = ( + self.entity_components.memory_block.data) + cdef unsigned int component_count = self.entity_components.count + cdef PositionStruct2D *global_pos + cdef PositionStruct2D *local_pos + cdef PositionStruct2D *parent_pos + cdef RotateStruct2D *global_rot + cdef RotateStruct2D *local_rot + cdef RotateStruct2D *parent_rot + cdef RelationStruct *parent + cdef unsigned int real_index + cdef float cs, sn + for parent in work_queue[0]: + if parent == NULL: + continue + real_index = parent.components_index * component_count + global_pos = component_data[real_index + 0] + global_rot = component_data[real_index + 1] + local_pos = component_data[real_index + 2] + local_rot = component_data[real_index + 3] + parent_pos = component_data[real_index + 4] + parent_rot = component_data[real_index + 5] + cs = cos(parent_rot.r) + sn = sin(parent_rot.r) + global_pos.x = parent_pos.x + ( + local_pos.x * cs - local_pos.y * sn) + global_pos.y = parent_pos.y + ( + local_pos.x * sn + local_pos.y * cs) + global_rot.r = parent_rot.r + local_rot.r + return 1 + +Factory.register('LocalPositionRotateSystem2D', cls=LocalPositionRotateSystem2D) \ No newline at end of file diff --git a/modules/core/setup.py b/modules/core/setup.py index b80f8111..9a5f4196 100644 --- a/modules/core/setup.py +++ b/modules/core/setup.py @@ -124,7 +124,7 @@ 'systems': [ 'gamesystem', 'staticmemgamesystem', 'position_systems', 'gameview', 'scale_systems', 'rotate_systems', 'color_systems', - 'gamemap', 'renderers', 'lifespan', 'animation', + 'gamemap', 'renderers', 'lifespan', 'animation', 'attachment_system' ], } @@ -138,8 +138,9 @@ module_files = modules[name] for module_name in module_files: core_modules[prefix + module_name] = [file_prefix + module_name + '.pyx'] - core_modules_c[prefix + module_name] = [file_prefix + module_name + '.c'] + core_modules_c[prefix + module_name] = [file_prefix + module_name + '.c', file_prefix + module_name + '.cpp'] check_for_removal.append(file_prefix + module_name + '.c') + check_for_removal.append(file_prefix + module_name + '.cpp') def build_ext(ext_name, files, include_dirs=[]): diff --git a/modules/docs/source/gamesystems.rst b/modules/docs/source/gamesystems.rst index ed92c9d4..66de3b0d 100644 --- a/modules/docs/source/gamesystems.rst +++ b/modules/docs/source/gamesystems.rst @@ -106,6 +106,33 @@ Rendering Systems .. autoclass:: kivent_core.systems.renderers.ScaledPolyRenderer :members: +Local coordinate system +======================= + +You can use local coordinates by using the **LocalPositionSystem2D** or +**LocalPositionRotateSystem2D**. +Both systems will implement a parent/children relationsship tree +and will update the children position (and rotation in case of the +**LocalPositionRotateSystem2D** system) according to their parents +and its own local systems. +All entities using this systems need to own the local and global components +required. + +.. autoclass:: kivent_core.systems.attachment_system.RelationComponent + :members: + +.. autoclass:: kivent_core.systems.attachment_system.RelationTreeSystem + :show-inheritance: + :members: attach_child, detach_child, remove_component, create_component, init_component, remove_subtree, has_ancestor_by_id + +.. autoclass:: kivent_core.systems.attachment_system.LocalPositionSystem2D + :show-inheritance: + :members: attach_child, detach_child, remove_component, create_component, init_component, remove_subtree, has_ancestor_by_id + +.. autoclass:: kivent_core.systems.attachment_system.LocalPositionRotateSystem2D + :show-inheritance: + :members: attach_child, detach_child, remove_component, create_component, init_component, remove_subtree, has_ancestor_by_id + Controlling the Viewing Area ============================