Wednesday, March 26, 2014

CSS Shooter (Game Programming)

Recently I created a new CSS shell and wrote about it in Son of CSS Shell at http://firefoxosgaming.blogspot.com/2014/03/son-of-css-shell-game-programming.html. Having done that, I'd like to devote a few posts to working more with just plain old CSS. No Canvas, no SVG, not even CSS3. Some people call this CSS sprites but I'll just call it CSS game programming. And I'll be doing more with actual CSS sprites, which are a collection of images on a single file.

Like this:


But that's later. Right now I'm interested in doing more actual gaming with CSS. Today's game is a simple one, that is a shooter. Inspired by Breakout but with only one brick.


But then I was also inspired by the recent release of Yoshi's Island on the Nintendo 3DS and actually inspired by the earlier version on the Game Boy Advance that used a special moving cursor to aim shots.






Yoshi is in the center and the cursor (on the top right) moves up and down in an arc. At the right moment you press a button and Yoshi's egg flies in that direction! Well, I'm not going to replicate Yoshi's Island but I will say that the best version is on the Game Boy Advance and it looks like that version will be appearing on the Wii U Virtual Console very soon.

So I created a very simple shooter that aims a bullet from the bottom of the screen and shoots it at a target near the top of the screen. And to make it more challenging, the top target moves from left to right across the screen, while the bullet (until fired) moves back and forth. Just click on the screen at the right moment and you can hit the target.

Here's what the game looks like in play.


Tap anywhere to make the fuchsia bullet shoot up.


The bullet is on its way to the top of the screen. In this case, it looks like it is going to hit the green target. And it does! An alert is displayed telling you that you hit the target.


And if you didn't hit the target (easy to do since the cursor moves one way and the target another) you'll be told that you missed, but in either case you can start over again by just tapping on OK.


The Firefox OS App Manager makes it really easy to test and take screen shots. 

This is cool, but I had even more fun creating it by using the desktop of Firefox Nightly. As explained in the Son of CSS Shell post at http://firefoxosgaming.blogspot.com/2014/03/son-of-css-shell-game-programming.html, I created a simple shell that is responsive to whatever size the browser happens to be. So because this game doesn't have anything phone-specific, I ran it in the desktop version and resized it to vaguely phone size. I also hit Control+Shift+K to get the debugger going, and here is what it looks like:


I've just had a hit, but you can see the debugging output that shows me the target and ball matching, and the subsequent collision. You can also see the bullet and target colliding in the upper right-hand corner.

Working this way is very, very fast when you are creating, and if you're not doing something that requires a phone, I recommend it. Of course all  bets are off if you're doing Device Orientation!

Here's a similar screen showing the debugging output of the desktop browser for a miss. You can see that the bullet is on the left, far away from the target in the middle.


Firefox OS makes it so easy to make games that you have no excuse not to!

Code

And here is the code that makes this happen:

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>
    Simple CSS Shoot
  </title>

  <style>
     
    #ball
    {
      width: 20px;
      height: 20px;
      position: absolute;
      top: 200px;
      left: 100px;
      background-image: url(ball.png);
    }
   
    #target
    {
      width: 20px;
      height: 20px;
      position: absolute;
      top: 200px;
      left: 100px;
      background-color: green;  
    }

  </style>

  <div id="ball"></div>   
  <div id="target"></div>

  <script>

    // Global variables   
    var boardWidth = window.innerWidth;
    var boardHeight = window.innerHeight;   
    var ballHor = boardWidth / 2;
    var ballVer = boardHeight - 50; 
    var tarHor = boardWidth / 2;
    var tarVer = 50;   
    var changeHor = 10;
    var changeVer = 10;
   
    // Log initial board height & width.
    console.log("Board width = "
      + boardWidth);
    console.log("Board height = "
      + boardHeight);
  
    // requestAnimationFrame variations
    var requestAnimationFrame =
    window.requestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.msRequestAnimationFrame;
   
    // Stop scroll bars from interfering.
    document.documentElement.
      style.overflow = 'hidden';
  
    // Load event listener
    window.addEventListener("load",
      getLoaded, false);
     
    // Resize event listener
    window.addEventListener("resize",
      resizeMe, false);
     
    // Mouse down event listener
    window.addEventListener(
      "mousedown",
         bulletFly, false);

    // Get target information.
    var myTarget =
      document.querySelector("#target");

    // Get ball information.
    var myBall =
      document.querySelector("#ball");
  
    // Function called on page load.
    function getLoaded() {
   
      // Lock screen orientation to portrait.
      // Uses "moz" prefix.
      window.screen.
        mozLockOrientation("portrait"); 
      console.log("Locked to portrait");
     
      // Background color
      document.body.style.
        backgroundColor = "Peru";
  
      // Start game loop.
      gameLoop();   
      
      // Page loaded message
      console.log("The page is loaded."); 
    }
  
    // Game loop
    function gameLoop() {
  
      // Repeat game loop infinitely.
      animFrame =
        requestAnimationFrame(gameLoop);
  
      // The ball is now a bullet.
      bulletMove();      
     
      // Move the target.
      targetMove();
      }
  
    // Make the target move.
    function targetMove() {
   
      // Changes are calculated but do not
      // take effect until next time.     
      myTarget.style.left = tarHor + "px";
      myTarget.style.top = tarVer + "px";   

      // Calculate new horizontal component.
      tarHor = tarHor + 10;
    
      // Right edge hit, restart.
      if (tarHor > boardWidth)
          tarHor = 0;
    }
  
    // The bullet flies up.
    function bulletFly() {
   
      // Stop animation loop!
      cancelAnimationFrame(animFrame);    
     
      // Delay for animation motion
      // Outer loop - time set at end     
      loopTimer = setTimeout(function() {
     
        // Animation of the bullet
        // Inner loop - does the drawing
        bulletFrame =
          requestAnimationFrame(bulletFly); 
         
         // Calculate bullet fly up.
        ballVer = ballVer - 50;  
       
        // Changes are calculated but do not
        // take effect until next time.     
        myBall.style.left = ballHor + "px";
        myBall.style.top = ballVer + "px";   

        // Track bullet and target
        console.log("Ball: " + ballVer +
          " ," + ballHor + " Target: " +
          tarHor);
         
        // TEST - REMOVE
        // ballHor = tarHor;
       
        // Test for collision.
        // Horizontal the same
        // Vertical close to top.    
        if  (((ballHor < (tarHor + 20)) &&
              (ballHor > (tarHor - 20))) &&
          (ballVer < 60)) {
             
          // It's a hit!
          console.log("Collision");
          alert("You hit! Start over!");
         
          // Reset everything.
          startOver();
        }
       
        // Test for missing.
        // Vertical less than 50.
        if (ballVer < 60) {
          console.log("Missed!");
          alert("Missed! Start Over!");
         
          // Reset everything.
          startOver();
        }  
      } , 300); // Delay
    }
   
    // Reset everything and start over!
    function startOver(){
   
      // Reset ball and target.
      ballHor = boardWidth / 2;
      ballVer = boardHeight - 50; 
      tarHor = boardWidth / 2;
      tarVer = 50;   
     
      // Stop the drawing.
      cancelAnimationFrame(bulletFrame);
     
      // Stop the loop.
      clearTimeout(loopTimer);
     
      //Go to the game loop and start over.
      gameLoop();
    }
   
    // Calculate and move the ball. 
    function bulletMove() {
  
      // Changes are calculated but do not
      // take effect until next time.     
      myBall.style.left = ballHor + "px";
      myBall.style.top = ballVer + "px";   

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

      // Left edge hit, reverse direction.
      if (ballHor + changeHor < -10) {
        changeHor = -changeHor;
        ballHor = 10;
      }
    
      // Right edge hit, reverse direction.
      if (ballHor + changeHor >
        boardWidth - 10) {
          changeHor = -changeHor;
          // Compensate for unknown width.
          // We want multiples of 10.
          flr = Math.floor(boardWidth / 10);
          ballHor = 10 * flr;        
      }
    }

    // Testing in desktop browser only.
    // Changes board size to match new.
    function resizeMe() {
     
      //Change board size.
      boardWidth = window.innerWidth;
      boardHeight = window.innerHeight;  
     
      // Log initial board height & width.
      console.log("Board width = "
        + boardWidth);
      console.log("Board height = "
        + boardHeight);
    }
   
  </script>
</head>

<body>
  <footer>
    <small>
      Copyright &copy; 2014 Bob Thulfram
    </small>
  </footer>
</body>
</html>


This uses the Son of CSS shell, so I won't explain any of that here! But here are the new parts that you might like to know about.

Target CSS

I defined the target in CSS like this:

    #target
    {
      width: 20px;
      height: 20px;
      position: absolute;
      top: 200px;
      left: 100px;
      background-color: green;  
    }


Similar to the bullet, but instead of using an imported PNG image, I just used the power of CSS to create a box and give it the background color of green. If you want boxes, CSS can turn them out all day.

I also then gave the target a place in the browser DOM by making a DIV. Remember, there's no code in the body of the page!

  <div id="target"></div>

I also had to select the target to make it known to everybody.

    var myTarget =
      document.querySelector("#target");


This might seem like a strange way to do things, but it works in every browser, and especially in Firefox OS. Just do these things to introduce your CSS objects to the DOM and you can do anything you want with them. Follow these steps:

  1. Define the CSS object using CSS.
  2. Create a DIV with the ID of the CSS object you just defined.
  3. Select the object with querySelector and your CSS object is known to all.

Global

Then I added a few things to the global section that will be useful later:

    var tarHor = boardWidth / 2;
    var tarVer = 50; 


This puts the target near the top. It's horizontal will change when the game starts.  

I also changed a global for the ball:

    var ballVer = boardHeight - 50; 

This makes the vertical position of the ball near the bottom. The horizontal will change when the game starts.

Note: in the code, the bullet is called ball because I kept the old code from the shell. I don't like to change variable names if I don't have to. I hope you don't find it too confusing.

Touch Event

Next I added a touch event, also known as mousedown. The phone will interpret this as a touch event.

    window.addEventListener(
      "mousedown",
         bulletFly, false);


This will create an event listener which will call the bulletFly function when the screen is touched anywhere. And works with mouse clicks in the desktop browser. Two events for the price of one!

Game Loop

Add this function call to the game loop:

  bulletMove();

It was called ballMove() in the shell. It will move the bullet back and forth every time the game loop runs (governed by requestAnimationFrame).

Then add  the function call to move the target with this:

  targetMove();

Moving the Target

This moves the target!

    function targetMove() {
       
      myTarget.style.left = tarHor + "px";
      myTarget.style.top = tarVer + "px";   

      tarHor = tarHor + 10;
    
      if (tarHor > boardWidth)
          tarHor = 0;
    }


This just makes the target move from left to right. The target is drawn but the changes won't take effect until the next time through.

The target horizontal position is incremented by 10 and then a check is made. If the target would go off the right edge, the horizontal position is set to 0.

Moving the Bullet

The bullet moves right to left and then left to right. It makes the game more challenging to have two different motions.

    function bulletMove() {
  
      myBall.style.left = ballHor + "px";
      myBall.style.top = ballVer + "px";   

      ballHor = ballHor + changeHor;

      if (ballHor + changeHor < -10) {
        changeHor = -changeHor;
        ballHor = 10;
      }
    
      if (ballHor + changeHor >
        boardWidth - 10) {
          changeHor = -changeHor;
          flr = Math.floor(boardWidth / 10);
          ballHor = 10 * flr;        
      }
    }


This starts out the same as the ballMove function in the Son of CSS shell. But because it only goes left and right and doesn't hit the top or bottom, it just checks for the left and right edges.

If the left edge is hit, the direction changes and the bullet horizontal is set to 10.

But if the right edge is hit, the direction is changed, but a quick calculation takes place to make sure that the new bullet horizontal position is set to a multiple of 10. Because we can't know the exact screen size, this is one of the fun parts of making a responsive design.

Shooting the Bullet

This is the meat of the program. It does a lot of things!

    function bulletFly() {
   
      cancelAnimationFrame(animFrame);    
     
      loopTimer = setTimeout(function() {
     
        bulletFrame =
          requestAnimationFrame(bulletFly); 
         
        ballVer = ballVer - 50;  
       
        myBall.style.left = ballHor + "px";
        myBall.style.top = ballVer + "px";   

        console.log("Ball: " + ballVer +
          " ," + ballHor + " Target: " +
          tarHor);
         
        // TEST - REMOVE
        // ballHor = tarHor;
       
        if  (((ballHor < (tarHor + 20)) &&
              (ballHor > (tarHor - 20))) &&
          (ballVer < 60)) {
             
          console.log("Collision");
          alert("You hit! Start over!");
         
          startOver();
        }
       
        if (ballVer < 60) {
          console.log("Missed!");
          alert("Missed! Start Over!");
         
          startOver();
        }  
      } , 300); // Delay
    }


First the requestAnimationFrame used in the game loop is canceled. Once the bullet starts on its upward journey, you don't want the game loop to run any longer.

Next, a new loop is created, one that will provide a short delay between each motion of the bullet as it flies up. This loop uses setTimeout with an anonymous function. Inside that function is an inner loop that just runs a different requestAnimationFrame.

This will work in a lot of cases where you want motion. You calculate the motion, have a short delay in an outer loop, and then in an inner loop you use requestAnimationFrame to do the actual drawing. This is very efficient.

Once the loop is running, every time through is will move the bullet up 50 pixels and draw it. I added a console.log here so I could track the bullet.

Then I tested to see if a collision had taken place. This is a bit tricky, and watch those parentheses. I recommend you use an editor that will check parentheses! I use notepad++ and I'm very happy with it.

The calculation sees if the horizontal positions of the bullet and target are within a certain range of each other and then checks the vertical. The && is just a logical AND and says these three conditions have to be true:

  1. The bullet and target are close on one side horizontally.
  2. The bullet and target are close on the other side horizontally.
  3. The bullet is high enough to be close vertically to the target.

If all three are true, you have a collision. A console.log is displayed, an alert is displayed, and the startOver function is called to restart the game. 

If that doesn't happen, then a second check happens to see if the bullet went off the top of the screen. If it did, a similar console.log, alert, and call to startOver takes place.

Note: I added

   ballHor = tarHor;


in my testing to make it easy to see if the bullet was hitting the target but then commented it out. Sometimes this is a useful trick when you are trying to test something in a fast moving game.

Starting Over

I created a separate startOver function because the tasks were shared between colliding and going off the top of the screen.

    function startOver(){
   
      ballHor = boardWidth / 2;
      ballVer = boardHeight - 50; 
      tarHor = boardWidth / 2;
      tarVer = 50;   
     
      cancelAnimationFrame(bulletFrame);
     
      clearTimeout(loopTimer);
     
      gameLoop();
    }


This just sets the initial position of the bullet and target. It then cancels the two loops that were used to move the bullet up, and calls the game loop.

That's it!













No comments:

Post a Comment