Creating Pong Wars in LiveCode

Introduction

Pong Wars is a mesmerizing game concept where two balls represent day and night, each painting the canvas with their respective colors as they bounce around. In this tutorial, we'll implement this captivating game in LiveCode, creating a visual battle between light and dark.

The game mechanics are straightforward:

Let's build this game from scratch using LiveCode's powerful event-driven programming environment.

Setting Up the Game Structure

We'll start by defining our game structure and global variables. This setup will establish the foundation for our game, including constants, colors, and the grid system.

-- Global variables
global gCanvasWidth, gCanvasHeight
global gSquareSize, gNumSquaresX, gNumSquaresY
global gDayColor, gNightColor, gDayBallColor, gNightBallColor
global gSquares, gBalls
global gDayScore, gNightScore
global gMinSpeed, gMaxSpeed
global gIteration

-- Set up constants and initial variables
on preOpenStack
   -- Set canvas dimensions
   put 600 into gCanvasWidth
   put 600 into gCanvasHeight
   
   -- Set colors
   put "224,232,227" into gDayColor         -- MysticMint
   put "17,76,90" into gNightColor          -- NocturnalExpedition
   put "17,43,54" into gDayBallColor        -- OceanicNoir
   put "224,232,227" into gNightBallColor   -- MysticMint
   
   -- Set game parameters
   put 25 into gSquareSize
   put 5 into gMinSpeed
   put 10 into gMaxSpeed
   
   -- Calculate grid dimensions
   put gCanvasWidth div gSquareSize into gNumSquaresX
   put gCanvasHeight div gSquareSize into gNumSquaresY
   
   -- Initialize scores
   put 0 into gDayScore
   put 0 into gNightScore
   
   -- Initialize iteration counter
   put 0 into gIteration
end preOpenStack
LiveCode Tip: Prefixing variables with "g" for global, "t" for local, and "p" for parameters is a best practice in LiveCode development that improves code readability and maintenance.

Initializing the Game Board and Balls

Next, we'll set up our game board with a grid of squares and initialize the two balls - one for day and one for night. This is a continuation of our preOpenStack handler.

-- Continue the preOpenStack handler
   
   -- Set up the squares array (one half day, one half night)
   repeat with i = 1 to gNumSquaresX
      repeat with j = 1 to gNumSquaresY
         if i < (gNumSquaresX / 2) then
            put gDayColor into gSquares[i,j]
         else
            put gNightColor into gSquares[i,j]
         end if
      end repeat
   end repeat
   
   -- Set up the balls
   -- Ball 1 (Day ball)
   put gCanvasWidth / 4 into tX1
   put gCanvasHeight / 2 into tY1
   put 8 into tDx1
   put -8 into tDy1
   
   -- Ball 2 (Night ball)
   put (gCanvasWidth / 4) * 3 into tX2
   put gCanvasHeight / 2 into tY2
   put -8 into tDx2
   put 8 into tDy2
   
   -- Create balls array
   put empty into gBalls
   -- Ball 1
   put tX1 into gBalls[1]["x"]
   put tY1 into gBalls[1]["y"]
   put tDx1 into gBalls[1]["dx"]
   put tDy1 into gBalls[1]["dy"]
   put gDayColor into gBalls[1]["reverseColor"]
   put gDayBallColor into gBalls[1]["ballColor"]
   
   -- Ball 2
   put tX2 into gBalls[2]["x"]
   put tY2 into gBalls[2]["y"]
   put tDx2 into gBalls[2]["dx"]
   put tDy2 into gBalls[2]["dy"]
   put gNightColor into gBalls[2]["reverseColor"]
   put gNightBallColor into gBalls[2]["ballColor"]
   
   -- Start the game loop
   send "gameLoop" to me in 10 milliseconds
end preOpenStack
Note: In LiveCode, we use a multi-dimensional array to represent our grid of squares, and we use array notation with brackets to access array elements. The balls are set up as arrays with properties for position, velocity, color, and the color they'll create when they hit a square.

Creating the Game Loop

The heart of any game is its game loop. In LiveCode, we implement this using message passing to create a recursive loop that updates and renders the game state.

-- Main game loop
on gameLoop
   updateGame
   drawGame
   
   add 1 to gIteration
   if gIteration mod 1000 = 0 then
      put "Iteration:" && gIteration
   end if
   
   send "gameLoop" to me in 10 milliseconds
end gameLoop

-- Update game state
on updateGame
   -- Reset scores
   put 0 into gDayScore
   put 0 into gNightScore
   
   -- Update scores based on squares
   repeat with i = 1 to gNumSquaresX
      repeat with j = 1 to gNumSquaresY
         if gSquares[i,j] = gDayColor then
            add 1 to gDayScore
         else if gSquares[i,j] = gNightColor then
            add 1 to gNightScore
         end if
      end repeat
   end repeat
   
   -- Update ball positions and check for collisions
   repeat with tBallIndex = 1 to 2
      checkSquareCollision tBallIndex
      checkBoundaryCollision tBallIndex
      
      -- Update ball position
      add gBalls[tBallIndex]["dx"] to gBalls[tBallIndex]["x"]
      add gBalls[tBallIndex]["dy"] to gBalls[tBallIndex]["y"]
      
      -- Add randomness to ball movement
      addRandomness tBallIndex
   end repeat
   
   -- Update score display
   put "Day" && gDayScore && "| Night" && gNightScore into field "scoreField"
end updateGame
LiveCode Tip: LiveCode's message passing system is an elegant way to implement a game loop. By sending a message to the stack itself with a slight delay, we create a continuous loop that runs at approximately 100 frames per second.

Rendering the Game

Now, let's implement the drawing functions that will create and update our visual elements on the screen.

-- Draw the game
on drawGame
   -- Clear the canvas
   set the rect of graphic "canvas" to 0,0,gCanvasWidth,gCanvasHeight
   
   -- Draw squares
   drawSquares
   
   -- Draw balls
   repeat with tBallIndex = 1 to 2
      drawBall tBallIndex
   end repeat
end drawGame

-- Draw the grid of squares
on drawSquares
   lock screen
   repeat with i = 1 to gNumSquaresX
      repeat with j = 1 to gNumSquaresY
         -- Calculate position
         put (i - 1) * gSquareSize into tX
         put (j - 1) * gSquareSize into tY
         
         -- Create or update square graphic
         put "square_" & i & "_" & j into tSquareName
         if there is a graphic tSquareName then
            set the rect of graphic tSquareName to tX,tY,tX+gSquareSize,tY+gSquareSize
            set the backgroundColor of graphic tSquareName to gSquares[i,j]
         else
            create graphic tSquareName
            set the style of graphic tSquareName to "rectangle"
            set the rect of graphic tSquareName to tX,tY,tX+gSquareSize,tY+gSquareSize
            set the backgroundColor of graphic tSquareName to gSquares[i,j]
            set the borderWidth of graphic tSquareName to 0
         end if
      end repeat
   end repeat
   unlock screen
end drawSquares

-- Draw a ball
on drawBall pBallIndex
   put "ball_" & pBallIndex into tBallName
   
   -- Get ball properties
   put gBalls[pBallIndex]["x"] into tX
   put gBalls[pBallIndex]["y"] into tY
   put gBalls[pBallIndex]["ballColor"] into tColor
   put gSquareSize / 2 into tRadius
   
   -- Create or update ball graphic
   if there is a graphic tBallName then
      set the loc of graphic tBallName to tX,tY
   else
      create graphic tBallName
      set the style of graphic tBallName to "oval"
      set the backgroundColor of graphic tBallName to tColor
      set the borderWidth of graphic tBallName to 0
   end if
   
   -- Set ball size
   set the width of graphic tBallName to tRadius * 2
   set the height of graphic tBallName to tRadius * 2
   set the loc of graphic tBallName to tX,tY
end drawBall
Note: LiveCode's graphical objects are a powerful feature for game development. We dynamically create and update rectangle graphics for our squares and oval graphics for our balls. The lock screen and unlock screen commands prevent flickering during updates.

Implementing Collision Detection

A key aspect of our game is collision detection - both with squares and boundaries. Let's implement these vital functions.

-- Check if ball collides with squares and change their color
on checkSquareCollision pBallIndex
   put gBalls[pBallIndex]["x"] into tBallX
   put gBalls[pBallIndex]["y"] into tBallY
   put gBalls[pBallIndex]["reverseColor"] into tReverseColor
   put gSquareSize / 2 into tRadius
   
   -- Check multiple points around the ball's circumference
   repeat with tAngle = 0 to 359 step 45
      put tBallX + cos(tAngle * pi / 180) * tRadius into tCheckX
      put tBallY + sin(tAngle * pi / 180) * tRadius into tCheckY
      
      -- Convert to grid coordinates
      put floor(tCheckX / gSquareSize) + 1 into tI
      put floor(tCheckY / gSquareSize) + 1 into tJ
      
      -- Check if within bounds and not already the reverse color
      if tI >= 1 and tI <= gNumSquaresX and tJ >= 1 and tJ <= gNumSquaresY then
         if gSquares[tI,tJ] <> tReverseColor then
            -- Square hit! Update square color
            put tReverseColor into gSquares[tI,tJ]
            
            -- Determine bounce direction based on the angle
            if abs(cos(tAngle * pi / 180)) > abs(sin(tAngle * pi / 180)) then
               put -gBalls[pBallIndex]["dx"] into gBalls[pBallIndex]["dx"]
            else
               put -gBalls[pBallIndex]["dy"] into gBalls[pBallIndex]["dy"]
            end if
            
            -- Exit after handling collision
            exit repeat
         end if
      end if
   end repeat
end checkSquareCollision

-- Check if ball hits canvas boundaries
on checkBoundaryCollision pBallIndex
   put gBalls[pBallIndex]["x"] into tBallX
   put gBalls[pBallIndex]["y"] into tBallY
   put gBalls[pBallIndex]["dx"] into tBallDx
   put gBalls[pBallIndex]["dy"] into tBallDy
   put gSquareSize / 2 into tRadius
   
   -- Check x boundaries
   if (tBallX + tBallDx > gCanvasWidth - tRadius) or (tBallX + tBallDx < tRadius) then
      put -tBallDx into gBalls[pBallIndex]["dx"]
   end if
   
   -- Check y boundaries
   if (tBallY + tBallDy > gCanvasHeight - tRadius) or (tBallY + tBallDy < tRadius) then
      put -tBallDy into gBalls[pBallIndex]["dy"]
   end if
end checkBoundaryCollision
LiveCode Tip: For circular collision detection, checking multiple points around the ball's circumference provides more accurate results than checking just the center point. We use trigonometry to calculate these points.

Adding Randomness and Finalizing the Game

To make our game more interesting, we'll add slight randomness to the ball movement. We'll also create the user interface to complete our game.

-- Add slight randomness to ball movement
on addRandomness pBallIndex
   -- Get current speeds
   put gBalls[pBallIndex]["dx"] into tDx
   put gBalls[pBallIndex]["dy"] into tDy
   
   -- Add small random changes
   add (random(100) / 5000 - 0.01) to tDx
   add (random(100) / 5000 - 0.01) to tDy
   
   -- Limit maximum speed
   if tDx > gMaxSpeed then put gMaxSpeed into tDx
   if tDx < -gMaxSpeed then put -gMaxSpeed into tDx
   if tDy > gMaxSpeed then put gMaxSpeed into tDy
   if tDy < -gMaxSpeed then put -gMaxSpeed into tDy
   
   -- Ensure minimum speed
   if abs(tDx) < gMinSpeed then
      if tDx > 0 then
         put gMinSpeed into tDx
      else
         put -gMinSpeed into tDx
      end if
   end if
   
   if abs(tDy) < gMinSpeed then
      if tDy > 0 then
         put gMinSpeed into tDy
      else
         put -gMinSpeed into tDy
      end if
   end if
   
   -- Update ball speeds
   put tDx into gBalls[pBallIndex]["dx"]
   put tDy into gBalls[pBallIndex]["dy"]
end addRandomness

-- Setup the UI
on createInterface
   -- Create main canvas
   if there is not a graphic "canvas" then
      create graphic "canvas"
      set the style of graphic "canvas" to "rectangle"
      set the rect of graphic "canvas" to 0,0,gCanvasWidth,gCanvasHeight
      set the backgroundColor of graphic "canvas" to "white"
      set the borderWidth of graphic "canvas" to 0
   end if
   
   -- Create score display
   if there is not a field "scoreField" then
      create field "scoreField"
      set the rect of field "scoreField" to 0,gCanvasHeight+10,gCanvasWidth,gCanvasHeight+40
      set the textAlign of field "scoreField" to "center"
      set the textFont of field "scoreField" to "courier"
      set the textSize of field "scoreField" to 14
   end if
   
   -- Create credits
   if there is not a field "creditsField" then
      create field "creditsField"
      set the rect of field "creditsField" to 0,gCanvasHeight+50,gCanvasWidth,gCanvasHeight+80
      set the textAlign of field "creditsField" to "center"
      set the textSize of field "creditsField" to 10
      put "Pong Wars - Day vs Night" into field "creditsField"
   end if
end createInterface

-- Stack initialization
on openStack
   createInterface
end openStack
Note: The randomness adds unpredictability to the game while maintaining balanced gameplay through minimum and maximum speed limits. The interface creation code ensures we have all the UI elements needed for the game.

Conclusion

We've built a complete Pong Wars game in LiveCode from scratch! The game showcases many of LiveCode's strengths:

The result is a visually captivating game where day and night balls compete to claim territory on the game board. The collision physics, the color changes, and the slight randomness in ball movement all combine to create an engaging and somewhat hypnotic experience.

This project demonstrates how LiveCode can be used to create interactive visualizations and games with relatively little code. The readability of LiveCode's syntax and its object-oriented approach make it an excellent choice for projects like this.

Feel free to experiment by changing colors, adjusting speeds, or adding new features to make the game your own!

Possible Enhancements

Here are some ideas for extending the game: