top of page
Writer's pictureNostalgiq

RPG Builder Mod: Visualizing Your NPC AI Behavior through Gizmos

Updated: Feb 7

Intro

This mod gives you some visual information about the AITemplate assigned to your NPCs. As of right now this code depends on you having already done the mod that adds AI Overrides to the NPC Spawner. You have to start there if you want to use this mod in your version of RPGB. It would be possible to do this without that mod, but you would need to pull the AI templates from the assigned mobs, not from the Override.


Right now in RPGB there are three main ways to initialize your AI's default behavior, and these three handle most use cases you could want from your NPCs:

  • Idle

  • Roaming

  • Patrol

Each of these comes with a corresponding template (these are the filenames, so search your Project and you'll find them):

  • IdleTemplate

  • RoamingTemplate

  • PatrolTemplate

These three templates share four important fields:

  • View Angle: How wide can your NPC see? - Think of this as their peripheral vision

  • View Distance: How far can your NPC see? - Think of this as how far they can see :D

  • Auto Aggro Radius: How close can you get before the AI aggros you from any angle?

  • Movement Speed Modifier: How fast does the AI move in this state? It's a float, so a value of 0.50 means they walk at half speed, etc


Within the Roaming and Patrol templates you have some additional values, which are unique to those templates, such as Roam Radius or Pause Duration, which could also be worked into the spawners visually if you desired. I leave the Roaming Radius out of the code below for you to add as an exercise, if you so choose.


Here's another screenshot of how it helps you visualize your mobs in the scene:

The red circle is the aggro radius of the mob, the teal cones are their vision fields, and the green circles are their roam distances.


For me visualizing this additional information has been very helpful, just remember that if you have a ton of mobs in the scene you probably want to disable gizmos, but for individual debugging, or small sets of mobs, this works great.


Finally, check out a video of it in action. If you want this in your version of RPGB the code is below.


Disclaimer: There is no guarantee or future support offered regarding these changes. I tested them out at the time I wrote them, and they worked within the scope of my needs. However, I am not the author of RPGB and therefore am not the authoritative source on whether these mods are complete. Use at your own risk, and always backup your projects.


Modded RPGB v2.0.6. New code additions are always highlighted in green, whilst RPGB code is always highlighted in blue.


Mod NPCSpawnerInspector.cs

Near the top of the file, in the OnInspectorGUI() method, add the following code. This is adding boolean toggles to the Editor so we can turn the gizmos on and off as we desire.

...
GUIStyle removeButtonStyle = EditorSkin.GetStyle("SquareRemoveButtonSmallInspector");
GUIStyle addButtonStyle = EditorSkin.GetStyle("SquareAddButtonSmallInspector");

GUILayout.Space(5);
GUILayout.Label("Gizmo Settings", SubTitleStyle);
GUILayout.Space(5);
Spawner.hideSpawnerGizmosInPlayMode = EditorGUILayout.Toggle("Hide Gizmos in Play Mode?", Spawner.hideSpawnerGizmosInPlayMode);
GUILayout.Space(5);
Spawner.showSpawnerSpawnRadiusGizmo = EditorGUILayout.Toggle("Show Spawn Radius?", Spawner.showSpawnerSpawnRadiusGizmo);
Spawner.showSpawnerVisionGizmo = EditorGUILayout.Toggle("Show Vision Cone?", Spawner.showSpawnerVisionGizmo);
Spawner.showSpawnerAggroGizmo = EditorGUILayout.Toggle("Show Aggro Radius?", Spawner.showSpawnerAggroGizmo);

GUILayout.Space(5);
GUILayout.Label("Spawner Type", SubTitleStyle);
GUILayout.Space(5);
...

Mod NPCSpawner.cs

Next we work on the NPCSpawner script which is responsible for drawing the gizmos around the NPC. By default the only gizmo that's drawn is the Spawn Radius if it's a non-positional spawner. This helps you visualize the range where NPCs can spawn.


For our first change add these booleans which track the value of the toggles placed in the section above.

...
public class NPCSpawner : MonoBehaviour
{
    public NPCSpawnerSaver Saver;
    public AIData.SpawnerType spawnerType;
    public bool IsActive;
    public bool hideSpawnerGizmosInPlayMode = true;
    public bool gameIsRunning;
    public bool showSpawnerSpawnRadiusGizmo = true;
    public bool showSpawnerVisionGizmo = true;
    public bool showSpawnerAggroGizmo = true;
    public List<Coroutine> SpawningCoroutines = new List<Coroutine>();

    [Serializable]
    ...

Next we're going to completely replace the current implementation of OnDrawGizmos(). You can go ahead and comment out (it's best for you to comment the original code in case something goes wrong with the mod) and replace it with the entire section below.

public void DrawSpawnerRadius()
{
    if (areaHeight < 1) areaHeight = 1;
    if (usePosition)
    {
        Gizmos.color = Color.cyan;
        Gizmos.DrawWireSphere(transform.position, 0.5f);
        
        Vector3 origin = transform.position;
        Vector3 start = transform.right * areaRadius;
        Vector3 lastPos = origin + start;
        float angle = 0;
        int iter = 0;
        while (angle <= 360 & iter < 1000)
        {
            angle += 360 / 50f;
            Vector3 nextPosition = origin + (Quaternion.Euler(0, angle, 0) * start);

            Gizmos.color = gizmoColor;
            Gizmos.DrawLine(lastPos, nextPosition);
            Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + areaHeight, lastPos.z),
                new Vector3(nextPosition.x, nextPosition.y + areaHeight, nextPosition.z));
            lastPos = nextPosition;
            iter++;
        }
    }
    else
    {
        Vector3 origin = transform.position;
        Vector3 start = transform.right * areaRadius;
        Vector3 lastPos = origin + start;
        float angle = 0;
        int iter = 0;
        while (angle <= 360 & iter < 1000)
        {
            angle += 360 / 50f;
            Vector3 nextPosition = origin + (Quaternion.Euler(0, angle, 0) * start);

            Gizmos.color = gizmoColor;
            Gizmos.DrawLine(lastPos, nextPosition);
            Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + areaHeight, lastPos.z),
                new Vector3(nextPosition.x, nextPosition.y + areaHeight, nextPosition.z));

            Gizmos.color = lineColor;
            Gizmos.DrawLine(nextPosition,
                new Vector3(nextPosition.x, nextPosition.y + areaHeight, nextPosition.z));
            lastPos = nextPosition;
            iter++;
        }
    }
}

public void DrawAIAutoAggroRadius(Transform target)
{
    Gizmos.color = Color.red;
    Vector3 origin = target.position;
    Vector3 start = target.right * AIStateIdleTemplateDefault.AutoAggroDistance;
    Vector3 lastPos = origin + start;
    float angle = 0;
    int iter = 0;
    while (angle <= 360 & iter < 1000)
    {
        angle += 360 / 50f;
        Vector3 nextPosition = origin + (Quaternion.Euler(0, angle, 0) * start);
        
        Gizmos.DrawLine(lastPos, nextPosition);
        Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + 20, lastPos.z),
            new Vector3(nextPosition.x, nextPosition.y + 20, nextPosition.z));
        
        Gizmos.DrawLine(nextPosition,
            new Vector3(nextPosition.x, nextPosition.y + 20, nextPosition.z));
        lastPos = nextPosition;
        iter++;
    }
}

public void DrawAIViewDistance(Transform target)
{
    Gizmos.color = Color.cyan;
    Vector3 origin;
    Vector3 lastPos;
    Vector3 lookingDir;
    float angleDelta = AIStateIdleTemplateDefault.viewAngle / 10f;
    float viewAngleDelta = AIStateIdleTemplateDefault.viewAngle;
    float iterAngle;

    origin = new Vector3(target.transform.position.x, target.transform.position.y, target.transform.position.z);
    iterAngle = target.transform.rotation.y - viewAngleDelta;
    lookingDir = target.transform.forward * AIStateIdleTemplateDefault.viewDistance;
    
    lastPos = origin + Quaternion.AngleAxis(-viewAngleDelta, Vector3.up) * lookingDir;
    Gizmos.DrawLine(origin, new Vector3(origin.x, origin.y + 20, origin.z)); // line origin up
    Gizmos.DrawLine(origin, lastPos); // line across ground
    Gizmos.DrawSphere(lastPos, 0.5f);
    Gizmos.DrawLine(new Vector3(origin.x, origin.y + 20, origin.z), new Vector3(lastPos.x, lastPos.y + 20, lastPos.z)); // line across top
    Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + 20, lastPos.z), new Vector3(lastPos.x, lastPos.y + 20, lastPos.z)); // line between
    Gizmos.DrawLine(lastPos, new Vector3(lastPos.x, lastPos.y + 20, lastPos.z)); // line up
    int iter = 0;
    while (iter < 1000 & iterAngle <= viewAngleDelta - angleDelta)
    {
        iterAngle += angleDelta;
        Vector3 nextPosition = origin + Quaternion.AngleAxis(iterAngle, Vector3.up) * lookingDir;
        
        Gizmos.DrawLine(lastPos, nextPosition); // connect lines across bottom
        Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + 20, lastPos.z), new Vector3(nextPosition.x, nextPosition.y + 20, nextPosition.z)); // connect lines across top
        Gizmos.DrawLine(nextPosition, new Vector3(nextPosition.x, nextPosition.y + 20, nextPosition.z));  // draw lines straight up
        lastPos = nextPosition;
        iter++;
    }
    Gizmos.DrawLine(origin, lastPos);
    Gizmos.DrawLine(new Vector3(origin.x, origin.y + 20, origin.z), new Vector3(lastPos.x, lastPos.y + 20, lastPos.z));
    Gizmos.DrawLine(new Vector3(lastPos.x, lastPos.y + 20, lastPos.z), new Vector3(lastPos.x, lastPos.y + 20, lastPos.z));
    Gizmos.DrawLine(lastPos, new Vector3(lastPos.x, lastPos.y + 20, lastPos.z));
    Gizmos.DrawSphere(lastPos, 0.5f);
}

private void OnDrawGizmos()
{

    if (showSpawnerSpawnRadiusGizmo)
    {
        if (!hideSpawnerGizmosInPlayMode | !gameIsRunning)
        {
            DrawSpawnerRadius();      
        }
    }
    
    var overallNpcList = CurrentNPCs.Concat(CurrentPersistentNPCs).ToList();
    List<Transform> targets = new List<Transform>();
    if (overallNpcList.Count > 0)
    {
        for (int i = 0; i < overallNpcList.Count + 1; i++)
        {
            if (i == overallNpcList.Count)
            {
                if (!hideSpawnerGizmosInPlayMode) targets.Add(transform);
            }
            else
            {
                targets.Add(overallNpcList[i].transform);                        
            }
        }
    }
    
    if (targets.Count > 0)
    {
        foreach (Transform target in targets)
        {
            if (showSpawnerAggroGizmo) DrawAIAutoAggroRadius(target);
            if (showSpawnerVisionGizmo) DrawAIViewDistance(target);
        }
    }
    else
    {
        if (showSpawnerAggroGizmo) DrawAIAutoAggroRadius(transform);
        if (showSpawnerVisionGizmo) DrawAIViewDistance(transform);
    } 
}

The End

Next steps could be to have a global toggle for all the NPC Spawner gizmos instead of having it tied to each individual NPCSpawner.


One thing to keep in mind is that drawing gizmos on the screen can impact your performance. The screenshot below shows the performance impact of drawing all these on the screen. There's no optimization in this code to handle the performance impact, so just be mindful of that as you move forward. In practice you shouldn't notice any performance impact unless you're drawing a TON of these on the screen.


Comments


Commenting has been turned off.
bottom of page