Hello!
I'm using Tiled to design scenes with multiple layers. I want to keep every layer separate in VertexArrays, so, when I drawn a sprite on the scene, it can be hidden by any layer that is over it (by its z index or something alike, isn't big deal).
I want to draw only those tiles that are visible for the player, so, I can achieve some performance on games with many, many, many tiles. I have achieved that feature on my engine, yes, for every layer (individual VertexArrays) of the scenario.
However: I'm having a very big issue on performance, take a look at my screenshots:
When I move the view I lost like 33 frames per second (because the scenario is transversing the N VertexArrays looking for those visible vertexs on them?), That's no cool ;(.
Actually, if I move the view where just one or two layers are seen, I win some frames per second until reach 63 fps (I'm running the example at 60), yes, 56 fps is by itself indicator that what I'm doing, I'm doing it wrong xd ;(.
So, I'm looking for people that has manage to have good performance for their games using scenarios with N layers; I really need advice on achieving what I want (basically, hiding fully or partially an sprite with those elements of the map, previously designed with Tiled, that are over it).
Source code? Well, I have the relevant code here, but is Python:
# coding: utf-8
# This file is part of Thomas Aquinas.
#
# Thomas Aquinas is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Thomas Aquinas is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Thomas Aquinas. If not, see <http://www.gnu.org/licenses/>.
#
# veni, Sancte Spiritus.
import logging
from thirdparty.pytmx import tmxloader
from itertools import product, chain, izip_longest, cycle
import common
import media
import sfml
import os
from copy import copy
class TATileImageException(Exception): pass
# Extiende tu mente a esto
# https://github.com/SFML/SFML/wiki/Source:-TileMap-Render
class AbstractScene(sfml.Drawable):
"""Escena abstracta del juego.
Las escenas representan partes visibles del juego, ya sea una
pantalla de introduccion, creditos, o un campo de batalla.
Para poder hacer escenas funcionales, debe derivar de esta clase
cualquier escena que necesite.
Esta clase usa Super para inicializar a sfml.Drawable. Use super
en sus subclases!
"""
def __init__(self, scenemanager):
sfml.Drawable.__init__(self)
self.scenemanager = scenemanager
# Para cambiar una escena puede hacer lo siguiente:
# self.scenemanager.changescene(nuevaescena)
# Y eso es todo :)
def on_update(self, event):
"""El manejador de escenas llama este metodo para actualizar la logica.
Aqui se actualizara todaas las entidades pertenecientes a la escena.
Cada una recibe el mismo evento sacado de windw.events para hacer
algo con su controlador que quizas quiera algo más que las propiedades
de la entidad.
"""
for entity in chain.from_iterable(self.sprites):
entity.on_update(event)
# [···]
def loadmap(self, mapfilepath=None):
"""Carga el mapa de la respectiva escena.
No es necesario reimplementar éste método.
Todos los archivos de mapa a leer deben ser en
formato tmx, del software Tiled Map Editor
http://www.mapeditor.org/"""
if mapfilepath:
self.__tmxmapfile = common.settings.joinpaths(
common.settings.getrootfolder(),
"maps", mapfilepath)
self.tmxmapdata = tmxloader.load_tmx(self.__tmxmapfile)
heightlist = []
widthlist = []
tilesets = []
logging.info("Cargando las baldosas del escenario...")
# carga todas las baldosas del set de baldosas
# basado en el código escrito por bitcraft, del proyecto
# pytmx. Revisar el método load_images_pygame del archivo
# pytmx/tmxloader.py. fragmento de código bajo LGPL 3.
self.tmxmapdata.images = [0] * self.tmxmapdata.maxgid
for firstgid, tile in sorted((tile.firstgid, tile) for tile in \
self.tmxmapdata.tilesets):
filename = os.path.basename(tile.source)
tilesets.append(
media.loadimg("maps/tilesets/{0}".format(filename)))
w, h = tilesets[-1].size
widthlist.append(w)
heightlist.append(h)
tile_size = (tile.tilewidth, tile.tileheight)
totalheight = sum(heightlist[1:], 0)
real_gid = tile.firstgid - 1
# FIXME: sfml no convierte los valores hexadecimales a valores
# RGB de 0 a 255.
# colorkey = None
# if t.trans:
# colorkey = pygame.Color("#{0}".format(t.trans))
tilewidth = tile.tilewidth + tile.spacing
tileheight = tile.tileheight + tile.spacing
# some tileset images may be slightly larger than the tile area
# ie: may include a banner, copyright, ect.
# this compensates for that
width = ((int((w - tile.margin * 2) + tile.spacing) / tilewidth) \
* tilewidth) - tile.spacing
height = ((int((h - tile.margin * 2) + tile.spacing) / tileheight) \
* tileheight) - tile.spacing
# using product avoids the overhead of nested loops
p = product(xrange(tile.margin, height+tile.margin, tileheight),
xrange(tile.margin, width+tile.margin, tilewidth))
for (y, x) in p:
real_gid += 1
# Puede que el llamado a ese metodo devuelva una tupla
# Sólo Dios sabe porqué...
gids = self.tmxmapdata.mapGID(real_gid)
if gids == []: continue
# Esta operacion puede ser algo lenta...
# creamos una textura (imagen en memoria de vídeo)
# a partir de una imagen cargada de acuerdo a ciertas
# coordenadas. En esté caso, "extraeremos" una baldosa
# del set de imágenes de baldosas del respectivo mapa.
# se usan cuatro Vertexs, uno como cada esquina de un plano
# orden de coordenadas: X, Y
v1 = sfml.Vertex((0, 0), None, sfml.Vector2(
float(x), float(y + totalheight)))
v2 = sfml.Vertex((0, 0), None, sfml.Vector2(
v1.tex_coords.x + tile_size[0],
v1.tex_coords.y))
v3 = sfml.Vertex((0, 0), None, sfml.Vector2(
v1.tex_coords.x + tile_size[0],
v1.tex_coords.y + tile_size[1]))
v4 = sfml.Vertex((0, 0), None, sfml.Vector2(
v1.tex_coords.x,
v1.tex_coords.y + tile_size[1]))
quad = (v1, v2, v3, v4,)
logging.debug("Quad mapeado en: ({0}),"
" ({1}), ({2}), ({3})".format(
v1.tex_coords, v2.tex_coords,
v3.tex_coords, v4.tex_coords))
# No tengo ni la menor idea sobre que hace esté bucle for
for gid, flag in gids:
logging.debug("gid: {0}, flag: {1}".format(gid, flag))
self.tmxmapdata.images[gid] = quad
# Unimos todos los tiles sets en una sola imagen
## creamos una imagen del tamaño adecuado
widthlist.sort()
logging.info("Creando imagen de {0}x{1}".format(widthlist[-1],
sum(heightlist)))
alltilesimg = sfml.Image.create(widthlist[-1],
sum(heightlist))
previousimg = sfml.Rectangle(sfml.Vector2(0.0, 0.0),
sfml.Vector2(0.0, 0.0))
for tileset in tilesets:
logging.debug("Bliteando imagen a una altura de {0}".format(
previousimg.height))
alltilesimg.blit(tileset, (0, previousimg.height))
previousimg.height += tileset.height
# Finalmente, creamos la textura con todos los tilesets
self.scenetileset = sfml.Texture.from_image(alltilesimg)
# estos vertexarray llevaran los vertices visibles unicamente,
# por capa.
self.__vertexarraytodraw = [
sfml.VertexArray(sfml.PrimitiveType.QUADS),] * len(
self.tmxmapdata.tilelayers)
# Agregamos una lista de listas vacias para colocar a los sprites
# cada lista vacia representa una capa del scenario.
self.sprites = [[],] * len(self.tmxmapdata.tilelayers)
# POSICONANDO LOS TILES #
self.__posvertexs()
# PREPARANDO SOLAMENTE LOS TILES VISIBLES #
self.__refreshvisibletiles(self.scenemanager.window.view)
logging.info("Carga de baldosas exitosa!")
else:
self.__tmxmapfile = None
self.sprites = [[]]
def __refreshvisibletiles(self, currentview):
""" Revisa cuales baldoas son visibles para el jugador.
Aunque este metodo sea llamado, la revision de baldosas se realizara
unicamente si la diferencia entre el centro del view viejo y el view
actual es mayor al ancho y alto de una baldosa (en los ejes positivos
y negativos).
"""
try:
assert self.__oldviewcenter
except AttributeError:
# FIRST time!
logging.debug("La propiedad '__oldviewcenter'"
" no existe, creandola...")
self.__oldviewcenter = currentview.center
self.__oldviewcenter += sfml.Vector2(1000.0, 1000.0)
# obtenemos la diferencia entre los centros de cada view
currentdiff = self.__oldviewcenter - currentview.center
tileheight, tilewidth = (self.tmxmapdata.tileheight,
self.tmxmapdata.tilewidth)
if ((tilewidth <= currentdiff.x) or
(-tilewidth >= currentdiff.x)) or ((tileheight <= currentdiff.y) or
(-tileheight >= currentdiff.y)):
logging.debug("El centro del view de Window"
" a cambiado, diferencia: {0}".format(currentdiff))
# sencillamente limpiamos de vertexs cada vertexarray
for vertexarray in self.__vertexarraytodraw:
vertexarray.clear()
# Sí se ha movido el centro de forma significativa!
self.__oldviewcenter = currentview.center
# Creamos un rectangulo que representa la zona visible del escenario
rect = sfml.Rectangle(currentview.center - currentview.size / 2.0,
currentview.size + sfml.Vector2(tilewidth,
tileheight)
)
logging.debug("Recreando baldosas visibles...")
for array, arrayindex, xarrayrange in self.__vertexarrayranges:
for vertex in xarrayrange:
if (rect.contains(array[vertex].position) or
rect.contains(array[vertex + 1].position) or
rect.contains(array[vertex + 2].position) or
rect.contains(array[vertex + 3].position)):
# Esta baldosa existe!
self.__vertexarraytodraw[arrayindex].append(
array[vertex])
self.__vertexarraytodraw[arrayindex].append(
array[vertex + 1])
self.__vertexarraytodraw[arrayindex].append(
array[vertex + 2])
self.__vertexarraytodraw[arrayindex].append(
array[vertex + 3])
def __posvertexs(self):
""" Posiciona los vertices y los guarda en la lista de VertexArrays.
Lo ideal es llamar a esta funcion una vez luego de cargado el mapa.
Así tendremos posicionados todos los vertices dentro de sus vertexarrays
"""
# Obtenemos el alto y ancho de las baldosas
height, width = (self.tmxmapdata.tileheight,
self.tmxmapdata.tilewidth)
tiles = product(
xrange(len(self.tmxmapdata.tilelayers)),
xrange(self.tmxmapdata.width),
xrange(self.tmxmapdata.height - 1, -1, -1) \
if self.tmxmapdata.orientation == "isometric"\
else xrange(self.tmxmapdata.height))
vertexarraylist = [
sfml.VertexArray(sfml.PrimitiveType.QUADS),] * len(
self.tmxmapdata.tilelayers)
for layer, x, y in tiles:
quad = self.tmxmapdata.getTileImage(x, y, layer)
if quad:
# Tenemos dos formas de dibujar la baldosa
# si es ortografica, entonces se coloca de
# la siguiente forma: (x * width, y * height)
# Si es isometrica, entonces de la siguiente
# forma: ((x * width / 2) + (y * width / 2),
# (y * height / 2) - (x * height /2))
# Desempacamos los Vertexs
v1, v2, v3, v4 = quad
if self.tmxmapdata.orientation == "isometric":
v1.position = sfml.Vector2(
float((x * width / 2) + (y * width / 2)),
float((y * height / 2) - (y * height / 2)))
v2.position = sfml.Vector2(
v1.position.x + width, v1.position.y)
v3.position = sfml.Vector2(
v1.position.x + width, v1.position.y + height)
v4.position = sfml.Vector2(
v1.position.x, v1.position.y + height)
else:
v1.position = sfml.Vector2(
float(x * width), float(y * height))
v2.position = sfml.Vector2(v1.position.x + width,
v1.position.y)
v3.position = sfml.Vector2(v1.position.x + width,
v1.position.y + height)
v4.position = sfml.Vector2(v1.position.x,
v1.position.y + height)
vertexarraylist[layer].append(v1)
vertexarraylist[layer].append(v2)
vertexarraylist[layer].append(v3)
vertexarraylist[layer].append(v4)
# Generamos una comprension de lista, necesitaremos
# una de estas porque van a ser accedida muchas veces
# durante la visualizacion del escenario.
self.__vertexarrayranges = [(array,
vertexarraylist.index(array),
xrange(0, len(array), 4))
for array in
vertexarraylist]
# [···]
def draw(self, target, states):
""" Dibuja el mapa del escenario.
se usa el argumento *sprites para pasar grupos de sprites que deban
ser dibujados en encontrar la capa sprite. Éste grupo de sprites
deberá de tener un método on_draw que llamara al método on_draw
de cada uno de los sprites dentro del grupo.
"""
if self.__tmxmapfile:
self.__refreshvisibletiles(self.scenemanager.window.view)
states.texture = self.scenetileset
drawables = chain.from_iterable(
izip_longest(self.__vertexarraytodraw, self.sprites))
for drawable in drawables:
if isinstance(drawable, list):
drawable.sort(key=lambda entity: entity.sprite.position.y)
for entity in drawable:
sprite.on_draw()
target.draw(entity.sprite, states)
elif isinstance(drawable, sfml.VertexArray):
target.draw(drawable, states)
else:
target.clear(sfml.Color.WHITE)
self.sprites[-1].sort(key=lambda entity: entity.sprite.position.y)
logging.debug("Dibujando {0} entidade(s)".format(
len(drawable)))
for entity in self.sprites[-1]:
entity.on_draw()
target.draw(entity.sprite, states)
Thanks!