Assignment 10: Super Mushroom Ball!

In lab before, we made a Sprite class that inherited from the built-in PictureBox so that we could make an image move around on a form using a timer. In this assignment, we're going to make a fun little game out of it.

In this game, you are a valiant mushroom, fated by destiny to...oh, let's say save the moon. By not dying. The evil circles are jealous of the moon, which used to be a nice circle from the neighborhood but now has gotten famous and way stuck up, and are sworn to stop you at all costs. Here's how it works:

  • The player uses the a and d keys to move left and right, and space to jump
  • At random times, a circle will appear from the right and "roll" across the floor to the left
  • If the mushroom touches any circle, the game is over
  • A big timer shows at the top to see how long the player can last
  • As the game progresses, it gets harder (more circles, faster, bigger)
  • At random times, a circle will appear from the top of the screen and fall straight down (just passes through the floor)
  • The circles from the right will start to bounce across the screen instead of just rolling

Check out the empty repo to create your project in. Since this is a Windows Form app, you'll need to submit all the files, including the solution. And don't forget any images! Please omit the bin and obj directories when you commit. The repo is here:

https://cssvn.utrgv.edu/svn_etomai/201810_3328/assn10_smb/<username>

Enter Hero, Stage Right

Start a Windows Form project (or continue working with the one from lab). Recall from lab that we can right-click on the project in the Solution Explorer and Add->New Item... a Custom Control. (There is alse User Control but that is intended for composite controls combining many others). Our new control class, named Sprite, will inherit from PictureBox.

Start by placing a Sprite control on the form. Since we're setting it up in the designer, you can use the Properties tab to set the image. (If you're working from the lab code, comment out the Start method on Sprite for the moment.) We learned in lab that to get it to be the right size you need to:

  • Set SizeMode to Zoom
  • Set Image to the mushroom image, which you should load as a local resource (be sure to save the image in the project folder and commit it with everything else)
  • Set Size to the desired size (128x128 looks nice on my screen)

Next, open the code for your form. Create constants for the screen width and height. 1024x768 would be good, unless you're working on too small a screen. In the Form1_Load method (remember, add handlers through the form designer events list for that control), add code to set the form Size to those constants.

In the form designer, move the mushroom to start on the ground in the lower left hand corner, and pay attention to what the Location property is set to there. That will tell you the orientation of the coordinate system, and which corner of the image is being used as the anchor for positioning.

You Move Me

Add a timer to your form (if it's not already there). Create a constant in your form for the update interval (10) and set the timer's Interval property in Form1_Load. We're doing this in the load method to ensure that the value in the code and the values in the control is the same. We could read from the control every time we need to know, but the trade-off there would be inefficiency. Same with the form size.

In the timer1_Tick event handler, call the UpdateBehavior method on the mushroom Sprite.

To get key events, add KeyDown and KeyUp event handlers to the form. In the handlers, check to see which key was pressed or released and let the player Sprite know. You'll need an if-else tree to check for Key.A, Key.D, and Key.Space. Add public boolean properties for each key of interest to the Sprite class, and set them to true on key down and false on key up. Then, in Sprite.UpdateBehavior, you can move to the left or right based on which keys are being held (if both, stay still).

As we discussed, the distance to move is simply velocity * time. Velocity, at the moment, is just a SPEED constant for the mushroom, and time is the update interval (10ms). Update the x component of Sprite.Location accordingly (remember that Location is an immutable Point, so you actually have to set Location to a new Point entirely).

Run Into the Wall

Finally, let's not let the mushroom walk off the screen. Update your movement code to check and stop when you reach the left or right boundary.

Enemy Mine

The first ball enemy can be done pretty much just like the mushroom. Find a good evil circle image, add the control the form, and update it in timer1_Tick. At this point though, you should notice that the circle and the mushroom have different update behavior. We need a new class! We could create a comment parent, but at this point there is no compelling reason to do so. Instead, right click on Sprite in the class definition and select Rename.... Change the name to PlayerSprite and apply, and it will change throughout the project, including hidden files. Then create a second Custom Component called EnemySprite.

Add an EnemySprite to the form, and set it's Location to be off the right side. Then add EnemySprite.UpdateBehavior to make it move across to the left.

Collide!

Since we only care about things colliding with the player, the easiest approach will be to create a CheckPlayerCollision method on EnemySprite. This method will be called after UpdateBehavior in timer1_Tick. Pass in the player sprite, and it should return True if that enemy is touching the player, False otherwise. You can do circle collisions, which are close enough for circle images: check if the distance between the circle center and the mushroom center is less than the combined radii.

If CheckPlayerCollision returns True, stop the timer and show a big "Game Over" label.

Up, up and away!

When the player hits space, we want the mushroom to jump. That means giving it velocity in the y direction as well. To see how this works, make a few simple edits to PlayerSprite.UpdateBehavior first.

  • Add a velocityY variable to PlayerSprite
  • When the space key is held down, set velocityY to some non-zero value (is up positive or negative?)
  • When you update Location, also update the vertical location by adding velocityY * time to your vertical position

Your mushroom should now be able to fly away to the moon and live happily ever after. The end.

Not The End

We need some gravity around here. In the same way that we update position with position = position + (velocity * time), we can update velocity with velocity = velocity + (acceleration * time). This will invole a few changes.

  • We need to know if the player is on the ground or not. Add a bool variable for that.
  • When the player jumps, set that variable to true
  • When the player is on the ground:
    • Check for A/D and move left/right
    • Check for space to jump
    • Don't change velocityY or the player's vertical location
  • When the player is not on the ground:
    • Update velocityY based on an acceleration constant
    • Update the player's vertical location based on velocityY
    • Check to see if the player has landed back on the ground
    • Don't allow A/D to change horizontal movement! If they were moving right, keep going right regardless. You'll need variables to keep track of this.
    • Don't check for space to jump

The most elegant way to do this is to make velocity a Vector2. There are less special cases if you allow for velocity to be in any direction at any time. However, for this assignment, treating horizonal and vertical separately is simpler, and fine. We're not doing any angled collisions, so it's not necessary to use Vector2.

Finish the Game

Add a big timer to the top of the screen and you've got a playable game! Not much too it though. Here's the rest.

Multiple Enemies

Placing all the enemies you would ever need at once on the form in the designer is easy, but limited. You can take that approach, or you can programmatically add enemies on demand. In a bigger, more dynamic game, the latter would be necessary, but here you can go either way. If you want to do it programmatically, you'd need to implement a startup method (like we did in lab) to set the image and such. Something like:

public void Start(string filename)
{
    SizeMode = PictureBoxSizeMode.Zoom;
    Image = Image.FromFile(filename);
    Size = new Size(128, 128);
}

(Side note: dynamically creating and destroying objects is costly at run-time, so the real correct strategy is pooling - programmatically creating a fixed set of objects and re-using them, expanding the set as needed.)

You'll also want to collect all the enemy objects up in an array variable in your form so that timer1_Tick can loop over it rather than hardcoding the update/collide calls for each one.

Multiple Behaviors

You need to make three types of enemy behaviors - rolling, falling and bouncing. All three still use the same CheckPlayerCollision, so this is a good case for inheritance. Alternatively, you could set a mode variable in EnemySprite and switch between three different UpdateBehavior methods in the same class.

The bouncing behavior is implemented much like jumping for the player, only the velocity is set to start, and each time it hits the floor, it bounces back up (invert the vertical velocity). I'd recommend decaying it a bit on that inversion so that it bounces lower each time and looks cooler.

To have different sizes and speeds, you'll need variables in EnemySprite rather than constants.

The Coup d'Etat

The game would be much cooler if the mushroom kept running to the right, instead of being in a fixed box. This isn't required, but if you got here, then it's a great way to finish. Then the goal can be to get to some point at the end rather than just to survive.

To implement this, think about the screen as a camera into the world. Every entity has a position, so say at tree is at x=10. If the screen camera is at x=0, then the tree appears on the screen at x=10. But, if you move the camera to x=2, then the tree appears on the screen at x=8.

You still want to work with all your entities in world coordinates. It's way too confusing to do otherwise. But then once you're done updating everyone's positions, you have another loop that goes through and updates the screen coordinates for each Sprite as explained above. In this game setup, the world coordinates would be a private variable, while Sprite.Location is the screen coordinates.

You would want the screen camera to be locked on to the player (horizontally), so the calculation for screen coordinates for any entity is it's horizontal position relative to the player. Since the camera doesn't move up and down, the vertical coordinates would be the same as in the world. (Note: in practice, it's better to have a separate location variable for the camera, so that it can "attach" and "detach" from the player if needed. You just update that variable every frame to "follow" the player.)

Oh, and you'd probably want to put some images in the background so that you can tell the player is moving. Since the camera is fixed to the player, it will always be in the same place (horizontally) on the screen.