Tuesday 14 June 2016

The Torment of Alfred McSilvernuts: The "Game"

We now know how to control outputs and read input signals. Together with timers and interrupts that's all we need to build a simple controller and program something to control with it. No, let's top it up a notch and make a whole console! Let's start off with the connection diagram:

Circuit diagram of my very first "console".
Isn't it glorious? Powered by a single ATtiny85, it has a two button "controller" for one player and a mind-blowing three pixel monochrome LED "display". Yeah, those quotation marks are there for a reason.


So, my plan is to use the PB2..0 pins as output ports to light up some red LEDs and the PB3 and PB4 pins to read the button state. Clear as the fifth beer of the evening after passing through the system, no? Only noteworthy thing is that I have connected the buttons straight to the ground as I'm making my job easier by utilizing the internal pull-up resistors of the chip.

The circuit assembled on two breadboards.

Our "console" needs also a "game". With three LEDs and two buttons one doesn't make a new Megaman but how about this:  

A successful business man, Alfred McSilvernuts, woke up to evil laughter.

"Mwahaha, thy time was short but the list of thy sins is out of this world. However, as a nice guy that I am, I give thee one last chance to repent."

"NEVER,", yelled McSilvernuts "for my heart is black as my ass is wide! Those poor bastards deserve all the shit I managed to sell them and all their hard-earned money belongs rightfully to my bottomless bank account!"

"Very well. For now on, thou shall spend thy eternity trapped in this one-dimensional doom. No matter which direction thou travel, no matter how far and for how long, thou shall find thee circling around, always ending up in this very place thou now stand on."

Then there was a loud, low roar, like the heaven itself had emptied its bowels after a near-fatal combination of hard liquor and questionable fast food. The roar faded, ending up in complete silence and emptiness. Except for Alfred McSilvernuts and his prison.

Shitty backstory but so will be the "game" itself; the player moves Alfred (i.e. a lit LED) around his prison (a row of LEDs). There's no goal, no way to lose, no way to win. The quotation marks have found their place. However, "The Torment of Alfred McSilvernuts" is an important step as it will contain all the core principles (except for sound) that I'll be using in the future games and devices as well.

Let's start off with the topic of last post: timers and interrupts. To make games run smoothly, I want run them at a constant frame rate. To do this I'll use the Timer/Counter0 Overflow interrupt to tell the processor how often it should execute the frame worth of game logic, update the screen, read the controller input, and so on.

First, we'll add our preamble to the source code:
 #define F_CPU 1000000UL  
 #define L_BTN 0b10000  
 #define R_BTN 0b01000  
   
 #include <avr/interrupt.h>  
 #include <avr/io.h>  

In addition to the lines present in the last example, I defined two constants corresponding to the pins PB4 and PB3 to which the buttons are connected. Not necessary but makes the code more readable.
 volatile uint8_t player_position = 0;  
 volatile uint8_t old_state = 0;  

Here I initialise two variables. One holding the position of the player character and the other holding the state of the pins during the previous frame.
 void init_timer(){  
   
   //Init counter  
   TCNT0 = 0;  
     
   //set the clock, prescaler 64  
   TCCR0B |= (1 << CS01) | (1 << CS00);  
   
   //enable overflow interrupt  
   TIMSK |= 1 << TOIE0;  
   
 }  

For readability, I put the initialisation code of the Timer/Counter0 to a separate funtion. With 1 MHz clock signal and prescaler of 64, I'll get for the overflow frequency 1MHz/256/64 = 61.035...Hz, which is quite nice a frame rate. And now for the interesting part:
 ISR(TIM0_OVF_vect){  
   //Read the pin states  
   uint8_t pin_state = PINB;  
   
   //Move left if L_BTN down and the button state has changed  
   if (L_BTN & ~pin_state & old_state)  
     if (player_position == 0)  
       player_position = 2;  
     else  
       player_position--;  
   
   //Move left if R_BTN down and the button state has changed  
   if (R_BTN & ~pin_state & old_state)  
     if (player_position == 2)  
       player_position = 0;  
     else  
       player_position++;  
   
   //replace the old pin state with the current one  
   old_state = pin_state;  
     
   //Clear "screen"  
   PORTB &= ~0b00111;  
   
   //Put "player" on the "screen"  
   PORTB |= (1 << player_position);  
   
 }  

The Interrupt Service Routine for the Timer0 Overflow is the heart of the program. Called every frame, it contains all the game logic and input/output code. In the beginning, we define a new local variable pin_state and assign the current state of the pins to it. The pins are accessed through PINB register and it contains the states of both the input and output pins.

Next comes the game logic; we check whether a button is pressed and move the player character accordingly. When a button is pressed, it connects the corresponding input pin to the ground or LOW signal. When not, the pin is set HIGH (why? keep reading..). To get the state of the left button, we take bitwise AND (&) between L_BTN and pin_state. This is true ONLY if the bit in pin_state corresponding to one defined in L_BTN is HIGH. But wait! That's not what we want. We want to know whether it is LOW. This is done simply taking the bitwise inverse (LOWs to HIGH and vice versa) of pin_state with the NOT (~) operator.

In addition, we take the bitwise AND with the old_state variable. This ensures that the expression is true only when the old_state of the pin was HIGH (i.e. the button was not pressed down) during the previous frame. Now the if clause reacts only when the button is pressed down and doesn't care whether the player holds it down or not.

If the following conditions apply, then the program moves the player. Since my high-tech display has three LEDs, the player has three possible positions marked with numbers 0, 1, and 2. When the left button is pressed, the program tries to move the player character left by subtracting one from the position. If the player is far left (position is 0), it jumps over the edge to the far right.

The same procedure is repeated for the right button and after that the old_state is replaced with the current one to be passed to the next frame. Then the display is cleared by setting all the output pins to zero. The NOT operator flips 0b00111, where I marked the output pins with ones, to 0b11000. Now when I take the bitwise AND with PORTB, it leaves the first two bits as they are and sets the last three to LOW.

The last step is to place the player on screen. This is done by shifting 1 or 0b001 the number of bits indicated by player_position and taking bitwise OR with PORTB to set the output and after the frame is completed and the program moves to wait for the next interrupt.

And here's the result! Bon Appétit!




No comments:

Post a Comment