123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- #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
- {
- /// <summary>
- /// 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.
- /// </summary>
- 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<Gem> gems = new List<Gem>();
- private List<Enemy> enemies = new List<Enemy>();
- // 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
- /// <summary>
- /// Constructs a new level.
- /// </summary>
- /// <param name="serviceProvider">
- /// The service provider that will be used to construct a ContentManager.
- /// </param>
- /// <param name="fileStream">
- /// A stream containing the tile data.
- /// </param>
- 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<Texture2D>("Backgrounds/Layer" + i + "_" + segmentIndex);
- }
- // Load sounds.
- exitReachedSound = Content.Load<SoundEffect>("Sounds/ExitReached");
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="fileStream">
- /// A stream containing the tile data.
- /// </param>
- private void LoadTiles(Stream fileStream)
- {
- // Load the level and ensure all of the lines are the same length.
- int width;
- List<string> lines = new List<string>();
- 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.");
- }
- /// <summary>
- /// Loads an individual tile's appearance and behavior.
- /// </summary>
- /// <param name="tileType">
- /// The character loaded from the structure file which
- /// indicates what should be loaded.
- /// </param>
- /// <param name="x">
- /// The X location of this tile in tile space.
- /// </param>
- /// <param name="y">
- /// The Y location of this tile in tile space.
- /// </param>
- /// <returns>The loaded tile.</returns>
- 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));
- }
- }
- /// <summary>
- /// Creates a new tile. The other tile loading methods typically chain to this
- /// method after performing their special logic.
- /// </summary>
- /// <param name="name">
- /// Path to a tile texture relative to the Content/Tiles directory.
- /// </param>
- /// <param name="collision">
- /// The tile collision type for the new tile.
- /// </param>
- /// <returns>The new tile.</returns>
- private Tile LoadTile(string name, TileCollision collision)
- {
- return new Tile(Content.Load<Texture2D>("Tiles/" + name), collision);
- }
- /// <summary>
- /// Loads a tile with a random appearance.
- /// </summary>
- /// <param name="baseName">
- /// The content name prefix for this group of tile variations. Tile groups are
- /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
- /// </param>
- /// <param name="variationCount">
- /// The number of variations in this group.
- /// </param>
- private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
- {
- int index = random.Next(variationCount);
- return LoadTile(baseName + index, collision);
- }
- /// <summary>
- /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
- /// </summary>
- 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);
- }
- /// <summary>
- /// Remembers the location of the level's exit.
- /// </summary>
- 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);
- }
- /// <summary>
- /// Instantiates an enemy and puts him in the level.
- /// </summary>
- 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);
- }
- /// <summary>
- /// Instantiates a gem and puts it in the level.
- /// </summary>
- 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);
- }
- /// <summary>
- /// Unloads the level content.
- /// </summary>
- public void Dispose()
- {
- Content.Unload();
- }
- #endregion
- #region Bounds and collision
- /// <summary>
- /// 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.
- /// </summary>
- 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;
- }
- /// <summary>
- /// Gets the bounding rectangle of a tile in world space.
- /// </summary>
- public Rectangle GetBounds(int x, int y)
- {
- return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
- }
- /// <summary>
- /// Width of level measured in tiles.
- /// </summary>
- public int Width
- {
- get { return tiles.GetLength(0); }
- }
- /// <summary>
- /// Height of the level measured in tiles.
- /// </summary>
- public int Height
- {
- get { return tiles.GetLength(1); }
- }
- #endregion
- #region Update
- /// <summary>
- /// Updates all objects in the world, performs collision between them,
- /// and handles the time limit with scoring.
- /// </summary>
- 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;
- }
- /// <summary>
- /// Animates each gem and checks to allows the player to collect them.
- /// </summary>
- 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);
- }
- }
- }
- /// <summary>
- /// Animates each enemy and allow them to kill the player.
- /// </summary>
- 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);
- }
- }
- }
- /// <summary>
- /// Called when a gem is collected.
- /// </summary>
- /// <param name="gem">The gem that was collected.</param>
- /// <param name="collectedBy">The player who collected this gem.</param>
- private void OnGemCollected(Gem gem, Player collectedBy)
- {
- score += Gem.PointValue;
- gem.OnCollected(collectedBy);
- }
- /// <summary>
- /// Called when the player is killed.
- /// </summary>
- /// <param name="killedBy">
- /// 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.
- /// </param>
- private void OnPlayerKilled(Enemy killedBy)
- {
- Player.OnKilled(killedBy);
- }
- /// <summary>
- /// Called when the player reaches the level's exit.
- /// </summary>
- private void OnExitReached()
- {
- Player.OnReachedExit();
- exitReachedSound.Play();
- reachedExit = true;
- }
- /// <summary>
- /// Restores the player to the starting point to try the level again.
- /// </summary>
- public void StartNewLife()
- {
- Player.Reset(start);
- }
- #endregion
- #region Draw
- /// <summary>
- /// Draw everything in the level from background to foreground.
- /// </summary>
- 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);
- }
- /// <summary>
- /// Draws each tile in the level.
- /// </summary>
- 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
- }
- }
|