Gegner-KI
Teil des Abschnitts Spielarchitektur in Unity der Unity-Journey von Coddy. Lektion 31 von 50.
Das Gehirn eines Gegners ist eine Zustandsmaschine mit einem Zustand pro Verhalten. Dieses Skelett läuft zwischen seinen Posten hin und her, verfolgt einen Spieler, der sich bis auf 6 Einheiten nähert, und schwingt einmal pro Sekunde mit seinem Schwert, wenn es nahe genug ist. Jeder Zustand entscheidet selbst, wie er verlassen wird, und das Skelett behält die Hilfsfunktionen, die sie gemeinsam nutzen:
public class Skeleton : MonoBehaviour
{
public Transform player;
public Transform[] posts;
public float speed = 2f;
public StateMachine machine = new StateMachine();
public IState patrol, chase, attack;
void Start()
{
patrol = new PatrolState(this);
chase = new ChaseState(this);
attack = new AttackState(this);
machine.ChangeState(patrol);
}
void Update() { machine.Tick(); }
public float PlayerDistance() { return Vector3.Distance(transform.position, player.position); }
public void WalkTo(Vector3 target)
{
target.y = transform.position.y;
transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
}
}
public class PatrolState : IState
{
private Skeleton skel;
private int next;
public PatrolState(Skeleton skel) { this.skel = skel; }
public void Enter() { }
public void Exit() { }
public void Tick()
{
if (skel.PlayerDistance() < 6f) { skel.machine.ChangeState(skel.chase); return; }
Vector3 post = skel.posts[next].position;
post.y = skel.transform.position.y;
skel.WalkTo(post);
if (skel.transform.position == post) next = (next + 1) % skel.posts.Length;
}
}
public class ChaseState : IState
{
private Skeleton skel;
public ChaseState(Skeleton skel) { this.skel = skel; }
public void Enter() { }
public void Exit() { }
public void Tick()
{
float d = skel.PlayerDistance();
if (d > 6f) { skel.machine.ChangeState(skel.patrol); return; }
if (d < 1.5f) { skel.machine.ChangeState(skel.attack); return; }
skel.WalkTo(skel.player.position);
}
}
public class AttackState : IState
{
private Skeleton skel;
private float cooldown;
public AttackState(Skeleton skel) { this.skel = skel; }
public void Enter() { cooldown = 0f; }
public void Exit() { }
public void Tick()
{
if (skel.PlayerDistance() > 1.5f) { skel.machine.ChangeState(skel.chase); return; }
cooldown -= Time.deltaTime;
if (cooldown <= 0f) { Debug.Log("Swing!"); cooldown = 1f; }
}
}Die Zustände werden einmal erstellt und bleiben so lange bestehen wie das Skelett, daher bleiben ihre Felder zwischen den Besuchen erhalten. next von PatrolState ist auch nach einer Verfolgung noch gesetzt, und das Skelett läuft weiter zu dem Posten, auf den es gerade zusteuert. Enter setzt nur das zurück, was neu beginnen soll: AttackState setzt cooldown auf 0, sodass der erste Schwerthieb erfolgt, sobald das Skelett den Spieler erreicht.
Die gesamte Bewegung läuft über WalkTo, hier MoveTowards auf dem Transform, wodurch das Skelett durch Wände läuft. Um das Skelett stattdessen mit einem Rigidbody zu bewegen, setzt WalkTo die Geschwindigkeit des Körpers, und das Skelett aktualisiert die Zustandsmaschine in FixedUpdate, wo Time.deltaTime dem Physikschritt entspricht. Die drei Zustände ändern sich nicht: Sie geben an, wohin es gehen soll, der Besitzer gibt an, wie.
Aufgabe
MittelEin Ritter bewacht den Burghof. Die Pfeile bewegen den Player, dessen Health über TakeDamage(int amount) verfügt (beides ist bereits eingerichtet). Knight.cs enthält die Felder, die im Inspector ausgefüllt werden (waypoints W0 bis W2, player, playerHealth, speed 3 und die unten aufgeführten Reichweiten), sowie Hilfsmethoden, die sein Rigidbody verwenden: DistanceTo(point) und PlayerDistance() messen die Entfernung über den Boden, WalkTo(target) bewegt sich auf einen Punkt zu, Stop() bleibt stehen. Gib ihm ein Gehirn:
- Erstelle in
Knight.csdie Zustandsmaschine und die drei Zustände (Felderpatrol,chaseundattack), beginne mit der Patrouille und rufe in FixedUpdate bei jedem Tick die Zustandsmaschine auf. - Schreibe in
KnightStates.csdie KlassenPatrolState,ChaseStateundAttackState, die jeweils den Ritter als Argument im Konstruktor entgegennehmen. - Patrouille: Gehe zu
waypoints[next]; sobald du weniger als 0.3 davon entfernt bist, gehe zum nächsten Punkt und nach dem letzten zurück zum ersten. Ist der Spieler näher alssightRange(5), wird er verfolgt. Nach einer Verfolgung setzt die Patrouille den Weg zum Wegpunkt fort, auf den sie zuging. - Verfolgung: Gehe zum Spieler. Bei weniger als
attackRange(1.5): angreifen. Bei mehr alsloseRange(8): patrouillieren. Zwischen 5 und 8 wird die Verfolgung fortgesetzt, damit der Ritter am Rand seiner Sicht nicht zwischen den beiden Zuständen hin und her wechselt. - Angriff: Bleib stehen und füge dem Spieler sofort 1 Schaden zu, dann jede Sekunde, solange er sich innerhalb von
attackRangebefindet; ist er weiter entfernt, nimm die Verfolgung wieder auf.
Drücke Play, lass den Ritter dich entdecken, einen Treffer landen und renne dann weg. Drücke anschließend Check.
Probier es selbst
using UnityEngine;
// PatrolState, ChaseState und AttackState
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Spielarchitektur in Unity
1Komponenten statt Hierarchien
Kleine KomponentenEin Basisskript für GegnerAbstrakte KomponentenKomponenten im ZusammenspielRückblick: Modularer Geschützturm4Serialisierte Daten
Serialisierbare KlassenListen im InspectorEnums im InspectorRückblick: Loot-Tabelle7Zustandsautomaten
Zustände als KlassenEin ZustandsautomatÜbergängeGegner-KIWiederholung: Türzustände10Projekt: Arena-Überlebenskampf
Der HeldGegnertypen2Interfaces in Unity
IDamageableInterfaces und TryGetComponentGetComponentsRückblick: Interaktionstaste5ScriptableObjects
Daten-AssetsWaffenwerteGemeinsame EinstellungenWellen als DatenRückblick: Item-Datenbank8Wiederverwendbarer Code
ErweiterungsmethodenEin generisches SingletonGenerische HilfsmethodenRückblick: FindNearest