In Depth

If someone is looking down a corridor and sees another person move behind a wall, the moving person will disappear from view since they are now obscured by the wall. This is depth perception.

In real life, depth, and vision in general, is based off light bouncing off objects before reaching the eyes. 3D games calculate this differently, but generally achieve the same effect. But what about 2D games?

Some 2D games handle this by using layers. Take this crudely drawn image as an example.

Korbi does not like ninjas.

The sky, sea, clouds and ground would make up a background layer. The tree is part of a foreground layer. Korbi - the pink character - and the ninja - are part of a playable layer.

These layers are drawn in a specific order - background, playable and foreground. The background is drawn first, and the foreground is drawn last. This means that everything on the playable layer will always be displayed on top of the background, but behind the foreground. In the above example, the ninja is drawn before the tree, giving the illusion that it is hidden behind it. When Korbi reaches the tree, they will also end up being displayed behind it.

Korbi really does not like ninjas.

When using MonoGame, drawing to the screen is done using a thing called the SpriteBatch.

Drawing on each frame is wrapped with SpriteBatch.Begin() and SpriteBatch.End(). In between, sprites are drawn to the screen with SpriteBatch.Draw(), and text with SpriteBatch.DrawString().

Layers can be drawn by using multiple Begin and End calls.

void Draw(SpriteBatch spriteBatch)
{
    spriteBatch.Begin();

    // draw background layer

    spriteBatch.End();

    spriteBatch.Begin();

    // draw playable layer

    spriteBatch.End();

    spriteBatch.Begin();

    // draw foreground layer

    spriteBatch.End();
}

Alternatively, there could be a single Begin and End call, with the coder having listed each thing to draw in a specific order. But both of these require the coder to list out everything in a specific order.

At the most basic, the Begin call does not take any parameters. Draw and DrawString on the other hand require a few things. Both of them require a position to draw at, and a colour to draw with. Draw also needs the texture the sprite is part of, and DrawString needs the string to draw and a font to use to draw with.

For some more complex behaviour, some extra parameters can be passed in. Both Draw and DrawString have a parameter called depth, which is a value between 0 and 1. By default, if a Draw call is made with a higher depth then it will display in front of something that did a Draw call with a lower one. This can be reversed by passing SpriteSortMode.FrontToBack into the Begin call.

Keeping track of the depth of all objects individually would be a bit of a nightmare, so creating a GameObject class that wraps the texture, position and depth and then having a list of those objects would make this a little bit easier. Then when creating each object, a depth can be set which can be used when drawing the object.

public class Level
{
    private List<GameObject> gameObjects;

    public Level()
    {
        gameObjects = new List<GameObject>();

        // Add the background with a depth of 0
        gameObjects.Add(new GameObject("background", Vector2.Zero, 0));

        // Add Korbi with a depth of 0.5
        gameObjects.Add(new GameObject("korbi", new Vector2(300, 400), 0.5f));

        // Add a tree with a depth of 1
        gameObjects.Add(new GameObject("tree", new Vector2(500, 400), 1));
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.Begin();

        // draw each object
        foreach (GameObject gameObject in gameObjects)
        {
            gameObject.Draw(spriteBatch);
        }

        spriteBatch.End();
    }
}

private class GameObject
{
    // a bunch of variables used for drawing
    private Texture2D texture;

    private Vector2 position;

    private Rectangle rectangle;

    private float rotation;

    private float depth;

    public GameObject(string textureName, Vector2 position, float depth)
    {
        // load the texture from the content manager
        texture = ContentManager.Load<Texture2D>(textureName);

        this.position = position;

        // depending on the rectangle's size, some parts of the texture can
        // be excluded. this just sets the rectangle up to draw all of the texture
        rectangle = new Rectangle(0, 0, texture.Width, texture.Height);

        rotation = 0;
        this.depth = depth;
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.Draw(texture, position, rectangle, Color.White, rotation,
            Vector2.Zero, Vector2.One, SpriteEffects.None, depth);
    }
}

This is great for some kind of platformer, but Halloween Game has a weird perspective. If Korbi was in the game, and there was a box of apples, if Korbi was behind the box of apples then they should appear behind it.

Korbi also seems angry around apples.

Without applying any depths to the box or Korbi, whichever one is drawn last will appear in front.  Looking at the left part of the above example, we can see that if Korbi is drawn last then they appear in front of the box. Since Korbi should appear behind the box, we can apply a depth of 0 to Korbi and a depth of 1 to the box.

Doing that will produce the middle part of the example, with Korbi now displaying behind the box. But if Korbi approaches the box from the bottom, they will end up displaying like the right hand side of the example - behind the box when they should be in front.

To fix this, we'll need to change the depth of Korbi depending on their position. The closer to the bottom of the screen an object is, the higher it's depth should be. Which means having to do some maths.

public class Level
{
    private int levelHeight = 480;

    ... existing level code

    public float GetDepth(int y, int height)
    {
        return y + height / (float)levelHeight;
    }
}

The GetDepth method takes in an object's y position, it's height and then works out where it is relative to the level height.

If Korbi is 64 pixels tall, and they are at a y position of 0 that means 0 + 64 / 480 = 0.133.

If Korbi moved towards the bottom of the screen, at a y of 300, that means 300 + 64 / 480 = 0.758.

To make this work, some tweaks to the code need to be made. (Disclaimer: I haven't actually tested this)

public class Level
{
    private int levelHeight = 480;

    private List<GameObject> gameObjects;

    public Level()
    {
        gameObjects = new List<GameObject>();

        gameObjects.Add(new GameObject("background", Vector2.Zero));

        gameObjects.Add(new GameObject("korbi", new Vector2(300, 400)));

        gameObjects.Add(new GameObject("apple_box", new Vector2(500, 400)));
    }

    public void Update()
    {
        // go through each object
        foreach (GameObject gameObject in gameObjects)
        {
            // get the depth using the object's y position and height
            float depth = GetDepth(gameObject.Y, gameObject.Height);

            // set the object's depth
            gameObject.SetDepth(depth);
        }
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.Begin();

        foreach (GameObject gameObject in gameObjects)
        {
            gameObject.Draw(spriteBatch);
        }

        spriteBatch.End();
    }

    public float GetDepth(int y, int height)
    {
        return y + height / (float)levelHeight;
    }
}

private class GameObject
{
    public int Y => (int)position.Y;

    public int Height => rectangle.Height;

    private Texture2D texture;

    private Vector2 position;

    private Rectangle rectangle;

    private float rotation;

    private float depth;

    public GameObject(string textureName, Vector2 position)
    {
        texture = ContentManager.Load<Texture2D>(textureName);
        this.position = position;
        rectangle = new Rectangle(0, 0, texture.Width, texture.Height);
        rotation = 0;
    }

    public void SetDepth(float depth)
    {
        this.depth = depth;
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.Draw(texture, position, rectangle, Color.White, rotation,
            Vector2.Zero, Vector2.One, SpriteEffects.None, depth);
    }
}

So that's how depth works in MonoGame. The GetDepth method in Halloween Game has a little bit more to it - safeguards making sure it sticks between 0 and 1, and taking into account the level's y position too since it's not always 0 which ruins the calculation - but they aren't needed for this example.

Korbi may or may not return in the future. But not next week, because that's all about languages.

Popular posts from this blog

Getting Started with the FireRed Decompilation

Phoning It In

Tonight Matthew, I'm Going to Make...