Skip to content

Housekeeping and Morale

Most of today was a lot of housekeeping tasks. There were quite a few board items that were not tagged properly that I had created, nor did they have any sort of description of what they should actually do (besides the title). I should have done this sooner but there were a lot to get through and I got burned out after getting the ones I thought we would immediately need to address.

I also took some time to finish writing up the devlog from yesterday. I had a lot of the screenshots/gifs put in so I could remember the order of things, but I ended up working later than I wanted to last night and did not have time at the end to finish adding the actual text of the devlog.

Morale: Making Mood Matter

High-Level Overview

The next major system to tackle is the Crew Morale system. At it's core, it is pretty simple. There is a default value and a list of modifiers that affect that value. Here is a simple diagram showing how this functions.

Placeholder

We start with some base Morale value, 50 in this case. Then we go through the list of modifiers and sum up their modifier values. In the example below, after applying each of the debuff modifiers, the working value is down -45 giving us a total of 15. Then we apply all of the buffs (+20) for a final total of 35. This is event-driven, so it only updates when there are changes to the modifiers.

The end goal of this feature is to have thresholds, similar to the Warmth mechanic, where once the Crew has reached a certain threshold, there is a chance that they panic and run away from the Caravan. I haven't gotten this part in yet but it is next on the list.

Placeholder

Integrating the Warmth Mechanic

Since we don't have the Glimmerstalkers in yet (the main antagonists of the game and primary sources of morale modifiers) I only needed to incorporate the morale modifiers generated by the Warmth mechanics. Each state in the Warmth mechanic, except Normal has an associated morale modifier (the exact values will come through playtesting):

  • Warm (+)
  • Hypothermia (-)
  • Freezing (--)

I wanted to start with the Freezing state, since that was the biggest one, and the one that we already had built into the Crew controller as an explicit state. I added in a call to add the Freezing modifier when we enter the Freezing state and remove it when we leave the state.

private void HandleWarmthStateChange(WarmthStatus state)
{
    switch (state)
    {
        case WarmthStatus.Freezing:
            _freezingTimer.Start();
            _moraleMechanic.AddModifier(_freezingModifierTemplate);

            break;
        default:
            _freezingTimer.Stop();
            _freezingTimer.Reset();
            _moraleMechanic.RemoveModifier(_freezingModifierTemplate);

            break;
    }
}

Looks simple, right? Well, now it is. This actually stumped me for quite a while, trying to figure out how I wanted to handle adding/creating the modifiers.

Wild Brainstorming

It took me longer than I'd like to admit to come up with a solution I liked for handling the modifiers. The biggest constraint that I was running into was that I wanted to use ScriptableObjects (the Templates) as the "data" containers/configuration for each modifier (similar to how we do the spells). Normally, this wouldn't be that big of a deal. As mentioned, we do this already for the spells, so what is the problem? The problem lies in the fact that we need to support multiple "instances" of the modifiers, whereas we'll only ever have one instance of each spell.

Because we need to support multiple instances of the same modifier (i.e. they can have the "Minor Fear" applied multiple times) we needed a way to track them individually (i.e. if they clear after a certain duration, that needs to be tracked separately). If we put the timer in the Template, then they would all share the same timer and it would be a mess. The modifier you just put on clears immediately after because the original modifier's timer is up. No good!

The other constraint that we had was that we wanted to support modifiers that would be cleared either by a timer (Timed) or ones that had to be cleared programmatically (Manual). These had different requirements, most notably the Timed ones needed a timer of some sort while the Manual ones didn't.

Unified CheckToClear Method

My initial thought was to have a CheckToClear method on the Template. Then in the Morale system, it would call that every frame and the Template would decide what it needed to clear itself. This would have allowed me to stuff a timer into the Timed modifiers that it would check to see if it had elapsed yet. The Manual modifiers could always just return false so they never cleared.

I was feeling pretty good about this solution but I forgot something critical. I needed to support multiple instances. This solution led to the example I mentioned above with all of the "instances" using the same timer under the hood. I also put "instances" in quotes, because they weren't really instances. They were all a reference to the same Template.

All this led me to decide that I would need at least two things: a ScriptableObject (referred to as a Template) to represent the data/config and a wrapper class that I could instance and store. I went through multiple designs with this, each one getting more and more complicated as I tried to work through limitations. If we were only supporting "Timed" modifiers, this would have been easier because we could make assumptions. However, we also want to support "Manual" modifiers, that will stay in the list until they are explicitly removed.

ScriptableObject Sub-Types

Next, I tried making sub-ScriptableObjects, like I did for the spells, with one being Timed and the other Manual. The Timed one had duration config fields in it. Then, when I went to add a modifier, I would check to see if it was a Timed one and use a particular wrapper for it that had a SimpleTimer in it.

I got through adding support for the Timed ones when I ran into the problem of, how do I store all of the modifiers? - Do I try to cram them into one list? - That would make the "current morale" calculations easy, since I could just burn through one list - Do I maintain separate Timed and Manual lists? - This would make supporting the different needs easier but means coalescing the lists when I need to calculate them - Do I use a dictionary instead, with the Template as the key and some wrapper as the value? - This would make it easier to see how many of each Template type there is. Good for checking if we've reached our MaxInstance limit or not - This still, ultimately, ran into the same problem of the two types needing different information. It just pushes the problem around and doesn't fix it

I tried several variations of this and none of them sat right with me. They all just felt dirty and convoluted.

K.I.S.S

Ultimately, I decided to forego sub-classing and all of that and go back to the basics. The humble (lesser) god-object. Instead of having multiple sub-types, all of the information would be on the MoraleModifierBase class and we would use an enum flag to determine how it should be treated. This may not be the best practice but 1) this is a game jam and 2) this class is incredibly small with a very low chance of growing wildly.

This simplified everything. I could just use a simple wrapper class that holds the Template and an (optional) timer.

private class MoraleModifier
{
    public MoraleModifierBase Template { get; set; }
    public SimpleTimer Timer { get; set; } = new SimpleTimer();
}

[CreateAssetMenu(fileName = "MoraleModifier", menuName = "THGJ22/Morale/Morale Modifier")]
public class MoraleModifierBase : ScriptableObject
{
    [Header("Config")]
    [field: SerializeField]
    public int Amount { get; private set; }

    [field: SerializeField]
    public int MaxInstances { get; private set; } = 1;

    [field: SerializeField]
    public float ClearDuration { get; private set; }

    [field: SerializeField]
    public MoraleModifierClearType ClearType { get; private set; }
}

public enum MoraleModifierClearType
{
    Timed,
    Manual
}

Armed with this, I was able to utilize a single list with all of the modifiers in it and all of the instances were properly instanced. Now, I just needed to account for the different Clear Types when creating the wrapper class. To do this, we check the ClearType and if it is Timed we initialize a new SimpleTimer and start it. Otherwise, we just leave it null.

public void AddModifier(MoraleModifierBase modifierTemplate)
{
    var totalWithSameTemplate = _modifiers.Count(m => m.Template == modifierTemplate);

    if (totalWithSameTemplate >= modifierTemplate.MaxInstances) return;

    SimpleTimer modifierTimer = null;

    if (modifierTemplate.ClearType == MoraleModifierClearType.Timed)
    {
        modifierTimer = new SimpleTimer(modifierTemplate.ClearDuration);
        modifierTimer.Start();
    }

    var modifier = new MoraleModifier
    {
        Template = modifierTemplate,
        Timer = modifierTimer
    };

    _modifiers.Add(modifier);
}

Then when we go to check the timers, we kick out early if they are not Timed modifiers.

private void CheckToClearModifiers(float deltaTime)
{
    for (int i = _modifiers.Count - 1; i >= 0; i--)
    {
        var modifier = _modifiers[i];

        if (modifier.Template.ClearType != MoraleModifierClearType.Timed || modifier.Timer == null) continue;

        modifier.Timer.Update(deltaTime);

        if (modifier.Timer.HasElapsed)
        {
            _modifiers.Remove(modifier);
        }
    }
}

Better Way to Handle Templates

Now that I had the basis down for how I wanted to handle things internally to the MoraleMechanic class, I started looking harder at the public interface of the class. As you can see in the above example of the AddModifier method, I was taking in the MoraleModifierBase directly. While this would have worked, it was kind of awkward to need the Templates all over the place so they could be passed in.

I wanted to have it more self contained, so the caller could just say what Modifier they wanted to add/remove and it would take care of itself. This led me to rework it to use an enum as the primary value being passed around, then internally it would do a lookup for the Template that matches the enum value.

This also meant we need to somehow map between the enum values and the Templates. A dictionary stood out to me as the best way: enum as the key, Template as the value. So I went about making a new ScriptableObject, a MoraleModifierSet, that would allow me to define different sets and use them (like for "skittish" Crew who are more affected by negative modifiers...but that's for stretch goals).

If you've worked with Unity, SerializeField, and the Inspector though, you've probably come across the problem on Dictionaries not being supported in the Inspector. It has been a while since I've needed a dictionary in the Inspector, so I had to go digging to see what people were saying for supporting dictionaries. I tried a couple of options that I didn't quite like. Ultimately, I went with a List of key/value pairs with dictionary-like accessors.

[CreateAssetMenu(fileName = "MoraleModifierSet", menuName = "THGJ22/Morale/Morale Modifier Set")]
public class MoraleModifierSet : ScriptableObject
{
    [SerializeField]
    private List<ModifierKeyValue> _templates = new List<ModifierKeyValue>();

    public MoraleModifierBase this[MoraleModifierType type]
    {
        get => _templates.FirstOrDefault(t => t.Type == type)?.Template;
    }

    [Serializable]
    private class ModifierKeyValue
    {
        [field: SerializeField]
        public MoraleModifierType Type { get; set; }

        [field: SerializeField]
        public MoraleModifierBase Template { get; set; }
    }
}

This allows me to view them (roughly) as key/value pair while allowing me to make the actual collection read-only. Here you can see how the actual MoraleModifierBase looks in the Unity Inspector, as well as the Modifier Set.

Placeholder

Placeholder

The AddModifier method was refactored to look like this:

public void AddModifier(MoraleModifierType modifierType)
{
    var totalWithSameTemplate = _modifiers.Count(m => m.Template.Type == modifierType);
    var modifierTemplate = _modifierTemplates[modifierType];

    if (totalWithSameTemplate >= modifierTemplate.MaxInstances) return;

    SimpleTimer modifierTimer = null;

    if (modifierTemplate.ClearType == MoraleModifierClearType.Timed)
    {
        modifierTimer = new SimpleTimer(modifierTemplate.ClearDuration);
        modifierTimer.Start();
    }

    var modifier = new MoraleModifier
    {
        Template = modifierTemplate,
        Timer = modifierTimer
    };

    _modifiers.Add(modifier);
}

And the calling code was refactored to this:

private void HandleWarmthStateChange(WarmthStatus state)
{
    switch (state)
    {
        case WarmthStatus.Freezing:
            _freezingTimer.Start();
            _moraleMechanic.AddModifier(MoraleModifierType.Freezing);

            break;
        default:
            _freezingTimer.Stop();
            _freezingTimer.Reset();
            _moraleMechanic.RemoveModifier(MoraleModifierType.Freezing);

            break;
    }
}

Seeing it all in Action...Maybe

Now that I had the base system in place, I needed to test it before adding a bunch more modifiers. It should be adding the Freezing modifier to the Crew when they reach the Freezing state. I threw some console logs in the Morale update method, hit play, and waited for their Morale to drop. And waited. And waited.

Some more console logs later, it turns out that the Crew prefab did not have the Warmth mechanic added as a component, so nothing ever triggered the Freezing state in the Crew. Easy-peasy. I got that added in and configured, ready for more testing. Hit play and waited for their Morale to drop. This time I had logs to see the Crews' Warmth and Morale and was feeling good. The Crews' Warmth dropped below the Freezing threshold but their Morale did not drop.

Even more logs to see what the Warmth state was. This was like trying to find a needle in a haystack. Because we had four characters with the Warmth mechanic and three with the Moral mechanic, we were dumping at least seven log messages per frame. I was having a hard time determining which Crew was associated with which numbers and which states were tied to which Crew. It was a bloodbath and something needed to change.

Improved Debugging

Needing better information, I turned my attention from finding the issue to gathering better, more actionable information.

The first change I made was to give the Crew actual names. Instead of them all being CrewMemeber_Crew (Clione), now they would have distinct names like Crew_Jarnathan. This made associating individual values to their respective Crew much easier. This helped quite a bit when looking through the logs but the sheer number of them was still obnoxious.

The next change I made was adding a "debug panel" to the game. This can be toggled with the backtick/tilde (~) key. It contains a list of characters, including their names and the values/states for their Warmth/Morale.

Placeholder

Anywhere in the code can call update functions to set the current values of the Warmth or Morale.

private void Update()
{
    _warmthValue = ApplyWarmthDelta(-_depletionRate * Time.deltaTime, true);

    DebugUI.Instance.UpdateWarmth(name, _warmthValue, _currentState);

    CheckAndApplyState();
}

Notice that I didn't say you can update the Names. That is because, behind the scenes, it is using the name to find an existing detail line entry to update, or creating one if it doesn't exist. Essentially, the name is the key and a new entry will be added for each new name that is sent to it. Here you can see a snippet showing the Morale being updated and it "safe" getting the Detail line.

public void UpdateMorale(string name, int morale)
{
    var details = GetDetails(name);

    details.SetMorale(morale);
}

private DebugDetailsUI GetDetails(string name)
{
    if (!_details.TryGetValue(name, out var details))
    {
        details = Instantiate(_detailsPrefab, _detailsContainer.transform);
        details.SetName(name);
        _details.Add(name, details);
    }

    return details;
}

All of this combined to gave a much nicer view of the current states of the Crew. The improved view made it very obvious what was (or in this case, wasn't) happening. If you watch long enough in the first gif of the debug panel, you'd see that when the Crews' Warmth values drop below 25 they drop into the Hypothermia state. However, when they drop below 10 they don't drop into the Freezing state.

The Fix

After bringing the issue up to the team, Andrew pointed out that the order of the Warmth thresholds matters. I had been ordering them in the "natural" flow of warmest to coldest. Like so:

Placeholder

Andrew had his ordered differently, from when he initially built the system. His made more sense from a mathematical/programmatic standpoint. Here was his:

Placeholder

As you can see, it you were to start at the top, you could just check them one right after another and the logic works out. Compare that to mine where if Hypothermia is checked before Freezing then it will always return first, even if the value is below 10.

I added a board item as a stretch goal to make that system more order-independent but for now, we'll run with the Order that Andrew had, since it works and we should only need to adjust the actual threshold values moving forward. The actual order of the states won't change though.

After adjusting the order of the thresholds, the Warmth mechanic changed states as expected. Success!

Placeholder

Funeral Procession

While testing out the Freezing state on the Crew, I did run into an interesting bug. When the Crew die, they do not stop following their assigned sled. Instead, they die then float along side it. Hopefully an easy fix, but it was a good way to end the night.

Andrew summed it up perfectly:

"Gonna make it to the outpost one way or another"

Placeholder