using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace XNAPacMan { /// /// Defines a position on the board where a ghost has died or a fruit was eaten, as well as the score earned. /// This is used for knowing where to draw those scores /// struct ScoreEvent { public ScoreEvent(Position position, DateTime when, int score) { Position = position; When = when; Score = score; } public Position Position; public DateTime When; public int Score; } /// /// GameLoop is the main "game" object; this is basically where the action /// takes place. It's responsible for coordinating broad game logic, /// drawing the board and scores, as well as linking with the menu. /// public class GameLoop : Microsoft.Xna.Framework.DrawableGameComponent { public GameLoop(Game game) : base(game) { // TODO: Construct any child components here } /// /// Allows the game component to perform any initialization it needs to before starting /// to run. This is where it can query for any required services and load content. /// public override void Initialize() { // We don't want XNA calling this method each time we resume from the menu, // unfortunately, it'll call it whatever we try. So the only thing // we can do is check if it has been called already and return. Yes, it's ugly. if (spriteBatch_ != null) { GhostSoundsManager.ResumeLoops(); return; } // Otherwise, this is the first time this component is Initialized, so proceed. GhostSoundsManager.Init(Game); Grid.Reset(); Constants.Level = 1; spriteBatch_ = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch)); graphics_ = (GraphicsDeviceManager)Game.Services.GetService(typeof(GraphicsDeviceManager)); soundBank_ = (SoundBank)Game.Services.GetService(typeof(SoundBank)); scoreFont_ = Game.Content.Load("Score"); scoreEventFont_ = Game.Content.Load("ScoreEvent"); xlife_ = Game.Content.Load("sprites/ExtraLife"); ppill_ = Game.Content.Load("sprites/PowerPill"); crump_ = Game.Content.Load("sprites/Crump"); board_ = Game.Content.Load("sprites/Board"); boardFlash_ = Game.Content.Load("sprites/BoardFlash"); bonusEaten_ = new Dictionary(); bonus_ = new Dictionary(9); bonus_.Add("Apple", Game.Content.Load("bonus/Apple")); bonus_.Add("Banana", Game.Content.Load("bonus/Banana")); bonus_.Add("Bell", Game.Content.Load("bonus/Bell")); bonus_.Add("Cherry", Game.Content.Load("bonus/Cherry")); bonus_.Add("Key", Game.Content.Load("bonus/Key")); bonus_.Add("Orange", Game.Content.Load("bonus/Orange")); bonus_.Add("Pear", Game.Content.Load("bonus/Pear")); bonus_.Add("Pretzel", Game.Content.Load("bonus/Pretzel")); bonus_.Add("Strawberry", Game.Content.Load("bonus/Strawberry")); scoreEvents_ = new List(5); bonusPresent_ = false; bonusSpawned_ = 0; eatenGhosts_ = 0; Score = 0; xlives_ = 2; paChomp_ = true; playerDied_ = false; player_ = new Player(Game); ghosts_ = new List { new Ghost(Game, player_, Ghosts.Blinky), new Ghost(Game, player_, Ghosts.Clyde), new Ghost(Game, player_, Ghosts.Inky), new Ghost(Game, player_, Ghosts.Pinky)}; ghosts_[2].SetBlinky(ghosts_[0]); // Oh, dirty hack. Inky needs this for his AI. soundBank_.PlayCue("Intro"); LockTimer = TimeSpan.FromMilliseconds(4500); base.Initialize(); } /// /// Allows the game component to update itself. /// /// Provides a snapshot of timing values. public override void Update(GameTime gameTime) { // Some events (death, new level, etc.) lock the game for a few moments. if (DateTime.Now - eventTimer_ < LockTimer) { ghosts_.ForEach(i => i.LockTimer(gameTime)); // Also we need to do the same thing for our own timer concerning bonuses bonusSpawnedTime_ += gameTime.ElapsedGameTime; return; } // Remove special events older than 5 seconds scoreEvents_.RemoveAll(i => DateTime.Now - i.When > TimeSpan.FromSeconds(5)); // If the player had died, spawn a new one or end game. if (playerDied_) { // extra lives are decremented here, at the same time the pac man is spawned; this makes those // events seem linked. xlives_--; //xlives_++; // Give infinite lives to the evil developer; if (xlives_ >= 0) { playerDied_ = false; player_ = new Player(Game); ghosts_.ForEach(i => i.Reset(false, player_)); scoreEvents_.Clear(); } else { // The game is over Menu.SaveHighScore(Score); Game.Components.Add(new Menu(Game, null)); Game.Components.Remove(this); GhostSoundsManager.StopLoops(); return; } } // When all crumps have been eaten, wait a few seconds and then spawn a new level if (noCrumpsLeft()) { if (Constants.Level < 21) { bonusSpawned_ = 0; Grid.Reset(); player_ = new Player(Game); ghosts_.ForEach(i => i.Reset(true, player_)); soundBank_.PlayCue("NewLevel"); LockTimer = TimeSpan.FromSeconds(2); Constants.Level++; return; } else { // Game over, you win. Menu.SaveHighScore(Score); Game.Components.Add(new Menu(Game, null)); Game.Components.Remove(this); GhostSoundsManager.StopLoops(); return; } } Keys[] inputKeys = Keyboard.GetState().GetPressedKeys(); // The user may escape to the main menu with the escape key if (inputKeys.Contains(Keys.Escape)) { Game.Components.Add(new Menu(Game, this)); Game.Components.Remove(this); GhostSoundsManager.PauseLoops(); // will be resumed in Initialize(). No need for stopping them // if the player subsequently quits the game, since we'll re-initialize GhostSoundManager in // Initialize() if the player wants to start a new game. return; } // Eat crumps and power pills. if (player_.Position.DeltaPixel == Point.Zero) { Point playerTile = player_.Position.Tile; if (Grid.TileGrid[playerTile.X, playerTile.Y].HasCrump) { soundBank_.PlayCue(paChomp_ ? "PacMAnEat1" : "PacManEat2"); paChomp_ = !paChomp_; Score += 10; Grid.TileGrid[playerTile.X, playerTile.Y].HasCrump = false; if (Grid.TileGrid[playerTile.X, playerTile.Y].HasPowerPill) { Score += 40; eatenGhosts_ = 0; for (int i = 0; i < ghosts_.Count; i++) { if (ghosts_[i].State == GhostState.Attack || ghosts_[i].State == GhostState.Scatter || ghosts_[i].State == GhostState.Blue) { ghosts_[i].State = GhostState.Blue; } } Grid.TileGrid[playerTile.X, playerTile.Y].HasPowerPill = false; } // If that was the last crump, lock the game for a while if (noCrumpsLeft()) { GhostSoundsManager.StopLoops(); LockTimer = TimeSpan.FromSeconds(2); return; } } } // Eat bonuses if (bonusPresent_ && player_.Position.Tile.Y == 17 && ((player_.Position.Tile.X == 13 && player_.Position.DeltaPixel.X == 8) || (player_.Position.Tile.X == 14 && player_.Position.DeltaPixel.X == -8))) { LockTimer = TimeSpan.FromSeconds(1.5); Score += Constants.BonusScores(); scoreEvents_.Add(new ScoreEvent(player_.Position, DateTime.Now, Constants.BonusScores())); soundBank_.PlayCue("fruiteat"); bonusPresent_ = false; if (bonusEaten_.ContainsKey(Constants.BonusSprite())) { bonusEaten_[Constants.BonusSprite()]++; } else { bonusEaten_.Add(Constants.BonusSprite(), 1); } } // Remove bonus if time's up if (bonusPresent_ && ((DateTime.Now - bonusSpawnedTime_) > TimeSpan.FromSeconds(10))) { bonusPresent_ = false; } // Detect collision between ghosts and the player foreach (Ghost ghost in ghosts_) { Rectangle playerArea = new Rectangle((player_.Position.Tile.X * 16) + player_.Position.DeltaPixel.X, (player_.Position.Tile.Y * 16) + player_.Position.DeltaPixel.Y, 26, 26); Rectangle ghostArea = new Rectangle((ghost.Position.Tile.X * 16) + ghost.Position.DeltaPixel.X, (ghost.Position.Tile.Y * 16) + ghost.Position.DeltaPixel.Y, 22, 22); if (!Rectangle.Intersect(playerArea, ghostArea).IsEmpty) { // If collision detected, either kill the ghost or kill the pac man, depending on state. if (ghost.State == GhostState.Blue) { GhostSoundsManager.StopLoops(); soundBank_.PlayCue("EatGhost"); ghost.State = GhostState.Dead; eatenGhosts_++; int bonus = (int)(100 * Math.Pow(2, eatenGhosts_)); Score += bonus; scoreEvents_.Add(new ScoreEvent(ghost.Position, DateTime.Now, bonus)); LockTimer = TimeSpan.FromMilliseconds(900); return; } else if (ghost.State != GhostState.Dead ) { KillPacMan(); return; } // Otherwise ( = the ghost is dead), don't do anything special. } } // Periodically spawn a fruit, when the player isn't on the spawn location // otherwise we get an infinite fruit spawning bug if ((Grid.NumCrumps == 180 || Grid.NumCrumps == 80) && bonusSpawned_ < 2 && ! (player_.Position.Tile.Y == 17 && ((player_.Position.Tile.X == 13 && player_.Position.DeltaPixel.X == 8) || (player_.Position.Tile.X == 14 && player_.Position.DeltaPixel.X == -8)))) { bonusPresent_ = true; bonusSpawned_++; bonusSpawnedTime_ = DateTime.Now; } // Now is the time to move player based on inputs and ghosts based on AI // If we have returned earlier in the method, they stay in place player_.Update(gameTime); ghosts_.ForEach(i => i.Update(gameTime)); base.Update(gameTime); } /// /// Nice to have for debug purposes. We might want the level to end early. /// /// Whether there are no crumps left on the board. bool noCrumpsLeft() { return Grid.NumCrumps == 0; } /// /// AAAARRRGH /// void KillPacMan() { player_.State = State.Dying; GhostSoundsManager.StopLoops(); soundBank_.PlayCue("Death"); LockTimer = TimeSpan.FromMilliseconds(1811); playerDied_ = true; bonusPresent_ = false; bonusSpawned_ = 0; } /// /// This is called when the game should draw itself. /// /// Provides a snapshot of timing values. public override void Draw(GameTime gameTime) { base.Draw(gameTime); // The GameLoop is a main component, so it is responsible for initializing the sprite batch each frame spriteBatch_.Begin(); Vector2 boardPosition = new Vector2( (graphics_.PreferredBackBufferWidth / 2) - (board_.Width / 2), (graphics_.PreferredBackBufferHeight / 2) - (board_.Height / 2) ); // When all crumps have been eaten, flash until new level is spawned // Draw the player and nothing else, just end the spritebatch and return. if (noCrumpsLeft()) { spriteBatch_.Draw(((DateTime.Now.Second * 1000 + DateTime.Now.Millisecond) / 350) % 2 == 0 ? board_ : boardFlash_, boardPosition, Color.White); player_.Draw(gameTime, boardPosition); spriteBatch_.End(); return; } // Otherwise... // Draw the board spriteBatch_.Draw(board_, boardPosition, Color.White); // Draw crumps and power pills Tile[,] tiles = Grid.TileGrid; for (int j = 0; j < Grid.Height; j++) { for (int i = 0; i < Grid.Width; i++) { if (tiles[i, j].HasPowerPill) { spriteBatch_.Draw(ppill_, new Vector2( boardPosition.X + 3 + (i * 16), boardPosition.Y + 3 + (j * 16)), Color.White); } else if (tiles[i, j].HasCrump) { spriteBatch_.Draw(crump_, new Vector2( boardPosition.X + 5 + (i * 16), boardPosition.Y + 5 + (j * 16)), Color.White); } } } // Draw extra lives; no more than 20 though for (int i = 0; i < xlives_ && i < 20; i++) { spriteBatch_.Draw(xlife_, new Vector2(boardPosition.X + 10 + (20 * i), board_.Height + boardPosition.Y + 10), Color.White); } // Draw current score spriteBatch_.DrawString(scoreFont_, "SCORE", new Vector2(boardPosition.X + 30, boardPosition.Y - 50), Color.White); spriteBatch_.DrawString(scoreFont_, Score.ToString(), new Vector2(boardPosition.X + 30, boardPosition.Y - 30), Color.White); // Draw current level spriteBatch_.DrawString(scoreFont_, "LEVEL", new Vector2(boardPosition.X + board_.Width - 80, boardPosition.Y - 50), Color.White); spriteBatch_.DrawString(scoreFont_, Constants.Level.ToString(), new Vector2(boardPosition.X + board_.Width - 80, boardPosition.Y - 30), Color.White); // Draw a bonus fruit if any if (bonusPresent_) { spriteBatch_.Draw(bonus_[Constants.BonusSprite()], new Vector2(boardPosition.X + (13 * 16) + 2, boardPosition.Y + (17 * 16) - 8), Color.White); } // Draw captured bonus fruits at the bottom of the screen int k = 0; foreach (KeyValuePair kvp in bonusEaten_) { for (int i = 0; i < kvp.Value; i++) { spriteBatch_.Draw(bonus_[kvp.Key], new Vector2(boardPosition.X + 10 + (22 * (k + i)), board_.Height + boardPosition.Y + 22), Color.White); } k += kvp.Value; } // Draw ghosts ghosts_.ForEach( i => i.Draw(gameTime, boardPosition)); // Draw player player_.Draw(gameTime, boardPosition); // Draw special scores (as when a ghost or fruit has been eaten) foreach (ScoreEvent se in scoreEvents_) { spriteBatch_.DrawString(scoreEventFont_, se.Score.ToString(), new Vector2(boardPosition.X + (se.Position.Tile.X * 16) + se.Position.DeltaPixel.X + 4, boardPosition.Y + (se.Position.Tile.Y * 16) + se.Position.DeltaPixel.Y + 4), Color.White); } // Draw GET READY ! at level start if (player_.State == State.Start) { spriteBatch_.DrawString(scoreFont_, "GET READY!", new Vector2(boardPosition.X + (board_.Width / 2) - 58, boardPosition.Y + 273), Color.Yellow); } // Display number of crumps (for debug) //spriteBatch_.DrawString(scoreFont_, "Crumps left :" + Grid.NumCrumps.ToString(), Vector2.Zero, Color.White); spriteBatch_.End(); } // DRAWING Dictionary bonus_; Texture2D xlife_; Texture2D board_; Texture2D boardFlash_; Texture2D crump_; Texture2D ppill_; SpriteFont scoreFont_; SpriteFont scoreEventFont_; SoundBank soundBank_; GraphicsDeviceManager graphics_; SpriteBatch spriteBatch_; // LOGIC List ghosts_; Player player_; TimeSpan lockTimer_; DateTime eventTimer_; int bonusSpawned_; bool bonusPresent_; DateTime bonusSpawnedTime_; Dictionary bonusEaten_; bool playerDied_; bool paChomp_; int xlives_; int score_; int eatenGhosts_; List scoreEvents_; /// /// The player's current score. /// public int Score { get { return score_; } private set { if ((value / 10000) > (score_ / 10000)) { soundBank_.PlayCue("ExtraLife"); xlives_++; } score_ = value; } } /// /// For how much time we want to lock the game. /// private TimeSpan LockTimer { get { return lockTimer_; } set { eventTimer_ = DateTime.Now; lockTimer_ = value; } } } }