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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJztmNkOgjAQRas8uCWuiWtEFPeV//86x9gGMmlKqXQcl4cTICnp4XagE0IhROiRNtABup7nceGq8Vsw8PqU/B5ZDYERMGbgo6MCVIEAefuYawJMCzADGkATaBFksQRWBYg9+u0NfmvgCGze6GfKL7bMk8Jvy9AvQV7Y7yye9X8h9sM1h6/z8sJ1WYaf7j3Yo/zUeZ4fzrWs/HboWvkUzc/3+mJPzEGuny2nEvxqkrpI9yITgeU4xfxFv45I+wv1rCZaluN097n4jUTaX/SAfg4Dy3G6+3zWHjWR0Pc5nDwjQh+X3ozC6yqPNt/O7P4TlzS/LTZ+D6dEjv11P1zX3Pxc8sv2F9jvxsAPZ8ktP91ac/ajzM+lN6P0+xYiBg6fAs6K27/B/xrT4Xt/+/Me7lpst88= + + + 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)