ebSkola

4.3 State machines AI uzvedībai

Stundas uzdevums: Implementēt AI ar stāvokļu mašīnu (state machine) reālistiskai uzvedībai.

SR 2.4.18. OOP SR 2.4.19. Algoritmu efektivitāte

80 min plāns: Teorija un paraugs (~10 min) · 1. uzdevums (~20 min) - izveido stāvokļu enum un pārslēgšanu · 2. uzdevums (~25 min) - pievieno pārejas noteikumus · 3. uzdevums (~20 min) - iztestē visus stāvokļus · Iesniegšana (~5 min).

Pirms sāc: atver savu projektu ar ienaidnieku. Stāvokļu mašīna nozīmē vienu vienkāršu noteikumu: ienaidnieks vienā brīdī var būt tikai vienā stāvoklī.

Teorija: State machine konceptu

State machine (stāvokļu mašīna) ir AI dizaina paraugs, kur objekts vienlaikus ir vienā no diskrētiem stāvokļiem un pāriet starp tiem pēc noteikumiem.

using Godot;

public enum EnemyState
{
    Idle,       // gaida
    Patrol,     // staigā
    Chase,      // dzenas
    Attack,     // uzbrūk
    Dead
}

public partial class Enemy : CharacterBody2D
{
    private EnemyState state = EnemyState.Idle;
    private Vector2 patrolTarget;
    private Player player;

    public override void _PhysicsProcess(double delta)
    {
        switch (state)
        {
            case EnemyState.Idle:
                if (PlayerVisible()) state = EnemyState.Chase;
                else if (IdleTooLong()) state = EnemyState.Patrol;
                break;
            case EnemyState.Patrol:
                MoveToPatrolTarget();
                if (PlayerVisible()) state = EnemyState.Chase;
                break;
            case EnemyState.Chase:
                MoveToward(player.Position);
                if (InAttackRange()) state = EnemyState.Attack;
                else if (!PlayerVisible()) state = EnemyState.Idle;
                break;
            case EnemyState.Attack:
                AttackPlayer();
                if (!InAttackRange()) state = EnemyState.Chase;
                break;
        }
    }
}

Kā šo izmantot projektā

Atceries: ar redzamu efektu editorā nepietiek. Paskaidro, kura C# klase glabā stāvokli, kura metode to maina un kā Godot node struktūra izmanto šo kodu.

Pārbaudi: C# kodā pamatoti izvēlēti List, Dictionary vai HashSet, AI stāvokļi ir skaidri, un sarežģītākas sistēmas var profilēt vai atkļūdot.

public class AiDataCheckpoint
{
    public string Lesson = "4.3 State machines AI uzvedībai";
    public bool UsesCSharp = true;
    public bool ChoosesContainer = true;
    public bool SeparatesAiState = true;
    public bool HandlesRemovalSafely = true;
}

Praktiskie uzdevumi

1. uzdevums -

Izveido stāvokļu enum un pārslēgšanu

Beigās ienaidnieks zinās, kurā stāvoklī tas atrodas.

  1. Ieraksti private enum EnemyState { Idle, Patrol, Chase, Attack, Dead }.
  2. Pievieno klasei lauku private EnemyState state = EnemyState.Idle;.
  3. Uzraksti _PhysicsProcess metodē switch (state) { ... }.
  4. Pievieno katram stāvoklim savu case ar izdruku un break;.
  5. Būvē un palaid - jāizdrukājas tikai Idle.
  6. Nomaini sākuma stāvokli uz Patrol un palaid vēlreiz.
  7. Pieraksti, kāpēc vienlaikus izdrukājas tikai viens stāvoklis.

Gatavs, kad: Output panelī atkārtojas tikai viena stāvokļa nosaukums, nevis visi pieci.

2. uzdevums -

Pievieno pārejas noteikumus

Beigās ienaidnieks pats pārslēgsies starp stāvokļiem.

  1. Uzraksti metodi private void ChangeState(EnemyState newState), kas izdrukā pāreju.
  2. Izrēķini katrā kadrā attālumu līdz spēlētājam ar Position.DistanceTo(player.Position).
  3. Pārslēdz uz Chase, ja attālums ir mazāks par 300.
  4. Pārslēdz uz Attack, ja attālums ir mazāks par 50.
  5. Pārslēdz atpakaļ uz Patrol, ja attālums pārsniedz 400.
  6. Būvē un pastaigā ar spēlētāju tuvāk un tālāk.
  7. Pieraksti, kāpēc atgriešanās slieksnis (400) ir lielāks par dzīšanās slieksni (300).

Gatavs, kad: pieejot tuvāk, ienaidnieks pāriet CHASE un ATTACK, bet aizejot - atgriežas PATROL.

3. uzdevums -

Iztestē visus stāvokļus

Beigās tu būsi pārliecinājies, ka neviens stāvoklis neiestrēgst.

  1. Pieraksti tabulu ar visiem pieciem stāvokļiem.
  2. Atzīmē katram, no kura stāvokļa tajā var nokļūt.
  3. Pārbaudi spēlē katru pāreju un atzīmē tabulā, vai tā notika.
  4. Atrodi stāvokli, no kura nevar izkļūt, ja tāds ir.
  5. Pārbaudi, kas notiek, kad ienaidnieka HP nokrīt līdz nullei.
  6. Pievieno pāreju uz Dead un pārliecinies, ka no tā vairs neizkļūst.
  7. Pieraksti, kurš stāvoklis ir gala stāvoklis.

Gatavs, kad: tabulā katrai pārejai ir atzīme, un no Dead stāvokļa ienaidnieks vairs neatgriežas.

Papildu uzdevums - Pievieno stāvokļa taimeri

Ja pamatdarbs ir gatavs, neļauj stāvokļiem mainīties pārāk bieži.

  1. Pievieno lauku double state_time = 0.0;.
  2. Palielini to katrā kadrā ar delta.
  3. Atiestati to uz nulli katrā ChangeState izsaukumā.
  4. Neļauj mainīt stāvokli, ja state_time ir mazāks par 0.3.
  5. Pārbaudi, ka ienaidnieks vairs nemirgo starp diviem stāvokļiem.

Gatavs, kad: stāvot tieši uz sliekšņa robežas, ienaidnieks vairs nemaina stāvokli vairākas reizes sekundē.

Ko sagatavo

Stundas mērķis: Tavam ienaidniekam ir strādājoša stāvokļu mašīna ar pieciem stāvokļiem, pārejas notiek pēc attāluma līdz spēlētājam, un Dead ir gala stāvoklis, no kura nevar izkļūt.

GitHub krātuvē jābūt:

  1. Fails Enemy.cs ar enum un switch.
  2. README.md ievietota video saite vai ekrānuzņēmumu sērija krātuvē, kurā redzamas pārejas PATROL -> CHASE -> ATTACK.
  3. README.md tabula ar atzīmētām pārbaudītajām stāvokļu pārejām.
  4. README.md: kāpēc atgriešanās slieksnis ir lielāks par dzīšanās slieksni.

Kā iesniedz: Veic commit un push. Skolotāja norādītajā vietā ievieto tikai GitHub krātuves saiti; visiem prasītajiem failiem, README.md pierakstiem un pierādījumiem jābūt pašā krātuvē.

Biežākās kļūdas

  • Stāvoklis paliek IDLE: Pārbaudi, vai pārejas nosacījumi tiek izpildīti.
  • Crash, kad player == null: Lieto null check pirms piekļūšanas.
  • Animation neattēlojas: AnimationPlayer ne pareizi pievienots vai animation name nepareizs.

Godot ekrānuzņēmumi

State machine diagramma: IDLE → PATROL (timer), IDLE/PATROL → CHASE (player vision), CHASE → ATTACK (range), ATTACK → CHASE (out of range), * → DEAD (hp 0).
State machine diagramma: IDLE → PATROL (timer), IDLE/PATROL → CHASE (player vision), CHASE → ATTACK (range), ATTACK → CHASE (out of range), * → DEAD (hp 0).
Spēles aina: spēlētājs centrā, ienaidnieks ar 'CHASE' debug label tuvojas no kreisās puses.
Spēles aina: spēlētājs centrā, ienaidnieks ar 'CHASE' debug label tuvojas no kreisās puses.

Koda piemērs (paplašināts)

public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }

public override void _PhysicsProcess(double delta)
{
    stateTimer += delta;

    switch (state)
    {
        case EnemyState.Idle:
            if (playerInVision) ChangeState(EnemyState.Chase);
            else if (stateTimer > 3.0) ChangeState(EnemyState.Patrol);
            break;

        case EnemyState.Patrol:
            Vector2 patrolDir = (patrolTarget - Position).Normalized();
            Velocity = patrolDir * 100.0f;
            MoveAndSlide();
            if (Position.DistanceTo(patrolTarget) < 10) SwapPatrolTarget();
            if (playerInVision) ChangeState(EnemyState.Chase);
            break;

        case EnemyState.Chase:
            Vector2 chaseDir = (playerPos - Position).Normalized();
            Velocity = chaseDir * 200.0f;
            MoveAndSlide();
            if (Position.DistanceTo(playerPos) < 50) ChangeState(EnemyState.Attack);
            break;

        case EnemyState.Attack:
            if (stateTimer >= 1.0)
            {
                player.TakeDamage(10);
                stateTimer = 0.0;
            }
            break;
    }
}

private void ChangeState(EnemyState newState)
{
    state = newState;
    stateTimer = 0.0;
}
Ienaidnieks staigā patrol līdz pamana spēlētāju, dzenas un uzbrūk; izvairoties - atgriežas patrol režīmā.