My Thresholds for Refactoring

I’ve read a book called Code Complete about a decade ago. The mantra of that book is “the goal of software is to manage complexity.” That phrase is repeated throughout the book and it has stuck with me ever since. The idea is that since we make software to manage complexity, we should strive to manage the complexity of our code as well. The book promotes maintainability as the primary metric to work on when making software. If your code is maintainable, other software metrics like robustness, correctness, and efficiency will follow. I don’t want to elaborate how and why this is. The book writes about this in detail. I have experienced this to be true based on my years of software development.

This mindset of maintainability first has also served me well when I became a game programmer. Video games are somewhat in a precarious position because you’re not managing a real world complexity. Rather, the complexity is artificial or made entirely from imagination. It is in the form of game design which includes the graphical representation and game mechanics. It’s still a complexity, nonetheless, and often times more complicated, too. Over the years of programming, I have developed this brain personality that starts nagging me when I reach thresholds in my code. I don’t want to write about how to make your code maintainable per se. Instead, I wanted to share some common thresholds in code that I keep in mind that results to refactoring. By doing this frequently and sometimes unconsciously, it leads to maintainable code. In programming lingo, these are also referred to as “code smells”. There are lots of those but I’ll only enumerate the ones that I commonly encounter, thus also commonly refactor.

Cyclomatic Complexity

Cyclomatic complexity is simply the total number of control flow branches within a function, method or routine plus one. Control flow branches are caused by if, while, for, foreach, do-while, catch clauses (including finally), and each case in a switch statement. Each AND (&&) and OR (||) operators are also included in the count. I consider anything that breaks the linear flow of statements as a branch. Thinking about cyclomatic complexity like this makes it transferrable to any programming language that may have special operators or constructs. My threshold is 10. This means that when I see a function that has cyclomatic complexity of more than 10, that function needs to be refactored. This can simply be fixed by introducing a new function and move some of the control flow branching from the original function to the new one.

For example, the following method has a cyclomatic complexity of 4 (remember plus one) and is safe from being ripped apart:

public static Transform FindTransformByName(Transform transformRoot, string name) {
    if (name.Equals(transformRoot.name)) {
        return transformRoot;
    }

    // search in children
    Transform searchResult = null;
    foreach (Transform childTransform in transformRoot) {
        searchResult = FindTransformByName(childTransform, name);
        if (searchResult != null) {
           return searchResult;
        }
    }

    return null;
}

Of course there are exceptions. If there’s a switch statement that has 10 or more case branches and there’s no other logical way to write such function, I’ll probably just leave it be. But this rarely happens. There are many ways to restructure code to prevent such pattern.

Parameter Count

This is self explanatory. It’s the number of paramaters that a function/method/routine has. My threshold for this is 7. Seven parameters to me is already a stretch and I itch to refactor such function already. More than 7 is really a no no.

There are many ways to fix this. Introducing a new function and moving some parameters there might do the trick. A set of parameters that logically goes together might be bundled into a struct or another class. Functions with this amount of parameters are probably big. Turning the whole function into a separate class where the parameters become member variables could be viable. This way you can further refactor the big function to several smaller functions all contained in the new class.

Nesting Depth

Nesting is when you indent a block of code due to a control flow like if, for, or while. For example the following code has nesting of 3:

for(...) {
    if(...) {
        if(...) {
        }
    }
}

Deep nesting is bad because it makes code hard to read thus making it less maintainable. My threshold is 3. Three is already an eyesore to me. Any nest I see beyond that is refactored away to another function.

Inheritance Depth

I’m glad that modern programming is moving away from inheritance. Game engines like Unity uses components as building blocks instead of inheritance. In hindsight, this makes inheritance no longer the defacto way of making a game unlike in the old days. I’ll no longer elaborate why inheritance makes software less maintainable. You can search about that. Nonetheless, inheritance is still a tool that can be used properly. I do use it albeit sparingly.

Inheritance depth is the amount of ancestors in an inheritance chain. In the following example, GreatGrandChild has an inheritance depth of 3:

class Guy { ... }
class Child : Guy { ... }
class GrandChild : Child { ... }
class GreatGrandChild : GrandChild { ... }

My threshold for inheritance depth is 3. When such a thing happen, I become very careful with deriving a class in that hierarchy. An inheritance depth of more than 3 means that I may have to restructure my code. Sometimes there’s no other better way. I do let it go if adding another child class is the quickest and most logical way to do something. Just inside me, the alarm bells are already so loud when that happens. The key to managing inheritance is to not use it frequently and design it with a flat structure as much as possible, like maintain an inheritance depth of 1.

Callback Count

I want to be clear that I’m not talking about callback chain count here. That’s different. If you have JavaScript in web background, you know what I’m talking about. Callback chaining is barely used in game programming, at least for me because I avoid it. Callback count is merely the amount of callbacks that you use in a class. If you have an exposed member variable that is an Action<> or Func<> that can be assigned from outside of the class, that’s probably a callback.

What usually happens is I introduce one callback. Then later on I introduce another one. Then much later, I add a new one. Then I see this and mentally decide that it’s ugly.

My threshold for callback count is 2. Three is just too much for me. At that point, I’ll refactor that into an interface and manage instances of the interface instead. Maintaining separate callbacks becomes unwieldy and clunky when there are more of them. For example, say you have this class that has a callback count of 4:

class RouteAssignmentManager {
    private List<Action<RouteAssignment>> assignmentAddedActions = …;
    private List<Action<RouteAssignment>> assignmentRemovedActions = …;
    private List<Action<RouteAssignment>> assignmentIncreasedActions = …;
    private List<Action<RouteAssignment>> assignmentDecreasedActions = …;

    public void AddAssignmentAddedAction(Action<RouteAssignment> action) {
        ...
    }

    public void AddAssignmentRemovedAction(Action<RouteAssignment> action) {
        ...
    }

    public void AddAssignmentIncreasedAction(Action<RouteAssignment> action) {
        ...
    }

    public void AddAssignmentDecreasedAction(Action<RouteAssignment> action) {
        ...
    }
}

Whenever you want to add any one of these callbacks, you may want to add the others as well because they are logically related. For example, when you forget to implement the removed action pair for an add action, you might introduce bugs.

In this case, it’s better to turn it into an interface and manage only a single list of instances, instead:

interface RouteAssignmentManagerObserver {
    void OnAssignmentAdded(RouteAssignment assignment);
    void OnAssignmentRemoved(RouteAssignment assignment);
    void OnAssignmentIncreased(RouteAssignment assignment);
    void OnAssignmentDecreased(RouteAssignment assignment);
}

class RouteAssignmentManager {
    private List<RouteAssignmentManagerObserver> observers = …;

    public void AddObserver(RouteAssignmentManagerObserver observer) {
        ...
    }
}

This is better because every time you want to listen to changes to RouteAssignmentManager, you are ensured that you don’t forget to implement the other related methods. If you want to add a kind of change that you want to broadcast, you can simply add a method on the interface. This will break the implementing classes and will force you to look at those classes to see if they need to implement the new change notification. You reduce the amount of variables that you need to manage, too. Now, it’s just one list.

There are exceptions of course. If the callbacks are totally unrelated, sometimes it’s not logical to introduce an interface to combine them. But still, I would try to limit callbacks to 2.

Duplicate Code

When I’m writing a function, sometimes I need a few lines of code that I wrote a while ago. I’ll then copy and paste that to the function I’m writing. I just introduced duplicate code. This first copy is usually ok. Another copy, however, is no longer good. This means that I now have the same code in three different places. This is my threshold for duplicate code. Three strikes, and to a new function you go. For code with more than 5 lines of code, 2 copies should send them to a function.

Lines of Code

I’m referring to lines of code of a single class here. I usually write just one class for each file. My threshold is 500 lines of code. I don’t refactor right away if I see a class with this many lines of code. Rather, it signals to me that it’s probably no longer appropriate to add more logic or some kind of maintenance code to the class. Small functions like getter and setter are still ok to add. Small functions that are totally related to the class variables are also fine. 500 lines of code tells me that new features should probably be coded in a new class.

Conclusion

I’m not saying that these thresholds are the most optimal. These are just my preference and has become a habit. You can decide on your own thresholds. For example, if you can handle a cyclomatic complexity of 20, good for you because I can’t.

Lastly, if you want a bigger collection of code smells and how to refactor them, there’s also a book for that. I’m not advertising the books and there’s no way I’m related to the authors. I’m saying that they’re good books and they are helpful.

How about you? What are your thresholds?

Advertisements

Some Utility Code

Every now and then, I’d like to share some of my small utility classes that I’ve made a long time ago but are still being used until now. The following are what I call some of my mini utilities. They can be used in your own Unity projects.

(Note that I’ve removed method description comments so that the code part looks more compact.)

Comment Component

A lot of times have come up where I have an object or prefab that contains a lot of components. Sometimes, I want to be able to note about the values I’ve entered for some component or state why the component is there. Sadly, Unity doesn’t have a feature where you can comment on components (let me know if you know one). However, it’s quite easy enough to roll your own “comment” component.

public class Comment : MonoBehaviour {

    [SerializeField]
    private string text;

    void Awake() {
        DestroyImmediate(this); // auto destroy to save memory
    }

    public string Text {
        get {
	    return text;
	}

        set {
	    this.text = value;
        }
    }

}

// This is the editor script. Place it under an Editor folder.
[CustomEditor(typeof(Comment))]
public class CommentEditor : Editor {

    private Comment targetComponent;

    void OnEnable() {
        this.targetComponent = (Comment)this.target;
    }

    public override void OnInspectorGUI() {
        if(targetComponent.Text == null) {
            // use an empty string so that it won't throw null pointer exception
            targetComponent.Text = "";
        }

        targetComponent.Text = GUILayout.TextArea(targetComponent.Text, GUILayout.Height(100), GUILayout.MinWidth(200));
    }

}

To use, just add the Comment component and type your comment text in the textbox.

CommentComponent

UI Image Sprite Animation

I tried to use the animation timeline for animating sprites in a UI Image. But it didn’t work or maybe I was using it wrong. I got impatient, so I turned to code.

public class ImageAnimation : MonoBehaviour {

    [SerializeField]
    private Image image;

    [SerializeField]
    private float framesPerSecond = 15;

    [SerializeField]
    private Sprite[] sprites; // The sequence of sprites comprising the animation

    private float polledTime;

    private int currentFrameIndex;

    private void Awake() {
        Assertion.AssertNotNull(this.image);
        Assertion.Assert(this.framesPerSecond > 0);

        this.polledTime = 0;

        // Reset
        this.currentFrameIndex = 0;
        SetSprite(this.currentFrameIndex);
    }

    private void Update() {
        this.polledTime += UnityEngine.Time.deltaTime;

        // We didn't cache this so we can see the effect of framesPerSecond on the fly like tweaking it in editor
        float timePerFrame = 1.0f / this.framesPerSecond;

        while(this.polledTime > timePerFrame) {
            this.polledTime -= timePerFrame;

            // Show next frame
            this.currentFrameIndex = (this.currentFrameIndex + 1) % this.sprites.Length;
            SetSprite(this.currentFrameIndex);
        }
    }

    private void SetSprite(int index) {
        this.image.sprite = this.sprites[index];
    }

}

This is very straightforward to use. Just specify an Image target, a frame rate, and the sequence of sprites to show. That’s it. It will play on loop. Nothing fancy. I’ve use this on the game over screen of Academia.

GameOver

SelectionSequence

Political Animals had a common UI where you select an item from a finite set of items using next and previous buttons. I’ve noticed that I’m repeating the same code spread across different UI screens. So I made a generic class for this.

public class SelectionSequence<T> {

    private readonly List<T> items = new List<T>();

    private int currentIndex = 0;

    public SelectionSequence(T[] array) {
        this.items.AddRange(array);
        Reset();
    }

    public void Reset() {
        this.currentIndex = 0;
    }

    public void MoveToNext() {
        this.currentIndex = (currentIndex + 1) % this.items.Count;
    }

    public void MoveToPrevious() {
        int decremented = this.currentIndex - 1;
        this.currentIndex = decremented < 0 ? this.items.Count - 1 : decremented;
    }

    public T Current {
        get {
            return this.items[this.currentIndex];
        }
    }

    public void Select(int index) {
        this.currentIndex = index;
    }

    public void Select(T item) {
        for(int i = 0; i < this.items.Count; ++i) {
            if(this.items[i].Equals(item)) {
                Select(i);
                return;
            }
        }

        throw new Exception("Can't find the item to select: " + item.ToString());
    }

    public int Count {
        get {
            return this.items.Count;
        }
    }

    public T GetAt(int index) {
        return this.items[index];
    }

}

// Usage
// Say you have an array of LanguageOption
LanguageOption[] languages = ...;
SelectionSequence<LanguageOption> langSequence = new SelectionSequence<LanguageOption>(languages);

// When next button is clicked
langSequence.MoveToNext();

// When previous button is clicked
langSequence.MoveToPrevious();

// Do something with current selection
ChangeLanguage(langSequence.Current);

This kind of selection have come up in Academia, too.

SelectionExample

These are just some of my cute little classes. They’re small but they have been very useful. Here’s some programming tip, think of making classes like making a new invention or a device. That way, you will think in terms of narrowing functionality as much as possible and expose only appropriate public methods. This will also make your classes more modular. Over time, you will develop a library of robust and reusable code.

Our Tile Class

The foundation of every tile based games is the structure of their tile model. I think our game Academia has come a long way in the evolution of its tile structure. It’s robust enough to be shown to the public. If you’re making a tile based game, hopefully this helps you.

I’ll show you the member variables first:

public class Tile {

    private readonly TileGrid grid; // The parent grid
    private readonly Cell cell;

    private NodeHandle handle;

    // Note here that this is a map of layer and TileItem pair
    private Dictionary<string, TileItem> itemMap = new Dictionary<string, TileItem>();

    // Note here that task items are added to a separate map which uses its BuildItem as the key
    private Dictionary<string, TileItem> taskItemMap = new Dictionary<string, TileItem>();

    private readonly IntVector2 position;

    private int itemFlags;

    // May be null if tile is not in any zone
    private Zone zone;

    ...
}

We included the parent TileGrid so we could easily find other tiles whenever we have a reference to one tile. This helps a lot when neighbor tiles are needed. cell contains the information of the world tile position, cell width and height, bottom left position, top right position, etc. NodeHandle handle acts as the node in our A* framework. position is the tile position using integer coordinates.

The dictionaries itemMap and taskItemMap are used to store TileItem instances in each layer. A TileItem contains information about a tile. For example, say a TileItem instance for a table object. This TileItem instance means that a table occupies this tile. It also contains information like if the tile is blocked or not (for example wall). A Tile can have multiple TileItem instances for cases like the tile having a floor, dirt, and an object on top of it. Each of this are in different layers. The use of dictionary also implies that there can only be one TileItem per layer. This helps in checking if a tile already contains an item in a certain layer. This is usually used for preventing players from building objects on tiles that already have existing objects.

We differentiate between normal items and task items. Normal built items are added on itemMap while task items go on taskItemMap. This differentiation is needed so that items in multiple layers can be built in the tile. For example, build floor then build a table on top of it. Tasks used to be stored in only one layer but we found this to be inadequate, thus the current implementation.

itemFlags is a bitmask containing a bunch of information like if the tile is blocked or not, does it block students or not, does it contain a “character slot”, orientation of the character if one uses the slot. We specifically used a bitmask for faster “reachable” checking during A* search.

zone is the Zone instance where the tile is located. We added this for optimization purposes. We had to query the zone in a certain tile position in the old implementation which is slow and not very ideal.

Here’s the full class (removed function comments because “<text>” like these messes WordPress’ formatting):

public class Tile {

        private readonly TileGrid grid; // The parent grid
        private readonly Cell cell;

        private NodeHandle handle;

        // Note here that this is a map of layer and TileItem pair
        private Dictionary<string, TileItem> itemMap = new Dictionary<string, TileItem>();

        // Note here that task items are added to a separate map which uses its BuildItem as the key
        private Dictionary<string, TileItem> taskItemMap = new Dictionary<string, TileItem>();

        private readonly IntVector2 position;

        private int itemFlags;

        // May be null if tile is not in any zone
        private Zone zone;

        public Tile(TileGrid grid, Cell cell) {
            this.grid = grid;
            this.cell = cell;
            this.position = new IntVector2(cell.x, cell.y);
        }

        public void Add(TileItem tileItem) {
            if (tileItem.Data.WorkerTask) {
                Assertion.Assert(!this.taskItemMap.ContainsKey(tileItem.BuildItemData.TileLayer));
                this.taskItemMap[tileItem.BuildItemData.TileLayer] = tileItem;
            } else {
                Assertion.Assert(!this.itemMap.ContainsKey(tileItem.Layer)); // Should not contain the item yet
                this.itemMap[tileItem.Layer] = tileItem;

                // Add the flag as well
                this.itemFlags |= tileItem.Flags;
            }
        }

        public void Remove(string tileLayer, string tileItemId) {
            // Can't remove task items through this method
            // Use RemoveTask() instead
            Assertion.Assert(!TileLayers.TASKS.EqualsString(tileLayer));

            TileItem item = null;
            Assertion.Assert(this.itemMap.TryGetValue(tileLayer, out item)); // Item should exist

            Assertion.Assert(item.Data.ItemId.Equals(tileItemId));
            this.itemMap.Remove(tileLayer);
            Assertion.Assert(!Contains(tileLayer));

            // Recreate the flags from the existing items
            this.itemFlags = 0;
            foreach (KeyValuePair<string, TileItem> entry in this.itemMap) {
                this.itemFlags |= entry.Value.Flags;
            }
        }

        public void RemoveTask(string buildItemLayer, string tileItemId) {
            // Must be the layer of the built item
            Assertion.Assert(!TileLayers.TASKS.EqualsString(buildItemLayer));

            TileItem taskItem = this.taskItemMap.Find(buildItemLayer);
            Assertion.AssertNotNull(taskItem);

            Assertion.Assert(taskItem.Data.ItemId.Equals(tileItemId));
            this.taskItemMap.Remove(buildItemLayer);
        }

        public bool Contains(string layerName) {
            // Note that a task layer is no longer just one item
            // It's a layer of items by itself
            Assertion.Assert(!TileLayers.TASKS.Equals(layerName));
            return this.itemMap.ContainsKey(layerName);
        }

        public bool ContainsTask(string layerName) {
            // Note that a task layer is no longer just one item
            // It's a layer of items by itself
            Assertion.Assert(!TileLayers.TASKS.Equals(layerName));
            return this.taskItemMap.ContainsKey(layerName);
        }

        public TileItem GetItem(string layerName) {
            // Note that a task layer is no longer just one item
            // It's a layer of items by itself
            Assertion.Assert(!TileLayers.TASKS.Equals(layerName));

            TileItem item = null;
            this.itemMap.TryGetValue(layerName, out item);

            // This may return null
            // Client code should check for this
            return item;
        }

        public TileItem GetTaskItem(string layerName) {
            // Note that a task layer is no longer just one item
            // It's a layer of items by itself
            Assertion.Assert(!TileLayers.TASKS.Equals(layerName));
            return this.taskItemMap.Find(layerName);
        }

        public bool HasTaskItems {
            get {
                return this.taskItemMap.Count > 0;
            }
        }

        public TileItem GetTopTaskItem() {
            if(!HasTaskItems) {
                return null;
            }

            for(int i = 0; i < TileLayers.ORDERED_LAYERS.Length; ++i) {                 
                TileItem taskItem = GetTaskItem(TileLayers.ORDERED_LAYERS[i]);                 
                if(taskItem != null) {                     
                    return taskItem;                 
                }             
            }             
            return null;         
        }         

        public bool HasCharacterSlot {             
            get {                 
                return (this.itemFlags & TileItemLayout.CHARACTER_SLOT) > 0;
            }
        }

        public bool HasPhysicalBlocker {
            get {
                return (this.itemFlags & TileItemLayout.PHYSICAL_BLOCKER) > 0;
            }
        }

        public bool HasStudentBlocker {
            get {
                return (this.itemFlags & TileItemLayout.STUDENT_BLOCKER) > 0;
            }
        }

        internal NodeHandle Handle {
            get {
                return handle;
            }

            set {
                this.handle = value;
            }
        }

        public Cell Cell {
            get {
                return cell;
            }
        }

        public TileGrid Grid {
            get {
                return grid;
            }
        }

        public IntVector2 Position {
            get {
                return position;
            }
        }

        public Zone Zone {
            get {
                return zone;
            }

            set {
                zone = value;
            }
        }

        public bool Contains(Vector3 worldPosition) {
            return this.cell.Contains(worldPosition);
        }

    }

GOAP For Our New Game

I’m excited that we’re making a builder type of game in the likes of Prison Architect Banished, and Rimworld. I love playing such games. Our’s is a school management game where you can design classrooms, offices, hire teachers, design curriculum, and guide students to their educational success.

I’m excited that we’re making a builder type of game in the likes of Prison Architect Banished, and Rimworld. I love playing such games. Our’s is a school management game where you can design classrooms, offices, hire teachers, design curriculum, and guide students to their educational success.

currentgamescreenshot

For every new game, it’s always my aim to try to implement a new algorithm or system and learn something new. I’ve always been fascinated with an AI planning system called Goal Oriented Action Planning or GOAP. If you’re not familiar with it, here’s a simple tutorialI haven’t developed such system myself as the games that I’ve made so far have no use for it. I think it’s the perfect AI system for builder games. I hope I’m right.

Why GOAP

The primary reason is I’m lazy. I don’t want to wire and connect stuff like you do with Finite State Machines and Behaviour Trees. I just want to provide a new action and my agents will use it when needed. Another main reason is I’ve reckoned that there’s going to be a lot of action order combinations in the game. I don’t want to enumerate all of those combinations. I want the game agents to just discover them and surprise the player.

Another important reason is the AI system itself is an aide for development. There’s going to be lots of objects in the game that the agents may interact with. While I’m adding them one by one, I’ll just add the actions that can be done with the object and the agents will do the rest. I don’t have to reconfigure them much every time there’s a new action available. Just add the action and it’s done.

Some Tweaks

While making the system, I had some ideas that would make the generic GOAP system better. They sure have paid off.

Multiple Sequenced Actions

Per GOAP action, instead of doing only one action, our custom GOAP action contains a set of modular atomic actions. Each atomic action is executed in sequence. This is what it looks like in editor:

multipleactions

By doing it this way, I can make reusable atomic actions that can be used by any agent. A GOAP action then is just a named object that contains preconditions, effects, and a set of atomic actions.

GoapResult

I incorporated the concept of action results like how it is in Behaviour Trees. An atomic action execution returns either SUCCESS, FAILED, or RUNNING. This is what the atomic action base class looks like:

public abstract class GoapAtomAction {

    public virtual void ResetForPlanning(GoapAgent agent) {
    }

    public virtual bool CanExecute(GoapAgent agent) {
        return true;
    }

    public virtual GoapResult Start(GoapAgent agent) {
        return GoapResult.SUCCESS;
    }

    public virtual GoapResult Update(GoapAgent agent) {
        return GoapResult.SUCCESS;
    }

    public virtual void OnFail(GoapAgent agent) {
    }

}

When an atom action returns FAILED, the whole current plan fails and the agent will plan again. A RUNNING result means that the current action is still running, thus also means that the current plan is still ongoing. A SUCCESS result means that the action has done its execution and can proceed to the next atomic action. When all of the atomic actions returned SUCCESS, the whole GOAP action is a success and the next GOAP action in the plan will be executed.

This concept makes it easy for me to add failure conditions while an action is being executed. Whenever one action fails, the agent automatically replans and proceeds to execute its new set of actions.

Condition Resolver

Condition Resolvers are objects that can query current world conditions which you need during planning. I implemented this as another base class in our system. The concrete classes can then be selectable in the editor. This is what the base class looks like:

public abstract class ConditionResolver {

    private bool resolved;
    private bool conditionMet;

    public ConditionResolver() {
        Reset();
    }

    public void Reset() {
        this.resolved = false;
        this.conditionMet = false;
    }

    public bool IsMet(GoapAgent agent) {
        if(!this.resolved) {
            // Not yet resolved
            this.conditionMet = Resolve(agent);
            this.resolved = true;
        }

        return this.conditionMet;
    }

    protected abstract bool Resolve(GoapAgent agent);

}

Note here that it has logic such that Resolve() will only be invoked once. Concrete subclasses need to only override this method. Such method may execute complex calculations so we need to make sure that it’s only called once when needed during planning.

This is what it looks like in editor:

conditionresolvers

All conditions default to false unless they have a resolver which is used to query the actual state of the condition.

Usage

Once the conditions, resolvers, and actions have been set up, all that’s left to do is to add goal conditions and invoke Replan().

void Start() {
    this.agent = GetComponent();
    Assertion.AssertNotNull(this.agent);

    // Start the AI
    this.agent.ClearGoals();
    this.agent.AddGoal("StudentBehaviour", true);
    this.agent.Replan();
}

If there are new goals to satisfy, the same calls can be invoked to change the goal(s) for a new plan to be executed.

So Far So Good

Our custom GOAP system is working well for us… for now. I now have working worker agents and student agents. More will be added. Here’s hoping that we don’t need to revamp the system as we’re already so deep with it.