Open World in Unity – Initial Investigations

Spider-Man is not only one of my favourite comic books characters but is also one of my favourite video games of all time.

Of course, I was pretty excited when they put up a technical post-mortem (yes excited. I know. I am a massive nerd.)

Since watching the post mortem, in my typical punk, “fuck custom game engines, I can do it in Unity” way. I started looking at how you could do an Open World City in Unity.

This article explains my first step of an investigation into making an Open World city in the vein of Spider-Man or GTA.

The first thing I did was grabbed the Windridge City package from Unity which has assets that I can use to make a city and a 3rd Person Character controller off the asset store.

I followed other games and made the city as a grid. Each with individual tiles. Each tile has a trigger box collider on it.

I wrote a couple of scripts to build and manage the city and its tiles

CityMap.cs

public class CityMap : MonoBehaviour
{
    [SerializeField]
    private List<CityTile> _tiles = new List<CityTile>();

    [SerializeField]
    private int _radius = 2;

    [System.Serializable]
    private class LODData
    {
        public int Radius;
        public int LodIndex;
    }

    [SerializeField]
    private LODData[] _lodData;

    private void Awake()
    {
        Events.Instance.AddListener<MovedTileEvent>(OnMoveTile);
    }

    private void OnMoveTile(MovedTileEvent e)
    {
        int count = _tiles.Count;
        foreach (var lod in _lodData)
        {
            int lodRadius = lod.Radius;
            var xPosTiles = _tiles.Where(t => t.X >= e.X + lodRadius);
            var yPosTiles = _tiles.Where(t => t.Y >= e.Y + lodRadius);
            var xNegTiles = _tiles.Where(t => e.X <= e.X - lodRadius);
            var yNegTiles = _tiles.Where(t => e.Y <= e.Y - lodRadius);
            var tiles = new List<CityTile>();
            tiles.AddRange(xPosTiles);
            tiles.AddRange(yPosTiles);
            tiles.AddRange(xNegTiles);
            tiles.AddRange(yNegTiles);
            tiles.ForEach(x => x.LodGroup.ForceLOD(lod.LodIndex));
        }
    }

    public void AddTile(CityTile tile)
    {
        _tiles.Add(tile);
    }
}

CityTile.cs

using UnityEngine;

public class CityTile : MonoBehaviour
{
    public int X;
    public int Y;

    public LODGroup LodGroup { private set; get; }

    private void Awake()
    {
        LodGroup = GetComponentInChildren<LODGroup>();
        LodGroup.fadeMode = LODFadeMode.CrossFade;
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            Events.Instance.Raise(new MovedTileEvent() { X = this.X, Y = this.Y });
        }
    }
}

The idea of the above was simple. When a player moved the tile we would decide if we should Cull, show a lower LOD of the tile or show the full LOD.

This would be done by overriding the functionality of the LOD Group.

In CityMap.cs you can see this private serialized class

    [System.Serializable]
    private class LODData
    {
        public int Radius;
        public int LodIndex;
    }

    [SerializeField]
    private LODData[] _lodData;

Essentially, when the player moves tiles, the game takes the players current tile and culls, LODs or does nothing based on how far away the tile is from the current one.

    private void OnMoveTile(MovedTileEvent e)
    {
        int count = _tiles.Count;
        foreach (var lod in _lodData)
        {
            int lodRadius = lod.Radius;
            var xPosTiles = _tiles.Where(t => t.X >= e.X + lodRadius);
            var yPosTiles = _tiles.Where(t => t.Y >= e.Y + lodRadius);
            var xNegTiles = _tiles.Where(t => e.X <= e.X - lodRadius);
            var yNegTiles = _tiles.Where(t => e.Y <= e.Y - lodRadius);
            var tiles = new List<CityTile>();
            tiles.AddRange(xPosTiles);
            tiles.AddRange(yPosTiles);
            tiles.AddRange(xNegTiles);
            tiles.AddRange(yNegTiles);
            tiles.ForEach(x => x.LodGroup.ForceLOD(lod.LodIndex));
        }
    }

This was the result

This is a very basic first-pass implementation. In the next investigation, I will be looking at combining a pooling manager and having marked up tiles where gameobjects are placed and “LODed” based on what tile the player is on.