简体   繁体   中英

Side Scrolling Background for 2d XNA game, Help desperately needed

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.

You need to offset where the tile is drawn, like so:

Vector2 position = new Vector2(x, y) * Tile.Size;

turns into

Vector2 position = new Vector2(x * Tile.Size + cameraOffset.X, y * Tile.Size + cameraOffset.Y);

This is an edit to your DrawTiles method.

A more typical way to move your scene around with a camera is to use a transformation matrix. What I do is create a Camera class with various properties (Position, Rotation, Zoom, Origin) and have a method on to make transform like this:

    public Matrix GetTransform()
    {
        var translationMatrix = Matrix.CreateTranslation(new Vector3(-Position.X, -Position.Y, 0));
        var rotationMatrix = Matrix.CreateRotationZ(Rotation);
        var scaleMatrix = Matrix.CreateScale(new Vector3(Zoom, Zoom, 1));
        var originMatrix = Matrix.CreateTranslation(new Vector3(Origin.X, Origin.Y, 0));

        return translationMatrix * rotationMatrix * scaleMatrix * originMatrix;
    }

Then when you draw your sprite batch you can simply pass the matrix into the last parameter like so:

_spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, null, null, null, _camera.GetTransform());

When you do it this way you don't need to offset anything so you'll need to take out all of the camera offset code you've already got. Just draw things they way they are and transform the entire view at draw time.

Once last thing to note is that if you are using mouse or touch coordinates you'll also need to transform the input in reverse.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM