Source file unity-csharp/game-ai/utility-ai/UtilityAction.cs from the GDnD code examples. Download raw file

// GDnD wiki example
// Demonstrates: utility AI building blocks - considerations with AnimationCurve response curves, multiplied into an action score
// Related pages: [[utility-ai]], [[utility-score]], [[game-ai-agent-design]]
 
using System;
using System.Collections.Generic;
using UnityEngine;
 
// Everything the villager can "sense", each already normalised to 0..1 by the agent.
public enum UtilityInput { Hunger, Tiredness, NeedMoney, HasMoney, ThreatNearby }
 
public enum ActionKind { Eat, Sleep, Work, Flee, Idle }
 
// One question an action asks about the world, e.g. "how hungry am I?".
// The response curve turns the raw 0..1 input into a 0..1 score, so a designer can
// say "hunger barely matters until it is high" by bending a curve, not editing code.
[Serializable]
public class Consideration
{
    public UtilityInput input;
    public AnimationCurve curve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
 
    public Consideration() { }                 // used by the Inspector's "+" button
 
    public Consideration(UtilityInput input, AnimationCurve curve)
    {
        this.input = input;
        this.curve = curve;
    }
 
    public float Score(Func<UtilityInput, float> sense) =>
        Mathf.Clamp01(curve.Evaluate(Mathf.Clamp01(sense(input))));
}
 
[Serializable]
public class UtilityAction
{
    public string name;
    public ActionKind kind;
    public Transform location;                 // where the action happens (unused by Flee)
    [Range(0f, 2f)] public float weight = 1f;  // designer bias for the whole action
    public List<Consideration> considerations = new List<Consideration>();
 
    // Multiplying means any consideration scoring 0 vetoes the action ("no money,
    // no food"). The catch: every extra consideration below 1 drags the total down,
    // so an action with four considerations is penalised against one with one.
    // The geometric mean (nth root of the product) is one way to compensate.
    public float Score(Func<UtilityInput, float> sense, bool compensate)
    {
        float product = 1f;
        foreach (Consideration c in considerations)
        {
            product *= c.Score(sense);
            if (product <= 0f) return 0f;      // early out: vetoed
        }
 
        int n = considerations.Count;
        if (compensate && n > 1)
            product = Mathf.Pow(product, 1f / n);
 
        return product * weight;
    }
}