KI-Programmierung Teil 05: Zustandsautomaten

Die KI Einheit aus dem vorangegangenen Artikel kannte nur einen einzigen Zustand – den Random Walk (RandomWalk()). Das soll jetzt geändert werden, denn zusätzlich dazu soll unsere Einheit nun auch Flucht- (Evade()) und Angriffsverhalten (Chase()) demonstrieren können.

bool BehaviourState_RandomWalk;
bool BehaviourState_Chase;
bool BehaviourState_Evade;

Unsere KI Einheit wird damit zu einem so genannten Zustandsautomaten (state machine). Anhand ihrer Wahrnehmung kann die Einheit selbstständig entscheiden, wann und in welchen Zustand sie wechseln möchte.

Werfen wir nun einen genaueren Blick auf das Verhaltensmuster, welches in der Update_BehaviourStates()-Methode der CAIEntity-Klasse neu implementiert wurde:

Zunächst befindet sich die Einheit im Random-Walk-Zustand. Hat die Einheit einen Angreifer wahrgenommen – mittels ihrer ungerichteten Wahrnehmung – dann wechselt sie in den Fluchtmodus. Sollte dies nicht der Fall sein, und sollte die Einheit stattdessen selbst ein Angriffsziel ausgemacht haben – mittels ihrer gerichteten Wahrnehmung – dann wechselt sie in den Angriffsmodus über.

Hinweis:
Obgleich das Verhalten nun intelligenter erscheint, wurde bereits im Rahmen der Implementierung im Vorfeld exakt festgelegt, wie sich die Einheit unter welchen Bedingungen verhalten wird. Im nächsten Kapitel werde ich zeigen, wie dieses Verhalten mittels eines Persönlichkeitsprofils natürlicher gestaltet werden kann.

class CAIEntity
{
public:

    bool BehaviourState_RandomWalk;
    bool BehaviourState_Chase;
    bool BehaviourState_Evade;

    bool EntityIsChased;
    bool EntityIsChasing;

    long IDOfSelectedEnemy;

    D3DXVECTOR3 WorldSpacePosition;
    D3DXVECTOR3 Velocity;
    D3DXVECTOR3 MovementDirection;
    D3DXVECTOR3 ViewDirection;

    // Rundum Wahrnehmung
    float AllAroundPerceptionDistanceMax;
    float AllAroundPerceptionDistanceMaxSq;

    // Jede Einheit kann separat wahrgenommen werden.
    // Der Arrayindex entspricht dem Index der wahrgenommenen
    // Einheit:
    bool*        AllAroundPerception;
    D3DXVECTOR3* AllAroundPerceptionPos;

    // gerichtete Wahrnehmung
    float CosFieldOfView;
    float DirectedPerceptionDistanceMax;
    float DirectedPerceptionDistanceMaxSq;

    // Jede Einheit kann separat wahrgenommen werden.
    // Der Arrayindex entspricht dem Index der wahrgenommenen
    // Einheit:
    bool*        DirectedPerception;
    D3DXVECTOR3* DirectedPerceptionPos;

    // Anzahl der wahrnehmbaren Objekte:
    long NumPerceptionObjects;

    CAIEntity()
    {
        BehaviourState_RandomWalk = true;
        BehaviourState_Chase = false;
        BehaviourState_Evade = false;

        AllAroundPerception = NULL;
        DirectedPerception = NULL;

        AllAroundPerceptionPos = NULL;
        DirectedPerceptionPos = NULL;

        NumPerceptionObjects = 0;

        WorldSpacePosition = g_NullVector;
        Velocity = g_NullVector;
        MovementDirection = g_NullVector;
        ViewDirection = g_NullVector;

        AllAroundPerceptionDistanceMax = 0.0f;
        AllAroundPerceptionDistanceMaxSq = 0.0f;
        AllAroundPerception = false;

        DirectedPerceptionDistanceMax = 0.0f;
        DirectedPerceptionDistanceMaxSq = 0.0f;
        CosFieldOfView = 1.0f;
        DirectedPerception = false;
    }

    ~CAIEntity()
    {
        SAFE_DELETE_ARRAY(AllAroundPerception)
        SAFE_DELETE_ARRAY(DirectedPerception)
        SAFE_DELETE_ARRAY(AllAroundPerceptionPos)
        SAFE_DELETE_ARRAY(DirectedPerceptionPos)
    }

    // Wahrnehmungen zurücksetzen:
    void Reset_Perceptions(void)
    {
        EntityIsChased = false;
        EntityIsChasing = false;

        for(long i = 0; i < NumPerceptionObjects; i++)
        {
            AllAroundPerception[i] = false;
            DirectedPerception[i] = false;
        }
    }

    void Set_NumPerceptionObjects(long numObjects)
    {
        SAFE_DELETE_ARRAY(AllAroundPerception)
        SAFE_DELETE_ARRAY(DirectedPerception)

        SAFE_DELETE_ARRAY(AllAroundPerceptionPos)
        SAFE_DELETE_ARRAY(DirectedPerceptionPos)

        NumPerceptionObjects = numObjects;

        AllAroundPerception = new bool[NumPerceptionObjects];
        DirectedPerception = new bool[NumPerceptionObjects];

        AllAroundPerceptionPos = new D3DXVECTOR3[NumPerceptionObjects];
        DirectedPerceptionPos = new D3DXVECTOR3[NumPerceptionObjects];

        for(long i = 0; i < NumPerceptionObjects; i++)
        {
            AllAroundPerception[i] = false;
            DirectedPerception[i] = false;
        }
    }

    void Set_AllAroundPerceptionParameter(float distance)
    {
        AllAroundPerceptionDistanceMax = distance;
        AllAroundPerceptionDistanceMaxSq  = AllAroundPerceptionDistanceMax;
        AllAroundPerceptionDistanceMaxSq *= AllAroundPerceptionDistanceMax;
    }

    void Set_DirectedPerceptionParameter(float distance,
                                         float cosFieldOfView)
    {
        CosFieldOfView = cosFieldOfView;
        DirectedPerceptionDistanceMax = distance;
        DirectedPerceptionDistanceMaxSq  = DirectedPerceptionDistanceMax;
        DirectedPerceptionDistanceMaxSq *= DirectedPerceptionDistanceMax;
    }

    void Set_Random_WorldSpacePosition(float minX, float maxX,
                                       float minY, float maxY,
                                       float minZ, float maxZ)
    {
        WorldSpacePosition = D3DXVECTOR3(frnd(minX, maxX),
                                         frnd(minY, maxY),
                                         frnd(minZ, maxZ));
    }

    void Update_BehaviourStates(void)
    {
        // Zunächst befindet sich die Einheit im Random-Walk-Zustand:
        BehaviourState_RandomWalk = true;
        BehaviourState_Chase = false;
        BehaviourState_Evade = false;

        IDOfSelectedEnemy = -1;

        long i;

        // Hat die Einheit einen Angreifer wahrgenommen
        // (mittels ihrer ungerichteter Wahrnehmung),
        // dann wechselt sie in den Fluchtmodus:

        for(i = 0; i < NumPerceptionObjects; i++)
        {
            if(AllAroundPerception[i] == true && EntityIsChased == true)
            {
                BehaviourState_RandomWalk = false;
                IDOfSelectedEnemy = i;
                BehaviourState_Evade = true;
                break;
            }
        }

        if(BehaviourState_Evade == true)
            return;

        // Sollte dies nicht der Fall sein, und sollte die Einheit
        // stattdessen selbst ein Angriffsziel ausgemacht haben
        // (mittels ihrer gerichteten Wahrnehmung), dann wechselt
        // sie in den Angriffsmodus über:

        for(i = 0; i < NumPerceptionObjects; i++)
        {
            if(DirectedPerception[i] == true)
            {
                BehaviourState_RandomWalk = false;
                IDOfSelectedEnemy = i;
                BehaviourState_Chase = true;
                break;
            }
        }
    }

    void Chase(float velocity)
    {
        if(BehaviourState_Chase == false)
            return;

        D3DXVECTOR3 OldWorldSpacePosition = WorldSpacePosition;

        if(WorldSpacePosition.x <
           DirectedPerceptionPos[IDOfSelectedEnemy].x)
            WorldSpacePosition.x += frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.x >
                DirectedPerceptionPos[IDOfSelectedEnemy].x)
            WorldSpacePosition.x -= frnd(0.9f, 1.1f)*velocity;

        if(WorldSpacePosition.y <
           DirectedPerceptionPos[IDOfSelectedEnemy].y)
            WorldSpacePosition.y += frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.y >
                DirectedPerceptionPos[IDOfSelectedEnemy].y)
            WorldSpacePosition.y -= frnd(0.9f, 1.1f)*velocity;

        if(WorldSpacePosition.z <
           DirectedPerceptionPos[IDOfSelectedEnemy].z)
            WorldSpacePosition.z += frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.z >
                DirectedPerceptionPos[IDOfSelectedEnemy].z)
            WorldSpacePosition.z -= frnd(0.9f, 1.1f)*velocity;

        // in diesem speziellen Beispiel gilt Augen geradeaus
        // (in Bewegungsrichtung):
        ViewDirection = WorldSpacePosition - OldWorldSpacePosition;
        D3DXVec3Normalize(&ViewDirection, &ViewDirection);
    }

    void Evade(float velocity)
    {
        if(BehaviourState_Evade == false)
            return;

        //Haken schlagen, um dem Gegner besser zu entkommen:
       
if(lrnd(0, 4) == 2)
        {
            BehaviourState_RandomWalk = true;
            RandomWalk(5, 1.5f*velocity, 2.0f*velocity);
            BehaviourState_RandomWalk = false;
            return;
        }

        D3DXVECTOR3 OldWorldSpacePosition = WorldSpacePosition;

        if(WorldSpacePosition.x <
           DirectedPerceptionPos[IDOfSelectedEnemy].x)
            WorldSpacePosition.x -= frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.x >
                DirectedPerceptionPos[IDOfSelectedEnemy].x)
            WorldSpacePosition.x += frnd(0.9f, 1.1f)*velocity;

        if(WorldSpacePosition.y <
           DirectedPerceptionPos[IDOfSelectedEnemy].y)
            WorldSpacePosition.y -= frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.y >
                DirectedPerceptionPos[IDOfSelectedEnemy].y)
            WorldSpacePosition.y += frnd(0.9f, 1.1f)*velocity;

        if(WorldSpacePosition.z <
           DirectedPerceptionPos[IDOfSelectedEnemy].z)
            WorldSpacePosition.z -= frnd(0.9f, 1.1f)*velocity;

        else if(WorldSpacePosition.z >
                DirectedPerceptionPos[IDOfSelectedEnemy].z)
            WorldSpacePosition.z += frnd(0.9f, 1.1f)*velocity;

        // in diesem speziellen Beispiel gilt Augen geradeaus
        // (in Bewegungsrichtung):
        ViewDirection = WorldSpacePosition - OldWorldSpacePosition;
        D3DXVec3Normalize(&ViewDirection, &ViewDirection);
    }

    void RandomWalk(long DirectionChangeFactor,
                    float minVelocity, float maxVelocity)
    {
        if(BehaviourState_RandomWalk == false)
            return;

        // zu schnelle Richtungsänderungen vermeiden:
        if(DirectionChangeFactor < 5)
            DirectionChangeFactor = 5;

        if(lrnd(0, DirectionChangeFactor) == 3)
        {
            MovementDirection = D3DXVECTOR3(frnd(-1.0f, 1.0f),
                                            frnd(-1.0f, 1.0f),
                                            frnd(-1.0f, 1.0f));

            D3DXVec3Normalize(&MovementDirection, &MovementDirection);

            // in diesem speziellen Beispiel gilt Augen geradeaus:
            ViewDirection = MovementDirection;

            float velocityAmount = frnd(minVelocity, maxVelocity);
            Velocity = MovementDirection*velocityAmount;
        }

        WorldSpacePosition += Velocity;
    }
};


void Check_AIEntity_DistancesAndPerception(CAIEntity* pAIEntity,
                                           long NumEntities)
{
    long i, j;

    // Wahrnehmungen zurücksetzen:
    for(i = 0; i < NumEntities; i++)
    {
        pAIEntity[i].Reset_Perceptions();
    }

    D3DXVECTOR3 DistanceVector;
    D3DXVECTOR3 NormalizedDistanceVector;
    float DistanceSq;
    float InvDistance;
    float dot;

    for(i = 0; i < NumEntities; i++)
    {
        for(j = i+1; j < NumEntities; j++)
        {
            DistanceVector = pAIEntity[i].WorldSpacePosition -
                             pAIEntity[j].WorldSpacePosition;

            DistanceSq = D3DXVec3LengthSq(&DistanceVector);
            InvDistance = 1.0f/sqrtf(DistanceSq);

            NormalizedDistanceVector = InvDistance*DistanceVector;

            // Rundum Wahrnehmung prüfen:
            if(pAIEntity[i].AllAroundPerceptionDistanceMaxSq > DistanceSq)
            {
                pAIEntity[i].AllAroundPerception[j] = true;
                pAIEntity[i].AllAroundPerceptionPos[j] =
                             pAIEntity[j].WorldSpacePosition;
            }

            if(pAIEntity[j].AllAroundPerceptionDistanceMaxSq > DistanceSq)
            {
                pAIEntity[j].AllAroundPerception[i] = true;
                pAIEntity[j].AllAroundPerceptionPos[i] =
                             pAIEntity[i].WorldSpacePosition;
            }

            // gerichtete Wahrnehmung prüfen:
            if(pAIEntity[i].DirectedPerceptionDistanceMaxSq > DistanceSq)
            {
                // Negatives Vorzeichen, weil NormalizedDistanceVector in
                // Richtung von pAIEntity[i] zeigt!!!!
                dot = -D3DXVec3Dot(&NormalizedDistanceVector,
                                   &pAIEntity[i].ViewDirection);

                if(dot > pAIEntity[i].CosFieldOfView)
                {
                    pAIEntity[i].DirectedPerception[j] = true;
                    pAIEntity[i].DirectedPerceptionPos[j] =
                                 pAIEntity[j].WorldSpacePosition;

                    if(pAIEntity[i].EntityIsChasing == false)
                    {
                        pAIEntity[i].EntityIsChasing = true;
                        pAIEntity[j].EntityIsChased = true;
                    }
                }
            }

            if(pAIEntity[j].DirectedPerceptionDistanceMaxSq > DistanceSq)
            {
                dot = D3DXVec3Dot(&NormalizedDistanceVector,
                                  &pAIEntity[j].ViewDirection);

                if(dot > pAIEntity[j].CosFieldOfView)
                {
                    pAIEntity[j].DirectedPerception[i] = true;
                    pAIEntity[j].DirectedPerceptionPos[i] =
                                 pAIEntity[i].WorldSpacePosition;

                    if(pAIEntity[j].EntityIsChasing == false)
                    {
                        pAIEntity[j].EntityIsChasing = true;
                        pAIEntity[i].EntityIsChased = true;
                    }
                }
            }
        }
    }
}

Interessante Artikel