Open World Prototype: City Streaming Technique and Source Code

I created an 8×8 city, meaning a total of 64 blocks. I created the buildings in Unity using cubes and textures. The main feature for this requirement was the streaming of the city, which meant that when the player was at a certain block in the city only the blocks that were necessary to be present for gameplay should be spawn, and the rest should be deleted. There are three scripts that take care of this process: TriggerVolume, BlockStreamScript and StreamManager

Trigger Volumes

I began by creating a script that creates a trigger volume for each block. I used all colliders in the block in order to obtain the bounds of the collider. I made sure to include a rate variable so that I could play around with the size of the collider that is created because sometimes it ended up being 2x bigger than the city block.

[code language=”csharp”]
using UnityEngine;
using System.Collections;
/// <summary>
/// Trigger Volume – Class used to generate a TriggerVolume for an assigned obj
/// </summary>
public class TriggerVolume : MonoBehaviour
{
public GameObject obj; // obj that will contain TriggerVolume
private BoxCollider coll; // Instance of Box Collider
public Vector3 rate; // Rate/Size of collider
/// =========================
/// START
/// <summary>
/// Initialize TriggerVolume
/// </summary>
/// =========================
void Start ()
{
GenerateTriggerVolume();
}
/// =========================
/// GENERATE TRIGGER VOLUME
/// <summary>
/// Generates the Trigger Volume for the obj
/// </summary>
/// =========================
private void GenerateTriggerVolume()
{
// check if there’s an object
if (obj)
{
// combined Bounds instance
Bounds combinedBounds = new Bounds();
// Obtain renderers from children
Collider[] renderers = obj.GetComponentsInChildren<Collider>();
foreach (Collider r in renderers)
combinedBounds.Encapsulate(r.bounds);
// Add a box collider to triger
coll = this.gameObject.AddComponent<BoxCollider>();
coll.isTrigger = true;
// Assign bounds
coll.center = new Vector3(0, 0, 0);
// Check if the size of bounds is not zero
if (combinedBounds.size != Vector3.zero)
// Assign size with respective rate for each dimension
coll.size = new Vector3(combinedBounds.size.x * rate.x, combinedBounds.size.y * rate.y, combinedBounds.size.z * rate.z);
else
// Bounds is zero then, assign the rate as size of the collider
coll.size = rate;
}//check obj
}
}

[/code]

Setting Each City Block

My next step was to create a script to be attached to a city block. This counts with an instance to the stream manager and contains the position of the block in the city. The reason I decided to create this script was because I thought it would be easier if I passed the position of the block to the streamManager and then generate the tiles beside it.

[code language=”csharp”]
using UnityEngine;
using System.Collections;
/// <summary>
/// BlockStreamScript – Script used to communicate with streamMgr from block and load neighbor tiles
/// </summary>
public class BlockStreamScript : MonoBehaviour
{
public StreamMgr streamMgr; // Instance of stream manager
public int x; // Position X of tile
public int y; // Position Y of tile
/// ======================
/// START
/// <summary>
/// Starts instance
/// </summary>
/// ======================
void Start()
{
Init();
}
/// ======================
/// INIT
/// <summary>
/// Initialize this instance
/// </summary>
/// ======================
private void Init()
{
streamMgr = GameObject.Find("Game Manager").GetComponent<StreamMgr>();
// check if no streamMgr was found
if (!streamMgr)
Debug.LogError("No Stream Manager was found!!!");
}
/// ======================
/// ON TRIGGER ENTER
/// <summary>
/// Executes on triggerEnter
/// </summary>
/// <param name="other">Other collider</param>
/// ======================
void OnTriggerEnter(Collider other)
{
// check if other is player
if (other.tag == "Player")
// Load neighbor tiles
LoadNeighbors();
}
/// ======================
/// LOAD NEIGHBORS
/// <summary>
/// Calls LoadNeighborTiles from StreamManager
/// </summary>
/// ======================
private void LoadNeighbors()
{
// Check if there’s a stramMgr
if(streamMgr)
{
// load neighbors of this tile
streamMgr.LoadNeighborTiles(x, y);
}
}
}
[/code]

Loading/Unloading City Blocks through Coroutines

The stream Manager takes the position of the block that the player is currently at and loads the necessary blocks. Blocks created are added to an “Instantiated” list, which I iterate through later to unload the rest of the blocks.

When testing the streaming of the city, there were moments where the game would lag a bit because of the loading and unloading of blocks! In order to solve this, first I tried to use threads and basically take care of the loading through it. However, Application.LoadLevelAdditiveAsync() can only be called from the main thread so I had to look for a second option and I found that Coroutines could do the job. Using coroutines solved the problem, making the game run smoothly!

[code language=”csharp”]
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// StreamMgr – Class used to load and unloaded portions of the world
/// </summary>
public class StreamMgr : MonoBehaviour
{
public List<string> Instantiated = new List<string>();
public Vector2 MapDimension; // Dimension of Map
///———————————————
/// IS CREATED
/// <summary>
/// Returns true if instance has been created. Otherwise, it returns false.
/// </summary>
/// <param name="s">Name of instance to check for</param>
/// <returns> True if instance has been created, false otherwise</returns>
///———————————————
public bool isCreated(string s)
{
foreach(string inst in Instantiated)
{
if (inst == s)
return true;
}
return false;
}
///———————————————
/// ADD INSTANCE
/// <summary>
/// Adds instance to list
/// </summary>
/// <param name="s">Name of instance to add to list</param>
///———————————————
public void addInstance(string s)
{
Instantiated.Add(s);
}
///———————————————
/// REMOVE INSTANCE
/// <summary>
/// Removes instance from list
/// </summary>
/// <param name="s">Name of instance to remove from list</param>
///———————————————
public void removeInstance(string s)
{
// remove from list
Instantiated.Remove(s);
}
///———————————————
/// LOAD NEIGHTBOR TILES
/// <summary>
/// Starts Coroutine that calls upon LoadRegions
/// </summary>
/// <param name="_x">Position of tile in X</param>
/// <param name="_y">Position of tile in Y</param>
/// ———————————————
public void LoadNeighborTiles(int _x, int _y)
{
StartCoroutine(LoadRegions(_x, _y));
}
/// ———————————————
/// LOAD REGIONS
/// <summary>
/// IEnumerator Coroutine tat will be called to load regions around given X and Y positions
/// </summary>
/// <param name="_x">Position X of tile</param>
/// <param name="_y">Position Y of tile</param>
/// <returns>Null</returns>
/// ———————————————
IEnumerator LoadRegions(int _x, int _y)
{
List<string> tilesLoaded = new List<string>();
tilesLoaded.Clear();
// Add current tile
string block = "Block " + _x.ToString() + _y.ToString();
tilesLoaded.Add(block);
// ==============
// LOAD
// ==============
// Iterate X
for (int x = -1; x <= 1; x++)
{
// Iterate Y
for (int y = -1; y <= 1; y++)
{
// Check when X and Y are zero
if (x == 0 && y == 0 || x != 0 && y != 0)
continue;
//——————
// Obtain positions
//——————
int posX = _x + x;
int posY = _y + y;
// Check if positions are inbound
if (posX >= 0 && posX < MapDimension.x && posY >= 0 && posY < MapDimension.y)
{
block = "Block " + posX.ToString() + posY.ToString();
// Check if the tile has not been created
if (!isCreated(block))
{
// Load block
Application.LoadLevelAdditiveAsync(block);
Instantiated.Add(block);
}
tilesLoaded.Add(block);
}//inbound
}// uterate Y
}// ITerate X
//=============
// UNLOAD
//==============
// Iterate through Instantiated
for (int i = 0; i < Instantiated.Count; i ++)
{
bool delete = true;
// Iterate through tilesLoaded
foreach (string loaded in tilesLoaded)
{
// check if tile is Loaded
if (Instantiated[i] == loaded)
delete = false;
}
// check if tile should be deleted
if (delete)
{
// Obtain gameObject
GameObject levelData = GameObject.Find(Instantiated[i]);
// Delete gameObject
GameObject.DestroyObject(levelData);
// Remove from instantiated
Instantiated.Remove(Instantiated[i]);
}
}//iterate instantiated
yield return null;
}
}
[/code]

I will upload an Executable of the game once I make sure that blocks do not just pop up in camera view! 🙂

2 thoughts on “Open World Prototype: City Streaming Technique and Source Code

Leave a Reply

Your email address will not be published. Required fields are marked *