- Home /
Sequencing game events
I'm really struggling with coming up with the most elegant way to sequence a very simple chain of events in my GameManager.
The problem is similar but what is discussed here but involves the overall sequence of game events.
I'd like to do some cleanup (disable all enemies and player), then display a "Level Starting" with a level number for 3 seconds, then spawn everything. My GameManager script is on the same object as a UIManager script and I'd like to keep the UI separate from the game logic. I can easily achieve what I want by using events or C# Action, the GameManager invoking an event and the UIManager responding to it. But it seems too convoluted for what I need (the UIManager is the only script interested in displaying a "Level Starting" message). So I'm trying this by coupling the UIManager to the GameManager script, like this:
void Awake()
{
// Check for unconnected prefabs
Assert.IsNotNull(PrefabAsteroid);
Assert.IsNotNull(PrefabUFO);
Assert.IsNotNull(player);
// Reset the count variable
AsteroidController.countAsteroids = 0;
// Connect to the UIManager
UIManager = GetComponent<UIManager>();
// Set the level number
level = 0;
}
Then I start the game sequence as a coroutine, like this:
void Start()
{
// Instantiate the player
// This GameObject is persistent and doesn't get
// destroyed until the end of the game.
player = Instantiate(player, Vector2.zero, Quaternion.identity);
player.Lives = 3;
StartCoroutine(GameSequence());
}
Now I can tell the UI to display the message I need and do the next steps after some time:
IEnumerator GameSequence()
{
// Disable player & enemies
// Some function here
// Display the level number
UIManager.AnnounceLevel(level);
yield return new WaitForSeconds(3);
// Spawn asteroid
// Start the UFO spawner
// Spawn player
// etc...
// Restart when level is clear
}
However this way of sequencing stuff presents many problems, at least the way I see it:
The UIManager is responsible for displaying the "Starting Level" message but it also handles its duration on the screen (3 seconds, set in UIManager). If I ever need to change it I have to do it in two places. If I pass the required duration to the UIManager and use the same variable in the yield statement I'm essentially making the GameManager decide how long the UI displays the message. What I if I want later to have the UI do a flashy 12-second animation when the level starts?
Again, this can be solved with events but then I'd need the UIManager to invoke an event saying "I've finished", which is going to make the GameManager coroutine too complex in handling that event (at least for me).
It seems weird to have the entire game sequence inside a coroutine.
How would you achieve something as simple as that without over-engineering everything? Seems like a trivial thing to do, and I more or less managed to make it work, but it seems over-engineered to me.
Thanks a lot for your help.
looks like you are halfway there...
// Connect to the UI$$anonymous$$anager
UI$$anonymous$$anager = GetComponent<UI$$anonymous$$anager>();
you just need something similar in the UI$$anonymous$$anager class; a reference to the Game$$anonymous$$anager.
I would recommend you setup the Gme$$anonymous$$anager as a "State $$anonymous$$achine" and allow time and/or other objects (like the UI$$anonymous$$anager) to set (or just check) the current state. I'd also do my processing in Update(), rather than a coroutine
Answer by Harinezumi · Jan 31, 2018 at 11:22 AM
This is the classical problem of flexible design vs. hard-coded solutions.
It is good that you are looking for more flexible solutions and making it cleaner, but also consider how likely it is you will need to change it? If not very likely, then a hard-coded solution is good enough...
That said, I love general solutions, and this can be solved various ways. One is using a state machine where each state is represented by a class, and each state does something different (e.g. CleanUpEnemies State script, CountDownState script, etc.). If you make these script MonoBehaviours and put them on separate GameObjects, you can use OnEnable() and OnDisable() methods to activate and deactivate a state.
However, I think in your case the primary problem is that you don't separate the display logic from the control logic: your UIManager should only display things, and not know how long does it display a thing. For example, your GameManager can slowly decrease remainingTime
in a Coroutine then call UIManager.SetCountDownText(((int)remainingTime).ToString());
. This way you take out the logic from the UIManager.
Does this make sense? Feel free to comment if you have more questions!
It does makes sense, especially when you mention a classical problem, which I seem to have run into. Let me explain. I am learning C# and Unity simultaneously, with only some basic background on program$$anonymous$$g from Python and PHP. I can make the game work. I have made the game work. Spawning, scoring, shooting, everything was looking perfect. I can easily implement what @Glurth wrote in the comments, with timers and booleans etc. But the more I learned, the more I realised how my scripts were the worst examples of spaghetti code.
I enjoy writing code not mainly in order to create games, but because I enjoy abstractions which have flexible implementations. I am very much attached to the "Code is poetry" maxim of the Wordpress folks. So I'd rather spend a lot of time on making code elegant rather than finishing the game and tell myself that I know program$$anonymous$$g. I'm not too concerned about reusing my components. Some of them I might, with $$anonymous$$or adjustments, because they handle a simple task well. But it's not my major problem (I'm learning by myself and not working in a multi-programmer environment). I just like doing things elegantly. In my professional life I'm a video editor and I know how two different timelines can achieve the same result when you play them, with one looking like a garbage bag and the other one being instantly clear and functional.
So in the last days I've digested a lot of material on interfaces, delegates, events, dependencies and class decoupling. A whole lot. And I've rewritten my game completely, splitting classes into functional blocks and communicating between them with Action delegates. Obviously, I've read opinions that say decoupling shouldn't be a religion and that closely-related classes can and even should be inter-dependent. That's why I came back to the idea of coupling both managers with GetComponent, something which I have avoided almost completely in my manager classes. $$anonymous$$y working project was running fine like that, and I wasn't even using Update() in any class, only FixedUpdate() for some forces on rigidbodies and OnCollision methods. It gave me a good understanding of how flexible Unity was.
I understand what you wrote about separate GameObjects working together as a state machine, but this seems too weird to me. I still find it strange that I can't sequence a series of methods with some pause in between, without making them coroutines and adding a yield statement right at the top so that the method doesn't execute immediately.
$$anonymous$$aybe I'm thinking too much about the problem and should lean more towards a "whatever works" approach. I know a lot of people do. But I keep saying to myself that if I can't come up with a simple architecture for an Asteroids clone, I would find more complex projects to be impossible for me to make. And I don't like this idea :)
@Cirrocumulus , what you write makes perfect sense, I also like to solve things nicely, and I wish there were more people like this.
So I understand that using GameObjects as states sounds weird, although it does solve the idea of separation of states. Let's put that idea aside for a while and see different approaches.
I created an extension method for $$anonymous$$onoBehaviour called DelayedCall()
which takes as a parameter an Action to call back and a float delay. So you would call from any $$anonymous$$onoBehaviour DelayedCall(DoSomething, 3f);
. What DelayedCall() does internally is to start a *static coroutine* on the caller $$anonymous$$onoBehaviour which then just does
yield return new WaitForSeconds(delay); if (action != null) { action.Invoke(); }`. This way you could have what you want, chaining together states with delays in between (but be careful, the calls do not wait for each other!).
Otherwise the best option I see is to have a central coroutine that calls different parts something like this:
private IEnumerator LevelSetup () {
DoFirstStep();
yield return new WaitForSeconds(1);
DoSecondStep();
yield return new WaitForSeconds(1);
...
}
Yes, this last option is what I think I'm finally left with :) Thanks a lot for your feedback and help! $$anonymous$$uch appreciated.
Timers/delays: Rather than use yield, I prefer to check the currentTime against my Timer's recorded StartTime, plus the timer's duration.
I also prefer to create a Timer Class such that my monobehaviors contain one (or many) as a member(s). Then I "check" it (or start it), during the monbehavior's Update(),
This is useful because the timer-check function can return a "fraction complete" float value, rather than just a boolean (like yield/or not) for "timer complete". I've found the fraction complete value, very useful for animations.
You could also write some variants of the timer class timers to use either "game-time" or "real-time".
Interesting approach, I will think about using it. Do you wrap the Timer into a non-monobehaviour class?
Also, don't you worry about Update() running unnecessarily all the time? Wouldn't running the timer in a coroutine be more efficient (in case it is not a logic that runs all the time)?