Virtual Spelunking: Tiles and Shadows


Last week, I used a combination of algorithms to procedurally generate caves and render them on screen as arrays of ‘#’s and ‘.’s. While text-based rendering is great for debugging and Rogue-likes, sometimes you need something a little more graphical. Rendering the floor and wall spaces created by the random map generator as tile sprites should do just the trick.

For my cave tile rendering implementation, I decided to use some of the best, free graphic resources available on the web: Danc’s Miraculously Flexible Game Prototyping Tiles. In addition to being attractive, this tileset is also very easy to use because each tile is a self-contained piece. With the exception of shadows (which I will cover later), tiles don’t affect each other visually so you don’t need to change which tile you draw based on its neighbors. Because you can treat each tile as a self-contained piece it’s very easy to map a tile to the floor and wall spaces created by the procedural cave generator and end up with something like this:

Adding a vertical offset to the wall tiles to raise them by about 36 pixels makes them actually look like walls:

Not a bad start, but the map still feels flat. This is where the shadow tiles in prototyping tile kit come into play. Danc has lamented that people using his prototyping tiles often don’t take advantage of the shadow tiles he provides. He has a very good reason for feeling this way: they can make a world of difference in the visual quality of a map.

At first glance, implementing the rules for applying the shadow textures can be a bit daunting. Even with the provided diagram and instructions (see “Shadow Tile Placement”), it can take some time to grok the logic needed to place the shadow tiles correctly. For my shadow placement algorithm I broke down the logic as follows:

For a given tile

  1. If a taller tile exists to the N, E, S, or W
    Draw the N, E, S, or W shadow on the current tile
  2. If a taller tile exists to the SW
    Draw the SW shadow on the current tile only if the tile to the west is the same height or less
  3. If a taller tile exists to the SE
    Draw the SE shadow on the current tile only if the tile to the east is the same height or less
  4. If a taller tile exists to the NW
    Draw the NW shadow on the current tile only if the tiles to the west and to the north are the same height or less
  5. If a taller tile exists to the NE
    Draw the NE shadow on the current tile only if the tiles to the east and to the north are the same height or less

Or in code:

private void _DrawShadow(int i, int j, Vector2 drawPos, float widthIncrement, float heightIncrement)
{
    int currTileHeight = _GetTileHeight(_CaveTileToRenderTile(i, j));

    int iMinus = i > 0 ? i - 1 : i;
    int iPlus = i < _arrayToRender.Count - 1 ? i + 1 : i;
    int jMinus = j > 0 ? j - 1 : j;
    int jPlus = j < _arrayToRender[i].Count - 1 ? j + 1 : j;

    if (_GetTileHeight(_CaveTileToRenderTile(iMinus, jMinus)) > currTileHeight)
    {
        if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) <= currTileHeight && _GetTileHeight(_CaveTileToRenderTile(i, jMinus)) <= currTileHeight)
        {
            _DrawShadowTile(TileShadowType.NorthWest, drawPos, widthIncrement, heightIncrement);
        }
    }
    if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) > currTileHeight)
    {
        _DrawShadowTile(TileShadowType.North, drawPos, widthIncrement, heightIncrement);
    }
    if (_GetTileHeight(_CaveTileToRenderTile(iMinus, jPlus)) > currTileHeight)
    {
        if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) <= currTileHeight && _GetTileHeight(_CaveTileToRenderTile(i, jPlus)) <= currTileHeight)
        {
            _DrawShadowTile(TileShadowType.NorthEast, drawPos, widthIncrement, heightIncrement);
        }
    }
    if (_GetTileHeight(_CaveTileToRenderTile(i, jMinus)) > currTileHeight)
    {
        _DrawShadowTile(TileShadowType.West, drawPos, widthIncrement, heightIncrement);
    }
    if (_GetTileHeight(_CaveTileToRenderTile(i, jPlus)) > currTileHeight)
    {
        _DrawShadowTile(TileShadowType.East, drawPos, widthIncrement, heightIncrement);
    }
    if (_GetTileHeight(_CaveTileToRenderTile(iPlus, jMinus)) > currTileHeight)
    {
        if (_GetTileHeight(_CaveTileToRenderTile(i, jMinus)) <= currTileHeight)
        {
            _DrawShadowTile(TileShadowType.SouthWest, drawPos, widthIncrement, heightIncrement);
        }
    }
    if (_GetTileHeight(_CaveTileToRenderTile(iPlus, j)) > currTileHeight)
    {
        _DrawShadowTile(TileShadowType.South, drawPos, widthIncrement, heightIncrement);
    }
    if (_GetTileHeight(_CaveTileToRenderTile(iPlus, jPlus)) > currTileHeight)
    {
        if (_GetTileHeight(_CaveTileToRenderTile(i, jPlus)) <= currTileHeight)
        {
            _DrawShadowTile(TileShadowType.SouthEast, drawPos, widthIncrement, heightIncrement);
        }
    }
}

Danc’s instructions also include two special case instructions for shadow placement, but I didn’t need them for my map so I didn’t implement them.

Finally, in order to have the walls look taller, I implemented a bit of logic to render some of the walls with the 2-unit tall wall tile instead of the 1-unit tall version. In order to not obscure floor tiles located north of walls, I only drew tall walls in cases where the given wall tile had no floor tiles above it in the same column. Walls with floors to their north are rendered as 1-unit tall walls so the player will never find themselves completely obscured by the wall to their south.

Now that’s starting to look like a cave worth exploring!

Here’s the tile and shadow rendering code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace ProceduralWorldLib
{
    public enum RenderTileType
    {
        Floor,
        Wall,
        TallWall
    }

    public enum TileShadowType
    {
        SouthEast,
        South,
        SouthWest,
        East,
        West,
        NorthEast,
        North,
        NorthWest
    }

    public class MapRendererTilesComponent : MapRendererBaseComponent
    {
        class TileTexture
        {
            public string TexturePath;
            public Texture2D Texture;
            public Vector2 PositionOffset;
            public bool DrawFloor;              //Should we draw floor under this tile or not? (Will be drawn sans positionoffset)

            public TileTexture(string _texturePath, Vector2 _positionOffset, bool _drawFloor)
            {
                TexturePath = _texturePath;
                Texture = null;
                PositionOffset = _positionOffset;
                DrawFloor = _drawFloor;
            }
        }

        private SpriteBatch _spriteBatch;

        private int _tileWidth;
        private int _tileHeight;
        private Dictionary&lt;RenderTileType, TileTexture&gt; _tileTextures;
        private Dictionary&lt;TileShadowType, TileTexture&gt; _shadowTextures;
        private float _drawScale;

        private bool _drawShadows;
        private Vector2 _cameraPosition;

        public MapRendererTilesComponent(Game game, List&lt;List&lt;CaveSpaceType&gt;&gt; arrayToRender, int tileWidth, int tileHeight, float drawScale)
            : base(game, arrayToRender)
        {
            _tileWidth = tileWidth;
            _tileHeight = tileHeight;
            _tileTextures = new Dictionary&lt;RenderTileType, TileTexture&gt;(2);
            _shadowTextures = new Dictionary&lt;TileShadowType, TileTexture&gt;(8);
            _drawScale = drawScale;

            _drawShadows = true;
        }

        public Vector2 CameraPosition
        {
            get
            {
                return _cameraPosition;
            }
            set
            {
                _cameraPosition = value;
            }
        }

        public float DrawScale
        {
            set
            {
                _drawScale = value;
            }
        }

        public bool DrawShadows
        {
            set
            {
                _drawShadows = value;
            }
        }

        public void AddTile(RenderTileType spaceType, string texturePath, Vector2 posOffset)
        {
            _tileTextures.Add(spaceType, new TileTexture(texturePath, posOffset, false));
        }

        public void AddShadowTile(TileShadowType spaceType, string texturePath)
        {
            _shadowTextures.Add(spaceType, new TileTexture(texturePath, Vector2.Zero, false));
        }

        public override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            _spriteBatch = new SpriteBatch(Game.GraphicsDevice);

            foreach (KeyValuePair&lt;RenderTileType, TileTexture&gt; entry in _tileTextures)
            {
                entry.Value.Texture = Game.Content.Load&lt;Texture2D&gt;(entry.Value.TexturePath);
                Debug.Assert(entry.Value.Texture != null);
            }

            foreach (KeyValuePair&lt;TileShadowType, TileTexture&gt; entry in _shadowTextures)
            {
                entry.Value.Texture = Game.Content.Load&lt;Texture2D&gt;(entry.Value.TexturePath);
                Debug.Assert(entry.Value.Texture != null);
            }

            base.LoadContent();
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            _spriteBatch.Begin();

            Vector2 cameraTransform = _GetCameraTransform();
            Vector2 drawPos = Vector2.Zero;
            float widthIncrement = _tileWidth * _drawScale;
            float heightIncrement = _tileHeight * _drawScale;

            for (int i = 0; i < _arrayToRender.Count; ++i)
            {
                for (int j = 0; j < _arrayToRender[i].Count; ++j)
                {
                    RenderTileType renderTileType = _CaveTileToRenderTile(i, j);

                    Color drawColor = Color.White;
                    Vector2 tilePos = drawPos + cameraTransform + _tileTextures[renderTileType].PositionOffset * _drawScale;
                    
                    if (_tileTextures[renderTileType].DrawFloor)
                    {
                        //Draw floor underneath tiles with negative Y offset since you'll be able to see below them
                        _spriteBatch.Draw(_tileTextures[RenderTileType.Floor].Texture, drawPos + cameraTransform, null, drawColor, 0.0f, Vector2.Zero, _drawScale, SpriteEffects.None, 0.0f);
                    }
                    _spriteBatch.Draw(_tileTextures[renderTileType].Texture, tilePos, null, drawColor, 0.0f, Vector2.Zero, _drawScale, SpriteEffects.None, 0.0f);
                    if (_drawShadows)
                    {
                        _DrawShadow(i, j, tilePos, widthIncrement, heightIncrement);
                    }
                    drawPos.X += widthIncrement;
                }

                drawPos.X = 0.0f;
                drawPos.Y += heightIncrement;
            }

            _spriteBatch.End();
            base.Draw(gameTime);
        }

        private RenderTileType _CaveTileToRenderTile(int i, int j)
        {
            RenderTileType renderTileType = RenderTileType.Floor;
            if (_arrayToRender[i][j] == CaveSpaceType.Wall)
            {
                bool bFloorToNorth = false;
                for( int tempI = i; tempI > 0; --tempI)
                {
                    if (_arrayToRender[tempI][j] == CaveSpaceType.Floor)
                    {
                        bFloorToNorth = true;
                        break;
                    }
                }
                renderTileType = bFloorToNorth ? RenderTileType.Wall : RenderTileType.TallWall;
            }
            return renderTileType;
        }

        private Vector2 _GetCameraTransform()
        {
            return -_cameraPosition;
        }

        private int _GetTileHeight(RenderTileType renderTileType)
        {
            switch (renderTileType)
            {
                case RenderTileType.Floor:
                    return 0;
                case RenderTileType.Wall:
                    return 1;
                case RenderTileType.TallWall:
                    return 2;
            }
            return 0;
        }

        private void _DrawShadow(int i, int j, Vector2 drawPos, float widthIncrement, float heightIncrement)
        {
            int currTileHeight = _GetTileHeight(_CaveTileToRenderTile(i, j));

            int iMinus = i > 0 ? i - 1 : i;
            int iPlus = i < _arrayToRender.Count - 1 ? i + 1 : i;
            int jMinus = j > 0 ? j - 1 : j;
            int jPlus = j < _arrayToRender[i].Count - 1 ? j + 1 : j;

            if (_GetTileHeight(_CaveTileToRenderTile(iMinus, jMinus)) > currTileHeight)
            {
                if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) <= currTileHeight && _GetTileHeight(_CaveTileToRenderTile(i, jMinus)) <= currTileHeight)
                {
                    _DrawShadowTile(TileShadowType.NorthWest, drawPos, widthIncrement, heightIncrement);
                }
            }
            if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) > currTileHeight)
            {
                _DrawShadowTile(TileShadowType.North, drawPos, widthIncrement, heightIncrement);
            }
            if (_GetTileHeight(_CaveTileToRenderTile(iMinus, jPlus)) > currTileHeight)
            {
                if (_GetTileHeight(_CaveTileToRenderTile(iMinus, j)) <= currTileHeight && _GetTileHeight(_CaveTileToRenderTile(i, jPlus)) <= currTileHeight)
                {
                    _DrawShadowTile(TileShadowType.NorthEast, drawPos, widthIncrement, heightIncrement);
                }
            }
            if (_GetTileHeight(_CaveTileToRenderTile(i, jMinus)) > currTileHeight)
            {
                _DrawShadowTile(TileShadowType.West, drawPos, widthIncrement, heightIncrement);
            }
            if (_GetTileHeight(_CaveTileToRenderTile(i, jPlus)) > currTileHeight)
            {
                _DrawShadowTile(TileShadowType.East, drawPos, widthIncrement, heightIncrement);
            }
            if (_GetTileHeight(_CaveTileToRenderTile(iPlus, jMinus)) > currTileHeight)
            {
                if (_GetTileHeight(_CaveTileToRenderTile(i, jMinus)) <= currTileHeight)
                {
                    _DrawShadowTile(TileShadowType.SouthWest, drawPos, widthIncrement, heightIncrement);
                }
            }
            if (_GetTileHeight(_CaveTileToRenderTile(iPlus, j)) > currTileHeight)
            {
                _DrawShadowTile(TileShadowType.South, drawPos, widthIncrement, heightIncrement);
            }
            if (_GetTileHeight(_CaveTileToRenderTile(iPlus, jPlus)) > currTileHeight)
            {
                if (_GetTileHeight(_CaveTileToRenderTile(i, jPlus)) <= currTileHeight)
                {
                    _DrawShadowTile(TileShadowType.SouthEast, drawPos, widthIncrement, heightIncrement);
                }
            }
        }

        private void _DrawShadowTile(TileShadowType shadowType, Vector2 drawPos, float widthIncrement, float heightIncrement)
        {
            Vector2 shadowDrawPos = drawPos + _shadowTextures[shadowType].PositionOffset;
            _spriteBatch.Draw(_shadowTextures[shadowType].Texture, shadowDrawPos, null, Color.White, 0.0f, Vector2.Zero, _drawScale, SpriteEffects.None, 0.0f);
        }
    }
}
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print

One thought on “Virtual Spelunking: Tiles and Shadows

  1. Pingback: Virtual Spelunking: Exits and Treasure | Game Dev Without a Cause

Comments are closed.