Source code for rubato.structure.gameobject.tilemap.tiled

"""A Tiled tilemap."""
from __future__ import annotations
from .. import Component, Spritesheet, Rectangle, Polygon, GameObject
from .... import Vector, get_path, Surface, Color, Draw, Display
import pytiled_parser as parse
import pytiled_parser.tiled_object as parse_obj
from pathlib import Path


[docs]class Tilemap(Component): """ A tilemap that is loaded from a Tiled map file. Once a tilemap component is created, it won't stay updated with the map file. To automatically add hitboxes to the gameobject, use Tiled Objects. We support Rectangles and Polygons in layers or on the individual tiles. We do not support all of Tiled's features, but we do support the most common ones. Here is a list of major features that we DO NOT support: * Infinite maps * Non-orthogonal maps * Non-rectangular tiles * Animated tiles * Multiple tilesets per map * Image layers * Tile and layer opacity * Parallax scrolling * Worlds * Terrain Args: map_path: The path to the map file. scale: The scale of the tilemap. Defaults to (1, 1). collider_tag: The tag of the colliders. Defaults to "". z_index: The z index of the tilemap. Defaults to 0. hidden: Whether the tilemap is hidden. Defaults to False. """ def __init__( self, map_path: str, scale: Vector | tuple[float, float] = (1, 1), collider_tag: str = "", z_index: int = 0, hidden: bool = False ): super().__init__((0, 0), 0, z_index, hidden) if map_path == "": return # This is a hack to allow us to clone the component quickly self._scale = (*scale,) self._collider_tag = collider_tag self._polygons = GameObject() m = parse.parse_map(Path(get_path(map_path))) self._tileset = m.tilesets[1] self._sprites = Spritesheet( str(self._tileset.image), (self._tileset.tile_width, self._tileset.tile_height), ) self._out = Surface( int(m.map_size.width * self._tileset.tile_width), int(m.map_size.height * self._tileset.tile_height), scale, ) if (bg := m.background_color) is not None: self._out.fill(Color(bg.red, bg.green, bg.blue, bg.alpha)) for layer in m.layers: if isinstance(layer, parse.LayerGroup): self._process_layergroup(layer) elif isinstance(layer, parse.TileLayer): self._process_tilelayer(layer) elif isinstance(layer, parse.ObjectLayer): self._process_objectlayer(layer) def _process_layergroup(self, layer: parse.LayerGroup, extra_offset: tuple = (0, 0)): if not layer.visible or layer.opacity == 0 or layer.layers is None: return for l in layer.layers: coords = ( layer.coordinates.x + layer.offset.x + extra_offset[0], layer.coordinates.y + layer.offset.y + extra_offset[1], ) if isinstance(l, parse.TileLayer): self._process_tilelayer(l, coords) elif isinstance(l, parse.ObjectLayer): self._process_objectlayer(l, coords) elif isinstance(l, parse.LayerGroup): self._process_layergroup(l, coords) def _process_tilelayer(self, layer: parse.TileLayer, extra_offset: tuple = (0, 0)): if layer.data is None or not layer.visible or layer.opacity == 0: return horiz_flip = 1 << 31 vert_flip = 1 << 30 diag_flip = 1 << 29 l = Surface(self._out.width, self._out.height, (3, 3)) for y, row in enumerate(layer.data): for x, tile in enumerate(row): if tile == 0: continue flip_x, flip_y, flip_diag = False, False, False if tile & horiz_flip == horiz_flip: flip_x = True tile &= ~horiz_flip if tile & vert_flip == vert_flip: flip_y = True tile &= ~vert_flip if tile & diag_flip == diag_flip: flip_diag = True tile &= ~diag_flip t = self._sprites.get( int((tile - 1) % self._sprites.grid_size.x), int((tile - 1) // self._sprites.grid_size.x), ).clone() if flip_diag: t.flip_anti_diagonal() if flip_x: t.flip_x() if flip_y: t.flip_y() if self._tileset.tiles and (t_info := self._tileset.tiles.get(tile - 1)) is not None: if t_info.objects is not None and isinstance(t_info.objects, parse.ObjectLayer): self._process_objectlayer( t_info.objects, (x * self._tileset.tile_width, y * self._tileset.tile_height) ) l._blit( t, dst_rect=( x * self._tileset.tile_width, y * self._tileset.tile_height, self._tileset.tile_width, self._tileset.tile_height, ) ) self._out._blit( l, dst_rect=( int(layer.coordinates.x + layer.offset.x + extra_offset[0]), int(layer.coordinates.y + layer.offset.y + extra_offset[1]), self._out.width, self._out.height, ) ) def _process_objectlayer(self, layer: parse.ObjectLayer, extra_offset: tuple = (0, 0)): if not layer.visible or layer.opacity == 0: return for obj in layer.tiled_objects: if not obj.visible: continue if isinstance(obj, parse_obj.Rectangle): p = ( extra_offset[0] + obj.coordinates.x + layer.offset.x + layer.coordinates.x, extra_offset[1] + obj.coordinates.y + layer.offset.y + layer.coordinates.y, ) p = Display._top_left_to_center(p, (obj.size.width, obj.size.height)) p = self._out._convert_to_cartesian_space(p) p = (p[0] * self._scale[0], p[1] * self._scale[1]) self._polygons.add( Rectangle( round(obj.size.width * self._scale[0]), round(obj.size.height * self._scale[1]), offset=p, tag=self._collider_tag, ) ) elif isinstance(obj, parse_obj.Polygon): p = ( extra_offset[0] + obj.coordinates.x + layer.offset.x + layer.coordinates.x, extra_offset[1] + obj.coordinates.y + layer.offset.y + layer.coordinates.y, ) p = Display._top_left_to_center(p, (obj.size.width, obj.size.height)) p = self._out._convert_to_cartesian_space(p) p = (p[0] * self._scale[0], p[1] * self._scale[1]) self._polygons.add( Polygon( [( x * self._scale[0], -y * self._scale[1], ) for x, y in obj.points], offset=p, tag=self._collider_tag, ), )
[docs] def setup(self): self._polygons.parent = self.gameobj
[docs] def draw(self, camera): Draw.queue_surface(self._out, self.true_pos(), self.true_z(), camera)
[docs] def clone(self) -> Tilemap: t = Tilemap("") t._out = self._out.clone() t.offset = self.offset.clone() t._scale = (*self._scale,) t.z_index = self.z_index t._collider_tag = self._collider_tag t._polygons = self._polygons.clone() t.rot_offset = self.rot_offset t.hidden = self.hidden return t