Saturday, June 16, 2018

Tutorial: Pong

Reminder: My repo URL is https://github.com/Chysn/O_C_Tutorial1

Like twenty years ago, I owned a Kurzweil K2000. It had what we call today--but didn't call back then--an Easter Egg. It was a Pong game that you could play from the panel, and it generated MIDI notes when the ball bounced off a wall. I thought about this game this week, and decided that it would be a nice project to demonstrate a lot of things about Ornaments and Crime development.

Before I describe the game, I want to do a couple of shouts out. First, to my first two actual followers. You guys are the best! Second, to Patrick Dowling, one of the main O_C developers, who has been really responsive and thorough, and told me things about what's going on inside O_C that I couldn't have found by scouring the code. I'm kind of in a place where I feel like I can do just about anything I can think of, largely thanks to Patrick's generosity.

The Game


It's the Pong we all know and love, with a few twists. As a ball bounces its way across the screen, the player defends the left side of the screen with a "paddle," and the O_C defends the right side. It's an unfair game, though, because the O_C can't lose. As you return the ball and level up, the ball gets faster, and your paddle gets smaller and closer to your opponent. The odds are not in your favor!

Controls are as follows:

Up/Down Buttons: Move the paddle up and down. This is really to illustrate the use of the buttons' event handler, and you really don't want to play the game with these things.

Encoders: Both encoders move the paddle up and down.

CV Input 1: Negative values move the paddle up, and positive values move the paddle down. There's a "center detent," a small range that doesn't move the paddle at all. This is to compensate for noise that gets into the ADC.

Output A: When the ball bounces off your paddle, a short 5V trigger is sent to Output A.

Output B: When the ball bounces off anything else, a short 5V trigger is sent to Output B.

Output C: Sends 0 to 4-ish volts, based on the Y position of the ball. 0V is the top of the screen.

Output D: Sends 0 to 4-ish volts, based on the Y position of the player paddle. 0V is the top of the screen.

What I Learned to Do


By studying the APP_PONGGAME.ino code and comments, as well as this post, the following skills can be learned:
  • How to organize code: delegating graphics to the Menu function and CV processing to either the ISR or Loop function;
  • Use of what Patrick Dowling calls the "quick and dirty" Graphics library;
  • Use of button and encoder events;
  • Use of the Init function for setting initial states.
Please pull a code update from GitHub (see this link if you need to catch up here) and look at the APP_PONGGAME.ino file. I'm going to assume by now that you know how this app was added to the system, and we're going to be looking at only this file.

I think the code itself is pretty well-documented, so I'm not going to repeat that information here, for the most part. Instead I want to focus on the organization of the code.

PONGGAME_menu()


menu() is called as part of the main loop in o_c_REV.ino. The screen is redrawn every time menu() is called; nothing persists between calls. According to Dowling, the Graphics calls write to a memory buffer, which is later transferred to the screen.

The screen shares a SPI port with the DAC, and the access is interleaved. The Graphics functions are only valid in the menu() and the screensaver() scopes, and O_C will crash if you try to use the drawing tools anywhere else.

In Pong, I'm drawing the frame and header in PONGGAME_menu(), and then drawing the pieces with class methods.

Note that I'm using menu() only for displaying stuff. Anything that does CV processing or calculation is done elsewhere. Of course, you can arrange your menu() methods however you see fit; but I like to strictly divide presentation from other business logic and I/O.

After a number of seconds specified in the calibration menu, O_C switches from calling menu() to calling screensaver(). This is to prevent burn-in of the OLED display. In this case, since this is a visual application, I don't really want a screensaver to happen, and I don't want the display to go dark. So I'm just calling menu() from within screensaver() like this:

void PONGGAME_screensaver() {
    PONGGAME_menu();
}


I wouldn't recommend this approach for actual music-related apps. My preference is for the screen to go blank, but you can create your own screensavers in the same way that you create your own menus.

PONGGAME_loop()


This might be where I go off the rails a bit. The loop() function was, according to Dowling, a vestige of older versions of the O_C extended firmware. If you look at the included apps, none of them use loop(), but instead use the ISR for handling all I/O and business logic.

A major difference between loop() and the ISR is that the loop runs once for each main loop iteration, while the ISR, which is based on a timer, runs every 60 microseconds. For the purposes of Pong, using loop() is plenty fast. So here, loop() is the controller counterpart to menu(). It calculates (but does not display) the ball movement, and it handles the CV state with I/O functions.

ISR


In Pong, the ISR is used for timing. We don't want things to move with every invocation of menu() or loop(), or the game wouldn't be playable. So there are a few class properties that act as countdown values before something happens (or is allowed to happen).

For example, when the ball hits the player's paddle to return the ball, 5V trigger is sent from Out A. The following code determines if the ball is being returned, and sets return_countdown:

// All these conditions are just asking "Did the ball hit the paddle while traveling left?"
if ((ball_x <= paddle_x + PADDLE_WIDTH) && (ball_x >= paddle_x)
  && (ball_y <= paddle_y + paddle_h) && (ball_y >= paddle_y) && dir_x < 0) {    
    // If so, bounce the ball, increase the score, and set the hit trigger to fire the reward
    // CV trigger at the next loop() call.
    dir_x = -dir_x;
    score++;
    return_countdown = TRIGGER_CYCLE_LENGTH;

    // Level up!!
    if (!(score % 5)) LevelUp();

}

The next time loop() is executed, it checks for a nonzero value of return_countdown, and sets the Out A voltage accordingly with a ternary operator:

// Ball return trigger (when the ball hits the player paddle)
uint32_t out_A = return_countdown >= 0 ? 5 : 0;
...
OC::DAC::set_pitch(DAC_CHANNEL_A, 1, out_A);

It's the ISR() method that counts down these countdown values, at the rate of -1 every 60 microseconds.

TL;DR Overview

So the basic structure here is
  • PONGGAME_loop() determines how the ball moves and updates CV states using Pong::MoveBall() and Pong::UpdateCVState()
  • PONGGAME_menu() draws the game board and pieces using game-specific draw methods in the Pong class
  • PONGGAME_screensaver() spits in the face of safety and does the same thing as PONGGAME_menu()
  • PONGGAME_isr() calls Pong::ISR(), which decrements countdown timers for the ball movement, the player paddle, and two trigger states
  • PONGGAME_handleButtonEvent() and _handleEncoderEvent() respond to onboard control events by moving the player paddle up and down   

Graphics


I don't think I need to get too much into the weeds on the graphics library. Your app classes will have access to a global object called graphics, which can be used in any app's menu() or screensaver() function, or class methods or functions called by those.

The available methods are in software/src/drivers/weegfx.h. The methods here are so straightforward that they're essentially self-documenting. I'll certainly talk about graphics in later posts; but if you spend a bit of time looking at weegfx.h and weegfx.cpp, I think you'll get it.

Exercises

  1. Create a patch that can be played by this game
  2. Create a patch that can play this game
I might circle back to this app to talk about more things later, as I realize that I need to clarify more concepts. For now, install it and have some fun with it.

No comments:

Post a Comment

Pitch Calculation and Output

"If pitches were horses, we'd all be eatin' steak." --Jayne Cobb And where have I been for almost two months? I was busy...