Pre-requisite: it is assumed
that you have read through
the prior tutorials, and are familiar with basic I/O concepts, and how to
echo messages to the top and bottom of the screen in an XNACS1Lib-based
game, including data about the left thumbstick.
It may be useful to refer to refer to
the diagram illustrating the Draw-Update loop
(which explains that your
initialization code will be called once, and then the XNACS1Lib will repeatedly call your Update method
(after which it will Draw the screen) ), in order to have a clear picture of how your game executes.
Goals:
To gain experience writing code that interacts with users via the
XNACS1Lib
library.
In this tutorial, we will:
Examine some simple,
instance
variables
Examine some
instance
variables that are non-simple, in the sense
of being composed of several simple variables
Examine how to initialize instance variables in the same statement that
declares them
Download and unzip the zip file and you will see an
ExampleProgram
folder. Open the
ExampleProgram
folder, the
EXE
folder contains the compiled program and you can double click on the
.sln
file to work with the source code.
Once we compile and run this project, the program displays "
ThumbStick
Position X:0 Position Y: 0
" at the top of the screen, and "
Thumbstick
as Vector2: {X:0 Y:0}
" at the bottom of the screen.
If you move the left thumbstick towards the right, you'll see the X value
change to be a positive number in response (on both the top and the bottom of the screen). If you move
the left thumbstick towards the left, you'll see the X value change to a
negative value. Similarly with moving the left thumbstick forwards & backwards
(sometimes described as "up and down"), you'll see the Y values change
appropriately.
2. Background
:
The output of this tutorial is almost exactly the same as
the output from the prior tutorial. For this tutorial, we will continue to
focus on variables, instead of output. Since we examined local variables in detail in the prior
tutorial, we'll be focusing on instance variables in this tutorial.
Because different information needs to be kept around for
different lengths of time, there are different types of variables. For
example, a player's score should exist throughout the entire game, so we will
need to use a long-lived
instance variable
to keep track of the score. What we would like
to have is a variable that will
exist through the entire lifetime of the game
. We will use an
instance variable
to store this long-lived data. What this means is
that the space in the computer's memory that stores the instance variable will
be found when the Game1 object is first created, and will continue to exist
until the Game1 object is destroyed. Since our Game1 object
is
our
game, this means that any instance variables we create here will be available
throughout the game.
3. Examining The Program:
Let's examine the
C# source code that produces the behavior we see on-screen
Declaring the instance variables
We need to declare our instance variables before we can use them.
Instance
variables are placed
inside the class
,
but
outside any
methods
(of the class).
public
class Game1 :
XNACS1Base
{
// Note that instance variables go INSIDE the class (i.e. below the
{ for the class )
// and OUTSIDE any methods.
// Here at the top of the class is a good place for them
private
float
thumbX;
private
float
thumbY;
private
Vector2
thumb;
public Game1() :
base (new Vector2
(0,
0), 100.0f)
{
}
The instance variable declaration itself looks almost exactly like the
declaration for a local variable. For example, the declaration
private
float
thumbX;
has a type (
float
),
a name (
thumbX
), and ends with a
semi-colon (
float
thumbX
;
),
just like a local variable declaration does. What's new is that we now
also have the word
private
out in front of all that.
private
means that we can only use this instance
variable within this class (i.e., at any point in this file after the
open-curly-brace immediately below the line
public
class
Game1
:
XNACS1Base
,
and above the matching curly-brace for that class)
Your other options (instead of
private
) are
public
and
protected
.
Since most of our games will be completely
contained inside of the Game1 class, it (normally) won't really matter which
one you choose. As you'll learn later on (once you get to object
oriented programming), this choice can actually be important to understand.
A good rule of thumb (both now, and once
you learn object oriented programming) is to make instance variables
private, unless you specifically have a reason to make them something else.
These variables are created at the same
time that the Game1 object is created. Since XNA creates the Game1
object when the program starts, this means that your instance variables are
created when the program starts.
You can initialize instance variables on
the same line that they're created. So you could write
private
float
thumbX = 0.0f;
, if you wanted to.
This can be useful for simplistic initialization ('put 0 into this
variable'), but isn't very useful overall because of two major reasons.
First, you can only assign values to the variable that C# can calculate when
the program is being compiled - you're not allowed to say that thumbX is
assigned the X value of the hero's current location, or the player's current
score, or anything else that changes as the game goes on. Second,
there's no way to pass additional data in to use when you're initializing
the variable, unlike with the InitializeWorld method.
For these reasons, you are encouraged to do
all your initialization in the InitializeWorld method,
as demonstrated below.
InitializeWorld():
We told C# to create instance variables for our Game1. It's
important that we give our variables well-defined values before we use them,
like so:
protected
override
void
InitializeWorld()
{
World.SetWorldCoordinate(
new
Vector2
(0,0),
100.0f);
// Initialize the floating point values
thumbX = 0.0f;
thumbY = 0.0F;
// Also initialize the Vector2 named thumb
thumb =
new
Vector2
(0.0f,
0.0f);
}
The first thing that InitializeWorld does is to call SetWorldCoordinate
using the same parameters that we used in prior tutorials, since we want the
lower-left corner to still be at (0,0), and since we want the width of the
window to be 100 units wide:
SetWorldCoordinate(
new
Vector2
(0,0),
100.0f);
Notice that we initialize the simple types by simply assigning a value (
thumbX
= 0.0f;
).
Notice also that we use the number
0.0f
- the
f
means that this is a
float
.
If we leave off the
f
, and list a whole number (a number without
anything after the decimal point) (example:
thumbX =
0
;
), then C#
will assume that 0 is actually an
int
. This
will actually work out ok, because C# can convert integers to floats
automatically. Even though this will compile, it's better to put
the
f
on, so that it's clear what type of number you intend to
work with.
If we leave off the
f
, and list a decimal number (example:
thumbX = 0
.0
;
), then
C# will assume that 0 is actually a
double
- a
double-precision floating point number. If you do this, this will NOT
compile, because a double requires twice as much space as a float, and
therefore C# cannot automatically convert from a double (the 'larger', more
precise number) to a float (the 'smaller', less precise number). For
this reason, if you have a decimal number (e.g.,
0.0
), you need to put the
f
on
(e.g., 0.0
f
), in order for your code to
compile.
You can use lowercase f or uppercase F, as demonstrated above.
Normally, you pick one and use that one consistently. But this is
mostly a stylistic choice, so use whichever one looks better to you.
Unlike local variables, instance variables
are (technically) assigned a default value. Microsoft provides
a
list of default values that the various C# simple types have here
. This
means that even if you didn't assign 0.0f to thumbX, it would still have the
value 0.0f, since that is the default value for a float.
It's still a good idea to initialize your
variables before using them, just so that it's clear to you what value you
intended the instance variables to have.
Everything that we've done up to this point
- declaring instance variables and initializing them
-
forms the
"Initialize" step of the program
. What's important to
remember about the initialize step is that it's only done once, and because
of that, it's used to set up variables that the program will use later on.
UpdateWorld():
At this stage, we'll make use of the UpdateWorld
method in order to have our Update step actually do something.
This is where we read the current state of the left thumbstick, and then
display a message at the top of the screen telling the user about the
thumbstick.
There are a number of new things here, for right now, we'll start by
looking at the first Version, and ignore everything else in the method:
protected
override
void
UpdateWorld()
{
if
(GamePad.ButtonBackClicked())
this
.Exit();
thumb =
GamePad.ThumbSticks.Left;
thumbX =
GamePad.ThumbSticks.Left.X;
thumbY =
GamePad.ThumbSticks.Left.Y;
EchoToTopStatus(
"ThumbStick
Position X:"
+ thumbX +
" Position Y:"
+ thumbY);
EchoToBottomStatus(
"ThumbStick
as Vector2: "
+
thumb);
}
The actual assignment statements copy the values on the right hand side,
into the variables on the left-hand side, exactly as in the previous
tutorials.
Notice that because we start the UpdateWorld step by assigning new
values to the instance variables, the values that we used to initialized the
instance variables are lost. None-the-less, it's still a good idea to
get in the habit of initializing your instance variables, so that you don't
forget to initialize them when you have to.
Everything that we've done up to this point
- declaring instance variables and initializing them
-
forms the
"Update" step of the program
. What's important to
remember about the Update step is that it's done many times per second, and therefore is used to change the
game (including what's displayed on the screen) as the game goes on.
FURTHER EXERCISES::
Start from a blank starter project (1000.201, if you need it), and re-do
the code from memory as much as possible. On your first try, do what
you can, and keep the above code open so that when you get stuck, you can
quickly look up what you forgot (and that after you finish a line, so that
you can compare your line to the 'correct' line). On the next try, do
the same thing, but try to use the finished code less. Repeat this
until you can type everything, without referring the tutorial's code.
Repeat this exercise daily for several days, so that you really get the
hang of this. As you go on, periodically review this by re-doing this
exercise.
State clearly what the difference between local and instance variables
are, in terms of the lifetime of the variables. Given an example of a
situation where you'd want to use local variable (and also explain why an instance variable isn't a
great choice for that situation), then give a situation where you'd want to
(or need
to) use an instance variable (and also explain why a local variable isn't a
great choice for that situation)
When talking about variables, we can discuss their
scope
.
Loosely put, a variable's scope is the region of source code in which we can
use the variable (i.e., no compiler errors and no logical errors result from
using the variable).
In your own words, describe the scope of instance variables.
Let's say that you have a program, which is included in the box below.
For each comment, you should be able to describe whether an instance
variable (such as
thumbX
) is in scope, or not.
// Location A: Is thumbX in scope here?
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;
// Location B:
Is thumbX in scope here?
namespace
InstanceVariableAssignment
{
// Location C: Is thumbX in scope here?
public
class
Game1
:
XNACS1Base
{
private
float
thumbX;
private
float
thumbY;
private
Vector2
thumb;
// Location D: Is thumbX in scope here?
public
Game1()
:
base
(
new
Vector2
(0,
0), 100.0f)
{
// Location E: Is thumbX in scope here?
}
protected
override
void
InitializeWorld()
{
// Location F: Is thumbX in scope here?
thumbX = 0.0f;
thumbY = 0.0F;
thumb =
new
Vector2
(0.0f,
0.0f);
// Location G: Is thumbX in scope here?
}
protected
override
void
UpdateWorld()
{
// Location H: Is thumbX in scope here?
if
(GamePad.ButtonBackClicked())
this
.Exit();
thumb = GamePad.ThumbSticks.Left;
thumbX = GamePad.ThumbSticks.Left.X;
thumbY = GamePad.ThumbSticks.Left.Y;
// Location I: Is thumbX in scope here?
EchoToTopStatus(
"ThumbStick
Position X:"
+ thumbX +
" Position Y:"
+ thumbY);
EchoToBottomStatus(
"ThumbStick
as Vector2: "
+
thumb);
}
}
// Location J: Is thumbX in scope here?
}
Starting with the project that's used by the above tutorial, try leaving out the InitializeWorld code, and see if
the program still works. Feel free to comment the method out, so that
you cant put it back in if you want.
Why does it still work (or not work)?
Why is
it good to put in the initialization even if it's not strictly required?
Starting with the project that's used by the above tutorial, try
removing the
private
word on the instance variable declaration. Does the program still work
(i.e., does it compile? Does it run without crashing? Does it
produce the same/correct results?).
Writing Code On Your Own: The Right Thumbstick
Start this exercise using
Exercise_
6
's starter project
,
which is a nearly identical copy of the project that was used in the above
tutorial. Change the program so that instead of updating the top and
bottom status bars based on the right thumbstick, instead of the left
thumbstick. You can access the right thumbstick using the phrase ,
GamePad.ThumbSticks.Right
in the
appropriate places.