1
#region File Description
2 //-----------------------------------------------------------------------------
5 // Microsoft XNA Community Game Platform
6 // Copyright (C) Microsoft Corporation. All rights reserved.
7 //-----------------------------------------------------------------------------
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
;
17 using Microsoft
.Xna
.Framework
.Input
;
19 namespace Platformer2D
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.
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.
37 get { return 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);
50 private Random random
= new Random(354668); // Arbitrary, but constant seed
58 public bool ReachedExit
60 get { return reachedExit; }
64 public TimeSpan TimeRemaining
66 get { return timeRemaining; }
68 TimeSpan timeRemaining
;
70 private const int PointsPerSecond
= 5;
73 public ContentManager Content
75 get { return content; }
77 ContentManager content
;
79 private SoundEffect exitReachedSound
;
84 /// Constructs a new level.
86 /// <param name="serviceProvider">
87 /// The service provider that will be used to construct a ContentManager.
89 /// <param name="fileStream">
90 /// A stream containing the tile data.
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
);
112 exitReachedSound
= Content
.Load
<SoundEffect
>("Sounds/ExitReached");
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.
120 /// <param name="fileStream">
121 /// A stream containing the tile data.
123 private void LoadTiles(Stream fileStream
)
125 // Load the level and ensure all of the lines are the same length.
127 List
<string> lines
= new List
<string>();
128 using (StreamReader reader
= new StreamReader(fileStream
))
130 string line
= reader
.ReadLine();
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.
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.");
164 /// Loads an individual tile's appearance and behavior.
166 /// <param name="tileType">
167 /// The character loaded from the structure file which
168 /// indicates what should be loaded.
171 /// The X location of this tile in tile space.
174 /// The Y location of this tile in tile space.
176 /// <returns>The loaded tile.</returns>
177 private Tile
LoadTile(char tileType
, int x
, int y
)
183 return new Tile(null, TileCollision
.Passable
);
187 return LoadExitTile(x
, y
);
191 return LoadGemTile(x
, y
);
195 return LoadTile("Platform", TileCollision
.Platform
);
199 return LoadEnemyTile(x
, y
, "MonsterA");
201 return LoadEnemyTile(x
, y
, "MonsterB");
203 return LoadEnemyTile(x
, y
, "MonsterC");
205 return LoadEnemyTile(x
, y
, "MonsterD");
209 return LoadVarietyTile("BlockB", 2, TileCollision
.Platform
);
213 return LoadVarietyTile("BlockB", 2, TileCollision
.Passable
);
215 // Player 1 start point
217 return LoadStartTile(x
, y
);
221 return LoadVarietyTile("BlockA", 7, TileCollision
.Impassable
);
223 // Unknown tile type character
225 throw new NotSupportedException(String
.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType
, x
, y
));
230 /// Creates a new tile. The other tile loading methods typically chain to this
231 /// method after performing their special logic.
233 /// <param name="name">
234 /// Path to a tile texture relative to the Content/Tiles directory.
236 /// <param name="collision">
237 /// The tile collision type for the new tile.
239 /// <returns>The new tile.</returns>
240 private Tile
LoadTile(string name
, TileCollision collision
)
242 return new Tile(Content
.Load
<Texture2D
>("Tiles/" + name
), collision
);
247 /// Loads a tile with a random appearance.
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.
253 /// <param name="variationCount">
254 /// The number of variations in this group.
256 private Tile
LoadVarietyTile(string baseName
, int variationCount
, TileCollision collision
)
258 int index
= random
.Next(variationCount
);
259 return LoadTile(baseName
+ index
, collision
);
264 /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
266 private Tile
LoadStartTile(int x
, int y
)
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
);
278 /// Remembers the location of the level's exit.
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
);
291 /// Instantiates an enemy and puts him in the level.
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
);
302 /// Instantiates a gem and puts it in the level.
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
);
313 /// Unloads the level content.
315 public void Dispose()
322 #region Bounds and collision
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.
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
;
343 /// Gets the bounding rectangle of a tile in world space.
345 public Rectangle
GetBounds(int x
, int y
)
347 return new Rectangle(x
* Tile
.Width
, y
* Tile
.Height
, Tile
.Width
, Tile
.Height
);
351 /// Width of level measured in tiles.
355 get { return tiles.GetLength(0); }
359 /// Height of the level measured in tiles.
363 get { return tiles.GetLength(1); }
371 /// Updates all objects in the world, performs collision between them,
372 /// and handles the time limit with scoring.
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
;
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
&&
412 Player
.BoundingRectangle
.Contains(exit
))
418 // Clamp the time remaining at zero.
419 if (timeRemaining
< TimeSpan
.Zero
)
420 timeRemaining
= TimeSpan
.Zero
;
424 /// Animates each gem and checks to allows the player to collect them.
426 private void UpdateGems(GameTime gameTime
)
428 for (int i
= 0; i
< gems
.Count
; ++i
)
432 gem
.Update(gameTime
);
434 if (gem
.BoundingCircle
.Intersects(Player
.BoundingRectangle
))
437 OnGemCollected(gem
, Player
);
443 /// Animates each enemy and allow them to kill the player.
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
);
460 /// Called when a gem is collected.
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
);
472 /// Called when the player is killed.
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.
478 private void OnPlayerKilled(Enemy killedBy
)
480 Player
.OnKilled(killedBy
);
484 /// Called when the player reaches the level's exit.
486 private void OnExitReached()
488 Player
.OnReachedExit();
489 exitReachedSound
.Play();
494 /// Restores the player to the starting point to try the level again.
496 public void StartNewLife()
506 /// Draw everything in the level from background to foreground.
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
);
528 /// Draws each tile in the level.
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
;
541 // Draw it in screen space.
542 Vector2 position
= new Vector2(x
, y
) * Tile
.Size
;
543 spriteBatch
.Draw(texture
, position
, Color
.White
);