Search Unity

2D Tile Mapping in Unity Demystified

Discussion in 'Community Learning & Teaching' started by BlueSin, Nov 16, 2016.

?

Did you find this guide helpful?

  1. Yes

  2. No

  3. A little

Results are only viewable after voting.
  1. BlueSin

    BlueSin

    Joined:
    Apr 26, 2013
    Posts:
    137
    2D Tile Mapping in Unity
    Demystified
    Introduction
    This is probably one of the most controversial topics in the Unity 2D development world. It is also something I see a great deal of questions about. I've spent the better part of about 5 years playing around with 2D in Unity and while the potential has grown exponentially, there is still room for growth.

    This tutorial is directed at helping you get started with building your own 2D worlds in Unity. At the end of the day every game will have its own path it will need to take, so I won't be going too terribly in depth. This tutorial will be geared towards top-down and side-scroller style games, and will not delve into the infinite mysteries of hexagonal, isometric, etc. mapping.

    Finally, this tutorial is geared for both beginners and advanced developers and artists. So if something is looking a little basic, feel free to skim over those parts if you need! As always feel free to ask questions in reply or by sending me a message. If this guide gets alot of interest, votes, and likes I will be more than happy to expand upon it and add more specifics.

    Thinking Ahead
    I think just about every developer's first stab at building 2D worlds in Unity is to build an array of game objects to be their "tile map". This is a great, simple strategy if you are building very, very small maps. But if you are building worlds larger than say 64x64 tiles you probably found out pretty quickly that this method is not conventional for building tile worlds in Unity.

    Another mistake many will make in their Map class designs is to have a Tile class or struct, and their Map class will have a list or array of these Tile instances. Again, not a bad strategy for small maps, but when you are building larger worlds you will notice your load times significantly increase the larger the world gets.

    Another mistake I might point out is that many do not consider all the things that will go into their world. Take your class 2D top-down RPG like Chrono Trigger for example. You will have numerous "layers" in your map, things that get drawn in front of and behind your player and other actors. Then of course you have your base tiles, and tile decorations, objects, actors, etc. Once run together your maps start to become heavier.

    My point here is that you need to think ahead. Proper planning of your game projects is vastly important for you to realize the needs of your game. You don't want to force your players to have super computers to play a 2D tile-based pixel art game! So enough pointing out mistakes, lets move on to solving them!

    Tile Map Design

    Screenshot of Starbound by Chucklefish

    The first thing I am going to say here is that the way you design your maps may be vastly different than what is described here. It all depends on what you are trying to do, and I could write a few books worth of content on how to cover the majority of them. So I will cover a bit of it, but try to leave you with enough knowledge to make the decision yourselves.

    The first thing we are going to cover here is map design, that being the layers of a map. In Unity, sorting order & sorting layers are excellent methods of prioritizing what gets drawn in front of or behind what.

    Let's take a moment to analyze the map layers for a Terraria / Starbound style map, which is a 2D tile based, pixel art, side-scrolling sandbox game. We'll cover the terminology and the layers themselves.



    Terraria / Starbound Style Map

    Parallax Backgrounds
    Ever notice in that 2D side scroller how the backdrop moves? Maybe those rolling hills move in opposing directions at varying speeds? This effect is known as parallax, and will represent the very back layer of your map. You can find these types in both top-down and side-scrolling games. And again, Terraria and Starbound both use parallaxing in their maps if you need examples.


    Decorations vs. Base Tiles
    So what is a decoration? Simply put, a decoration is a tile that has transparency in it such as ore nestled inside stone, or grass/flowers on top of the terrain. Decorations are drawn in front of their respective layer, so decorations for the foreground get drawn in front of the foreground and decorations for the background get drawn in front of the background.

    What is a base tile? Base tiles are tiles that typically have no transparency whatsoever. There are however exceptions and a classic example is Terraria / Starbound, where the edge tiles have transparency in them, but still classify as a base tile. Typically if there is an empty base tile, we do not put a decoration there. Unless you think it might look beautiful, then by all means.

    So now, why have these decorations and not just make them objects, or integrate ores into base tiles rather than having to draw additional tiles? Very good point, and that is one of the decisions you need to make regarding your game projects and both have merits. For example, if I use decorations I can place ore inside anything without having to draw or work on more tiles. If you integrate ores into your base tiles you will have to create tiles for dirt with ore, dirt without ore, stone with ore, stone without ore, etc, etc.

    Terraria makes ores its very own tile type so they don't have to deal with ores inside other materials. Whereas Starbound uses decorations so they can put ores inside just about anything.

    The Object Layer
    Continuing with our Terraria / Starbound style map example, objects differ from decorations in that they can have scripts attached to them. For example, planted throughout the procedurally generated worlds of Starbound & Terraria you will find treasure chests, containers, and other interactables. You cannot simply represent these objects with tiles, we need to know what function they serve in the world and integrating this functionality and attaching it to tiles over-complicates the map design. This functionality also needs to extend to any objects the player may place such as anvils, forges, beds, doors, etc. As a rule, these objects are drawn behind the player.

    The Actor Layer
    So what exactly is an actor? An actor is any moving object that traverses your world such as NPCs, monsters, players, etc. These are fairly self explanatory, and are also referred to as entities. Typically they are driven by a controller, and for non-player based actors their controller is driven by an AI (Artificial Intelligence).

    One additional thing to think about here is if your actor fires projectiles or effects of any kind. These projectiles/effects are typically drawn in front of actors in the sorting order.

    Background vs. Foreground
    This should be fairly self explanatory. But returning to our picture above, foreground is drawn in front of actors and effects in a side-scroller typically. Background is drawn behind actors, effects, and objects. Again, this is pretty self explanatory so I won't go into much more detail than that.

    The Collision Layer
    Another layer that is not drawn in the picture above is the collision layer. Unless you are making a space shmup you probably want a surface for your player to stand on. The collidable layer is essentially the foreground. So if there is no tile there, then it is air and your player can fall through that space. If there is a tile there, there your player should be standing on it.

    Top-Down Maps


    Alot of what was described above was meant for side scrollers. But maybe you are making a top-down style game? Alot of the same rules apply, but the layers vary a bit. Also typically in a top-down style map there is a sorting order variability. Your player needs to be able to stand in front of an object and have it drawn behind them, and stand behind that same object and have it drawn in front of them. This is a more advanced concept so I won't cover it here in detail. A simple calculation for basic handling of this is:

    Code (CSharp):
    1. renderer.sortingOrder = (int)(mapHeight - transform.position.y);

    Your objects and entities will need to make use of this calculation for their sorting order. However your entities sorting order will need to be constantly updated as they move across your map.

    Essentially your layers in a top-down style map are going to be Base Tile, Tile Decoration, Actor, Object, Height 1, and Height 2.

    Base Tile is the very bottom layer that everything gets drawn on top of. This is where you draw your grass, dirt, etc.

    Decoration is where you will draw mushrooms, logs, flowers, dirt paths, etc. Everything gets drawn on top of this layer as well except base tiles obviously.

    Actor & Object we have already described above.

    Height 1 and Height 2 are the height layers that add height to your map, if that makes any sense at all. These are typically drawn in front of the player, like roofs, overhanging bridges, etc.


    Tile Map Coding
    This is where we start getting into the meat and potatoes of your map design. How you respresent your maps in code will ultimately mean your success or your downfall.

    I always ask developers to not just think of the future of their current game project, but think ahead to your future game projects. Design a base editor / tile engine that will be the foundation for all your games. Then expand upon it within your individual game projects as your game needs.

    Showing World Details
    Another thing I want you to think about is if the player will be able to view the details of worlds. In Terraria, a player can play on several different worlds and you let them decide which world they want to play on. For stuff like that you are going to need to load your world file in to get the data for that world such as its name, size, etc.

    Consequently, your world files will typically contain ALL the tile data as well. We don't need the tile data yet at world selection. So for these scenarios I recommend creating a light-weight class that gets saved alongside your map data (in a separate file) that holds the name, size, etc. details you want to show. Here is an example of this class from one of my current projects:

    Code (CSharp):
    1. using System;
    2.  
    3. using System.IO;
    4. using System.Runtime.Serialization.Formatters.Binary;
    5. using UnityEngine;
    6.  
    7. [Serializable]
    8. public struct PaperWorldData
    9. {
    10.     #region Fields & Properties
    11.  
    12.     string name;
    13.     public string GetName { get { return name; } }
    14.  
    15.     PaperWorld.WorldSize worldSize;
    16.     public PaperWorld.WorldSize GetWorldSize { get { return worldSize; } }
    17.  
    18.     int width, height;
    19.     public int GetWidth { get { return width; } }
    20.     public int GetHeight { get { return height; } }
    21.  
    22.     #endregion
    23.  
    24.     #region Constructors
    25.  
    26.     public PaperWorldData(string name, PaperWorld.WorldSize worldSize, int width, int height)
    27.     {
    28.         this.name = name;
    29.         this.worldSize = worldSize;
    30.         this.width = width;
    31.         this.height = height;
    32.     }
    33.  
    34.     #endregion
    35.  
    36.     #region Public Methods
    37.  
    38.     /// <summary>
    39.     /// Saves this Paper World Data to a file matching map name.
    40.     /// </summary>
    41.     public void Save()
    42.     {
    43.         BinaryFormatter bf = new BinaryFormatter();
    44.         FileStream file = File.Create(Application.persistentDataPath + Global.File_Save_Path + name + Global.World_Header_Extension);
    45.         bf.Serialize(file, this);
    46.         file.Close();
    47.     }
    48.  
    49.     #endregion
    50. }

    The Map Class
    So I mentioned earlier one of the common mistakes of beginners was to place their tiles inside a tile class or struct. I strongly recommend against this, as I have personally timed deserialization times of Map classes with large tile arrays of tiles inside these classes, and you get a massive speed gain when you STOP using them.

    Instead I recommend just throwing out arrays of int, sbyte, or byte to represent your tiles. Not jagged or multi-dimensional arrays mind you, regular arrays. Why? Yet more massive speed gains.

    Auto Tiling
    Okay so what is auto tiling you might be asking yourself? In a traditional tilesheet, tiles will have corners, middles, etc. that are meant to be put together to represent tiles of the same type next to each other. (i.e. grass, dirt, etc.) Let's see an illustration of that:


    Auto Tiling Example

    On the left side we see a map with no auto tiling. On the right we see a map with auto tiling. See the difference now? Looking back at some of your favorite tile based games, there is hardly a single one of them that doesn't use this.

    Now auto-tiling is yet another one of those things that I just cannot go in depth with. For these things, I will be leaving you with links in my resources section. Essentially you have 4-bit auto tiling which takes only tiles left, right, above, and below into consideration. And then you have 8-bit auto tiling which takes all edges including corners into consideration. The result of which is a larger tilesheet. 4-bit auto tiling tilesheets are typically represented by 16 tiles, whereas 8-bit auto tiling tilesheets can represent upwards of 64 tlies.

    I will typically autotile an entire map when I load the tile data in as a part of the map "loading" process. Autotiling can be an expensive and time consuming process if not done in a bitwise way, but regardless try to write your design around only doing a full map autotile at load. And if I am making a sandbox world that can be edited, I will autotile a 3x3 tile area around edited tiles when an edit was made. AutoTiling a point rather than a whole map is a very light-weight, and fast paced process.

    Representing Tile Data in Code

    Tiles are represented in code as a 3 part process.

    Tile ID: The index of a tile within an array.
    Tile Value: The tilesheet to use.
    Tile Sub-Value: The actual tile within a tilesheet to use.


    Ok, let's throw out a few lines of code here as an example of how we should represent tiles in code.

    Code (CSharp):
    1. ushort[] baseTiles = new ushort[mapWidth * mapHeight];
    2. [NonSerialized]
    3. byte[] baseAutoTiles = new byte[mapWidth * mapHeight];

    The index that I am using to access a tile in either of these arrays is the Tile ID.

    The first array represents the tilesheet to use for every base tile in the world. This is our Tile Value. However, it doesn't tell you what tile to use within that sheet. This is the purpose of the second array, which holds the tile in the tilesheet I should be using. That is Tile Sub-Value. This is also where autotiling comes in to play.

    Notice how I have chosen not to serialize my autotile array? This is unnecessary storage information when writing our map file. So long as I have the tilesheet data, my autotiling algorithm will be able to tell me exactly which tile in the sheet I should be using.

    Also pay close attention to my usage of types here. For the first array, I know that my world will use more than 255 tilesheets. Therefore I could not use a byte and instead used the next smallest type, a ushort which allows up to 65,535 tilesheets. Way more than enough! Also for my autotile data, I do not expect to have more than 255 tiles in a tilesheet. So I used a byte in this scenario. That means each tile in my world is represented in memory using 24-bits of data. If I had carelessly used integers for this, I would be looking at 64-bits of data. This becomes incredibly important to minimize not only memory usage, but for multiplayer when we need to send tile data to clients. If my entire map was represented by only this one tile and it was 128x128 tiles wide/high, it would be a 48KB map. So just keep that in mind, the bits add up!

    Alright, so now let's take a look at a whole map class for a map that I am currently writing. I have broken the map up into 3 parts: Map Header, Map Data, and Map Layer. Map header was shown above and contains the map details. It is written to file as a .map file. Map Data and Map Layer are written as a single file with the extension of .dat. For the sake of simplicity in my coding, I repeat the map header information in the map data as it is only a few bytes of extra data. This could turn out to be a bad idea so keep that in mind if you are copying this code.


    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using System.Runtime.Serialization.Formatters.Binary;
    5. using UnityEngine;
    6.  
    7. /// <summary>
    8. /// Name: PaperWorld.cs
    9. /// Created by: BlueSin
    10. /// Created on: 10/12/2014
    11. /// Last Updated by: BlueSin
    12. /// Last Updated on: 11/16/2016
    13. /// Copyright: Pixelsoft Games, LLC. 2014 - 2016
    14. /// Description: TODO
    15. /// </summary>
    16. [Serializable]
    17. public class PaperWorld
    18. {
    19.     public enum WorldSize { Small, Medium, Large, Custom }
    20.  
    21.     #region Fields
    22.  
    23.     /// <summary>
    24.     /// Serialized Fields
    25.     /// </summary>
    26.     // Name of the current world
    27.     string name;
    28.     // Size of the world
    29.     WorldSize worldSize;
    30.     // Width & Height of the current world
    31.     int width, height;
    32.     // List of layers in the world
    33.     List<PaperWorldLayer> layers;
    34.  
    35.     #endregion
    36.  
    37.     #region Properties
    38.  
    39.     /// <summary>
    40.     /// Gets the name of this world.
    41.     /// </summary>
    42.     public string GetName { get { return name; } }
    43.     /// <summary>
    44.     /// Gets the size of this world.
    45.     /// </summary>
    46.     public WorldSize GetWorldSize { get { return worldSize; } }
    47.     /// <summary>
    48.     /// Gets the width of this world in tiles.
    49.     /// </summary>
    50.     public int GetWidth { get { return width; } }
    51.     /// <summary>
    52.     /// Gets the height of this world in tiles.
    53.     /// </summary>
    54.     public int GetHeight { get { return height; } }
    55.     /// <summary>
    56.     /// Gets the number of layers in this map.
    57.     /// </summary>
    58.     public int GetLayerCount { get { return layers.Count; } }
    59.  
    60.     #endregion
    61.  
    62.     #region Constructors
    63.  
    64.     /// <summary>
    65.     /// Default constructor for the PaperWorld class.
    66.     /// </summary>
    67.     public PaperWorld() { }
    68.  
    69.     /// <summary>
    70.     /// Overloaded constructor for the PaperWorld class that uses a pre-defined World Size.
    71.     /// </summary>
    72.     /// <param name="name">Name of this world.</param>
    73.     public PaperWorld(string name, WorldSize worldSize)
    74.     {
    75.         this.name = name;
    76.         this.worldSize = worldSize;
    77.  
    78.         if (GetWorldSize == WorldSize.Custom)
    79.             MeasureWorld(WorldSize.Small);
    80.         else
    81.             MeasureWorld(GetWorldSize);
    82.  
    83.         layers = new List<PaperWorldLayer>();
    84.     }
    85.  
    86.     /// <summary>
    87.     /// Overloaded constructor for the PaperWorld class that uses custom World Size.
    88.     /// </summary>
    89.     /// <param name="name">Name of this world.</param>
    90.     /// <param name="width">Width of this world in tiles.</param>
    91.     /// <param name="height">Height of this world in tiles.</param>
    92.     public PaperWorld(string name, int width, int height)
    93.     {
    94.         this.name = name;
    95.         worldSize = WorldSize.Custom;
    96.         this.width = width;
    97.         this.height = height;
    98.         layers = new List<PaperWorldLayer>();
    99.     }
    100.  
    101.     #endregion
    102.  
    103.     #region Public Methods
    104.  
    105.     #region Layers
    106.  
    107.     /// <summary>
    108.     /// Adds a new layer to the map.
    109.     /// </summary>
    110.     public void AddLayer(bool autoTile)
    111.     {
    112.         layers.Add(new PaperWorldLayer(GetWidth, GetHeight, autoTile));
    113.     }
    114.  
    115.     /// <summary>
    116.     /// Removes the last layer from the map.
    117.     /// </summary>
    118.     public void RemoveLayer()
    119.     {
    120.         layers.RemoveAt(layers.Count - 1);
    121.     }
    122.  
    123.     #endregion
    124.  
    125.     #region Tiles
    126.  
    127.     /// <summary>
    128.     /// Gets a tile value by x, y coordinate.
    129.     /// </summary>
    130.     /// <param name="x">X coordinate of the tile.</param>
    131.     /// <param name="y">Y coordinate of the tile.</param>
    132.     /// <param name="layerID">Layer ID the tile is located in.</param>
    133.     public ushort GetTileValue(int x, int y, int layerID)
    134.     {
    135.         return layers[layerID].GetTileValue(GetTileID(x, y));
    136.     }
    137.  
    138.     /// <summary>
    139.     /// Gets a tile sub value by x, y coordinate.
    140.     /// </summary>
    141.     /// <param name="x">X coordinate of the tile.</param>
    142.     /// <param name="y">Y coordinate of the tile.</param>
    143.     /// <param name="layerID">Layer ID the tile is located in.</param>
    144.     public byte GetTileSubValue(int x, int y, int layerID)
    145.     {
    146.         return layers[layerID].GetTileSubValue(GetTileID(x, y));
    147.     }
    148.  
    149.     /// <summary>
    150.     /// Sets a tile by x, y coordinate.
    151.     /// </summary>
    152.     /// <param name="x">X coordinate of the tile.</param>
    153.     /// <param name="y">Y coordinate of the tile.</param>
    154.     /// <param name="layerID">Layer ID the tile is located in.</param>
    155.     /// <param name="value">Value to set to the tile.</param>
    156.     /// <param name="subValue">Sub Value to set to the tile.</param>
    157.     public void SetTile(int x, int y, int layerID, ushort value, byte subValue)
    158.     {
    159.         layers[layerID].SetTile(GetTileID(x, y), value, subValue);
    160.     }
    161.  
    162.     #endregion
    163.  
    164.     #region Decorations
    165.  
    166.     /// <summary>
    167.     /// Gets a decoration value by x, y coordinate.
    168.     /// </summary>
    169.     /// <param name="x">X coordinate of the decoration.</param>
    170.     /// <param name="y">Y coordinate of the decoration.</param>
    171.     /// <param name="layerID">Layer ID the decoration is located in.</param>
    172.     public ushort GetDecorationValue(int x, int y, int layerID)
    173.     {
    174.         return layers[layerID].GetDecorationValue(GetTileID(x, y));
    175.     }
    176.  
    177.     /// <summary>
    178.     /// Gets a decoration sub value by coordinate.
    179.     /// </summary>
    180.     /// <param name="x">X coordinate of the decoration.</param>
    181.     /// <param name="y">Y coordinate of the decoration.</param>
    182.     /// <param name="layerID">Layer ID the decoration is located in.</param>
    183.     public byte GetDecorationSubValue(int x, int y, int layerID)
    184.     {
    185.         return layers[layerID].GetDecorationSubValue(GetTileID(x, y));
    186.     }
    187.  
    188.     /// <summary>
    189.     /// Sets a decoration by x, y coordinate.
    190.     /// </summary>
    191.     /// <param name="x">X coordinate of the decoration.</param>
    192.     /// <param name="y">Y coordinate of the decoration.</param>
    193.     /// <param name="layerID">Layer ID the decoration is located in.</param>
    194.     /// <param name="value">Value to set to the decoration.</param>
    195.     /// <param name="subValue">Sub Value to set to the decoration.</param>
    196.     public void SetDecoration(int x, int y, int layerID, ushort value, byte subValue)
    197.     {
    198.         layers[layerID].SetDecoration(GetTileID(x, y), value, subValue);
    199.     }
    200.  
    201.     #endregion
    202.  
    203.     #region Utility
    204.  
    205.     /// <summary>
    206.     /// Gets a tile ID by x, y coordinate.
    207.     /// </summary>
    208.     /// <param name="x">X coordinate of the tile.</param>
    209.     /// <param name="y">Y coordinate of the tile.</param>
    210.     public int GetTileID(int x, int y)
    211.     {
    212.         return (y * width) + x;
    213.     }
    214.  
    215.     /// <summary>
    216.     /// Saves the current Paper World to a file.
    217.     /// </summary>
    218.     public void Save()
    219.     {
    220.         BinaryFormatter bf = new BinaryFormatter();
    221.         FileStream file = File.Create(Application.persistentDataPath + Global.File_Save_Path + name + Global.World_Data_Extension);
    222.         bf.Serialize(file, this);
    223.         file.Close();
    224.     }
    225.  
    226.     #endregion
    227.  
    228.     #endregion
    229.  
    230.     #region Private Methods
    231.  
    232.     #region Utility
    233.  
    234.     /// <summary>
    235.     /// Sets tile width & height of the world based on world size.
    236.     /// </summary>
    237.     private void MeasureWorld(WorldSize worldSize)
    238.     {
    239.         switch(worldSize)
    240.         {
    241.             case WorldSize.Small: // Small
    242.                 width = Global.SM_World_Width;
    243.                 height = Global.SM_World_Height;
    244.                 break;
    245.             case WorldSize.Medium: // Medium
    246.                 width = Global.MED_World_Width;
    247.                 height = Global.MED_World_Height;
    248.                 break;
    249.             case WorldSize.Large: // Large
    250.                 width = Global.LG_World_Width;
    251.                 height = Global.LG_World_Height;
    252.                 break;
    253.         }
    254.     }
    255.  
    256.     #endregion
    257.  
    258.     #endregion
    259. }

    Code (CSharp):
    1. using System;
    2.  
    3. /// <summary>
    4. /// Name: PaperWorldLayer.cs
    5. /// Created by: BlueSin
    6. /// Created on: 10/12/2014
    7. /// Last Updated by: BlueSin
    8. /// Last Updated on: 11/16/2016
    9. /// Copyright: Pixelsoft Games, LLC. 2014 - 2016
    10. /// Description: TODO
    11. /// </summary>
    12. [Serializable]
    13. public class PaperWorldLayer
    14. {
    15.     #region Fields
    16.  
    17.     // Should this layer be auto tiled?
    18.     bool autoTile;
    19.  
    20.     // TileSheet values for tiles & decorations
    21.     ushort[] tileValues;
    22.     ushort[] decorationValues;
    23.  
    24.     [NonSerialized]
    25.     // Tile values for tiles
    26.     byte[] tileSubValues;
    27.     [NonSerialized]
    28.     // Tile values for decorations
    29.     byte[] decorationSubValues;
    30.  
    31.     #endregion
    32.  
    33.     #region Constructors
    34.  
    35.     /// <summary>
    36.     /// Default constructor for the PaperWorldLayer class.
    37.     /// </summary>
    38.     public PaperWorldLayer() { }
    39.  
    40.     /// <summary>
    41.     /// Overloaded constructor for the PaperWorldLayer class.
    42.     /// </summary>
    43.     /// <param name="worldWidth"></param>
    44.     /// <param name="worldHeight"></param>
    45.     /// <param name="autoTile"></param>
    46.     public PaperWorldLayer(int worldWidth, int worldHeight, bool autoTile)
    47.     {
    48.         tileValues = new ushort[worldWidth * worldHeight];
    49.         decorationValues = new ushort[worldWidth * worldHeight];
    50.         tileSubValues = new byte[worldWidth * worldHeight];
    51.         decorationSubValues = new byte[worldWidth * worldHeight];
    52.         this.autoTile = autoTile;
    53.     }
    54.  
    55.     #endregion
    56.  
    57.     #region Public Methods
    58.  
    59.     #region Tiles
    60.  
    61.     /// <summary>
    62.     /// Gets a tile's value by tile ID.
    63.     /// </summary>
    64.     /// <param name="tileID">ID of the tile.</param>
    65.     /// <returns></returns>
    66.     public ushort GetTileValue(int tileID)
    67.     {
    68.         return tileValues[tileID];
    69.     }
    70.  
    71.     /// <summary>
    72.     /// Gets a tile's sub value by ID.
    73.     /// </summary>
    74.     /// <param name="tileID"></param>
    75.     /// <returns></returns>
    76.     public byte GetTileSubValue(int tileID)
    77.     {
    78.         return tileSubValues[tileID];
    79.     }
    80.  
    81.     /// <summary>
    82.     /// Sets a tile by tile ID, and also sets sub value.
    83.     /// </summary>
    84.     /// <param name="tileID">ID of the tile.</param>
    85.     /// <param name="value">Value to set the tile to.</param>
    86.     /// <param name="subValue">Sub Value to set the tile to.</param>
    87.     public void SetTile(int tileID, ushort value, byte subValue)
    88.     {
    89.         tileValues[tileID] = value;
    90.         tileSubValues[tileID] = subValue;
    91.     }
    92.  
    93.     #endregion
    94.  
    95.     #region Decorations
    96.  
    97.     /// <summary>
    98.     /// Gets a tile decoration's value by tile ID.
    99.     /// </summary>
    100.     /// <param name="tileID">ID of the decoration.</param>
    101.     /// <returns></returns>
    102.     public ushort GetDecorationValue(int tileID)
    103.     {
    104.         return decorationValues[tileID];
    105.     }
    106.  
    107.     /// <summary>
    108.     /// Gets a tile decoration's sub value by tile ID.
    109.     /// </summary>
    110.     /// <param name="tileID">ID of the decoration.</param>
    111.     /// <returns></returns>
    112.     public byte GetDecorationSubValue(int tileID)
    113.     {
    114.         return decorationSubValues[tileID];
    115.     }
    116.  
    117.     /// <summary>
    118.     /// Sets a tile decoration's sub value by tileID.
    119.     /// </summary>
    120.     /// <param name="tileID">ID of the decoration.</param>
    121.     /// <param name="value">Value to set to the decoration.</param>
    122.     /// <param name="subValue">Sub Value to set to the decoration.</param>
    123.     public void SetDecoration(int tileID, ushort value, byte subValue)
    124.     {
    125.         decorationValues[tileID] = value;
    126.         decorationSubValues[tileID] = subValue;
    127.     }
    128.  
    129.     #endregion
    130.  
    131.     #region Auto Tiling
    132.  
    133.     /// <summary>
    134.     /// Auto tiles the entire layer.
    135.     /// </summary>
    136.     public void AutoTileAll()
    137.     {
    138.         if (!autoTile)
    139.             return;
    140.  
    141.         // TODO: Implement auto tiling algorithm
    142.     }
    143.  
    144.     /// <summary>
    145.     /// Auto tiles a single point.
    146.     /// </summary>
    147.     /// <param name="x"></param>
    148.     /// <param name="y"></param>
    149.     public void AutoTilePoint(int x, int y)
    150.     {
    151.         if(!autoTile)
    152.             return;
    153.  
    154.         // TODO: Implement auto tiling algorithm
    155.     }
    156.  
    157.     #endregion
    158.  
    159.     #endregion
    160. }

    And there it is. Keep in mind this code is still being designed and optimized, as well as commented. So there may be some gaps there but it is a very solid set of code.

    One thing I will point out here is that I threw my tile information for the layers into a separate layers class that is represented as a list in my Map Data. Using a list WILL slow down access times for tile inquiries. However, by doing things this way my code is not only more organized, but I can build my tile engine to automatically create objects to represent however many layers my maps will have. That is, one object per layer which I will go into in the next section. If I had not done things this way, I would have to tell it manually how many layers to expect, which starts getting into specific game design. But by doing it this way, I can now use this code across all of my tile map games.

    Another thing I will point out is that my Map Layer class has a boolean field called autotile. The purpose of this is to tell the class whether that layer should be auto tiled or not. This leaves me the choice of auto tiling or manually tiling layers later on in an editor.

    If you build it...
    Okay so now we have planned the design, and written our code. Now the big question is, how the heck do we implement it? Truth be told there are many ways, some use Game Objects, others use Quads, while others use Meshes. Each has its merits, and its downsides, and its rightful place. Me personally? I am a mesh guy, even for small maps, mesh it out.

    Unfortunately, there is simply way too much to cover on the topic of implementation by any means, especially meshes. So again I will leave you with a nifty link in the resources section. The tutorial I followed up until it started implementing 3D at which point I stopped. Then I tailored the code to my own devices.

    So lets cover some concepts of your "tile engine", if it could be called that.

    Chunk Mapping

    Unless your maps are incredibly small, at some point or another you are going to need to look into implementing things using chunks. What is a chunk you ask? A chunk is a chunk of your map, X tiles wide by Y tiles high. When a player approaches an unloaded chunk, it looks into the tile data, builds a mesh to visually represent its area, and displays it. As your player moves away from displayed chunks and they go out of view, your chunk will unload. This method ensures we are only displaying the portions of the map we need.

    Caching and Recycling
    When a chunk is unloaded, what should we do with the data it used to build itself? We should cache it if we can! This will allow us to more quickly rebuild that chunk when the player goes near it again.

    What about the non-cache based resources? Recycle! Recycle your game objects & meshes into a pool for use later.

    The only time we need to dump our cache is when we use editors or have a sandbox game like Terraria or Starbound. A player destroys a block and the cached data is no longer valid. So we need to build a new cache and rebuild the chunk visually. It is important that you make whatever adjustments you can to build your chunks as quickly as possible. Things like physics and liquids come into mind here, not to mention visual acuity. Don't want your players seeing that chunk getting destroyed or built!

    Pooling
    Pools are your friend when it comes to game design. If you do not understand how to pool, I will leave you a nifty link in the resources section. Pooling allows you to re-use an object later without having to re-instantiate it again. A good example is bullets. Why create a brand new game object every time if you don't have to? Recycle it with pooling today!

    Multi-Threading
    Okay, first let me start out by saying Unity is NOT THREAD SAFE. You take your life into your own hands when you decide to thread in Unity. Now with that said does that mean you cannot do it? You absolutely can, and I will leave you a link to another valuable resource in the resources section to learn how to do it. I myself use it for chunk building, among other things and it is beyond amazing.

    Pathfinding
    For pathfinding I always recommend an A* Pathfinding algorithm that plots paths using a graph. This is always a fun topic to discuss. So for a top-down game I find the easiest way to handle this is to have an array that is the same size as your tile array and you can mark tiles as open / closed on your graph.

    For side scrollers it gets a bit more complex. Usually a tile of 0 = air (or open), whereas anything else is closed. Some entities can ignore this however such as burrowing worms that move behind the terrain (like Terraria worm monsters).

    Dealing with Tile Bleeding & Tearing
    Alot of people working on these maps have issues with tile bleeding. If you are following the mesh way like I do and have properly calculated texture units in your UV mapping then I have a few extra suggestions for you. First and foremost go to Edit > Project Settings > Quality and disable Anti-Aliasing and Anisotropic from all quality settings. Also ensure your sprite sheets are imported using Point filtering. I also typically leave the pixels per unit setting at the default of 100, and disable mip mapping. Also be sure to overrule your quality for each asset to true color, being sure to set the size appropriately.

    Asset Store vs. DIY
    The Asset Store is a valuable tool that can be used in game design. I don't want to put in the effort or don't know how, so I will buy a solution. If you know what you are doing, then by all means go ahead. But I strongly recommend that if you DON'T know how to do it yourself, you learn how to do so first.

    Why do I say that? Because many asset store resources are not going to be capable of doing what you want your game to do. So either you can build your game around that asset store purchase, or you will have to tailor it to do what you want. And if you have no idea how to do the thing in the first place, how are you possibly going to tailor it to do what you want?

    Now, you gain 2 valuable assets by doing it yourself. First and foremost, you gain the knowledge of how something works. Secondly, you can create a powerful asset that is custom tailored to your specific game project. Hand made assets will always trump asset store purchases hands down, at least in my opinion.

    Resources
    Auto-Tiling Guide #1
    Auto-Tiling Guide #2
    Auto-Tiling Guide #3
    Unity Voxel Tutorial (only follow until 3D)
    Pooling in Unity
    Multi-Threading in Unity

    Terraria Game
    Starbound Game

    Conclusion
    And so we have come to the end of what has been a very long lecture void of very many pictures. For all the reading I apologize, but this guide was intended to focus on strategy, raising questions, etc. Essentially, it was more focused on the WHY than the HOW. I hope you were able to take something from it, and I look forward to hearing about your creations moving forward! Good luck!
     
    Last edited: Dec 15, 2016
  2. ShokeR0

    ShokeR0

    Joined:
    Feb 24, 2016
    Posts:
    112
    This was extremely helpful. thanks
     
    BlueSin likes this.
  3. BlueSin

    BlueSin

    Joined:
    Apr 26, 2013
    Posts:
    137
    Thank you very much, I really appreciate it. And I am always willing to add to this guide if anybody is interested in more details of course. Or if you think there is something I have not covered that you are curious about.
     
    theANMATOR2b likes this.
  4. htmoore

    htmoore

    Joined:
    Nov 18, 2016
    Posts:
    6
    Thank BlueSin. Very good info.
     
  5. Luxumbra

    Luxumbra

    Joined:
    Sep 21, 2018
    Posts:
    1
    Hi there, I'm reading this a few years later, but this was a wonderful amount of information that for some reason no one really talks about! I'm currently making my first foray into coding a top down 2d RPG, but its my first time coding as well as my first time making a game on my own (I made a 2D puzzle platformer with my friend). A big question I keep wanting to find an answer to is "Can I make different sized assets that do not follow a tile pattern?" An obvious example would be Hyper Light Drifter. HLD seems to use a mixture of tile sets and large assets, like a statue or building. Does this cause problems? Is it as simple as dragging and dropping it in? Does it need to follow rules, like an even amount of pixels? or can they be odd shaped? Sorry for the text overflow, but I can't seem to fins any forums that discuss this :/ Thanks again for posting all of this information :)