Go Down

Topic: Toorum's Quest II - ATmega328P based retro video game with 256 color graphics (Read 22707 times) previous topic - next topic

nickgammon


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.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

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.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

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. :)
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

ph77

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.

ph77

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...

nickgammon

Quote

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).

Quote

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.

Quote

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.

Quote

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.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

ph77


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, that explains it :)

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.

nickgammon

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.

Code: [Select]

// 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. :)
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

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:

Code: [Select]

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:

Code: [Select]

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.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

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

First, the room directions:

Code: [Select]

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:

Code: [Select]


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:

Code: [Select]

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


That is:

Code: [Select]

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


Decoding the room layout gives this:

Code: [Select]

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:

Code: [Select]

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 =          @
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

And now put it all together:

Code: [Select]


|                                                                                                     |
|                                         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   | | | @           !               - | * ! ! ! ! ! !                         = | | | | | | | | | | | | | | |
| | | - | | | | - | - | | | | | - - - - - - - - | | | - - - - - - - - - - - - - - | | - - - - - - - - - - - - - - - - - - - - | | | | | | | | | | | | | | |
| | | | | | = | | | | | | | | | | | | | = | | | | | | | | = | | | | | |       | | | | | | = | | | | | | | | | | | | | | | | | | |
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

nickgammon

More screenshots:





It looks better than that in real life. The camera tends to make the screen look washed out.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

ph77

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

Quote

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.

Quote
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! :) 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!

nickgammon

Quote

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.

Quote

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.
Please post technical questions on the forum, not by personal message. Thanks!

More info: http://www.gammon.com.au/electronics

JO3RI

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 http://logicalzero.com/gamby/ and the Video shield (http://www.wayneandlayne.com/projects/video-game-shield/) and are still struggling with the sound engine.

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

Nederlandstalige sectie - http://arduino.cc/forum/index.php/board,77.0.html -

Go Up