Getting Things Moving
After completing this chapter, you will be able to:
As you saw at the end of Chapter 3, “2D Graphics, Coordinates and Game State,” the behavior of your game objects has become more complex. You may have noticed this in the Simple Game Object and Simple Game State projects, in which you created soccer ball and basketball game objects. The behavior given to those objects allows them to move around the game window, detecting and responding to collisions with the window’s edges.
This chapter covers the basic math you need to implement some common behaviors and concepts used across many games, including concepts for projectiles, velocity, and more advanced collision-based behaviors.
Rotating textures
Understanding how a texture rotates is a good place to start, because texture rotation can be useful for a variety of tasks, including orienting your objects in the game world, adding rolling support to an object, modifying the direction an object faces, and creating more natural collision reactions. However, before you can implement behaviors like this, it’s worth exploring the basics of rotation, which you’ll do in this next project.
The Rotate Textured Primitive project
This project demonstrates how to move an object around the game window and rotate it—either clockwise or counterclockwise. It also demonstrates how to select the Ball object (an instance of TexturedPrimitive) or Logo object (another instance of TexturedPrimitive) and scale it. You can see an example of this project running in Figure 4-1.
Figure 4-1. Running the Rotate Textured Primitive project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Modifying the TexturedPrimitive class
Note If you need to review or refresh the concept of radians, refer to http://reference.wolfram.com/legacy/teachersedition/Teacher/AlgebraTrigMathematica24.Radiansanddegrees.html. If the cognitive leap from degrees to radians is cumbersome, the library provides convenient helper methods called MathHelper.ToRadians() and MathHelper.ToDegrees().
public class TexturedPrimitive
{
...
protected float mRotateAngle; // In radians, clockwise rotation
...
}
public TexturedPrimitive(String imageName, Vector2 position, Vector2 size)
{
...
mRotateAngle = 0f;
}
public TexturedPrimitive(String imageName)
{
...
mRotateAngle = 0f;
}
public float RotateAngleInRadian
{
get { return mRotateAngle; }
set { mRotateAngle = value; }
}
public void Update(Vector2 deltaTranslate, Vector2 deltaScale, float deltaAngleInRadian)
{
mPosition += deltaTranslate;
mSize += deltaScale;
mRotateAngle += deltaAngleInRadian;
}
Note As you may already know, the term delta is used as a synonym for change in mathematics. Therefore, when a parameter is named deltaAngleInRadian, you can think of it as the angle of change, in radians.
virtual public void Draw()
{
// Define location and size of the texture
Rectangle destRect = Camera.ComputePixelRectangle(Position, Size);
// Define the rotation origin
Vector2 org = new Vector2(mImage.Width / 2, mImage.Height / 2);
// Draw the texture
Game1.sSpriteBatch.Draw(
mImage,
destRect, // Area to be drawn in pixel space
null,
Color.White,
mRotateAngle, // Angle to rotate (clockwise)
org, // Image reference position
SpriteEffects.None, 0f);
}
// Work with TexturedPrimitive
TexturedPrimitive mBall, mUWBLogo;
TexturedPrimitive mWorkPrim;
public GameState()
{
// Create the primitives
mBall = new TexturedPrimitive("Soccer",
new Vector2(30, 30), new Vector2(10, 15));
mUWBLogo = new TexturedPrimitive("UWB-JPG",
new Vector2(60, 30), new Vector2(20, 20));
mWorkPrim = mBall;
}
public void UpdateGame()
{
#region Select which primitive to work on
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mWorkPrim = mBall;
else if (InputWrapper.Buttons.B == ButtonState.Pressed)
mWorkPrim = mUWBLogo;
#endregion
#region Update the work primitive
float rotation = 0;
if (InputWrapper.Buttons.X == ButtonState.Pressed)
rotation = MathHelper.ToRadians(1f); // 1 degree pre-press
else if (InputWrapper.Buttons.Y == ButtonState.Pressed)
rotation = MathHelper.ToRadians(-1f); // 1 degree pre-press
mWorkPrim.Update(
InputWrapper.ThumbSticks.Left,
InputWrapper.ThumbSticks.Right,
rotation);
#endregion
}
public void DrawGame()
{
mBall.Draw();
FontSupport.PrintStatusAt(
mBall.Position,
mBall.RoateAngleInRadian.ToString(),
Color.Red);
mUWBLogo.Draw();
FontSupport.PrintStatusAt(
mUWBLogo.Position,
mUWBLogo.Position.ToString(),
Color.Black);
FontSupport.PrintStatus(
"A-Soccer B-Logo LeftThumb:Move RightThumb:Scale X/Y:Rotate",
null);
}
Observing and testing the results
Build and run the project. Does it behave as you expected? If so, try rotating the textures clockwise and counterclockwise. Notice that they rotate around the center of the image, because we defined the origin or reference position of the image. You’ll see more details about this important concept later in this book.
The next concept we’ll cover is vectors. Euclidean vectors are used across many fields of study, including mathematics, physics, computer science, and engineering. They are particularly important in games; nearly every game uses vectors in one way or another. Because they are used so extensively, this section is devoted to understanding and utilizing vectors in games.
One of the most common uses for vectors is to represent an object’s displacement and direction. This can be done easily, because a vector is defined by its size and direction. Using only this small amount of information, you can represent attributes such as an object’s velocity or acceleration. If you have the position of an object, its direction, and its velocity, then you have sufficient information to move it around the window without user input.
Note A vector’s size is often referred to as its length, or magnitude.
Before going any further, it is important to review the concepts of a vector, starting with how you can define one. A vector can be specified using two points. For example, given the arbitrary position Pa = (Xa , Ya ) and the arbitrary position Pb = (Xb , Yb ), you can define the vector from Pa to Pb or (Vab) as Pb –Pa. You can see this represented in the following equations and Figure 4-2:
Figure 4-2. A vector being defined by two points
Now that you have a vector Vab, you can easily ascertain its length (or size) and its direction. A vector’s length is equal to the distance between the two points that created it. In this example, the length of Vab is equal to the distance between Pa and Pb, while the direction of Vab goes from Patoward Pb.
In MonoGame, the Vector2 class implements the functionality of a two-dimensional (2D) vector. Conveniently, you can also use the Vector2 class to represent positions in 2D space, such as a point. In the preceding example, Pa, Pb, and Vab can all be implemented as instances of the Vector2 class; however, Vab is the only mathematically defined vector. Pa and Pb represent positions, or points used to create a vector.
Recall that a vector can also be normalized. A normalized vector (also known as a unit vector) always has a size of 1. You can see a normalized vector represented in the following equation and Figure 4-3:
Figure 4-3. A vector being normalized
Vectors can also be rotated. If, for example, you have the vector Vr = (Xr , Yr ) and wish to rotate it by θ (radians), then you can use the equations shown following to derive Xr and Yr. Figure 4-4 shows the ro1tation of a vector being applied.
Figure 4-4. A vector being rotated by the angle theta (in radians)
Lastly, it is important to remember that vectors are defined by their direction and size; in other words, two vectors can be equal to each other independent of the locations of the vectors. Figure 4-5 shows two vectors (Va and Vbc) that are located at different positions but have the same direction and magnitude. Because their direction and magnitude are the same, these vectors are equal to each other. In contrast, the vector Vd is not the same because its direction and magnitude are different from the others.
Figure 4-5. Three valid vectors represented in 2D space with two vectors equal to each other
This project demonstrates how to represent and manipulate vectors within the game window. You can select and manipulate each of the vectors by changing its attributes, such as direction and size. The project shows vectors being represented in three different ways. Figure 4-6 shows an example of this project running.
Figure 4-6. Running the Show Vector project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resource, which can be found in the Chapter04SourceCodeImagesAndFontsUsed folder, into your content project before you begin:
Creating the ShowVector class
protected static Texture2D sImage = null; // Singleton for the class
private static float kLenToWidthRatio = 0.2f;
static private void LoadImage()
{
if (null == sImage)
ShowVector.sImage = Game1.sContent.Load<Texture2D>("RightArrow");
}
static public void DrawPointVector(Vector2 from, Vector2 dir)
{
...
}
Because this function is a bit lengthy, let’s look at each piece separately:
LoadImage();
Note The sign of the angle is positive when the rotation is counterclockwise.
To do this, you first normalize the given vector by dividing the vector with its length. With a normalized vector, the length is the size of the hypotenuse and has a value of 1, the value of the X component is the size of adjacent, and the value of the Y component is the size of the opposite. In this way, arc cosine on the normalized vector’s X value is the angle of rotation from the horizontal direction, as illustrated in the following image.
The sign of the Y value indicates if the rotation is counterclockwise (positive) or clockwise (negative). In the code that follows, you can see how the angle is found:
#region Step 4b. Compute the angle to rotate
float length = dir.Length();
float theta = 0f;
if (length > 0.001f)
{
dir /= length;
theta = (float)Math.Acos((double)dir.X);
if (dir.X < 0.0f)
{
if (dir.Y > 0.0f)
theta = -theta;
}
Else
{
if (dir.Y > 0.0f)
theta= -theta;
}
}
#endregion
Note The x-axis is used because the direction of the image used needs to be taken into account. The image used for this project points horizontally. If the image were pointed vertically, then the preceding computation would need to use the y-axis as the reference. Alternatively, you can rotate a vertically pointing image by 90 degrees before including it in the project.
#region Step 4c. Draw Arrow
// Define location and size of the texture to show
Vector2 size = new Vector2(length, kLenToWidthRatio * length);
Rectangle destRect = Camera.ComputePixelRectangle(from, size);
// destRect is computed with respect to the "from" position
// on the left side of the texture.
// We only need to offset the reference
// in the y from top left to middle left.
Vector2 org = new Vector2(0f, ShowVector.sImage.Height/2f);
Game1.sSpriteBatch.Draw(ShowVector.sImage, destRect, null, Color.White,
theta, org, SpriteEffects.None, 0f);
#endregion
#region Step 4d. Print status message
String msg;
msg = "Direction=" + dir + " Size=" + length;
FontSupport.PrintStatusAt(from + new Vector2(2, 5), msg, Color.Black);
#endregion
static public void DrawFromTo(Vector2 from, Vector2 to)
{
DrawPointVector(from, to - from);
}
static public Vector2 RotateVectorByAngle(Vector2 v, float angleInRadian)
{
float sinTheta = (float)(Math.Sin((double)angleInRadian));
float cosTheta = (float)(Math.Cos((double)angleInRadian));
float x, y;
x = cosTheta * v.X + sinTheta * v.Y;
y = -sinTheta * v.X + cosTheta * v.Y;
return new Vector2(x, y);
}
Now that you have implemented the ShowVector class, you can modify the GameState class to support the desired vectors.
Modifying the GameState class
// Size of all the positions
Vector2 kPointSize = new Vector2(5f, 5f);
// Work with TexturedPrimitive
TexturedPrimitive mPa, mPb; // The locators for showing Point A and Point B
TexturedPrimitive mPx; // to show same displacement can be applied to any position
TexturedPrimitive mPy; // To show we can rotate/manipulate vectors independently
Vector2 mVectorAtPy = new Vector2(10, 0); // Start with vector in the X direction;
TexturedPrimitive mCurrentLocator;
public GameState()
{
// Create the primitives
mPa = new TexturedPrimitive("Position",
new Vector2(30, 30), kPointSize, "Pa");
mPb = new TexturedPrimitive("Position",
new Vector2(60, 30), kPointSize, "Pb");
mPx = new TexturedPrimitive("Position",
new Vector2(20, 10), kPointSize, "Px");
mPy = new TexturedPrimitive("Position",
new Vector2(20, 50), kPointSize, "Py");
mCurrentLocator = mPa;
}
#region Step 3a. Change current selected vector
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mCurrentLocator = mPa;
else if (InputWrapper.Buttons.B == ButtonState.Pressed)
mCurrentLocator = mPb;
else if (InputWrapper.Buttons.X == ButtonState.Pressed)
mCurrentLocator = mPx;
else if (InputWrapper.Buttons.Y == ButtonState.Pressed)
mCurrentLocator = mPy;
#endregion
#region Step 3b. Move Vector
// Change the current locator position
mCurrentLocator.Position +=
InputWrapper.ThumbSticks.Right;
#endregion
#region Step 3c. Rotate Vector
// Left thumbstick-X rotates the vector at Py
float rotateYByRadian = MathHelper.ToRadians(
InputWrapper.ThumbSticks.Left.X);
#endregion
#region Step 3d. Increase/Decrease the length of vector
// Left thumbstick-Y increase/decrease the length of vector at Py
float vecYLen = mVectorAtPy.Length();
vecYLen += InputWrapper.ThumbSticks.Left.Y;
#endregion
#region Step 3e. Compute vector changes
// Compute the rotated direction of vector at Py
mVectorAtPy = ShowVector.RotateVectorByAngle(mVectorAtPy, rotateYByRadian);
mVectorAtPy.Normalize(); // Normalize vectorAtPy to size of 1f
mVectorAtPy *= vecYLen; // Scale the vector to the new size
#endregion
public void DrawGame()
{
// Drawing the vectors
Vector2 v = mPb.Position - mPa.Position; // Vector V is from Pa to Pb
// Draw Vector-V at Pa, and Px
ShowVector.DrawFromTo(mPa.Position, mPb.Position);
ShowVector.DrawPointVector(mPx.Position, v);
// Draw vectorAtPy at Py
ShowVector.DrawPointVector(mPy.Position, mVectorAtPy);
mPa.Draw();
mPb.Draw();
mPx.Draw();
mPy.Draw();
// Print out text message to echo status
FontSupport.PrintStatus(
"Locator Positions: A=" + mPa.Position +
" B=" + mPb.Position,
null);
}
With the basics of vectors out of the way, we can now address more vector-based game-specific concepts, starting with the idea of front direction. This simple idea stems from the need to understand which direction a game object is facing. For example, in a platform-style game, front direction determines which direction the hero character or enemy characters faces.
Front direction can be used to affect the gameplay or simply flip to a character’s texture upon a directional change. Adding support for front direction provides you with a convenient tool for achieving many gamelike actions.
This project demonstrates how to use the concept of front direction by controlling a rocket, the direction it faces, and therefore the direction of its projectiles. The rocket can move, rotate, and fire. The user’s goal is to catch a bee by hitting it with a fired projectile. If the projectile touches the bee, it causes the bee to respawn at a random position. Figure 4-7 shows an example of this project in action.
Figure 4-7. Running the Front Direction project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter04SourceCodeImagesAndFontsUsed folder, into your content project before you begin:
Modifying the GameState class
// Rocket support
Vector2 mRocketInitDirection = Vector2.UnitY; // This does not change
TexturedPrimitive mRocket;
// Support the flying net
TexturedPrimitive mNet;
bool mNetInFlight = false;
Vector2 mNetVelocity = Vector2.Zero;
float mNetSpeed = 0.5f;
// Insect support
TexturedPrimitive mInsect;
bool mInsectPreset = true;
// Simple game status
int mNumInsectShot;
int mNumMissed;
public GameState()
{
// Create and set up the primitives
mRocket = new TexturedPrimitive("Rocket", new Vector2(5, 5), new Vector2(3, 10));
// Initially the rocket is pointing in the positive y direction
mRocketInitDirection = Vector2.UnitY;
mNet = new TexturedPrimitive("Net", new Vector2(0, 0), new Vector2(2, 5));
mNetInFlight = false; // until user press "A", rocket is not in flight
mNetVelocity = Vector2.Zero;
mNetSpeed = 0.5f;
// Initialize a new insect
mInsect = new TexturedPrimitive("Insect", Vector2.Zero, new Vector2(5, 5));
mInsectPreset = false;
// Initialize game status
mNumInsectShot = 0;
mNumMissed = 0;
}
mRocket.RotateAngleInRadian += MathHelper.ToRadians(InputWrapper.ThumbSticks.Right.X);
mRocket.Position += InputWrapper.ThumbSticks.Left;
/// Set net to flight
if (InputWrapper.Buttons.A == ButtonState.Pressed)
{
mNetInFlight = true;
mNet.RotateAngleInRadian = mRocket.RotateAngleInRadian;
mNet.Position = mRocket.Position;
mNetVelocity = ShowVector.RotateVectorByAngle(
mRocketInitDirection,
mNet.RotateAngleInRadian) * mNetSpeed;
}
if (!mInsectPreset)
{
float x = 15f + ((float)Game1.sRan.NextDouble() * 30f);
float y = 15f + ((float)Game1.sRan.NextDouble() * 30f);
mInsect.Position = new Vector2(x, y);
mInsectPreset = true;
}
if (mNetInFlight)
{
mNet.Position += mNetVelocity;
if (mNet.PrimitivesTouches(mInsect))
{
mInsectPreset = false;
mNetInFlight = false;
mNumInsectShot++;
}
if ((Camera.CollidedWithCameraWindow(mNet) !=
Camera.CameraWindowCollisionStatus.InsideWindow))
{
mNetInFlight = false;
mNumMissed++;
}
}
public void DrawGame()
{
mRocket.Draw();
if (mNetInFlight)
mNet.Draw();
if (mInsectPreset)
mInsect.Draw();
// Print out text message to echo status
FontSupport.PrintStatus(
"Num insects netted = " + mNumInsectShot +
" Num missed = " + mNumMissed, null);
}
Observing the results
Now that you have implemented the necessary code, build and run the program. Test the program to see if it behaves as you would expect. Notice that the rocket points in the intended direction when the user uses the right thumbstick. This is because the front direction is correctly oriented with the image.
As you might have noticed when coding this project, the GameState class contains a large number of variables that belong to other objects within the game state, such as the rocket object (the TexturedPrimitive) and its initial direction vector. This is fine for demonstrating the front direction concept; however, in a real game, organizing your objects’ variables within the game state is considered bad practice. You’ll address this issue by creating a GameObject class in the next project.
As you saw at the end of the last section, using the GameState class to prototype and learn new concepts can be a quick and effective way to get a project up and running. However, as a developer it is your job to design your game’s architecture, which you should generally strive to make as straightforward and clean as possible, yet still maintain the ability to modify and expand it. When you include groups of related variables in the GameState class, the game’s architecture can become complex and convoluted as your game grows in scale and diversity. In addition, creating similar objects becomes a tedious process, as properties for each object also need to be declared. The simplest way to solve these issues, as you may know, is to create a new class with the desired behaviors and properties.
This section combines the properties and behaviors declared in the previous project’s GameState class into a new class called GameObject. These properties include front direction, velocity, and speed, and provide you with improved control over the movement of a game object.
Note Velocity can be thought of as the rate of change of the position of an object. Velocity is made up of both a direction and speed (size). If this is familiar, it is because velocity lends itself perfectly to a vector, which also has a direction and size.
This project demonstrates how to control a game object by moving it around the game window. You move the game object (the rocket) by adjusting its speed and front direction. Figure 4-8 shows an example of this project running.
Figure 4-8. Running the Game Object project
The project’s controls are as follows:
The goal of the project is as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter04SourceCodeImagesAndFontsUsed folder, into your content project before you begin:
Creating the GameObject class
public class GameObject : TexturedPrimitive
{
...
}
// Initial front direction (when RotateAngle is 0)
protected Vector2 mInitFrontDir = Vector2.UnitY;
// GameObject behavior: velocity
protected Vector2 mVelocityDir; // If not zero, always normalized
protected float mSpeed;
protected void InitGameObject()
{
mVelocityDir = Vector2.Zero;
mSpeed = 0f;
}
public GameObject(String imageName, Vector2 position,
Vector2 size, String label = null)
: base(imageName, position, size, label)
{
InitGameObject();
}
virtual public void Update()
{
mPosition += (mVelocityDir * mSpeed);
}
public Vector2 InitialFrontDirection
{
get { return mInitFrontDir; }
set
{
float len = value.Length();
// If the input vector is well defined
if (len > float.Epsilon)
mInitFrontDir = value / len;
else
mInitFrontDir = Vector2.UnitY;
}
}
public Vector2 FrontDirection
{
get
{
return ShowVector.RotateVectorByAngle(mInitFrontDir, RotateAngleInRadian); }
set
{
float len = value.Length();
if (len > float.Epsilon)
{
value *= (1f / len);
double theta = Math.Atan2(value.Y, value.X);
mRotateAngle = -(float)(theta-Math.Atan2(mInitFrontDir.Y, mInitFrontDir.X));
}
}
}
public Vector2 Velocity
{
get { return mVelocityDir * Speed; }
set
{
mSpeed = value.Length();
if (mSpeed > float.Epsilon)
mVelocityDir = value/mSpeed;
else
mVelocityDir = Vector2.Zero;
}
}
public float Speed
{
get { return mSpeed; }
set { mSpeed = value; }
}
public Vector2 VelocityDirection
{
get { return mVelocityDir; }
set
{
float s = value.Length();
if (s > float.Epsilon)
{
mVelocityDir = value / s;
}
else
mVelocityDir = Vector2.Zero;
}
}
Now that the GameObject class is complete and provides the necessary properties and behaviors, you can modify the GameState class to use them within the game.
Vector2 kInitRocketPosition = new Vector2(10, 10);
// Rocket support
GameObject mRocket;
// The arrow
GameObject mArrow;
public GameState()
{
mRocket = new GameObject("Rocket", kInitRocketPosition, new Vector2(3, 10));
mArrow = new GameObject("RightArrow", new Vector2(50, 30), new Vector2(10, 4));
// Initially pointing in the x direction
mArrow.InitialFrontDirection = Vector2.UnitX;
}
#region Step 3a. Control and fly the rocket
mRocket.RotateAngleInRadian +=
MathHelper.ToRadians(InputWrapper.ThumbSticks.Right.X);
mRocket.Speed += InputWrapper.ThumbSticks.Left.Y * 0.1f;
mRocket.VelocityDirection = mRocket.FrontDirection;
if (Camera.CollidedWithCameraWindow(mRocket) !=
Camera.CameraWindowCollisionStatus.InsideWindow)
{
mRocket.Speed = 0f;
mRocket.Position = kInitRocketPosition;
}
mRocket.Update();
#endregion
#region Step 3b. Set the arrow to point toward the rocket
Vector2 toRocket = mRocket.Position - mArrow.Position;
mArrow.FrontDirection = toRocket;
#endregion
public void DrawGame()
{
mRocket.Draw();
mArrow.Draw();
// print out text message to echo status
FontSupport.PrintStatus(
"Rocket Speed(LeftThumb-Y)=" + mRocket.Speed +
" VelocityDirection(RightThumb-X):" +
mRocket.VelocityDirection, null);
FontSupport.PrintStatusAt(mRocket.Position, mRocket.Position.ToString(), Color.White);
}
Observing the results
By grouping behaviors into a new class rather than declaring multiple variables within the game state, you have created a better-defined object and simplified your project by splitting it into more manageable pieces. You can now instantiate several of these objects without increasing the complexity of your code drastically. In upcoming examples, you’ll see that game objects can also become quite complex; however, by isolating them within a class, you can effectively hide much of the complexity. This makes it easier for you to use the object within the game and to modify the object’s overall behavior.
With game objects now moving around the screen, you can now look at the more specific behavior known as chasing. Chasing behavior is useful for many tasks within games. Some of the most common are projectile-based or bullet-based behaviors and simple enemy artificial intelligence (AI).
The goal of a chasing object is usually to catch the game object that it is targeting. This requires programmatic manipulation of the chaser’s front direction and velocity so it can home in on its target. However, it is generally important to avoid implementing a chaser that has perfect aim and always hits its target—because if the projectile always hits its target and characters are unable to avoid being hit, the challenge will essentially be removed from the game. However, this does not mean you should not implement a perfect chaser if your game design requires it. You’ll implement a chaser in the next project.
This project demonstrates how to control a rocket game object using both thumbsticks to control the rocket’s speed and direction. Pressing the A button shoots a snake that chases the rocket. You can then manipulate the rocket and attempt to avoid being hit. Figure 4-9 shows an example of this project running.
Figure 4-9. Running the Chaser Object project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Before digging into the implementation of chasing behavior, you’ll review the mathematics required to correctly rotate or turn a game object during a chase. The two concepts you need to understand for this operation are the dot product and the cross product.
The dot product of two normalized vectors provides you with the means to find the angle between those vectors. For example, take a look at the following statements.
Given the following:
Then the following is true:
Note If you need to review or refresh the concept of a dot product, refer to http://www.mathsisfun.com/algebra/vectors-dot-product.html.
Additionally, if both vectors V1 and V2 are normalized, then V1 ⋅ V2 = cos θ. You can see this concept reinforced in Figure 4-10.
Figure 4-10. The angle between two vectors, which can be found through the dot product
Lastly, it is also important to recognize that if V1 ⋅ V2 = 0 , then the two vectors are perpendicular.
The cross product of two vectors produces a vector that is orthogonal, or perpendicular, to both of the original vectors. In 2D games, where the 2D dimensions lie flat on the screen, the cross product results in a vector that either points inward (toward the screen) or outward (away from the screen). This may seem odd, because it’s not intuitive that crossing two vectors in 2D or XY space would result in a vector that lies in the third dimension, or the Z space. However, this vector is essential for the sole purpose of determining whether the game object needs to rotate in the clockwise or counterclockwise direction. Let’s take a detailed look at the following statements to clarify the mathematics involved.
Given the following:
Then the following is true:
(V1 × V2) is equal to a vector perpendicular to both V1 and V2.
Note If you need to review or refresh the concept of a cross product, refer to http://www.mathsisfun.com/algebra/vectors-cross-product.html.
Additionally, you know that the cross product of two vectors in the XY space results in a vector in the Z direction. Lastly, when V1 crosses V2 > 0, you know that V1 is in the clockwise direction from V2 ; similarly, when V1 × V2 < 0, you know that V1 is in the counterclockwise direction. Figure 4-11 should help clarify this concept.
Figure 4-11. The cross product on two vectors
Creating the ChaserGameObject class
public class ChaserGameObject : GameObject
{
...
}
// The target to go toward
protected TexturedPrimitive mTarget;
// Have we hit the target yet?
protected bool mHitTarget;
// How rapidly the chaser homes in on the target
protected float mHomeInRate;
public ChaserGameObject(String imageName, Vector2 position, Vector2 size,
TexturedPrimitive target) : base(imageName, position, size, null)
{
Target = target;
mHomeInRate = 0.05f;
mHitTarget = false;
mSpeed = 0.1f;
}
#region Step 4a.
if (null == mTarget)
return;
// Move the GameObject in the velocity direction
base.Update();
#endregion
#region Step 4b.
mHitTarget = PrimitivesTouches(mTarget);
if (!mHitTarget)
{
#region Calculate angle
Vector2 targetDir = mTarget.Position - Position;
float distToTargetSq = targetDir.LengthSquared();
targetDir /= (float) Math.Sqrt(distToTargetSq);
float cosTheta = Vector2.Dot(FrontDirection, targetDir);
float theta = (float)
Math.Acos(cosTheta);
#endregion
#region Calculate rotation direction
if (theta > float.Epsilon)
{
// Not quite aligned ...
Vector3 fIn3D = new Vector3(FrontDirection, 0f);
Vector3 tIn3D = new Vector3(targetDir, 0f);
Vector3 sign = Vector3.Cross(tIn3D, fIn3D);
RotateAngleInRadian += Math.Sign(sign.Z) * theta * mHomeInRate;
VelocityDirection = FrontDirection;
}
#endregion
}
#endregion
public float HomeInRate { get { return mHomeInRate; } set { mHomeInRate = value; } }
public bool HitTarget { get { return mHitTarget; } }
public bool HasValidTarget { get { return null != mTarget; } }
public TexturedPrimitive Target
{
get { return mTarget; }
set
{
mTarget = value;
mHitTarget = false;
if (null != mTarget)
{
FrontDirection = mTarget.Position - Position;
VelocityDirection = FrontDirection;
}
}
}
With the ChaserGameObject now complete, all that is left is to update the GameState class to use its behavior.
ChaserGameObject mChaser;
// Simple game status
int mChaserHit, mChaserMissed;
public GameState()
{
...
mChaser = new ChaserGameObject("Chaser", Vector2.Zero, new Vector2(6f, 1.7f), null);
// Initially facing in the negative x direction
mChaser.InitialFrontDirection = -Vector2.UnitX;
mChaser.Speed = 0.2f;
mChaserHit = 0;
mChaserMissed = 0;
}
public void UpdateGame()
{
...
#region 3. Check/launch the chaser!
if (mChaser.HasValidTarget)
{
mChaser.ChaseTarget();
if (mChaser.HitTarget)
{
mChaserHit++;
mChaser.Target = null;
}
if (Camera.CollidedWithCameraWindow(mChaser) !=
Camera.CameraWindowCollisionStatus.InsideWindow)
{
mChaserMissed++;
mChaser.Target = null;
}
}
if (InputWrapper.Buttons.A == ButtonState.Pressed)
{
mChaser.Target = mRocket;
mChaser.Position = mArrow.Position;
}
#endregion
}
public void DrawGame()
{
mRocket.Draw();
mArrow.Draw();
if (mChaser.HasValidTarget)
mChaser.Draw();
// Print out text message to echo status
FontSupport.PrintStatus("Chaser Hit=" + mChaserHit + " Missed=" + mChaserMissed, null);
}
Observations
Now that the project is completed, build and run the program. Test to see whether the chasing object behaves as expected. Additionally, if everything is functioning as expected, try modifying the mHomeInRate variable within the chaser to a higher or lower value. Adjusting this variable should make the chaser itself more or less accurate.
Summary
This chapter has provided basic information about working with rotating textures. You should now be familiar with positive and negative rotation directions, working with the rotation reference position (the pivot), and accomplishing specific rotation goals by manipulating the pivot position.
You also reviewed vectors in 2D space. A vector is defined by its direction and magnitude. Vectors are convenient for describing defined displacements (velocities). You reviewed some foundational vector operations, including normalization of a vector, and how to calculate dot and cross products. You worked with these operators to implement the front-facing direction capability and create simple autonomous behaviors such as pointing toward a specific object and chasing.
Lastly, and as in previous chapters, you have seen that by properly abstracting common behaviors into game objects, you can greatly reduce the complexity of your game implementation. In the next chapter, you’ll continue investigating interesting game object behaviors in the form of more accurate collision determination and simple physics simulations.
Quick reference
To |
Do this |
---|---|
Rotate a TexturedPrimitive object | Ensure the rotation is represented as radians (by calling the MathHelper.ToRadians() function to convert from degrees to radians) and set the rotation by calling TexturedPrimitive.RotateAngleInRadian. |
Draw a vector from position Pa to Pb | Call ShowVector.DrawFromTo(Pa, Pb). |
Draw a vector V at position Pa | Call ShowVector.DrawPointVector(Pa, V). |
Visualize the rotation of a vector V by rotation theta | Once again, make sure theta is in radians, and then call ShowVector.RotateVectorByAngle(V, theta). |
Compute a vector from position Pa to Pb | Use Vector2 v = Pb - Pa;. |
Compute the size of a given vector V | Use float VectorSize = V.Length();. |
Normalize a given vector Va | Use Va.Normalize();. |
Compute the angle between vectors Va and Vb | Use the following: Va.Normalize(); // Remember vectors must be normalized Vb.Normalize(); // For dot product to return cosine of angle float angle = (float) Math.Acos(Vector2.dot(Va, Vb)) |
Compute the cross product between vectors Va and Vb | Use the following: Vector3 Vx = new Vector3(Va, 0f); // Cross product is in 3D space Vector3 Vy = new Vector3(Vb, 0f); Vector3 crossResult = Vector3.Cross(Vx, Vy); |
Compute whether you should turn clockwise or counterclockwise to rotate from Va to Vb | Use the following (continue from the preceding code): if (Math.Sign(crossResult.Z) > 0) Counterclockwise rotation if (Math.Sign(crossResult.Z) < 0) Clockwise rotation |
Set the initial front-facing direction of a GameObject | Set GameObject.InitialFrontDirection. |
Set or get the current front-facing direction of a GameObject | Set or get GameObject.FrontDirection. |
Set or get the velocity of a GameObject | Set or get GameObject.VelocityDirection and GameObject.Speed. |
Implement home-in chasing | Instantiate ChaserGameObject and set ChaserGameObject.Target to the appropriate target. |