| XNA Game-Themed CS1 Examples (XGC1) | |
|
Release 2.0 (XNA V3.1) |
|
References:
Goals:
1. Obtain the example code
When the game starts, you'll see a screen that
looks similar to this:

By using the left thumbstick, you can move the soccer ball around the screen. By using the right thumbstick, you can roll the soccer ball. To "roll" the ball means to rotates (or turn) the image of the soccer ball, while moving it sideways. Currently, you can only roll the ball left or right - you can't roll it up or down. Pressing the 'A' button causes the game to create a new soccer ball, back at the center of the screen. Any older soccer balls are left on the screen, and cannot be controlled after the new soccer ball is created.
During the tutorial, you will change this so that when the 'A' button is pressed, a new soccer ball is created in the middle of the screen (just like now), but the old soccer ball is destroyed, instead of simply leaving it on the screen.
2. Examining The Program:
In this chapter, we will begin to focus on many of the important concepts of Object Oriented Programming, which is commonly abbreviated as OOP.
Throughout many of these tutorials, we have used SoccerBalls in all our programs. In each tutorial, the soccer balls have all had the sort of properties - where are they, what texture (picture) to display, and (possibly) it's velocity. While every single soccer ball that we've ever coded up is not quite identical to the others, they are clearly very, very similar, and so it would be nice to not have to re-type this all the SoccerBall code each and every time. Instead, we would like to separate all the code (including data) about SoccerBalls into a separate 'block', that we can then easily reuse. We call this block a class; code that is neatly separated into a separate class is said to be encapsulated. (Technically, to say that a class is properly encapsulated means that the data fields and logic/methods have been set up with appropriate access modifiers, but we'll ignore this detail for right now :) )
In addition to encapsulating all the data and logic (methods) that related to the SoccerBall into it's own class, we will also physically move that class into a separate file. This way, the details about how a soccer ball works goes into the separate file, and everything that relates to how the soccer ball gets used specifically in this game still goes into the Game.cs file. For example, the logic that causes the soccer ball to roll is put into the soccer ball's file. The logic that says that when the player pushes the right thumbstick rightwards, the soccer ball should be told to roll righwards goes into the Game.cs file. The goal is to make Game1.cs much more simple, by 'hiding' the details of exactly how the SoccerBall rolls
As you go through this tutorial, try to focus on the difference between a class, which defines what all objects in a particular category have in common, and an object (also known as an instance of a class), which is a particular block of memory, with particular set of values associated with it. For example, we might define a class to describe all cars. In our simplified Car class, each car might have an engine, a maximum capacity to store some fuel, and a data field dedicated to recording how much fuel each Car object currently has in it. One particular instance of this class, such as the author's car, might have a V8 engine, a fuel capacity of 15 galleons (of gasoline), and might currently (as of this writing) have exactly 1.2 galleons in the tank. You might own a car with a V6 engine, a fuel capacity of 10 galleons, and might currently have 9.7 galleons in the tank. The point here is that there is an abstract definition of what all cars have in common (this is the class), which defines what each individual object have in common. The individual objects (the author's car, your car) store information such as exactly how much fuel is left in a particular, concrete car.
Note: we're starting this tutorial with a very basic example - there's no real need to wrap the soccer ball into it's own class, because the XNACS1Circle does most of what we need already. Knowing this, you should use your familiarity with how SoccerBalls (and basket balls, etc, etc) have been implemented in previous tutorials to allow you to focus on those concepts that pertain specifically to Object-Oriented Programming.
2. SoccerBall.cs:
Let's examine the C# source code contained in the separate SoccerBall.cs file.
(You may need to select the View → Solution Explorer menu item, then open up the project, and double-click on the file named SoccerBall.cs, in order to examine the source code directly)
|
using
System;
using
System.Collections.Generic;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Audio;
using
Microsoft.Xna.Framework.Content;
using
Microsoft.Xna.Framework.GamerServices;
using
Microsoft.Xna.Framework.Graphics;
using
Microsoft.Xna.Framework.Input;
using
Microsoft.Xna.Framework.Net;
using
Microsoft.Xna.Framework.Storage;
using
XNACS1Lib;
namespace
SimpleObject
{ |
|
///
<summary>
/// Class: SoccerBall, a simple circle and can roll and bounce.
///
</summary>
public
class
SoccerBall
{
#region constants private to this
class
private
const float RADIUS = 3.0f;
private
const float INIT_X = 50.0f;
private
const float INIT_Y = 30.0f;
const float
ROLL_ANGLE = 1.2f;
// constants, fields, and methods
// are private by default in C#
const float
MOVE_UNIT = 0.1f;
// So these two constants are 'private' // even though we didn't put 'private' here!
#endregion
|
#region constants private to this
class
private
const float RADIUS = 3.0f;
private
const float INIT_X = 50.0f;
private
const float INIT_Y = 30.0f;
const float
ROLL_ANGLE = 1.2f;
// constants, fields, and methods
// are private by default in C#
const float
MOVE_UNIT = 0.1f;
// So these two constants are 'private'
// even though we didn't put 'private' here!
#endregion
Because these named constants have been declared within the SoccerBall class, and because they have been declared to be private, we are NOT allowed to use these constants outside of this class.
Remember that the class starts with the open-curly brace ( { ) after the "public class SoccerBall" line, and the class ends with the matching close curly brace ( } )
Notice that if we don't specify private ( nor public, nor protected), then C# will assume that the named constant is private. Even though we technically don't need to write out private for each of the named constants (see the last two constants for an example of this), it's good practice to write it out just be very, very clear that you intend these constants to be private, on purpose.
Specifically, we are NOT allowed to use these constants in the Game1.cs file.
Similarly, any private constants (or data fields) declared in the Game1 class CAN NOT be accessed here, in the SoccerBall class.
If we had declared these constants to be public, we could have used them in the Game1.cs file. The reason why we want these to be private is that they're used by the private, internal implementation of the SoccerBall class - nothing else should need to know about these, therefore we don't want another piece of code to 'accidentally' use these, thinking that it's ok.
|
const
float ROLL_ANGLE = 1.2f;
// constants, fields, and methods
// are private by default in C#
const
float MOVE_UNIT = 0.1f;
// So these two constants are
'private' // even though we didn't put 'private' here!
#endregion
// Private instance variables
private
XNACS1Circle m_TheBall;
/// <summary>
/// Create
a soccer ball at fixed location. /// </summary> |
As you can see, the SoccerBall class has included a reference to an XNACS1Circle object in it's definition. This means that each and every instance of the SoccerBall class (i.e., each and every SoccerBall object) can refer to it's own, separate XNACS1Circle object).
This is why we see multiple soccer ball images on the screen, as we keep creating more SoccerBall objects - each separate SoccerBall object keeps a reference to own XNACS1Circle object, which is what's drawn on the screen.
You'll notice that the instance variable we've declared is marked as being private, meaning that each SoccerBall object can access the XNACS1Circle object named m_TheBall. However, the code found in the Game1.cs file cannot access this private variable.
|
// Private instance variables
private
XNACS1Circle m_TheBall;
///
<summary>
/// Create
a soccer ball at fixed location.
///
</summary>
public SoccerBall()
{
m_TheBall = new
XNACS1Circle(new
Vector2(INIT_X, INIT_Y), RADIUS);
m_TheBall.Texture = "SoccerBall";
}
/// <summary>
/// Make it look like the soccer ball
is rolling
/// </summary>
public
void RollTheSoccer(float
dx)
{ |
Each time we create a SoccerBall object, we'd like to ensure that certain 'initialization' steps are done, in order to ensure that each and every SoccerBall has been properly set up before it gets used. We do that in the above constructor, which creates a brand-new XNACS1Circle object at the location (INIT_X, INIT_Y), and with the image of the soccer ball on it.
Notice that we declare the constructor to be public, meaning that code in the Game1.cs file can make use of this constructor.
Normally, constructors are always declared to be public. One of the few reasons to create a private/protected constructor is if you specifically want to PREVENT people from creating an object using the new operator. However, this is a pretty advanced usage of private, and constructors - don't worry about using it here
Notice also that while this appears to be a method (because it is - constructors are essentially just methods that C# will call for you), there is no return type specified - not even void! This is because constructors are not allowed to return a value, no matter what. In order to help enforce this, the C# language makes sure that we can't even specify a return value.
|
public
SoccerBall()
{
m_TheBall = new
XNACS1Circle(new
Vector2(INIT_X, INIT_Y), RADIUS);
m_TheBall.Texture = "SoccerBall";
}
///
<summary>
/// Make
it look like the soccer ball is rolling
///
</summary>
public void
RollTheSoccer(float dx)
{
m_TheBall.RotateAngle -= dx * ROLL_ANGLE;
m_TheBall.CenterX += dx * MOVE_UNIT;
XNACS1Base.World.ClampAtWorldBound(m_TheBall);
}
/// <summary>
/// Move
the ball by the input delta.
/// </summary>
/// <param name="delta">amount
to move the ball</param>
|
m_TheBall.RotateAngle -= dx * ROLL_ANGLE;
|
public
void RollTheSoccer(float
dx)
{
m_TheBall.RotateAngle -= dx * ROLL_ANGLE;
m_TheBall.CenterX += dx * MOVE_UNIT;
XNACS1Base.World.ClampAtWorldBound(m_TheBall);
}
///
<summary>
/// Move
the ball by the input delta.
///
</summary>
///
<param name="delta">amount
to move the ball</param>
public void
MoveTheBall(Vector2 delta)
{
m_TheBall.Center += delta;
}
public
Vector2 CurrentPosition()
{
return
m_TheBall.Center;
}
|
|
public
void MoveTheBall(Vector2 delta)
{
m_TheBall.Center += delta;
}
///
<summary>
///
///
</summary>
///
<returns>return
the current center position of the ball</returns>
public
Vector2 CurrentPosition()
{
return m_TheBall.Center;
}
/// <summary>
/// remove
the ball from auto draw set. The ball will not show up anymore!
/// </summary>
public
void RemoveFromDraw()
{
m_TheBall.RemoveFromAutoDrawSet();
}
|
|
/// <summary>
///
/// </summary>
/// <returns>return the current center
position of the ball</returns>
public
Vector2 CurrentPosition()
{
return
m_TheBall.Center;
}
///
<summary>
/// remove
the ball from auto draw set. The ball will not show up anymore!
///
</summary>
public void
RemoveFromDraw()
{
m_TheBall.RemoveFromAutoDrawSet();
}
}
|
2. Game1.cs:
Now that we've moved all the code that pertains to the soccer ball into the separate class (and file), you'll notice how simple this file becomes. In addition to the standard, boilerplate code at the top of the file, the actual program has become trivial. InitializeWorld sets the dimensions of the world, and then creates a SoccerBall object. UpdateWorld creates a new SoccerBall object (if the 'A' button has been pressed), and then has a collection of single-line updates to the SoccerBall object. Because the SoccerBall object is responsible for actually updating (and drawing) the image, this code is very simple.
You'll notice that what we've done with the code (between Game1.cs and SoccerBall.cs) is to rearrange the existing logic into two separate classes, so that the overall program is now better (it's more object oriented). This is the essence of (code) refactoring - to refactor code means to rearrange it so that the resulting code is more desirable in some way.
|
///
<summary>
/// This is the main type for your game
///
</summary>
public
class
Game1 : XNACS1Base
{
//
SoccerBall m_TheBall;
// This is the instance to the
soccer ball.
|
|
protected
override void
InitializeWorld()
{
World.SetWorldCoordinate(new
Vector2(0, 0), 100.0f);
// Must create the ball!
m_TheBall = new
SoccerBall();
}
|
|
protected
override void UpdateWorld()
{
if (GamePad.ButtonBackClicked())
this.Exit();
if (GamePad.ButtonAClicked())
{
// If we do not remove existing
ball, we will
// simply get more and more each
time we recreate!
// SHOULD UN-Comment the following
line!!
// m_TheBall.RemoveFromDraw();
m_TheBall = new SoccerBall();
}
m_TheBall.MoveTheBall(GamePad.ThumbSticks.Left);
m_TheBall.RollTheSoccer(GamePad.ThumbSticks.Right.X);
EchoToTopStatus("Left Thumb Stick to move the ball, right thumb stick to roll the ball");
EchoToBottomStatus("Current ball
position:" + m_TheBall.CurrentPosition());
}
|
// m_TheBall.RemoveFromDraw();
m_TheBall = new SoccerBall();
FURTHER EXERCISES: