Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: [SOLVED] VertexArray as individual layers: efficiency issue  (Read 2291 times)

0 Members and 1 Guest are viewing this topic.

shackra

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
    • http://swt.encyclomundi.org
[SOLVED] VertexArray as individual layers: efficiency issue
« on: February 14, 2013, 12:58:00 am »
Hello! ;D

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! :)
« Last Edit: February 14, 2013, 11:17:22 pm by shackra »

GNU User
Python programmer
Blog

shackra

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
    • http://swt.encyclomundi.org
Re: VertexArray as individual layers: efficiency issue
« Reply #1 on: February 14, 2013, 10:47:51 pm »
Never ever do something like this:

In [1]: import sfml

In [2]: arrays = [sfml.Ver]
sfml.Vertex       sfml.VertexArray  

In [2]: arrays = [sfml.VertexArray(sfml.PrimitiveType.QUADS),] * 10
 

Why? Plain simple for the layman:

In [3]: arrays
Out[3]:
[<sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>,
 <sfml.graphics.VertexArray at 0x12b0510>]

In [4]:

As you can see, there are 9 references to the same 1 object. So, no matter in which "layer" you may want to append a vertex, because of those references you are adding the vertexs of 10 layers to the same VertexArray, and drawing 10 times the same VertexArray, etc.

What  a novice mistake I committed xd.
That solves part of my issue, the FPS drops to 55~ when moving the view, that's is acceptable... However, I'm dealing with the issue of hiding the sprite behind other objects on the map. So, any advice or help on this will be appreciated :)

EDIT:

I realized that Tiled have a type of layer called "objects layer", and I noticed that I can place pieces of my tileset on the map, even forming thing like a house or a tree... so I can process all the objects that aren't polygons and load them as sprites, duh! xd
« Last Edit: February 14, 2013, 11:16:26 pm by shackra »

GNU User
Python programmer
Blog