›_ebskola.lv
sākt stundu →
Prog II · 4. tēma · 6 stundas - C# · AI · šautene

Datu struktūras //
un AI

C# List un Dictionary, ienaidnieku AI ar NavigationAgent2D un A* algoritmu. Rezultāts - Top-down šautene ar gudru AI.

6 stundas C# · List · Dictionary A* pathfinding
# 01 stundu plāns

6 stundas - C# kolekcijas, AI un top-down šautene

noslēguma projekts →
# 02 špikeris

4. tēmas špikeris - datu struktūras un AI

Saraksti, vārdnīcas, stāvokļu mašīna, pathfinding un object pooling. Katrs bloks atbilst vienai stundai.

4.1 Saraksti: List pret Godot Array

using Godot;
using Godot.Collections;
using System.Collections.Generic;

// Python:  enemies = []  /  enemies.append(e)  /  len(enemies)
List<Enemy> enemies = new();
enemies.Add(enemy);                 // pievieno beigās
enemies.Count;                      // garums
enemies.RemoveAt(1);                // izņem otro
enemies[10];                        // met kļūdu, ja nav

foreach (Enemy enemy in enemies) { enemy.UpdateEnemy(); }  // Python: for e in enemies

// NEDZĒS cikla laikā! Vispirms savāc, tad dzēs:
List<Enemy> toRemove = new();
foreach (Enemy enemy in enemies) if (!enemy.Alive) toRemove.Add(enemy);
foreach (Enemy enemy in toRemove) enemies.Remove(enemy);

// Godot Array - dažādi tipi vienā sarakstā, vajadzīgs saziņai ar editoru
Array bullets = new(); bullets.Add(42); bullets.Add("teksts");

List<T> - iekšējam C# kodam (ātrs, tipa-drošs). Array - kad dati jānodod Godot.

4.2 Vārdnīcas: Dictionary pret Godot Dictionary

using Godot;
using Godot.Collections;
using System.Collections.Generic;

// Python:  inventars = {}  /  inventars["zobens"] = 5
Dictionary<string, int> inventory = new();
inventory["Health Potion"] = 5;

// DROŠI:
int GetAmount(string name)
{
    if (inventory.TryGetValue(name, out int amount)) return amount;
    return 0;
}

foreach ((string name, int count) in inventory)   // Python: for k, v in d.items()
    GD.Print(name, " x", count);

// Dictionary<TKey,TValue>  statiski tipots, ātra meklēšana
// Godot Dictionary         Godot tips: dažādi vērtību tipi, der saglabāšanai
Godot.Collections.Dictionary save = new();
save["level"] = 5;
save["name"] = "Anna";

Ja atslēga var nebūt vārdnīcā, lieto TryGetValue, nevis tiešu kvadrātiekavu lasīšanu.

4.3 Stāvokļu mašīna: viens stāvoklis vienlaikus

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

public partial class Enemy : CharacterBody2D
{
    private EnemyState state = EnemyState.Idle;
    private double stateTime = 0.0;
    private Player player;

    private void ChangeState(EnemyState nextState)
    {
        state = nextState;
        stateTime = 0.0;
    }

    public override void _PhysicsProcess(double delta)
    {
        stateTime += delta;
        double distance = Position.DistanceTo(player.Position);

        // Slieksnis ATPAKAĻ ir LIELĀKS nekā uz priekšu -> nemirgo uz robežas
        if (state != EnemyState.Dead)
        {
            if (distance < 50) ChangeState(EnemyState.Attack);
            else if (distance < 300) ChangeState(EnemyState.Chase);
            else if (distance > 400) ChangeState(EnemyState.Patrol);
        }

        switch (state)
        {
            case EnemyState.Patrol: Patrol(); break;
            case EnemyState.Chase: Chase(); break;
            case EnemyState.Attack: Attack(); break;
            case EnemyState.Dead: break;   // gala stāvoklis
        }
    }
}

DEAD ir gala stāvoklis - no tā nav nevienas izejošas pārejas.

4.4 Pathfinding: apiet sienu, nevis spiesties tajā

// Scēnā: NavigationRegion2D -> NavigationPolygon -> Bake
//        Ienaidniekam bērns: NavigationAgent2D

private void Chase()
{
    NavigationAgent2D agent = GetNode<NavigationAgent2D>("NavigationAgent2D");
    agent.TargetPosition = player.Position;

    Vector2 next = agent.GetNextPathPosition();
    Vector2 dir = (next - Position).Normalized();

    Velocity = dir * speed;
    MoveAndSlide();
}

// A* algoritms, ko Godot izmanto iekšēji:
//   g = cik tālu jau esmu gājis
//   h = taisnais attālums līdz mērķim (novērtējums)
//   f = g + h  -> vienmēr izvēlas soli ar mazāko f

Bez navigācijas zonas ienaidnieks spiežas sienā. Ar to - apiet apkārt.

4.5 Object pooling: neveido un nedzēs, bet lieto atkārtoti

public class BulletPool
{
    private readonly List<Bullet> pool = new();
    private const int PoolSize = 50;

    public void Initialize(Node parent)
    {
        for (int i = 0; i < PoolSize; i++)
        {
            Bullet bullet = new Bullet();
            bullet.Visible = false;
            bullet.SetProcess(false);
            parent.AddChild(bullet);
            pool.Add(bullet);
        }
    }

    public Bullet GetBullet()
    {
        foreach (Bullet bullet in pool)
            if (!bullet.Visible) return bullet;    // atkārtoti lieto neaktīvo
        return null;                               // visi aizņemti
    }
}

// Bez pooling: 200 šāvieni = 200 Instantiate + 200 QueueFree -> FPS krīt
// Ar pooling:  mezglu skaits scēnā paliek nemainīgs

Ja GetBullet() atgriež null, pārbaudi to - citādi programma avarēs.

C# nav.GetNextPathPosition(); // A* aprēķina ceļu automātiski # NavigationAgent2D + List<Enemy> = gudra, efektīva AI sistēma