1. Your first game

Before creating these files, complete desktop setup and run demos.tutorial_game. Keep the terminal open to see tracebacks.

A game is a folder. Create one under your own group name:

mkdir -p games/myname/mygame/code games/myname/mygame/images

Four things go in it:

games/myname/mygame/
├── code/mygame.py        your game; the module name matches the folder
├── images/               PNGs plus __images__.yaml
├── menu.png              the icon shown in the console menu, 64 x 30 pixels
└── meta.json             how the launcher lists it

menu.png is a 64 by 30 pixel picture, which the launcher shows on your game’s tile. Make it in any pixel editor, or copy games/demos/tutorial_game/menu.png to start with and change it. Keep it simple and dark, like the display: transparent pixels draw nothing.

meta.json

This is what opts your game into VS2:

{
  "api": "vs2",
  "api_revision": 2,
  "title": "My Game",
  "order": 50
}

api and api_revision are both required. title and order control the menu entry.

Images

Ventilastation cannot open PNGs directly. images/__images__.yaml describes how they become sprite strips:

palettegroups:
  main:
    - strip: ship.png
      frames: 3
    - strip: enemy.png
      frames: 6
    - strip: trench.png
      frames: 8
    - strip: numerals.png
      frames: 12
      glyphs: "0123456789 *"
  text:
    - strip: steel8x8.png
      frames: 256

This is the complete file for the finished game, so you can write it once. This chapter only uses ship.png; the other strips appear in the chapters that follow, and the glyphs: line is explained in chapter 5. Every strip needs a frames: entry, because the file does not say how many images a PNG holds.

A strip is a horizontal filmstrip of equally sized frames — a 3-frame strip is one PNG three times as wide as one frame:

The ship.png strip: three 19 by 19 pixel frames side by side, a level ship and the ship turned to each side

ship.png is 57 pixels wide and holds three frames, so its entry says frames: 3.

A palette group is a set of images that share 256 colours; put images that look alike in one group. The text font is in a group of its own, because its colours have little in common with the game art.

To follow along, copy all five PNGs Trench Run uses into your images/ folder from games/demos/tutorial_game/images/: ship.png, enemy.png, trench.png, numerals.png and steel8x8.png. This chapter only draws ship.png, but the emulator builds every PNG the yaml lists, and one that is missing stops it from starting, so copy them all now.

The emulator recompiles changed PNGs into a ROM every time it starts, so you just edit and rerun.

The code

code/mygame.py needs a scene — a Scene subclass — and a main() that returns an instance:

import vs2
from vs2.controls import *


class Game(vs2.Scene):
    def build(self):
        self.world = self.layer("world", projection=vs2.TUNNEL)
        self.ship = self.world.sprite("ship.png", y=0)
        self.ship.x = -(self.ship.width // 2)

    def update(self):
        if joy1.held(LEFT):
            self.ship.x += 1
        if joy1.held(RIGHT):
            self.ship.x -= 1


def main():
    return Game()

That is a complete program, though not much of a game yet: it puts a ship on the disc and lets you steer it. Run it by naming the game, which skips the menu:

./vs-emu.sh --game myname.mygame        # vs-emu.bat --game myname.mygame on Windows

The name is the group and the folder, joined with a dot. When the game exits (the back button, or a crash) the emulator returns to the menu as usual, and the game is also there under Más aplicaciones, then myname, then My Game; run ./vs-emu.sh with no arguments to start from the menu. The ship sits at the bottom of the disc, because x = 0 is the bottom and y = 0 is the rim:

The emulator showing the ship at the bottom of the disc

The ship at x = 0, y = 0: the bottom of the disc, on the rim.

A sprite’s x is where its image starts, not its middle, so the second line of build() backs up by half the ship’s width, 9 columns, to centre the ship on the bottom.

Hold the left button and x counts up: at the bottom of the disc that is the direction of the player’s left, as the diagram in the next chapter shows. The ship slides toward the left side of the disc:

The emulator showing the ship moved toward the lower left

After holding left for a moment.

Controls

The code asks a controller for its buttons: joy1.held(LEFT) is true while the left button is down. On the console players use the game controllers. In the desktop emulator the keyboard stands in for them:

Keys

Controller 1

Arrow keys, or W A S D

LEFT RIGHT UP DOWN

Space O P Y

A B X Y

Page Up / Page Down

START / BACK

A USB gamepad works too. joy2 is the second controller, on H J K L, Z X C V, Home and End. Chapter 6 explains how held() differs from just_pressed(); until then every button in the tutorial is held().

When something goes wrong

Errors and anything your game print()s appear in the terminal where you ran vs-emu.sh. A crash is marked Rotor traceback, with the file and line that raised it. Watch that window while you edit, since mistakes like the one in the next section show up there and not on the disc.

What those two methods mean

Build runs once each time the scene is entered and creates everything the scene will ever draw. update() runs about 33 times a second (once per tick) and moves what already exists.

A scene starts building, becomes sealed when build returns, and is closed by pop, push or switch

A scene’s life. Only the building state may create layers and drawables.

The split is enforced. When build() returns, the scene is sealed: try to create a sprite from update() and you get

SceneSealedError: sprite() is only allowed while Game.build() runs

So running out of sprites becomes an error the first time you enter the scene, not a surprise in the middle of play.

Leaving the game

Players leave with the back button (Y or BACK), or after 30 seconds without input. Both return to the launcher on their own, so your game needs no exit code.

The file so far

Everything from this chapter in one file, the game as it stands after chapter 1. If yours misbehaves, compare it with this one. It is docs/vs2/tutorial/steps/step1_first_game.py in the repository.

# Trench Run, chapter 1: a ship you can steer.
import vs2
from vs2.controls import *


class Game(vs2.Scene):
    def build(self):
        self.world = self.layer("world", projection=vs2.TUNNEL)
        self.ship = self.world.sprite("ship.png", y=0)
        self.ship.x = -(self.ship.width // 2)     # centred on x = 0, the bottom of the disc

    def update(self):
        if joy1.held(LEFT):
            self.ship.x += 1        # at the bottom of the disc, x counts up toward the left
        if joy1.held(RIGHT):
            self.ship.x -= 1


def main():
    return Game()

This ship is where Trench Run starts. Over the next chapters it gets a tunnel to fly down, enemies to dodge, a score and a game-over screen. Next: the circular display, and why x behaves differently from y.