4.3 State machines AI uzvedībai
Stundas uzdevums: Implementēt AI ar stāvokļu mašīnu (state machine) reālistiskai uzvedībai.
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.
- Ieraksti
private enum EnemyState { Idle, Patrol, Chase, Attack, Dead }. - Pievieno klasei lauku
private EnemyState state = EnemyState.Idle;. - Uzraksti
_PhysicsProcessmetodēswitch (state) { ... }. - Pievieno katram stāvoklim savu
casear izdruku unbreak;. - Būvē un palaid - jāizdrukājas tikai
Idle. - Nomaini sākuma stāvokli uz
Patrolun palaid vēlreiz. - 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.
- Uzraksti metodi
private void ChangeState(EnemyState newState), kas izdrukā pāreju. - Izrēķini katrā kadrā attālumu līdz spēlētājam ar
Position.DistanceTo(player.Position). - Pārslēdz uz
Chase, ja attālums ir mazāks par 300. - Pārslēdz uz
Attack, ja attālums ir mazāks par 50. - Pārslēdz atpakaļ uz
Patrol, ja attālums pārsniedz 400. - Būvē un pastaigā ar spēlētāju tuvāk un tālāk.
- 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.
- Pieraksti tabulu ar visiem pieciem stāvokļiem.
- Atzīmē katram, no kura stāvokļa tajā var nokļūt.
- Pārbaudi spēlē katru pāreju un atzīmē tabulā, vai tā notika.
- Atrodi stāvokli, no kura nevar izkļūt, ja tāds ir.
- Pārbaudi, kas notiek, kad ienaidnieka HP nokrīt līdz nullei.
- Pievieno pāreju uz
Deadun pārliecinies, ka no tā vairs neizkļūst. - 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.
- Pievieno lauku
double state_time = 0.0;. - Palielini to katrā kadrā ar
delta. - Atiestati to uz nulli katrā
ChangeStateizsaukumā. - Neļauj mainīt stāvokli, ja
state_timeir mazāks par 0.3. - 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:
- Fails
Enemy.csarenumunswitch. - README.md ievietota video saite vai ekrānuzņēmumu sērija krātuvē, kurā redzamas pārejas PATROL -> CHASE -> ATTACK.
- README.md tabula ar atzīmētām pārbaudītajām stāvokļu pārejām.
- 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
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;
}