diff --git a/apps/demo/demo-stitched.py b/apps/demo/demo-stitched.py
new file mode 100644
index 0000000..dd7c7c5
--- /dev/null
+++ b/apps/demo/demo-stitched.py
@@ -0,0 +1,207 @@
+"""
+
+Rendering demo showing 9 TMX maps rendered at once
+
+
+Very basic! No animations.
+
+"""
+from __future__ import annotations
+
+from pathlib import Path
+from typing import List
+
+import pygame
+from pygame.locals import (
+ K_UP,
+ K_DOWN,
+ K_LEFT,
+ K_RIGHT,
+ K_MINUS,
+ K_EQUALS,
+ K_ESCAPE,
+ K_r,
+)
+from pygame.locals import KEYDOWN, VIDEORESIZE, QUIT
+from pytmx.util_pygame import load_pygame
+
+import pyscroll
+from pyscroll.data import MapAggregator, TiledMapData
+from pyscroll.group import PyscrollGroup
+
+# define configuration variables here
+CURRENT_DIR = Path(__file__).parent
+RESOURCES_DIR = CURRENT_DIR
+HERO_MOVE_SPEED = 200 # pixels per second
+
+
+def init_screen(width: int, height: int) -> pygame.Surface:
+ screen = pygame.display.set_mode((width, height), pygame.RESIZABLE)
+ return screen
+
+
+def load_image(filename: str) -> pygame.Surface:
+ return pygame.image.load(str(RESOURCES_DIR / filename))
+
+
+class Hero(pygame.sprite.Sprite):
+ def __init__(self) -> None:
+ super().__init__()
+ self.image = load_image("hero.png").convert_alpha()
+ self.velocity = [0, 0]
+ self._position = [0.0, 0.0]
+ self._old_position = self.position
+ self.rect = self.image.get_rect()
+ self.feet = pygame.Rect(0, 0, self.rect.width * 0.5, 8)
+
+ @property
+ def position(self) -> List[float]:
+ return list(self._position)
+
+ @position.setter
+ def position(self, value: List[float]) -> None:
+ self._position = list(value)
+
+ def update(self, dt: float) -> None:
+ self._old_position = self._position[:]
+ self._position[0] += self.velocity[0] * dt
+ self._position[1] += self.velocity[1] * dt
+ self.rect.topleft = self._position
+ self.feet.midbottom = self.rect.midbottom
+
+ def move_back(self, dt: float) -> None:
+ self._position = self._old_position
+ self.rect.topleft = self._position
+ self.feet.midbottom = self.rect.midbottom
+
+
+class QuestGame:
+ map_path = RESOURCES_DIR / "grasslands.tmx"
+
+ def __init__(self, screen: pygame.Surface) -> None:
+ self.screen = screen
+ self.running = False
+
+ world_data = MapAggregator((16, 16))
+ for filename, offset in [
+ ("stitched0.tmx", (-20, -20)),
+ ("stitched1.tmx", (0, -20)),
+ ("stitched2.tmx", (20, -20)),
+ ("stitched3.tmx", (-20, 0)),
+ ("stitched4.tmx", (0, 0)),
+ ("stitched5.tmx", (20, 0)),
+ ("stitched6.tmx", (-20, 20)),
+ ("stitched7.tmx", (0, 20)),
+ ("stitched8.tmx", (20, 20)),
+ ]:
+ tmx_data = load_pygame(RESOURCES_DIR / filename)
+ pyscroll_data = TiledMapData(tmx_data)
+ world_data.add_map(pyscroll_data, offset)
+
+ self.map_layer = pyscroll.BufferedRenderer(
+ data=world_data,
+ size=screen.get_size(),
+ clamp_camera=True,
+ )
+ self.map_layer.zoom = 2
+ self.group = PyscrollGroup(map_layer=self.map_layer, default_layer=0)
+
+ # put the hero in the center of the map
+ self.hero = Hero()
+ self.hero.layer = 0
+ self.hero.position = (400, 400)
+
+ # add our hero to the group
+ self.group.add(self.hero)
+
+ def draw(self) -> None:
+ self.group.center(self.hero.rect.center)
+ self.group.draw(self.screen)
+
+ def handle_input(self) -> None:
+ """
+ Handle pygame input events
+
+ """
+ for event in pygame.event.get():
+ if event.type == QUIT:
+ self.running = False
+ break
+
+ elif event.type == KEYDOWN:
+ if event.key == K_ESCAPE:
+ self.running = False
+ break
+
+ elif event.key == K_r:
+ self.map_layer.reload()
+
+ elif event.key == K_EQUALS:
+ self.map_layer.zoom += 0.25
+
+ elif event.key == K_MINUS:
+ value = self.map_layer.zoom - 0.25
+ if value > 0:
+ self.map_layer.zoom = value
+
+ # this will be handled if the window is resized
+ elif event.type == VIDEORESIZE:
+ self.screen = init_screen(event.w, event.h)
+ self.map_layer.set_size((event.w, event.h))
+
+ # use `get_pressed` for an easy way to detect held keys
+ pressed = pygame.key.get_pressed()
+ if pressed[K_UP]:
+ self.hero.velocity[1] = -HERO_MOVE_SPEED
+ elif pressed[K_DOWN]:
+ self.hero.velocity[1] = HERO_MOVE_SPEED
+ else:
+ self.hero.velocity[1] = 0
+
+ if pressed[K_LEFT]:
+ self.hero.velocity[0] = -HERO_MOVE_SPEED
+ elif pressed[K_RIGHT]:
+ self.hero.velocity[0] = HERO_MOVE_SPEED
+ else:
+ self.hero.velocity[0] = 0
+
+ def update(self, dt: float):
+ """
+ Tasks that occur over time should be handled here
+
+ """
+ self.group.update(dt)
+
+ def run(self):
+ clock = pygame.time.Clock()
+ self.running = True
+
+ try:
+ while self.running:
+ dt = clock.tick() / 1000.0
+ self.handle_input()
+ self.update(dt)
+ self.draw()
+ pygame.display.flip()
+
+ except KeyboardInterrupt:
+ self.running = False
+
+
+def main() -> None:
+ pygame.init()
+ pygame.font.init()
+ screen = init_screen(800, 600)
+ pygame.display.set_caption("Quest - An epic journey.")
+
+ try:
+ game = QuestGame(screen)
+ game.run()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ pygame.quit()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/apps/demo/hero.png b/apps/demo/hero.png
new file mode 100644
index 0000000..bf9592e
Binary files /dev/null and b/apps/demo/hero.png differ
diff --git a/apps/demo/stitched.world b/apps/demo/stitched.world
new file mode 100644
index 0000000..56b7806
--- /dev/null
+++ b/apps/demo/stitched.world
@@ -0,0 +1,69 @@
+{
+ "maps": [
+ {
+ "fileName": "stitched0.tmx",
+ "height": 320,
+ "width": 320,
+ "x": -320,
+ "y": -320
+ },
+ {
+ "fileName": "stitched1.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 0,
+ "y": -320
+ },
+ {
+ "fileName": "stitched2.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 320,
+ "y": -320
+ },
+ {
+ "fileName": "stitched3.tmx",
+ "height": 320,
+ "width": 320,
+ "x": -320,
+ "y": 0
+ },
+ {
+ "fileName": "stitched4.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 0,
+ "y": 0
+ },
+ {
+ "fileName": "stitched5.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 320,
+ "y": 0
+ },
+ {
+ "fileName": "stitched6.tmx",
+ "height": 320,
+ "width": 320,
+ "x": -320,
+ "y": 320
+ },
+ {
+ "fileName": "stitched7.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 0,
+ "y": 320
+ },
+ {
+ "fileName": "stitched8.tmx",
+ "height": 320,
+ "width": 320,
+ "x": 320,
+ "y": 320
+ }
+ ],
+ "onlyShowAdjacentMaps": false,
+ "type": "world"
+}
diff --git a/apps/demo/stitched0.tmx b/apps/demo/stitched0.tmx
new file mode 100644
index 0000000..21f44e5
--- /dev/null
+++ b/apps/demo/stitched0.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched1.tmx b/apps/demo/stitched1.tmx
new file mode 100644
index 0000000..59818d4
--- /dev/null
+++ b/apps/demo/stitched1.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched2.tmx b/apps/demo/stitched2.tmx
new file mode 100644
index 0000000..21f44e5
--- /dev/null
+++ b/apps/demo/stitched2.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched3.tmx b/apps/demo/stitched3.tmx
new file mode 100644
index 0000000..bd56da0
--- /dev/null
+++ b/apps/demo/stitched3.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched4.tmx b/apps/demo/stitched4.tmx
new file mode 100644
index 0000000..e9f4b0a
--- /dev/null
+++ b/apps/demo/stitched4.tmx
@@ -0,0 +1,14 @@
+
+
diff --git a/apps/demo/stitched5.tmx b/apps/demo/stitched5.tmx
new file mode 100644
index 0000000..bd56da0
--- /dev/null
+++ b/apps/demo/stitched5.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched6.tmx b/apps/demo/stitched6.tmx
new file mode 100644
index 0000000..21f44e5
--- /dev/null
+++ b/apps/demo/stitched6.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched7.tmx b/apps/demo/stitched7.tmx
new file mode 100644
index 0000000..59818d4
--- /dev/null
+++ b/apps/demo/stitched7.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/demo/stitched8.tmx b/apps/demo/stitched8.tmx
new file mode 100644
index 0000000..21f44e5
--- /dev/null
+++ b/apps/demo/stitched8.tmx
@@ -0,0 +1,9 @@
+
+
diff --git a/pyscroll/__init__.py b/pyscroll/__init__.py
index d25c0fb..b5710fe 100644
--- a/pyscroll/__init__.py
+++ b/pyscroll/__init__.py
@@ -3,7 +3,7 @@
from .isometric import IsometricBufferedRenderer
from .orthographic import BufferedRenderer
-__version__ = 2, 28
+__version__ = 2, 29
__author__ = "bitcraft"
__author_email__ = "leif.theden@gmail.com"
__description__ = "Pygame Scrolling"
diff --git a/pyscroll/common.py b/pyscroll/common.py
index d7d2e60..a2b8f07 100644
--- a/pyscroll/common.py
+++ b/pyscroll/common.py
@@ -3,9 +3,11 @@
from contextlib import contextmanager
from typing import Any, List, Tuple, Union
-from pygame import Rect, Surface
+from pygame import Rect, Surface, Vector2
RectLike = Union[Rect, Tuple[Any, Any, Any, Any]]
+Vector2D = Union[Tuple[float, float], Tuple[int, int], Vector2]
+Vector2DInt = Tuple[int, int]
@contextmanager
diff --git a/pyscroll/data.py b/pyscroll/data.py
index 15f457e..94da885 100644
--- a/pyscroll/data.py
+++ b/pyscroll/data.py
@@ -20,10 +20,14 @@
except ImportError:
pass
-from .common import rect_to_bb, RectLike
+from .common import rect_to_bb, RectLike, Vector2DInt
from .animation import AnimationFrame, AnimationToken
-__all__ = ('PyscrollDataAdapter', 'TiledMapData')
+__all__ = (
+ "PyscrollDataAdapter",
+ "TiledMapData",
+ "MapAggregator",
+)
class PyscrollDataAdapter:
@@ -229,17 +233,6 @@ def _get_tile_image_by_id(self, id):
"""
raise NotImplementedError
- def convert_surfaces(self, parent: pygame.Surface, alpha: bool = False):
- """
- Convert all images in the data to match the parent.
-
- Args:
- alpha: if True, then do not discard alpha channel
- parent: Surface used to convert the others
-
- """
- raise NotImplementedError
-
def get_animations(self):
"""
Get tile animation data.
@@ -379,3 +372,107 @@ def rev(seq, start, stop):
except KeyError:
# not animated, so return surface from data, if any
yield x, y, l, images[gid]
+
+
+class MapAggregator(PyscrollDataAdapter):
+ """
+ Combine multiple data sources with an offset
+
+ Currently this is just in a test phase.
+
+ Has the following limitations:
+ - Tile sizes must be the same for all maps
+ - No tile animations
+ - Sprites cannot be under layers
+ - Cannot remove maps once added
+
+ """
+ def __init__(self, tile_size):
+ super().__init__()
+ self.tile_size = tile_size
+ self.map_size = 0, 0
+ self.maps = list()
+ self._min_x = 0
+ self._min_y = 0
+
+ def _get_tile_image(self, x: int, y: int, l: int) -> Surface:
+ """
+ Required for sprite collation - not implemented
+
+ """
+ pass
+
+ def _get_tile_image_by_id(self, id):
+ """
+ Required for sprite collation - not implemented
+
+ """
+ pass
+
+ def add_map(self, data: PyscrollDataAdapter, offset: Vector2DInt):
+ """
+ Add map data and position it with an offset
+
+ Args:
+ data: Data Adapater, such as TiledMapData
+ offset: Where the upper-left corner is, in tiles
+
+ """
+ assert data.tile_size == self.tile_size
+ rect = pygame.Rect(offset, data.map_size)
+ ox = self._min_x - offset[0]
+ oy = self._min_y - offset[1]
+ self._min_x = min(self._min_x, offset[0])
+ self._min_y = min(self._min_y, offset[1])
+ mx = 0
+ my = 0
+ # the renderer cannot deal with negative tile coordinates,
+ # so we must move all the offsets if there is a negative so
+ # that all the tile coordinates are >= (0, 0)
+ self.maps.append((data, rect))
+ if ox > 0 or oy > 0:
+ for data, rect in self.maps:
+ rect.move_ip((ox, oy))
+ mx = max(mx, rect.right)
+ my = max(my, rect.bottom)
+ else:
+ rect.move_ip(-self._min_x, -self._min_y)
+ mx = max(mx, rect.right)
+ my = max(my, rect.bottom)
+ self.map_size = mx, my
+
+ def remove_map(self, data: PyscrollDataAdapter):
+ """
+ Remove map - not implemented
+
+ """
+ raise NotImplementedError
+
+ def get_animations(self):
+ """
+ Get animations - not implemented
+
+ """
+ pass
+
+ def reload_data(self):
+ """
+ Reload the tiles - not implemented
+
+ """
+ pass
+
+ @property
+ def visible_tile_layers(self):
+ layers = set()
+ for data, offset in self.maps:
+ layers.update(list(data.visible_tile_layers))
+ return sorted(layers)
+
+ def get_tile_images_by_rect(self, view: RectLike):
+ view = pygame.Rect(view)
+ for data, rect in self.maps:
+ ox, oy = rect.topleft
+ clipped = rect.clip(view).move(-ox, -oy)
+ for x, y, l, image in data.get_tile_images_by_rect(clipped):
+ yield x + ox, y + oy, l, image
diff --git a/pyscroll/orthographic.py b/pyscroll/orthographic.py
index 7ad6e16..fcc6fc6 100644
--- a/pyscroll/orthographic.py
+++ b/pyscroll/orthographic.py
@@ -4,20 +4,23 @@
import math
import time
from itertools import chain, product
-from typing import List, Tuple, TYPE_CHECKING, Callable, Union
+from typing import List, TYPE_CHECKING, Callable
import pygame
from pygame import Rect, Surface
-from .common import surface_clipping_context, RectLike
+from .common import (
+ surface_clipping_context,
+ RectLike,
+ Vector2D,
+ Vector2DInt,
+)
from .quadtree import FastQuadTree
if TYPE_CHECKING:
from .data import PyscrollDataAdapter
log = logging.getLogger(__file__)
-Vector2D = Union[Tuple[float, float], Tuple[int, int], pygame.Vector2]
-Vector2DInt = Tuple[int, int]
class BufferedRenderer:
diff --git a/setup.py b/setup.py
index b8fa1a1..a924f99 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,13 @@
#!/usr/bin/env python
# encoding: utf-8
-# pip install wheel
+# pip install wheel twine
# python3 setup.py sdist bdist_wheel
# python3 -m twine upload --repository pypi dist/*
from setuptools import setup
setup(
name="pyscroll",
- version="2.28",
+ version="2.29",
description="Fast scrolling maps library for pygame",
author="bitcraft",
author_email="leif.theden@gmail.com",