1
0
mirror of https://github.com/Memorix101/UnityXNA/ synced 2024-12-30 15:25:35 +01:00
Barnaby Smith 6fe889760d First commit. Proof of concept implementation.
The XNA 4.0 PlatformerGame sample is successfully running inside Unity3D
3.5.
Implemented a basic game loop, game timing, content loading for
Texture2D, SoundEffect and Song. Emulated SpriteBatch drawing for
sprites and strings (note SpriteFont is not yet supported to all strings
are rendered using the default GUI label font). Songs can be played
using an AudioSource attached to the XNATest game object which acts as
an emulator for MediaPlayer. Playing a SoundEffect creates a game object
with an AudioSource attached which is automatically deleted when the
sound finishes. Implemented keyboard input with a limited set of XNA
Keys mapping to Unity3D KeyCodes.
2012-07-07 20:57:54 +01:00

554 lines
18 KiB
C#

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