using System; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace XNAPacMan { /// /// Defines the position of an entity (player, ghost) on the board. /// public struct Position { public Position(Point Tile, Point DeltaPixel) { this.Tile = Tile; this.DeltaPixel = DeltaPixel; } /// /// The tile the entity is on. /// public Point Tile; /// /// How many pixels the entity is off its nominal tile. /// public Point DeltaPixel; } public enum Direction { Up, Down, Left, Right }; public enum State { Start, Normal, Dying }; /// /// This is the yellow pac man that eat dots and gets killed /// repetitively unless you're good. /// public class Player { public Player(Game game) { Reset(); this.game = game; updatesPerPixel_ = Constants.PacManSpeed(); spriteBatch_ = (SpriteBatch)game.Services.GetService(typeof(SpriteBatch)); eatingFrames_ = new Texture2D[] { game.Content.Load("sprites/PacManEating1"), game.Content.Load("sprites/PacManEating2"), game.Content.Load("sprites/PacManEating3"), game.Content.Load("sprites/PacManEating4"), game.Content.Load("sprites/PacManEating5"), game.Content.Load("sprites/PacManEating6"), game.Content.Load("sprites/PacManEating7"), game.Content.Load("sprites/PacManEating8"), game.Content.Load("sprites/PacManEating9"), }; dyingFrames_ = game.Content.Load("sprites/DyingSheetNew"); } /// /// Allows the Player to update itself. /// /// Provides a snapshot of timing values. public void Update(GameTime gameTime) { // First, deal with keyboard input. Get only the direction keys. Keys[] validKeys = { Keys.Up, Keys.Down, Keys.Left, Keys.Right }; Keys[] pressedKeys = (from k in Keyboard.GetState().GetPressedKeys() where validKeys.Contains(k) select k).ToArray(); // If the player is pressing more than one key, we don't turn. Trying to filter keys would be // either bogus or overly complex for the needs of this game, and yes, this is from experience. if (pressedKeys.Length == 1) { // At level start, Pac Man is facing right, although he is drawn full. Pressing // Right simply makes him start moving in that direction; pressing Left make him change direction // first; this must be handled separately from the usual case. if (state_ == State.Start) { if (pressedKeys[0] == Keys.Left || pressedKeys[0] == Keys.Right) { state_ = State.Normal; } if (pressedKeys[0] == Keys.Left) { TryTurn(pressedKeys[0]); } } // Normal case : turn if required direction != current direction. else if ((direction_.ToString() != pressedKeys[0].ToString())) { TryTurn(pressedKeys[0]); } } TryMove(); } /// /// Ensures that if the Pac Man moves, it is a legal move /// void TryMove() { if (state_ == State.Start) { return; } // If between two tiles, movement is always allowed since TryTurn() // always ensures direction is valid if (position_.DeltaPixel != Point.Zero) { DoMove(); } // Special case : the tunnel. else if ((position_.Tile == new Point(0, 14) && direction_ == Direction.Left) || (position_.Tile == new Point(27, 14) && direction_ == Direction.Right)) { DoMove(); } // If on a tile, we only move if the next tile in our direction is open else if ((direction_ == Direction.Up && Grid.TileGrid[position_.Tile.X, position_.Tile.Y - 1].Type == TileTypes.Open) || (direction_ == Direction.Down && Grid.TileGrid[position_.Tile.X, position_.Tile.Y + 1].Type == TileTypes.Open) || (direction_ == Direction.Left && Grid.TileGrid[position_.Tile.X - 1, position_.Tile.Y].Type == TileTypes.Open) || (direction_ == Direction.Right && Grid.TileGrid[position_.Tile.X + 1, position_.Tile.Y].Type == TileTypes.Open)) { DoMove(); } } /// /// Effectively moves the Pac Man according to member variable direction_. /// void DoMove() { // Everytime the updateCount == updatesPerPixel, move one pixel in // the desired direction and reset the updateCount. updateCount_++; updateCount_ %= updatesPerPixel_; if (updateCount_ == 0) { // Now is a nice time to update our speed, like we do for the ghosts. if (Ghost.NextTile(direction_, position_).HasCrump) { updatesPerPixel_ = Constants.PacManSpeed() + 2; } else { updatesPerPixel_ = Constants.PacManSpeed(); } // Move one pixel in the desired direction if (direction_ == Direction.Up) { position_.DeltaPixel.Y--; } else if (direction_ == Direction.Down) { position_.DeltaPixel.Y++; } else if (direction_ == Direction.Left) { position_.DeltaPixel.X--; } else if (direction_ == Direction.Right) { position_.DeltaPixel.X++; } // By moving one pixel we might have moved to another tile completely if (position_.DeltaPixel.X == 16) { // Special case : the tunnel if (position_.Tile.X == 27) { position_.Tile.X = 0; } else { position_.Tile.X++; } position_.DeltaPixel.X = 0; } else if (position_.DeltaPixel.X == -16) { // Special case : the tunnel if (position_.Tile.X == 0) { position_.Tile.X = 27; } else { position_.Tile.X--; } position_.DeltaPixel.X = 0; } else if (position_.DeltaPixel.Y == 16) { position_.Tile.Y++; position_.DeltaPixel.Y = 0; } else if (position_.DeltaPixel.Y == -16) { position_.Tile.Y--; position_.DeltaPixel.Y = 0; } } } /// /// Allows the Player to be drawn to the screen. Assumes spritebatch.begin() has been called, and /// spritebatch.end() will be called afterwards. /// /// Provides a snapshot of timing values. public void Draw(GameTime gameTime, Vector2 boardPosition) { // The position is taken as a function of the board position, the tile on which the pac man is, how many // pixels he is off from this tile, and the size of the pac man itself versus the size of a tile (16). Vector2 position; position.X = boardPosition.X + (position_.Tile.X * 16) + position_.DeltaPixel.X - ((eatingFrames_[0].Width - 16) / 2); position.Y = boardPosition.Y + (position_.Tile.Y * 16) + position_.DeltaPixel.Y - ((eatingFrames_[0].Height - 16) / 2); // At level start, just draw the full pac man and exit if (state_ == State.Start) { spriteBatch_.Draw(eatingFrames_[0], position, Color.White); } else if (state_ == State.Normal) { // The frame index is taken as a function of how much the pac man is off from a tile, the size of // a tile (always 16 pixels), and how many frames we use for the animation. int frame = Math.Abs(position_.DeltaPixel.X + position_.DeltaPixel.Y) / (16 / usedFramesIndex_.Length); frame = (int)MathHelper.Clamp(frame, 0, usedFramesIndex_.Length - 1); RenderSprite(eatingFrames_[usedFramesIndex_[frame]], null, boardPosition, position); } else if (state_ == State.Dying) { int timeBetweenFrames = 90; // Sound "Death" is 1811 milliseconds long, we have 20 frames to go through. //timer_ += gameTime.ElapsedRealTime; timer_ += gameTime.ElapsedGameTime; int index = (timer_.Seconds * 1000 + timer_.Milliseconds) / timeBetweenFrames; if (index > 19) { return; } RenderSprite(dyingFrames_, new Rectangle(26 * index, 0, 26, 26), boardPosition, position); } } /// /// 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) { Rectangle rect = rectangle == null ? new Rectangle(0, 0, spriteSheet.Width, spriteSheet.Height) : rectangle.Value; // What happens when we are crossing to the other end by the tunnel? // We detect if part of the pacman 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, 26 - deltaPixel, 26); var leftPortionPosition = new Vector2(boardPosition.X, position.Y); var rightPortion = new Rectangle(rect.X, rect.Y, deltaPixel, 26); var rightPortionPosition = new Vector2(boardPosition.X + (16 * 28) - deltaPixel, position.Y); spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, Color.White); spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, Color.White); } // Next, to the right else if (position.X > (boardPosition.X + (16 * 28) - 26)) { int deltaPixel = (int)((position.X + 26) - (boardPosition.X + (16 * 28))); // Number of pixels off the board var leftPortion = new Rectangle(rect.X + 26 - deltaPixel, rect.Y, deltaPixel, 26); var leftPortionPosition = new Vector2(boardPosition.X, position.Y); var rightPortion = new Rectangle(rect.X, rect.Y, 26 - deltaPixel, 26); var rightPortionPosition = new Vector2(position.X, position.Y); spriteBatch_.Draw(spriteSheet, leftPortionPosition, leftPortion, Color.White); spriteBatch_.Draw(spriteSheet, rightPortionPosition, rightPortion, Color.White); } // Draw normally - in one piece else { spriteBatch_.Draw(spriteSheet, position, rect, Color.White); } } /// /// Should be called anytime the Pac Man needs to be reset (game start, level start) /// void Reset() { state_ = State.Start; direction_ = Direction.Right; usedFramesIndex_ = new int[] { 0, 1, 2 }; position_ = new Position { Tile = new Point(13, 23), DeltaPixel = new Point(8, 0) }; updateCount_ = 0; } /// /// Ensures that if the Pac Man turns, it's in a valid direction. /// /// Direction the player tries to steer the Pac Man towards void TryTurn(Keys input) { // If we're between two tiles, we can only turn 180 if (position_.DeltaPixel != Point.Zero) { if ((direction_ == Direction.Up && input == Keys.Down) || (direction_ == Direction.Down && input == Keys.Up) || (direction_ == Direction.Left && input == Keys.Right) || (direction_ == Direction.Right && input == Keys.Left)) { // Turning 180 between two tiles implies destination tile is open, // no other validation to be done DoTurn(input); } } // Special case : the tunnel. else if ((input == Keys.Left && position_.Tile.X == 0) || (input == Keys.Right && position_.Tile.X == 27)) { DoTurn(input); } // We're exactly on a tile; this is the "general" case // Do turn if the destination tile is open else if ((input == Keys.Up && Grid.TileGrid[position_.Tile.X, position_.Tile.Y - 1].Type == TileTypes.Open) || (input == Keys.Down && Grid.TileGrid[position_.Tile.X, position_.Tile.Y + 1].Type == TileTypes.Open) || (input == Keys.Left && Grid.TileGrid[position_.Tile.X - 1, position_.Tile.Y].Type == TileTypes.Open) || (input == Keys.Right && Grid.TileGrid[position_.Tile.X + 1, position_.Tile.Y].Type == TileTypes.Open)) { DoTurn(input); } } /// /// This effectively makes Pac Man turn. /// We have to update the sprites used for animation, /// and if the Pac Man is between two tiles, change his Position. /// /// Direction to turn towards void DoTurn(Keys newDirection) { switch (newDirection) { case Keys.Up: direction_ = Direction.Up; usedFramesIndex_ = new int[] { 0, 7, 8 }; if (position_.DeltaPixel != Point.Zero) { position_.Tile.Y += 1; position_.DeltaPixel.Y -= 16; } break; case Keys.Down: direction_ = Direction.Down; usedFramesIndex_ = new int[] { 0, 3, 4 }; if (position_.DeltaPixel != Point.Zero) { position_.Tile.Y -= 1; position_.DeltaPixel.Y += 16; } break; case Keys.Left: direction_ = Direction.Left; usedFramesIndex_ = new int[] { 0, 5, 6 }; if (position_.DeltaPixel != Point.Zero) { position_.Tile.X += 1; position_.DeltaPixel.X -= 16; } break; case Keys.Right: direction_ = Direction.Right; usedFramesIndex_ = new int[] { 0, 1, 2 }; if (position_.DeltaPixel != Point.Zero) { position_.Tile.X -= 1; position_.DeltaPixel.X += 16; } break; } } Game game; TimeSpan timer_; SpriteBatch spriteBatch_; Texture2D dyingFrames_; Texture2D[] eatingFrames_; int[] usedFramesIndex_; int updatesPerPixel_; int updateCount_; Position position_; Direction direction_; State state_; public State State { get { return state_; } set { state_ = value; } } public Direction Direction { get { return direction_; } } public Position Position { get { return position_; } } } }