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:
a
and d
keys to move left and right, and space
to jumpCheck 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>
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:
SizeMode
to Zoom
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)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.
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).
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.
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.
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.
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.
velocityY
variable to PlayerSprite
space
key is held down, set velocityY
to some non-zero value (is up positive or negative?)Location
, also update the vertical location by adding velocityY * time
to your vertical positionYour mushroom should now be able to fly away to the moon and live happily ever after. 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.
bool
variable for that.velocityY
or the player's vertical locationvelocityY
based on an acceleration constantvelocityY
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
.
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.
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.
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 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.