Designing Core Gameplay Loop
Designing Core Gameplay Loop

Designing Core Gameplay Loop

Tags
DesignEngineeringC++Dev LogCode
Owner
Justin Nearing

This chapter started as a rubber duck for how I wanted to build the core gameplay loop for my game LETSGOLETSGO.

The intent of the game is to generate some kind of musical composition during at runtime.

The player is presented with different choices, which provide the direction of the song:

Instead of random platforms, I want to present the player with 3 platforms, and the one they step on sets the key the rest of the song will be in.

Then another set of audio platforms appear which will set whether the song is in major/minor.

Then show platforms that will select which instrument to use.

And follow this pattern until something resembling music is playing.

In this chapter, I will explain the design I came up with, as well how I got here.

So, LETSGO!

What I ended up with

After a couple days of thought, here’s the software design I have come up with:

image

The intent is to create a musical composition throughout the course of the games runtime.

This is achieved in Phases.

Phases contain a set of Actions that describe how to resolve the Phase.

Phases implement a PhaseController interface that describes the common commands necessary for controlling the lifetime of the phase.

A Phase Manager owns a set of PhaseController, responsible for issuing the commands for each Phase’s lifetime.

When a Phase is active, it invokes commands to an ActionExecutor, which more-or-less maintains a queue of Actions to cycle through.

So, when a SetTonic Phase is activated, it will send a Create Audio Platform Action to the ActionExecutor, and start listening for a Player Stepped on an Audio Platform event to be triggered by any AudioPlatform.

When it receives that event, it will consume the Note that event fires with, and use it to update the “Tonic” in our State object.

This means that our State, Phase management, and gameplay execution is logically separated from each other.

This is the design I’ll start building out in code in the next chapter.

However, I want to describe how I got to this design.

It didn’t happen overnight- it took a solid week of refining an initial idea into what I presented above.

I want to share where I started from, and how I got to this final design.

Step One: Map out a rough idea

When painting a picture, you rarely want to start with details- jumping straight into contour, line work, etc.

Often, a better approach is to use a wide brush to start blocking in general shapes using broad strokes.

With that in mind, this is the first broad strokes plan for the core gameplay loop:

image

I am imagining a Composer that is responsible for managing the musical composition at runtime.

I’m thinking of a separate Conductor that acts as the intermediary between player actions and composer.

You’ll notice that in the final design, I don’t have a Composer or Conductor class- but it’s this orchestral conceptualization that frames how I’m thinking about the design.

The Conductor doesn’t make up the music, it merely tells the instrumentalists what to be doing, and when.

Similarly, the Composer doesn’t actually play the music its creating, its only defining which notes each instrument should play.

In context of my game, these two entities interact with each other through Phases.

Phases describes the actions the Conductor needs to take, and the data the Composer needs to move to the next Phase.

The Phase “SetTonic” gives the Conductor the action “create audio platform”, and when the platform is stepped on, tells the Composer we’re in the key “D flat”.

What I want to achieve from this design is that the logic concerned with creating the musical composition is separate from the actions the player takes.

A more naïve approach would have the state, the musical composition, and the gameplay actions all tightly correlated.

I am working very hard in this design to have these as separate logical domains that are decoupled as possible.

Event Driven Gameplay

So I have this “Composer” domain of logic, which I want separate from the “Conductor” domain.

But they still have to interact with each other.

One pattern I like to use for separating logical domains is through event invocation.

I’ve used Event Systems in Unity with C#, and I feel its appropriate for this domain separation.

Unreal supports an event system through the use of “Delegates”:

Basically we define that some class will fire an event, and set up other classes to “listen” for that event to fire.

The nice thing about how this works is the thing listening for the event doesn’t have to know anything about the thing triggering the event.

⁉️
This lack of knowledge can also be a downside.

The listening object doesn’t have access to the state of the object sending the event.

This can get you into trouble if you’re needing some data and haven’t designed your system correctly.

This idea of having an event system was enough to refine the initial plan into this:

image

This is OK.

I have the domain separation which I’ve deemed very important, and have some kind of idea of those domains throwing events around at each other.

The problem here though this concept of a Phase Resolution Object which contains a property Note.

The method signature for the UpdateComposer would be something like:

void UpdateComposer(PhaseResolution input) {};

This assumes a class called PhaseResolution exists, which may or may not contain the property Note, and I’m going to have a hell of a time figuring out what to do with the many kinds of PhaseResolutions available, the properties they may contain, and the logic that needs to be triggered on resolution.

Not great.

Describe Your Data First

At this point, I decided to take a look at this concept of a Phase more carefully.

It’s kind of the glue connecting our logical domains together.

So here I map out an example of what a PhaseManager type entity would be concerned with:

Name
State
Eligible
Repeatable?
Eligible Phase
Set Tonic
Complete
False
False
Intro
Set Third
Currently Active
True
False
Intro
Set Mode
Pending
False
False
Intro
Bass Drop
Pending
False
True
Bridge
BPM Switch
Pending
True
True
Bridge, Chorus, Outro

What this tells me is that there needs to be an object representing Set Tonic

During the game, the PhaseManager will activate the Set Tonic phase when eligible.

The intent of this Phase is to present the player with a choice of Notes:

image

When the player steps on the F# platform, it needs to tell the Composer that the Tonic for this composition is F#.

However, something like a BPM Switch phase does not update the state in this way. It has no knowledge of “notes”. It changes a completely different variable owned by a completely different entity.

So let’s pseudo some code:

class SetTonic { 
	ComposerState State; 
	
	void Activate() {
		StartListeningForEvent(PlayerSetNote(Note note));
	};
	
	void OnPlayerSetNote(Note note) {
		State.SetTonic(note);
	};
	
	void Deactivate() {
		StopListeningForEvent(PlayerSetNote(Note note));
	};
};

The idea here is when the F# platform is stepped on, it fires a PlayerSetNote(F#) event.

And our SetTonic phase is listening for this event to be fired.

But only if the PhaseManager tells it to do so- on activate, it starts listening for that event, and will only set the States tonic property when active.

The idea being the BPM Switch phase has its own class defining what it needs to do when the phase is active.

Using interfaces to manage common commands

Here’s the thing, I don’t want our PhaseManager to contain a SetTonic object or a BPMSwitch object.

It doesn’t care what each Phase does, it only cares about managing the Phases lifetime, which is only a small part of what each Phase is.

For these kinds of cases, I like the idea of using interfaces to explicitly define how the PhaseManager expects to manage each Phase.

An interface is basically an abstract class that defines a few empty methods.

class PhaseController {
	void Initialize() {};
	void Activate()   {};
	void Deactivate() {};
};

Our SetTonic class then implements the interface:

class SetTonic : PhaseController { 
	ComposerState State; 
	
	void Initialize() {
		GetComposerState();
	}
	
	void Activate() {
		StartListeningForEvent(PlayerSetNote(Note note));
	};
	
	void Deactivate() {
		StopListeningForEvent(PlayerSetNote(Note note));
	};

};

Because we define SetTonic as a PhaseController, it must have the methods we defined in the PhaseController class.

The useful part of this pattern is when we define our PhaseManager:

class PhaseManager { 
	TArray<PhaseController> Phases;
	
	// As a very rough implementation
	void ActivateNextPhase(){
		Phases[0].Deactivate();
		Phases[1].Activate(); 
	};
};

Here our PhaseManager only knows SetTonic in terms of the methods defined in PhaseController.

It doesn’t know about the method “GetComposerState”, it only knows the activation/initialization methods.

Executing Phases through Actions

So, we have a way of managing the lifetime of a Phase.

Now we need to connect it to the “Conductor”.

The idea I had for the Conductor is that it only cares about what gameplay actions are necessary.

It should not have any knowledge about the composers state. It don’t care if the Tonic has been set or not. It has no knowledge of a Tonic.

Even in the case where the SetTonic phase is currently active, I don’t want it to know anything about the “SetTonic” object itself.

It is only concerned with executing a set of Actions.

An Action itself is a functional gameplay operation.

It’s “Spawn 3 AudioPlatforms in front of the player”

“Reduce volume on synth to 0”

We’re literally describing what to do- not how to do it, or why we’re doing it.

The best place to define what actions a SetTonic phase needs to do is in our SetTonic object:

class SetTonic : PhaseController { 
	ComposerState State; 
	
	// The list of gameplay actions to send to Conductor
	array<Action> Actions = {
		SpawnAudioPlatform, 
		TriggerHarpGliss,
	}
	
	void Initialize() {
		GetComposerState();
	}
	
	void Activate() {
		StartListeningForEvent(PlayerSetNote(Note note));
		
		// When the phase is activated, send actions to Conductor to be executed
		SendEvent_AddToConductorQueue(Actions);
	};
	
	void Deactivate() {
		StopListeningForEvent(PlayerSetNote(Note note));
	};

};

Here we’ve updated our SetTonic phase with a set of Actions.

The idea is to trigger the audio platforms, and when a platform is stepped we trigger a harp gliss (a ascending group of notes performed on a harp)

I think this works- the harp gliss Action waits for the same PlayerSetNote() event that the SetTonic phase object does.

When it has receives this event, it can use that F# to determine which audio cue should be fired. (I’m imagining a bunch of audio clips containing the harp gliss sound, one for each note. When it has the PlayerSetNote “F#”, it fires the FSharp_HarpGliss.wav sound)

I may have to put some thought into how exactly the Conductor is managing the Actions to be run.

There may be multiple Actions happening at the same time, so naively processing an array might not work.

But we can reuse the patterns defined in our Phase management for Action management:

interface Action {
	void Initialize();
	void Activate();
};

class TriggerHarpGliss : Action {
	Scale Scale;
	Note Tonic;
	KVPair AudioCues;
	
	ComposerState State;
	
	void Initialize() {
		Scale = State.Scale;
		Tonic = State.Tonic;
	};
	
	void Activate() {
		// KVPair value of key {Scale, Tonic} to trigger audio queue
		// This would need UnrealQuartz access 
		TriggerAudioCue(Scale, Tonic);
		
		SendEvent_RemoveFromConductorQueue(this);
	};
};

class Conductor {
	Queue<Action> Actions; 
	
	void RecieveEvent_AddToConductorQueue(TArray<Action> actionList) {};
	void ReceiveEVent_RemoveFromConductorQueue(Action) {};
	
	void ProcessQueue(){
		Actions[0].Deactivate();
		Actions[1].Initialize();
		Actions[1].Activate();
	};
};

How it’s processing that queue is still open for interpretation, but this gives us a reasonable looking initial design for our final product.

Now that’s all pseudo-code, but I think this is a reasonable implementation for the core loop of my game.

In theory it should allow me to write a wide range of Actions and Phases, allowing specific implementations of each that vary widely from each other.

I do have to be careful with cases where I’m firing/consuming events.

Order of operations depending on when an event is fired can become somewhat complicated.

But we’ll cross that bridge when we get there.

In the next chapter, I’ll start actually writing some damn code, implementing the above design into C++.

I’m sure that things will fall apart, as I’m sure there are assumptions I’ve made in this design that will not reflect reality.

But, I will have a plan going in, and that is often half the battle.

🔨
In the next chapter, I start actually trying to implement this design in code. I expect to get rekt by the experience:

Building the Gameplay Loop

Rubber Duck

🦆
Everything in this section is a relatively unedited scratchpad of me smashing my head against the keyboard trying to get to the final design described above. You’re more than welcome to read on, but it’s a touch scatterbrained and only here for posterity. I’ve distilled the things I talk about below into the article above.

A composition is built in Phases.

  1. Set tonic/3rd/modes/etc. - have player define the Music Theory bit
  2. Instrument selection - which synth/lead/percussion does the player select?
  3. Song structure - AABA - ABAB - etc.

The Composer holds state, as well as holds the definition for each Phase:

  • Here’s what the Conductor needs to do during the “Set Tonic” Phase
  • Here’s what the Conductor needs to select between two synths

The Composer takes the output of the Conductor, generates what is needed for the next Phase, then sends the next prepared Phase back to the Conductor .

I envision Phases as pure data.

A list of instructions for the Conductor, and the expected output the Conductor needs to return.

The Conductor feels purely functional- Just a bunch of actions it is capable of doing: Taking a list of arbitrary actions, doing those actions in order, emitting the result.

💡
This use of “actions” might be a good candidate for the Command pattern.

Here’s attempt number 2:

image

So a couple things going on here.

I’ve set that the Composer has a ordered list of Phases it wants to run through, and has an Current Phase.

I then have this object containing the details needed by the Composer to resolve the current Phase. Like a “Phase Initialization” object, but I’m not sure about that name.

Point is, it contains the Action expected to resolve the phase (I’m not going to think about supporting multiple Actions per Phase just yet).

It also has the Inputs required for that Action to be successfully invoked, and the expected output.

Now I’m not exactly sure I need the expected output.

The way I’ve structured the Composer is that it will take that Phase Initialization object, somehow invoke the Actions contained, and marshal the result of the Action into a Phase Resolution Object.

This infers an event system, where the Conductor is listening for anything that triggers an Update event.

The nice thing about this kind of Update event is that the Conductor doesn’t really need to know about what the expected output should be. It just sends the output it gets, and the Composer can handle the result- including handling a bad result.

I’ve used Event Systems in Unity with C#.

Not sure if there’s any out-of-the-box patterns with Unreal C++.

Looks like in Unreal there is “Delegates”. I’ll have to do some reading on this.

Hol’ Up, Am I Getting Too Weird With It?

One question I want to ask myself first though is if I’m over-engineering this?

In theory I could just hard code the phase list, making direct calls per phase, etc.

But I feel the above approach is a good separation of state, execution, and data:

  • The Composer is holding the order of operations and the generated runtime data.
  • The Conductor is executing a versatile set of supported operations.
  • Pure “phase data” objects are flowing through the two systems.

It does seem a bit complex, but really what I’ve designed here is the core gameplay loop for the entire game.

It feels like this structure would also allow for Phases containing multiple Actions. Like a bass drop:

  • Reduce volume on lead 2 bars
  • Add 2 bar percussion riser
  • Percussion switch on drop
  • Lead switch on drop
  • Big ol’ inception chunga bass at max volume

Which means there’s Actions that can contain other Actions.

icon
This kind of vibes with the research I’ve done into Hierarchical Task Networks, where you have a concept of an “primitive task,” which contains no sub-tasks, and compound tasks which do:

It’s been a dream to build an HTN implementation, even though there’s off-the-shelf plugins for Unreal. Expensive though.

You need a relatively versatile gameplay loop system manage that set of actions, and keep it in context of a somewhat legitimate sounding musical composition.

So I think I’ll continue with this design.

Obviously it is still a bit rough, and not quite refined enough to put into code, but this does seem to be a legit path forward.

Phase Lifetime

  1. Composer arranges Phases into an ordered list
    1. Phases have Dependencies on other Phase objects
    2. This is done at Composer Init time
    3. Open question if the Phases are set in stone, or dynamic as the composition progresses
      1. No reason why you couldn’t give the Player a choice to add a new Phase
  2. Composer sets currently active Phase(s)
  3. Phase Name
    Phase State
    Eligible to Activate?
    Repeatable?
    Set Tonic
    Complete
    False
    False
    Set Third
    Currently Active
    True
    False
    Set Mode
    Pending
    False
    False
    Bass Drop
    Pending
    False
    True
    BPM Switch
    Pending
    True
    True

This shows a problem with just having an ordered list of Phases.

Really, there is a “bag” of Pending phases eligible to be activated.

While technically you could change the beats per minute at any time, this would be a relatively rare thing to do.

I think the problem is that I’m confusing Phases with Actions.

Another way to think about Phases is the actual structure of a song:

Intro
Verse
Bridge
Chorus
Verse
Chorus
Outro

In the Intro, there are Actions that make sense- Set which musical scale we’re using, start the various instruments.

In those Phases you have a set of eligible Actions:

Action Name
Action State
Eligible
Repeatable?
Eligible Phase
Set Tonic
Complete
False
False
Intro
Set Third
Currently Active
True
False
Intro
Set Mode
Pending
False
False
Intro
Bass Drop
Pending
False
True
Bridge
BPM Switch
Pending
True
True
Bridge, Chorus, Outro

Let’s pseudo some code:

enum ActionState
	PENDING,
	ACTIVE,
	COMPLETE,
};

struct Action {
	FString Name;      // Name
	ActionState State; // Action State
	bool IsRepeatable; 
	bool IsEligible;
};

struct Phase {
	FString Name
	TArray<Action> Actions; // Eligible Phase 

	
};

// Problem Here
struct PhaseResolution {
	Phase Phase;
	
}

struct Composer {
	// Current Composer State
	Note Tonic; 
	Scale Scale; 
	
	// Managing Phases
	TArray<Phases> Phases;
	void InitializePhases();
	void UpdatePhases();
	
	// Interacting with Conductor
	void InvokeEvent_ComposerCommand(Conductor.RecieveCommand, Phase);
	void RecieveEvent_ComposerUpdate(PhaseResolution);
};

struct Conductor {
	
	//event handler
	void RecieveCommand();
}

Ok so above there’s that rough idea.

Here’s the issue I’m running into. To resolve the Phase “SetTonic” you need a Note to be entered into Composer State.

Basically, I don’t want to polymorph a “PhaseResolution object and write one being “SetTonic” and another “SetThird” and another “SetMode”. Yuck.

I’ll have to think on this more.

2 Days Later

Ok I think I go it.

The Phase Manager is concerned with the state of the Tonic note for the composition.

A Tonic is an object containing a Note, and the methods by which the state is updated.

// The Phase manager will interact with Phases through an interface
// Common methods that defines how to manage the Phase
interface StateController {
	void ActivateState();
	void DeactivateState();
};

// This means that we have a Stateful object known as Tonic
// When Active, it has a specific way of managing its state 
class Tonic : StateController {
	Note Note; 
	
	void ActivateState() {
		ListenForEvent(PlayerSetNote(Note note);
	};
	
	void DeactivateState(
		StopListeningForEvent(PlayerSetNote());
	);
};

// Need more thinking on this part:
class PhaseManager {
	TArray<StateController> Phases;
	
	void ActivateNextPhase(
		if (NextPhase == SetTonic) {
			Tonic::ActivateState();
		};
	);
};

Upon further reflection, I don’t got it.

The entire point of using an interface here is so that PhaseManager only knows about StateController.

Which means it doesn’t have access to the unique Tonic property Note

What I’m trying to do here is avoid a big switch statement for phase activation.

We could do something like separate the entity concerned with the order of phases and the entity storing Composer state.

class PhaseManager { 
	TArray<PhaseController> Phases;
	
	// As a very rough implementation
	void ActivateNextPhase(){
		Phases[0].Deactivate();
		Phases[1].Activate(); // pretend is "Tonic"
	};
};

class ComposerState {
	Note Tonic; 
	Scale Scale;
	
	void SetTonic(Note note) {};
	void SetScale(Scale scale) {};
}

class Tonic: PhaseController { 
	ComposerState State; 
	
	void Activate() {
		StartListeningForEvent(PlayerSetNote(Note note));
	};
	
	void OnPlayerSetNote(Note note) {
		State.SetTonic(note);
	};
	
	void Deactivate() {
		StopListeningForEvent(PlayerSetNote(Note note));
	};
};

I think that makes sense in terms of a complete loop.

Composer is a group of entities containing a State class and a Phase Manager class.

Each Phase expresses what to do during its lifetime, with the Manager owning the lifetime of all Phases.

So to circle back to the Conductor:

I want Conductor to be totally ignorant of State.

I would prefer Conductor to also be ignorant of Phases.

I want it to just sit there, listening for sets of Actions to resolve.

In other words, I do not want the Conductor to know about this thing called Tonic.

It just listens for something to give it a command.

class Tonic : PhaseController {
	ComposerState State;
	TArray<Action> ActionList = (
		SpawnAudioPlatformSet,
		TriggerHarpGliss,
	);

	void Activate() {
		StartListeningForEvent(OnPlayerSetNote(Note note));
		SendEvent_AddToConductorQueue(ActionList);
	};
	
	void OnPlayerSetNote(Note note) {
		State.SetTonic(note);
	};
};


class Conductor {
	Queue<Action> Actions; 
	
	void RecieveEvent_AddToConductorQueue(TArray<Action> actionList) {};
	
	void ProcessQueue(){
		// Activate next Action in the queue if eligible
	};
};

So in this updated iteration I’m adding the concept of where the Actions live.

The Tonic phase contains a set of Actions which is pushed to the Conductor on activation.

The Conductor then contains logic for simply processing each Action it receives.

In the above example I’m roughing in some idea of a queue of Actions the Conductor owns, but the point it is it is solely concerned with the execution of Actions.

Which means that I might consider an Action interface so the Conductor doesn’t have to worry about what a SpawnAudioPlatformSet actually is.

interface Action {
	void Initialize();
	void Activate();
};

class TriggerHarpGliss : Action {
	Scale Scale;
	Note Tonic;
	KVPair AudioCues;
	
	ComposerState State;
	
	void Initialize() {
		Scale = State.Scale;
		Tonic = State.Tonic;
	};
	
	void Activate() {
		// KVPair value of key {Scale, Tonic} to trigger audio queue
		// This would need UnrealQuartz access 
		TriggerAudioCue(Scale, Tonic);
		
		SendEvent_RemoveFromConductorQueue(this);
	};
};

class Conductor {
	Queue<Action> Actions; 
	
	void RecieveEvent_AddToConductorQueue(TArray<Action> actionList) {};
	void ReceiveEVent_RemoveFromConductorQueue(Action) {};
	
	void ProcessQueue(){
		Actions[0].Deactivate();
		Actions[1].Initialize();
		Actions[1].Activate();
	};
};

Now whether that Action is sending a “I am complete event” for consumption or directly calling a common RemoveFromQueue can be left open for interpretation at this point.

Most importantly, I can’t see any obvious flaws in logic.

The only thing I’m not totally happy about is how Tonic and TriggerHarpGliss are storing and accessing a State property.

That being said I’m not totally convinced its a bad thing either.

Final Initial Design

Names subject to change, of course.

The intent is to create a musical composition throughout the course of the games runtime.

This is achieved in Phases.

Phases contain a list of Actions on how to resolve the Phase.

Phases implement a PhaseController interface that describes the common commands necessary for controlling the lifetime of the phase.

A Phase Manager own a set of PhaseControllers, responsible for issuing the commands for each Phase’s lifetime.

When a Phase is active, it invokes commands to a “Conductor” (ActionExecutor?), which more-or-less maintains a queue of Actions to cycle through.

So, when a SetTonic Phase is activated, it will send a “Create Audio Platform” event to the ActionExecutor and start listening for a “Player Stepped on an Audio Platform" event to fire.

When it receives that event, it will send the Note that event fires with to the Tonic property in our State object.

This means that our State, Phase management, and gameplay execution is logically separated from each other.

image