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.
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
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
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
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
lock screen and unlock screen commands prevent flickering during updates.
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
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
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!
Here are some ideas for extending the game: