tasting on MonoGame
[tastes.git] / Platformer2D / Platformer2D.Core / Game / Level.cs
blob4f2e38aba51b6d483b871e6212dbc0c4554fbd2f
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
10 using System;
11 using System.Collections.Generic;
12 using Microsoft.Xna.Framework;
13 using Microsoft.Xna.Framework.Content;
14 using Microsoft.Xna.Framework.Graphics;
15 using Microsoft.Xna.Framework.Audio;
16 using System.IO;
17 using Microsoft.Xna.Framework.Input;
19 namespace Platformer2D
21 /// <summary>
22 /// A uniform grid of tiles with collections of gems and enemies.
23 /// The level owns the player and controls the game's win and lose
24 /// conditions as well as scoring.
25 /// </summary>
26 class Level : IDisposable
28 // Physical structure of the level.
29 private Tile[,] tiles;
30 private Texture2D[] layers;
31 // The layer which entities are drawn on top of.
32 private const int EntityLayer = 2;
34 // Entities in the level.
35 public Player Player
37 get { return player; }
39 Player player;
41 private List<Gem> gems = new List<Gem>();
42 private List<Enemy> enemies = new List<Enemy>();
44 // Key locations in the level.
45 private Vector2 start;
46 private Point exit = InvalidPosition;
47 private static readonly Point InvalidPosition = new Point(-1, -1);
49 // Level game state.
50 private Random random = new Random(354668); // Arbitrary, but constant seed
52 public int Score
54 get { return score; }
56 int score;
58 public bool ReachedExit
60 get { return reachedExit; }
62 bool reachedExit;
64 public TimeSpan TimeRemaining
66 get { return timeRemaining; }
68 TimeSpan timeRemaining;
70 private const int PointsPerSecond = 5;
72 // Level content.
73 public ContentManager Content
75 get { return content; }
77 ContentManager content;
79 private SoundEffect exitReachedSound;
81 #region Loading
83 /// <summary>
84 /// Constructs a new level.
85 /// </summary>
86 /// <param name="serviceProvider">
87 /// The service provider that will be used to construct a ContentManager.
88 /// </param>
89 /// <param name="fileStream">
90 /// A stream containing the tile data.
91 /// </param>
92 public Level(IServiceProvider serviceProvider, Stream fileStream, int levelIndex)
94 // Create a new content manager to load content used just by this level.
95 content = new ContentManager(serviceProvider, "Content");
97 timeRemaining = TimeSpan.FromMinutes(2.0);
99 LoadTiles(fileStream);
101 // Load background layer textures. For now, all levels must
102 // use the same backgrounds and only use the left-most part of them.
103 layers = new Texture2D[3];
104 for (int i = 0; i < layers.Length; ++i)
106 // Choose a random segment if each background layer for level variety.
107 int segmentIndex = levelIndex;
108 layers[i] = Content.Load<Texture2D>("Backgrounds/Layer" + i + "_" + segmentIndex);
111 // Load sounds.
112 exitReachedSound = Content.Load<SoundEffect>("Sounds/ExitReached");
115 /// <summary>
116 /// Iterates over every tile in the structure file and loads its
117 /// appearance and behavior. This method also validates that the
118 /// file is well-formed with a player start point, exit, etc.
119 /// </summary>
120 /// <param name="fileStream">
121 /// A stream containing the tile data.
122 /// </param>
123 private void LoadTiles(Stream fileStream)
125 // Load the level and ensure all of the lines are the same length.
126 int width;
127 List<string> lines = new List<string>();
128 using (StreamReader reader = new StreamReader(fileStream))
130 string line = reader.ReadLine();
131 width = line.Length;
132 while (line != null)
134 lines.Add(line);
135 if (line.Length != width)
136 throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
137 line = reader.ReadLine();
141 // Allocate the tile grid.
142 tiles = new Tile[width, lines.Count];
144 // Loop over every tile position,
145 for (int y = 0; y < Height; ++y)
147 for (int x = 0; x < Width; ++x)
149 // to load each tile.
150 char tileType = lines[y][x];
151 tiles[x, y] = LoadTile(tileType, x, y);
155 // Verify that the level has a beginning and an end.
156 if (Player == null)
157 throw new NotSupportedException("A level must have a starting point.");
158 if (exit == InvalidPosition)
159 throw new NotSupportedException("A level must have an exit.");
163 /// <summary>
164 /// Loads an individual tile's appearance and behavior.
165 /// </summary>
166 /// <param name="tileType">
167 /// The character loaded from the structure file which
168 /// indicates what should be loaded.
169 /// </param>
170 /// <param name="x">
171 /// The X location of this tile in tile space.
172 /// </param>
173 /// <param name="y">
174 /// The Y location of this tile in tile space.
175 /// </param>
176 /// <returns>The loaded tile.</returns>
177 private Tile LoadTile(char tileType, int x, int y)
179 switch (tileType)
181 // Blank space
182 case '.':
183 return new Tile(null, TileCollision.Passable);
185 // Exit
186 case 'X':
187 return LoadExitTile(x, y);
189 // Gem
190 case 'G':
191 return LoadGemTile(x, y);
193 // Floating platform
194 case '-':
195 return LoadTile("Platform", TileCollision.Platform);
197 // Various enemies
198 case 'A':
199 return LoadEnemyTile(x, y, "MonsterA");
200 case 'B':
201 return LoadEnemyTile(x, y, "MonsterB");
202 case 'C':
203 return LoadEnemyTile(x, y, "MonsterC");
204 case 'D':
205 return LoadEnemyTile(x, y, "MonsterD");
207 // Platform block
208 case '~':
209 return LoadVarietyTile("BlockB", 2, TileCollision.Platform);
211 // Passable block
212 case ':':
213 return LoadVarietyTile("BlockB", 2, TileCollision.Passable);
215 // Player 1 start point
216 case '1':
217 return LoadStartTile(x, y);
219 // Impassable block
220 case '#':
221 return LoadVarietyTile("BlockA", 7, TileCollision.Impassable);
223 // Unknown tile type character
224 default:
225 throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
229 /// <summary>
230 /// Creates a new tile. The other tile loading methods typically chain to this
231 /// method after performing their special logic.
232 /// </summary>
233 /// <param name="name">
234 /// Path to a tile texture relative to the Content/Tiles directory.
235 /// </param>
236 /// <param name="collision">
237 /// The tile collision type for the new tile.
238 /// </param>
239 /// <returns>The new tile.</returns>
240 private Tile LoadTile(string name, TileCollision collision)
242 return new Tile(Content.Load<Texture2D>("Tiles/" + name), collision);
246 /// <summary>
247 /// Loads a tile with a random appearance.
248 /// </summary>
249 /// <param name="baseName">
250 /// The content name prefix for this group of tile variations. Tile groups are
251 /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
252 /// </param>
253 /// <param name="variationCount">
254 /// The number of variations in this group.
255 /// </param>
256 private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
258 int index = random.Next(variationCount);
259 return LoadTile(baseName + index, collision);
263 /// <summary>
264 /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
265 /// </summary>
266 private Tile LoadStartTile(int x, int y)
268 if (Player != null)
269 throw new NotSupportedException("A level may only have one starting point.");
271 start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
272 player = new Player(this, start);
274 return new Tile(null, TileCollision.Passable);
277 /// <summary>
278 /// Remembers the location of the level's exit.
279 /// </summary>
280 private Tile LoadExitTile(int x, int y)
282 if (exit != InvalidPosition)
283 throw new NotSupportedException("A level may only have one exit.");
285 exit = GetBounds(x, y).Center;
287 return LoadTile("Exit", TileCollision.Passable);
290 /// <summary>
291 /// Instantiates an enemy and puts him in the level.
292 /// </summary>
293 private Tile LoadEnemyTile(int x, int y, string spriteSet)
295 Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
296 enemies.Add(new Enemy(this, position, spriteSet));
298 return new Tile(null, TileCollision.Passable);
301 /// <summary>
302 /// Instantiates a gem and puts it in the level.
303 /// </summary>
304 private Tile LoadGemTile(int x, int y)
306 Point position = GetBounds(x, y).Center;
307 gems.Add(new Gem(this, new Vector2(position.X, position.Y)));
309 return new Tile(null, TileCollision.Passable);
312 /// <summary>
313 /// Unloads the level content.
314 /// </summary>
315 public void Dispose()
317 Content.Unload();
320 #endregion
322 #region Bounds and collision
324 /// <summary>
325 /// Gets the collision mode of the tile at a particular location.
326 /// This method handles tiles outside of the levels boundries by making it
327 /// impossible to escape past the left or right edges, but allowing things
328 /// to jump beyond the top of the level and fall off the bottom.
329 /// </summary>
330 public TileCollision GetCollision(int x, int y)
332 // Prevent escaping past the level ends.
333 if (x < 0 || x >= Width)
334 return TileCollision.Impassable;
335 // Allow jumping past the level top and falling through the bottom.
336 if (y < 0 || y >= Height)
337 return TileCollision.Passable;
339 return tiles[x, y].Collision;
342 /// <summary>
343 /// Gets the bounding rectangle of a tile in world space.
344 /// </summary>
345 public Rectangle GetBounds(int x, int y)
347 return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
350 /// <summary>
351 /// Width of level measured in tiles.
352 /// </summary>
353 public int Width
355 get { return tiles.GetLength(0); }
358 /// <summary>
359 /// Height of the level measured in tiles.
360 /// </summary>
361 public int Height
363 get { return tiles.GetLength(1); }
366 #endregion
368 #region Update
370 /// <summary>
371 /// Updates all objects in the world, performs collision between them,
372 /// and handles the time limit with scoring.
373 /// </summary>
374 public void Update(
375 GameTime gameTime,
376 KeyboardState keyboardState,
377 GamePadState gamePadState,
378 AccelerometerState accelState,
379 DisplayOrientation orientation)
381 // Pause while the player is dead or time is expired.
382 if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
384 // Still want to perform physics on the player.
385 Player.ApplyPhysics(gameTime);
387 else if (ReachedExit)
389 // Animate the time being converted into points.
390 int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
391 seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
392 timeRemaining -= TimeSpan.FromSeconds(seconds);
393 score += seconds * PointsPerSecond;
395 else
397 timeRemaining -= gameTime.ElapsedGameTime;
398 Player.Update(gameTime, keyboardState, gamePadState, accelState, orientation);
399 UpdateGems(gameTime);
401 // Falling off the bottom of the level kills the player.
402 if (Player.BoundingRectangle.Top >= Height * Tile.Height)
403 OnPlayerKilled(null);
405 UpdateEnemies(gameTime);
407 // The player has reached the exit if they are standing on the ground and
408 // his bounding rectangle contains the center of the exit tile. They can only
409 // exit when they have collected all of the gems.
410 if (Player.IsAlive &&
411 Player.IsOnGround &&
412 Player.BoundingRectangle.Contains(exit))
414 OnExitReached();
418 // Clamp the time remaining at zero.
419 if (timeRemaining < TimeSpan.Zero)
420 timeRemaining = TimeSpan.Zero;
423 /// <summary>
424 /// Animates each gem and checks to allows the player to collect them.
425 /// </summary>
426 private void UpdateGems(GameTime gameTime)
428 for (int i = 0; i < gems.Count; ++i)
430 Gem gem = gems[i];
432 gem.Update(gameTime);
434 if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
436 gems.RemoveAt(i--);
437 OnGemCollected(gem, Player);
442 /// <summary>
443 /// Animates each enemy and allow them to kill the player.
444 /// </summary>
445 private void UpdateEnemies(GameTime gameTime)
447 foreach (Enemy enemy in enemies)
449 enemy.Update(gameTime);
451 // Touching an enemy instantly kills the player
452 if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
454 OnPlayerKilled(enemy);
459 /// <summary>
460 /// Called when a gem is collected.
461 /// </summary>
462 /// <param name="gem">The gem that was collected.</param>
463 /// <param name="collectedBy">The player who collected this gem.</param>
464 private void OnGemCollected(Gem gem, Player collectedBy)
466 score += gem.PointValue;
468 gem.OnCollected(collectedBy);
471 /// <summary>
472 /// Called when the player is killed.
473 /// </summary>
474 /// <param name="killedBy">
475 /// The enemy who killed the player. This is null if the player was not killed by an
476 /// enemy, such as when a player falls into a hole.
477 /// </param>
478 private void OnPlayerKilled(Enemy killedBy)
480 Player.OnKilled(killedBy);
483 /// <summary>
484 /// Called when the player reaches the level's exit.
485 /// </summary>
486 private void OnExitReached()
488 Player.OnReachedExit();
489 exitReachedSound.Play();
490 reachedExit = true;
493 /// <summary>
494 /// Restores the player to the starting point to try the level again.
495 /// </summary>
496 public void StartNewLife()
498 Player.Reset(start);
501 #endregion
503 #region Draw
505 /// <summary>
506 /// Draw everything in the level from background to foreground.
507 /// </summary>
508 public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
510 for (int i = 0; i <= EntityLayer; ++i)
511 spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
513 DrawTiles(spriteBatch);
515 foreach (Gem gem in gems)
516 gem.Draw(gameTime, spriteBatch);
518 Player.Draw(gameTime, spriteBatch);
520 foreach (Enemy enemy in enemies)
521 enemy.Draw(gameTime, spriteBatch);
523 for (int i = EntityLayer + 1; i < layers.Length; ++i)
524 spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
527 /// <summary>
528 /// Draws each tile in the level.
529 /// </summary>
530 private void DrawTiles(SpriteBatch spriteBatch)
532 // For each tile position
533 for (int y = 0; y < Height; ++y)
535 for (int x = 0; x < Width; ++x)
537 // If there is a visible tile in that position
538 Texture2D texture = tiles[x, y].Texture;
539 if (texture != null)
541 // Draw it in screen space.
542 Vector2 position = new Vector2(x, y) * Tile.Size;
543 spriteBatch.Draw(texture, position, Color.White);
549 #endregion