Enemy Internal Design Guidelines - Orhu/Summer2023Project GitHub Wiki

Quick Reference

Finite State Machine and its Components

This will be exploring the current framework of the finite state machine we are using for enemies in Cardificer. This page will be updated as needed to reflect any new changes in the state machine's workings. Refer here when designing enemy behaviors.

State

A state refers to a "state of being" that an enemy is currently in. For example, a rat might have a "Wander" state where it randomly wanders around the room. State consists of:

  • Actions on enter
  • Actions on update (every frame)
  • Actions on exit
  • Transitions (evaluated every frame AFTER actions are performed)

Transition

A transition is how the state machine moves between states. All transitions attached to a state will be evaluated every frame after all Actions are performed. The order of events is On State Enter > On State Update > Transitions > On State Update > Transitions > ... > On State Exit.

Transition consists of:

  • Decision to evaluate
  • True state
  • False state

Decision

A decision is simply a condition to evaluate. We nicely package it as a "Decision" object so that we can plug it into the state machine without ever doing any scripting.

Decision consists of:

  • Evaluates a condition, returns true or false

Action

An action is pretty self-explanatory. It is an action this unit will perform. As I was designing actions, there were some scenarios where I only wanted to do an action if a condition was met at the time. That is where the second type of action comes in: the conditional action.

Two Types of Actions:

  • Conditional Action has:
    • Decision to evaluate
    • Action if true (can be nothing)
    • Action if false (can be nothing)
  • Single Action has:
    • Action to perform

Other Notes

Here are a few other quirks of the system to keep in mind:

The "Remain in State" State

Consider the following two Transitions:

Attack to Chase (Transition 1):

  • Decision: Something happened?
  • If true, switch to Chase
  • If false, switch to Attack

Attack to Chase (Transition 2):

  • Decision: Something happened?
  • If true, switch to Chase
  • If false, Remain in State

In this scenario, the difference between Transition 1 and Transition 2 is the false case.

In Transition 1, if we evaluate false on the Decision, then we explicitly switch to the Attack State despite it being the State we are already in. This would re-run the Attack State's On Enter Action list, since we are still performing a switch, even if it is from the same State as it is going to.

In Transition 2, if we evaluate false on the Decision, then we use a "Remain in State" keyword. "Remain in State" is how we indicate we want to continue in this state without re-running all of the On State Enter Actions.

Generally speaking, as a rule of thumb, most of the time you will be using Remain in State. Explicitly switching to the same state you are already in should only be done in certain edge cases where it is important for the enemy's logic to work correctly.

Targeting

As of build 0.1.0.0, enemies use the same target for pathfinding and targeting. This is behavior that will change as I modify the state machine system. Enemies currently use different targets for pathfinding and attacking.

When designing new enemies, specifically mention whether you want to set the "Pathfinding Target" or the "Attack Target". Generally speaking, Pathfinding Target should always target feet (since that is where the collider that can be pushed is), and Attack Target should always target center since you want enemies to shoot at the Player's center.

When talking about targeting something else, such as a tile, be as specific as possible so I can understand the behavior you are looking for. Here are some examples of what I mean:

Bad: Target Player Feet (Note: Attack or Pathfinding? Since it is feet, I would likely assume pathfinding)

Good: Pathfinding-Target Player Feet (Note: This version removes any assumptions and specifically tells me what you want the enemy to aim at)

Bad: Target Player Center (Note: Attack or Pathfinding? Since it is center, I would likely assume attack)

Good: Attack-Target Player Center (Note: This version removes any assumptions and specifically tells me what you want the enemy to aim at)

Bad: Pathfinding-Target Random Tile (Note: In this case, I would likely assume you want this unit to path to the tile's center)

Good: Pathfinding-Target Random Tile Bottom-Left (Note: This version removes the guesswork and provides a specific location to path towards)

Implementing Enemies & Reusing Behaviors

Every component of the finite state machine is a scriptable object. This means that a great deal of enemy logic, including all States and Transitions, can be implemented without ever touching a script.

The only coding that would be required is for any Actions or Decisions that do not already exist from another enemy. However, many Actions are very generic such as "Target Player" which likely would already exist as a ready-to-create scriptable object.

As new Actions/Decisions are coded, they will be added to the Unity right-click menu under FSM (stands for Finite State Machine)

Actions and Decisions that have been coded previously can be reused. Likewise, States and Transitions or any other component of the state machine can be re-used based on another enemy if it exists.

In that case, simply writing something along the lines of:

"Chase State:

  • Same as Rat"

would suffice. However, keep in mind that, in this case, the rat has no other states besides chase so you will need to specify a new transition if this enemy uses other states. Otherwise, the state would exist but would have no way to be exited/entered for that new enemy.

Final Words

The idea of me writing this up is that it takes the guesswork out of translating English behaviors from the wiki into specific behaviors the state machine can support. Oftentimes the longest development time on enemies is simply figuring out how to translate the English description of an enemy's behavior into this framework and consulting designers on how the enemy should behave. This creates a constantly moving target for implementing enemies: If an implementor misinterprets or doesn't have a clear idea of what a behavior is, the designer needs to give feedback, then it needs to be changed, then more feedback, etc.

Instead, I think it would be best if whoever creates the enemy initially (and therefore has the vision of how they want it to behave) to build out one of these state-machine-style behavior trees. This should drastically cut down on implementation time by significantly reducing or eliminating how much back-and-forth and reworking is needed before an enemy behaves how the designer wants it to. It also means that any behaviors that don't work as desired are fully understood by designers, and it becomes their wheelhouse to figure out how to make their desired behaviors work within this framework.

Basically, if the wiki is a player-facing manual on the enemy's behavior, this serves as the internal-facing manual on the enemy's behavior.

An Example

Here is an example of how the Goblin currently works in build 0.1.0.0, and what I'd be looking for to implement a new enemy. When creating enemies, try to follow a similar notation and be as specific as possible when creating any new Actions or Decisions.

Goblin, starts in Idle State:
	Attack State:
		On Update (every frame):
			1. Single Action: Attack-Target Player Center
			2. Single Action: Fire Attack
		On Exit:
			1. Single Action: Pathfinding-Target Player Feet
		On Enter:
			Nothing
		
		Transitions:
			1. Attack to Chase
				- Decision: Do I have line of sight (max 4 tiles range) to the player?
				- If true, Remain in State
				- If false, switch to Chase State
		
	Chase State:
		On Update (every frame):
			1. Conditional Action: 
				- Decision: Have I reached my previous pathfinding-targeted destination?
				- If true, Single Action: Pathfinding-Target Player Feet
				- If false, do nothing
			2. Single Action: Walk to Pathfinding-Target
		On Exit:
			Nothing
		On Enter:
			Nothing
			
		Transitions:
			1. Chase to Idle 
				- Decision: Is Player within 4 tiles of me?
				- If true, Remain in State
				- If false, switch to Idle State
			2. Chase to Attack
				- Decision: Do I have line of sight (max 4 tiles range) to the player?
				- If true, switch to Attack State
				- If false, Remain in State
	
	Idle State:
		On Update (every frame):
			1. Conditional Action:
				- Decision: Have I reached my previous-pathfinding-targeted destination?
				- If true, Pathfinding-Target Random Tile
				- If false, do nothing
			2. Single Action: Walk to Pathfinding-Target 
		On Exit:
			Nothing
		On Enter:
			Nothing
			
		Transitions:
			1. Idle to Chase
				- Decision: Is Player within 4 tiles of me?
				- If true, switch to Chase State
				- If false, Remain in State

Based on this, it is easy to create a list of all the total actions/decisions that will be needed to create this enemy:

Decisions:

  • Have I reached my previous-pathfinding-targeted destination?
  • Do I have line of sight (max 4 tiles range) to the player?
  • Is Player within 4 tiles of me?

Actions:

  • Fire Attack
  • Walk to Pathfinding-Target
  • Various Target-setting that can be abstracted to a single scriptable object, with various serialized fields for specific targeting settings

With the behavior tree made, it is simply up to the programmer to ensure all the requested Decisions/Actions are implemented. Once that is done, it becomes trivial to actually implement the enemy (clicking and dragging scriptable objects in editor to link them together), and either the designer or the programmer could easily do the task with no room for misinterpretation at any point along the way.