[Home] [Downloads] [Search] [Help/forum]

Gammon Forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  Electronics
. -> [Folder]  Microprocessors
. . -> [Subject]  Toorum's Quest II - Retro video game and console
Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?

Toorum's Quest II - Retro video game and console

Postings by administrators only.

[Refresh] Refresh page


Posted by Nick Gammon   Australia  (21,405 posts)  [Biography] bio   Forum Administrator
Date Sat 11 Oct 2014 10:41 PM (UTC)

Amended on Wed 29 Oct 2014 12:00 AM (UTC) by Nick Gammon

Message
This post is a "mirror" of parts of a post made by Petri Häkkinen on the Arduino forum, in case the original post ever gets lost due to a forum upgrade or something.

All credit for the design, and code, goes to Petri Häkkinen.

His blog

Original post on Arduino forum

This post will also mention my own construction notes and suggestions as I made up a copy of the project.

Credits:

Hardware & software by Petri Häkkinen
Graphics tiles by Antti Tiihonen
Titlescreen by Juho Salila

Demo


YouTube video showing game



Image shows title screen, game screen, his circuit board, prototype board.



Circuit board showing hand-drawn circuit.

Features



  • Based on ATmega328P running at 16 Mhz (same as Arduino Uno).

  • The game has a display resolution of 104x80 with 256 colors.

  • Video mode is tile based and supports up to 3 sprites per scan line.

  • Sprites are multiplexed so there can be unlimited number of sprites vertically on the screen.

  • 4 audio channels with triangle, pulse, sawtooth and noise waveforms.

  • Chiptune music playroutine and sound effects.

  • NES controller support.



Hardware notes


(From http://petenpaja.blogspot.fi/)


The design is based on ATmega328P microcontroller (MCU) which has only 2 kilobytes of RAM and 32 kilobytes of program memory. The MCU is clocked at 16 Mhz, which makes the specs exactly the same as in Arduino Uno (intentional choice btw., because the initial prototype was built on Arduino Uno). Everything, the NTSC video signal generation, sound synthesis, music playroutine and game logic is running on the MCU, so many things had to be hand-optimized in assembler language. Only a single additional IC, the AD725 is needed in addition to the ATmega328P.

The most interesting part of the hardware is video signal generation. Here's the basic idea, inspired by Uzebox (which uses a more powerful MCU running at almost doubled clock rate btw.). The MCU outputs 8-bit colors in R3G3B2 format every sixth clock cycle and the bits are turned into analog voltages using resistor DAC. The R,G,B analog signals are fed to AD725, which is RGB to NTSC/PAL encoder. The AD725 outputs composite video signal. The AD725 requires a 14.31818Mhz clock signal for NTSC color modulation, so I have a DIP14 packaged crystal oscillator on board for this. The hardware design of the video stage is mostly based on the reference design in the AD725 datasheet. The AD725 is a surface mount part so I bought a SOIC28-DIP adapter for it and modded it to a SIOC16-DIP adapter to take less space the PCB.

The image quality is actually very good, the best we can get with composite video I would say. The picture is rock solid, only slight jittering can be seen between highly saturated colors problem inherent with composite video. If you going to build the console on breadboard expect lower visual quality -- only by building this on a PCB can you get rock solid picture. The breadboard version is actually not that bad, but after seeing the quality of PCB version you can't go back :)


Schematic


(From http://petenpaja.blogspot.fi/)

Modifications in red by Nick Gammon (provision for PAL).



Full-size schematic: http://gammon.com.au/images/Arduino/tq2_schematic_v11_revised.png


For PAL TV standard you ground pin 1 of the AD725 chip, and use a different frequency oscillator. I have noted the wire colours of the NES controller I used, they won't necessarily be the same on yours.

The AD725 pins 9 and 11 are not connected to anything.

Parts list


(From http://petenpaja.blogspot.fi/)


Apart from standard value resistors and capacitors you need the following parts:


  • ATmega328P (easily found anywhere)
  • 16Mhz crystal
  • AD725 (I ordered 5 from China)
  • 14.31818Mhz crystal oscillator in DIP8 or DIP14 package (RS components has the DIP14 version)
  • SOIC28-DIP adapter for AD725 (I got it from Sparkfun)
  • The 10uF filtering caps on the power supply lines should be tantalum (recommened by AD725 datasheet)
  • 3.18k, 1.58k and 806 resistors (1% tolerance) for DAC
  • NES controller
  • NES controller socket (these can be bought online, e.g. from www.parallax.com)


You can also build this on a breadboard and connect it to Arduino Uno board. The compiled code fits into program memory with the standard Uno bootloader. It's much easier and faster to build the console like this but of course the end result won't be as pretty.


Tiled graphics mode with sprites


(From http://petenpaja.blogspot.fi/)


Video generation is written in AVR assembler. I don't know if anybody else has written a tile based color graphics mode with sprites on an 16Mhz Arduino compatible setup before. The MCU has only 2 kilobytes of RAM, so it's not enough to hold a frame buffer. The game uses a display resolution of 104x80 so with 8-bit colors 8320 bytes would be needed for the frame buffer alone, clearly out of our reach. So, first I had to do a tiled graphics mode.

The game screen is made of 13x10 tiles, each 8x8 pixels. A tile, therefore, consumes 64 bytes of program memory. I have a tile buffer of 13x10 pointers that point into tile graphics in program memory. On each scanline, I fetch the tile pointer from RAM, and pull 8 pixels from the tile and output the pixels exactly every 6 cycles. With pixel width of 6 cycles and with doubled scanlines the pixels are approximately square on screen. Pulling a pixel from program memory takes 3 cycles and outputting a pixel takes 1 cycle, so there are only 2 cycles remaining to fetch the tile addresses. With careful ordering of instructions and unrolling the loop it can be done. Overall, it was fairly easy to get the basic tiling setup working.

However, things started to get much more complicated because I also wanted to have sprites on top of the tiles. The ATmega328P running at 16Mhz is not fast enough to do the tiles and mask sprites on tiles during the time period of a scanline. It took me a while to figure out how do the sprites. Then it hit me. Because I have doubled the scanlines, I have actually two scanlines of time to process a single row of 104 pixels. In order to pull this off I had to use double buffering, so that while I was computing the next row of pixels, I was pulling in the previously computed scanline and still outputting pixels every 6th cycle. So on even scanlines, I do as many tiles as possible (which turned out to be 9 tiles), write the pixels to a scanline buffer WHILE reading the pixels of the previous scanline and outputting them to screen. On odd scanlines, I do the remaining 4 tiles, write the resulting pixels to memory, mask sprites on top of the tiles, and again while pulling pixels from previously computed scanline and outputting them to screen. A buffer holds a single row of 104 pixels. After two scanlines the buffers are swapped. Doing everything while outputting a pixel every 6 cycles meant that every cycle had to be counted.

There is actually three seperate "threads" running in the code and the threads are manually interleaved. This was very painful to code but eventually I managed to do it. The result is a video mode where I can have 3 sprites on each scanline. The game has actually more sprites because I can reuse the hardware sprites vertically on the screen by using multiplexing. I have a buffer in RAM which stores the sprite locations and image pointers for three sprites on each scanline. Multiplexing the sprites is as simple as writing the sprite data to the buffer in the correct place.


Multichannel music and sound effects


(From http://petenpaja.blogspot.fi/)


I also wanted to have multichannel music in the spirit of C64's SID and Rob Hubbard (the best chiptune musician ever, just listen to the music of Commando, International Karate or Monty on the Run if you don't believe me). Unfortunately there is not enough time left on the scanlines to do any sound synthesis. So sound had to be generated in the vertical blank period when the MCU is not busy doing the tiles and sprites. There are max 263 scanlines on a NTSC screen, so I fill a buffer of 263 bytes of 8-bit audio samples during the vblank. The video generation reads the samples and sends them out of the chip using pulse width modulation (PWM). Since we are constantly sending out samples while generating new samples, the sound needs to be double buffered. Otherwise clips and pops can be heard.

The audio system supports 4 channels, with triangle, pulse with varying pulse width, sawtooth and noise waveforms. Volume is controlled using ADSR envelopes. Oscillators and mixing is coded in assembler. The music playroutine is pretty much a standard four channel tracker with support for pulse width animation, volume slides, arpeggios, vibrato and portamento effects. Music data is compressed in memory so that each track row uses only 1 byte. The catchy tune was composed by Antti Tiihonen aka jpeeba using a custom textmode tracker I wrote just for this project.


Other tidbits


(From http://petenpaja.blogspot.fi/)


Rooms are stored compressed in program memory using a simple RLE compression. I had very limited RAM left because all the sound buffers, scanline buffers, tile pointers and sprite buffers use up almost every byte of available RAM. I could not have the game state of every room simultaneously in memory. So when the player moves to a new rooms I store only collected hearts, gold and opened doors as compressed bitfields in RAM. This way each inactive room consumes only 1 byte of memory as long as there are only 8 things per room to be stored.

Some of the tiles are animated on the screen: gold pieces, hearts and the princess are technically background tiles. I only swap their tile pointers every few frames. To make this really fast I scan only a single row of tiles per frame, so that the whole screen is updated every 10 frames.

There are actually three different video modes in the game: the main game mode with tiles and sprites (13x10 tiles), untiled titlescreen mode with 128x80 resolution and intro text mode with 14x10 tiles with no sprites. I did not need sprites for the titlescreen and there was space left in program memory so I could afford a slightly bigger resolution for the titlescreen. I couldn't fit the intro text beautifully into only 13x10 tiles, so I had to do a custom graphics mode with one more tile horizontally for the intro ;-)

In the end, there are only a few bytes of RAM and about 200 bytes of program memory left. I know by optimizing and with better compression techniques (and removing one of two of the extra video modes) I could fit even more into memory but luckily the game does not really need more stuff.


Source code


Source code on GitHub

If that goes down a copy made in October 2014 is here: http://gammon.com.au/Arduino/toorumquest2.zip

Nick Gammon's construction notes


I got it to work using PAL by just making the changes shown on the circuit. Somewhat strangely, the code did not seem to need adjustment.

My own assembled version:



Output from it:



Dungeon layout


I've managed to deduce a bit of the dungeon layout.

First, the room directions:


const PROGMEM prog_uchar roomadj[] = {

//  Left  Right   Up  Down      Room 
      0,   1,    0,    4,    //   0   
      0,   2, 0xFF,    5,    //   1   
      1,   3,    0,    6,    //   2   
      2,   0,    0,    7,    //   3   
      0,   5,    0,    9,    //   4   
      4,   6,    1,   10,    //   5   
      5,   7,    2,   11,    //   6   
      6,   8,    3,   12,    //   7   
      7,   0,    0,   13,    //   8   
      0,  10,    4,    0,    //   9   
      9,  11,    5,    0,    //  10   
     10,  12,    6,    0,    //  11   
     11,  13,    7,    0,    //  12   
     12,  14,    8,    0,    //  13   
     13,   0,    0,    0,    //  14   
  };

/*
Room layout:

   0  1  2  3
   4  5  6  7  8
   9 10 11 12 13 14
   
*/


Next the room encoding. Let's use this to make it easier:



const PROGMEM prog_uchar roomNibbleToByte[] = {
  TILE_WALL_DARK,
  TILE_EMPTY,
  TILE_WALL,
  TILE_KEY,
  TILE_LADDER,
  TILE_GOLD,
  TILE_PRINCESS,
  TILE_DOOR,
  TILE_WYVERN,
  TILE_WYVERN_2ND,
  TILE_SPIKES,
  TILE_GHOST_RIGHT,
  TILE_GHOST_LEFT,
  TILE_GHOST_LEFT_2ND,
  TILE_HEART,
};


So for example:


const PROGMEM prog_uchar rooms[] = {
	0x10,0xc1,0x10


That is:


1 x Dark tile
12 x empty tiles
1 x Dark tile


Decoding the room layout gives this:


Room 0

|
|
|
|
| - - - - - - - - - - - -
|   K |           |
| = * |           |
| = - |   P       #
| = | | - - - - - - - - -




Room 1


                W
          - - -
        - | *
- - - - | | - - - - - - -
          | |
          |     W w
      =   |   *
- - - = - | - - - - - - -




Room 2





- - - - -   - - - -   - -
      | |   | | |       |
                |   -   |
        ! ! !           |
- - - - - - - - - - = - |
                        |



Room 3

                        |
                        |
                        |
                G   *   |
- - - - - - - - - - - - |
| | | | | | | | | | | | |
| | | | | | | | | | | | |
| | | | | | | | | | | | |
| | | | | | | | | | | | |
| = | | | | | | | | | | |



Room 4

| = | | | | | | | | | | |
| =
| =
| =     g           w
| =   - - - w   -
| =     |             - -
| =                   | |
| = g     ! ! ! !   = | |
| - - - - - - - - - = | |
| | | = | | | | | | | | |



Room 5

| | | = | | | | | | | | |
    | = |   | | | = | | |
    # = |         g   K |
    - - | w - = - - - - |
              = | *
-           - - | -
|   -             |   -
| z @   - G       | -
| - - - | - = - - | - - -
| | | | | | | | | | = | |



Room 6

| | | | | | | | | | = | |
|     * |           = | |
|     - | W         = | |
| -           - - - - | |
  | - - - -       | K   |
  |       #       | - = |
          - - -       = |
  -   G       = - - - - |
- | - - - - - = | | | | |
| | | | | | | | | | | | |



Room 7

| | | | | | | | | | | | |
|
| W   *
|     -                 -
| -           -       - |
|       G             | |
|   - - - -       - - | |
|   # = | K   !   * | | |
| - - = | - - - - - | | |
| | | | | | | | | | | | |



Room 8

| | | | | | | | | | | | |
              |         |
              #       - |
-   - - W - - - - - = | |
|                   = | |
|         - = -     =   |
|       * | = | -   =   |
| ! ! ! - | = | | g = K |
| - - - | | = | | - - - |
| | | | | | | | | | = | |



Room 9

| | | | | | | | | | = | |
|                 | = #
|                   - -
|           -
|     W       -         -
| K     -   * |       - |
| - -   | ! - |   -   | |
| | | ! | - | | ! | ! | |
| | | - | | | | - | - | |
| | | | | | = | | | | | |



Room 10

| | | | | | = | | | | | |
          | = * |       |
          | = - |       |
                  W     |
- - -
| G
| - - -           - - - -
| | |     ! ! !   g   | |
| | | - - - - - - - - | |
| | | | | | | = | | | | |



Room 11

| | | | | | | = | | | | |
|       | | | =       K
| *           G       -
| - w - - = - -       | -
  |       =
  #       g     ! !
- -     - - -   - - -
| @           !
| - - - - - - - - - - - -
| | | = | | | | | |



Room 12

| | | = | | | | | |
      = | | | | | | - - -
    - -                 |
- - |                   |

        -           w
      - | W   - -     -
    - | * ! ! ! ! ! !
- - | | - - - - - - - - -
| | | | | | = | | | | | |



Room 13

| | | | | | = | | | | | |
| K | | | | = | | |     #
|   g       =           -
| - - - - - - - - - = - |
      |             = | |
      | * W   w     = | |
      | -   -   -   = | |
                    = | |
- - - - - - - - - - - | |
| | | | | | | | | | | | |



Room 14

| | | | | | | | | | | | |
                  | | | |
-                 | | | |
|     - - W     @ | | | |
|   - | |       - | | | |
|         g   *   | | | |
| - - - - - - - - | | | |
| | | | | | | | | | | | |
| | | | | | | | | | | | |
g g g g g g g | | z z G G


Using the following legend:


WALL_DARK =      |
EMPTY =          
WALL =           _
KEY =            K
LADDER =         =
GOLD =           *
PRINCESS =       P
DOOR =           #
WYVERN =         W
WYVERN_2ND =     w
SPIKES =         !
GHOST_RIGHT =    G
GHOST_LEFT =     g
GHOST_LEFT_2ND = z
HEART =          @


And now put it all together:



I inked in the solid parts as best I could in Photoshop, so the structure of the cave is more obvious.

Doors = green squares (8 of each)
Keys = blue squares (8 of each)
Gold = yellow squares
Enemies = red squares
Hearts (lives) = purple squares

More screenshots






These were taken from my TV, the photos don't really do the screen justice, it looks better than that in real life.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


5,410 views.

Postings by administrators only.

[Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]


Written by Nick Gammon - 5K   profile for Nick Gammon on Stack Exchange, a network of free, community-driven Q&A sites   Marriage equality

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( https://gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Hosted at FutureQuest]