Even if following the OOP style of coding and it helps you in defining your game logic into related chunks, you still need to worry about how all of these things work with each other. In the beginning, when not much of your game is coded up yet, it is simple and this kind of concern may seem unnecessary. Here is an example image of what could potentially happen very quickly.
Not only this is hard to understand the execution flow from any given point, this is what’s known as the spaghetti code. Because all the classes are depended on each other without any intuitive pattern that could be used to “untangle” this mess, once your code base gets into this state, you won’t be able to make changes at all. If you make a change in one spot, you will end up breaking something in totally unexpected places in your game.
One way I have grown to use to combat this issue is to use the concept of uni-directional data flow. What is data? Data is just what we usually point to with our variables. The strings, the numbers and the Lua tables. We usually call a function while passing it some data and get some data returned. If you could just imagine these functions as transformation blocks, our parameter data goes in and comes out as a different form of data, in a way taking a “transformation”. What if we design the classes in our code in such a way that, in order to achieve a certain “task” our execution flow goes through series of functions only in one direction while transforming data in each step? We no longer have such tangled dependencies between our classes anymore and everything would begin to look much simpler.
Not all situation is going to look like that. For example, data won’t necessarily feed back to SomeClassA in the end again. This is just one diagram of a system that is built using the uni-directional approach. So what is so good about this as opposed to the above spaghetti architecture? There are many reasons but for me, the one biggest advantage is the amount of mental work required to understand the complexity of the whole system. It is much easier to debug or make changes to a system that is not “tangled” with all kinds of dependencies. Enough theory talks, let’s get into writing a part of my team’s game using this design pattern. I will literally be using the code from this tutorial and put it into the game.
What are we building?
Every RPG game has a dialogue system which allows the users to talk to different NPCs in the game that allows you to do many different things: accept quests, tell a story, merchant shops, etc. We will build the dialogue engine that could be used to quickly create any kind of interactive dialogues in the game. In order to test this engine, we will pick one specific use case. In our game, we have a tutorial map where the brand new users join and talk to different NPCs to get a certain weapon that they offer. This will be used to test our dialogue engine as we build it out. Let’s first consider the requirements.
- In the first response, NPC will greet you. At the bottom of the dialogue window, you will be given 3 options to respond back to the NPC. (1) Ask more about the weapon he is giving out, (2) Choose to equip his weapon, (3) Say bye to close the conversation
- If you are already wearing the weapon the NPC is offering, the option to click to wear this weapon won’t appear.
- When the user clicks to ask more about how to use the weapon, next dialogue window will show up to explain how to use it. At the bottom of this window, there will only be 2 options now. (1) Equip the weapon or (2) Say bye to close conversation.
So it seems like we just need to worry about 2 dialogue windows in this simple example. But it is slightly tricky because the NPC knows you have talked to him before or not. Let’s put aside this one specific use case and move onto designing the generic dialogue engine. Here is the high-level design of the data flow. All of the code shown here will be in the Local Script.
First, we need to prepare the model that needs to be fed into the dialogue engine. In order to do this, we may even need to get some data from the server. (ex. if the NPC we are talking to is a quest giver, we need to find out if the current player has completed the quest or not. This is because the NPC will say different things depending on the quest status. We will have to ask the server script to get the quest status of the current player.) The DialogueModelInjector will prepare all the required data and output a Lua table that represents the current state of the dialogue to be had. This model is then fed into the DialogueEngine. This is where the transformation happens. The DialogueEngine will locate the right transformation node in its tree based on the DialogueModel that was fed in. It will then pass that DialogueModel into the node and let it do one of two things.
- The node may choose to output a DialogueGuiModel and pass it to the GUI window to render the next phase of the dialogue.
- Or the node may choose to invoke the DialogueModelInjector to feed in a new model to the engine to run again.
If the engine decides to output a GUI model and passes it to the dialogue GUI window, the GUI will interpret this model to show this however it wants. In a typical dialogue window, we usually have some options at the bottom that the user can click. This will trigger the GUI to possibly trigger the DialogueModelInjector to repeat the cycle again with a new model. This may seem too ambiguous and hard to understand so far but the important thing is the execution flow only ever goes in one direction. DialogueModelInjector is the starting point of each cycle. In this tutorial, we will focus on coding everything except the GUI window.
The class design diagram shown above is not 100% accurate. Since every dialogue in the game is all different, you can’t possibly have “one” DialogueModelInjector and “one” DialogueEngine for all the dialogues in the game. If that would be the case, for example, that DialogueEngine would have a massive tree for every possible conversation in the game. Instead, those classes are really abstract classes. You will have to implement a child concrete class for each of them that specifically implements the single case of the dialogue. These are some more OOP concepts which I did not cover in the last tutorial. But if you just give a quick study on what abstract classes are all about you may get them quickly. For our specific use case, we will need these classes.
When designing classes in OOP style of programming, you can quickly get into a messy web of class dependencies. This is highly undesirable and there are many patterns that can be used to avoid these situations. Uni-directional data flow is just one of the patterns that can be used to simplify the execution flow as well as the data flow. Overall it just makes it easier for the coder to understand his own code even after a long time of staying away from it. There are many other patterns available and it is highly recommended to study pros and cons of each approach. Pick the right one for your use case.