Level.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // Level.cs
  4. //
  5. // Microsoft XNA Community Game Platform
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. using System;
  10. using System.Collections.Generic;
  11. using Microsoft.Xna.Framework;
  12. using Microsoft.Xna.Framework.Content;
  13. using Microsoft.Xna.Framework.Graphics;
  14. using Microsoft.Xna.Framework.Audio;
  15. using System.IO;
  16. using Microsoft.Xna.Framework.Input.Touch;
  17. using Microsoft.Xna.Framework.Input;
  18. namespace Platformer
  19. {
  20. /// <summary>
  21. /// A uniform grid of tiles with collections of gems and enemies.
  22. /// The level owns the player and controls the game's win and lose
  23. /// conditions as well as scoring.
  24. /// </summary>
  25. class Level : IDisposable
  26. {
  27. // Physical structure of the level.
  28. private Tile[,] tiles;
  29. private Texture2D[] layers;
  30. // The layer which entities are drawn on top of.
  31. private const int EntityLayer = 2;
  32. // Entities in the level.
  33. public Player Player
  34. {
  35. get { return player; }
  36. }
  37. Player player;
  38. private List<Gem> gems = new List<Gem>();
  39. private List<Enemy> enemies = new List<Enemy>();
  40. // Key locations in the level.
  41. private Vector2 start;
  42. private Point exit = InvalidPosition;
  43. private static readonly Point InvalidPosition = new Point(-1, -1);
  44. // Level game state.
  45. private Random random = new Random(354668); // Arbitrary, but constant seed
  46. public int Score
  47. {
  48. get { return score; }
  49. }
  50. int score;
  51. public bool ReachedExit
  52. {
  53. get { return reachedExit; }
  54. }
  55. bool reachedExit;
  56. public TimeSpan TimeRemaining
  57. {
  58. get { return timeRemaining; }
  59. }
  60. TimeSpan timeRemaining;
  61. private const int PointsPerSecond = 5;
  62. // Level content.
  63. public ContentManager Content
  64. {
  65. get { return content; }
  66. }
  67. ContentManager content;
  68. private SoundEffect exitReachedSound;
  69. #region Loading
  70. /// <summary>
  71. /// Constructs a new level.
  72. /// </summary>
  73. /// <param name="serviceProvider">
  74. /// The service provider that will be used to construct a ContentManager.
  75. /// </param>
  76. /// <param name="fileStream">
  77. /// A stream containing the tile data.
  78. /// </param>
  79. public Level(IServiceProvider serviceProvider, Stream fileStream, int levelIndex)
  80. {
  81. // Create a new content manager to load content used just by this level.
  82. content = new ContentManager(serviceProvider, "Content");
  83. timeRemaining = TimeSpan.FromMinutes(2.0);
  84. LoadTiles(fileStream);
  85. // Load background layer textures. For now, all levels must
  86. // use the same backgrounds and only use the left-most part of them.
  87. layers = new Texture2D[3];
  88. for (int i = 0; i < layers.Length; ++i)
  89. {
  90. // Choose a random segment if each background layer for level variety.
  91. int segmentIndex = levelIndex;
  92. layers[i] = Content.Load<Texture2D>("Backgrounds/Layer" + i + "_" + segmentIndex);
  93. }
  94. // Load sounds.
  95. exitReachedSound = Content.Load<SoundEffect>("Sounds/ExitReached");
  96. }
  97. /// <summary>
  98. /// Iterates over every tile in the structure file and loads its
  99. /// appearance and behavior. This method also validates that the
  100. /// file is well-formed with a player start point, exit, etc.
  101. /// </summary>
  102. /// <param name="fileStream">
  103. /// A stream containing the tile data.
  104. /// </param>
  105. private void LoadTiles(Stream fileStream)
  106. {
  107. // Load the level and ensure all of the lines are the same length.
  108. int width;
  109. List<string> lines = new List<string>();
  110. using (StreamReader reader = new StreamReader(fileStream))
  111. {
  112. string line = reader.ReadLine();
  113. width = line.Length;
  114. while (line != null)
  115. {
  116. lines.Add(line);
  117. if (line.Length != width)
  118. throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
  119. line = reader.ReadLine();
  120. }
  121. }
  122. // Allocate the tile grid.
  123. tiles = new Tile[width, lines.Count];
  124. // Loop over every tile position,
  125. for (int y = 0; y < Height; ++y)
  126. {
  127. for (int x = 0; x < Width; ++x)
  128. {
  129. // to load each tile.
  130. char tileType = lines[y][x];
  131. tiles[x, y] = LoadTile(tileType, x, y);
  132. }
  133. }
  134. // Verify that the level has a beginning and an end.
  135. if (Player == null)
  136. throw new NotSupportedException("A level must have a starting point.");
  137. if (exit == InvalidPosition)
  138. throw new NotSupportedException("A level must have an exit.");
  139. }
  140. /// <summary>
  141. /// Loads an individual tile's appearance and behavior.
  142. /// </summary>
  143. /// <param name="tileType">
  144. /// The character loaded from the structure file which
  145. /// indicates what should be loaded.
  146. /// </param>
  147. /// <param name="x">
  148. /// The X location of this tile in tile space.
  149. /// </param>
  150. /// <param name="y">
  151. /// The Y location of this tile in tile space.
  152. /// </param>
  153. /// <returns>The loaded tile.</returns>
  154. private Tile LoadTile(char tileType, int x, int y)
  155. {
  156. switch (tileType)
  157. {
  158. // Blank space
  159. case '.':
  160. return new Tile(null, TileCollision.Passable);
  161. // Exit
  162. case 'X':
  163. return LoadExitTile(x, y);
  164. // Gem
  165. case 'G':
  166. return LoadGemTile(x, y);
  167. // Floating platform
  168. case '-':
  169. return LoadTile("Platform", TileCollision.Platform);
  170. // Various enemies
  171. case 'A':
  172. return LoadEnemyTile(x, y, "MonsterA");
  173. case 'B':
  174. return LoadEnemyTile(x, y, "MonsterB");
  175. case 'C':
  176. return LoadEnemyTile(x, y, "MonsterC");
  177. case 'D':
  178. return LoadEnemyTile(x, y, "MonsterD");
  179. // Platform block
  180. case '~':
  181. return LoadVarietyTile("BlockB", 2, TileCollision.Platform);
  182. // Passable block
  183. case ':':
  184. return LoadVarietyTile("BlockB", 2, TileCollision.Passable);
  185. // Player 1 start point
  186. case '1':
  187. return LoadStartTile(x, y);
  188. // Impassable block
  189. case '#':
  190. return LoadVarietyTile("BlockA", 7, TileCollision.Impassable);
  191. // Unknown tile type character
  192. default:
  193. throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
  194. }
  195. }
  196. /// <summary>
  197. /// Creates a new tile. The other tile loading methods typically chain to this
  198. /// method after performing their special logic.
  199. /// </summary>
  200. /// <param name="name">
  201. /// Path to a tile texture relative to the Content/Tiles directory.
  202. /// </param>
  203. /// <param name="collision">
  204. /// The tile collision type for the new tile.
  205. /// </param>
  206. /// <returns>The new tile.</returns>
  207. private Tile LoadTile(string name, TileCollision collision)
  208. {
  209. return new Tile(Content.Load<Texture2D>("Tiles/" + name), collision);
  210. }
  211. /// <summary>
  212. /// Loads a tile with a random appearance.
  213. /// </summary>
  214. /// <param name="baseName">
  215. /// The content name prefix for this group of tile variations. Tile groups are
  216. /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
  217. /// </param>
  218. /// <param name="variationCount">
  219. /// The number of variations in this group.
  220. /// </param>
  221. private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
  222. {
  223. int index = random.Next(variationCount);
  224. return LoadTile(baseName + index, collision);
  225. }
  226. /// <summary>
  227. /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
  228. /// </summary>
  229. private Tile LoadStartTile(int x, int y)
  230. {
  231. if (Player != null)
  232. throw new NotSupportedException("A level may only have one starting point.");
  233. start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
  234. player = new Player(this, start);
  235. return new Tile(null, TileCollision.Passable);
  236. }
  237. /// <summary>
  238. /// Remembers the location of the level's exit.
  239. /// </summary>
  240. private Tile LoadExitTile(int x, int y)
  241. {
  242. if (exit != InvalidPosition)
  243. throw new NotSupportedException("A level may only have one exit.");
  244. exit = GetBounds(x, y).Center;
  245. return LoadTile("Exit", TileCollision.Passable);
  246. }
  247. /// <summary>
  248. /// Instantiates an enemy and puts him in the level.
  249. /// </summary>
  250. private Tile LoadEnemyTile(int x, int y, string spriteSet)
  251. {
  252. Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
  253. enemies.Add(new Enemy(this, position, spriteSet));
  254. return new Tile(null, TileCollision.Passable);
  255. }
  256. /// <summary>
  257. /// Instantiates a gem and puts it in the level.
  258. /// </summary>
  259. private Tile LoadGemTile(int x, int y)
  260. {
  261. Point position = GetBounds(x, y).Center;
  262. gems.Add(new Gem(this, new Vector2(position.X, position.Y)));
  263. return new Tile(null, TileCollision.Passable);
  264. }
  265. /// <summary>
  266. /// Unloads the level content.
  267. /// </summary>
  268. public void Dispose()
  269. {
  270. Content.Unload();
  271. }
  272. #endregion
  273. #region Bounds and collision
  274. /// <summary>
  275. /// Gets the collision mode of the tile at a particular location.
  276. /// This method handles tiles outside of the levels boundries by making it
  277. /// impossible to escape past the left or right edges, but allowing things
  278. /// to jump beyond the top of the level and fall off the bottom.
  279. /// </summary>
  280. public TileCollision GetCollision(int x, int y)
  281. {
  282. // Prevent escaping past the level ends.
  283. if (x < 0 || x >= Width)
  284. return TileCollision.Impassable;
  285. // Allow jumping past the level top and falling through the bottom.
  286. if (y < 0 || y >= Height)
  287. return TileCollision.Passable;
  288. return tiles[x, y].Collision;
  289. }
  290. /// <summary>
  291. /// Gets the bounding rectangle of a tile in world space.
  292. /// </summary>
  293. public Rectangle GetBounds(int x, int y)
  294. {
  295. return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
  296. }
  297. /// <summary>
  298. /// Width of level measured in tiles.
  299. /// </summary>
  300. public int Width
  301. {
  302. get { return tiles.GetLength(0); }
  303. }
  304. /// <summary>
  305. /// Height of the level measured in tiles.
  306. /// </summary>
  307. public int Height
  308. {
  309. get { return tiles.GetLength(1); }
  310. }
  311. #endregion
  312. #region Update
  313. /// <summary>
  314. /// Updates all objects in the world, performs collision between them,
  315. /// and handles the time limit with scoring.
  316. /// </summary>
  317. public void Update(
  318. GameTime gameTime,
  319. KeyboardState keyboardState,
  320. GamePadState gamePadState,
  321. TouchCollection touchState,
  322. AccelerometerState accelState,
  323. DisplayOrientation orientation)
  324. {
  325. // Pause while the player is dead or time is expired.
  326. if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
  327. {
  328. // Still want to perform physics on the player.
  329. Player.ApplyPhysics(gameTime);
  330. }
  331. else if (ReachedExit)
  332. {
  333. // Animate the time being converted into points.
  334. int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
  335. seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
  336. timeRemaining -= TimeSpan.FromSeconds(seconds);
  337. score += seconds * PointsPerSecond;
  338. }
  339. else
  340. {
  341. timeRemaining -= gameTime.ElapsedGameTime;
  342. Player.Update(gameTime, keyboardState, gamePadState, touchState, accelState, orientation);
  343. UpdateGems(gameTime);
  344. // Falling off the bottom of the level kills the player.
  345. if (Player.BoundingRectangle.Top >= Height * Tile.Height)
  346. OnPlayerKilled(null);
  347. UpdateEnemies(gameTime);
  348. // The player has reached the exit if they are standing on the ground and
  349. // his bounding rectangle contains the center of the exit tile. They can only
  350. // exit when they have collected all of the gems.
  351. if (Player.IsAlive &&
  352. Player.IsOnGround &&
  353. Player.BoundingRectangle.Contains(exit))
  354. {
  355. OnExitReached();
  356. }
  357. }
  358. // Clamp the time remaining at zero.
  359. if (timeRemaining < TimeSpan.Zero)
  360. timeRemaining = TimeSpan.Zero;
  361. }
  362. /// <summary>
  363. /// Animates each gem and checks to allows the player to collect them.
  364. /// </summary>
  365. private void UpdateGems(GameTime gameTime)
  366. {
  367. for (int i = 0; i < gems.Count; ++i)
  368. {
  369. Gem gem = gems[i];
  370. gem.Update(gameTime);
  371. if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
  372. {
  373. gems.RemoveAt(i--);
  374. OnGemCollected(gem, Player);
  375. }
  376. }
  377. }
  378. /// <summary>
  379. /// Animates each enemy and allow them to kill the player.
  380. /// </summary>
  381. private void UpdateEnemies(GameTime gameTime)
  382. {
  383. foreach (Enemy enemy in enemies)
  384. {
  385. enemy.Update(gameTime);
  386. // Touching an enemy instantly kills the player
  387. if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
  388. {
  389. OnPlayerKilled(enemy);
  390. }
  391. }
  392. }
  393. /// <summary>
  394. /// Called when a gem is collected.
  395. /// </summary>
  396. /// <param name="gem">The gem that was collected.</param>
  397. /// <param name="collectedBy">The player who collected this gem.</param>
  398. private void OnGemCollected(Gem gem, Player collectedBy)
  399. {
  400. score += Gem.PointValue;
  401. gem.OnCollected(collectedBy);
  402. }
  403. /// <summary>
  404. /// Called when the player is killed.
  405. /// </summary>
  406. /// <param name="killedBy">
  407. /// The enemy who killed the player. This is null if the player was not killed by an
  408. /// enemy, such as when a player falls into a hole.
  409. /// </param>
  410. private void OnPlayerKilled(Enemy killedBy)
  411. {
  412. Player.OnKilled(killedBy);
  413. }
  414. /// <summary>
  415. /// Called when the player reaches the level's exit.
  416. /// </summary>
  417. private void OnExitReached()
  418. {
  419. Player.OnReachedExit();
  420. exitReachedSound.Play();
  421. reachedExit = true;
  422. }
  423. /// <summary>
  424. /// Restores the player to the starting point to try the level again.
  425. /// </summary>
  426. public void StartNewLife()
  427. {
  428. Player.Reset(start);
  429. }
  430. #endregion
  431. #region Draw
  432. /// <summary>
  433. /// Draw everything in the level from background to foreground.
  434. /// </summary>
  435. public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
  436. {
  437. for (int i = 0; i <= EntityLayer; ++i)
  438. spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
  439. DrawTiles(spriteBatch);
  440. foreach (Gem gem in gems)
  441. gem.Draw(gameTime, spriteBatch);
  442. Player.Draw(gameTime, spriteBatch);
  443. foreach (Enemy enemy in enemies)
  444. enemy.Draw(gameTime, spriteBatch);
  445. for (int i = EntityLayer + 1; i < layers.Length; ++i)
  446. spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
  447. }
  448. /// <summary>
  449. /// Draws each tile in the level.
  450. /// </summary>
  451. private void DrawTiles(SpriteBatch spriteBatch)
  452. {
  453. // For each tile position
  454. for (int y = 0; y < Height; ++y)
  455. {
  456. for (int x = 0; x < Width; ++x)
  457. {
  458. // If there is a visible tile in that position
  459. Texture2D texture = tiles[x, y].Texture;
  460. if (texture != null)
  461. {
  462. // Draw it in screen space.
  463. Vector2 position = new Vector2(x, y) * Tile.Size;
  464. spriteBatch.Draw(texture, position, Color.White);
  465. }
  466. }
  467. }
  468. }
  469. #endregion
  470. }
  471. }