I am trying to make a simple side scrolling game in XNA (monogame) as a learning exercise, but I am having some issues getting my head around scrolling the level. Basically, I am trying to make a simple game where the level scrolls from left to right with a player which stays stationary apart from when jumping over obstacles.
I initially took the old platformer starter kit and stripped most of its functionality out like the player, gems and enemies. Essentially, all that remains is the functionality to load a level from a text file. It loops through each line of the file determining what type of tile exists and then draws a new Texture 2D for the tile.
I have followed a few tutorials about making a background scroll from right to left, but I cannot get the tiles themselves to scroll.
I want to create a fixed view with the player in it then move the rest of the world to the left.
I would never normally paste so much source as I doubt anyone will be bothered to look at it (here's to hoping) but here is the level class, main (program) class (the player class has no real functionality in it as it just draws a sprite to a selected vector2.
Level:
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content;
namespace WP8_Game
{ public class Level { // Physical structure of the level. private Tile[,] tiles; private Layer[] layers; int mLineIndex;
// The layer which entities are drawn on top of.
private const int EntityLayer = 2;
private Vector2 cameraPosition;
// Level content.
public ContentManager Content
{
get { return content; }
}
ContentManager content;
#region Loading
public Level(IServiceProvider serviceProvider, Stream fileStream, int lineIndex)
{
// Create a new content manager to load content used just by this level.
content = new ContentManager(serviceProvider, "Content");
mLineIndex = lineIndex;
LoadTiles(fileStream);
layers = new Layer[3];
layers[0] = new Layer(Content, "Backgrounds/Layer0", 0.2f);
layers[1] = new Layer(Content, "Backgrounds/Layer1", 0.5f);
layers[2] = new Layer(Content, "Backgrounds/Layer2", 0.8f);
}
/// <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);
}
}
}
/// <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); }
}
/// <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, new Vector2(x, y), TileCollision.Passable);
// Impassable block
case '#':
return LoadTile("BlockA0", x, y, 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, int x, int y, TileCollision collision)
{
return new Tile(Content.Load<Texture2D>("Tiles/" + name), new Vector2(x, y), collision);
}
/// <summary>
/// Unloads the level content.
/// </summary>
public void Dispose()
{
Content.Unload();
}
#endregion
#region Bounds and 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);
}
#endregion
#region Draw
/// <summary>
/// Draw everything in the level from background to foreground.
/// </summary>
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
spriteBatch.Begin();
for (int i = 0; i <= EntityLayer; ++i)
layers[i].Draw(spriteBatch, cameraPosition);
spriteBatch.End();
ScrollCamera(spriteBatch.GraphicsDevice.Viewport, gameTime);
Matrix cameraTransform = Matrix.CreateTranslation(-cameraPosition.X, 0.0f, 0.0f);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise, null, cameraTransform);
DrawTiles(spriteBatch);
spriteBatch.End();
spriteBatch.Begin();
for (int i = EntityLayer + 1; i < layers.Length; ++i)
layers[i].Draw(spriteBatch, cameraPosition);
spriteBatch.End();
}
private void ScrollCamera(Viewport viewport, GameTime gameTime)
{
//Add to the camera positon, So we can see the origin
cameraPosition.X = cameraPosition.X + (viewport.Width / 2);
cameraPosition.Y = cameraPosition.Y + (viewport.Height / 2);
//Smoothly move the camera towards the player
cameraPosition.X = MathHelper.Lerp(cameraPosition.X, 10, 0.1f);
cameraPosition.Y = MathHelper.Lerp(cameraPosition.Y, 10, 0.1f);
//Undo the origin because it will be calculated with the Matrix (I know this isnt the best way but its what I had real quick)
cameraPosition.X = cameraPosition.X - (viewport.Width / 2);
cameraPosition.Y = cameraPosition.Y - (viewport.Height / 2);
//Shake the camera, Use the mouse to scroll or anything like that, add it here (Ex, Earthquakes)
//Round it, So it dosent try to draw in between 2 pixels
cameraPosition.Y = (float)Math.Round(cameraPosition.Y);
cameraPosition.X = (float)Math.Round(cameraPosition.X);
//Clamp it off, So it stops scrolling near the edges
cameraPosition.X = MathHelper.Clamp(cameraPosition.X, 1f, Width * Tile.Width);
cameraPosition.Y = MathHelper.Clamp(cameraPosition.Y, 1f, Height * Tile.Height);
}
/// <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
#region Update
/// <summary>
/// Updates all objects in the level
/// </summary>
public void Update(GameTime gameTime)
{
// 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 cameraOffset = new Vector2(10, 0);
tiles[x, y].Position = new Vector2(x--, y);
}
}
}
}
#endregion
}
}
Program:
using System;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace WP8_Game
{
/// <summary>
/// This is the main type for your game
/// </summary>
public class Program : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private Player player;
// Meta-level game state.
private int levelIndex = -1;
private Level level;
// The number of levels in the Levels directory of our content. We assume that
// levels in our content are 0-based and that all numbers under this constant
// have a level file present. This allows us to not need to check for the file
// or handle exceptions, both of which can add unnecessary time to level loading.
private const int numberOfLevels = 3;
public Program()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
player = new Player();
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// Load the player resources
Vector2 playerPosition = new Vector2(100, 100);
player.Initialize(Content.Load<Texture2D>("Sprites/Player/player"), playerPosition);
//Load the next level
LoadNextLevel();
}
private void LoadNextLevel()
{
// move to the next level
levelIndex = (levelIndex + 1) % numberOfLevels;
// Unloads the content for the current level before loading the next one.
if (level != null)
level.Dispose();
// Load the level.
string levelPath = string.Format("Content/Levels/{0}.txt", levelIndex);
using (Stream fileStream = TitleContainer.OpenStream(levelPath))
level = new Level(Services, fileStream, levelIndex);
}
private void ReloadCurrentLevel()
{
--levelIndex;
LoadNextLevel();
}
/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// all content.
/// </summary>
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
//camera.Update(gameTime, player);
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
// Start drawing
spriteBatch.Begin();
// Draw the Player
player.Draw(spriteBatch);
//Draw the level
level.Draw(gameTime, spriteBatch);
// Stop drawing
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Any advice would be greatly appreciated, I am so lost and am not sure where to start.