#region File Description //----------------------------------------------------------------------------- // Game.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; #if ANDROID using Android.App; #endif using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; using Microsoft.Xna.Framework.Storage; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Media; #endregion namespace ChaseAndEvade { /// /// Sample showing how to implement simple chase, evade, and wander AI behaviors. /// The behaviors are based on the TurnToFace function, which was explained in /// AI Sample 1: Aiming. /// public class ChaseAndEvadeGame : Game { /// /// TankAiState is used to keep track of what the tank is currently doing. /// enum TankAiState { // chasing the cat Chasing, // the tank has gotten close enough that the cat that it can stop chasing it Caught, // the tank can't "see" the cat, and is wandering around. Wander } /// /// MouseAiState is used to keep track of what the mouse is currently doing. /// enum MouseAiState { // evading the cat Evading, // the mouse can't see the "cat", and it's wandering around. Wander } #region Constants // The following values control the different characteristics of the characters // in this sample, including their speed, turning rates. distances are specified // in pixels, angles are specified in radians. // how fast can the cat move? const float MaxCatSpeed = 7.5f; // how fast can the tank move? const float MaxTankSpeed = 5.0f; // how fast can he turn? const float TankTurnSpeed = 0.10f; // this value controls the distance at which the tank will start to chase the // cat. const float TankChaseDistance = 250.0f; // TankCaughtDistance controls the distance at which the tank will stop because // he has "caught" the cat. const float TankCaughtDistance = 60.0f; // this constant is used to avoid hysteresis, which is common in ai programming. // see the doc for more details. const float TankHysteresis = 15.0f; // how fast can the mouse move? const float MaxMouseSpeed = 8.5f; // and how fast can it turn? const float MouseTurnSpeed = 0.20f; // MouseEvadeDistance controls the distance at which the mouse will flee from // cat. If the mouse is further than "MouseEvadeDistance" pixels away, he will // consider himself safe. const float MouseEvadeDistance = 200.0f; // this constant is similar to TankHysteresis. The value is larger than the // tank's hysteresis value because the mouse is faster than the tank: with a // higher velocity, small fluctuations are much more visible. const float MouseHysteresis = 60.0f; #endregion #region Fields GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SpriteFont spriteFont; Texture2D tankTexture; Vector2 tankTextureCenter; Vector2 tankPosition; TankAiState tankState = TankAiState.Wander; float tankOrientation; Vector2 tankWanderDirection; Texture2D catTexture; Vector2 catTextureCenter; Vector2 catPosition; Texture2D mouseTexture; Vector2 mouseTextureCenter; Vector2 mousePosition; MouseAiState mouseState = MouseAiState.Wander; float mouseOrientation; Vector2 mouseWanderDirection; Random random = new Random (); #endregion #region Initialization public ChaseAndEvadeGame () { graphics = new GraphicsDeviceManager (this); Content.RootDirectory = "Content"; #if WINDOWS_PHONE graphics.SupportedOrientations = DisplayOrientation.Portrait; graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 800; TargetElapsedTime = TimeSpan.FromTicks(333333); #elif !MONOMAC graphics.PreferredBackBufferWidth = 320; graphics.PreferredBackBufferHeight = 480; #endif graphics.IsFullScreen = true; } /// /// Overridden from the base Game.Initialize. Once the GraphicsDevice is setup, /// we'll use the viewport to initialize some values. /// protected override void Initialize () { base.Initialize (); // once base.Initialize has finished, the GraphicsDevice will have been // created, and we'll know how big the Viewport is. We want the tank, cat // and mouse to be spread out across the screen, so we'll use the viewport // to figure out where they should be. Viewport vp = graphics.GraphicsDevice.Viewport; tankPosition = new Vector2 (vp.Width / 4, vp.Height / 2); catPosition = new Vector2 (vp.Width / 2, vp.Height / 2); mousePosition = new Vector2 (3 * vp.Width / 4, vp.Height / 2); } /// /// Load your graphics content. /// protected override void LoadContent () { // create a SpriteBatch, and load the textures and font that we'll need // during the game. spriteBatch = new SpriteBatch (graphics.GraphicsDevice); spriteFont = Content.Load ("Arial"); tankTexture = Content.Load ("tank"); catTexture = Content.Load ("cat"); mouseTexture = Content.Load ("mouse"); // once all the content is loaded, we can calculate the centers of each // of the textures that we loaded. Just like in the previous sample in // this series, the aiming sample, we want spriteBatch to draw the // textures centered on their position vectors. SpriteBatch.Draw will // center the sprite on the vector that we pass in as the "origin" // parameter, so we'll just calculate that to be the middle of // the texture. tankTextureCenter = new Vector2 (tankTexture.Width / 2, tankTexture.Height / 2); catTextureCenter = new Vector2 (catTexture.Width / 2, catTexture.Height / 2); mouseTextureCenter = new Vector2 (mouseTexture.Width / 2, mouseTexture.Height / 2); } #endregion #region Update and Draw /// /// Allows the game to run logic. /// protected override void Update (GameTime gameTime) { // handle input will read the controller input, and update the cat // to move according to the user's whim. HandleInput (); // UpdateTank will run the AI code that controls the tank's movement... UpdateTank (); // ... and UpdateMouse does the same thing for the mouse. UpdateMouse (); // Once we've finished that, we'll use the ClampToViewport helper function // to clamp everyone's position so that they stay on the screen. tankPosition = ClampToViewport (tankPosition); catPosition = ClampToViewport (catPosition); mousePosition = ClampToViewport (mousePosition); base.Update (gameTime); } /// /// This function takes a Vector2 as input, and returns that vector "clamped" /// to the current graphics viewport. We use this function to make sure that /// no one can go off of the screen. /// /// an input vector /// the input vector, clamped between the minimum and maximum of the /// viewport. private Vector2 ClampToViewport (Vector2 vector) { Viewport vp = graphics.GraphicsDevice.Viewport; vector.X = MathHelper.Clamp (vector.X, vp.X, vp.X + vp.Width); vector.Y = MathHelper.Clamp (vector.Y, vp.Y, vp.Y + vp.Height); return vector; } /// /// This function contains the code that controls the mouse. It decides what the /// mouse should do based on the position of the cat: if the cat is too close, /// it will attempt to flee. Otherwise, it will idly wander around the screen. /// /// private void UpdateMouse () { // first, calculate how far away the mouse is from the cat, and use that // information to decide how to behave. If they are too close, the mouse // will switch to "active" mode - fleeing. if they are far apart, the mouse // will switch to "idle" mode, where it roams around the screen. // we use a hysteresis constant in the decision making process, as described // in the accompanying doc file. float distanceFromCat = Vector2.Distance (mousePosition, catPosition); // the cat is a safe distance away, so the mouse should idle: if (distanceFromCat > MouseEvadeDistance + MouseHysteresis) { mouseState = MouseAiState.Wander; } // the cat is too close; the mouse should run: else if (distanceFromCat < MouseEvadeDistance - MouseHysteresis) { mouseState = MouseAiState.Evading; } // if neither of those if blocks hit, we are in the "hysteresis" range, // and the mouse will continue doing whatever it is doing now. // the mouse will move at a different speed depending on what state it // is in. when idle it won't move at full speed, but when actively evading // it will move as fast as it can. this variable is used to track which // speed the mouse should be moving. float currentMouseSpeed; // the second step of the Update is to change the mouse's orientation based // on its current state. if (mouseState == MouseAiState.Evading) { // If the mouse is "active," it is trying to evade the cat. The evasion // behavior is accomplished by using the TurnToFace function to turn // towards a point on a straight line facing away from the cat. In other // words, if the cat is point A, and the mouse is point B, the "seek // point" is C. // C // B // A Vector2 seekPosition = 2 * mousePosition - catPosition; // Use the TurnToFace function, which we introduced in the AI Series 1: // Aiming sample, to turn the mouse towards the seekPosition. Now when // the mouse moves forward, it'll be trying to move in a straight line // away from the cat. mouseOrientation = TurnToFace (mousePosition, seekPosition, mouseOrientation, MouseTurnSpeed); // set currentMouseSpeed to MaxMouseSpeed - the mouse should run as fast // as it can. currentMouseSpeed = MaxMouseSpeed; } else { // if the mouse isn't trying to evade the cat, it should just meander // around the screen. we'll use the Wander function, which the mouse and // tank share, to accomplish this. mouseWanderDirection and // mouseOrientation are passed by ref so that the wander function can // modify them. for more information on ref parameters, see // http://msdn2.microsoft.com/en-us/library/14akc2c7(VS.80).aspx Wander (mousePosition, ref mouseWanderDirection, ref mouseOrientation, MouseTurnSpeed); // if the mouse is wandering, it should only move at 25% of its maximum // speed. currentMouseSpeed = .25f * MaxMouseSpeed; } // The final step is to move the mouse forward based on its current // orientation. First, we construct a "heading" vector from the orientation // angle. To do this, we'll use Cosine and Sine to tell us the x and y // components of the heading vector. See the accompanying doc for more // information. Vector2 heading = new Vector2 ( (float)Math.Cos (mouseOrientation), (float)Math.Sin (mouseOrientation)); // by multiplying the heading and speed, we can get a velocity vector. the // velocity vector is then added to the mouse's current position, moving him // forward. mousePosition += heading * currentMouseSpeed; } /// /// UpdateTank runs the AI code that will update the tank's orientation and /// position. It is very similar to UpdateMouse, but is slightly more /// complicated: where mouse only has two states, idle and active, the Tank has /// three. /// private void UpdateTank () { // However, the tank's behavior is more complicated than the mouse's, and so // the decision making process is a little different. // First we have to use the current state to decide what the thresholds are // for changing state, as described in the doc. float tankChaseThreshold = TankChaseDistance; float tankCaughtThreshold = TankCaughtDistance; // if the tank is idle, he prefers to stay idle. we do this by making the // chase distance smaller, so the tank will be less likely to begin chasing // the cat. if (tankState == TankAiState.Wander) { tankChaseThreshold -= TankHysteresis / 2; } // similarly, if the tank is active, he prefers to stay active. we // accomplish this by increasing the range of values that will cause the // tank to go into the active state. else if (tankState == TankAiState.Chasing) { tankChaseThreshold += TankHysteresis / 2; tankCaughtThreshold -= TankHysteresis / 2; } // the same logic is applied to the finished state. else if (tankState == TankAiState.Caught) { tankCaughtThreshold += TankHysteresis / 2; } // Second, now that we know what the thresholds are, we compare the tank's // distance from the cat against the thresholds to decide what the tank's // current state is. float distanceFromCat = Vector2.Distance (tankPosition, catPosition); if (distanceFromCat > tankChaseThreshold) { // just like the mouse, if the tank is far away from the cat, it should // idle. tankState = TankAiState.Wander; } else if (distanceFromCat > tankCaughtThreshold) { tankState = TankAiState.Chasing; } else { tankState = TankAiState.Caught; } // Third, once we know what state we're in, act on that state. float currentTankSpeed; if (tankState == TankAiState.Chasing) { // the tank wants to chase the cat, so it will just use the TurnToFace // function to turn towards the cat's position. Then, when the tank // moves forward, he will chase the cat. tankOrientation = TurnToFace (tankPosition, catPosition, tankOrientation, TankTurnSpeed); currentTankSpeed = MaxTankSpeed; } else if (tankState == TankAiState.Wander) { // wander works just like the mouse's. Wander (tankPosition, ref tankWanderDirection, ref tankOrientation, TankTurnSpeed); currentTankSpeed = .25f * MaxTankSpeed; } else { // this part is different from the mouse. if the tank catches the cat, // it should stop. otherwise it will run right by, then spin around and // try to catch it all over again. The end result is that it will kind // of "run laps" around the cat, which looks funny, but is not what // we're after. currentTankSpeed = 0.0f; } // this calculation is also just like the mouse's: we construct a heading // vector based on the tank's orientation, and then make the tank move along // that heading. Vector2 heading = new Vector2 ( (float)Math.Cos (tankOrientation), (float)Math.Sin (tankOrientation)); tankPosition += heading * currentTankSpeed; } /// /// Wander contains functionality that is shared between both the mouse and the /// tank, and does just what its name implies: makes them wander around the /// screen. The specifics of the function are described in more detail in the /// accompanying doc. /// /// the position of the character that is wandering /// /// the direction that the character is currently /// wandering. this parameter is passed by reference because it is an input and /// output parameter: Wander accepts it as input, and will update it as well. /// /// the character's orientation. this parameter is /// also passed by reference and is an input/output parameter. /// the character's maximum turning speed. private void Wander (Vector2 position, ref Vector2 wanderDirection, ref float orientation, float turnSpeed) { // The wander effect is accomplished by having the character aim in a random // direction. Every frame, this random direction is slightly modified. // Finally, to keep the characters on the center of the screen, we have them // turn to face the screen center. The further they are from the screen // center, the more they will aim back towards it. // the first step of the wander behavior is to use the random number // generator to offset the current wanderDirection by some random amount. // .25 is a bit of a magic number, but it controls how erratic the wander // behavior is. Larger numbers will make the characters "wobble" more, // smaller numbers will make them more stable. we want just enough // wobbliness to be interesting without looking odd. wanderDirection.X += MathHelper.Lerp (-.25f, .25f, (float)random.NextDouble ()); wanderDirection.Y += MathHelper.Lerp (-.25f, .25f, (float)random.NextDouble ()); // we'll renormalize the wander direction, ... if (wanderDirection != Vector2.Zero) { wanderDirection.Normalize (); } // ... and then turn to face in the wander direction. We don't turn at the // maximum turning speed, but at 15% of it. Again, this is a bit of a magic // number: it works well for this sample, but feel free to tweak it. orientation = TurnToFace (position, position + wanderDirection, orientation, .15f * turnSpeed); // next, we'll turn the characters back towards the center of the screen, to // prevent them from getting stuck on the edges of the screen. Vector2 screenCenter = Vector2.Zero; screenCenter.X = graphics.GraphicsDevice.Viewport.Width / 2; screenCenter.Y = graphics.GraphicsDevice.Viewport.Height / 2; // Here we are creating a curve that we can apply to the turnSpeed. This // curve will make it so that if we are close to the center of the screen, // we won't turn very much. However, the further we are from the screen // center, the more we turn. At most, we will turn at 30% of our maximum // turn speed. This too is a "magic number" which works well for the sample. // Feel free to play around with this one as well: smaller values will make // the characters explore further away from the center, but they may get // stuck on the walls. Larger numbers will hold the characters to center of // the screen. If the number is too large, the characters may end up // "orbiting" the center. float distanceFromScreenCenter = Vector2.Distance (screenCenter, position); float MaxDistanceFromScreenCenter = Math.Min (screenCenter.Y, screenCenter.X); float normalizedDistance = distanceFromScreenCenter / MaxDistanceFromScreenCenter; float turnToCenterSpeed = .3f * normalizedDistance * normalizedDistance * turnSpeed; // once we've calculated how much we want to turn towards the center, we can // use the TurnToFace function to actually do the work. orientation = TurnToFace (position, screenCenter, orientation, turnToCenterSpeed); } /// /// Calculates the angle that an object should face, given its position, its /// target's position, its current angle, and its maximum turning speed. /// private static float TurnToFace (Vector2 position, Vector2 faceThis, float currentAngle, float turnSpeed) { // consider this diagram: // B // /| // / | // / | y // / o | // A-------- // x // // where A is the position of the object, B is the position of the target, // and "o" is the angle that the object should be facing in order to // point at the target. we need to know what o is. using trig, we know that // tan(theta) = opposite / adjacent // tan(o) = y / x // if we take the arctan of both sides of this equation... // arctan( tan(o) ) = arctan( y / x ) // o = arctan( y / x ) // so, we can use x and y to find o, our "desiredAngle." // x and y are just the differences in position between the two objects. float x = faceThis.X - position.X; float y = faceThis.Y - position.Y; // we'll use the Atan2 function. Atan will calculates the arc tangent of // y / x for us, and has the added benefit that it will use the signs of x // and y to determine what cartesian quadrant to put the result in. // http://msdn2.microsoft.com/en-us/library/system.math.atan2.aspx float desiredAngle = (float)Math.Atan2 (y, x); // so now we know where we WANT to be facing, and where we ARE facing... // if we weren't constrained by turnSpeed, this would be easy: we'd just // return desiredAngle. // instead, we have to calculate how much we WANT to turn, and then make // sure that's not more than turnSpeed. // first, figure out how much we want to turn, using WrapAngle to get our // result from -Pi to Pi ( -180 degrees to 180 degrees ) float difference = WrapAngle (desiredAngle - currentAngle); // clamp that between -turnSpeed and turnSpeed. difference = MathHelper.Clamp (difference, -turnSpeed, turnSpeed); // so, the closest we can get to our target is currentAngle + difference. // return that, using WrapAngle again. return WrapAngle (currentAngle + difference); } /// /// Returns the angle expressed in radians between -Pi and Pi. /// the angle to wrap, in radians. /// the input value expressed in radians from -Pi to Pi. /// private static float WrapAngle (float radians) { while (radians < -MathHelper.Pi) { radians += MathHelper.TwoPi; } while (radians > MathHelper.Pi) { radians -= MathHelper.TwoPi; } return radians; } /// /// This is called when the game should draw itself. Nothing too fancy in here, /// we'll just call Begin on the SpriteBatch, and then draw the tank, cat, and /// mouse, and some overlay text. Once we're finished drawing, we'll call /// SpriteBatch.End. /// protected override void Draw (GameTime gameTime) { GraphicsDevice device = graphics.GraphicsDevice; device.Clear (Color.CornflowerBlue); spriteBatch.Begin (); // draw the tank, cat and mouse... spriteBatch.Draw (tankTexture, tankPosition, null, Color.White, tankOrientation, tankTextureCenter, 1.0f, SpriteEffects.None, 0.0f); spriteBatch.Draw (catTexture, catPosition, null, Color.White, 0.0f, catTextureCenter, 1.0f, SpriteEffects.None, 0.0f); spriteBatch.Draw (mouseTexture, mousePosition, null, Color.White, mouseOrientation, mouseTextureCenter, 1.0f, SpriteEffects.None, 0.0f); // and then draw some text showing the tank's and mouse's current state. // to make the text stand out more, we'll draw the text twice, once black // and once white, to create a drop shadow effect. Vector2 shadowOffset = Vector2.One; spriteBatch.DrawString (spriteFont, "Tank State: \n" + tankState.ToString (), new Vector2 (10, 10) + shadowOffset, Color.Black); spriteBatch.DrawString (spriteFont, "Tank State: \n" + tankState.ToString (), new Vector2 (10, 10), Color.White); spriteBatch.DrawString (spriteFont, "Mouse State: \n" + mouseState.ToString (), new Vector2 (10, 90) + shadowOffset, Color.Black); spriteBatch.DrawString (spriteFont, "Mouse State: \n" + mouseState.ToString (), new Vector2 (10, 90), Color.White); spriteBatch.End (); base.Draw (gameTime); } #endregion #region Handle Input /// /// Handles input for quitting the game. /// void HandleInput () { #if WINDOWS_PHONE KeyboardState currentKeyboardState = new KeyboardState(); #else KeyboardState currentKeyboardState = Keyboard.GetState (); MouseState currentMouseState = Mouse.GetState (); #endif #if IPHONE GamePadState currentGamePadState = GamePad.GetState (PlayerIndex.One); // Check for exit. if (currentKeyboardState.IsKeyDown (Keys.Escape) || currentGamePadState.Buttons.Back == ButtonState.Pressed) { Exit (); } #else // Check for exit. if (currentKeyboardState.IsKeyDown (Keys.Escape)) { Exit (); } #endif // check to see if the user wants to move the cat. we'll create a vector // called catMovement, which will store the sum of all the user's inputs. Vector2 catMovement = Vector2.Zero; //Move toward the touch point. We slow down the cat when it gets within a distance of MaxCatSpeed to the touch point. float smoothStop = 1; #if IPHONE // check to see if the user wants to move the cat. we'll create a vector // called catMovement, which will store the sum of all the user's inputs. catMovement = currentGamePadState.ThumbSticks.Left; // flip y: on the thumbsticks, down is -1, but on the screen, down is bigger // numbers. catMovement.Y *= -1; if (currentKeyboardState.IsKeyDown (Keys.Left) || currentGamePadState.DPad.Left == ButtonState.Pressed) { catMovement.X -= 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Right) || currentGamePadState.DPad.Right == ButtonState.Pressed) { catMovement.X += 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Up) || currentGamePadState.DPad.Up == ButtonState.Pressed) { catMovement.Y -= 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Down) || currentGamePadState.DPad.Down == ButtonState.Pressed) { catMovement.Y += 1.0f; } TouchCollection currentTouchCollection = TouchPanel.GetState(); if (currentTouchCollection != null ) { if (currentTouchCollection.Count > 0) { Vector2 touchPosition = currentTouchCollection[0].Position; if (touchPosition != catPosition) { catMovement = touchPosition - catPosition; float delta = MaxCatSpeed - MathHelper.Clamp(catMovement.Length(), 0, MaxCatSpeed); smoothStop = 1 - delta / MaxCatSpeed; } } } #else if (currentKeyboardState.IsKeyDown (Keys.Left)) { catMovement.X -= 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Right)) { catMovement.X += 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Up)) { catMovement.Y -= 1.0f; } if (currentKeyboardState.IsKeyDown (Keys.Down)) { catMovement.Y += 1.0f; } Vector2 mousePosition = new Vector2 (currentMouseState.X, currentMouseState.Y); if (currentMouseState.LeftButton == ButtonState.Pressed && mousePosition != catPosition) { catMovement = mousePosition - catPosition; float delta = MaxCatSpeed - MathHelper.Clamp (catMovement.Length (), 0, MaxCatSpeed); smoothStop = 1 - delta / MaxCatSpeed; } #endif // normalize the user's input, so the cat can never be going faster than // CatSpeed. if (catMovement != Vector2.Zero) { catMovement.Normalize (); } catPosition += catMovement * MaxCatSpeed * smoothStop; } #endregion } }