Integration Phase 5: Setting up a Character to Compute and Follow Paths

Following a path is a much more complex problem than simply running an A* calculation to find the path. Path following implies interaction with locomotion and steering systems that are dynamic and sometimes limited. Dynamic events may require the path to change—for instance, if the NavData in the vicinity of the character changes. You need a dynamic avoidance system to avoid colliding with moving obstacles and other characters, etc.

In this phase of the integration, we will use the tools provided by the pathfollowing layer of Gameware Navigation to get a character to find and follow a path.

The Bot lifecycle

The key component in the pathfollowing layer is the Bot class. Typically, you assign an instance of the Bot class to the classes that represent each of the game characters that you want to use the Gameware Navigation path finding and path following system. (Other kinds of characters, such as player characters, will typically be represented by CylinderObstacles; see Using Dynamic Obstacles and TagVolumes.) The Bot uses a pathfinding query to compute a path to a destination you specify, and “follows” that path by calculating a desired velocity at each frame.

The lifecycle of the Bot implies direct interaction from the corresponding game entity in at least three places: initialization, updating at each frame, and destruction.

As you continue with your integration, you will see this same philosophy used for other classes, such as dynamic obstacles, TagVolumes, and points of interest.

Initialization

When you initialize your character, you need to:

Update

Each frame, your game character needs to interact with the Bot in order to start a new path computation when needed, to update the current state of the Bot, and to interpret and apply the results of the path following system.

  • While your Bot is following a path, your character class should pick up the suggested velocity at each frame by calling m_navBot->GetBotOutput().m_outputVelocity. You modify this desired trajectory if necessary in order to be compatible with your animation and steering systems. You should apply the final result to the character in the game.
  • Any time the position or velocity of your character changes, you must also update the Bot accordingly. You can use the member functions in the Bot class to update the Bot position and velocity. For example, to access the Bot position, you can call Bot::SetPosition().
  • You will likely also need to read and respond to the current state of the path following system in various different ways. The example below includes a simple test for arrival at the destination. For more details, see Monitoring Path Following.

Destruction

When you are finished with your game character:

  • Call the Bot::RemoveFromDatabase() method to remove it from consideration by the other Bots in the World.
  • Set all pointers that you have saved to the Bot object in your game to KY_NULL. This decrements the reference counting mechanism; when no references remain, the object is transparently destroyed.

Example

The following example shows a relatively simple use of the Bot class by the game character class MyGameEntity. After spawning, the character plans and follows path back and forth between its initial position and a given destination.

[code from Tutorial_FirstIntegration.cpp]

#include "gwnavruntime/world/bot.h"
...

class MyGameEntity
{
public:

    MyGameEntity()
        : m_startPosition(0.0f, 0.0f, 0.0f)
        , m_destinationPosition(0.0f, 0.0f, 0.0f)
        , m_position(0.0f, 0.0f, 0.0f)
        , m_velocity(0.0f, 0.0f, 0.0f)
        , m_navBot(KY_NULL)
    {}

    void Initialize(Kaim::World* world, const Kaim::Vec3f& startPosition, const Kaim::Vec3f& destination);
    void Destroy();
    void Update(KyFloat32 simulationStepsInSeconds);
    bool HasArrived();

public:
    Kaim::Vec3f m_startPosition;
    Kaim::Vec3f m_destinationPosition;

    Kaim::Vec3f m_position;
    Kaim::Vec3f m_velocity;

    Kaim::Ptr<Kaim::Bot> m_navBot;
};

void MyGameEntity::Initialize(Kaim::World* world, const Kaim::Vec3f& startPosition, const Kaim::Vec3f& destination)
{
    m_position = startPosition;
    m_startPosition = startPosition;
    m_destinationPosition = destination;

    m_navBot = *KY_NEW Kaim::Bot;

    // We initialize the Bot at our desired start position
    Kaim::BotInitConfig botInitConfig;
    botInitConfig.m_database = world->GetDatabase(0);
    botInitConfig.m_startPosition = m_position;
    m_navBot->Init(botInitConfig);

    ...

    // We add the Bot to our Database.
    m_navBot->AddToDatabase();
    ...
}

void MyGameEntity::Destroy()
{
    m_navBot->RemoveFromDatabase();
    m_navBot = KY_NULL;
}

void MyGameEntity::Update(KyFloat32 simulationStepsInSeconds)
{
    if (m_navBot->GetFollowedPath() == KY_NULL && m_navBot->IsComputingNewPath() == false) // We need to compute a Path!
    {
        // Here, we ask the Bot to launch a path computation.
        // We should test the return code of this function, but as this tutorial is pretty simple, we are sure it cannot fail:
        //  - The AStarQuery is set from the default NavigationProfile.
        //  - We do not ask for a path computation while another path computation is in process.
        // Note that Kaim::Bot::ComputeNewPathToDestination() will run the query during the Kaim::World::Update().
        m_navBot->ComputeNewPathToDestination(m_destinationPosition); 
    }

    // If we are arrived...
    if (HasArrived())
    {
        m_velocity = Kaim::Vec3f::Zero();

        // Clear the followed path.
        m_navBot->ClearFollowedPath();

        // Swap the positions.
        Kaim::Vec3f swap = m_destinationPosition;
        m_destinationPosition = m_startPosition;
        m_startPosition = swap;

        // Restart a move.
        m_navBot->ComputeNewPathToDestination(m_destinationPosition);
    }

    // Retrieve the velocity suggested by the path following system, if available.
    m_velocity = m_navBot->GetBotOutput().m_outputVelocity;

    // Perform a simple integration.
    m_position += m_velocity*simulationStepsInSeconds;

    // Inform our Bot that the entity has moved.
    // Note that the update will be effective after next World::Update() call
    m_navBot->SetPosition(m_position);
    m_navBot->SetVelocityAndFrontDirection(m_velocity);
}

bool MyGameEntity::HasArrived()
{  
    KyFloat32 arrivalPrecisionRadius = m_navBot->GetConfig().m_pathProgressConfig.m_checkPointRadius;
    if (m_navBot->HasReachedPosition(m_destinationPosition, arrivalPrecisionRadius))
    {
        return true;
    }

    return false;
}

class MyGameLevel
{
public:
    ...
    void Update(float deltaTimeInSeconds);
    ...
protected:
    ...     
    MyGameEntity m_entity;
};

bool MyGameLevel::Initialize(Kaim::World* world)
{
    ... Do all other initializations
 
    // Initialize my entity
    m_entity.Initialize(world, Kaim::Vec3f(-11.5963f,1.06987f,10.4563f), Kaim::Vec3f(-16.636f,-26.7078f,8.10107f));
    ...

    return true;
}
...

void MyGameLevel::Update(float deltaTimeInSeconds)
{
    ...
    m_entity.Update(deltaTimeInSeconds);
}

void MyGameLevel::Destroy()
{
    // Destroy my entity.
    m_entity.Destroy();
 
    ... Do all other destructions and releases.
}

The code above mostly uses the default behavior of the path following system. However, the path following system is highly configurable; you can tailor it to the needs of your game by customizing it in various ways, responding to events that happen as the character follows the path, and even by writing your own implementations of the classes involved if necessary. For details, see Path Finding and Path Following.

Testing

Connect the Navigation Lab to your game, and you should see a yellow cylinder representing your character, moving back and forth between the start and end points set up for the AstarQuery. For example:

Note that: