Class: RGame::Engine::TileMap

Inherits:
Object
  • Object
show all
Defined in:
lib/rgame/engine/tile_map.rb

Overview

An orthogonal tile map parsed from a Tiled .tmx. Holds the per-layer gid arrays and geometry, and answers collision queries via its Tileset.

parse takes a string, so the parsing itself needs no filesystem at all; load adds the file plumbing on top — reading the .tmx, following it to the .tsx it names, and working out where the tileset image sits relative to that. Both belong here. What does not is the image: a texture is a GPU handle, and the renderer that owns one lives a layer below and may not name this class (see CLAUDE.md, "The rule points both ways"). So load hands back a path and stops there.

Constant Summary collapse

FLIP_MASK =

strip Tiled's flip/rotation flags from a gid

0x1FFFFFFF

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(width:, height:, tile_width:, tile_height:, tileset_source:, firstgid:, layers:, above: []) ⇒ TileMap

Returns a new instance of TileMap.



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/rgame/engine/tile_map.rb', line 85

def initialize(width:, height:, tile_width:, tile_height:, tileset_source:, firstgid:, layers:, above: [])
  @width = width
  @height = height
  @tile_width = tile_width
  @tile_height = tile_height
  @pixel_width = width * tile_width
  @pixel_height = height * tile_height
  @tileset_source = tileset_source
  @firstgid = firstgid
  @above = above
  @tileset = nil
  @tiles = build_tiles(layers)
end

Instance Attribute Details

#firstgidObject (readonly)

Returns the value of attribute firstgid.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def firstgid
  @firstgid
end

#heightObject (readonly)

Returns the value of attribute height.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def height
  @height
end

#pixel_heightObject (readonly)

Returns the value of attribute pixel_height.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def pixel_height
  @pixel_height
end

#pixel_widthObject (readonly)

Returns the value of attribute pixel_width.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def pixel_width
  @pixel_width
end

#tile_heightObject (readonly)

Returns the value of attribute tile_height.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def tile_height
  @tile_height
end

#tile_widthObject (readonly)

Returns the value of attribute tile_width.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def tile_width
  @tile_width
end

#tilesetObject

Returns the value of attribute tileset.



27
28
29
# File 'lib/rgame/engine/tile_map.rb', line 27

def tileset
  @tileset
end

#tileset_sourceObject (readonly)

Returns the value of attribute tileset_source.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def tileset_source
  @tileset_source
end

#widthObject (readonly)

Returns the value of attribute width.



25
26
27
# File 'lib/rgame/engine/tile_map.rb', line 25

def width
  @width
end

Class Method Details

.layer_flag?(layer_el, name) ⇒ Boolean

True if a layer carries a Tiled bool custom property name set to "true".

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
83
# File 'lib/rgame/engine/tile_map.rb', line 75

def self.layer_flag?(layer_el, name)
  props = layer_el.elements['properties']
  return false unless props

  props.each_element('property') do |property|
    return property.attributes['value'] == 'true' if property.attributes['name'] == name
  end
  false
end

.load(tmx_path) ⇒ Object

Reads a .tmx and everything it points at, returning [map, image_path].

Two values rather than one because they are two kinds of thing: the map is the grid, and the path is where its pixels happen to live. Keeping the second off the map means a stand-in map in a spec has one less method to answer, and the renderer's protocol stays "things about the grid".

Every path is resolved relative to the file that named it — the .tsx relative to the .tmx, the image relative to the .tsx — which is what Tiled itself writes and what lets a map be moved as a set.



39
40
41
42
43
44
45
46
# File 'lib/rgame/engine/tile_map.rb', line 39

def self.load(tmx_path)
  map = parse(File.read(tmx_path))

  tsx_path = File.join(File.dirname(tmx_path), map.tileset_source)
  map.tileset = Tileset.parse(File.read(tsx_path), firstgid: map.firstgid)

  [map, File.join(File.dirname(tsx_path), map.tileset.image_source)]
end

.parse(tmx_string) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/rgame/engine/tile_map.rb', line 48

def self.parse(tmx_string)
  root = REXML::Document.new(tmx_string).root
  tileset_el = root.elements['tileset']

  layers = []
  above = []
  root.each_element('layer') do |layer_el|
    raw = Base64.decode64(layer_el.elements['data'].text.strip)
    gids = Zlib::Inflate.inflate(raw).unpack('V*')
    gids.map! { |g| g & FLIP_MASK }
    layers << gids
    above << layer_flag?(layer_el, 'above')
  end

  new(
    width: root.attributes['width'].to_i,
    height: root.attributes['height'].to_i,
    tile_width: root.attributes['tilewidth'].to_i,
    tile_height: root.attributes['tileheight'].to_i,
    tileset_source: tileset_el.attributes['source'],
    firstgid: tileset_el.attributes['firstgid'].to_i,
    layers: layers,
    above: above
  )
end

Instance Method Details

#above_layer?(index) ⇒ Boolean

Whether a layer is drawn above the actors (a tree canopy, roof, etc.) rather than beneath them. Marked in Tiled with a bool layer property above; layers without it default to below. Purely a rendering distinction (collision still considers every layer).

Returns:

  • (Boolean)


107
108
109
# File 'lib/rgame/engine/tile_map.rb', line 107

def above_layer?(index)
  @above[index] || false
end

#gid(layer_index, col, row) ⇒ Object



115
116
117
118
119
# File 'lib/rgame/engine/tile_map.rb', line 115

def gid(layer_index, col, row)
  return 0 unless in_bounds?(col, row)

  @tiles[col, row, layer_index]
end

#in_bounds?(col, row) ⇒ Boolean

Returns:

  • (Boolean)


111
112
113
# File 'lib/rgame/engine/tile_map.rb', line 111

def in_bounds?(col, row)
  col >= 0 && row >= 0 && col < @width && row < @height
end

#layer_countObject



99
100
101
# File 'lib/rgame/engine/tile_map.rb', line 99

def layer_count
  @tiles.depth
end

#solid_at?(world_x, world_y) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/rgame/engine/tile_map.rb', line 130

def solid_at?(world_x, world_y)
  solid_tile?((world_x / @tile_width).floor, (world_y / @tile_height).floor)
end

#solid_tile?(col, row) ⇒ Boolean

Solid if any layer has a solid tile at (col, row). Out of bounds is not solid — the camera/bounds clamp keeps the player inside the map.

Returns:

  • (Boolean)


123
124
125
126
127
128
# File 'lib/rgame/engine/tile_map.rb', line 123

def solid_tile?(col, row)
  return false unless in_bounds?(col, row)

  @tiles.depth.times { |layer| return true if @tileset.solid?(@tiles[col, row, layer]) }
  false
end