As far as introductions go this project really doesn't need one. The plan was to tease a few features (used and unused in final build). The project itself isn't the main focus and it's instead more about features that were made throughout the December 2024 Tech Jam.
A ton of features were just not included in the final build and even finished features were rushed in a last ditch effort to get something out the door. A few things like simulated military ranks, NPC persistence, a decision component that other NPCs can use to query a decision for a given situation are a few things that are super cool and I was excited to show off that I just did not spend enough time on to actually put in the final build.
Targeting and Awareness is generally where the heavy lifting of most game NPCs happen so improving those behaviors should be a priority. In "Aim Improvements" I used Second Order Dynamics (a system that uses a math formula known as Second-Order ODE) to simulate these behaviors. Long story short the NPC aims more akin to natural player movements.
Here is a spectator view of uninterpolated NPC aim compared to using Second Order Dynamics As far as awareness goes there's generally a few methods used for NPCs, with scene based engines like Unity Engine NPCs will either use triggers to find all NPCs in an area then filter them out with a line-of-sight raycast and/or find objects with a component or tag. This method works and can be optimized further in the larger scale by even incorporating chunks so only nearby entities are searched for but there's just a lot to be desired.
Here I'm experimenting with a method I've only seen in a random indie game devlog (expanded much further to fit my preferences of course). This system instead uses raycasts which on their own are very efficient but still have a cost so it's good to still keep optimization in mind. As many lines being drawn I run the checks on FixedUpdate instead of the standard Update loop for frame stability and I'm only checking for collisions to further improve optimization, even with a very heavily dense set of rays I can still run ~20 NPCs (all awake so still running all of their logic) before dipping below 60 fps.
The real power comes from what you can detect from patterns in the rays. Unfortunately I didn't get around to it but the original plan was for this to both help object detection and dynamic navigation. If I were to make it so I use standard NPC object detection I would be able to significantly reduce the amount of rays used.
I'm hoping someone reads this and tries tackling doing completely dynamic locomotion using raycasts. I love navmesh and since the navmesh in S&Box is really quick to update there's no real reason to take on a project like this other than for the pure fun aspect of it. There are still imperfections when it comes to hammer and navmesh so maybe doing a hybrid system could be promising
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()
{
}
}
}
This is another feature that never made it into the final project but is worth noting. A really big thing that makes NPCs immersive is when they are essentially just a randomized clone. I spent a few days working on a script that would house relatively lightweight information on a Battalion. I indented for this project to be more freeform/fun to play with and I wanted there to be a tracking on how much damage you did to different groups/if they need to send in a new battalion
While I haven't played many open world games I can really apricate the persistence of important and semi-important NPCs.
I created a SoldierData structure that maintains whether or not a soldier is spawned, alive, the visual data, and some function data as well. The idea is that either a soldier prefab can either be spawned around these values or an already spawned soldier prefab that isn't bound to an identity could be then bound to a similar/identical preexisting identity.
public struct SoldierData
{
public Rank rank;
public HumanData.Ethnicity ethnicity;
public HumanData.Name name;
public HumanData.Gender gender;
public bool isAlive = true;
public bool spawned { get { return soldierInstance.IsValid(); } }
//private bool isSpawned = false;
public AiSoldier soldierInstance { get { return _soldierInstance; } set { _soldierInstance = value; } }
private AiSoldier _soldierInstance;
public SoldierData( HumanData.Name name, Rank rank, HumanData.Gender gender, HumanData.Ethnicity ethnicity, AiSoldier soldierInstance = null )
{
this.name = name;
this.rank = rank;
this.gender = gender;
this.ethnicity = ethnicity;
if ( soldierInstance != null )
{
this._soldierInstance = soldierInstance;
//isSpawned = true;
}
}
}
A lot didn't make it into the game and even more never even got worked on. All the scenes and the menu were made within 24 hours since I just straight up forgot I needed to make something presentable so the project is definitely also lacking in the quality department. I was also 100% overambitious when it came to what I wanted to put into this project. For example, I wanted to make an active ragdolls tool so people would use more active ragdolls in their project since active ragdolls don't have to be floppy like in TABS and can actually animate near identically to the base animation depending on your setup.
I strongly believe there's a lot of cool tech people are too intimidated to peruse and this project is mainly intended to either spark inspiration or show some things you can add in a day or two to give your game a more carved out identity by using interesting tech rather than having to rely on a unique appearance.
Comments