LiveCode Pong

Posted by Scott McDonald

Time to start with a game. Which to make? I know, Pong! Before rolling your eyes, it is good to start with something simple. And simple Pong is. The name is an abbreviation of Ping Pong (table tennis). A "ball" (actually a small square of white light) bounces around the screen, while two players control a paddle (a skinny white rectangle) at the left and right ends of the screen. The ball bounces off the top and bottom edges of the screen, but if your paddle is not in position to hit it back when it gets to your end of the screen, then off the edge it goes and your opponent gets a point.

Pretty basic, and a good game to start with on a LiveCode game making journey. In this game you will see:

  • A game loop in action
  • One way to make a ball bounce
  • LiveCode graphics used instead of images
  • "Artificial Intelligence" code

Before examining the game, a few words about a couple of conventions I use. Strict Compilation Mode is on in the LiveCode IDE Preferences so variables are always declared. It may seem time-saving to keep Strict Compilation Mode turned off and not declare any variables, but don't do it. Sure in a small game all goes well but once your games get larger, wasting an hour trying to find out why your program doesn't work is not fun. You then find a typo that Strict Compilation Mode would have highlighted immediately you did a save.

With variables, I follow 3 conventions.

  1. Variable names at the card or stack level begin with a lowercase s followed by an uppercase letter.
  2. Variable names in a handler begin with a lowercase letter.
  3. Variables use CamelCase, where a capital letter is used if the variable name is a compound word.

These conventions are a personal preference and make code more readable to me. Name your variables any way you want, but trust me about Strict Compilation Mode.

With that out of the way, let's look at some code.

Further below is the StartGame handler. (It may be easier to download the stack and have the Main card script open in LiveCode, while reading this page.) This is called to initialize all those variables I have declared and to call GameLoop for the first time.

The first 3 lines remove any messages that are in the message queue. This is normally a good idea when starting a game, because if there are already messages pending that will call your GameLoop (which can easily happen during development and debugging) if you don't first remove them, weird things could happen and you will be left scratching your head wondering what has gone wrong.

So why am I wasting time declaring variables at the top of the card, and then filling them with values that are not going to change during the running of the game? Three reasons:

  1. Putting the width of the graphic for the ball into the variable sBallWidth saves time later, typing sBallWidth is much easier than the width of graphic "ball".
  2. Putting, for example, a value into sPlayerPaddleX based on the card size, means that if the size of the card is changed I do not have to change any of the code.
  3. When your game runs it takes LiveCode much longer to process the width of graphic "ball" than getting the value stored in sBallWidth.

For turn based games, or Pong where not much processing is required, the extra time taken in reason (3) is unimportant, but reasons (1) and (2) still apply. A little investment at the start of your coding means less typing and more flexibility later on. When your games get more complex and require smooth animation, the faster processing from (3) will be an asset. Why not start with good habits that will repay you ten-fold in the future?

command StartGame
  repeat for each line loopLine in the pendingmessages
    cancel item 1 of loopLine
  end repeat
  
  put the width of graphic "ball" into sBallWidth
  put the height of graphic "ball" into sBallHeight
  
  put the width of this card into sCardWidth
  put the height of this card into sCardHeight
  
  put the width of graphic "playerPaddle" into sPaddleWidth
  put the height of graphic "playerPaddle" into sPaddleHeight
  
  put sCardWidth-sPaddleWidth into sPlayerPaddleX
  put sPaddleWidth into sCPUPaddleX
  put sCardHeight div 2 into sCPUPaddleY
  set the loc of graphic "cpuPaddle" to sCPUPaddleX,sCPUPaddleY
  
  put kSpeedX into sSpeedX
  put kSpeedY into sSpeedY
  put sCardWidth div 2 into sBallX
  put sCardHeight div 2 into sBallY
  set the loc of graphic "ball" to sBallX,sBallY
  
  put 0 into field "cpuScore"
  put 0 into field "playerScore"
  hide field "GameOver"
  
  put 1 into sCPUState
  put kCPUSpeed into sCPUSpeed
  
  put true into sBallInPlay
  put false into sGameOver
  -- start motion
  GameLoop
end StartGame

After setting up the variables, at the end of StartGame call GameLoop. Games with animation (that happen even when the player is doing nothing), nearly always have a "game loop". These are the actions that are continually repeated to check for events and update the display. Below is the game loop for Pong. It does a set of actions and unless the game is over, it calls itself in 2 ticks. Since there are 60 ticks each second, sending the GameLoop message in 2 ticks results in a game that runs at 30 frames per second.

So what does GameLoop do? It moves the ball, does the AI (artificial intelligence, the overblown term for the code that moves the CPU paddle), checks for a collision with the card edges and paddles, and finally checks whether the ball has left the card.

command GameLoop
  MoveBall
  ProcessAI
  CheckCollisions
  CheckMissedBall
  if sGameOver then
   GameOver
  else
    -- run game at 30 fps
    send "GameLoop" to me in 2 ticks
  end if
end GameLoop

MoveBall adds the speed of the ball in horizontal and vertical directions to the current position. Then the loc (location) of the ball is set to the new position.

In LiveCode a graphic is a visual object that LiveCode draws. This is different from a bitmap image that you load into your game after creating it in a separate image editor. For a simple game like Pong you can draw directly on the card the required graphics in the LiveCode IDE before you start coding. This can save time in the early stages of development, and in the case of Pong is more than adequate for the finished game.

command MoveBall add sSpeedX to sBallX
  add sSpeedY to sBallY
  set the loc of graphic "ball" to sBallX,sBallY
end MoveBall

After moving the ball the paddle on the left is moved according to the AI. If you are thinking this isn't artificial intelligence, fair enough, but ProcessAI is a useful label for the handler that controls the CPU player.

This handler uses what is called a state machine. A state machine is good for algorithms that have a number of different "states" or conditions that each do a different action, and normally depend on the previous state. A state machine can be represented as a variable at the card level to keep track of the state, and a switch statement.

Here the CPU player has four possible states:

  1. Getting ready to move randomly
  2. Moving randomly while waiting for you to hit the ball
  3. Getting ready to move towards the incoming ball
  4. Moving towards the ball.

If you look carefully at this code you may wonder how the state changes from 2 to 3 and from 4 back to 1. The answer is that sCPUState is also set outside of this handler. In CheckCollisions examined later, sCPUState is changed after the ball hits a paddle.

Note, state machines are not limited to AI applications and games. If you have code with lots of if then statements, where this depends on that, which affects this, and you find yourself having trouble understanding the logic, then consider converting your code to a state machine.

The random numbers put into the sCPUCounter, sCPUWobble1 and sCPUWobble2 variables give the CPU paddle some "personality." Even though you (the human player) know you are playing against a stupid computer, it adds to the fun if the computer acts more human. The wobbling in the paddle movement help you pretend (even if only subconsciously) that you are playing against a machine who, just maybe, is thinking.

command ProcessAI
  local paddleRect
  switch sCPUState
    case 1 -- prepare to move randomly
      put random(60) into sCPUCounter
      put -sCPUSpeed div 2 into sCPUSpeed
      put 2 into sCPUState
      break
    case 2 -- move randomly
      subtract 1 from sCPUCounter
      if sCPUCounter > 0 then
        add sCPUSpeed to sCPUPaddleY
        set the loc of graphic "cpuPaddle" to sCPUPaddleX,sCPUPaddleY
        if sCPUPaddleY < sPaddleHeight then put abs(sCPUSpeed) into sCPUSpeed
        if sCPUPaddleY > sCardHeight - sPaddleHeight then
          put -abs(sCPUSpeed) into sCPUSpeed
        end if
      else
        put -sCPUSpeed into sCPUSpeed
        put random(60) into sCPUCounter
      end if
      break
    case 3 -- prepare to follow ball
      put 4 into sCPUState
      put random(sPaddleHeight div 2) into sCPUWobble1
      put random(sPaddleHeight div 2) into sCPUWobble2
      break
    case 4 -- following ball
      put the rectangle of graphic "cpuPaddle" into paddleRect
      -- only move the paddle if not inline with ball
      if (sBallY < item 2 of paddleRect + sCPUWobble1) or (sBallY > item 4 of paddleRect - sCPUWobble2) then
        if sBallY < item 2 of paddleRect + sCPUWobble1 then put -kCPUSpeed into sCPUSpeed
        if sBallY > item 4 of paddleRect - sCPUWobble2 then put kCPUSpeed into sCPUSpeed
        add sCPUSpeed to sCPUPaddleY
        set the loc of graphic "cpuPaddle" to sCPUPaddleX,sCPUPaddleY
      end if
      break
  end switch
end ProcessAI

Next in the GameLoop is a call to CheckCollisions to check when the ball hits the top or bottom of the card or a paddle. This is the longest handler in Pong. It could be shorter, but the original Pong makes the gameplay more interesting by changing the angle of bounce depending on where the paddle is hit.

Many of the versions of Pong you find on the internet use a simple bounce algorithm where the ball bounces off the paddle at the supplementary angle of the impact. This is easy to code, but results in a ball that bounces around the screen in a less interesting way, often at angles that are multiples of 45 degrees. Boring.

Instead, here the the paddle is divided into 9 regions using this line:

put round(9 * ((sBallY - item 2 of paddleRect) / sPaddleHeight)) into paddleRegion

Then the vertical speed of the ball is varied depending on the value of paddleRegion. Since the horizontal speed of the ball never changes, increasing or decreasing the vertical speed has the effect of changing the angle of bounce. This has a side affect that at the steeper angles the ball moves faster in the direction it is heading. This adds to the illusion that you have just pulled off a tricky shot and given the ball an extra hard hit.

Right at the bottom of CheckCollisions a simple check is done to make the ball bounce off the top and bottom edges of the card. In CheckCollisions the code used for the two paddle collisions is almost identical. If I was wanting to reduce the amount of lines in Pong, this common code be put into a separate handler and called twice. This time laziness got the better, and I decided it is good enough. Even though it could have been made more elegant.

command CheckCollisions
  local buffer,paddleY,paddleRect,paddleRegion
  -- hit player paddle?
  if (sBallX > (sPlayerPaddleX - (sBallWidth + sPaddleWidth) div 2)) and sBallInPlay then
    put the rectangle of graphic "playerPaddle" into paddleRect
    if (sBallY > item 2 of paddleRect - sBallHeight div 2) and (sBallY < item 4 of paddleRect + sBallHeight div 2) then
      put 3 into sCPUState
      put -sSpeedX into sSpeedX
      -- needed in case ball is "inside" the the paddle during collision
      put sPlayerPaddleX - ((sBallWidth + sPaddleWidth) div 2) into sBallX
      -- divide paddle into 9 regions from 0 to 8
      put round(9 * ((sBallY - item 2 of paddleRect) / sPaddleHeight)) into paddleRegion
      if paddleRegion < 4 then
        put -kSpeedY - 0.2 * (3 - paddleRegion) into sSpeedY
      else
        if paddleRegion > 4 then
          put kSpeedY + 0.2 * paddleRegion into sSpeedY
        else
          put 0 into sSpeedY
        end if
      end if
    else
      put false into sBallInPlay
    end if
  end if
  -- hit CPU paddle?
  if (sBallX < (sCPUPaddleX + (sBallWidth + sPaddleWidth) div 2)) and sBallInPlay then
    put the rectangle of graphic "cpuPaddle" into paddleRect
    if (sBallY > item 2 of paddleRect - sBallHeight div 2) and (sBallY < item 4 of paddleRect + sBallHeight div 2) then
      put 1 into sCPUState
      put -sSpeedX into sSpeedX
      -- needed in case ball is "inside" the the paddle during collision
      put sCPUPaddleX + ((sBallWidth + sPaddleWidth) div 2) into sBallX
      -- divide paddle into 9 regions from 0 to 8
      put round(9 * ((sBallY - item 2 of paddleRect) / sPaddleHeight)) into paddleRegion
      if paddleRegion < 4 then
        put -kSpeedY - 0.2 * (3 - paddleRegion) into sSpeedY
      else
        if paddleRegion > 4 then
          put kSpeedY + 0.2 * paddleRegion into sSpeedY
        else
          put 0 into sSpeedY
        end if
      end if
    else
      put false into sBallInPlay
    end if
  end if
  -- hit top or bottom wall
  if (sBallY < sBallHeight div 2) or (sBallY > sCardHeight - sBallHeight div 2) then put -sSpeedY into sSpeedY
end CheckCollisions

So the ball has been moved, the CPU has moved the left paddle, and we have checked whether the ball has hit anything. What if a player misses the ball? Then CheckMissedBall is next in the game loop.

Here a couple of checks are made. If the ball has gone off the left or right edge of the card, update the appropriate score. If a player reaches 15, the game is over.

If the game is not over, a ball is "served" towards the player who lost the point. An interesting line here is:

put any item of "-1,1" * random(kSpeedY) into sSpeedY

The "any item" phrase is one of those powerful LiveCode features that can substitute for a few lines of code in other languages. The any keyword means randomly return one of the items in the list. Similar sort of code can be used for randomly picking a line or character too. There are other ways to set the vertical speed, but this code makes it clear that the direction will be either positive or negative.

command CheckMissedBall
  local ballMissed
  if sBallX < -sBallWidth then
    put true into ballMissed
    add 1 to field "playerScore"
    if field "playerScore" = 15 then put true into sGameOver
    -- serve ball towards CPU
    put -kSpeedX into sSpeedX
    put 3 into sCPUState
  end if
  if sBallX > sCardWidth + sBallWidth then
    put true into ballMissed
    add 1 to field "cpuScore"
    if field "cpuScore" = 15 then put true into sGameOver
    -- serve ball towards player
    put kSpeedX into sSpeedX
    put 1 into sCPUState
  end if
  if ballMissed and not sGameOver then
    wait 2 seconds
    put true into sBallInPlay
    put any item of "-1,1" * random(kSpeedY) into sSpeedY
    put sCardWidth div 2 into sBallX
    put sCardHeight div 2 into sBallY
    set the loc of graphic "ball" to sBallX,sBallY
  end if
end CheckMissedBall

If it is game over the GameOver card is shown after a short pause. Because the scores are stored in the fields that show the information, there is a little trick used to access the scores. Note the line:

if field "playerScore" of me > field "cpuScore" of me then

includes the of me phrase. This is needed because after:

go to card "GameOver"

the score fields are no longer visible and cannot be accessed without some extra information. The of me means, "refer to the object that is on the object that contains this script." Since all this code is the script for the card that contains the fields:

field "playerScore" of me

works in the same way as:

field "playerScore" of card "Main"

but is simpler and if you change the name of the card, you do not need to update your code. How convenient is that?

command GameOver
  show field "GameOver"
  wait 2 seconds
  go to card "GameOver"
  if field "playerScore" of me > field "cpuScore" of me then
    put "You Win. Do you want to play again to let me have another go?" into field "GameOverMessage"
  else
    put "I Win. Do you want to play again to try and beat me?" into field "GameOverMessage"
  end if
end GameOver

Near the end now. There are two more handlers. To remind me that these handlers are for messages that LiveCode sends they begin with a lowercase letter. The moveMouse handler updates the position of the player paddle. The mouseMove message is sent regularly by LiveCode as you move the mouse, so if you were wondering why GameLoop has no code for moving the player paddle, this is the reason. It happens in this handler asynchronously from the game loop.

command openCard
  StartGame
end openCard

on mouseMove pMouseH,pMouseV
  set the loc of graphic "playerPaddle" to sPlayerPaddleX,pMouseV
end mouseMove

That's about it. So without working hard to keep the code small, a fully working Pong re-creation in under 200 (not counting comments) lines of LiveCode.

Download the complete LiveCode Pong stack made in LiveCode 6.1.2 from here:LiveCode Pong.livecode.

Happy LiveCoding.

Tagged: arcade beginner retro

Friday, November 15, 2013

0 Responses

Be the first to make a comment.


COMMENTS ARE CLOSED

 

Legal Stuff

All blog posts are copyright and cannot be re-used without permission. But all the code and scripts are dedicated to the public domain. Use such code and scripts in any way you want, but I am not responsible if they don't work for you.

Contact

Further comments or feedback? You can contact me by email at: