using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace XNAPacMan {
public enum GhostState { Home, Scatter, Attack, Blue, Dead }
public enum Ghosts { Blinky, Pinky, Inky, Clyde }
///
/// One of the ghosts that try to kill Pac Man.
///
public class Ghost {
///
/// Instantiates a ghost.
///
/// A reference to the Game object, needed for access to services.
/// A reference to the Pac Man, needed for AI.
/// Which ghost, needed for appearance and behavior.
public Ghost(Game game, Player player, Ghosts identity) {
spriteBatch_ = (SpriteBatch)game.Services.GetService(typeof(SpriteBatch));
ghostBase1_ = game.Content.Load("sprites/GhostBase");
ghostBase2_ = game.Content.Load("sprites/GhostBase2");
ghostChased_ = game.Content.Load("sprites/GhostChased");
eyesBase_ = game.Content.Load("sprites/GhostEyes");
eyesCenter_ = game.Content.Load("sprites/GhostEyesCenter");
colorBase_ = Constants.colors(identity);
identity_ = identity;
previousNumCrumps_ = 0;
Reset(true, player);
wiggle_ = true;
direction_ = new Direction();
lastJunction_ = new Point();
scatterTiles_ = Constants.scatterTiles(identity);
}
///
/// Put the ghosts back in their home, ready to begin a new attack
///
/// True at level start, false otherwise
/// The pac man. Pac Man is often respawned with the ghosts, so they need to know about the new Pac Man.
public void Reset(bool newLevel, Player player) {
State = GhostState.Home; // Ghosts start at home
previousState_ = GhostState.Home; // Sounds are played when currentState != previousState.
// So to get them playing at level start, we do this simple hack.
updateCount_ = 0;
initialJumps_ = Constants.InitialJumps(identity_, newLevel);
position_ = Constants.startPosition(identity_);
scheduleStateEval_ = true;
lastJunction_ = new Point();
player_ = player;
scatterModesLeft_ = 4;
UpdateSpeed();
}
///
/// When we need to lock the game, make sure the timers here are updated to reflect
/// the "waste of time".
///
public void LockTimer(GameTime gameTime) {
timeInCurrentState += gameTime.ElapsedGameTime;
}
///
/// In case this ghost is Inky, he will need a reference to blinky in order
/// to find his way around the maze. Otherwise, setting this has no effect.
///
/// A reference to Blinky.
public void SetBlinky(Ghost blinky) {
blinky_ = blinky;
}
///
/// Call this every game update to get those ghosts moving around.
///
/// Provides a snapshot of timing values.
public void Update(GameTime gameTime) {
if (scheduleStateEval_) {
StateEval();
}
// After the AI has had the occasion to run, update lastJuntion.
if (position_.DeltaPixel == Point.Zero && IsAJunction(position_.Tile)) {
lastJunction_ = position_.Tile;
}
Move();
}
void PlaySound() {
if (State == GhostState.Attack || State == GhostState.Scatter || State == GhostState.Home) {
if (Grid.NumCrumps < 50) {
GhostSoundsManager.playLoopAttackVeryFast();
} else if (Grid.NumCrumps < 120) {
GhostSoundsManager.playLoopAttackFast();
} else {
GhostSoundsManager.playLoopAttack();
}
} else if (State == GhostState.Blue) {
GhostSoundsManager.playLoopBlue();
} else if (State == GhostState.Dead) {
GhostSoundsManager.playLoopDead();
}
}
void StateEval() {
GhostState initialState = State;
scheduleStateEval_ = false;
switch (State) {
case GhostState.Home:
// Ghost exit the home state for the scatter state when they get to the row
// above the home
if (position_.Tile.Y == 11 && position_.DeltaPixel.Y == 0) {
// Select inital direction based on scatter tiles
if (Constants.scatterTiles(identity_)[0].X < 13) {
direction_ = Direction.Left;
} else {
direction_ = Direction.Right;
}
if (scatterModesLeft_ > 0) {
State = GhostState.Scatter;
} else {
State = GhostState.Attack;
}
return;
}
// Ghosts move up when they are aligned with the entrance
else if (position_.Tile.X == 13 && position_.DeltaPixel.X == 8) {
direction_ = Direction.Up;
}
// When on one side, move towards middle when on the bottom and time's up
// If time's not up, keep bouncing up and down
else if ((position_.DeltaPixel.Y == 8) &&
((position_.Tile.X == 11 && position_.DeltaPixel.X == 8) ||
(position_.Tile.X == 15 && position_.DeltaPixel.X == 8))) {
if (position_.Tile.Y == 14) {
initialJumps_--;
if (initialJumps_ == 0) {
if (position_.Tile.X == 11) {
direction_ = Direction.Right;
} else {
direction_ = Direction.Left;
}
} else {
direction_ = Direction.Up;
}
} else if (position_.Tile.Y == 13) {
direction_ = Direction.Down;
}
}
break;
case GhostState.Scatter:
// Attempt to reverse direction upon entering this state
if (previousState_ == GhostState.Attack) {
scatterModesLeft_--;
if (NextTile(OppositeDirection(direction_)).IsOpen) {
direction_ = OppositeDirection(direction_);
}
}
AIScatter();
int timeInScatterMode = scatterModesLeft_ <= 2 ? 5 : 7;
if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(timeInScatterMode)) {
State = GhostState.Attack;
}
break;
case GhostState.Dead:
// Attempt to reverse direction upon entering this state
if (previousState_ != GhostState.Dead && previousState_ != GhostState.Blue) {
if (NextTile(OppositeDirection(direction_)).IsOpen) {
direction_ = OppositeDirection(direction_);
}
} else {
AIDead();
}
if (position_.DeltaPixel.X == 8 && position_.DeltaPixel.Y == 8) {
if (position_.Tile.Y == 14) {
State = GhostState.Home;
}
}
break;
case GhostState.Attack:
// Attempt to reverse direction upon entering this state
if (previousState_ != GhostState.Attack && previousState_ != GhostState.Blue) {
if (NextTile(OppositeDirection(direction_)).IsOpen) {
direction_ = OppositeDirection(direction_);
}
} else {
AIAttack();
}
if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(20)) {
State = GhostState.Scatter;
}
break;
case GhostState.Blue:
// Attempt to reverse direction upon entering this state
if (previousState_ != GhostState.Blue) {
if (NextTile(OppositeDirection(direction_)).IsOpen) {
direction_ = OppositeDirection(direction_);
}
} else {
// TODO : make special blue AI
AIAttack();
}
// When blue time is over, revert to attack mode.
if ((DateTime.Now - timeInCurrentState) > TimeSpan.FromSeconds(Constants.BlueTime())) {
State = GhostState.Attack;
}
break;
}
// TODO : move all these magic numbers to the Constants class.
// We select a new sound only upon some state change, or when
// the number of crumps goes below certain thresholds.
if ((initialState != previousState_) ||
(Grid.NumCrumps == 199 && previousNumCrumps_ == 200) ||
(Grid.NumCrumps == 19 && previousNumCrumps_ == 20)) {
PlaySound();
}
previousState_ = initialState;
}
void Move() {
updateCount_++;
updateCount_ %= updatesPerPixel_;
if (updateCount_ == 0) {
// There is no point running the full AI each update, especially considering we
// want this code to run at 1000 updates per second. We run it each time it moves by a pixel.
scheduleStateEval_ = true;
// Now is a nice time to evaluate whether our current speed is correct. This variable
// may change under a wide array of circumstances, so by putting it here we probably generate
// too many checks but we ensure correct behavior.
UpdateSpeed();
switch (direction_) {
case Direction.Up:
position_.DeltaPixel.Y--;
if (position_.DeltaPixel.Y < 0) {
position_.DeltaPixel.Y = 15;
position_.Tile.Y--;
}
break;
case Direction.Down:
position_.DeltaPixel.Y++;
if (position_.DeltaPixel.Y > 15) {
position_.DeltaPixel.Y = 0;
position_.Tile.Y++;
}
break;
case Direction.Left:
position_.DeltaPixel.X--;
if (position_.DeltaPixel.X < 0) {
position_.DeltaPixel.X = 15;
position_.Tile.X--;
// Special case : the tunnel
if (position_.Tile.X < 0) {
position_.Tile.X = Grid.Width - 1;
}
}
break;
case Direction.Right:
position_.DeltaPixel.X++;
if (position_.DeltaPixel.X > 15) {
position_.DeltaPixel.X = 0;
position_.Tile.X++;
// Special case : the tunnel
if (position_.Tile.X > Grid.Width - 1) {
position_.Tile.X = 0;
}
}
break;
}
}
}
void UpdateSpeed() {
int baseSpeed = Constants.GhostSpeed();
if (State == GhostState.Home) {
updatesPerPixel_ = baseSpeed * 2;
} else if (State == GhostState.Blue) {
updatesPerPixel_ = (int)(baseSpeed * 1.5);
} else if (identity_ == Ghosts.Blinky && Grid.NumCrumps <= Constants.CruiseElroyTimer()) {
updatesPerPixel_ = baseSpeed - 1;
}
// If in the tunnel, reduce speed
else if (position_.Tile.Y == 14 &&
((0 <= position_.Tile.X && position_.Tile.X <= 5) ||
(22 <= position_.Tile.X && position_.Tile.X <= 27))) {
updatesPerPixel_ = baseSpeed + 5;
} else {
updatesPerPixel_ = baseSpeed;
}
}
///
/// Guides the ghost towards his "favored" area of the board, as defined by scatterTiles_.
///
void AIScatter() {
// As with AIAttack(), all the method does is change direction if necessary,
// which only happens when exactly at a junction
if (position_.DeltaPixel != Point.Zero || !IsAJunction(position_.Tile)) {
return;
}
// Scatter AI may be overriden by certain tiles
if (AIOverride()) {
return;
}
// If we are on one of our favored tiles, go on to the next
int favoredTile = scatterTiles_.FindIndex(i => i == position_.Tile);
int nextFavoredTile = (favoredTile + 1) % (scatterTiles_.Count);
if (favoredTile != -1) {
direction_ = FindDirection(scatterTiles_[nextFavoredTile]);
}
// If our last junction was a favored tile and this one isn't, ignore it
else if (!scatterTiles_.Contains(lastJunction_)) {
// Otherwise, find scatter tile closest to our position and head for it.
List orderedTiles = scatterTiles_.ToList();
orderedTiles.Sort((a, b) => Vector2.Distance(new Vector2(position_.Tile.X, position_.Tile.Y),
new Vector2(a.X, a.Y)).
CompareTo(
Vector2.Distance(new Vector2(position_.Tile.X, position_.Tile.Y),
new Vector2(b.X, b.Y))));
direction_ = FindDirection(orderedTiles[0]);
}
}
///
/// Guides the ghost to try to reach the player
///
void AIAttack() {
// All this method does is change direction if necessary.
// There is only one case in which we may potentially change direction :
// when the ghost is exactly at a junction.
if (position_.DeltaPixel != Point.Zero || !IsAJunction(position_.Tile)) {
return;
}
// Attack AI may be overriden by certain tiles
if (AIOverride()) {
return;
}
switch (identity_) {
case Ghosts.Blinky:
AttackAIBlinky();
break;
case Ghosts.Pinky:
AttackAIPinky();
break;
case Ghosts.Inky:
AttackAIInky();
break;
case Ghosts.Clyde:
AttackAIClyde();
break;
default:
throw new ArgumentException();
}
}
void AIDead() {
// When aligned with the entrance, go down. When the ghost has fully entered the house,
// stateEval will change state to Home.
if (position_.Tile.Y == 11 && position_.Tile.X == 13 && position_.DeltaPixel.X == 8) {
direction_ = Direction.Down;
}
// Otherwise, only change direction if the ghost is exactly on a tile
else if (position_.DeltaPixel == Point.Zero) {
// We direct the ghost towards one of the two squares above the home. When he reaches one,
// he still has to move slightly right or left in order to align with the entrance.
if (position_.Tile.X == 13 && position_.Tile.Y == 11) {
direction_ = Direction.Right;
} else if (position_.Tile.X == 14 && position_.Tile.Y == 11) {
direction_ = Direction.Left;
}
// Otherwise, when at a junction, head for the square above home closest to us.
else if (IsAJunction(position_.Tile)) {
if (position_.Tile.X > 13) {
direction_ = FindDirection(new Point(14, 11));
} else {
direction_ = FindDirection(new Point(13, 11));
}
}
}
}
///
/// Blinky is extremely straightforward : head directly for the player
///
void AttackAIBlinky() {
direction_ = FindDirection(player_.Position.Tile);
}
///
/// Pinky tries to head for two tiles ahead of the player.
///
void AttackAIPinky() {
Tile nextTile = NextTile(player_.Direction, player_.Position);
Tile nextNextTile = NextTile(player_.Direction, new Position(nextTile.ToPoint, Point.Zero));
direction_ = FindDirection(nextNextTile.ToPoint);
}
///
/// Inky is a bit more random. He will try to head for a square situated across
/// the pac man from blinky's location.
///
void AttackAIInky() {
Tile nextTile = NextTile(player_.Direction, player_.Position);
Tile nextNextTile = NextTile(player_.Direction, new Position(nextTile.ToPoint, Point.Zero));
Vector2 line = new Vector2(blinky_.Position.Tile.X - nextNextTile.ToPoint.X, blinky_.Position.Tile.Y - nextNextTile.ToPoint.Y);
line *= 2;
Point destination = new Point(position_.Tile.X + (int)line.X, position_.Tile.Y + (int)line.Y);
// Prevent out-of-bounds exception
destination.X = (int)MathHelper.Clamp(destination.X, 0, Grid.Width - 1);
destination.Y = (int)MathHelper.Clamp(destination.Y, 0, Grid.Height - 1);
direction_ = FindDirection(destination);
}
///
/// Clyde is the bizarre one. When within 8 tiles of the player,
/// he will head for his favored corner (AIScatter). When farther away,
/// he will near the player using Blinky's AI.
///
void AttackAIClyde() {
float distanceToPlayer = Vector2.Distance(
new Vector2(player_.Position.Tile.X, player_.Position.Tile.Y),
new Vector2(position_.Tile.X, position_.Tile.Y));
if (distanceToPlayer >= 8) {
AttackAIBlinky();
} else {
AIScatter();
}
}
// On certain tiles, we force all the ghosts in a particular direction.
// This is to prevent them from bunching up too much, and give attentive
// players some sure-fire ways to escape.
bool AIOverride() {
if (position_.Tile.Y == 11 && (position_.Tile.X == 12 || position_.Tile.X == 15)) {
return (direction_ == Direction.Right || direction_ == Direction.Left);
} else if (position_.Tile.Y == 20 && (position_.Tile.X == 9 || position_.Tile.X == 18)) {
return (direction_ == Direction.Right || direction_ == Direction.Left);
} else {
return false;
}
}
///
/// Returns the opposite of the specified direction
///
/// direction
/// opposite direction
Direction OppositeDirection(Direction d) {
switch (d) {
case Direction.Up:
return Direction.Down;
case Direction.Down:
return Direction.Up;
case Direction.Left:
return Direction.Right;
case Direction.Right:
return Direction.Left;
default:
throw new ArgumentException();
}
}
///
/// Returns in what direction we should go next in order to reach the destination.
///
/// Where we want to go
/// Where to go next
Direction FindDirection(Point destination) {
// We use a stupid but very efficient algorithm : go in the direction that
// closes most distance between position and destination. This works well given
// there are no dead ends and multiple paths leading to the destination.
// However, we can never turn 180, and will rather choose a less-than-optimal path.
int xDistance = destination.X - position_.Tile.X;
int yDistance = destination.Y - position_.Tile.Y;
var directions = new List(4);
// Order directions by shortest path. Note: there is probably a better way to do this, probably if I hadn't used an
// enum for directions in the first place.
if (Math.Abs(xDistance) > Math.Abs(yDistance)) {
if (xDistance > 0) {
if (yDistance < 0) { // Direction is Up-Right, favor Right
directions = new List { Direction.Right, Direction.Up, Direction.Down, Direction.Left };
} else { // Direction is Down-Right, favor Right
directions = new List { Direction.Right, Direction.Down, Direction.Up, Direction.Left };
}
} else {
if (yDistance < 0) { // Direction is Up-Left, favor Left
directions = new List { Direction.Left, Direction.Up, Direction.Down, Direction.Right };
} else { // Direction is Down-Left, favor Left
directions = new List { Direction.Left, Direction.Down, Direction.Up, Direction.Right };
}
}
} else {
if (xDistance > 0) {
if (yDistance < 0) { // Direction is Up-Right, favor Up
directions = new List { Direction.Up, Direction.Right, Direction.Left, Direction.Down };
} else { // Direction is Down-Right, favor Down
directions = new List { Direction.Down, Direction.Right, Direction.Left, Direction.Up };
}
} else {
if (yDistance < 0) { // Direction is Up-Left, favor Up
directions = new List { Direction.Up, Direction.Left, Direction.Right, Direction.Down };
} else { // Direction is Down-Left, favor Down
directions = new List { Direction.Down, Direction.Left, Direction.Right, Direction.Up };
}
}
}
// Select first item in the list to meet two essential conditions : the path ahead is open,
// and it's not the opposite direction. If we can't meet both conditions, just return the first
// direction that leads to an open square.
int index = directions.FindIndex(i => i != OppositeDirection(direction_) && NextTile(i).IsOpen);
if (index != -1) {
return directions[index];
} else {
// Put a breakpoint here, this should never happen.
return directions.Find(i => NextTile(i).IsOpen);
}
}
///
/// Retrieves the next tile in the specified direction from the ghost's position.
///
/// Direction in which to look
/// The tile
Tile NextTile(Direction d) {
return NextTile(d, position_);
}
///
/// Retrieves the next tile in the specified direction from the specified position.
///
/// Direction in which to look
/// Position from which to look
/// The tile
public static Tile NextTile(Direction d, Position p) {
switch (d) {
case Direction.Up:
if (p.Tile.Y - 1 < 0) {
return Grid.TileGrid[p.Tile.X, p.Tile.Y];
} else {
return Grid.TileGrid[p.Tile.X, p.Tile.Y - 1];
}
case Direction.Down:
if (p.Tile.Y + 1 >= Grid.Height) {
return Grid.TileGrid[p.Tile.X, p.Tile.Y];
} else {
return Grid.TileGrid[p.Tile.X, p.Tile.Y + 1];
}
case Direction.Left:
// Special case : the tunnel
if (p.Tile.X == 0) {
return Grid.TileGrid[Grid.Width - 1, p.Tile.Y];
} else {
return Grid.TileGrid[p.Tile.X - 1, p.Tile.Y];
}
case Direction.Right:
// Special case : the tunnel
if (p.Tile.X + 1 >= Grid.Width) {
return Grid.TileGrid[0, p.Tile.Y];
} else {
return Grid.TileGrid[p.Tile.X + 1, p.Tile.Y];
}
default:
throw new ArgumentException();
}
}
///
/// Returns whether the specified tile is a junction.
///
/// Tile to check
/// whether the specified tile is a junction
bool IsAJunction(Point tile) {
if (NextTile(direction_).Type == TileTypes.Open) {
// If the path ahead is open, we're at a junction if it's also open
// to one of our sides
if (direction_ == Direction.Up || direction_ == Direction.Down) {
return ((NextTile(Direction.Left).IsOpen) ||
(NextTile(Direction.Right).IsOpen));
} else {
return ((NextTile(Direction.Up).IsOpen) ||
(NextTile(Direction.Down).IsOpen));
}
}
// If the path ahead is blocked, then we're definitely at a junction, because there are no dead-ends
else {
return true;
}
}
///
/// Assumes spritebatch.begin() was called
///
/// Provides a snapshot of timing values.
public void Draw(GameTime gameTime, Vector2 boardPosition) {
// Ghosts have a two-frame animation; we use a bool to toggle between
// the two. We divide DateTime.Now.Milliseconds by the period.
if (((DateTime.Now.Millisecond / 125) % 2) == 0 ^ wiggle_) {
wiggle_ = !wiggle_;
}
Vector2 position;
position.X = boardPosition.X + (position_.Tile.X * 16) + (position_.DeltaPixel.X) - 6;
position.Y = boardPosition.Y + (position_.Tile.Y * 16) + (position_.DeltaPixel.Y) - 6;
Vector2 eyesBasePosition;
eyesBasePosition.X = position.X + 4;
eyesBasePosition.Y = position.Y + 6;
Vector2 eyesCenterPosition = new Vector2();
switch (direction_) {
case Direction.Up:
eyesBasePosition.Y -= 2;
eyesCenterPosition.X = eyesBasePosition.X + 2;
eyesCenterPosition.Y = eyesBasePosition.Y;
break;
case Direction.Down:
eyesBasePosition.Y += 2;
eyesCenterPosition.X = eyesBasePosition.X + 2;
eyesCenterPosition.Y = eyesBasePosition.Y + 6;
break;
case Direction.Left:
eyesBasePosition.X -= 2;
eyesCenterPosition.X = eyesBasePosition.X;
eyesCenterPosition.Y = eyesBasePosition.Y + 3;
break;
case Direction.Right:
eyesBasePosition.X += 2;
eyesCenterPosition.X = eyesBasePosition.X + 4;
eyesCenterPosition.Y = eyesBasePosition.Y + 3;
break;
}
if (State == GhostState.Blue) {
if (((DateTime.Now - timeInCurrentState).Seconds < 0.5 * Constants.BlueTime())) {
RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, Color.Blue);
RenderSprite(ghostChased_, null, boardPosition, position, Color.White);
} else {
bool flash = (DateTime.Now.Second + DateTime.Now.Millisecond / 200) % 2 == 0;
RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, flash ? Color.Blue : Color.White);
RenderSprite(ghostChased_, null, boardPosition, position, flash ? Color.White : Color.Blue);
}
} else if (State == GhostState.Dead) {
RenderSprite(eyesBase_, null, boardPosition, eyesBasePosition, Color.White);
RenderSprite(eyesCenter_, null, boardPosition, eyesCenterPosition, Color.White);
} else {
RenderSprite(wiggle_ ? ghostBase1_ : ghostBase2_, null, boardPosition, position, colorBase_);
RenderSprite(eyesBase_, null, boardPosition, eyesBasePosition, Color.White);
RenderSprite(eyesCenter_, null, boardPosition, eyesCenterPosition, Color.White);
}
}
///
/// Allows rendering across the tunnel, which is tricky.
///
/// Source texture
/// Portion of the source to render. Pass null for rendering the whole texture.
/// Top-left pixel of the board in the screen
/// Where to render the texture.
void RenderSprite(Texture2D spriteSheet, Rectangle? rectangle, Vector2 boardPosition, Vector2 position, Color color) {
Rectangle rect = rectangle == null ? new Rectangle(0, 0, spriteSheet.Width, spriteSheet.Height) :
rectangle.Value;
int textureWidth = rectangle == null ? spriteSheet.Width : rectangle.Value.Width;
int textureHeight = rectangle == null ? spriteSheet.Height : rectangle.Value.Height;
// What happens when we are crossing to the other end by the tunnel?
// We detect if part of the sprite is rendered outside of the board.
// First, to the left.
if (position.X < boardPosition.X) {
int deltaPixel = (int)(boardPosition.X - position.X); // Number of pixels off the board
var leftPortion = new Rectangle(rect.X + deltaPixel, rect.Y, textureWidth - deltaPixel, textureHeight);
var leftPortionPosition = new Vector2(boardPosition.X, position.Y);
var rightPortion = new Rectangle(rect.X, rect.Y, deltaPixel, textureHeight);
var rightPortionPosition = new Vector2(boardPosition.X + (16 * 28) - deltaPixel, position.Y);
spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, color);
spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, color);
}
// Next, to the right
else if (position.X > (boardPosition.X + (16 * 28) - textureWidth)) {
int deltaPixel = (int)((position.X + textureWidth) - (boardPosition.X + (16 * 28))); // Number of pixels off the board
var leftPortion = new Rectangle(rect.X + textureWidth - deltaPixel, rect.Y, deltaPixel, textureHeight);
var leftPortionPosition = new Vector2(boardPosition.X, position.Y);
var rightPortion = new Rectangle(rect.X, rect.Y, textureWidth - deltaPixel, textureHeight);
var rightPortionPosition = new Vector2(position.X, position.Y);
spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, color);
spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, color);
}
// Draw normally - in one piece
else {
spriteBatch_.Draw(spriteSheet, position, rect, color);
}
}
// DRAWING
SpriteBatch spriteBatch_;
Texture2D ghostBase1_;
Texture2D ghostBase2_;
Texture2D ghostChased_;
Texture2D eyesBase_;
Texture2D eyesCenter_;
Color colorBase_;
bool wiggle_;
// LOGIC
Ghost blinky_; // Only Inky needs this for his AI
Ghosts identity_;
Direction direction_;
Position position_;
GhostState state_;
GhostState previousState_;
List scatterTiles_;
Point lastJunction_;
DateTime timeInCurrentState;
Player player_;
int updatesPerPixel_;
bool scheduleStateEval_;
int scatterModesLeft_;
int initialJumps_;
int previousNumCrumps_;
int updateCount_;
public GhostState State { get { return state_; } set { state_ = value; timeInCurrentState = DateTime.Now; } }
public Position Position { get { return position_; } }
public Ghosts Identity { get { return identity_; } }
}
}