In this post, I add a new type of layer dedicated to characters. These layers can draw tiles anywhere and updates their OpenGL data every time we render a frame.
This post is part of the OpenGL 2D Facade series
Objective
To check that the new layer type works fine, I propose to move a character in the level using the arrow keys:
There are no collision checks; the character can go anywhere, as long as it is inside the world. Also, note that the view follows the character.
Facade improvement
I extend the facade to introduce different layer types:

There is still a Layer
abstract class that contains features shared by all layers. Note that there is no more the setGridMesh()
method specific to a grid layer. I also rename setTexture()
to setTileset()
to get a more generic API. I guess that it is clear now that, in the specific case of OpenGL, we store tilesets in textures. In other cases, for instance using Pygame for rendering, we could store tilesets in Pygame surfaces.
The two new abstract classes GridLayer
and CharactersLayer
are the interfaces for grid layers (what we previously done) and character layers.
The API of the CharactersLayer
is the following one:
- The
setCharacterCount()
defines the maximum number of characters the layer can display. The facade user must call it first. - The
setCharacterLocation()
sets the screen location of a character. Like the translation in the view, (x,y) are float coordinates where round values correspond to cells. - The
setCharacterTile()
sets the tile of a character.
OpenGL implementation
We base both implementations of the layer classes (OpenGLGridLayer
and CharactersLayer
) on an OpenGL mesh. It is mostly as before, with a Vertex Array Object (VAO) and its Vertex Buffer Objects (VBO). We gather all the related code into the GLMesh
class, moved from the previous OpenGLLayer
class.
The setData()
has a new argument dynamic
. If set to True
, we create VBOs with the GL_DYNAMIC_DRAW
flag; otherwise, the flag is GL_STATIC_DRAW
as before. For instance, for the vertices buffer:
# Static/Dynamic flag if dynamic: flags = GL_DYNAMIC_DRAW else: flags = GL_STATIC_DRAW # Copy vertices data to GPU glBindBuffer(GL_ARRAY_BUFFER, vertexVboId) vertices = np.ascontiguousarray(vertices.flatten()) glBufferData(GL_ARRAY_BUFFER, 4 * len(vertices), vertices, flags) glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None)
GL_DYNAMIC_DRAW
and GL_STATIC_DRAW
flags are only optimization hints. Rendering is fine even if you choose the wrong one and can be a bit slower in some cases.
OpenGLGridLayer class
The OpenGLGridLayer
class has a single setTile()
method. It is like the previous setGridMesh()
method of the OpenGLLayer
class.
OpenGLCharactersLayer class
Set character count
The setCharacterCount()
method of the OpenGLCharactersLayer
class initiliazes the mesh:
def setCharacterCount(self, count: int): if self.__vertices is not None: raise RuntimeError("Update of characters count is not supported") self.__vertices = np.empty([count, 4, 2], dtype=np.float32) self.__uvMap = np.empty([count, 4, 2], dtype=np.float32) self.__faces = np.empty([count, 4], dtype=np.uint) faceCount = 0 for charIndex in range(count): self.setCharacterLocation(charIndex, 0, 0) self.setCharacterTile(charIndex, self.__transparentTile[0], self.__transparentTile[1]) self.__faces[charIndex, 0] = faceCount * 4 self.__faces[charIndex, 1] = faceCount * 4 + 1 self.__faces[charIndex, 2] = faceCount * 4 + 2 self.__faces[charIndex, 3] = faceCount * 4 + 3 faceCount += 1 self.setData(self.__vertices, self.__faces, self.__uvMap, dynamic=True)
We save the mesh (vertices
, faces
, and uvMap
) in attributes and change it in other methods.
The loop (lines 9-17) initializes each quad of the mesh. Line 10 puts the character at the top-left corner of the level (coordinates 0,0). Line 11 assigns a transparent tile to characters: consequently, after initialization, all characters are invisible. Lines 13-16 add a new set of four vertex indices for the quad.
Note: We set the transparentTile
attribute in the setTileSet()
method as the bottom right tile of the tileset (assumed as fully transparent):
def setTileset(self, fileName: str, tileWidth: int, tileHeight: int): super(OpenGLCharactersLayer, self).setTileset(fileName, tileWidth, tileHeight) self.__transparentTile = ( self.textureWidth // self.tileWidth - 1, self.textureHeight // self.tileHeight - 1 )
Note: we call the setData()
method with dynamic=True
. It is to tell OpenGL that we update our mesh regularly. It is only an optimization hint; if you forgot it, the rendering works fine.
Set character location
The setCharacterLocation()
method defines the location of a character:
def setCharacterLocation(self, charIndex: int, x: float, y: float): assert 0 <= charIndex < self.__vertices.shape[0] spriteScreenX1 = -1 + x * self.screenTileWidth spriteScreenY1 = 1 - y * self.screenTileHeight spriteScreenX2 = spriteScreenX1 + self.screenTileWidth spriteScreenY2 = spriteScreenY1 - self.screenTileHeight self.__vertices[charIndex, 0] = [spriteScreenX1, spriteScreenY2] self.__vertices[charIndex, 1] = [spriteScreenX1, spriteScreenY1] self.__vertices[charIndex, 2] = [spriteScreenX2, spriteScreenY1] self.__vertices[charIndex, 3] = [spriteScreenX2, spriteScreenY2]
You can recognize the “magic formula” we previously created.
Set character tile
The setCharacterTile()
method defines the tile of a character:
def setCharacterTile(self, charIndex: int, tileX: float, tileY: float): assert 0 <= charIndex < self.__vertices.shape[0] spriteTextureX1 = tileX * self.textureTileWidth spriteTextureY1 = tileY * self.textureTileHeight spriteTextureX2 = spriteTextureX1 + self.textureTileWidth spriteTextureY2 = spriteTextureY1 + self.textureTileHeight self.__uvMap[charIndex, 0] = [spriteTextureX1, spriteTextureY2] self.__uvMap[charIndex, 1] = [spriteTextureX1, spriteTextureY1] self.__uvMap[charIndex, 2] = [spriteTextureX2, spriteTextureY1] self.__uvMap[charIndex, 3] = [spriteTextureX2, spriteTextureY2]
You can also recognize the “magic formula” for the case of the texture.
Update OpenGL mesh data
The draw()
method sends the mesh data to OpenGL every time we call it. Then the super method renders the mesh as usual:
def draw(self): self.updateData(self.__vertices, self.__faces, self.__uvMap) super(OpenGLCharactersLayer, self).draw()
Main program
Create the characters layer
At the beginning of the program, we create the characters layer:
charsLayer = guiFacade.createCharactersLayer() charsLayer.setTileset("characters.png", 32, 32) charsLayer.setCharacterCount(1) charsLayer.setCharacterTile(0, 1, 0) characterX = 13.0 characterY = 8.0 charsLayer.setCharacterLocation(0, characterX, characterY) characterSpeed = 1.0 / 2
We create only one character, but you can try to create more: don’t forget to set a tile; otherwise, it will be transparent!
Main game loop
The main game loop moves the character and adjusts the view to ensure that he/she is always visible:
guiFacade.init() translationX = 0.0 translationY = 0.0 minTranslationX = 0 minTranslationY = 0 viewWidth = guiFacade.screenWidth / groundTileWidth viewHeight = guiFacade.screenHeight / groundTileHeight maxTranslationX = levelWidth - viewWidth maxTranslationY = levelHeight - viewHeight while not guiFacade.closingRequested: # Update inputs state guiFacade.updateInputs() # Keyboard key single press keyboard = guiFacade.keyboard for keyEvent in keyboard.keyEvents: if keyEvent.type == KeyEvent.KEYDOWN: if keyEvent.key == Keyboard.K_ESCAPE: guiFacade.closingRequested = True break keyboard.clearKeyEvents() # Keyboard key multi/continuous press if keyboard.isKeyPressed(Keyboard.K_LEFT): characterX -= characterSpeed charsLayer.setCharacterTile(0, 4, 0) if keyboard.isKeyPressed(Keyboard.K_RIGHT): characterX += characterSpeed charsLayer.setCharacterTile(0, 7, 0) if keyboard.isKeyPressed(Keyboard.K_UP): characterY -= characterSpeed charsLayer.setCharacterTile(0, 10, 0) if keyboard.isKeyPressed(Keyboard.K_DOWN): characterY += characterSpeed charsLayer.setCharacterTile(0, 1, 0) if characterX <= 0: characterX = 0 if characterY <= 0: characterY = 0 if characterX > levelWidth - 1: characterX = levelWidth - 1 if characterY > levelHeight - 1: characterY = levelHeight - 1 charsLayer.setCharacterLocation(0, characterX, characterY) # View if characterX < translationX + 10: translationX = characterX - 10 if characterY < translationY + 10: translationY = characterY - 10 if characterX > translationX + viewWidth - 10: translationX = characterX - viewWidth + 10 if characterY > translationY + viewHeight - 10: translationY = characterY - viewHeight + 10 if translationX < minTranslationX: translationX = minTranslationX if translationY < minTranslationY: translationY = minTranslationY if translationX >= maxTranslationX: translationX = maxTranslationX if translationY >= maxTranslationY: translationY = maxTranslationY guiFacade.setTranslation(translationX, translationY) # Render scene guiFacade.render()
Lines 2-9 initialize variables to handle the view.
Lines 15-21 stops the loop if the player presses the escape key.
Lines 24-46 updates the character location (in characterX
and characterY
variables) depending on arrow keys. They ensure that the location is correct and that the character does not go outside the level. They update the tile according to the character orientation using the setCharacterTile()
method.
Lines 49-67 updates the view. It is as before, except that we set the view according to the location of the character. We don’t update the view every time, but only when the character is close to a screen edge.
Final program
In the next post, we’ll start to work on text rendering.