@Xilef: Here's an implementation of the RMXP Tilemap class I wrote for a GOSU integration project I never finished. I can't tell you exactly how performant it is, or if the "flash data" part actually works right (nobody as far as I know actually used that part of the interface anyway). But it has full autotile and Z-ordering support.
[rgss]# RMXP Tilemap (gosu)
#
# Readable/Writable Attributes :
# - viewport : Viewport used for sprites
# - map_data : 3D Table of Tile ID Data
# - flash_data : 3D Table of Tile Color Data
# - priorities : 3D Table of Tile Priorities
# - ox, oy : Tilemap layer offsets
# - zoom : Zoom value as fraction
# - tilesize : Zoom value as size of individual tile (normal is 32.0)
# - visible : Tilemap Visible Flag
# - is_a_plane : Behaves like a Plane (loops around edges)
# - tileset : Name of Bitmap
# - autotiles : Array of Autotile Filenames
class Tilemap
VP = 2 # Number of tiles beyond viewport to refresh
FA = 64 # Alpha value of the color of flash data
Autotile_Speed = 20 # Number of frames between autotile animations
Autotiles = [
[416,432,512,528], [ 64,432,512,528], [416, 80,512,528], [ 64, 80,512,528],
[416,432,512,176], [ 64,432,512,176], [416, 80,512,176], [ 64, 80,512,176],
[416,432,160,528], [ 64,432,160,528], [416, 80,160,528], [ 64, 80,160,528],
[416,432,160,176], [ 64,432,160,176], [416, 80,160,176], [ 64, 80,160,176],
[384,400,480,496], [384, 80,480,496], [384,400,480,176], [384, 80,480,176],
[224,240,320,336], [224,240,320,176], [224,240,160,336], [224,240,160,176],
[448,464,544,560], [448,464,160,560], [ 64,464,544,560], [ 64,464,160,560],
[608,624,704,720], [ 64,624,704,720], [608, 80,704,720], [ 64, 80,704,720],
[384,464,480,560], [224,240,704,720], [192,208,288,304], [192,208,288,176],
[256,272,352,368], [256,272,160,368], [640,656,736,752], [ 64,656,736,752],
[576,592,672,688], [576, 80,672,688], [192,272,288,368], [192,208,672,688],
[576,656,672,752], [256,272,736,752], [192,272,672,752], [ 0, 16, 96,112]
]
DEFAULTS = {:tileset => nil, :autotiles => [], :map_data => nil,
:flash_data => nil, :priorities => nil, :visible => true,
x => 0,
y => 0, :zoom => 1.0, :is_a_plane => true,
:manager => nil}
# Public Instance Variables
attr_reader :gosuwindow # Instance of Gosu::Window it's drawing to
attr_reader :tileset # Tileset Image
attr_reader :autotiles # Array of Autotile Images
attr_accessor :map_data # 3D Table of Tile Settings
attr_reader :flash_data # 3D Table of Sprite Flash Colors
attr_accessor :priorities # 3D Table of Tileset Priorities
attr_accessor :visible # Tilemap Visibility
attr_accessor
x # Bitmap Offsets
attr_accessor
y # Bitmap Offsets
attr_accessor :zoom # Zoom Fraction
attr_accessor :is_a_plane # Loops around edges
# Object Initialization
def initialize(manager = nil, opts = {})
@gosuwindow = $GameWindow
DEFAULTS.merge!(opts).each do |sym, val|
instance_variable_set("@#{sym}", val)
end
@manager = manager unless manager.nil?
@autotiles_cache = @autotiles
self.flash_data = @flash_data
@tiles = []
@frame = 0
(@manager.nil? ? @gosuwindow.draw_manager : @manager).manage(self)
end
def dispose
@disposed = true
end
def disposed?
@disposed == true
end
# Get "viewport"
def viewport
@manager
end
# Set "viewport"
def viewport=(manager)
(@manager.nil? ? @gosuwindow.draw_manager : @manager).unmanage(self)
(manager.nil? ? @gosuwindow.draw_manager : manager).manage(self)
@manager = manager
end
# Get Tile Size
def tilesize
(zoom * 32).to_i
end
# Set Tile Size
def tilesize=(tilesize)
self.zoom = tilesize / 32.0
end
# Set tileset graphic. Recaches the internal tileset.
def tileset=(tileset)
@tiles[384..-1] = Gosu::Image.load_tiles(@gosuwindow, tileset, 32, 32, true)
@tileset = tileset
end
# Set list of autotiles. Recaches the internal tileset.
def autotiles=(autotiles)
autotiles.each_with_index do |autotile, i|
Autotiles.each_with_index do |tiles, j|
key = (i + 1) * 48 + j
@tiles[key] = []
# Draws Auto-Tile Rects
for f in 0..(autotile.width / 96)
bmp = Gosu::Image.new(@gosuwindow,
TexPlay::EmptyImageStub.new(32, 32), true)
tiles.each_with_index do |pos, k|
opts = {:crop => [x1 = pos%96+f*96, x2=pos / 96, x1 + 16, x2 + 16]}
bmp.splice(k % 2 * 16, k / 2 * 16, autotile, opts)
end
@tiles[key][f] = bmp
end
end
end
@autotiles = @autotiles_cache = autotiles
end
# Set flash data. For speed we maintain a separate list of GOSU::Color
# object for actual display.
def flash_data=(flash_data)
@flash_data = flash_data
if flash_data.nil?
@flash_data_colors = nil
else
@flash_data_colors = Hash.new
for y in 0...flash_data.ysize
for x in 0...flash_data.xsize
hex = @flash_data[x, y]
@flash_data_colors[[x, y]] =
Gosu::Color.rgba((hex<<8)*16, ((hex<<4)%16)*16, (hex%16)*16, FA))
end
end
end
end
# Dispose
def dispose
@visible = false
super
end
# Update autotile animation
def update
@frame += 1
end
# Per-frame draw function
def draw
return if @map_data.nil? or @priorities.nil? or !@visible
self.autotiles = @autotiles if @autotiles != @autotiles_cache
ts = tilesize
frame = @frame / Autotile_Speed
xmax = (((@viewport.nil? ? @gosuwindow : @viewport.rect).width + @ox) / ts)
ymax = (((@viewport.nil? ? @gosuwindow : @viewport.rect).height + @oy) / ts)
ydts = @oy / ts
yrange = ydts - VP...ymax + VP
xrange = (@ox / ts) - VP...xmax + VP
for y in yrange
ytop = y - ydts
if y < 0 or y >= @map_data.ysize
next unless @is_a_plane
y %= @map_data.ysize
y += @map_data.ysize if y < 0
end
draw_y = y * ts - @oy
for x in xrange
if x < 0 or x >= @map_data.xsize
next unless @is_a_plane
x %= @map_data.xsize
x += @map_data.xsize if x < 0
end
draw_x = x * ts - @ox
for layer in 0...@map_data.zsize
id = @map_data[x, y, layer]
next if id == 0
z = (ytop + z) * 32 + 64 unless (z = @priorities[id]) == 0
tile = (tile.is_a?(Array) ? @tiles[id][frame % tile.size] : @tiles[id])
tile.draw(draw_x, draw_y, z, @zoom, @zoom)
end
unless @flash_data.nil?
c = @flash_data_colors[[x, y]]
@gosuwindow.draw_quad(draw_x, draw_y, c, draw_x+ts, draw_y, c,
draw_x, draw_y+ts, c, draw_x+ts, draw_y+ts, c)
end
end # for x
end # for y
end
end
[/rgss]
The whole $GameWindow and @manager bit relates to the drawing hierarchy I set up; the manager object basically functions as a viewport. The code for the Manager class is below, but you probably will just take that part out:
[rgss]# Manager class
# Instances of this class keep track of a series of objects (which may
# optionally be sorted by block; see Enumerable#sort!) and may delegate certain
# actions to those objects.
class Manager
def initialize(&sort)
@managed = []
@sort = sort
end
def manage(*objects)
@managed |= objects
end
def unmanage(*objects)
@managed.clear if objects.first == :all
@managed -= objects
end
def delegate(manage_method = nil, &block)
@managed.sort!(&@sort)
if block_given?
@managed.each(&block)
return
end
return if manage_method.nil?
@managed.each do |object|
object.__send__(manage_method) if object.respond_to?(manage_method)
end
end
end
[/rgss]
This also does the rendering with the GOSU library instead of directly with OpenGL. `Gosu::Image.load_tiles(...)` slices the tileset into a number of different textures in the graphics card, one for each tile, which is probably...very wrong. The autotiles are the same way. The @tiles array should probably contain UV coordinates instead of pointers to individual textures; the autotiles are compiled from four 16x16 tiles instead of being 32x32 tiles themselves. Similarly, the flash data is drawn as a quad but should probably be colors applied to the vertices instead. Inside the draw method, you can probably guess what tile.draw does (tile being a Gosu::Image, which is a loaded texture).
In hindsight, this probably isn't all that helpful. Actually, everything you need to reverse engineer the Tilemap is in the RMXP help file! Well, almost everything. The autotiles are not properly documented, but at least my code will help you with that. I really don't think there's any better way to handle it than an int[48][4]; there are 48 possible configurations, each of which referencing 4 corners. I suppose an int[48][4][2] might alleviate the need to convert the integer into a coordinate pair. There's also the animation to worry about, which I have handled.
What was actually a lot more difficult to figure out was the way that RGSS serializes its internal classes. I've figured it out and written pure Ruby that will load and save to the same data formats:
[rgss]class Color < Gosu::Color
def initialize(red, green, blue, alpha = 255)
super(alpha.to_i, red.to_i, green.to_i, blue.to_i)
end
def set(red, green, blue, alpha = 255)
self.red, self.green, self.blue, self.alpha = red, green, blue, alpha
end
def blend(color)
self.clone.blend!(color)
end
def blend!(color)
self.red =(self.red *(255-color.alpha)/255)+(color.red *color.alpha/255)
self.green=(self.green*(255-color.alpha)/255)+(color.green*color.alpha/255)
self.blue =(self.blue *(255-color.alpha)/255)+(color.blue *color.alpha/255)
self.alpha += (color.alpha * (255 - self.alpha) / 255)
self
end
def to_a
[red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0]
end
def dup
self.class.new(self.red, self.green, self.blue, self.alpha)
end
def clone
self.class.new(self.red, self.green, self.blue, self.alpha)
end
# Dump to String (compatible with RGSS Color dump format)
def _dump(marshal_depth = -1)
[self.red, self.green, self.blue, self.alpha].pack('E4')
end
# Load from String (compatible with RGSS Color dump format)
def self._load(data)
new(*data.unpack('E4'))
end
White = new(255, 255, 255)
Shadow = new(0, 0, 0, 160)
Clear = new(0, 0, 0, 0)
Normal = White
Disabled = new(255, 255, 255, 128)
System = new(192, 224, 255)
Crisis = new(255, 255, 64)
Knockout = new(255, 64, 0)
end
class Tone
attr_reader :red, :green, :blue, :gray
def initialize(red, green, blue, gray = 0)
self.red, self.green, self.blue, self.gray = red, green, blue, gray
end
def red=(value)
@red = [[-255, value].max, 255].min
end
def green=(value)
@green = [[-255, value].max, 255].min
end
def blue=(value)
@blue = [[-255, value].max, 255].min
end
def gray=(value)
@gray = [[0, value].max, 255].min
end
def blend(tone)
self.clone.blend!(tone)
end
def blend!(tone)
self.red += tone.red
self.green += tone.green
self.blue += tone.blue
self.gray += tone.gray
self
end
# Dump to String (compatible with RGSS Tone dump format)
def _dump(marshal_depth = -1)
[@red, @green, @blue, @gray].pack('E4')
end
# Load from String (compatible with RGSS Tone dump format)
def self._load(data)
new(*data.unpack('E4'))
end
end
# Rewritten the Table class. This is slower than RGSS but more portable.
class Table
# Initialize the table with variable length arguments
def initialize(xsize, ysize = 1, zsize = 1)
resize(xsize, ysize, zsize)
end
# Set the size of (and number of) the dimensions; clears the table
def resize(xsize, ysize = 1, zsize = 1)
@data = Array.new(xsize * ysize * zsize, 0)
@xsize, @ysize, @zsize = xsize, ysize, zsize
end
# Returns the size at a specific depth, or 1 if out of range
def xsize ; @xsize ; end
def ysize ; @ysize ; end
def zsize ; @zsize ; end
# Retrieve data
def [](x, y = 0, z = 0)
# Return nil if out of range
return nil if (i = get_index(x, y, z)).nil?
@data
end
# Set data
def []=(*args)
# Remove the last argument, which is what the data will be set to
operand = args.pop.to_i
# Return nil if out of range
return nil if (i = get_index(*args).nil?)
@data = operand
end
# Check arguments for size and within range
def get_index(x, y, z)
return nil if x < 0 or x >= xsize
return nil if y < 0 or y >= ysize
return nil if z < 0 or z >= zsize
(z * @ysize * @xsize) + (y * @xsize) + x
end
# Dump to String (compatible with RGSS Table dump format for Integers)
def _dump(marshal_depth = -1)
# First, store information about the size
size_info = [@sizes.length, *@sizes]
size_info << 0x01 while size_info.length < 4
output = size_info.pack('I*')
# Then, store some empty data
output += [0xD2].pack('I')
# All values are integers (shorts), so store them as such
output += @data.pack('s*')
output
end
# Load from String (compatible with RGSS Table dump format for Integers)
def self._load(data)
# Get depth of the Table
depth = data.slice!(0...4).unpack('I')[0]
# Get size of each dimension
sizes = data.slice!(0...[12, depth * 4].max).unpack('I*')[0...depth]
output = Table.new(*sizes)
data.slice!(0...4) # Skip this weird data
# Integer format: load each piece of data in the correct order
output.instance_variable_set@data, data.unpack('s*'))
return output
rescue RangeError
# Catches failed attempts to slice existing data due to out of range
raise RuntimeError, 'Load data for Table is not as long as specified. ' +
'The file may be corrupted.'
end
# Process through each element in order
def each(&block)
@data.each do |val|
args = [val]
args << i / (@ysize*@zsize) if block.arity > 1
args << (i % @zsize) / @ysize if block.arity > 2
args << i % (@ysize*@zsize) if block.arity > 3
yield *args
end
end
end
[/rgss]
I haven't taken a look at these in a long time and I'm starting to be embarrassed by the possibility that they are wrong. But I hope this helps you at least.