Toorum's Quest II - ATmega328P based retro video game with 256 color graphics

PetriH:
Uzebox is an impressive feat indeed

Yep. I hadn't seen those before. They look ideal for a project I'm thinking about (I need 240x224 graphics for it) but they're really really expensive. $74 for a kit? Ouch!

I can get two Raspberry Pis +change for that (or even an Arduino Uno+Gameduino).

New version with music and sound effects!

I also wrote a technical writeup and released the source code:

I'll happily answer any questions about the design or source code.

I know that at least Nick was interested to see the source code, so I'm bumping this one time in case the update was unnoticed.

A VGA line is 256pixels long using an 8MHz pixelclock.
That is 32characters minus a few for sync and porch.

24 or 25 char per line should be possible.
It's not "a heck of alot more" but reaching Arcade Graphics level.

I just saw this on Hackaday. Well done.

PetriH:
I know that at least Nick was interested to see the source code, so I'm bumping this one time in case the update was unnoticed.

I just got my AD725 chip in the mail yesterday so I'll try to get your demo working. Thanks for all the info.

OK, I got that to work more-or-less. Proof:

The screen is slightly noisy because I had a long cable run to the TV and also assembled it on a breadboard. One slight snag, I don't have an NES controller so I can't actually move my character around. I'll order one from eBay, but what sort do I want exactly? Just a classic NES controller?

Errata:

  • On the circuit 4FSC pin number is not marked (it is pin 3).
  • You might mention that AD725 pins 9 and 11 are NC.
  • On the Atmega328 the AUD pin number is not marked (it looks like it is pin 11).

By the way, I didn't have a 14.31818 MHz oscillator (waiting on a delivery) so I used the 17.734475 MHz one which had arrived. I tied pin 1 to Gnd rather than +5V (to indicate PAL) and it works fine.

I haven't tested the sound yet.

It's ultra cool however. I'm really impressed you managed to squeeze this sort of performance out of it. Once I get my controller I'll be able to give it a real test.

Got the sound working, that sounds good. Maybe I can make an NES emulator with a Wii nunchuk. Hmm, I wonder what the NES protocol is? I could have a second Arduino reading the nunchuk and outputting the appropriate serial stream.

Hmm, a bit of Google-fu seems to indicate that the emulator needs to respond to a /LATCH and then respond with 8 bits upon receiving a clock signal. An SPI slave configuration might achieve that quite nicely.

Oh well, that might be tomorrow's project. :slight_smile:

Wow, you got it running already! That was fast! To my knowledge you're the first one.

It's hard to say from the photo but it looks like the colors are somehow wrong, too much red in highlights. Maybe the color signals are wired incorrectly or maybe the resistor values are inaccurate. Or maybe it's just the photo? Also, please change aspect ratio of your TV to 4:3, it looks much better that way.

About the image quality in general. I couldn't get a high quality video signal until I built it on PCB. On PCB the signal is very clean. I guess the breadboard and all the wires cause too much interference.

Yes, the project uses a standard NES controller (not SNES). The NES controller has a 8-bit shift register, so it's a matter of reading the bits one by one. See gamepad.cpp for details.

Thank you for the corrections. I will update the schematic.

Oh, I just realized that you're running the AD725 in PAL mode but the video signal is synced to NTSC. Your TV is probably confused about this and tries to interpret the colors as if it were receiving a NTSC signal? This might be the culprit for the strange colors… or maybe not, I would expect the colors to be totally off...

It's hard to say from the photo but it looks like the colors are somehow wrong, too much red in highlights.

Well, I have cough not put in all the resistors yet. I was testing to make sure it worked. I haven't put in the 3k ones. I note that you only have 2 x 3k resistors, and therefore the blue signal won't be quite the same as the red and green ones (no doubt so you could output everything through PORTD).

Oh, I just realized that you're running the AD725 in PAL mode but the video signal is synced to NTSC.

I was pleasantly surprised that it worked at all. I expected to have to wade through the code and made modifications to allow for the different pixel rate.

I couldn't get a high quality video signal until I built it on PCB.

I must confess that my PCB building skills are almost non-existent. I managed to make once a while back but couldn't drill the holes into it in any sort of straight line.

The NES controller has a 8-bit shift register, so it's a matter of reading the bits one by one. See gamepad.cpp for details.

My controller emulator is not exactly working, I am debugging that right now.

Oh, that explains it :slight_smile:

Blue has only 2 bits so that colors fit into 8 bits -- the human eye is least responsive to blue, so that's why red and green have more bits.

btw. the values of resistors are critical to get linear distribution of color bits. Resistors values are 806, 1.58K and 3.16K. Each value is approximately x2 the previous one. You can use other values as long as they follow the same pattern: x1, x2, x4.

Right, I made a NES controller emulator using a Wii nunchuk. The protocol was a tiny bit obscure (one latch pulse and seven clock pulses) but I got there. For timing reasons I made a tight loop where it waited for latch, and then clocked out the 8 values. Then in the 1/60th of a second I should have available I read the nunchuk ready for next time.

// NES controller emulator using Wii nunchuk
// Author: Nick Gammon
// Date:   1st December 2013

#include <Nunchuk.h>
#include <Wire.h>

const byte JOYSTICK_LOW_THESHOLD = 100;
const byte JOYSTICK_HIGH_THESHOLD = 200;

void setup ()
{
  // don't have bogus initial data
  pinMode (12, INPUT_PULLUP);
  
  // initialize nunchuk
  Nunchuk::begin ();
  // first read
  Nunchuk::read ();
  
}  // end of setup

byte reply [8]; // A, B, SELECT, START, UP, DOWN, LEFT, RIGHT

void loop ()
{
  reply [0] = Nunchuk::z_button;  // A
  reply [1] = Nunchuk::c_button;  // B
  reply [2] = 0;   // SELECT  ??
  reply [3] = Nunchuk::c_button && Nunchuk::z_button;  // START (both Z and C buttons together)
  reply [4] = Nunchuk::joy_y_axis > JOYSTICK_HIGH_THESHOLD;  // UP
  reply [5] = Nunchuk::joy_y_axis < JOYSTICK_LOW_THESHOLD;   // DOWN
  reply [6] = Nunchuk::joy_x_axis < JOYSTICK_LOW_THESHOLD;   // LEFT
  reply [7] = Nunchuk::joy_x_axis > JOYSTICK_HIGH_THESHOLD;  // RIGHT
  
  noInterrupts ();

  // wait for LATCH (pin 10) to go high
  while ((PINB & bit(2)) == 0)
    { }
    
  PORTB |= bit (4); // start off with HIGH output
  DDRB |= bit (4);  // set MISO (pin 12) to output
  
  // wait for LATCH (pin 10) to go low again
  while (PINB & bit(2))
    { }

  // After LATCH we immediately output the first bit
  
  // set MISO (pin 12) to appropriate value for the "A" button
  if (reply [0])
    PORTB &= ~bit (4);
  else
    PORTB |= bit (4);

  // do 7 more data pulses
  for (byte i = 1; i < 8; i++)
    {
    // wait for CLOCK (pin 13) to go high
    while ((PINB & bit(5)) == 0)
      { }
      
    // set MISO (pin 12) to appropriate value
    if (reply [i])
      PORTB &= ~bit (4);
    else
      PORTB |= bit (4);

    // wait for CLOCK (pin 13) to go low
    while (PINB & bit(5))
      { }
    }  // end of 8 pulses

  PORTB |= bit (4); // end off with HIGH output
  DDRB &= ~bit (4);  // set MISO (pin 12) to input
  
  interrupts ();

  // read for next time around
  Nunchuk::read ();
}  // end of loop

With the assistance of that I was able to play the game. Now I'll take a look at how the code works. :slight_smile:

Once again, I think this is an incredible feat of code optimization to fit all this into an Atmega328P.

Not only do you have 256-colour output, but also sound, an introductory screen, and explanatory text. It runs on both NTSC and PAL, as I proved. It also interfaces with the NES controller, so you can have A, B, SELECT, START, UP, DOWN, LEFT, and RIGHT buttons.

The output is jitter free, I personally have a bit of noise but that would be from the breadboard, and the fact that there is no shielding.

The code just fits:

Binary sketch size: 31,986 bytes (of a 32,256 byte maximum)

If you ran it on a larger processor like the Atmega1280 (for example the Bobuino, or just the bare chip, which is available in DIP format) you would have access to a lot more program memory (128 kB) which would allow for a lot more sprites, rooms, game logic etc.

It would be nice if you could document the room data layout. A lot of us (including me) would find it challenging to modify or improve the code, but no doubt would have fun adding extra rooms or changing the layout.

If this sort of stuff was documented:

const PROGMEM prog_uchar rooms[] = {
	0x10,0xc1,0x10,0xc1,0x10,0xc1,0x10,0xc1,0x10,0xc2,0x10,0x11,0x13,0x10,0x51,0x10,0x31,0x10,0x14,0x15,
	0x10,0x51,0x10,0x31,0x10,0x14,0x12,0x10,0x11,0x16,0x31,0x17,0x31,0x10,0x14,0x20,0x92,0xf1,0x61,0x18,
	0x91,0x32,0x91,0x12,0x10,0x15,0x61,0x42,0x20,0x72,0x51,0x20,0xb1,0x10,0x21,0x18,0x19,0x61,0x14,0x11,
	0x10,0x11,0x15,0x51,0x32,0x14,0x12,0x10,0x72,0xf1,0xf1,0xf1,0x71,0x52,0x11,0x42,0x11,0x22,0x31,0x20,
	0x11,0x30,0x31,0x10,0x81,0x10,0x11,0x12,0x11,0x10,0x41,0x3a,0x51,0x10,0xa2,0x14,0x12,0x10,0xc1,0x10,
...

That would be great! And how did you draw the graphics? I can imagine using Photoshop (or similar) in indexed colour mode, and outputting raw data, to draw tiles and suchlike.

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:

|                                                                                                     |
|                                         W                                                           |
|                                   - - -                                                             |
| Start                           - | *                                                       G   *   |
| - - - - - - - - - - - - - - - - | | - - - - - - - - - - - -   - - - -   - - - - - - - - - - - - - - |
|   K |           |                 | |                   | |   | | |       | | | | | | | | | | | | | |
| = * |           |                 |     W w                       |   -   | | | | | | | | | | | | | |
| = - |   P       #             =   |   *                   ! ! !           | | | | | | | | | | | | | |
| = | | - - - - - - - - - - - - = - | - - - - - - - - - - - - - - - - - = - | | | | | | | | | | | | | |
| = | | | | | | | | | | | | | | = | | | | | | | | | | | | | | | | | | | = | | | | | | | | | | | | | | | | | | | | | | | | | | | |            
| =                           | = |   | | | = | | | |     * |           = | | |                                       |         |
| =                           # = |         g   K | |     - | W         = | | | W   *                                 #       - |
| =     g           w         - - | w - = - - - - | | -           - - - - | | |     -                 - -   - - W - - - - - = | |
| =   - - - w   -                       = | *         | - - - -       | K   | | -           -       - | |                   = | |
| =     |             - - -           - - | -         |       #       | - = | |       G             | | |         - = -     =   |
| =                   | | |   -             |   -             - - -       = | |   - - - -       - - | | |       * | = | -   =   |
| = g     ! ! ! !   = | | | z @   - G       | -       -   G       = - - - - | |   # = | K   !   * | | | | ! ! ! - | = | | g = K |
| - - - - - - - - - = | | | - - - | - = - - | - - - - | - - - - - = | | | | | | - - = | - - - - - | | | | - - - | | = | | - - - |
| | | = | | | | | | | | | | | | | | | | | | | = | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | = | |
| | | | | | | | | | = | | | | | | | | = | | | | | | | | | | | | | = | | | | | | | | = | | | | | |       | | | | | | = | | | | | | | | | | | | | | | | | | |
|                 | = #             | = * |       | |       | | | =       K         = | | | | | | - - - | K | | | | = | | |     #                   | | | |
|                   - -             | = - |       | | *           G       -       - -                 | |   g       =           - -                 | | | |
|           -                               W     | | - w - - = - -       | - - - |                   | | - - - - - - - - - = - | |     - - W     @ | | | |
|     W       -         - - - -                       |       =                                               |             = | | |   - | |       - | | | |
| K     -   * |       - | | G                         #       g     ! !               -           w           | * W   w     = | | |         g   *   | | | |
| - -   | ! - |   -   | | | - - -           - - - - - -     - - -   - - -           - | W   - -     -         | -   -   -   = | | | - - - - - - - - | | | |
| | | ! | - | | ! | ! | | | | |     ! ! !   g   | | | @           !               - | * ! ! ! ! ! !                         = | | | | | | | | | | | | | | |
| | | - | | | | - | - | | | | | - - - - - - - - | | | - - - - - - - - - - - - - - | | - - - - - - - - - - - - - - - - - - - - | | | | | | | | | | | | | | |
| | | | | | = | | | | | | | | | | | | | = | | | | | | | | = | | | | | |       | | | | | | = | | | | | | | | | | | | | | | | | | |

More screenshots:

It looks better than that in real life. The camera tends to make the screen look washed out.

I was away for a few hours and you have already made a NES controller emulator and reverse engineered the room layout! :astonished:

If you ran it on a larger processor like the Atmega1280 (for example the Bobuino, or just the bare chip, which is available in DIP format) you would have access to a lot more program memory (128 kB) which would allow for a lot more sprites, rooms, game logic etc.

Sure. However, the idea was to see what could be done with the ATmega328P / Arduino Uno. It's always possible to throw more hardware at the problem, but it leads to a never ending loop… what comes next when the Atmega1280 is full? I think that limitations are ultimately good and bring out the creativity in us.

Also, it's possible to add more stuff to the game by using better compression schemes and optimizing the code further. For example, the titlescreen could be made to use the tiled graphics mode, so that the custom video mode for the titlescreen could be removed. This would free up almost 1KB. Also the titlescreen image alone takes 10KB (it's an uncompressed 128x80 bitmap). So making it smaller would free up memory to be used for rooms, sounds, whatever.

And how did you draw the graphics?

Graphics were made using Photoshop by my friend Antti. All tiles are contained in a single image. I made a Lua script which reads the file and writes out a C header file. The music was made, also by Antti, with a music tracker software I programmed.

I see you have already reverse engineered the room data. Well done! :slight_smile: If you need more info on the format of the data or anything else, I'm happy to answer.

And thank you for looking into this and building the console!

I made a Lua script which reads the file and writes out a C header file.

Excellent choice. When I was playing with fonts and I needed to reverse rows/columns or do similar stuff I used Lua.

I think that limitations are ultimately good and bring out the creativity in us.

I agree in principle. The concept of solving slow execution by just getting a faster processor (or more RAM) means you aren't actually attempting to find better ways of using existing hardware.

This is really nicly done ! I would love to learn more on the music and sound part. Can we use your music software tracker? Do you have a music player for Arduino?

I ask this because we (TEAM a.r.g.) are creating 8-bit games for the Gamby shield GAMBY: Arduino Retro Gaming Shield and the Video shield (Video Game Shield | Wayne and Layne) and are still struggling with the sound engine.

TEAM a.r.g. http://www.team-arg.org