Resizing tiles according to fixed screen resolutions.

Started by
5 comments, last by larsbutler 10 years ago

I'm developing an engine in Python/Pygame and I need some help with what I consider to be a challenging problem. I've posted the beginning of it here as "Pygrid" (But I've found another project with the same name, so I've changed it to Pythun. It's also thousand times more complex now.) In order for you to be able to help me, I'll have to describe my engine and the problem.

The Engine Module:

So far I have 12 classes: Global, Grid, GridMap, Grect, Grict, Brict, Brect, GrectArray, Controller, Animation, Tileset, Levels.

Global

A class with only static attributes and methods. It is meant to provide useful static methods like "loadImage()" from raw Pygame (SDL).

Grid

A grid object asks for 4 arguments in its constructor: A number for the width of the grid in tiles, a number for the height of the grid in tiles, a number for the fixed width in pixels of each tile and a number for the fixed height in pixels of each tile. The Grid is the main class of the engine. The positions of game-objects in each grid are defined by key-positions, not by pixels (Things move by tiles, not pixels).

GridMap

A gridmap object is an overloaded hashmap/dictionary with permutations of each possible key-position in a specified grid. You pass a grid to its constructor, and it creates a dictionary with every possible combination of (x,y) position in the grid.

Grect (Grid Rectangle)

A grect object has a position in a given grid and a color (It's a raw rectangle with a raw color. No image).

Grict (Grid Image Rectangle)

The same as the above one, but much more complex. It takes a path and a name (For the image file), a boolean defining wether it is used as a spritesheet or not, and if it is, it takes a colorkey in order to deal with fixed-color transparency (By default, the colorkey is equivalent to the 0x and 0y position of the raw image file).

Brect (Background Rectangle)

This object just takes a raw color and fills the whole given grid.

Brict (Background Image Rectangle)

The same as above, but much more complex. Just like the grict, it has a path and a name for a raw image file, but it takes a boolean defining wether it scrolls in a given direction or not (Background image scrolling is supported natively).

GrectArray

An array of grects (Actually, a list).

Controller

A controller object is master of a slave. The slave is a grid object (A grect or grectarray or grict). It controls/moves the slave through the key-positions of the given grid object.

Animation

An animation object is a sequence (overloaded python list) of pygame surfaces (Where each surface is a frame of the animation). The animation can loop fowards or backwards, and it provides an "animate()" method to iterate through the frames in a game-loop. (Pygame has a native "Sprite" class or whatever, but I decided to make all these things from scratch in order to fit in my grid-based engine.)

Tileset (The beast).

It does not seem to be complicated, but it is. It takes parameters from a parsed XML-map from the Tiled software and map the whole tiles from a whole tileset in a given grid (It's horrid, but it works). I'm not going to talk about the "Levels" class because it is incomplete. Here's the code for the Tileset class (My engine is open-source, and I'll release a stable version relatively soon):


class Tileset():

    """
    A Pythun Tileset makes things easier to work with tilesets. (I'll document the beast later).
    """

    def __init__(self, xmlMapFile, path, name, isSpriteSheet=False, colorkey=None):

        self.xml = esp.ESP(xmlMapFile) # It creates a XML Tree for the Tileset instance.
        # Collected information from the TILED .tmx/XML file.
        self.widthInPixels = int(self.xml.getValueInElementByTag('image','width'))
        self.heightInPixels = int(self.xml.getValueInElementByTag('image','height'))
        self.widthInTiles = int(self.xml.getValueInElementByTag('map','width'))
        self.heightInTiles = int(self.xml.getValueInElementByTag('map','height'))
        self.tileWidth = int(self.xml.getValueInElementByTag('tileset','tilewidth'))
        self.tileHeight = int(self.xml.getValueInElementByTag('tileset','tileheight'))
        # Two Grids. One for the map, and one for the tileset.
        self.mapGrid = Grid([self.widthInTiles,self.heightInTiles],[self.tileWidth,self.tileHeight])
        self.tilesetGrid = Grid([self.widthInPixels/self.widthInTiles,self.heightInPixels/self.heightInTiles],[self.tileWidth,self.tileHeight])
        # Trivial information.
        self.x = 0
        self.y = 0
        self.path = path
        self.name = name
        self.isSpriteSheet = isSpriteSheet
        self.colorkey = colorkey
        self.tileset = Grict(self.tilesetGrid,self.x,self.y,self.path,self.name,self.isSpriteSheet,self.colorkey)
        # Gids from the XML.
        self.gids = []
        for i in self.xml.getElementByTag('tile'):
            self.gids.append(i[1])
        # It creates a GridMap for the instance (A little tuple-trick of mine (See GridMap class)).
        self.gridMap = GridMap(self.mapGrid) # Instantiating, using the grid for the map as a grid reference. (mapGrid and GridMap are different things).
        self.gridKeys = self.gridMap.keys() # The keys of the instance of the GridMap (It returns the product of every possible combination of positions in the specified grid, in tuples.)
        self.gridKeys.sort() # They're dicts, so they need to be properly ordered for further XML-analysis.
        self.gridKeys = zip(self.gridKeys,self.gids) # Associates each tuplePosition-key with the XML-gids.
        subs = [Grict(self.tilesetGrid)] # Creates a default 0 Grict (It shall correspond to the 0-gid of the XML (It's necessary to have it, otherwise the 0 gid would be considered and the order would be wrong.)).
        subs[0].setSurface(pygame.Surface((1,1),pygame.SRCALPHA,32).convert_alpha()) # It sets a transparent pygame Surface to it.
        filtered = [] # A list for the Gricts corresponding to the tiles of the XML-map.
        w = self.tileWidth
        h = self.tileHeight
        # It iterates over the size of the tileset in pixels by step of the size of each tile (For each collumn it iterates over each row).
        for i in range(0,self.heightInPixels,self.tileHeight):
            for j in range(0,self.widthInPixels,self.tileWidth):
                g = Grict(self.tilesetGrid) # Creates a local Grict.
                # It sets a surface for the local grid, by the process of subsurfacing the original tileset.
                g.setSurface(self.tileset.getSubSurface((j,i,w,h)))
                # It appends each tile from the tileset as Grict in the subs list (After the default 0 Grict).
                g.setSurface(pygame.transform.scale(g.getSurface(), (16, 16)))
                subs.append(g)
        # That was ALL the tiles.
        # Now we filter only the gricts used in the map.
        for j in self.gids: # For each string-gid.
            for i in range(len(subs)): # It iterates over the size of the whole amount of tiles/gricts.
                if str(i) == j: # If the index-position equals to the gid (In the case, only the gids used in the map)
                    filtered.append(subs[i]) # Append the filtered grict to the filtered list.
        self.gids = zip(self.gids,filtered) # It combines the xml-map-gids with the filtered/right gricts/tiles.

    def getXML(self):

        """
        getXML() -> ESP
        It returns the parsed form of the XML map document used by the Tileset instance.
        """

        return self.xml

    def getWidthInPixels(self):

        """
        getWidthInPixels() -> int
        It returns the width of the image used by the tileset in pixels.
        """

        return self.widthInPixels

    def getHeightInPixels(self):

        """
        getHeightInPixels() -> int
        It returns the height of the image used by the tileset in pixels.
        """

        return self.heightInPixels

    def getWidthInTiles(self):

        """
        getWidthInTiles() -> int
        It returns the width of the image used by the tileset in tiles.
        """

        return self.widthInTiles

    def getHeightInTiles(self):

        """
        getHeightInTiles() -> int
        It returns the height of the image used by the tileset in tiles.
        """

        return self.heightInTiles

    def getTileWidth(self):

        """
        getTileWidth() -> int
        It returns the fixed width of each tile of the tileset.
        """

        return self.tileWidth

    def getTileHeight(self):

        """
        getTileHeight() -> int
        It returns the fixed height of each tile of the tileset.
        """

        return self.tileHeight

    def getMapGrid(self):

        """
        getMapGrid() -> Grid
        It returns the grid used for the whole map. (Used for the MapGrid class).
        """

        return self.mapGrid

    def getTilesetGrid(self):

        """
        getTilesetGrid() -> Grid
        It returns the grid used for the whole tileset.
        """

        return self.tilesetGrid

    def getX(self):

        """
        getX() -> int
        It returns the initial X position of the tileset (Not used by default).
        """

        return self.x

    def getY(self):

        """
        getY() -> int
        It returns the initial Y position of the tileset (Not used by default).
        """

        return self.y

    def getPath(self):

        """
        getPath() -> String
        It returns the path used for the tileset image file.
        """

        return self.path

    def getName(self):

        """
        getName() -> String
        It returns the name used for the tileset image file.
        """

        return self.name

    def isSpriteSheet(self):

        """
        isSpriteSheet() -> Boolean
        It returns wether the tileset image is used as a spritesheet or not (For colorkey purposes).
        This method is only here because the tileset image is loaded through a Grict.
        """

        return self.isSpriteSheet

    def getColorkey(self):

        """
        getColorkey() -> int
        This method is only here because the tileset image is loaded through a Grict.
        """

        return self.colorkey

    def getTileset(self):

        """
        getTileset() -> Grict
        It returns the grict (with the image) used for the tileset.
        """

        return self.tileset

    def setXML(self, newXML):

        """
        It sets a new ESP for the tileset instance.
        """

        self.xml = newXML

    def setWidthInPixels(self, newValue):

        """
        It sets a new width in pixels (int) for the tileset instance.
        """

        self.widthInPixels = newValue

    def setHeightInPixels(self, newValue):

        """
        It sets a new height in pixels (int) for the tileset instance.
        """

        self.heightInPixels = newValue

    def setWidthInTiles(self, newValue):

        """
        It sets a new width in tiles (int) for the tileset instance.
        """

        self.widthInTiles = newValue

    def setHeightInTiles(self, newValue):

        """
        It sets a new height in tiles (int) for the tileset instance.
        """

        self.heightInTiles = newValue

    def setTileWidth(self, newValue):

        """
        It sets a new fixed width for each tile of the tileset instance (int).
        """

        self.tileWidth = newValue

    def setTileHeight(self, newValue):

        """
        It sets a new fixed height for each tile of the tileset instance (int).
        """

        self.tileHeight = newValue

    def setMapGrid(self, newGrid):

        """
        It sets a new grid for the map grid of the tileset instance.
        """

        self.mapGrid = newGrid

    def setTilesetGrid(self, newGrid):

        """
        It sets a new grid for the tileset grid of the tileset instance.
        """

        self.tilesetGrid = newGrid

    def setX(self, newValue):

        """
        It sets a new x position for the tileset instance (int).
        """

        self.x = newValue

    def setY(self, newValue):

        """
        It sets a new y position for the tileset instance (int).
        """

        self.y = newValue

    def setPath(self, newPath):

        """
        It sets a new path for the image file of the tileset instance (String).
        """

        self.path = newPath

    def setName(self, newName):

        """
        It sets a new name for the image file of the tileset instance (String).
        """

        self.name = newName

    def setColorkey(self, newValue):

        """
        It sets a new colorkey for the image of the tileset instance (int).
        """

        self.colorkey = newValue

    def setTileset(self, newTileset):

        """
        It sets a whole new Grict for the tileset instance.
        """

        if isinstance(newTileset, Grict):
            self.tileset = newTileset
        else:
            raise error.InvalidObject("The given tileset is not a Grict.")

The engine loads maps with tilesets. The problem is the size of the screen. So far, the default screen size is based on the size of the whole map. Considering that it is stupid (Considering the fact that one could have a 8000 by 8000 worldmap size), I needed to work with cameras and fixed screen resolutions. So I just defined a good static dictionary in the Global class with all possible official resolutions I could find:


RESOLUTIONS = {"CGA":(320,200),"CIF":(352,288),"HVGA":(480,320),
                    "QVGA":(320,240),"SIF**":(384,288),"WVGA":(800,480),
                    "WVGA(NTSC_CROSS)":(854,480),"PAL_CROSS":(1024,576),
                    "WSVGA":(1024,600),"VGA(NTSC*)":(640,480),"PAL*":(768,576),
                    "SVGA":(800,600),"XGA":(1024,768),"XGA_PLUS":(1152,864),
                    "HD720":(1280,720),"WXGA1":(1280,768),"WXGA2":(1280,800),
                    "WSXGA_PLUS":(1680,1050),"HD1080":(1920,1080),"2K":(2048,1080),
                    "WUXGA":(1920,1200),"SXGA":(1280,1024),"SXGA_PLUS":(1400,1050),
                    "UXGA":(1600,1200),"QXGA":(2048,1536),"WQHD":(2560,1440),"WQXGA":(2560,1600),"QSXGA":(2560,2048)}

I have not implemented the camera-system yet, because I have to deal with this problem first. The thing is:

What if the size of each tile in a tileset is of 16 by 16? The screen size will be really, really small. I want to resize each individual tile according to the resolutions in the dictionary. How would you do that? (I mean, not exactly considering the python implementation). How would you resize each tile of a loaded tileset considering fixed screen resolutions for obvious purposes? It's common to have small tiles (Supposing that you want a game with a somewhat oldschool-looking), but I want to resize them all. Perhaps I'm not that good with math, and my brain is quite overloaded already implementing this whole madness, and I'm not being able to process the problem efficiently. I need to deal properly with fullscreen and I want each tile to be resized according to the present screen resolution. It might be an easy problem to solve when not considering the whole grid-system implementation.

I thank you very much for your help.

Creator and only composer at Poisone Wein and Übelkraft dark musical projects:

Advertisement

Alternatively, would it be easier just to resize the content after it's all rendered and scale that up? ie, everything gets rendered to a 256x256 bitmap but displayed at 512x512 or something. (or vice versa for the other way) That way, for your game, the units stay the same, tile sizes stay the same, etc. You may end up with some letterboxing, but that's probably no worse than if you resized the tiles.

Alternatively, would it be easier just to resize the content after it's all rendered and scale that up? ie, everything gets rendered to a 256x256 bitmap but displayed at 512x512 or something. (or vice versa for the other way) That way, for your game, the units stay the same, tile sizes stay the same, etc. You may end up with some letterboxing, but that's probably no worse than if you resized the tiles.

That was the first thing I thought to solve the problem, but I verified that I had no control over the "stretch" in fullscreen, and that the pygame or SDL display did not handle it very well. I've managed to solve the problem.

Consider that the visible area of the game is of 16 tiles, each one with 16 pixels (In both width and height). Then, we have a displayed game of 256 by 256 pixels. Nevertheless, if I'm currently using the "VGA(NTSC*)" resolution (640 by 480), then, we have the question:

What width for the tile I need to have in order to fill 640 pixels by using just 16 tiles? It happens that it is very easy to get the right value. It's just necessary to divide the current used width or height by the number of tiles.

256px width divided by 16 tiles gives us 16 (The size we had).

640px width divided by 16 tiles gives us 40. So, that's it! The width of each tile must be of 40 pixels in order to fill the whole screen with 16 tiles.

(It's worth mentioning that my implementation transforms the image automatically to fit the grid tile, so I just need to change the size of the tile.)

It worked beautifully. (It's just necessary to adjust to the environment and do the proper math with the variables, of course.. In the case of my engine, it was a bit messy..):


self.widthInPixels = int(self.xml.getValueInElementByTag('image','width'))
        self.heightInPixels = int(self.xml.getValueInElementByTag('image','height'))
        self.widthInTiles = int(self.xml.getValueInElementByTag('map','width'))
        self.heightInTiles = int(self.xml.getValueInElementByTag('map','height'))
        self.tileWidth = int(self.xml.getValueInElementByTag('tileset','tilewidth'))
        self.tileHeight = int(self.xml.getValueInElementByTag('tileset','tileheight'))
        # Two Grids. One for the map, and one for the tileset.
        print Global.res[0]/self.tileWidth
        self.mapGrid = Grid(((self.widthInTiles,self.heightInTiles),
                             ((self.tileWidth+((Global.res[0]/self.tileWidth)-self.tileWidth)),
                              (self.tileHeight+(Global.res[1]/self.tileWidth)-self.tileHeight))))
        self.tilesetGrid = Grid(((self.widthInPixels/self.widthInTiles,self.heightInPixels/self.heightInTiles),
                                 ((self.tileWidth+(Global.res[0]/self.tileWidth)-self.tileWidth),
                                  ((self.tileHeight+Global.res[1]/self.tileWidth)-self.tileHeight))))

Creator and only composer at Poisone Wein and Übelkraft dark musical projects:

Alternatively, would it be easier just to resize the content after it's all rendered and scale that up? ie, everything gets rendered to a 256x256 bitmap but displayed at 512x512 or something. (or vice versa for the other way) That way, for your game, the units stay the same, tile sizes stay the same, etc. You may end up with some letterboxing, but that's probably no worse than if you resized the tiles.

That was the first thing I thought to solve the problem, but I verified that I had no control over the "stretch" in fullscreen, and that the pygame or SDL display did not handle it very well. I've managed to solve the problem.

With the implementation you have there, aren't all your units going to be off, or do you have everything in tilewidth/tileheight? (Height/width may not be the same if the aspect ratio is funky!) Are all your jump heights in tileheight, and all your horizontal motions in tilewidth? Is gravity in tileheight? My fear is that the game wouldn't play the same on every platform.

I think you might be better off making sure you render to the full screen size, but doing it by filling the whole screen size black or whatever, and then rendering the game into the center of that full screen size, so there is no stretching at all. (or if you want to go the harder route, figure out how much stretching would be done, and render the black only when/where you would need to, so that if you expect 16:9 and get 4:3, you add the black bars yourself).

there are a few basic approaches to supporting multiple screen resolutions.

six come to mind offhand:

1. write everything using a virtual coordinate system (say 10000 x 10000) then convert on the fly to the current screen size. 5000,5000 virtual (the center of the screen) @ 1600x900 resolution = hardware pixel coordinates 800,450.

2. a variation on method 1: write everything using a single hardware resolution (such as 1600x900) then convert on the fly to the current screen size. nice if you've already started coding to one screen size. only one catch - scaling up to higher resolutions may introduce some graphic artifacts due to scaling inaccuracies. that's why 10000x10000 is better, you only scale down, never up - well, unless you're running at a rez higher than 10000x10000! <g>.

3. as mentioned above, use one resolution, then strechblit to the current screen size. can be slower than methods 1 and 2.

4. support just a few common resolutions, and write resolution dependent code for each. runs nice and fast. limits rez's supported. more coding work than some other methods.

5. don't support re-sizing of graphics which are resolution dependent. games with popup menus that get smaller as you increase screen size are like this. d3d draws pretty much the same at all resolutions, but sprites and fonts change size. I believe Oblivion might have done this. this is the easiest way. no extra work. but you get what you pay for, as they say.

6. don't support multiple screen resolutions at all. most games really only need to support a few resolutions, the one or two or three common high end ones that its really

designed to target, and perhaps one lower end one if you want to reach the legacy hardware base out there. if all supported rez's are similar in size, this plus method 5 can yield acceptable results.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Alternatively, would it be easier just to resize the content after it's all rendered and scale that up? ie, everything gets rendered to a 256x256 bitmap but displayed at 512x512 or something. (or vice versa for the other way) That way, for your game, the units stay the same, tile sizes stay the same, etc. You may end up with some letterboxing, but that's probably no worse than if you resized the tiles.

That was the first thing I thought to solve the problem, but I verified that I had no control over the "stretch" in fullscreen, and that the pygame or SDL display did not handle it very well. I've managed to solve the problem.

With the implementation you have there, aren't all your units going to be off, or do you have everything in tilewidth/tileheight? (Height/width may not be the same if the aspect ratio is funky!) Are all your jump heights in tileheight, and all your horizontal motions in tilewidth? Is gravity in tileheight? My fear is that the game wouldn't play the same on every platform.

I think you might be better off making sure you render to the full screen size, but doing it by filling the whole screen size black or whatever, and then rendering the game into the center of that full screen size, so there is no stretching at all. (or if you want to go the harder route, figure out how much stretching would be done, and render the black only when/where you would need to, so that if you expect 16:9 and get 4:3, you add the black bars yourself).

You were right! There was a problem I could not see considering that the size was fixed (16 by 16). So I just changed the whole tileset for 175x150 each tile and changed completely the number of tiles. (And it rendered completely wrong). NOW it's working correctly:


self.mapGrid = Grid(((self.widthInTiles,self.heightInTiles),
                     ((self.tileWidth+((Global.res[0]/self.widthInTiles)-self.tileWidth)), # Resolution width divided by WIDTH IN TILES. (Not the width of each tile)
                      (self.tileHeight+(Global.res[1]/self.heightInTiles)-self.tileHeight)))) # Resolution width divided by HEIGHT IN TILES.
self.tilesetGrid = Grid(((self.widthInPixels/self.widthInTiles,self.heightInPixels/self.heightInTiles),
                         ((self.tileWidth+(Global.res[0]/self.widthInTiles)-self.tileWidth),
                          ((self.tileHeight+Global.res[1]/self.heightInTiles)-self.tileHeight))))

Creator and only composer at Poisone Wein and Übelkraft dark musical projects:

I just have a comment about style. Please forgive the nitpick. =)

Implementing getters and setters in this way is totally unnecessary (and it's also not pythonic!). See: http://stackoverflow.com/questions/2627002/whats-the-pythonic-way-to-use-getters-and-setters

Coming from Java background, I found myself first doing this as well when I started writing Python. But since there no concept of "private" variables in Python, you can remove all of this superfluous code. Example:


class Foo(object):

  def __init__(self, x, y, bar):
    self.x = x
    self.y = y
    self.bar = bar

  def getX(self):
    return self.x

  def setX(self, x):
    self.x = x

  def getY(self):
    return self.y

  def setY(self, y):
    self.y = y

  def getBar(self):
    return self.bar

  def setBar(self, bar):
    self.bar = bar

foo = Foo(10, 15, 'blarg')
foo.setX(1000)
print(foo.getX())

versus a much shorter and idiomatic style:


class Foo(object):

  def __init__(self, x, y, bar):
    self.x = x
    self.y = y
    self.bar = bar

foo = Foo(10, 15, 'blarg')
foo.x = 1000
print(foo.x)

This topic is closed to new replies.

Advertisement