Optimization

Step 7
Step completed?

Graphic rendering optimization and display acceleration.

Graphic optimization

The modeling we have adopted for the snake is already optimized by our system of ring lists to store the successive positions occupied by the snake's head on the grid. So there's not much to optimize on that side.

On the other hand, we can bring a substantial optimization on the graphic rendering of the game scene... Indeed, if you manage to keep the snake alive long enough for its tail to reach a sufficient length, you will find that the game will start to slow down... (after about twenty apples swallowed).

Unlike C++, our Python script is interpreted by CircuitPython, which in turn executes precompiled routines in machine language. The intermediate layer induced by the interpreter already implies an intrinsic slowing down of your code. It is therefore in your best interest to apply procedures that are inexpensive in CPU time to perform your own routines.

Concerning the graphic rendering of the game scene, remember how to draw the snake:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def drawSnake():
    display.setColor(COLOR_SNAKE)
    n = snake['len']
    for i in range(n):
        x = snake['x'][i]
        y = snake['y'][i]
        display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

Each section is redrawn at each cycle of the main loop. However, if you think carefully, you will notice that it is not necessary to redraw the snake entirely. Well, since from one cycle to the next, what exactly changes?

Gamebuino META

Only the ends of the snake really need a graphic refresh. Moreover, before redrawing the game scene, we start by deleting everything:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    clearScreen()
    drawWalls()
    drawSnake()
    drawScore()
    drawApple()

These operations are expensive, but they are not necessary...

So the idea is not to delete everything when it is not necessary, and to redraw only what has changed compared to the previous cycle.

The first thing to do is to define an additional property at the game engine level that tells it whether or not, at the next cycle, it should completely clear the game scene before redrawing it:

# ----------------------------------------------------------
# Initialization
# ----------------------------------------------------------

game  = {
    'mode':    MODE_START,
    'score':   0,
    'refresh': True
}

True will be assigned to game['refresh''] when you want a complete refresh of the game scene, and False otherwise.

We can therefore modify the draw() function to take this new data into account:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    if game['refresh']:
        clearScreen()
        drawWalls()
        drawSnake()
    else:
        drawSnakeHead()
    drawScore()
    drawApple()

Here, the drawSnake() function is always the one that will be in charge of redrawing the entire snake. On the other hand, a new display function drawSnakeHead() is introduced here, which only redraws its head. We will define this function in a moment. But first, let's try to factor in our code....

To draw a section of the snake, we use the display.fillRect() function:

def drawSnake():
    display.setColor(COLOR_SNAKE)
    n = snake['len']
    for i in range(n):
        x = snake['x'][i]
        y = snake['y'][i]
        display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

It's a lot like the way we draw apples:

def drawApple():
    display.setColor(COLOR_APPLE)
    x = apple['x']
    y = apple['y']
    display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

Only the color of the rectangle finally changes... We can therefore factor the code by defining a common routine drawDot(), on which the functions drawSnake() and drawApple() can be based:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def drawSnake():
    n = snake['len']
    for i in range(n):
        drawDot(snake['x'][i], snake['y'][i], COLOR_SNAKE)

def drawApple():
    drawDot(apple['x'], apple['y'], COLOR_APPLE)

def drawDot(x, y, color):
    display.setColor(color)
    display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

Reusing the routines of your code is a good programming practice that I encourage you to systematically apply whenever possible.

We can now define the drawSnakeHead() function by also using the drawDot() function:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def drawSnakeHead():
    h = snake['head']
    drawDot(snake['x'][h], snake['y'][h], COLOR_SNAKE)

To erase the end of the tail, simply redraw it with the background color, and you're done! Let's add the clearSnakeTail() function that will handle this:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def clearSnakeTail():
    h = snake['head']
    n = snake['len']
    t = (h + 1) % n
    drawDot(snake['x'][t], snake['y'][t], COLOR_BG)

It remains to be determined now when to set the game['refresh'] value to True or False, and when to run clearSnakeTail().

Let's get back to our scheduler. At each cycle of the main loop, you enter the scheduler to update the game situation. In other words, if the snake is moving, it will necessarily change position with a call to the moveSnake() function. This function will change the position of its head and overwrite the last known position of the end of its tail. So you have to call the function clearSnakeTail() before it happens !

# ----------------------------------------------------------
# Game management
# ----------------------------------------------------------

def tick():
    if not game['refresh']:
        clearSnakeTail()
    
    if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_READY
        game['score'] = 0
    elif game['mode'] == MODE_READY:
        game['refresh'] = False
        handleButtons()
        moveSnake()
        if snakeHasMoved():
            game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if game['refresh']:
            game['refresh'] = False
        if didSnakeEatApple():
            game['score'] += 1
            game['refresh'] = True
            extendSnakeTail()
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START
            game['refresh'] = True

    draw()

This is the reason why we start immediately by analyzing the value of game['refresh']. And if its value is False we can deduce that the snake is already in motion. So we erase the extremity of the tail at that moment, before we can no longer do it because its position will have been overwritten by moveSnake():

if not game['refresh']:
    clearSnakeTail()

Then, when you enter the MODE_READY game phase, it means that the snake has just been set in motion. It is therefore necessary to set the game['refresh'] value to False to disable the total refresh of the game scene:

elif game['mode'] == MODE_READY:
    game['refresh'] = False
    handleButtons()
    moveSnake()
    if snakeHasMoved():
        game['mode'] = MODE_PLAY

Then we switch to the MODE_PLAY phase, and there things are likely to change:

  • when the snake swallows an apple,
  • or when the snake bites its tail or hits a wall.

In both cases, the game scene must be completely redrawn, so the value of game['refresh'] must be set to True. But the next time you go through this routine, you will have to immediately disable the global graphic refresh again (which will already have taken place):

elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if game['refresh']:
            game['refresh'] = False
        if didSnakeEatApple():
            game['score'] += 1
            game['refresh'] = True
            extendSnakeTail()
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START
            game['refresh'] = True

There you go! You can now test the result... You'll see that from now on, the game will no longer slow down, even after the snake has swallowed more than fifty apples!

Démo

This is the complete code.py scipt, which integrates everything we have done so far:

from gamebuino_meta import waitForUpdate, display, color, buttons
from random import randint

# ----------------------------------------------------------
# Global variables
# ----------------------------------------------------------

SCREEN_WIDTH  = 80
SCREEN_HEIGHT = 64
SNAKE_SIZE    = 2
SNAKE_LENGTH  = 4
SNAKE_EXTENT  = 2
COLS          = (SCREEN_WIDTH  - 4) // SNAKE_SIZE
ROWS          = (SCREEN_HEIGHT - 4) // SNAKE_SIZE
OX            = (SCREEN_WIDTH  - COLS * SNAKE_SIZE) // 2
OY            = (SCREEN_HEIGHT - ROWS * SNAKE_SIZE) // 2
COLOR_BG      = 0x69c0
COLOR_WALL    = 0xed40
COLOR_SNAKE   = 0xfd40
COLOR_APPLE   = 0x07f0
COLOR_SCORE   = 0xffff
MODE_START    = 0
MODE_READY    = 1
MODE_PLAY     = 2

# ----------------------------------------------------------
# Game management
# ----------------------------------------------------------

def tick():
    if not game['refresh']:
        clearSnakeTail()
    
    if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_READY
        game['score'] = 0
    elif game['mode'] == MODE_READY:
        game['refresh'] = False
        handleButtons()
        moveSnake()
        if snakeHasMoved():
            game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if game['refresh']:
            game['refresh'] = False
        if didSnakeEatApple():
            game['score'] += 1
            game['refresh'] = True
            extendSnakeTail()
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START
            game['refresh'] = True

    draw()

def spawnApple():
    apple['x'] = randint(0, COLS - 1)
    apple['y'] = randint(0, ROWS - 1)

def handleButtons():
    if buttons.pressed(buttons.LEFT):
        dirSnake(-1, 0)
    elif buttons.pressed(buttons.RIGHT):
        dirSnake(1, 0)
    elif buttons.pressed(buttons.UP):
        dirSnake(0, -1)
    elif buttons.pressed(buttons.DOWN):
        dirSnake(0, 1)

# ----------------------------------------------------------
# Snake management
# ----------------------------------------------------------

def resetSnake():
    x = COLS // 2
    y = ROWS // 2
    snake['x'] = []
    snake['y'] = []
    for _ in range(SNAKE_LENGTH):
        snake['x'].append(x)
        snake['y'].append(y)
    snake['head'] = SNAKE_LENGTH - 1
    snake['len']  = SNAKE_LENGTH
    snake['vx'] = 0
    snake['vy'] = 0

def dirSnake(dx, dy):
    snake['vx'] = dx
    snake['vy'] = dy

def moveSnake():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    h = (h + 1) % snake['len']
    snake['x'][h] = x + snake['vx']
    snake['y'][h] = y + snake['vy']
    snake['head'] = h

def snakeHasMoved():
    return snake['vx'] or snake['vy']

def didSnakeEatApple():
    h = snake['head']
    return snake['x'][h] == apple['x'] and snake['y'][h] == apple['y']

def extendSnakeTail():
    i = snake['head']
    n = snake['len']
    i = (i + 1) % n
    x = snake['x'][i]
    y = snake['y'][i]
    for _ in range(SNAKE_EXTENT):
        snake['x'].insert(i, x)
        snake['y'].insert(i, y)
    snake['len'] += SNAKE_EXTENT

def didSnakeBiteItsTail():
    h = snake['head']
    n = snake['len']
    x = snake['x'][h]
    y = snake['y'][h]
    i = (h + 1) % n
    for _ in range(n-1):
        if snake['x'][i] == x and snake['y'][i] == y:
            return True
        i = (i + 1) % n
    return False

def didSnakeHitTheWall():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    return x < 0 or x == COLS or y < 0 or y == ROWS

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    if game['refresh']:
        clearScreen()
        drawWalls()
        drawSnake()
    else:
        drawSnakeHead()
    drawScore()
    drawApple()

def clearScreen():
    display.clear(COLOR_BG)

def drawWalls():
    display.setColor(COLOR_WALL)
    display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)

def drawSnake():
    n = snake['len']
    for i in range(n):
        drawDot(snake['x'][i], snake['y'][i], COLOR_SNAKE)

def drawSnakeHead():
    h = snake['head']
    drawDot(snake['x'][h], snake['y'][h], COLOR_SNAKE)

def clearSnakeTail():
    h = snake['head']
    n = snake['len']
    t = (h + 1) % n
    drawDot(snake['x'][t], snake['y'][t], COLOR_BG)

def drawScore():
    display.setColor(COLOR_SCORE)
    display.print(2, 2, game['score'])

def drawApple():
    drawDot(apple['x'], apple['y'], COLOR_APPLE)

def drawDot(x, y, color):
    display.setColor(color)
    display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

# ----------------------------------------------------------
# Initialization
# ----------------------------------------------------------

game = {
    'mode':    MODE_START,
    'score':   0,
    'refresh': True
}

snake = {
    'x':    [],
    'y':    [],
    'head': 0,
    'len':  0,
    'vx':   0,
    'vy':   0
}

apple = { 'x': 0, 'y': 0 }

# ----------------------------------------------------------
# Main loop
# ----------------------------------------------------------

while True:
    waitForUpdate()
    tick()

You can now move on to the next and final step, in which we will refine the end of the game.

Steps