2
0

GameLoop.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Microsoft.Xna.Framework;
  5. using Microsoft.Xna.Framework.Audio;
  6. using Microsoft.Xna.Framework.Graphics;
  7. using Microsoft.Xna.Framework.Input;
  8. namespace XNAPacMan {
  9. /// <summary>
  10. /// Defines a position on the board where a ghost has died or a fruit was eaten, as well as the score earned.
  11. /// This is used for knowing where to draw those scores
  12. /// </summary>
  13. struct ScoreEvent {
  14. public ScoreEvent(Position position, DateTime when, int score) {
  15. Position = position;
  16. When = when;
  17. Score = score;
  18. }
  19. public Position Position;
  20. public DateTime When;
  21. public int Score;
  22. }
  23. /// <summary>
  24. /// GameLoop is the main "game" object; this is basically where the action
  25. /// takes place. It's responsible for coordinating broad game logic,
  26. /// drawing the board and scores, as well as linking with the menu.
  27. /// </summary>
  28. public class GameLoop : Microsoft.Xna.Framework.DrawableGameComponent {
  29. public GameLoop(Game game)
  30. : base(game) {
  31. // TODO: Construct any child components here
  32. }
  33. /// <summary>
  34. /// Allows the game component to perform any initialization it needs to before starting
  35. /// to run. This is where it can query for any required services and load content.
  36. /// </summary>
  37. public override void Initialize() {
  38. // We don't want XNA calling this method each time we resume from the menu,
  39. // unfortunately, it'll call it whatever we try. So the only thing
  40. // we can do is check if it has been called already and return. Yes, it's ugly.
  41. if (spriteBatch_ != null) {
  42. GhostSoundsManager.ResumeLoops();
  43. return;
  44. }
  45. // Otherwise, this is the first time this component is Initialized, so proceed.
  46. GhostSoundsManager.Init(Game);
  47. Grid.Reset();
  48. Constants.Level = 1;
  49. spriteBatch_ = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
  50. graphics_ = (GraphicsDeviceManager)Game.Services.GetService(typeof(GraphicsDeviceManager));
  51. soundBank_ = (SoundBank)Game.Services.GetService(typeof(SoundBank));
  52. scoreFont_ = Game.Content.Load<SpriteFont>("Score");
  53. scoreEventFont_ = Game.Content.Load<SpriteFont>("ScoreEvent");
  54. xlife_ = Game.Content.Load<Texture2D>("sprites/ExtraLife");
  55. ppill_ = Game.Content.Load<Texture2D>("sprites/PowerPill");
  56. crump_ = Game.Content.Load<Texture2D>("sprites/Crump");
  57. board_ = Game.Content.Load<Texture2D>("sprites/Board");
  58. boardFlash_ = Game.Content.Load<Texture2D>("sprites/BoardFlash");
  59. bonusEaten_ = new Dictionary<string, int>();
  60. bonus_ = new Dictionary<string, Texture2D>(9);
  61. bonus_.Add("Apple", Game.Content.Load<Texture2D>("bonus/Apple"));
  62. bonus_.Add("Banana", Game.Content.Load<Texture2D>("bonus/Banana"));
  63. bonus_.Add("Bell", Game.Content.Load<Texture2D>("bonus/Bell"));
  64. bonus_.Add("Cherry", Game.Content.Load<Texture2D>("bonus/Cherry"));
  65. bonus_.Add("Key", Game.Content.Load<Texture2D>("bonus/Key"));
  66. bonus_.Add("Orange", Game.Content.Load<Texture2D>("bonus/Orange"));
  67. bonus_.Add("Pear", Game.Content.Load<Texture2D>("bonus/Pear"));
  68. bonus_.Add("Pretzel", Game.Content.Load<Texture2D>("bonus/Pretzel"));
  69. bonus_.Add("Strawberry", Game.Content.Load<Texture2D>("bonus/Strawberry"));
  70. scoreEvents_ = new List<ScoreEvent>(5);
  71. bonusPresent_ = false;
  72. bonusSpawned_ = 0;
  73. eatenGhosts_ = 0;
  74. Score = 0;
  75. xlives_ = 2;
  76. paChomp_ = true;
  77. playerDied_ = false;
  78. player_ = new Player(Game);
  79. ghosts_ = new List<Ghost> { new Ghost(Game, player_, Ghosts.Blinky), new Ghost(Game, player_, Ghosts.Clyde),
  80. new Ghost(Game, player_, Ghosts.Inky), new Ghost(Game, player_, Ghosts.Pinky)};
  81. ghosts_[2].SetBlinky(ghosts_[0]); // Oh, dirty hack. Inky needs this for his AI.
  82. soundBank_.PlayCue("Intro");
  83. LockTimer = TimeSpan.FromMilliseconds(4500);
  84. base.Initialize();
  85. }
  86. /// <summary>
  87. /// Allows the game component to update itself.
  88. /// </summary>
  89. /// <param name="gameTime">Provides a snapshot of timing values.</param>
  90. public override void Update(GameTime gameTime) {
  91. // Some events (death, new level, etc.) lock the game for a few moments.
  92. if (DateTime.Now - eventTimer_ < LockTimer) {
  93. ghosts_.ForEach(i => i.LockTimer(gameTime));
  94. // Also we need to do the same thing for our own timer concerning bonuses
  95. bonusSpawnedTime_ += gameTime.ElapsedGameTime;
  96. return;
  97. }
  98. // Remove special events older than 5 seconds
  99. scoreEvents_.RemoveAll(i => DateTime.Now - i.When > TimeSpan.FromSeconds(5));
  100. // If the player had died, spawn a new one or end game.
  101. if (playerDied_) {
  102. // extra lives are decremented here, at the same time the pac man is spawned; this makes those
  103. // events seem linked.
  104. xlives_--;
  105. //xlives_++; // Give infinite lives to the evil developer;
  106. if (xlives_ >= 0) {
  107. playerDied_ = false;
  108. player_ = new Player(Game);
  109. ghosts_.ForEach(i => i.Reset(false, player_));
  110. scoreEvents_.Clear();
  111. }
  112. else { // The game is over
  113. Menu.SaveHighScore(Score);
  114. Game.Components.Add(new Menu(Game, null));
  115. Game.Components.Remove(this);
  116. GhostSoundsManager.StopLoops();
  117. return;
  118. }
  119. }
  120. // When all crumps have been eaten, wait a few seconds and then spawn a new level
  121. if (noCrumpsLeft()) {
  122. if (Constants.Level < 21) {
  123. bonusSpawned_ = 0;
  124. Grid.Reset();
  125. player_ = new Player(Game);
  126. ghosts_.ForEach(i => i.Reset(true, player_));
  127. soundBank_.PlayCue("NewLevel");
  128. LockTimer = TimeSpan.FromSeconds(2);
  129. Constants.Level++;
  130. return;
  131. }
  132. else { // Game over, you win.
  133. Menu.SaveHighScore(Score);
  134. Game.Components.Add(new Menu(Game, null));
  135. Game.Components.Remove(this);
  136. GhostSoundsManager.StopLoops();
  137. return;
  138. }
  139. }
  140. Keys[] inputKeys = Keyboard.GetState().GetPressedKeys();
  141. // The user may escape to the main menu with the escape key
  142. if (inputKeys.Contains(Keys.Escape)) {
  143. Game.Components.Add(new Menu(Game, this));
  144. Game.Components.Remove(this);
  145. GhostSoundsManager.PauseLoops(); // will be resumed in Initialize(). No need for stopping them
  146. // if the player subsequently quits the game, since we'll re-initialize GhostSoundManager in
  147. // Initialize() if the player wants to start a new game.
  148. return;
  149. }
  150. // Eat crumps and power pills.
  151. if (player_.Position.DeltaPixel == Point.Zero) {
  152. Point playerTile = player_.Position.Tile;
  153. if (Grid.TileGrid[playerTile.X, playerTile.Y].HasCrump) {
  154. soundBank_.PlayCue(paChomp_ ? "PacMAnEat1" : "PacManEat2");
  155. paChomp_ = !paChomp_;
  156. Score += 10;
  157. Grid.TileGrid[playerTile.X, playerTile.Y].HasCrump = false;
  158. if (Grid.TileGrid[playerTile.X, playerTile.Y].HasPowerPill) {
  159. Score += 40;
  160. eatenGhosts_ = 0;
  161. for (int i = 0; i < ghosts_.Count; i++) {
  162. if (ghosts_[i].State == GhostState.Attack || ghosts_[i].State == GhostState.Scatter ||
  163. ghosts_[i].State == GhostState.Blue) {
  164. ghosts_[i].State = GhostState.Blue;
  165. }
  166. }
  167. Grid.TileGrid[playerTile.X, playerTile.Y].HasPowerPill = false;
  168. }
  169. // If that was the last crump, lock the game for a while
  170. if (noCrumpsLeft()) {
  171. GhostSoundsManager.StopLoops();
  172. LockTimer = TimeSpan.FromSeconds(2);
  173. return;
  174. }
  175. }
  176. }
  177. // Eat bonuses
  178. if (bonusPresent_ && player_.Position.Tile.Y == 17 &&
  179. ((player_.Position.Tile.X == 13 && player_.Position.DeltaPixel.X == 8) ||
  180. (player_.Position.Tile.X == 14 && player_.Position.DeltaPixel.X == -8))) {
  181. LockTimer = TimeSpan.FromSeconds(1.5);
  182. Score += Constants.BonusScores();
  183. scoreEvents_.Add(new ScoreEvent(player_.Position, DateTime.Now, Constants.BonusScores()));
  184. soundBank_.PlayCue("fruiteat");
  185. bonusPresent_ = false;
  186. if (bonusEaten_.ContainsKey(Constants.BonusSprite())) {
  187. bonusEaten_[Constants.BonusSprite()]++;
  188. }
  189. else {
  190. bonusEaten_.Add(Constants.BonusSprite(), 1);
  191. }
  192. }
  193. // Remove bonus if time's up
  194. if (bonusPresent_ && ((DateTime.Now - bonusSpawnedTime_) > TimeSpan.FromSeconds(10))) {
  195. bonusPresent_ = false;
  196. }
  197. // Detect collision between ghosts and the player
  198. foreach (Ghost ghost in ghosts_) {
  199. Rectangle playerArea = new Rectangle((player_.Position.Tile.X * 16) + player_.Position.DeltaPixel.X,
  200. (player_.Position.Tile.Y * 16) + player_.Position.DeltaPixel.Y,
  201. 26,
  202. 26);
  203. Rectangle ghostArea = new Rectangle((ghost.Position.Tile.X * 16) + ghost.Position.DeltaPixel.X,
  204. (ghost.Position.Tile.Y * 16) + ghost.Position.DeltaPixel.Y,
  205. 22,
  206. 22);
  207. if (!Rectangle.Intersect(playerArea, ghostArea).IsEmpty) {
  208. // If collision detected, either kill the ghost or kill the pac man, depending on state.
  209. if (ghost.State == GhostState.Blue) {
  210. GhostSoundsManager.StopLoops();
  211. soundBank_.PlayCue("EatGhost");
  212. ghost.State = GhostState.Dead;
  213. eatenGhosts_++;
  214. int bonus = (int)(100 * Math.Pow(2, eatenGhosts_));
  215. Score += bonus;
  216. scoreEvents_.Add(new ScoreEvent(ghost.Position, DateTime.Now, bonus));
  217. LockTimer = TimeSpan.FromMilliseconds(900);
  218. return;
  219. }
  220. else if (ghost.State != GhostState.Dead ) {
  221. KillPacMan();
  222. return;
  223. }
  224. // Otherwise ( = the ghost is dead), don't do anything special.
  225. }
  226. }
  227. // Periodically spawn a fruit, when the player isn't on the spawn location
  228. // otherwise we get an infinite fruit spawning bug
  229. if ((Grid.NumCrumps == 180 || Grid.NumCrumps == 80) && bonusSpawned_ < 2 &&
  230. ! (player_.Position.Tile.Y == 17 &&
  231. ((player_.Position.Tile.X == 13 && player_.Position.DeltaPixel.X == 8) ||
  232. (player_.Position.Tile.X == 14 && player_.Position.DeltaPixel.X == -8)))) {
  233. bonusPresent_ = true;
  234. bonusSpawned_++;
  235. bonusSpawnedTime_ = DateTime.Now;
  236. }
  237. // Now is the time to move player based on inputs and ghosts based on AI
  238. // If we have returned earlier in the method, they stay in place
  239. player_.Update(gameTime);
  240. ghosts_.ForEach(i => i.Update(gameTime));
  241. base.Update(gameTime);
  242. }
  243. /// <summary>
  244. /// Nice to have for debug purposes. We might want the level to end early.
  245. /// </summary>
  246. /// <returns>Whether there are no crumps left on the board.</returns>
  247. bool noCrumpsLeft() {
  248. return Grid.NumCrumps == 0;
  249. }
  250. /// <summary>
  251. /// AAAARRRGH
  252. /// </summary>
  253. void KillPacMan() {
  254. player_.State = State.Dying;
  255. GhostSoundsManager.StopLoops();
  256. soundBank_.PlayCue("Death");
  257. LockTimer = TimeSpan.FromMilliseconds(1811);
  258. playerDied_ = true;
  259. bonusPresent_ = false;
  260. bonusSpawned_ = 0;
  261. }
  262. /// <summary>
  263. /// This is called when the game should draw itself.
  264. /// </summary>
  265. /// <param name="gameTime">Provides a snapshot of timing values.</param>
  266. public override void Draw(GameTime gameTime) {
  267. base.Draw(gameTime);
  268. // The GameLoop is a main component, so it is responsible for initializing the sprite batch each frame
  269. spriteBatch_.Begin();
  270. Vector2 boardPosition = new Vector2(
  271. (graphics_.PreferredBackBufferWidth / 2) - (board_.Width / 2),
  272. (graphics_.PreferredBackBufferHeight / 2) - (board_.Height / 2)
  273. );
  274. // When all crumps have been eaten, flash until new level is spawned
  275. // Draw the player and nothing else, just end the spritebatch and return.
  276. if (noCrumpsLeft()) {
  277. spriteBatch_.Draw(((DateTime.Now.Second * 1000 + DateTime.Now.Millisecond) / 350) % 2 == 0 ? board_ : boardFlash_, boardPosition, Color.White);
  278. player_.Draw(gameTime, boardPosition);
  279. spriteBatch_.End();
  280. return;
  281. }
  282. // Otherwise...
  283. // Draw the board
  284. spriteBatch_.Draw(board_, boardPosition, Color.White);
  285. // Draw crumps and power pills
  286. Tile[,] tiles = Grid.TileGrid;
  287. for (int j = 0; j < Grid.Height; j++) {
  288. for (int i = 0; i < Grid.Width; i++) {
  289. if (tiles[i, j].HasPowerPill) {
  290. spriteBatch_.Draw(ppill_, new Vector2(
  291. boardPosition.X + 3 + (i * 16),
  292. boardPosition.Y + 3 + (j * 16)),
  293. Color.White);
  294. }
  295. else if (tiles[i, j].HasCrump) {
  296. spriteBatch_.Draw(crump_, new Vector2(
  297. boardPosition.X + 5 + (i * 16),
  298. boardPosition.Y + 5 + (j * 16)),
  299. Color.White);
  300. }
  301. }
  302. }
  303. // Draw extra lives; no more than 20 though
  304. for (int i = 0; i < xlives_ && i < 20; i++) {
  305. spriteBatch_.Draw(xlife_, new Vector2(boardPosition.X + 10 + (20 * i), board_.Height + boardPosition.Y + 10), Color.White);
  306. }
  307. // Draw current score
  308. spriteBatch_.DrawString(scoreFont_, "SCORE", new Vector2(boardPosition.X + 30, boardPosition.Y - 50), Color.White);
  309. spriteBatch_.DrawString(scoreFont_, Score.ToString(), new Vector2(boardPosition.X + 30, boardPosition.Y - 30), Color.White);
  310. // Draw current level
  311. spriteBatch_.DrawString(scoreFont_, "LEVEL", new Vector2(boardPosition.X + board_.Width - 80, boardPosition.Y - 50), Color.White);
  312. spriteBatch_.DrawString(scoreFont_, Constants.Level.ToString(), new Vector2(boardPosition.X + board_.Width - 80, boardPosition.Y - 30), Color.White);
  313. // Draw a bonus fruit if any
  314. if (bonusPresent_) {
  315. spriteBatch_.Draw(bonus_[Constants.BonusSprite()], new Vector2(boardPosition.X + (13 * 16) + 2, boardPosition.Y + (17 * 16) - 8), Color.White);
  316. }
  317. // Draw captured bonus fruits at the bottom of the screen
  318. int k = 0;
  319. foreach (KeyValuePair<string, int> kvp in bonusEaten_) {
  320. for (int i = 0; i < kvp.Value; i++) {
  321. spriteBatch_.Draw(bonus_[kvp.Key], new Vector2(boardPosition.X + 10 + (22 * (k + i)), board_.Height + boardPosition.Y + 22), Color.White);
  322. }
  323. k += kvp.Value;
  324. }
  325. // Draw ghosts
  326. ghosts_.ForEach( i => i.Draw(gameTime, boardPosition));
  327. // Draw player
  328. player_.Draw(gameTime, boardPosition);
  329. // Draw special scores (as when a ghost or fruit has been eaten)
  330. foreach (ScoreEvent se in scoreEvents_) {
  331. spriteBatch_.DrawString(scoreEventFont_, se.Score.ToString(), new Vector2(boardPosition.X + (se.Position.Tile.X * 16) + se.Position.DeltaPixel.X + 4,
  332. boardPosition.Y + (se.Position.Tile.Y * 16) + se.Position.DeltaPixel.Y + 4), Color.White);
  333. }
  334. // Draw GET READY ! at level start
  335. if (player_.State == State.Start) {
  336. spriteBatch_.DrawString(scoreFont_, "GET READY!", new Vector2(boardPosition.X + (board_.Width / 2) - 58, boardPosition.Y + 273), Color.Yellow);
  337. }
  338. // Display number of crumps (for debug)
  339. //spriteBatch_.DrawString(scoreFont_, "Crumps left :" + Grid.NumCrumps.ToString(), Vector2.Zero, Color.White);
  340. spriteBatch_.End();
  341. }
  342. // DRAWING
  343. Dictionary<string, Texture2D> bonus_;
  344. Texture2D xlife_;
  345. Texture2D board_;
  346. Texture2D boardFlash_;
  347. Texture2D crump_;
  348. Texture2D ppill_;
  349. SpriteFont scoreFont_;
  350. SpriteFont scoreEventFont_;
  351. SoundBank soundBank_;
  352. GraphicsDeviceManager graphics_;
  353. SpriteBatch spriteBatch_;
  354. // LOGIC
  355. List<Ghost> ghosts_;
  356. Player player_;
  357. TimeSpan lockTimer_;
  358. DateTime eventTimer_;
  359. int bonusSpawned_;
  360. bool bonusPresent_;
  361. DateTime bonusSpawnedTime_;
  362. Dictionary<string, int> bonusEaten_;
  363. bool playerDied_;
  364. bool paChomp_;
  365. int xlives_;
  366. int score_;
  367. int eatenGhosts_;
  368. List<ScoreEvent> scoreEvents_;
  369. /// <summary>
  370. /// The player's current score.
  371. /// </summary>
  372. public int Score {
  373. get { return score_; }
  374. private set {
  375. if ((value / 10000) > (score_ / 10000)) {
  376. soundBank_.PlayCue("ExtraLife");
  377. xlives_++;
  378. }
  379. score_ = value;
  380. }
  381. }
  382. /// <summary>
  383. /// For how much time we want to lock the game.
  384. /// </summary>
  385. private TimeSpan LockTimer {
  386. get { return lockTimer_; }
  387. set { eventTimer_ = DateTime.Now; lockTimer_ = value; }
  388. }
  389. }
  390. }