SCHMETRIS

schmetris 0.3 screenshot
Author:Jakob Schmid
Commenced:2014-06-30
Language:C++
Frameworks:SDL 2.0
Tools:Visual Studio, Clang, Vim, GrafX2

Note: this game is a clone of Tetris (1984) by Alexey Pajitnov.

Download

Background

The Tetris Game That Never Was

Acorn Electron

Acorn Electron. I didn't have the fancy Acorn Data Recorder, I had a generic cheap tape recorder, which worked just fine.

BASIC

Freshly rebooted Acorn Electron, ready for some BASIC.

Exile

Exile (1988)

In the late 80s - early 90s, before I got an Amiga, I had borrowed an Acorn Electron, a budget home computer from 1983, with a 1MHz 6502A and 32K RAM. There were plenty of games released for the machine, including some really great ports of 80s arcade games, not to mention the technical marvels of 'Elite', the seminal space sim by David Braben and Ian Bell, and the free-form action adventure 'Exile' with physics simulation by Peter Irvin and Jeremy Smith, both originally written for the Acorn. However, I didn't have any of those. I had two games: 'Arcadians', a great unlicensed clone of Galaxian, and 'Planetoid', an excellent Defender clone. I played Planetoid obsessively, and eventually ruined my Return key from shooting green aliens really fast.

Somewhere at my parent's house, there is a tape with a lot of bad BASIC games that I wrote in this period. My main motivation at the time was that I needed more games, forcing me to make them myself. The Electron came with the great BBC BASIC, and having programmed COMAL 80 for years, I quickly picked it up. I didn't really have any resources or people to ask, I figured most of the technical details out for myself. If the tape hasn't deteriorated, it should still contain, among others, a Pac-Man clone with horribly unfair AI, and a pretty cool Space Invaders clone.

Graphics was done the classic way by graphing paper and binary numbers. A slow process, but when your thing appeared on the screen, you felt pretty clever.

The Electron only had 1 sound channel, producing a beep of a given frequency and fixed amplitude. This primitive interface didn't stop me from composing a title theme for my Space Invaders clone by entering frequencies directly into the program. Now, saving was a horribly slow process of rewinding and recording the program to tape, a process that could take minutes! So when my Mom decided to test the main power switch, I had finished the title theme and not saved once.

...

I'm sure the second version I wrote was even better than the first one.

During this time, I went on a hiking trip to Norway with my dad. To avoid boredom, I started developing a Tetris game in my mind and wrote BASIC routines down on paper whenever we reached a cabin. When the trip was finished, I had a complete Tetris program.

When I got home, I typed the program into the machine, but alas - the whole design failed with this error [1]:

Too many FORs

The Electron had a fixed limit of nested FOR loops, which I had somehow broken, probably from iterating over tiles on the board and tiles in the tetrominoes. This would have forced me to rethink the design and make the program from scratch, but at that point I had lost interest, moving on to making other games instead.

The Tetris Game That Is

FM synth

Sound effect synth implementation (early version)

Now, 20 or so years later, I spent a couple of days of my summer vacation to make my first actual Tetris game, for real this time.

I used C++ and the SDL 2.0 framework. I took me a day to get the basic game up and running, and a couple of days more to make something reasonably playable.

The graphics was inspired by the classic Tetris for the NES, and the game runs in 320x256 (scaled x3 by SDL).

Sound effects and a music replay routine was written from scratch:

  • A monophonic fixed-algorithm FM synth [2] handles all the sound effects.
  • Two simple FM synth channels plays a music sequence, tempo and timbre being controlled from the game state.

I made the classic GameBoy Tetris theme 'Korobeiniki' by typing in note numbers from my memory of the theme (' _ ' means pause):

7,  _,  2,   3,  5,  _,   3,   2,
0,  _,  0,   3,  7,  _,   5,   3,
2,  _,  2,   3,  5,  _,   7,   _,
3,  _,  0,   _,  0,  _,   _,   _,
_,  5,  _,   8, 12,  _,  10,   8,
7,  _,  3,   5,  7,  _,   5,   3,
2,  _,  2,   3,  5,  _,   7,   _,
3,  _,  0,   _,  0,  _,   _,   _,

which is then converted to frequencies:

frequency = base_frequency * 2^(note_number / 12);

The sound effect synth has three saw oscillators (for that brutal arcade sound) and implements this FM algorithm:

MODULATOR 1 -> MODULATOR 2 -> CARRIER

In version 0.6, I added drums, using enveloped sine generators for bass drum, a poor sounding 808-style cowbell, and the pitched part of the snare, and enveloped noise for hihats and snare. I got the noise algorithm from Blargg's emulation of the SNES SPC 700, as used in ZSNES and bsnes [3].

The game is playable now, complete with gradually increasing difficulty and high score saving. My teenage self would have been proud.


[1]The manual for the BBC Micro, including the AcornSoft BASIC, can be found here.
[2]Wikipedia: FM Synthesis.
[3]Blargg's Audio Libraries.

Revision History

Schmetris 0.6

Changelog:
  • Pause mode
  • Added drums!

Schmetris 0.5

Changelog:
  • Now with vsync'ed refresh
  • Reset auto move period on manual movement and new piece
  • Readjusted key repeat and fixed repeat reset

Schmetris 0.4

Changelog:
  • Added score saving
  • Locking audio thread while changing state prevents audio glitches
  • Adjusted and fixed key repeat
  • Added enter key as drop / select
  • Added version info to title screen
  • Added SFX to title screen menu
  • Adjusted music speed

Architecture Notes

It has been fun to apply certain design patterns and technical ideas that has been on my mind lately on a real project.

Below are a few notes about the architecture of the game.

Warning: technical mumbo-jumbo!

State-Controller-Rendering Pattern

The top-level game structure is derived from the MVC pattern:

                 Owner
                  < >
                   |
     .-------------+-------------.
     |             |             |
Controller ----> State <---- Rendering
           write       read

In this case, there are two state classes (game and input), with matching controllers, and two renderers, graphics and audio:

                     Owner
                      < >
                       |
.-------------------------------------------.

                 Input Ctrler
                      |
                      | write
                      v
         .--------> Input <-----------.
        |   read   [State]   read     |
        |                             |
        |                             |
Game Ctrler ------> Game  <------- Renderer,
            write   State    read   Audio

Game loop:

while ()
{
    input_controller.update(input);
    game_state.swap(); // swap double buffer
    game_controller.update_state(game_state, input);

    audio.update(game_state, input);
    renderer.render_screen(game_state);
}

Double-Buffered State

Double-buffering state enables analysis of changes between previous and current state:

class Game_state
{
    struct State
    {
        int x = 4;
    };
    State states[2];
    State *current, *previous;

    Game_state() : current(&states[0]), previous(&states[1]) {}
    void swap()
    {
        if( current == &states[0] )
        {
            states[1] = states[0]; // copy previous state
            current  = &states[1];
            previous = &states[0];
        }
        else
        {
            // same as the above with 0 and 1 swapped
        }
    }
};

input_controller.update(input);

// Update state
game_state.swap(); // prepare new state
game_controller.update_state(game_state, input);

renderer.render_screen(game_state);

Rendering can use derivations of state variables:

int dx = game_state.current->x - game_state.previous->x;
if(dx != 0)
    play_sound();

C++11 Notes

This was my first game using the new 2011 version of C++. C++11 is a bit easier to use than previous versions, and it has been fun to check out a few of the new features.

initializer_list

Initializing a sequence contained within a class is easy with an initializer_list:

class Player
{
    std::vector<int> seq;
    Player(std::initializer_list<int> seq) : seq(seq) {}
};

Player theme( { 0, 1, 2, 3 } );

Move construction

Returning a class or passing it to a function by value results in copying the data. If a class contains large amounts of data, this copying becomes a performance problem. Using a move constructor avoids copying by moving the data from one instance to another:

class Piece
{
    int *data;

    Piece(int size)
     : data(new int[size]) {} // allocate

    // Move ctor
    Piece(Piece&& other)
    {
        data = other.data; // move data to this instance
        other.data = NULL; // other shouldn't free the memory on destruction
    }

    Piece(Piece& other) = delete;           // don't use copy ctor
    void operator =(Piece& other) = delete; // don't use assignment

    ~Piece()
    {
        if(data) // don't delete when data moved to other instance
            delete[] data;
    }
};

std::vector<Piece> pieces;
pieces.push_back(Piece(3)); // Move construction avoids copying

State Pattern Implementation

Behaviour can be modified by setting a state function. The function type syntax is a bit hairy in C++, but works as expected:

class Game_controller
{
    // Declare a new function type 'Mode_func'
    typedef bool (Game_controller::*Mode_func)(Game_state &, Input &);
    Mode_func mode_func;

    void set_mode(Mode_func new_mode)
    {
        mode_func = new_mode;
    }
    bool update_state(Game_state &game_state, Input &input)
    {
        return (this->*mode_func)(game_state, input);
    }

    // Modes:
    bool mode_run       (Game_state &game_state, Input &input) { }
    bool mode_game_over (Game_state &game_state, Input &input) { }
};

game_controller.set_mode(&Game_controller::mode_game_over);
game_controller.update_state(game_state, input);

Multi-Channel SDL Audio Implementation

We start up the audio system:

SDL_AudioSpec want, have;

// ...
want.callback = audio_callback;
want.userdata = this;
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
SDL_PauseAudioDevice(dev, 0); // start audio playing

The callback zeroes the buffer and then renders the different channels:

static void audio_callback(void *userdata, Uint8 * stream, int len)
{
    Audio &a = *((Audio *)userdata);
    float * out = (float *)stream;
    int out_samples = len / sizeof(float);

    memset(stream, 0, len); // clear buffer
    a.fx.render   (out, out_samples);
    a.theme.render(out, out_samples);
    a.bass.render (out, out_samples);
}

Every instrument adds to the buffer instead of overwriting it:

void render(float *buf, int out_samples)
{
    while (s < out_samples)
    {
        ph += freq;
        float o = sinf(ph * 6.28f);
        *(buf++) += o * amp; // left
        *(buf++) += o * amp; // right
    }
}

Building on Mac OS X

Compilation on Mac OS X with clang:

Prerequisites:

libsdl 2.0
clang-3.4
libcxx

Install:

https://www.libsdl.org/download-2.0.php
# sudo port install clang-3.4 libcxx

Normal compile:

clang++ -std=c++11 -stdlib=libc++ -framework SDL2 main.cpp

Making Mac OS X Release Build

The executable created above only works on machines with SDL2 installed. For releases, we'll need to bundle SDL2 with our application.

The bundle is a directory hierarchy like this:

schmetris.app/
schmetris.app/Contents
schmetris.app/Contents/Frameworks
schmetris.app/Contents/Frameworks/SDL2.framework  <- copy from /Library/Frameworks/
schmetris.app/Contents/Info.plist                 <- meta data
schmetris.app/Contents/MacOS
schmetris.app/Contents/MacOS/schmetris            <- executable
schmetris.app/Contents/Resources
schmetris.app/Contents/Resources/tiles.bmp        <- data files
schmetris.app/Contents/Resources/font.bmp

A minimal Info.plist contains:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
      <key>CFBundleName</key>
      <string>Schmetris</string>
      <key>CFBundleDisplayName</key>
      <string>Schmetris</string>
      <key>CFBundleIdentifier</key>
      <string>dk.schmid.schmetris</string>
      <key>CFBundleVersion</key>
      <string>0.2</string>
      <key>CFBundlePackageType</key>
      <string>APPL</string>
      <key>CFBundleSignature</key>
      <string>strs</string>
      <key>CFBundleExecutable</key>
      <string>schmetris</string>
</dict>
</plist>

Make release executable:

clang++ -c                <- compile, don't link
        -std=c++11        <- C++11
        -stdlib=libc++
        -I../../include   <- my includes
        main.cpp

clang++ -rpath @executable_path/../Frameworks   <- relative path from executable
                                                   to SDL2 in bundle
        -framework SDL2
        -lc++
        main.o
        -o schmetris.app/Contents/MacOS/schmetris

Distributing to Windows Users

In order to get compatibility with Windows XP using Visual Studio 2013, I enabled:

Properties:Configuration Properties:General:General:Visual Studio 2013 - Windows XP

My release executable depended on the following files from:

Microsoft Visual Studio 12.0\VC\redist\x86\Microsoft.VC120.CRT:
  msvcp120.dll   <- VS2013 C++ library
  msvcr120.dll   <- VS2013 C library

Also the SDL2.dll depends on:

msvcrt.dll    <- C library (older version, found it in ``Windows/System32``):