VAR
byte i
PUB Main
lcd.Start(gfx.Start)
ctrl.Start
repeat
RunGame
PUB RunGame
gfx.Clear
repeat i from 0 to 7
gfx.Sprite (@centerline_gfx, 64 - gfx.Width (@centerline_gfx) / 2, i*8, 0)
gfx.Sprite (@ball_gfx, 64 - gfx.Width (@ball_gfx) / 2, 32 - gfx.Height (@ball_gfx) / 2, 0)
Pong, Restructured
In the SNAKE tutorial, we did the entire game inside of one main function. That’s fine for short games, but it becomes more and more problematic the longer your game gets.
This time around, we’re going to organize our game in a way that will be much more intuitive. By splitting our one big function into lots of smaller ones, it’ll be easier to organize our thoughts and understand the logic of our game as we expand it.
Which is… AWESOME!
Instructions
If a Pong Ball’s in a Forest…
Let’s take a moment to think about the defining essence of our game.
This is a pong game, so what does a pong game need? In the last section, we made a nice list to answer that question.
-
A table
-
A ball
-
Two paddles
-
A score board
Now, that’s what a pong game has in it, but what about gameplay? What happens in the game?
-
There’s a field; it just sits there
-
Either player can move a paddle up or down
-
A ball bounces around the screen
-
If it hits a paddle, it bounces back the other way
-
If it goes the past paddle, the other player scores a point, and the field resets
-
First one to 9 points wins, and the game starts over
-
The opponent in our game will be a computer
-
Everything needs to be drawn to the screen
That’s everything that happens in our game, so that’s how we should organize it.
A pattern you will commonly see when programming is a setup / run cycle. That is:
-
A setup phase where all the key variable and starting conditions for the program are set.
-
A run phase where the program continues to operate based on your parameters.
It’s kind of like playing with dominoes. You set up all the dominoes, then run away when they all start falling down (lol).
We had setup and run code in SNAKE, but we did it haphazardly just when we needed it. Now we’re going to do it for real.
Running the Game
Let’s move what we have into a running loop. This allows us to restart the game at will, and redraw the screen outside of the game loop, which is useful for things like animations, events, even title screens.
Splitting code into functions also empowers you, as you can control larger and larger behaviors with the same amount of effort.
We’ll throw a gfx.Clear
in there too because we’ll need one as soon as things start moving.
Better, but still not great. Let’s move the drawing into its own function too.
repeat
RunGame
PUB RunGame
gfx.Clear
repeat i from 0 to 7
gfx.Sprite (@centerline_gfx, 64 - gfx.Width (@centerline_gfx) / 2, i*8, 0)
gfx.Sprite (@ball_gfx, 64 - gfx.Width (@ball_gfx) / 2, 32 - gfx.Height (@ball_gfx) / 2, 0)
gfx.Sprite (@paddle_gfx, 4, 32 - gfx.Height (@paddle_gfx) / 2, 0)
gfx.Sprite (@paddle_gfx, 124 - gfx.Width (@paddle_gfx), 32 - gfx.Height (@paddle_gfx) / 2, 0)
DrawGraphics
lcd.Draw
PUB DrawGraphics
repeat i from 0 to 7
gfx.Sprite (@centerline_gfx, 64 - gfx.Width (@centerline_gfx) / 2, i*8, 0)
gfx.Sprite (@ball_gfx, 64 - gfx.Width (@ball_gfx) / 2, 32 - gfx.Height (@ball_gfx) / 2, 0)
gfx.Sprite (@paddle_gfx, 4, 32 - gfx.Height (@paddle_gfx) / 2, 0)
gfx.Sprite (@paddle_gfx, 124 - gfx.Width (@paddle_gfx), 32 - gfx.Height (@paddle_gfx) / 2, 0)
DAT
ball_gfx
word 0
word 8, 8
"BUT WHY?", you may be asking. It doesn’t make the program faster. it doesn’t add a feature, but it does make the program easier to read.
That, my friends, is called refactoring, and it doesn’t look like much, but it could mean the difference between being able to write a 1,000 line program and a 1,000,000 line program.
Setting Up the Game
We need to restart all the moving parts we’re going to have in pong, but currently there are none. Let’s tie the paddles and ball to some variables, so that we’ll be able to interact with them.
There are two players, one for each paddle, so for simplicity, let’s control them separately.
ctrl : "LameControl"
VAR
byte i
byte ballx
byte bally
byte playerx
byte playery
byte opponentx
byte opponenty
PUB Main
lcd.Start(gfx.Start)
ctrl.Start
repeat
We’re going to create a new function that runs before the game loop that sets the starting locations for all of our sprites.
PUB Main
lcd.Start(gfx.Start)
ctrl.Start
SetupGame
repeat
RunGame
PUB SetupGame
PUB RunGame
gfx.Clear
DrawGraphics
We already found all the starting values in the previous section. We just need to move them into the new function.
This is good too because all those gfx.Width
/gfx.Height
calls were getting messy. Now we only call them when we need them.
repeat
RunGame
PUB SetupGame
ballx := 64 - gfx.Width (@ball_gfx) / 2
bally := 32 - gfx.Height (@ball_gfx) / 2
playerx := 4
playery := 32 - gfx.Height (@paddle_gfx) / 2
opponentx := 124 - gfx.Width (@paddle_gfx)
opponenty := 32 - gfx.Height (@paddle_gfx) / 2
PUB RunGame
gfx.Clear
Then we change all of our gfx.Sprite
commands (except @centerline
) to use the new variables. The center line doesn’t move so it doesn’t need a variable.
lcd.Draw
PUB DrawGraphics
repeat i from 0 to 7
gfx.Sprite (@centerline_gfx, 63, i*8, 0)
gfx.Sprite (@ball_gfx, ballx, bally, 0)
gfx.Sprite (@paddle_gfx, playerx, playery, 0)
gfx.Sprite (@paddle_gfx, opponentx, opponenty, 0)
DAT
ball_gfx
word 0
Well, that wasn’t so bad! Organizing your code takes a little extra work, and it’s hard to see the results of it immediately, but it’s totally worth it. It’ll be so much easier to understand in the long run, as the program gets more complicated.
The Code
CON
_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000
OBJ
lcd : "LameLCD"
gfx : "LameGFX"
ctrl : "LameControl"
VAR
byte i
byte ballx
byte bally
byte playerx
byte playery
byte opponentx
byte opponenty
PUB Main
lcd.Start(gfx.Start)
ctrl.Start
SetupGame
repeat
RunGame
PUB SetupGame
ballx := 64 - gfx.Width (@ball_gfx) / 2
bally := 32 - gfx.Height (@ball_gfx) / 2
playerx := 4
playery := 32 - gfx.Height (@paddle_gfx) / 2
opponentx := 124 - gfx.Width (@paddle_gfx)
opponenty := 32 - gfx.Height (@paddle_gfx) / 2
PUB RunGame
gfx.Clear
DrawGraphics
lcd.Draw
PUB DrawGraphics
repeat i from 0 to 7
gfx.Sprite (@centerline_gfx, 63, i*8, 0)
gfx.Sprite (@ball_gfx, ballx, bally, 0)
gfx.Sprite (@paddle_gfx, playerx, playery, 0)
gfx.Sprite (@paddle_gfx, opponentx, opponenty, 0)
DAT
ball_gfx
word 0
word 8, 8
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
paddle_gfx
word 0
word 8, 16
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
word %%11111111
centerline_gfx
word 0
word 2, 4
word %%33
word %%33
word %%33
word %%33
View this example at /tutorials/HelloPong/PongRestructured.spin
.
Total Recap
In this section, you learned:
-
How to refactor a large function into smaller functions
-
How and why it’s important to separate startup logic from run logic
-
How to organize a project for growth
Think about this!
-
What will happen if you don’t call
SetupGame
before running the game? -
What’s the advantage of making
DrawGraphics
a separate function? -
Inside
RunGame
, we create a function calledDrawGraphics
that, well, drew graphics. Can you guess the names of some other functions we will create as we add more to the game? What do you think makes a good function name?