Wednesday, January 15, 2014

PaddleFox (Game Programming)

One of the simplest games to create is a variation of Pong that is based on the real-life sport of Racquetball. You can play by yourself and don't need a partner. Just slam the ball against a court made of three walls and bat it back and forth. I wanted to be more unique to choose a name for this game, so after some thought and research, I picked PaddleFox.

Why pick a name? Part of this series of posts is to create real games, and right now seems like a good time to pull together a lot of the technologies I've been exploring to make a "real" game and see how to put it in the store. This isn't going to be the greatest game in the marketplace and I certainly wouldn't charge money for it. I might expand on it later. There's an absolutely fabulous game I do love that is on the Android called Juggle by those wizards of Dundee, Scotland, known as Denki. I recommend it and I'd like to try something like it as I build up more skill. (Essentially it is similar to the game I'm building today, but introduces more balls and different sized ones.)

I'll see about submitting this game to the Firefox OS Marketplace and keep you posted. As I improve it over time, I'll let you know about that too. But I also have lots more game techniques I want to explore that aren't about bouncing balls, and can't wait to get going on a space shooter, a platform game, and a lite RPG.

But on to today's game. There's a ball, there's a paddle, and that's it. Hit the ball and it bounces off. Miss the ball and the game is over (but you don't need a quarter to play over again). This game uses SVG, but you can do the same game by using the corresponding bouncing ball and collision techniques I wrote about for CSS and Canvas.

Here is ... PaddleFox!


Tested and run on a Geeksphone Peak, my new pride and joy. I feel guilty not playing with my ZTE Open. I know it is sitting in the corner missing me, but I'm hoping to get back to it when I can safely upgrade to Firefox OS 1.3 so I can start using the App Manager.

Basically this game combines the bouncing ball technology of creating SVG with JavaScript and the SVG DOM with the Collision Detection built into SVG with getBBox with a touch of Touch! SVG is still a largely undiscovered country for game programming (but see a good one in esviji).

So, here's the code:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <title>
      PaddleFox
    </title>
   
    <style>
   
    #myHead
    {
      position:absolute;
      top: 10px;
      left: 10px;
    }
   
    #myPara
   
    {
      position:absolute;
      top: 60px;
      left: 10px;
    }
   
    </style>
    
    <script>

      // --- Global variables ---

      // Width and height of board in pixels
      var boardWidth = 320;
      var boardHeight = 460;
      var boardWidthPixels = boardWidth + "px";
      var boardHeightPixels = boardHeight + "px";
     
      var padMove = 20;  // Amount the paddle moves.
      var score = 0;     // Initial score.

      // URL for W3C definition of SVG elements
      var svgURL = "http://www.w3.org/2000/svg";
          
      // Variables for ball initial position.
      var ballHor = boardWidth / 2 - 20;
      var ballVer = 50;
      var ballHorPixels = ballHor + "px";
      var ballVerPixels = ballVer + "px";
     
      // Amount of vertical and horizontal change.
      // Think of this is a vector. Speed + direction.
      var changeHor = 5;
      var changeVer = 5;
     
        // Covers all versions of requestAnimationFrame.
        window.requestAnimationFrame =
        window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame; 
     
      // Page load event listener.
      window.addEventListener("load",
        runFirst, false);
       
      // Mousedown event listener.
      window.addEventListener(
        "mousedown", whereMouse, false);
                       
      // Runs when page loads.    
      function runFirst() { 

        // Define the game board as an SVG element.
        gameBoard =
          document.createElementNS(svgURL, "svg");
         
        // Width and height of the SVG board element.
        gameBoard.width.baseVal.valueAsString =
          boardWidthPixels;
        gameBoard.height.baseVal.valueAsString =
          boardHeightPixels;
         
        // You must append the board to the body.
        document.getElementById("pageBody").
          appendChild(gameBoard);
       
        // Define the ball as an SVG element.
        paddleBall =
          document.createElementNS(svgURL, "circle");
         
        // Width,  height, radius, and color of ball.
        paddleBall.cx.baseVal.valueAsString =
          ballHorPixels;
        paddleBall.cy.baseVal.valueAsString =
          ballVerPixels;
        paddleBall.r.baseVal.valueAsString =
          "10px";
        paddleBall.style.
          setProperty("fill","fuchsia","");
         
        // Attach the ball to the game board.
        gameBoard.appendChild(paddleBall);
       
        // Define the paddle.
        myPaddle =
          document.createElementNS(svgURL, "rect");
         
        // X, Y, width, height, and color of paddle.
        myPaddle.x.baseVal.valueAsString = "90px";
        myPaddle.y.baseVal.valueAsString = "400px";
        myPaddle.width.baseVal.valueAsString = "130px";
        myPaddle.height.baseVal.valueAsString = "20px";
        myPaddle.style.setProperty("fill", "darkcyan","");
       
        // Attach the paddle to the board.
        gameBoard.appendChild(myPaddle);
       
        // Color the page elements.
        pageBody.style.backgroundColor = "black";
        myHead.style.color = "red";
        myPara.style.color = "white";      
        
            // Start the game loop.
            gameLoop();
      }
       
      // Game loop. Runs as fast as it can.
    function gameLoop(){
     
            // Endless loop with requestAnimationFrame.
            requestAnimationFrame(gameLoop);
      
        // Move the ball.
        ballMove();        
    }
     
    // Move the ball.
    function ballMove() {
     
      // Changes are calculated but do not
      // take effect until next time through loop.
         
      // Calculate new vertical component.
      ballVer = ballVer + changeVer;

      // If top is hit, bounce off.
      if (ballVer + changeVer < 10)
        changeVer = -changeVer;
       
      // If bottom is hit, the game is over.
      if (ballVer + changeVer + 10 > boardHeight) {
        changeVer = -changeVer;
        // You missed!
        console.log("Hit bottom");
       
        // Game over - actions
        // Set the score to zero.
        score = 0;
        myPara.textContent = "Score = " + score;
       
        // Alert the player.
        alert("You missed the ball!  Start again");
       
        // Start over again.
        // Set ball position.
        ballHor = boardWidth / 2 - 20;
        ballVer = 50;
       
        // Set ball vector.
        changeHor = 5;
        changeVer = 5;
       
        // Set paddle position.
        myPaddle.setAttribute("x", "90px");
       
        // Exit here and don't do any more now.
        return;
      }

      // Calculate new horizontal component.
      ballHor = ballHor + changeHor;

      // If left edge hit, bounce off.
      if (ballHor + changeHor < 10)
        changeHor = -changeHor;
       
      // If right edge is hit, bounce off.
      if (ballHor + changeHor + 10 > boardWidth)
        changeHor = -changeHor;
       
      // Does a collision take place with paddle?    
      // Get bounding box of ball.
      bbBall = paddleBall.getBBox();      
      // Get bounding box of paddle.
      bbPaddle = myPaddle.getBBox();        
     
      // Compare bounding boxes.   
      // Is the ball intersecting the paddle?
      // Four conditions must be met.
      if  ((bbBall.x > bbPaddle.x) &&
        (bbBall.x < (bbPaddle.x + 140)) &&
        (bbBall.y > bbPaddle.y) &&
        (bbBall.y < bbPaddle.y + 10)){
          console.log("HIT"); 
         
          // If hitting the paddle, reverse vertical.
          changeVer = -changeVer;
         
          // Make a small spin to create variation.
          spin = Math.floor((Math.random()*5)) - 2;
          // Add spin to horizontal vector.
          changeHor = changeHor + spin; 
         
          // Bounce ball up to untangle from paddle.
          ballVer = ballVer - 20;
         
          // Increase score.
          score = score + 1;
          myPara.textContent = "Score = " + score;
      }         

      // Draw the ball with new coordinates.
      paddleBall.cx.baseVal.valueAsString =
        ballHor + "px";
      paddleBall.cy.baseVal.valueAsString =
        ballVer + "px"; 
    }

    // Handle mousedown event.
    // Tap once each time to move paddle.
    function whereMouse(evt) {
   
      // Get mouse coordinates.
      mouseX = evt.clientX;
      mouseY = evt.clientY;
     
      // Current paddle position.
      oldX = myPaddle.x.baseVal.value;   
     
      // Calculate which side click is on.
      // If tap is on right of center.
      if (mouseX > 160) {
        newX = oldX + padMove;
        // If new x would go off screen.
        if (newX > 210)
          newX = oldX;
      }
      // If tap is on left side of center.
      else {
        newX = oldX - padMove;
        // If new x would go off screen.
        if (newX < 10)
          newX = oldX;
      }
     
      // Move paddle.
      myPaddle.setAttribute("x", newX);     
    }   

  </script>
</head>
   
<body id="pageBody">
<h1 id="myHead">PaddleFox</h1>
  <p id="myPara">Score = 0</p>
</body>

</html>


268 lines of code. That's not too many! Here's how it works.

Shell

I use a standard HTML5 shell. The minimum you need to tell the browser that you're talkin' HTML5 and UTF-8. Here's the layout I use, but all this is up to you. The HTML5 part is not. Even though it is not directly about SVG, my post on CSS Shell is a good starter.

In general, here's how my shell is laid out. You can also call this a template, but I like shells, and Susie sells them down by the seashore!
  1. HTML5 minimum code
  2. CSS
  3. Globals
  4. Event listeners
  5. Page Load event handler
  6. Game Loop
  7. Function blocks
  8. Other event handlers
  9. HTML UI code (if any)
The HTML5 and Globals should be easy enough to figure out. I use globals when I want something to be accessible in any event handler.

CSS

Sometime I use CSS and sometimes I don't. When you are using SVG (which has cousins in CSS on its mother's side), you can avoid CSS, but it's always there in the background, cooking up delicious meals. Here's today's CSS:

    <style>
   
    #myHead
    {
      position:absolute;
      top: 10px;
      left: 10px;
    }
   
    #myPara
   
    {
      position:absolute;
      top: 60px;
      left: 10px;
    }
   
    </style>


I use the CSS here to position the heading and paragraph. If you don't absolutely position these text elements, the SVG pane will be drawn after these, but this will make it go off the bottom of the screen! When doing games, you need to make sure that everything is absolutely positioned.  Just to be different, I colored these text elements in code a little later:

        pageBody.style.backgroundColor = "black";
        myHead.style.color = "red";
        myPara.style.color = "white";      


I did this in the page load. I'm actually more comforable with JavaScript than CSS, but I'm learning CSS fast.

Event Listeners

Games are usually all about events, and you need to listen for them. Here are the listeners:

       window.addEventListener("load",
        runFirst, false);
      
      // Mousedown event listener.
      window.addEventListener(
        "mousedown", whereMouse, false);

The first is listening for the page load event and the second is looking for a mouse down event. On Firefox OS, the mouse down event is what I use for simple touch. Your finger is a mouse! You want just the mouse down, not the click (which is mouse down plus mouse up). Fast and Firefoxy!

Page Load Event Handler

I throw in everything here that should happen before you start running the game. This includes:
  1. SVG definitions for the SVG pane, the ball (a circle), and the paddle (a rect). This is explained in the SVG post about SVG with JavaScript and the SVG DOM with the Collision Detection built into SVG with getBBox
     
  2. Whatever else I want to set up (coloring the text, in this case).
     
  3. A simple call to the game loop. Game loops govern the action cycle in an arcade game and they've been around forever. 
Game Loop

This is a simple game loop. It just loops around and around, governed by requestAnimationFrame. All it does is draw the ball (ballMove).

Function Block

The only function I have is (that isn't an event handler or the game loop) is the one that moves the ball. This function, ballMove, does most of the heavy lifting. It does the following things:
  1. Calculate the new ball position based on the changes you made the last time through the loop. This is a little goofy, but works. The loop is always processing what happened last time through. For example, if you hit a ball, you bounce, but the actual bounce doesn't happen until you come back again. Fortunately the game loop is bringing you back several times a second, depending on the negotiation between requestAnimationFrame and your browser. First the vertical component of your ball's vector (direction of travel and speed) is calculated, later the horizontal component follows.

    ballVer = ballVer + changeVer;
  2. A check is made to see if you hit the top of the screen. If you did, you bounce!

          if (ballVer + changeVer < 10)
            changeVer = -changeVer;

     
  3. Next a similar check is made to see if you hit the bottom of the screen. Oh, oh! You don't want to do that. If you did, the following actions take place: your score is reset to zero, the player is alerted, the ball is reset to its original position, so is the paddle, and then you return to the game loop without drawing the ball in this function block.

          if (ballVer + changeVer + 10 > boardHeight) {
            changeVer = -changeVer;

            console.log("Hit bottom");
           
            score = 0;
            myPara.textContent = "Score = " + score;
           
            alert("You missed the ball!  Start again");
           
            ballHor = boardWidth / 2 - 20;
            ballVer = 50;
           
            changeHor = 5;
            changeVer = 5;
           
            myPaddle.setAttribute("x", "90px");
           
            return;
  4. Next you do the same for horizontal positions. Bounce off the left and right walls.

          ballHor = ballHor + changeHor;

          if (ballHor + changeHor < 10)
            changeHor = -changeHor;
           
          if (ballHor + changeHor + 10 > boardWidth)
            changeHor = -changeHor;
  5. Now its time to see if the ball hits the paddle or not. This is a smart ball and does all the thinking. The paddle just moves (but elsewhere). This is a simple process. Get the bounding boxes of the ball and paddle. Even though the ball is round, a box can be used to determine collisions. Then once you get the bounding boxes, use their data to check a rectangle that sits on top of the box. If the ball enters that rectangle, a collision takes place. That collision will then make the ball reverse its vertical vector component, add a small random change to the horizontal vector component (called spin), pop the ball up a bit so it doesn't stay tangled in the paddle, and adds +1 to your score.

          bbBall = paddleBall.getBBox();      
          // Get bounding box of paddle.
          bbPaddle = myPaddle.getBBox();        
         
          // Compare bounding boxes.   
          // Is the ball intersecting the paddle?
          // Four conditions must be met.
          if  ((bbBall.x > bbPaddle.x) &&
            (bbBall.x < (bbPaddle.x + 140)) &&
            (bbBall.y > bbPaddle.y) &&
            (bbBall.y < bbPaddle.y + 10)){
              console.log("HIT"); 
             
              // If hitting the paddle, reverse vertical.
              changeVer = -changeVer;
             
              // Make a small spin to create variation.
              spin = Math.floor((Math.random()*5)) - 2;
              // Add spin to horizontal vector.
              changeHor = changeHor + spin; 
             
              // Bounce ball up to untangle from paddle.
              ballVer = ballVer - 20;
             
              // Increase score.
              score = score + 1;
              myPara.textContent = "Score = " + score;


    The only tricky part is the actual collision detection.

                if ((bbBall.x > bbPaddle.x) &&
            (bbBall.x < (bbPaddle.x + 140)) &&
            (bbBall.y > bbPaddle.y) &&
            (bbBall.y < bbPaddle.y + 10)){


    This checks four conditions to make sure that the ball's bounding box is inside the paddle's bounding box and uses the && to say that all four must happen to trigger the collision.
  6. Finally if you didn't hit the bottom of the screen, the ball is drawn.

          paddleBall.cx.baseVal.valueAsString =
            ballHor + "px";
          paddleBall.cy.baseVal.valueAsString =
            ballVer + "px"; 
This is the only trick part, but it seems to work well. And unlike Canvas, you don't have to erase anything, you just move stuff. Well, CSS game programming can just throw stuff around, but only SVG can do really cool shape modification and filters (more about that later, lots more).

Mousedown Event Handler

 This is triggered any time you touch the screen. In order to make this a game, you have to do something. My thought is to make it simple, so you touch on the left half of the screen to move the paddle left, and touch on the right half to make it go right. Each time you tap the screen, the paddle will jump a specific number of pixels and you'll have to tap it again to move it again. This provides a small amount of challenge.

Interestingly enough, when testing this on desktop Firefox, the clicking to move is clumsy and slow. But on a phone, it's fast and very intuitive. No keyboard, no mouse, just your finger. Tap anywhere on the left to move left, etc.

The mousedown handler does the following:
  1. Get the coordinates of where the tap took place.
  2. Get the current paddle X position (we're only moving left and right).
  3. Calculate where the click took place. Center is at 160 pixels right of the left edge. If the tap is on the right, add padMove to the paddle X value. But check to make sure the paddle doesn't move off the right edge.
  4. Do the same for the left of center tap. Again, make sure you don't move the paddle off the left edge.
  5. Move the paddle!
Here's the code for the mousedown handler:

      mouseX = evt.clientX;
      mouseY = evt.clientY;
     
      oldX = myPaddle.x.baseVal.value;   

      if (mouseX > 160) {
        newX = oldX + padMove;

        if (newX > 210)
          newX = oldX;
      }

      else {
        newX = oldX - padMove;

        if (newX < 10)
          newX = oldX;
      }
     
      myPaddle.setAttribute("x", newX);     


Wrapping It Up

You just start the game and move the paddle, stopping the ball from its mad passion to slam into the bottom of the screen. Rack up your score and keep playing. Maybe even have fun. But the real point is to see how to pull together the elements of creation, motion, and collision to make a simple arcade game.

Next I'm going to try to put this in the Marketplace and relate my adventures. After that, more game programming, more game reviews, and so on until I run out of ideas (not soon). Any ideas you have or any simple game types you'd like me to create would be appreciated. Well, maybe not Call of Duty.

Written while listening to the soothing strains of the Panzer Dragoon Saga Memorial Edition soundtrack. One of my all-time favorite games, totally only on the Sega Saturn. I don't have it any longer but I still remember finishing it and really liking the world it took place in, one of archaeology and dragons.

No comments:

Post a Comment