# 5. Running Trained Agents In-Game Part of [[ML-Agents and Inference Engine]]. Prev: [[4. Training with mlagents-learn]]. Next: [[6. Sentis Directly for Any ONNX Model]]. ## 5.1 Inference Only setup On the agent's **Behavior Parameters**: 1. **Model**: drag `CoinRunner_Final` (a `ModelAsset`). 2. **Behavior Type**: **Inference Only** (Default also works when no trainer is running, but Inference Only makes intent explicit and avoids an accidental trainer connection in class). 3. **Inference Device**: **Default** (the 4.2 API says Default currently maps to Burst) or **Burst** explicitly. The ML-Agents Sentis page notes that for most ML-Agents models CPU inference is faster than GPU, because the networks are tiny and GPU dispatch and readback cost more than the math. Use **Compute Shader** only for large visual-observation models, and measure. 4. **Deterministic Inference**: on for a demo you want to replay identically (picks the most likely action instead of sampling); off for more lifelike variation. Press Play: no Python, no terminal. Make a build to prove it. ## 5.2 Difficulty brains with SetModel `Agent.SetModel(string behaviorName, ModelAsset model, InferenceDevice inferenceDevice = InferenceDevice.Default)` swaps the policy at runtime. Earlier checkpoints are naturally worse players, so they are an honest difficulty dial (no rubber-banding hacks). ```csharp using System; using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Policies; using Unity.InferenceEngine; // ModelAsset /// <summary> /// Picks one of several trained models for every agent it manages, from a 0..1 skill value. /// Feed skill from your dynamic-difficulty system (see Game Balance and Dynamic Difficulty). /// </summary> public sealed class AgentBrainDirector : MonoBehaviour { [Serializable] public struct Brain { public string label; // "Easy", "Medium", "Hard" public ModelAsset model; [Range(0f, 1f)] public float minSkill; // choose the highest brain whose minSkill <= skill } [SerializeField] string behaviorName = "CoinRunner"; [SerializeField] Brain[] brains; // order by minSkill ascending [SerializeField] InferenceDevice device = InferenceDevice.Default; [SerializeField] Agent[] agents; [SerializeField, Range(0f, 1f)] float skill = 0.5f; int _current = -1; public string CurrentLabel => _current >= 0 ? brains[_current].label : "(none)"; void Start() => Apply(force: true); public void SetSkill(float value) { skill = Mathf.Clamp01(value); Apply(force: false); } void Apply(bool force) { int pick = 0; for (int i = 0; i < brains.Length; i++) if (brains[i].minSkill <= skill) pick = i; if (!force && pick == _current) return; _current = pick; foreach (var a in agents) { if (a == null) continue; a.SetModel(behaviorName, brains[pick].model, device); } Debug.Log(quot;[AgentBrainDirector] skill {skill:0.00} -> {brains[pick].label}"); } void OnValidate() { if (Application.isPlaying && brains != null && brains.Length > 0) Apply(force: false); } } ``` Notes: - `SetModel` requires the new model to match the agent's observation and action spec; checkpoints from the same run always do. - Swapping mid-episode is fine for this agent (it has no memory). For agents with LSTM memory, swap at episode boundaries. - Hook `SetSkill` to whatever your DDA system produces ([[Game Balance and Dynamic Difficulty]]), or expose it as an Easy/Normal/Hard option in a settings menu ([[Accessibility]]). ## 5.3 Wrapping the agent so it reads like a designed enemy A raw policy has no wind-up and no tells. Rogers' enemy rules (Level 11 in [[Level Up! (Review)]]) want both. Put a tiny state machine *around* the agent, like the one in [[Enemy AI State Machine]]: the network drives only the **Chase** state. ```csharp using UnityEngine; using Unity.MLAgents; /// <summary> /// Gates a trained agent: Idle (agent off), Chase (agent decides), Telegraph (stop + wind-up), Cooldown. /// The network never decides when to attack; the designer does. /// </summary> public sealed class AgentStateGate : MonoBehaviour { enum State { Idle, Chase, Telegraph, Cooldown } [SerializeField] Agent agent; [SerializeField] DecisionRequester decisionRequester; [SerializeField] Rigidbody body; [SerializeField] Transform player; [SerializeField] float noticeRange = 12f; [SerializeField] float attackRange = 2f; [SerializeField] float telegraphTime = 0.6f; [SerializeField] float cooldownTime = 1.2f; [SerializeField] Animator animator; // optional: "Telegraph" trigger [SerializeField] AudioSource telegraphSound; // optional State _state = State.Idle; float _timer; void Start() => Enter(State.Idle); void Update() { float d = Vector3.Distance(transform.position, player.position); _timer -= Time.deltaTime; switch (_state) { case State.Idle: if (d < noticeRange) Enter(State.Chase); break; case State.Chase: if (d > noticeRange * 1.5f) Enter(State.Idle); else if (d < attackRange) Enter(State.Telegraph); break; case State.Telegraph: if (_timer <= 0f) { DoAttack(); Enter(State.Cooldown); } break; case State.Cooldown: if (_timer <= 0f) Enter(State.Chase); break; } } void Enter(State s) { _state = s; bool agentDrives = s == State.Chase; // Stop requesting decisions outside Chase so the policy cannot move the body. decisionRequester.enabled = agentDrives; if (!agentDrives) body.linearVelocity = new Vector3(0f, body.linearVelocity.y, 0f); if (s == State.Telegraph) { _timer = telegraphTime; if (animator) animator.SetTrigger("Telegraph"); if (telegraphSound) telegraphSound.Play(); } else if (s == State.Cooldown) _timer = cooldownTime; } void DoAttack() { // Spawn a hitbox / call your combat system here (see Combat Feel and Hit Feedback). Debug.Log(quot;{name} attacks"); } } ``` For this the agent must be trained with the **player** as its target instead of a coin (swap `coin` for the player transform in a copy of the agent, or train it on a moving coin). Disabling `DecisionRequester` means no new decisions; with "Take Actions Between Decisions" the last action could still be repeated, which is why the gate also zeroes velocity. Verify in play mode that the agent truly stops; if not, also set a flag the agent checks at the top of `OnActionReceived`. ## 5.4 Cost - Each decision runs one forward pass. With Decision Period 5 and 20 agents, that's about 200 inferences per second of tiny networks on Burst; usually trivial, but measure in the Profiler ([[Profiling and Optimization]]). - Agents that share a model are batched by ML-Agents per decision step. - On mobile or Quest (VR on Unity 6.3 LTS, see [[5.Quest 2 Performance and Rendering]]), keep Decision Period high and agent counts low, and test on device. ## Sources - `Agent.SetModel`: https://docs.unity3d.com/Packages/[email protected]/api/Unity.MLAgents.Agent.html - `BehaviorParameters`: https://docs.unity3d.com/Packages/[email protected]/api/Unity.MLAgents.Policies.BehaviorParameters.html - `InferenceDevice`: https://docs.unity3d.com/Packages/[email protected]/api/Unity.MLAgents.Policies.InferenceDevice.html - ML-Agents and Sentis: https://docs.unity3d.com/Packages/[email protected]/manual/Inference-Engine.html