top of page
Search

Log 27: Objectively I'm Making Progress

  • Writer: Kassandra McCormack
    Kassandra McCormack
  • Mar 5
  • 7 min read

Time to start working on mission objectives. Mission objectives were an interesting conundrum to work on; They are the first piece of the game that touches on being cross-level and I really had to think about how they would work going from a mission select screen to an actual gameplay level.


The first thing I did was create two new components: an "Objective Component" and a "Questor Component". The plan was that the Objective Component would store an Objective ID variable that indicates what kind of objective was accomplished and uses the Game State to send a dispatcher message to all listening Questor Components, which would track objective progress. I say "the plan" but it quickly fell apart.


Instead of trying to get everything to subscribe to dispatchers on everything else, I had decided to use the Game State because it is a singleton that can easily be accessed from anywhere.


The next thing was creating, storing, and assigning mission objectives. The creating and storing were obvious: a Data Table of missions. The hard part was how to select the mission from a Mission Select screen and have that carry over into the actual playable level. Which, when I thought about it, wasn't that hard: the only way to do that would be through the Game Instance, the only blueprint I know of that persists between levels. At this point I could have moved the Quest dispatcher over to the Game Instance as it is also an easily referenced singleton, but I decided to let it lie and keep it in the Game State.


While I was working on the Objective System I learned about Gameplay Tags, which are basically nested Booleans (for example: character.mook, character.captain, etc). I figured something like this would be useful for heavily repeated characters like standard units, but less so for unique characters like the mission boss. So, I created a new struct that includes a Name variable and a Gameplay Tag Container. The Name variable is for non-repeating characters (or specific bases) so I don't need to create dozens of tags that will only be used once.


Eventually I realized that I don't need the Questor component, because I don't plan on having character individual objectives, only team (Faction) ones. Therefore I only need to track the objectives in a central location, so I moved all the logic into the Game State. I should have made a component that went on the Game State, but at this point I did not know that was a possibility. Eventually I transferred all the Objective stuff into an Objective Tracker Component, but that was as I was finishing up this blog post.


The next thing I thought to work on was how to display objectives on the HUD, because

Just wanted something basic working.
Just wanted something basic working.

mission objectives are useless if the player either doesn't know what they are or can't easily keep track of them. Fortunately the Objective HUD doesn't have to be too obviously fancy: I made a Vertical Box on the HUD. For what to display in the Vertical Box, I had to make a new Widget that just held a Text Box (as apparently you can't dynamically make new text boxes raw, they have to be a part of a different widget). This was all easy so far, the hard part came when I wanted to strike through the text to show that the objective had been completed (or failed, but I'm not sure if I'm going to do optional objectives).


I wanted to animate the strike through to look like it was being drawn and not just appearing, after all: a bit of UI juice goes a long way. In order to display text strike throughs, the text box actually draws a stretched material over the text, a material I got to provide. That actually made animating it easier, because I could animate a single [Alpha] variable to control how much of the material is masked or not. The challenge came because I need the animation and strike to be the same over several potential HUDs (each character has their own HUD, and eventually the player will be able to switch between them). Because the objectives are tracked in a single place in the Game State, I added a new Dynamic Material Instance variable to the Objective struct, so each objective can be individually crossed out. Originally I had made a timeline on the Game State that played the animation, but I discovered that it can not handle more than one objective being crossed out at a time. I wound up having to make a separate actor whose only purpose in existing is to keep a reference to a given strike through material instance and animate its opacity mask. It may not necessarily be the most memory efficient but it was the only way I could think of at the time to make it work.

ree

While I was working on the strikethrough material I found (and fixed) an additional bug, but the blog reads better when it is all together so I'm going over it here. The bug I found was that although the text for the objectives displayed on the screen, it would not update when advancing the objective (for example defeating an enemy when the objective is "Defeat 5 enemies"). All in all, a pretty major bug.


Eventually, after much debugging, testing, and research, I discovered: the problem is that when one uses map variables, when one uses the [Find] node the value it returns is a copy, NOT a reference to the actual stored value. This means that any modifications made won't automatically apply to the values in the map and have to be manually updated to the map. While annoying to deal with and remember, not terribly difficult. I just had to remember to manually update all the maps every time I modify one of them.


The next thing I worked on was locked objectives. In order to easily track what is completed or not I changed the int array into an int-bool map, so I don't have to do enum comparisons constantly. Checking what is functionally an array of bools, was the easy part, eventually this function would grow far more complicated.

Unlocking an objective, by completing a prior one.
Unlocking an objective, by completing a prior one.

As I was finishing this up I realized that it would be possible to technically complete an objective before it became active (for example capturing a specific base or defeating a specific enemy). So, instead of restricting my ability to build future possible missions I wanted to add a check on inactive missions to check for advancement or completion. This was a simple boolean that I called [Can Accept Prior Progress], if true: means the objective gets updated, even if inactive. I also have to do an additional check after unlocking an objective to make sure it hasn't already been completed, in which case I need to animate the strikethrough at the same time as it unlocks and gets added to the HUD.


Upon my testing that these checks were working, I found a few bugs that needed to get worked out:


1) Some of the enemies were being counted twice for the defeat enemy objectives.

After extensive testing, I discovered that the double count only happened when the base got captured. Doing some debugging I discovered that because I hadn't given anything a specific name, nor a name to look for, the name comparison was returning true without even looking at the Gameplay Tags (I also discovered that I was comparing Tags incorrectly). The first thing I did was change the objective checking to only check the name of the ID if a specific name was given to look for; otherwise it would only compare Gameplay Tags. Then, I discovered that instead of just comparing two Tag Containers with an [Equals] node, they have to get compared to a Tag Query struct using the [Does Container Match Tag Query] node. Once I got that set up, and changed the Objective ID variable in the Objective struct to include a Tag Query instead of a Tag Container, it worked perfectly.


2) When a mission unlocks two entries were being added to the HUD instead of just one.

This one was interesting. Originally, during my debugging for this, I found out that an additional objective labeled "None" was getting added during one of my many for loop checks. So I implemented additional safeguards to make sure that would stop happening, but the double objective entries still kept showing up. I managed to fix it by changing a count check for the number of displays saved in the objective struct and instead of checking against a hard coded number, I checked against the number of cached HUDs. I still don't understand why it was happening, nor how I fixed it, but at this point I would take it.


3) Once an objective gets unlocked it would not only show the incorrect number of enemies defeated, but also not update further (although it would correctly cross out on completion).

This one appeared obvious to me, somewhere a map and a structure wasn't getting updated with either the new text or a reference to the original text box. After going through my code with a fine tooth comb and eventually working my way backwards I discovered that the new text display was, in fact, getting added to the main saved location, but the incrementing was happening in a locally saved map value within a function for use in a for each loop. So what I did was create a new Update function that would take references for the struct map values and the keys that needed to be updated and put that function at the end of every branch I could find.


4) Not so much a bug, but that objectives should stop updating when completed and should not go past the completion value.

This was easy I just put an additional check in the increment and decrement function to not just only progress if the objective is active (or inactive and can accept prior progress), but also check that the current value is less or greater than the required value, respectively.


And finally, I made a couple of mission complete screens, one for a victorious player and one for a defeated player. I had to make a quick function that checked that all mission objectives were completed (or were optional) and if so, it triggers a dispatcher. Once I start building menus and other levels I'll have to put in a timer and level transition, and when I build the AI it will have to turn them off, but for now the player can still run around and do things, they just can't see their HUD.


ree
ree

 
 
 

Comments


bottom of page