I wanted the level progression to have a rhythm of low and high suspense stages, instead of constantly increasing the difficulty. I implemented that by moving the board settings (walls, food, enemies) to config files. For anyone interested, here is the process in tutorial form.
First, add a ScriptableObject class for the config files. It stores all board config data as well as transition data for deciding which config to use in the next level. That way we can let the random number generator decide whether to use the current config again or to move to a successor config.
BoardConfig.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Completed
{
public class BoardConfig : ScriptableObject
{
public BoardManager.Count wallCount; // Range for our random number of walls per level.
public BoardManager.Count foodCount; // Range for our random number of food items per level.
public BoardManager.Count enemyCount; // Range for our random number of enemy units per level.
public List<BoardConfigTransition> transitions; // all possible successor configs
// returns a BoardConfig randomly selected from the transitions list
public BoardConfig GetNext()
{
float totalWeights = transitions.Sum( tr => tr.weight );
float rnd = Random.Range(0, totalWeights);
float sumSoFar = 0;
foreach(var transition in transitions)
{
sumSoFar += transition.weight;
if (sumSoFar >= rnd)
return transition.target;
}
return transitions.Last().target;
}
}
[System.Serializable]
public class BoardConfigTransition
{
public BoardConfig target;
public float weight;
public BoardConfigTransition(BoardConfig target, float weight)
{
this.target = target;
this.weight = weight;
}
}
}
Now you can remove the wallCount and foodCount variables from BoardManager, and make the SetupScene method take its object counts from a config.
BoardManager.SetupScene
//SetupScene initializes our level and calls the previous functions to lay out the game board
public void SetupScene (int level, BoardConfig boardConfig)
{
//Creates the outer walls and floor.
BoardSetup ();
//Reset our list of gridpositions.
InitialiseList ();
//Instantiate a random number of wall tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom(wallTiles, boardConfig.wallCount.minimum, boardConfig.wallCount.maximum);
//Instantiate a random number of food tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom(foodTiles, boardConfig.foodCount.minimum, boardConfig.foodCount.maximum);
//Instantiate a random number of enemies based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom(enemyTiles, boardConfig.enemyCount.minimum, boardConfig.enemyCount.maximum);
//Instantiate the exit tile in the upper right hand corner of our game board
Instantiate (exit, new Vector3 (columns - 1, rows - 1, 0f), Quaternion.identity);
}
GameManager is responsible for storing the config. Add a variable:
public BoardConfig boardConfig; //Configuration settings for the game board
Add it as an argument to the boardScript.SetupScene call, and update it when a new level is about to be created:
//This is called each time a scene is loaded.
void OnLevelWasLoaded(int index)
{
//Add one to our level number.
level++;
//Select the next board config
boardConfig = boardConfig.GetNext();
//Call InitGame to initialize our level.
InitGame();
}
You’re almost done with code, just one more tool for creating our config files:
Editor/CreateBoardConfig.cs
using UnityEngine;
using UnityEditor;
public class CreateBoardConfig
{
[MenuItem("Assets/Create/BoardConfig")]
static void DoCreateBoardConfig()
{
var config = ScriptableObject.CreateInstance<Completed.BoardConfig>();
AssetDatabase.CreateAsset(config, "Assets/EntryBoard.asset");
AssetDatabase.SaveAssets();
}
}
Now you need to set up the data files.
First, use the menu Assets > Create > BoardConfig to generate the config file Assets/EntryBoard. Make three copies of it (CRTL+D) and name them EasyBoard, RegularBoard and ChallengeBoard. The EntryBoard config is for the first level, the other ones will cycle throughout the remaining levels.
Use the inspector to populate them, I picked these values:
EntryBoard:
wallCount 10-12
foodCount 1-1
enemyCount 0-0
transitions:
EasyBoard 1
EasyBoard:
wallCount 5-10
foodCount 2-3
enemyCount 1-1
transitions:
EasyBoard 1
RegularBoard 1
RegularBoard:
wallCount 5-10
foodCount 1-2
enemyCount 2-2
transitions:
RegularBoard 1
ChallengingBoard 1
ChallengingBoard:
wallCount 4-8
foodCount 1-1
enemyCount 3-5
transition:
EasyBoard 1
Lastly, find the GameManager prefab and assign the EntryBoard asset to its boardConfig variable. Now you’re ready for testing. If you’re not sure what config is active at any point, inspect the GameManager instance in the scene.
You can see an alternative way to edit the config transitions graph here:
In the video I’m using the relations inspector with the following script:
Editor/BoardConfigBackend.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using RelationsInspector;
using RelationsInspector.Backend;
using System.Linq;
namespace Completed
{
public class BoardConfigBackend : ScriptableObjectBackend<BoardConfig,float>
{
public override IEnumerable<Tuple<BoardConfig, float>> GetRelations(BoardConfig config)
{
if (config.transitions == null)
yield break;
foreach (var transition in config.transitions)
yield return new Tuple<BoardConfig, float>(transition.target, transition.weight);
}
public override void CreateRelation(BoardConfig source, BoardConfig target, float tag)
{
if (source.transitions == null)
source.transitions = new List<BoardConfigTransition>();
source.transitions.Add( new BoardConfigTransition(target, tag) );
api.AddRelation(source, target, tag);
}
public override void DeleteRelation(BoardConfig source, BoardConfig target, float tag)
{
var targetEntries = source.transitions.Where( t => target == t.target);
if (!targetEntries.Any())
{
Debug.LogError("RemoveRelation: source is not related to target");
return;
}
source.transitions.Remove(targetEntries.First());
api.RemoveRelation(source, target, tag);
}
}
}