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.