diff --git a/modules/tiled/example/assets/maps/test_desert/desert.tmx b/modules/tiled/example/assets/maps/test_desert/desert.tmx
new file mode 100644
index 00000000..6d948deb
--- /dev/null
+++ b/modules/tiled/example/assets/maps/test_desert/desert.tmx
@@ -0,0 +1,70 @@
+
+
diff --git a/modules/tiled/example/assets/maps/test_desert/tmw_desert_spacing.png b/modules/tiled/example/assets/maps/test_desert/tmw_desert_spacing.png
new file mode 100644
index 00000000..fde05ac8
Binary files /dev/null and b/modules/tiled/example/assets/maps/test_desert/tmw_desert_spacing.png differ
diff --git a/modules/tiled/example/assets/sharders/positionshader.glsl b/modules/tiled/example/assets/sharders/positionshader.glsl
new file mode 100644
index 00000000..36630f8c
--- /dev/null
+++ b/modules/tiled/example/assets/sharders/positionshader.glsl
@@ -0,0 +1,44 @@
+---VERTEX SHADER---
+#ifdef GL_ES
+ precision highp float;
+#endif
+
+/* Outputs to the fragment shader */
+varying vec4 frag_color;
+varying vec2 tex_coord0;
+
+/* vertex attributes */
+attribute vec2 pos;
+attribute vec2 uvs;
+
+/* uniform variables */
+uniform mat4 modelview_mat;
+uniform mat4 projection_mat;
+uniform vec4 color;
+uniform float opacity;
+
+void main (void) {
+ frag_color = color * vec4(1.0, 1., 1.0, opacity);
+ tex_coord0 = uvs;
+ vec4 new_pos = vec4(pos.xy, 0.0, 1.0);
+ gl_Position = projection_mat * modelview_mat * new_pos;
+
+}
+
+
+---FRAGMENT SHADER---
+#ifdef GL_ES
+ precision highp float;
+#endif
+
+/* Outputs from the vertex shader */
+varying vec4 frag_color;
+varying vec2 tex_coord0;
+
+/* uniform texture samplers */
+uniform sampler2D texture0;
+
+void main (void){
+
+ gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
+}
diff --git a/modules/tiled/example/assets/textures/testatlas-0.png b/modules/tiled/example/assets/textures/testatlas-0.png
new file mode 100644
index 00000000..1a0fb51e
Binary files /dev/null and b/modules/tiled/example/assets/textures/testatlas-0.png differ
diff --git a/modules/tiled/example/assets/textures/testatlas.atlas b/modules/tiled/example/assets/textures/testatlas.atlas
new file mode 100644
index 00000000..63c1c7b8
--- /dev/null
+++ b/modules/tiled/example/assets/textures/testatlas.atlas
@@ -0,0 +1 @@
+{"testatlas-0.png": {"desert-38": [172, 410, 32, 32], "desert-39": [2, 444, 32, 32], "desert-34": [36, 478, 32, 32], "desert-35": [444, 478, 32, 32], "desert-36": [104, 410, 32, 32], "desert-37": [342, 478, 32, 32], "desert-30": [410, 478, 32, 32], "desert-31": [206, 410, 32, 32], "desert-32": [410, 444, 32, 32], "desert-33": [444, 410, 32, 32], "desert-16": [36, 410, 32, 32], "desert-17": [138, 444, 32, 32], "desert-14": [206, 444, 32, 32], "desert-15": [172, 444, 32, 32], "desert-12": [478, 444, 32, 32], "desert-13": [274, 478, 32, 32], "desert-10": [104, 444, 32, 32], "desert-11": [308, 478, 32, 32], "desert-18": [376, 410, 32, 32], "desert-19": [444, 444, 32, 32], "desert-8": [138, 410, 32, 32], "desert-9": [240, 444, 32, 32], "desert-4": [308, 444, 32, 32], "desert-5": [2, 478, 32, 32], "desert-6": [342, 444, 32, 32], "desert-7": [240, 410, 32, 32], "desert-1": [104, 478, 32, 32], "desert-2": [342, 410, 32, 32], "desert-3": [308, 410, 32, 32], "desert-29": [36, 376, 32, 32], "desert-28": [2, 410, 32, 32], "desert-23": [172, 478, 32, 32], "desert-22": [138, 478, 32, 32], "desert-21": [478, 478, 32, 32], "desert-20": [206, 478, 32, 32], "desert-27": [36, 444, 32, 32], "desert-26": [410, 410, 32, 32], "desert-25": [240, 478, 32, 32], "desert-24": [478, 410, 32, 32], "desert-48": [70, 376, 32, 32], "desert-41": [376, 478, 32, 32], "desert-40": [70, 444, 32, 32], "desert-43": [70, 478, 32, 32], "desert-42": [274, 444, 32, 32], "desert-45": [376, 444, 32, 32], "desert-44": [70, 410, 32, 32], "desert-47": [2, 376, 32, 32], "desert-46": [274, 410, 32, 32]}}
\ No newline at end of file
diff --git a/modules/tiled/example/example.kv b/modules/tiled/example/example.kv
new file mode 100644
index 00000000..f744a403
--- /dev/null
+++ b/modules/tiled/example/example.kv
@@ -0,0 +1,86 @@
+#:kivy 1.9.0
+#:import main __main__
+#:import path os.path
+#:import dirname os.path.dirname
+
+#:import GameWorld kivent_core.gameworld.GameWorld
+#:import Renderer kivent_core.systems.renderers.Renderer
+#:import TiledGameMap kivent_tiles.systems.gamemap.TiledGameMap
+#:import TileAnimator kivent_tiles.systems.animation.TiledAnimator
+#:import PositionSystem2D kivent_core.systems.position_systems.PositionSystem2D
+
+
+TestGame:
+:
+ gameworld: gameworld
+ app: app
+ GameWorld:
+ id: gameworld
+ gamescreenmanager: gamescreenmanager
+ size_of_gameworld: 20 * 1024
+ zones: {'general': 20 * 1024}
+ PositionSystem2D:
+ system_id: 'position'
+ gameworld: gameworld
+ zones: ['general']
+ Renderer:
+ id: map_renderer
+ shader_source: path.join(dirname(path.abspath(main.__file__)), 'assets/sharders/positionshader.glsl')
+ system_id: 'map_renderer'
+ gameworld: gameworld
+ gameview: 'camera'
+ zones: ['general']
+ system_names: ['map_renderer', 'position']
+ frame_count: 1
+ force_update: True
+ static_rendering: True
+ GameView:
+ id: gameview
+ currentmap: tilemap
+ system_id: 'camera'
+ gameworld: gameworld
+ size: root.size
+ pos: root.pos
+ do_touch_zoom: True
+ updateable: True
+ TiledAnimator:
+ id: map_animator
+ system_id: 'map_animator'
+ renderer_obj: map_renderer
+ gameworld: gameworld
+ updateable: True
+ TiledGameMap:
+ id: tilemap
+ system_id: 'tilemap'
+ map_source: path.join(dirname(path.abspath(main.__file__)), 'assets/maps/test_desert/desert.tmx')
+ atlas_source: path.join(dirname(path.abspath(main.__file__)), 'assets/textures/testatlas.atlas')
+ gameworld: gameworld
+ gameview: 'camera'
+ renderer: 'map_renderer'
+ animator_obj: map_animator
+ GameScreenManager:
+ id: gamescreenmanager
+ size: root.size
+ pos: root.pos
+ gameworld: gameworld
+
+:
+ MainScreen:
+ id: main_screen
+
+:
+ name: 'main'
+ FloatLayout:
+ DebugPanel:
+ size_hint: (0.2, 0.1)
+ pos_hint: {'x': 0.225, 'y': 0.025}
+
+:
+ Label:
+ pos: root.pos
+ size: root.size
+ font_size: root.size[1] * 0.5
+ halign: 'center'
+ valign: 'middle'
+ color: (1, 1, 1, 1)
+ text: 'FPS: ' + root.fps if root.fps != None else 'FPS:'
diff --git a/modules/tiled/example/main.py b/modules/tiled/example/main.py
new file mode 100644
index 00000000..fcaee493
--- /dev/null
+++ b/modules/tiled/example/main.py
@@ -0,0 +1,55 @@
+from kivy.app import App
+from kivy.clock import Clock
+from kivy.uix.widget import Widget
+from kivy.properties import StringProperty
+
+import kivent_core
+
+
+class TestGame(Widget):
+ def __init__(self, **kwargs):
+ super(TestGame, self).__init__(**kwargs)
+ self.gameworld.init_gameworld(
+ ['position', 'map_renderer', 'camera', 'map_animator', 'tilemap'],
+ callback=self.init_game)
+
+ def init_game(self):
+ self.setup_states()
+ self.set_state()
+ self.ids.tilemap.init()
+
+ def update(self, dt):
+ self.gameworld.update(dt)
+
+ def setup_states(self):
+ self.gameworld.add_state(
+ state_name='main',
+ systems_added=['map_renderer', 'map_animator'],
+ systems_removed=[],
+ systems_paused=[],
+ systems_unpaused=['map_renderer', 'map_animator'],
+ screenmanager_screen='main')
+
+ def set_state(self):
+ self.gameworld.state = 'main'
+
+
+class DebugPanel(Widget):
+ fps = StringProperty(None)
+
+ def __init__(self, **kwargs):
+ super(DebugPanel, self).__init__(**kwargs)
+ Clock.schedule_once(self.update_fps)
+
+ def update_fps(self, dt):
+ self.fps = str(int(Clock.get_fps()))
+ Clock.schedule_once(self.update_fps, .05)
+
+
+class ExampleApp(App):
+ def build(self):
+ pass
+
+
+if __name__ == '__main__':
+ ExampleApp().run()
diff --git a/modules/tiled/kivent_tiled/__init__.py b/modules/tiled/kivent_tiled/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/tiled/kivent_tiled/systems/__init__.py b/modules/tiled/kivent_tiled/systems/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/tiled/kivent_tiled/systems/animation.py b/modules/tiled/kivent_tiled/systems/animation.py
new file mode 100644
index 00000000..62239106
--- /dev/null
+++ b/modules/tiled/kivent_tiled/systems/animation.py
@@ -0,0 +1,48 @@
+from fractions import gcd
+from itertools import ifilter
+
+from kivy.factory import Factory
+from kivent_core.systems.gamesystem import GameSystem
+from kivy.properties import ListProperty, ObjectProperty, BooleanProperty
+
+
+class AnimationSystem(GameSystem):
+ dirty = BooleanProperty(False)
+ renderer_obj = ObjectProperty(None)
+
+ def update(self, dt):
+ system_id = self.system_id
+ renderer = self.renderer_obj
+ renderer_id = renderer.system_id
+ entities = self.gameworld.entities
+ for component in ifilter(None, self.components):
+ entity = entities[component.entity_id]
+ animation_comp = getattr(entity, system_id)
+ frame = animation_comp.current_frame
+ animation_comp.timer += dt
+ if animation_comp.timer >= frame.duration_seconds:
+ animation_comp.timer = 0
+ animation_comp.current_frame = frame = next(animation_comp.frames)
+ render_comp = getattr(entity, renderer_id)
+ render_comp.texture_key = frame.image
+ if not self.dirty:
+ self.dirty = True
+
+ if self.dirty:
+ renderer.update_trigger()
+ self.dirty = False
+
+
+class TiledAnimationSystem(AnimationSystem):
+ durations = ListProperty()
+
+ @property
+ def has_animations(self):
+ return bool(self.durations)
+
+ def set_update_time(self):
+ self.update_time = float(reduce(gcd, self.durations)) / 1000
+
+
+Factory.register('AnimationSystem', cls=AnimationSystem)
+Factory.register('TiledAnimationSystem', cls=TiledAnimationSystem)
diff --git a/modules/tiled/kivent_tiled/systems/gamemap.py b/modules/tiled/kivent_tiled/systems/gamemap.py
new file mode 100644
index 00000000..70bbeb36
--- /dev/null
+++ b/modules/tiled/kivent_tiled/systems/gamemap.py
@@ -0,0 +1,130 @@
+from itertools import cycle
+
+from kivy.factory import Factory
+from tmxloader.loader import TileMap
+from kivent_core.systems.gamemap import GameMap
+from kivy.properties import StringProperty, ObjectProperty
+from kivent_core.managers.resource_managers import texture_manager
+
+from ..utils import name_from_tile, name_from_source
+
+
+class TiledGameMap(GameMap):
+ map_source = StringProperty(None)
+ atlas_source = StringProperty(None)
+ system_id = StringProperty('tilemap')
+ renderer = StringProperty('map_renderer')
+ animator_obj = ObjectProperty(None)
+
+ def __init__(self, *args, **kwargs):
+ super(TiledGameMap, self).__init__(*args, **kwargs)
+ self.map = None
+ self.tiles = []
+ self.tile_number = None
+
+ def init(self):
+ self.load_tmx_file()
+ self.load_textures()
+
+ self.create_map()
+ self.init_animation()
+ self.map_size = self.map.size
+
+ def load_tmx_file(self):
+ if self.map_source is None:
+ raise Exception('No map source provided.')
+ self.map = TileMap(self.map_source, image_loader=self.image_loader, load_unused_tiles=True)
+
+ def load_textures(self):
+ if not self.atlas_source:
+ raise Exception('No map atlas provided!')
+ texture_manager.load_atlas(self.atlas_source)
+
+ def image_loader(self, tileset=None, image_layer=None):
+ def extract_image(tile=None):
+ if tileset and tile is not None:
+ if tileset.is_images_collection:
+ return name_from_source(tile.source)
+ return name_from_tile(tile)
+ return name_from_source(image_layer.source)
+ return extract_image
+
+ def create_map(self):
+ map_obj = self.map
+ self.create_tiles(map_obj.tile_layers.filter(visible=True))
+ self.create_images(map_obj.image_layers.filter(visible=True))
+
+ def init_animation(self):
+ animator_obj = self.animator_obj
+ if animator_obj and animator_obj.has_animations:
+ animator_obj.set_update_time()
+
+ def handle_tile_animation(self, animation_frames):
+ self.animator_obj.durations.extend(f.duration for f in animation_frames)
+ frames = cycle(animation_frames)
+ return {
+ 'timer': 0,
+ # since 'frames' is a generator obj, we need to store current frame
+ 'current_frame': next(frames),
+ 'frames': frames
+ }
+
+ def create_tiles(self, tile_layers):
+ renderer = self.renderer
+ append_tile = self.tiles.append
+ animator = self.animator_obj.system_id
+ init_entity = self.gameworld.init_entity
+ for layer in tile_layers:
+ for cell in layer:
+ w, h = cell.size
+ x, y = cell.pos
+ x += w / 2.0
+ y -= h / 2.0
+
+ component_order = ['position', renderer]
+ create_component_dict = {
+ 'position': (x, y),
+ renderer: {
+ 'texture': cell.image,
+ 'size': cell.size,
+ 'render': True
+ }
+ }
+
+ animation_frames = cell.tile.properties.get('animation_frames')
+ if animation_frames and animator:
+ animation_component_dict = self.handle_tile_animation(animation_frames)
+ create_component_dict[animator] = animation_component_dict
+ component_order.append(animator)
+ # since tiled allows to animate tile without use of original image.
+ # we should swap the initial texture with image of the first animation frame
+ first_frame_image = animation_component_dict['current_frame'].image
+ create_component_dict[renderer]['texture'] = first_frame_image
+
+ append_tile(init_entity(create_component_dict, component_order))
+
+ def create_images(self, image_layers):
+ append_tile = self.tiles.append
+ for layer in image_layers:
+ texture_name = layer.image
+ x, y = layer.pos
+ w, h = texture_manager.get_size_by_name(texture_name)
+ x += w / 2.0
+ y -= h / 2.0
+ renderer_id = self.renderer
+ create_component_dict = {
+ renderer_id: {
+ 'texture': texture_name,
+ 'size': (w, h),
+ 'render': True
+ },
+ 'position': (x, y),
+ }
+ component_order = ['position', renderer_id]
+ append_tile(self.gameworld.init_entity(create_component_dict, component_order))
+
+ def clear_map(self):
+ self.gameworld.entities_to_remove.extend(self.tiles)
+
+
+Factory.register('TiledGameMap', cls=TiledGameMap)
diff --git a/modules/tiled/kivent_tiled/utils/__init__.py b/modules/tiled/kivent_tiled/utils/__init__.py
new file mode 100644
index 00000000..557afbee
--- /dev/null
+++ b/modules/tiled/kivent_tiled/utils/__init__.py
@@ -0,0 +1,20 @@
+import re
+import os
+import unicodedata
+
+
+def name_from_tile(tile):
+ return slugify('{}-{}'.format(tile.parent.name, tile.gid))
+
+
+def name_from_source(source):
+ filename = os.path.split(source)[1]
+ return os.path.splitext(filename)[0]
+
+
+def slugify(value):
+ value = unicode(value)
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
+ value = re.sub('[^\w\s-]', '', value).strip().lower()
+ value = re.sub('[-\s]+', '-', value)
+ return value.encode()
diff --git a/modules/tiled/kivent_tiled/utils/atlas_from_tmx.py b/modules/tiled/kivent_tiled/utils/atlas_from_tmx.py
new file mode 100644
index 00000000..11e57b8c
--- /dev/null
+++ b/modules/tiled/kivent_tiled/utils/atlas_from_tmx.py
@@ -0,0 +1,121 @@
+import os
+import shutil
+import tempfile
+import argparse
+from itertools import chain
+
+from PIL import Image
+from tmxloader.loader import TileMap
+from kivent_tiled.utils import name_from_tile, name_from_source
+
+os.environ['KIVY_DOC_INCLUDE'] = '1'
+from kivy.atlas import Atlas
+
+
+class AtlasWrapper(object):
+ def __init__(self, map_source):
+ self.tilesets = []
+ self.image_layers = []
+ self.map = TileMap(map_source, image_loader=self.image_loader)
+
+ def add_tileset(self, tileset):
+ tileset_wrapper = TilesetWrapper(tileset)
+ self.tilesets.append(tileset_wrapper)
+ return tileset_wrapper
+
+ def add_image_layer(self, image_layer):
+ self.image_layers.append(ImageLayerWrapper(image_layer))
+
+ def image_loader(self, tileset=None, image_layer=None):
+ if tileset:
+ tileset_wrapper = self.add_tileset(tileset)
+ else:
+ self.add_image_layer(image_layer)
+
+ def extract_image(tile=None):
+ if tileset and tile is not None:
+ return tileset_wrapper.add_image(tile)
+ return name_from_source(image_layer.source)
+ return extract_image
+
+ def __iter__(self):
+ return (filename for wrapper in chain(self.tilesets, self.image_layers)
+ for filename in wrapper)
+
+ def create_atlas(self, **kwargs):
+ Atlas.create(filenames=set(self), **kwargs)
+
+
+class ImageCollectionWrapper(object):
+ def __init__(self, image_collection):
+ self.filenames = []
+ self.source = image_collection.source
+
+ def __iter__(self):
+ return iter(self.filenames)
+
+ def add_image(self, *args, **kwargs):
+ raise NotImplementedError
+
+
+class ImageLayerWrapper(ImageCollectionWrapper):
+ def __init__(self, image_collection):
+ super(ImageLayerWrapper, self).__init__(image_collection)
+ self.add_image()
+
+ def add_image(self):
+ source = self.source
+ self.filenames.append(source)
+ return name_from_source(source)
+
+
+class TilesetWrapper(object):
+ def __new__(cls, *args, **kwargs):
+ tileset, = args
+ if tileset.is_images_collection:
+ return ImageCollectionTilesetWrapper(tileset)
+ return SingleImageTilesetWrapper(tileset)
+
+
+class ImageCollectionTilesetWrapper(ImageCollectionWrapper):
+ def add_image(self, tile):
+ source = tile.source
+ self.filenames.append(source)
+ return name_from_source(source)
+
+
+class SingleImageTilesetWrapper(ImageCollectionTilesetWrapper):
+ image_extension = '.png'
+
+ def __init__(self, image_collection):
+ super(SingleImageTilesetWrapper, self).__init__(image_collection)
+ self.base_image = Image.open(self.source)
+ self.tmpdir = tempfile.mkdtemp()
+
+ def add_image(self, tile):
+ x, y, w, h = tile.rect
+ box = (x, y, x + w, y + h)
+ tile_image = self.base_image.crop(box)
+ tile_name = name_from_tile(tile)
+ image_path = os.path.join(self.tmpdir, tile_name + self.image_extension)
+ tile_image.save(image_path)
+ self.filenames.append(image_path)
+ return tile_name
+
+ def __del__(self):
+ shutil.rmtree(self.tmpdir)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="Wrapper around kivy's atlas class.")
+ parser.add_argument('map_source')
+ parser.add_argument('outname')
+ parser.add_argument('size', type=int, nargs=2)
+ parser.add_argument('padding', default=2, type=int, nargs='?')
+ parser.add_argument('use_path', default=False, nargs='?')
+
+ args = parser.parse_args()
+ kwargs = vars(args)
+ source = kwargs.pop('map_source')
+ atlas = AtlasWrapper(source)
+ atlas.create_atlas(**kwargs)