Coding the Lunar Lander

Building the Lunar Lander game simply requires putting together all the various elements in a new form. The program runs under a timer’s control. All user interaction happens through keyboard input, and the animations use images copied from an image list.

The Visual Design

I started by sketching out the visual design and the overall plan for the game. The visual interface is very simplistic. I wanted one ship, named picLander, and one landing platform, named picPlatform. Additionally, I added a series of labels to communicate various game variables to the user. Each of these labels is named to correspond to a specific variable. I’ll describe them in more detail in the ShowStats() method because I didn’t add them to the interface until I wrote that method.

In addition to the visual elements, I added a couple invisible controls. A timer control handles all the interval-based elements (which take up the bulk of the code), and I used an image list to support the variations of the lander spouting flames in different directions. The image list includes four images of the lander:

  1. No flames

  2. Flames on bottom

  3. Flames on left

  4. Flames on right

The Designer-Generated Code

Generally, I have not shown you the code generated by the Designer, but I show it to you here because I added a few elements. I’ll explain my modifications after the code listing.

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace Lander
{ 
/// <summary>
/// Basic Arcade Game
/// Demonstrates simple animation and keyboard controls
/// and use of timer.
/// Andy Harris, 1/16/02
/// </summary>

public class theForm : System.Windows.Forms.Form
{
  //my variables
  private double x, y;      //will show new position of lander
  private double dx, dy;    //difference in x and y
  private int fuel = 100;   //how much fuel is left
  private int ships = 3;    //number of ships player has
  private int score = 0;    //the player's current score

  //created by designer
  private System.Windows.Forms.Timer timer1;
  private System.Windows.Forms.PictureBox picPlatform;
  private System.Windows.Forms.Panel pnlScore;
  private System.Windows.Forms.Label lblDx;
  private System.Windows.Forms.Label lblDy;
  private System.Windows.Forms.Label lblShips;
  private System.Windows.Forms.Label lblFuel;
  private System.Windows.Forms.PictureBox picLander;
  private System.Windows.Forms.ImageList myPics;
  private System.Windows.Forms.Label lblScore;
  private System.ComponentModel.IContainer components;

  public theForm()
  {
     //
     // Required for Windows Form Designer support
     //
     InitializeComponent();
     //I added this call, to a method that starts up a round
     initGame();
  } 
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
      if( disposing )
      {
        if (components != null)
        {
          components.Dispose();
       }
     }
     base.Dispose( disposing );
   }

   #region Windows Form Designer generated code
   /// <summary>
   /// The main entry point for the application.
   /// </summary>
   [STAThread]
   static void Main()
   {
     Application.Run(new theForm());
   }

The Designer-written code shows its usual lack of flair but is functional. Note that I hid the InitializeComponent() method call because you shouldn’t generally mess with it if you are using the Designer. I added several small but important things to the first chunk of code. I included a number of class-level variables and made a small modification to the constructor.

Class-Level Variables

Variables defined at the class level can be regarded as the DNA of an object. You can learn a lot about how the object works by understanding these variables, especially if the program is well written and documented. If these conditions are met, you can see an overview of the entire program’s structure by looking at the variables.

Several of the variables (x, y, dx, and dy) are used to position the lander on the screen. You might be surprised to see that all four of these variables are doubles, rather than the integers you’ve used throughout the chapter for positioning components. The Point and Location classes that form the basis of screen motion require integers, but when I was testing the game, I found that integers did not give me the fine-grained control that I wanted. In particular, the effects of gravity were too difficult to get right, within the limits of integers. I decided to do my calculations with doubles and then convert the values to integers when needed. (Don’t panic. I’ll explain this as it comes up in the code listing.)

The fuel, ships, and score variables are used in scorekeeping and to determine when the game is over. All are integers.

Notice the comments after all the variables. Documenting all your important variables in this way is an excellent habit to form. You’ll find that the effort pays off when your program becomes complicated and you don’t remember what each of your cryptic variable names stands for.

The Constructor

You might recall that a constructor is a special method that helps build an instance of a class. It always has the same name as the class, and is automatically called when the class is created. In this case, the constructor is a method named theForm(). The Designer automatically created this constructor and added a call to the InitializeComponent() method (which it also created automatically). However, I also wanted something to happen the first time the program started. I wanted to set up all the initial conditions for the game. The instructions for this are stored in the initGame() method, which you’ll investigate shortly.

Sometimes you might be intimidated by the warnings not to change code created by the Designer. Although you certainly should be careful about doing this, you are the programmer. There is nothing wrong with adding your own code to a method generated by the Designer. In fact, it’s often necessary to do so. Still, you should avoid changing InitializeComponent unless you’re willing to finish writing the program without the assistance of the Designer. Changing the contents of that particular method can make it impossible for the Designer to read your code.

The timer1_Tick() Method

As you’ve seen throughout this chapter, much of the action in an arcade-style game happens in the tick method of the timer. The Lander game reinforces this observation.

private void timer1_Tick(object sender, System.EventArgs e) {
       //code that should happen on every timer tick (10 times/sec)

       //account for gravity
       dy+= .5;

       //increment score for being alive
       score += 100;

       //show ordinary (no flames) lander
       picLander.Image = myPics.Images[0];

      //call helper methods to handle details
      moveShip();
      checkLanding();
      showStats();
  } // end timer tick

This method does a great deal of work. All the main logic for the game flows through this method 10 times a second. However, many of the most critical elements are passed off to other methods.

First, I added .5 to dy to account for gravity. Each time the timer ticks, there will be a small force pulling the lander downwards. The exact amount for dy is a tricky thing to determine. This was the main reason I used doubles for the math. When dy had to be a whole number, gravity of 1 was just too powerful at 10 times per second, and 0 gave no gravity at all. One solution to the “heaviness” of a gravity of 1 is to lower the frame rate by changing the timer’s interval. When I tried this, the animation seemed too choppy. I decided, instead, to work with double values and then convert back to integers when needed. Sometimes you have to think creatively to get the results you want.

Then next thing I did was add a value to the score simply because the user survived another tick of the clock. It’s a long-standing tradition in arcade games never to add fewer than 100 points to the user’s score. I guess that it’s the video version of grade inflation.

I then displayed the version of the lander with no flames in the picture box by copying the appropriate image (number 0) from the ImageList. I did this because no flames should be the default setting. I’ll add the flames later if I discover that the user is pressing a key.

The last three lines of the tick() method call other custom methods. The timer has three more important jobs to do, but each is detailed enough to merit its own method. I gave the methods names indicating the main jobs that need to be done. Leaving the details of (for example) moving the lander, updating the score, and checking for collisions to other methods is a good idea because leaving all that code in the tick() method would make the method extremely complicated and difficult to read and fix. With the custom methods, it’s very easy to see the main flow without getting bogged down in details. Of course, you need to worry about the details at some point, but they’re easier to work with in isolation.

Whenever one method starts to be more than one screen long, consider breaking it into multiple methods. This way, you can break the work into smaller chunks that are easier to manage.

The moveShip() Method

The moveShip() method handles all the movement of the lander on the screen. The code should look familiar.

private void moveShip(){
  //change x and check for boundaries
  x += dx;
  if (x > this.Width - picLander.Width){
    x = 0;
  } // end if
  if (x < 0){
    x = Convert.ToDouble(this.Width - picLander.Width);
  } // end if

  //change y and check for boundaries
  y += dy;
  if (y > this.Height - picLander.Height){
    y = 0;
  } // end if
  if (y < 0){
    y = Convert.ToDouble(this.Height - picLander.Height);
  } // end if

  //move picLander to new location
  picLander.Location = new Point(Convert.ToInt32(x),Convert.ToInt32(y));
} // end moveShip

The process of modifying dx and dy is probably routine for you by now, but this routine has one twist. Because I’m working in double values, I can’t simply copy the screen location to x when I want to move to the right or the bottom of the screen. The compiler complains about the conversion from integer to double. I simply used Convert.ToDouble() to get past this problem.

Likewise, when I was ready to place the lander in its new position, the values of x and y were doubles, but I needed ints. Again, the Convert class came to the rescue.

The checkLanding() Method

The interesting moments in the game occur when the lander gets close to the platform. When these two components are in proximity, it means either a crash or a landing. The checkLanding() method determines whether the lander is near the landing pad and, if so, whether it is a safe landing or a horrible crash:

private void checkLanding(){

    //get rectangles from the objects
    Rectangle rLander = picLander.Bounds;
    Rectangle rPlatform = picPlatform.Bounds;

    //look for an intersection
    if (rLander.IntersectsWith(rPlatform)){
      //it's either a crash or a landing

      //turn off the timer for a moment
      timer1.Enabled = false;

      if (Math.Abs(dx) < 1){
        //horizontal speed OK
        if (Math.Abs(dy) < 3){
          //vertical speed OK
          if (Math.Abs(rLander.Bottom - rPlatform.Top) < 3){
            //landing on top of platform
            MessageBox.Show("Good Landing!");
            fuel += 30;
            score += 10000;
          } else { 
         // not on top of platform
         MessageBox.Show("You have to land on top.");
         killShip();
        } // end on top if
      } else {
        //dy too large
        MessageBox.Show("Too much vertical velocity!");
        killShip();
      } // end vertical if
    } else {
      //dx too large
      MessageBox.Show("too much horizontal velocity");
      killShip();
    } // end horiz if
    //reset the lander
    initGame();
  } // end if
} // end checkLanding

This code might look long and complex at first, but it is not difficult. The hardest part of writing this method was figuring out what constitutes a safe landing. I determined that a safe landing occurs only when all the following criteria are true at the same time:

  • The rectangles for the lander and platform intersect.

  • The horizontal speed (dx) of the lander is close to 0.

  • The vertical speed (dy) of the lander is smaller than 3.

  • The lander is on top of the platform.

The easiest structure for working with this kind of problem (where you can proceed only when several conditions are met) is a series of if statements nested inside each other. This structure is named (not surprisingly) nested ifs. I decided to turn each of the criteria in the list into an if statement and nest them all inside each other. In other words, I started by checking to see whether the platforms intersect. If you look back at the code, you see that the first if statement checks whether the rectangles intersect. If that condition is true, something has happened. If all the other conditions are true, the player has landed successfully, but if not, there has been a crash. If the rectangles do not intersect, there is no point checking the other conditions.

NOTE

IN THE REAL WORLD

The nested if structure is commonly used whenever a programmer needs to check for several conditions. In more traditional programming (the kind you’re more likely to get paid for), you commonly use nested ifs whenever you want to do some sort of validation. For example, if you write a program that processes an application form, you probably won’t allow the program to move on until you are sure that the user has entered data in all the fields and that all the data can be checked. Validation code usually uses the same nested if structure as the Lunar Lander game. However, there are not as many cool effects when the user doesn’t submit a valid email address. (I’ve always been tempted to add explosions in that sort of situation, but so far, I’ve been good.)

Checking the Horizontal Speed

If the rectangles do intersect, another if statement checks what the horizontal speed (measured with the variable dx) is. I wanted to require that the ship be nearly motionless along the X axis because the legs of such a craft can’t handle much sideways velocity. (Also, this rule makes the game more challenging!) I figured that a value between –1 and 1 for dx would be a good range. However, testing for a value greater than –1 and less than +1 is a little ugly. I decided to take advantage of the built-in absolute value method of the Math class, Math.Abs(). As you may recall fondly from math class, the absolute value of a number strips the sign off a number, so the absolute value of –1 is 1, and the absolute value of +1 is also 1. By using the Math.Abs() method on dx, I was able to determine a very small horizontal velocity with only one if statement.

Checking the Vertical Speed

The vertical speed is calculated much like the horizontal speed. You might be surprised that I still used the absolute value function here because the lander will always approach the platform from the top and will always have a positive dy value if a legal landing is possible. This is true, but I seriously thought at one time about allowing landings from the bottom (maybe you’re attaching a balloon to a floating platform). This would greatly change the strategy of the game and allow for interesting piloting situations, so I left the absolute value in here, just in case. (Does that sound like a dandy end-of chapter exercise, or what?)

Ensuring That the Lander Is on Top of the Platform

The last requirement for a healthy landing is that the lander must touch the top of the platform (at least for now). This turned out to be an easy thing to check. I just used properties of the picLander and picPlatform picture boxes to compare the bottom of picLander and the top of picPlatform. I used the absolute value method again to ensure that the bottom of the lander is within 3 pixels of the top of the platform.

Managing a Successful Landing

If the player passes all four landing tests, the program sends a congratulatory message, loads up more fuel, and adds significantly to the player’s score. If the player fails to pass any of the landing criteria, but the rectangles intersected, the program responds with an appropriate message and calls the killShip() method, which handles the details of lander destruction.

Handling User Crashes

If the rectangles intersected (regardless of the rest of the consequences), the method calls the initGame() method to reset the positions of the lander and platform.

The theForm_KeyDown() Method

The player interacts with the program through keyboard commands. The up arrow fires thrusters that slow the effects of gravity and can eventually push the lander upwards. The left and right buttons fire side-pointing thrusters that allow side-to-side mobility. The keyboard handling routine is familiar if you investigated the Mover program earlier in this chapter:

private void theForm_KeyDown(object sender,
      System.Windows.Forms.KeyEventArgs e) {
      //executes whenever user presses a key

      //spend some fuel
      fuel--;

      //check to see if user is out of gas
      if (fuel < 0) {
        timer1.Enabled = false;
        MessageBox.Show("Out of Fuel!!"); 
        fuel += 20;
        killShip();
        initGame();
      } // end if

      //look for arrow keys
      switch (e.KeyData) {
        case Keys.Up:
          picLander.Image = myPics.Images[1];
          dy-= 2;
          break;
        case Keys.Left:
          picLander.Image = myPics.Images[2];
          dx++;
          break;
        case Keys.Right:
          picLander.Image = myPics.Images[3];
          dx--;
          break;
        default:
          //do nothing
          break;
      } // end switch
    }  // end keyDown

Every time the user presses a key (even non-arrow keys!), the program uses up one unit of fuel. As I’ve said many times, if you increment or decrement a variable, you should test for boundary conditions. Because I’m decrementing the amount of fuel, checking for an empty gas tank is sensible. If the user runs out of gas, I disable the timer temporarily to stop the game flow and then display a message to the user so that he or she knows why the game stopped. I then call the killShip() method to take a ship out of the inventory, and I call the initGame() method to reset the speed and position of the lander and platform.

After dealing with the fuel situation, my attention turns to the actual key presses. Because I’m concerned with arrow keys here, I use the KeyDown() method and concentrate on e.KeyData. Depending on which key was pressed, I copy the appropriate image from the ImageList and set dx and dy to achieve the appropriate motion later when the timer ticks. Notice that I added a default condition to handle keystrokes other than arrow keys. If I were a nice guy, I would have used the default condition to add back the fuel value. Then, if the user accidentally hits a key, it would not cost precious fuel. However, as a game programmer, you can be mean if you want (cue maniacal laughter).

The showStats() Method

The showStats() method is called every time the timer ticks. Its job is to update the labels that display statistics, such as the score and the ships remaining to the user. This code is quite simple:

public void showStats(){
  //display the statistics
  lblDx.Text = "dx: " + dx;
  lblDy.Text = "dy: " + dy;
  lblFuel.Text = "fuel: " + fuel;
  lblShips.Text = "ships: " + ships;
  lblScore.Text = "score: " + score;
} // end showStats

A few assignment statements are all that is required. However, if you don’t provide adequate information to the user, your game will not be successful. Also, because updating the score happens often, it’s nice to have the code stored in a procedure.

The killShip() Method

The killShip() method is meant to be called whenever the user has lost a ship because of crashing or running out of fuel:

public void killShip(){
  //kill off a ship, check for end of game   
  DialogResult answer;
  ships--;
  if (ships <= 0){
    //game is over
    answer = MessageBox.Show("Play Again?","Game
Over",MessageBoxButtons.YesNo);
     if (answer == DialogResult.Yes){
       ships = 3;
       fuel = 100;
       initGame();
     } else { 
    Application.Exit();
    } // end if
  } // end 'no ships' if
} // end killShip

The act of killing off the player’s ship takes only one line of code. The real meat of the killShip() method is the part that checks whether the game is over. If the player is out of ships, the method asks the user whether he or she wants to play again, using a yes/no message box. If the player does want to play again, the number of ships is reset, and the initGame() method resets the speed and position of the lander and platform. If the user does not want to play again, the program exits completely with the call to Application.Exit().

The initGame() Method

The initGame() method is a real workhorse. It has a simple job but is called from several places in the program. The purpose of initGame() is to randomly set the location of the lander and platform and randomly choose the speed of the lander:

public void initGame(){
  // re-initializes the game
  Random roller = new Random();
  int platX, platY;

  //find random dx, dy values for lander
  dx = Convert.ToDouble(roller.Next(5) - 2);
  dy = Convert.ToDouble(roller.Next(5) - 2);

  //place lander randomly on form
  x = Convert.ToDouble(roller.Next(this.Width));
  y = Convert.ToDouble(roller.Next(this.Height));

  //place platform randomly on form (but make sure it appears)
  platX = roller.Next(this.Width - picPlatform.Width);
  platY = roller.Next(this.Height- picPlatform.Height);
  picPlatform.Location = new Point(platX,platY);

  //turn on timer
  timer1.Enabled = true; 

}   // end initGame

With all the randomization that happens in the initGame() method, you won’t be surprised to find an instance of the Random class defined in the method.

Choosing New dx and dy Values

I wanted dx and dy to be somewhere between 2 and –2, which is not directly possible with the Random class. However, I asked for an integer value between 0 and 4 (remember, the 5 refers to the number of possible responses, not the largest possible response), and I subtracted 2 from this number. This will give results evenly spaced between 2 and –2. However, the results are integers, and dx and dy are double variables, so I used the trusty convert object to get doubles where I wanted them.

Placing the Lander on the Form

Finding a legal place for the lander is relatively easy. The new X location should be somewhere between 0 and the width of the form, and the new Y should be between 0 and the height of the form. Again, it was necessary to convert to doubles because I chose to implement x and y as double values.

Placing the Platform on the Form

In my first attempt, I simply copied the lander code over to make the platform position code, but when I started play-testing, I discovered a problem. Every once in a while, the lander’s position is mainly off the screen, so it is impossible to land on. Players don’t mind being challenged, but they’ll get grumpy if you make it impossible to succeed. I had to modify the code slightly to guarantee that the landing platform would be visible on the screen. You simply subtract the width and height of the platform from the screen width and height, and it becomes impossible for the random number generator to create a position that will cause the platform to be invisible.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.29.119