An (In Depth) Introduction to Better NPCs
Posted 2 days ago
Plenty of NPCs feel very robotic and I would argue a good 90% of that doesn't come down to animations, scene context, nor lack of personality (even though some of those definitely contribute). A lot of redundancy in a NPC in general comes down to their thought loop. I'm focusing more on assailants/combat oriented NPCs but this concept 100% applies to friendly quest NPCs, companions, and hide n seek oriented NPCs.

One of the few things from this section that made it into the final project was the object identification loop.

I know it looks very unnecessary but on its own adds things like reaction time or even stops the NPC from identifying threats that it could not have had time to properly identify. Since the eye script applied on a child of the head bone moving the head toward the player is part of its functionality it means the head will also look at objects the NPC is trying to identify whether that be through clearing a room, noticing the player, investigating a falling object, bumping into each other, or even distractions in a stealth operation.

Additionally objects are also remembered for a short period after being unseen and after an even longer time are completely forgotten, this prevents the NPC from constantly being distracted by the same object while also preventing the NPC from being unphased by something it saw last year.

From just changing how the NPC identifies objects adds a cool bit of immersion that was entirely unintended.
Last thing worth noting here is I was working on a script called AiBrain.cs. The idea behind it was to be a decision making hub for all npcs so they could either query their own interests based on certain priorities compared to the action or instead choose to inquire another NPC. I didn't spend enough time to thoroughly investigate this shower though but here's the WIP script incase anyone else is interested in experimenting with the idea:
using Sandbox;
using System;

public sealed class AiBrain : Component
{
	[Property] public Dictionary<string, AiAction> actions = new Dictionary<string, AiAction>();
	[Property] public List<IPriority> priorities = new List<IPriority>();
	public bool? GetDecision(string action, List<object> context)
	{
		AiAction _action = actions[action];
		if ( _action != null )
		{
			float approval = 0;
			foreach ( IPriority priority in priorities )
			{
				foreach ( object _context in context )
				{
					int approv = priority.GetApproval( _context ).AsInt();

					if ( approv < _action.minRequiredApproval ) return false;
					if ( approv > _action.maxRequiredApproval ) return true;

					approval += approv;
				}
			}

			if ( priorities.Count * context.Count > 0 )
				approval /= priorities.Count * context.Count;

			if ( approval > _action.requiredApproval ) return true;

			return false;
		}

		return null;
	}

	public abstract class AiAction
	{
		// required median approval (equal or above) to get the decision to approve
		public float requiredApproval = 1;

		// Anything below this will cause the decision to immediately dissaprove
		public int minRequiredApproval = -1;

		// Anything above this will cause the decision to immediately approve
		public int maxRequiredApproval = 2;
	}

	public enum PriorityApproval
	{
		StronglyDisapprove = -2,
		Disapprove = -1,
		Impartial = 0,
		Approve = 1,
		StronglyApprove = 2,
	}

	public class IPriority : Object
	{
		public virtual PriorityApproval GetApproval( object context )
		{
			//if(context != null && typeof( AiContext ).IsAssignableFrom( context.GetType() )) return PriorityApproval.Approve;
			return PriorityApproval.Impartial;
		}

		public IPriority()
		{

		}
	}
}