#region File Description //----------------------------------------------------------------------------- // Level.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Audio; using System.IO; using Microsoft.Xna.Framework.Input.Touch; using Microsoft.Xna.Framework.Input; namespace Platformer { /// /// A uniform grid of tiles with collections of gems and enemies. /// The level owns the player and controls the game's win and lose /// conditions as well as scoring. /// class Level : IDisposable { // Physical structure of the level. private Tile[,] tiles; private Texture2D[] layers; // The layer which entities are drawn on top of. private const int EntityLayer = 2; // Entities in the level. public Player Player { get { return player; } } Player player; private List gems = new List(); private List enemies = new List(); // Key locations in the level. private Vector2 start; private Point exit = InvalidPosition; private static readonly Point InvalidPosition = new Point(-1, -1); // Level game state. private Random random = new Random(354668); // Arbitrary, but constant seed public int Score { get { return score; } } int score; public bool ReachedExit { get { return reachedExit; } } bool reachedExit; public TimeSpan TimeRemaining { get { return timeRemaining; } } TimeSpan timeRemaining; private const int PointsPerSecond = 5; // Level content. public ContentManager Content { get { return content; } } ContentManager content; private SoundEffect exitReachedSound; #region Loading /// /// Constructs a new level. /// /// /// The service provider that will be used to construct a ContentManager. /// /// /// A stream containing the tile data. /// public Level(IServiceProvider serviceProvider, Stream fileStream, int levelIndex) { // Create a new content manager to load content used just by this level. content = new ContentManager(serviceProvider, "Content"); timeRemaining = TimeSpan.FromMinutes(2.0); LoadTiles(fileStream); // Load background layer textures. For now, all levels must // use the same backgrounds and only use the left-most part of them. layers = new Texture2D[3]; for (int i = 0; i < layers.Length; ++i) { // Choose a random segment if each background layer for level variety. int segmentIndex = levelIndex; layers[i] = Content.Load("Backgrounds/Layer" + i + "_" + segmentIndex); } // Load sounds. exitReachedSound = Content.Load("Sounds/ExitReached"); } /// /// Iterates over every tile in the structure file and loads its /// appearance and behavior. This method also validates that the /// file is well-formed with a player start point, exit, etc. /// /// /// A stream containing the tile data. /// private void LoadTiles(Stream fileStream) { // Load the level and ensure all of the lines are the same length. int width; List lines = new List(); using (StreamReader reader = new StreamReader(fileStream)) { string line = reader.ReadLine(); width = line.Length; while (line != null) { lines.Add(line); if (line.Length != width) throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count)); line = reader.ReadLine(); } } // Allocate the tile grid. tiles = new Tile[width, lines.Count]; // Loop over every tile position, for (int y = 0; y < Height; ++y) { for (int x = 0; x < Width; ++x) { // to load each tile. char tileType = lines[y][x]; tiles[x, y] = LoadTile(tileType, x, y); } } // Verify that the level has a beginning and an end. if (Player == null) throw new NotSupportedException("A level must have a starting point."); if (exit == InvalidPosition) throw new NotSupportedException("A level must have an exit."); } /// /// Loads an individual tile's appearance and behavior. /// /// /// The character loaded from the structure file which /// indicates what should be loaded. /// /// /// The X location of this tile in tile space. /// /// /// The Y location of this tile in tile space. /// /// The loaded tile. private Tile LoadTile(char tileType, int x, int y) { switch (tileType) { // Blank space case '.': return new Tile(null, TileCollision.Passable); // Exit case 'X': return LoadExitTile(x, y); // Gem case 'G': return LoadGemTile(x, y); // Floating platform case '-': return LoadTile("Platform", TileCollision.Platform); // Various enemies case 'A': return LoadEnemyTile(x, y, "MonsterA"); case 'B': return LoadEnemyTile(x, y, "MonsterB"); case 'C': return LoadEnemyTile(x, y, "MonsterC"); case 'D': return LoadEnemyTile(x, y, "MonsterD"); // Platform block case '~': return LoadVarietyTile("BlockB", 2, TileCollision.Platform); // Passable block case ':': return LoadVarietyTile("BlockB", 2, TileCollision.Passable); // Player 1 start point case '1': return LoadStartTile(x, y); // Impassable block case '#': return LoadVarietyTile("BlockA", 7, TileCollision.Impassable); // Unknown tile type character default: throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y)); } } /// /// Creates a new tile. The other tile loading methods typically chain to this /// method after performing their special logic. /// /// /// Path to a tile texture relative to the Content/Tiles directory. /// /// /// The tile collision type for the new tile. /// /// The new tile. private Tile LoadTile(string name, TileCollision collision) { return new Tile(Content.Load("Tiles/" + name), collision); } /// /// Loads a tile with a random appearance. /// /// /// The content name prefix for this group of tile variations. Tile groups are /// name LikeThis0.png and LikeThis1.png and LikeThis2.png. /// /// /// The number of variations in this group. /// private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision) { int index = random.Next(variationCount); return LoadTile(baseName + index, collision); } /// /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected. /// private Tile LoadStartTile(int x, int y) { if (Player != null) throw new NotSupportedException("A level may only have one starting point."); start = RectangleExtensions.GetBottomCenter(GetBounds(x, y)); player = new Player(this, start); return new Tile(null, TileCollision.Passable); } /// /// Remembers the location of the level's exit. /// private Tile LoadExitTile(int x, int y) { if (exit != InvalidPosition) throw new NotSupportedException("A level may only have one exit."); exit = GetBounds(x, y).Center; return LoadTile("Exit", TileCollision.Passable); } /// /// Instantiates an enemy and puts him in the level. /// private Tile LoadEnemyTile(int x, int y, string spriteSet) { Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y)); enemies.Add(new Enemy(this, position, spriteSet)); return new Tile(null, TileCollision.Passable); } /// /// Instantiates a gem and puts it in the level. /// private Tile LoadGemTile(int x, int y) { Point position = GetBounds(x, y).Center; gems.Add(new Gem(this, new Vector2(position.X, position.Y))); return new Tile(null, TileCollision.Passable); } /// /// Unloads the level content. /// public void Dispose() { Content.Unload(); } #endregion #region Bounds and collision /// /// Gets the collision mode of the tile at a particular location. /// This method handles tiles outside of the levels boundries by making it /// impossible to escape past the left or right edges, but allowing things /// to jump beyond the top of the level and fall off the bottom. /// public TileCollision GetCollision(int x, int y) { // Prevent escaping past the level ends. if (x < 0 || x >= Width) return TileCollision.Impassable; // Allow jumping past the level top and falling through the bottom. if (y < 0 || y >= Height) return TileCollision.Passable; return tiles[x, y].Collision; } /// /// Gets the bounding rectangle of a tile in world space. /// public Rectangle GetBounds(int x, int y) { return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height); } /// /// Width of level measured in tiles. /// public int Width { get { return tiles.GetLength(0); } } /// /// Height of the level measured in tiles. /// public int Height { get { return tiles.GetLength(1); } } #endregion #region Update /// /// Updates all objects in the world, performs collision between them, /// and handles the time limit with scoring. /// public void Update( GameTime gameTime, KeyboardState keyboardState, GamePadState gamePadState, TouchCollection touchState, AccelerometerState accelState, DisplayOrientation orientation) { // Pause while the player is dead or time is expired. if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero) { // Still want to perform physics on the player. Player.ApplyPhysics(gameTime); } else if (ReachedExit) { // Animate the time being converted into points. int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f); seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds)); timeRemaining -= TimeSpan.FromSeconds(seconds); score += seconds * PointsPerSecond; } else { timeRemaining -= gameTime.ElapsedGameTime; Player.Update(gameTime, keyboardState, gamePadState, touchState, accelState, orientation); UpdateGems(gameTime); // Falling off the bottom of the level kills the player. if (Player.BoundingRectangle.Top >= Height * Tile.Height) OnPlayerKilled(null); UpdateEnemies(gameTime); // The player has reached the exit if they are standing on the ground and // his bounding rectangle contains the center of the exit tile. They can only // exit when they have collected all of the gems. if (Player.IsAlive && Player.IsOnGround && Player.BoundingRectangle.Contains(exit)) { OnExitReached(); } } // Clamp the time remaining at zero. if (timeRemaining < TimeSpan.Zero) timeRemaining = TimeSpan.Zero; } /// /// Animates each gem and checks to allows the player to collect them. /// private void UpdateGems(GameTime gameTime) { for (int i = 0; i < gems.Count; ++i) { Gem gem = gems[i]; gem.Update(gameTime); if (gem.BoundingCircle.Intersects(Player.BoundingRectangle)) { gems.RemoveAt(i--); OnGemCollected(gem, Player); } } } /// /// Animates each enemy and allow them to kill the player. /// private void UpdateEnemies(GameTime gameTime) { foreach (Enemy enemy in enemies) { enemy.Update(gameTime); // Touching an enemy instantly kills the player if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle)) { OnPlayerKilled(enemy); } } } /// /// Called when a gem is collected. /// /// The gem that was collected. /// The player who collected this gem. private void OnGemCollected(Gem gem, Player collectedBy) { score += Gem.PointValue; gem.OnCollected(collectedBy); } /// /// Called when the player is killed. /// /// /// The enemy who killed the player. This is null if the player was not killed by an /// enemy, such as when a player falls into a hole. /// private void OnPlayerKilled(Enemy killedBy) { Player.OnKilled(killedBy); } /// /// Called when the player reaches the level's exit. /// private void OnExitReached() { Player.OnReachedExit(); exitReachedSound.Play(); reachedExit = true; } /// /// Restores the player to the starting point to try the level again. /// public void StartNewLife() { Player.Reset(start); } #endregion #region Draw /// /// Draw everything in the level from background to foreground. /// public void Draw(GameTime gameTime, SpriteBatch spriteBatch) { for (int i = 0; i <= EntityLayer; ++i) spriteBatch.Draw(layers[i], Vector2.Zero, Color.White); DrawTiles(spriteBatch); foreach (Gem gem in gems) gem.Draw(gameTime, spriteBatch); Player.Draw(gameTime, spriteBatch); foreach (Enemy enemy in enemies) enemy.Draw(gameTime, spriteBatch); for (int i = EntityLayer + 1; i < layers.Length; ++i) spriteBatch.Draw(layers[i], Vector2.Zero, Color.White); } /// /// Draws each tile in the level. /// private void DrawTiles(SpriteBatch spriteBatch) { // For each tile position for (int y = 0; y < Height; ++y) { for (int x = 0; x < Width; ++x) { // If there is a visible tile in that position Texture2D texture = tiles[x, y].Texture; if (texture != null) { // Draw it in screen space. Vector2 position = new Vector2(x, y) * Tile.Size; spriteBatch.Draw(texture, position, Color.White); } } } } #endregion } }