1
0
mirror of https://github.com/Memorix101/UnityXNA/ synced 2024-12-30 15:25:35 +01:00

554 lines
18 KiB
C#
Raw Permalink Normal View History

#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
}
}