Hello game developers!
I have a semi-question semi-discussion about Event Systems and Singletons in Unity. I’m currently developing a 2D tile-based colony simulation game. Players will manage a colony of civilians, build their base in the tile grid, manage a farm, etc. It will be similar in genre to games like RimWorld and Dwarf Fortress.
I can’t shake the feeling that I am doing something wrong when it comes to my development style, and that I’m making things difficult for myself. I’ll post the way I’m doing things, and if any of what I am doing is wrong or has a better solution, I’d love to hear it. Any resources would be super helpful!
I currently have five managers that manage different aspects of the game: Saving, the map and tiles, jobs (‘tasks’), backing out of menus, and managing the repository of tiles. Each has a customized script to manage its function.
In the game, the player will be able to place an order to build an object, like a wall or a floor. This means this information needs to be given by the player, then it is processed into a visual on the tilemap, and as information to the AI civilians to build it. A screenshot is attached of the BuildOrderManager script.
It holds an order map, so that the game can visually display the order on the tilemap. It has a real map, so that when the order is finished being constructed, the real physical tilemap can be updated. It has a togglegroup, so that clicks can only occur when the toggle is enabled (i.e., the button on the UI is pressed and build mode is enabled). It has a tabgroup, a list of buttons to change what tile order to build. It has a JobManager, which feeds the information to the jobs so that the AI knows where and what to build and adds it to a queue to build. Finally, there is a list of tile information on how different tiles should behave (WoodWall, DirtFloor, RustedWall…).
This feels like a singleton workflow and I fear it may lead to hardship later down the line. However, I’m unsure of how to implement an Event System. Would placing each tile be a unique event, and then for each event made it adds it to the job queue which is a listener of the Event System? I have another script that handles backing out of menus, such that pressing Escape will back the user out or deselect something the same as pressing the back button. Would pressing Escape be another event, with a manager listening and handling the queue of what to back out of (such that only the latest entry is backed out of, not all menus)?
I feel like my current direction is not good and is beginning to be difficult to work with. If I wanted a total overhaul, how might an example implementation work? I’ve seen videos on Event Systems (namely GameDevGuide’s video), but I couldn’t really figure out how to use it and what should be considered an event. Is everything an event? And, wouldn’t there still be managers anyway?
I’ve attached additional screenshots of some other managers I have in the game. Is this good workflow? Will this cause headaches later? How are Event Systems implemented?
Thanks you guys!!
Here’s the JobManager script, which manages a couple example jobs (DoConstruct, and DoWander). It needs a survivor, the order manager, the tilemap, etc. Here’s the script:
public class JobManager : MonoBehaviour
{
public List<BuildOrderTile> BuildOrderTiles;
public Transform jobTarget;
public SurvivorAI survivor;
public BuildOrderManager orderManager;
public Tilemap map;
public bool jobsEmpty = false;
public bool isWorking = false;
public bool isWandering = false;
private void Start()
{
BuildOrderTiles = new List<BuildOrderTile>();
}
public IEnumerator DoConstruct()
{
isWorking = true;
foreach(BuildOrderTile tile in BuildOrderTiles.ToList())
{
//Get the transform of the tile
jobTarget.transform.position = map.GetCellCenterWorld(tile.thisTilePosition);
survivor.target = jobTarget;
yield return new WaitForSeconds(0.5f);
//While the distance between the survivor and this tile is too high, wait for the survivor to get there. This means DoConstruct will need to be a coroutine.
while(!survivor.reachedEndOfPath)
{
yield return new WaitForSeconds(0.25f);
}
//Clear the survivor's target; reached destination
//survivor.target = null;
//When the distance is close enough and construction is not finished, begin construction of the tile. While the survivor is nearby, add, idk 1% progress every tenth of a second
while(survivor.reachedEndOfPath && tile.constructionProgress != 100f)
{
tile.constructionProgress += 1f;
yield return new WaitForSeconds(0.05f);
}
//When construction is 100%, send a command to the tile to tell it it's finished. The tile should then turn itself into the tile it is meant to be, which it is associated with
//Call the build order manager that this tile needs to turn into the associated tile
orderManager.ConvertTile(tile);
//Remove this tile from the queue
BuildOrderTiles.Remove(tile);
}
jobsEmpty = true;
isWorking = false;
}
public IEnumerator DoWander()
{
isWandering = true;
GameObject WanderTarget = new(transform.name = "Wander Target");
WanderTarget.transform.SetParent(this.transform);
Vector3 randomDeviation = new(Random.Range(-5f, 5f), Random.Range(-5f, 5f), 0);
WanderTarget.transform.position = survivor.transform.position + randomDeviation;
survivor.target = WanderTarget.transform;
while(!survivor.reachedEndOfPath)
{
yield return new WaitForSeconds(0.15f);
}
//Example
yield return new WaitForSeconds(2f);
isWandering = false;
Destroy(WanderTarget);
}
}