Learning Game AI Programming with Lua
上QQ阅读APP看书,第一时间看更新

Agent-steering forces

With some basic agent properties and a full-fledged physics system supporting the sandbox, we can begin moving agents realistically through forces. This type of movement system is best known as a steering-based locomotion system. Craig Reynolds' Steering Behaviors For Autonomous Characters (http://www.red3d.com/cwr/papers/1999/gdc99steer.html) is best known for describing this style of steering system for moving characters. Steering forces allow for an easy classification of different movement types and allow for an easy way to apply multiple forces to a character.

As the sandbox uses the OpenSteer library to steer calculations, this makes it painless for Lua to request steering forces. While the steering calculations are left to OpenSteer, the application of forces will reside within our Lua Scripts.

Seeking

Seeking is one of the core steering forces and calculates a force that moves the agent toward their target. OpenSteer combines both seeking- and arrival-steering forces together in order to allow agents to slow down slightly before reaching their target destination:

local seekForce = agent:ForceToPosition(destination);

Applying steering forces to an agent

Creating a seeking agent requires two major components. The first is the ability to apply force calculations to an agent while updating their forward direction, and the second is the ability to clamp an agent's horizontal speed to its maxSpeed property.

Whenever we apply force to an agent, we will apply the maximum amount of force. In essence, this causes agents to reach their maximum speed in the shortest time possible. Without applying the maximum amount of force, small forces can end up having no effect on the agent. This is important because steering forces aren't always comparable with each other in terms of strength.

First, we'll implement an Agent_ApplyForce function to handle applying steering forces to the physics system and update the agent's forward direction if there is a change in the direction of the agent's velocity:

AgentUtilities.lua:

function AgentUtilities_ApplyPhysicsSteeringForce (
    agent, steeringForce, deltaTimeInSeconds)

    -- Ignore very weak steering forces.
    if (Vector.LengthSquared(steeringForce) < 0.1) then
        return;
    end
    
    -- Agents with 0 mass are immovable.
    if (agent:GetMass() <= 0) then
        return;
    end

    -- Zero out any steering in the y direction
    steeringForce.y = 0;

    -- Maximize the steering force, essentially forces the agent 
    -- to max acceleration.
    steeringForce =
        Vector.Normalize(steeringForce) * agent:GetMaxForce();

    -- Apply force to the physics representation.
    agent:ApplyForce(steeringForce);

    -- Newtons(kg*m/s^2) divided by mass(kg) results in
    -- acceleration(m/s^2).
    local acceleration = steeringForce / agent:GetMass();
    
    -- Velocity is measured in meters per second(m/s).
    local currentVelocity = agent:GetVelocity();
    
    -- Acceleration(m/s^2) multiplied by seconds results in
    -- velocity(m/s).
    local newVelocity =
        currentVelocity + (acceleration * deltaTimeInSeconds);

    -- Zero out any pitch changes to keep the Agent upright.
    -- NOTE: This implies that agents can immediately turn in any
    -- direction.
    newVelocity.y = 0;

    -- Point the agent in the direction of movement.
    agent:SetForward(newVelocity);
end

Before applying any steering force, we remove any steering force in the y axis. If our agents can fly, this would be undesirable, but as the primary type of agents the sandbox simulates are humanoid, humans can't move in the y axis unless they are jumping. Next, we normalize the steering force to a unit vector so that we can scale the steering force by the agent's maximum allowed force.

Tip

Using a maximum force for all steering calculations isn't required, but it produces desirable results. Feel free to play with how forces are applied to agents in order to see different types of steering behavior.

Once our force is calculated, we simply apply the force through the agent's ApplyForce function. Now, we take our force and calculate the agent's change in acceleration. With acceleration, we can derive the change in velocity by multiplying acceleration by deltaTimeInSeconds. The change in velocity is added to the current velocity of the agent and the resulting vector is the agent's forward direction.

This is just one of many ways in which you can apply steering forces to agents, and it makes many assumptions about how our agents will look when changing directions and speeds. Later, we can refine steering even further to smooth out changes in the direction, for example.

Tip

Remember that all time calculations take place in meters, seconds, and kilograms.

Clamping the horizontal speed of an agent

Next, we want to clamp the speed of our agent. If you think about the forces being applied to agents, acceleration changes will quickly cause the agent to move faster than their maximum speed allows.

When clamping the velocity, we only want to consider the agent's lateral velocity and ignore any speed attained through gravity. To calculate this, we first take the agent's speed and zero out all movement in the y axis. Next, we clamp the magnitude of the velocity with the agent's maximum speed.

Before assigning the velocity back to the agent, we set back the change in velocity in the y axis:

AgentUtilities.lua:

function AgentUtilities_ClampHorizontalSpeed(agent)
    local velocity = agent:GetVelocity();
    -- Store downward velocity to apply after clamping.
    local downwardVelocity = velocity.y;

    -- Ignore downward velocity since Agents never apply downward 
    -- velocity themselves.
    velocity.y = 0;
    
    local maxSpeed = agent:GetMaxSpeed();
    local squaredSpeed = maxSpeed * maxSpeed;
    
    -- Using squared values avoids the cost of using the square 
    -- root when calculating the magnitude of the velocity vector.
       if (Vector.LengthSquared(velocity) > squaredSpeed) then
        local newVelocity =
            Vector.Normalize(velocity) * maxSpeed;

        -- Reapply the original downward velocity after clamping.
        newVelocity.y = downwardVelocity;
        
        agent:SetVelocity(newVelocity);
    end
end

As we're clamping all of the horizontal speed of the agent in order to accurately calculate the agent's maximum speed, this has another significant trade-off. Any outside forces that affect the agent—for example, physics objects pushing the agent will have no effect on the actual velocity if the agent is already at their maximum speed.

Creating a seeking agent

With the application of forces and maximum speed out of the way, we can create a moving agent. Our seeking agent will calculate a seek force to their target position and will move to a new target position when the agent reaches the target radius of the target.

First, we calculate the seek steering force to the target destination and apply the force with the Agent_ApplyPhysicsSteeringForce function. Next, we call the Agent_ClampHorizontalSpeed function on the agent in order to remove any excess speed.

Additional debug information is drawn per frame in order to show you where the agent is moving toward and how large the target radius is around the target. If the agent moves within the target radius, a random target position is calculated and the agent starts seeking toward their new target destination all over again:

Agent.lua:

require "AgentUtilities";

function Agent_Initialize(agent)

    ...

    -- Assign a default target and acceptable target radius.
    agent:SetTarget(Vector.new(50, 0, 0));
    agent:SetTargetRadius(1.5);
end

function Agent_Update(agent, deltaTimeInMillis)
    local destination = agent:GetTarget();
    local deltaTimeInSeconds = deltaTimeInMillis / 1000;
    local seekForce = agent:ForceToPosition(destination);
    local targetRadius = agent:GetTargetRadius();
    local radius = agent:GetRadius();
    local position = agent:GetPosition();
    
    -- Apply seeking force.
    AgentUtilities_ApplyForce(
        agent, seekForce, deltaTimeInSeconds);
    AgentUtilities_ClampHorizontalSpeed(agent);

    local targetRadiusSquared =
        (targetRadius + radius) * (targetRadius + radius);

    -- Calculate the position where the Agent touches the ground.
    local adjustedPosition =
        agent:GetPosition() -
        Vector.new(0, agent:GetHeight()/2, 0);

    -- If the agent is within the target radius pick a new 
    -- random position to move to.
    if (Vector.DistanceSquared(adjustedPosition, destination) < 
        targetRadiusSquared) then

        -- New target is within the 100 meter squared movement 
        -- space.
        local target = agent:GetTarget();
        target.x = math.random(-50, 50);
        target.z = math.random(-50, 50);
        
        agent:SetTarget(target);
    end
    
    -- Draw debug information for target and target radius.
    Core.DrawCircle(
        destination, targetRadius, Vector.new(1, 0, 0));
    Core.DrawLine(position, destination, Vector.new(0, 1, 0));

    -- Debug outline representing the space the Agent can move
    -- within.
    Core.DrawSquare(Vector.new(), 100, Vector.new(1, 0, 0));
end

Rename the Lua file as follows:

src/my_sandbox/script/Agent.lua to
src/my_sandbox/script/SeekingAgent.lua

An agent seeking toward a random position

Pursuit

Creating a pursuing agent is very similar to seeking, except that we predict the target position of another moving agent. Start by creating a new PursuingAgent Lua script and implement the basic Agent_Cleanup, Agent_HandleEvent, Agent_Initialize, and Agent_Update functions:

Create Lua file as follows:

src/my_sandbox/script/PursuingAgent.lua

As a pursuing agent needs to have an enemy to pursue, we create a persistent Lua variable enemy outside the scope of our Initialize and Update functions. During the initialization of the agent, we will request all agents that are currently within the sandbox and assign the first agent to be our enemy.

A minor change we make to the pursuing agent compared to the seeking agent is to use an agent's PredictFuturePosition function to calculate its future position. We pass in the number of seconds we want to predict in order to create the pursuing agent's target position.

We can even make our pursuing agents slower than their enemy while still being able to catch up to a future predicted position when the enemy agent changes directions:

PursuingAgent.lua:

require "AgentUtilities";

local enemy;

function Agent_Cleanup(agent)
end

function Agent_HandleEvent(agent, event)
end

function Agent_Initialize(agent)
    AgentUtilities_CreateAgentRepresentation(
        agent, agent:GetHeight(), agent:GetRadius());

    -- Assign an acceptable target radius.
    agent:SetTargetRadius(1.0);
    -- Randomly assign a position to the agent.
    agent:SetPosition(Vector.new(
        math.random(-50, 50),
        0,

        math.random(-50, 50)));
    
    local agents = Sandbox.GetAgents(agent:GetSandbox());

    -- Find the first valid agent and assign the agent as an 
    -- enemy.
    for index = 1, #agents do
        if (agents[index] ~= agent) then
            enemy = agents[index];
            agent:SetTarget(enemy:GetPosition());
            break;
        end
    end

    -- Make the pursuing Agent slightly slower than the enemy.
    agent:SetMaxSpeed(enemy:GetMaxSpeed() * 0.8);
end

function Agent_Update(agent, deltaTimeInMillis)
    -- Calculate the future position of the enemy agent.
    agent:SetTarget(enemy:PredictFuturePosition(1));

    local destination = agent:GetTarget();
    local deltaTimeInSeconds = deltaTimeInMillis / 1000;
    local seekForce = agent:ForceToPosition(destination);
    local targetRadius = agent:GetTargetRadius();
    local position = agent:GetPosition();
    
    -- Apply seeking force to the predicted position.
    AgentUtilities_ApplyForce(
        agent, seekForce, deltaTimeInSeconds);
    AgentUtilities_ClampHorizontalSpeed(agent);

    -- Draw debug information for target and target radius.
    Core.DrawCircle(
        destination, targetRadius, Vector.new(1, 0, 0));
    Core.DrawLine(position, destination, Vector.new(0, 1, 0));
end

As the pursuing agent needs an enemy, we create the agent in the sandbox after our seeking agent is already initialized:

Sandbox.lua:

function Sandbox_Initialize(sandbox)

    ...

    Sandbox.CreateAgent(sandbox, "SeekingAgent.lua");
    Sandbox.CreateAgent(sandbox, "PursuingAgent.lua");
end

Running the sandbox, we'll now have a pursing agent chasing after our seeking agent.

An agent intercepting another agent

Fleeing

Creating a fleeing steering behavior is nearly identical to a seeking steering behavior. The only difference is that the agent will move away from its target instead of moving toward the target. Requesting a fleeing force can be done through the Agent.ForceToFleePosition function:

local forceToFlee = agent:ForceToFleePosition(position);

Evasion

Evasion is a steering behavior that tries to make the agent flee from another agent. This behavior is the exact opposite of pursue. Instead of steering toward an enemy's future position, we flee from the enemy's future position:

local forceToEvade = agent:ForceToFleePosition(
    enemy:PredictFuturePosition(timeInSeconds));

Wandering

A wandering behavior essentially returns a tangent steering force to the agent's forward vector. Wandering is meant to add deviation to an agent's movement and is not to be used as a steering force by itself. Taking in a delta time in milliseconds allows wandering forces to change at a constant rate:

local forceToWander = agent:ForceToWander(deltaTimeInMillis);

The target speed

To adjust our agents' speed to match their desired target speed, we can use the ForceToTargetSpeed function to calculate a steering force that will cause our agents to speed up or slow down:

local forceToSpeed = agent:ForceToTargetSpeed(targetSpeed);

Path following

Having an agent path follow requires two different steering behaviors. The first steering force, which is ForceToStayOnPath, keeps the agent from straying off the path, and the second steering force, which is ForceToFollowPath, moves the agent along the path.

Creating a path following agent

Creating a path following agent is very similar to our seeking agent, which is SeekingAgent.lua. First, create a new Lua script for the PathingAgent.lua agent:

Create the Lua file as follows:

src/my_sandbox/script/PathingAgent.lua

This time, we will utilize a DebugUtilities function in order to draw out a path for us. DebugUtilities is a Lua script in src/demo_framework/script/DebugUtilities.lua. When we initialize our pathing agent, we'll assign it the path to be traveled. Paths are considered looping by default, so our agent will keep traveling in a large circle:

PathingAgent.lua:

require "AgentUtilities";
require "DebugUtilities";

function Agent_Initialize(agent)

    AgentUtilities_CreateAgentRepresentation(
        agent, agent:GetHeight(), agent:GetRadius());

    -- Randomly assign a position to the agent.
    agent:SetPosition(Vector.new(
        math.random(-50, 50),
        0,
        math.random(-50, 50)));
end

function Agent_Update(agent, deltaTimeInMillis)
    local deltaTimeInSeconds = deltaTimeInMillis / 1000;
    
    -- Force to continue moving along the path, can cause the
    -- agent to veer away from the path.
    local followForce = agent:ForceToFollowPath(1.25);
    
    -- Force to move to the closest point on the path.
    local stayForce = agent:ForceToStayOnPath(1);
    
    -- Slight deviation force to alleviate bumping other pathing 
    -- agents.
    local wanderForce = agent:ForceToWander(deltaTimeInMillis);
    
    -- Sum steering forces using scalars.
    local totalForces = 
        Vector.Normalize(followForce) +
        Vector.Normalize(stayForce) * 0.25 +
        Vector.Normalize(wanderForce) * 0.25;

    local targetSpeed = 3;

    -- Accelerate pathing agents to a minimum speed.
    if (agent:GetSpeed() < targetSpeed) then
        local speedForce = agent:ForceToTargetSpeed(targetSpeed);
        totalForces = totalForces + Vector.Normalize(speedForce);
    end
    
    -- Apply the summation of all forces.
    AgentUtilities_ApplyPhysicsSteeringForce(
        agent, totalForces, deltaTimeInSeconds);
    AgentUtilities_ClampHorizontalSpeed(agent);
    
    -- Draw the agent's path as a looping path.
    DebugUtilities_DrawPath(agent:GetPath(), true);
end

The Agent_Update function has a few new additions, which show you how to add two steering forces together. Both ForceToFollowPath and ForceToStayOnPath are added together with a lower weight associated with the StayOnPath force. An additional ForceToTargetSpeed function is also added to pathing agents in order to make sure that they don't fall below a minimum speed.

Creating pathing agents in the sandbox is identical to creating other agents, except that this time, we'll create 20 different agents with varying speeds—all following the same path. Once you run the sandbox, notice how agents will collide with each other and be unable to pass other agents. All we're missing is collision avoidance for a nice-looking path following:

Sandbox.lua:

    -- Default path to assign to path following agents.
local path = {
    Vector.new(0, 0, 0),
    Vector.new(30, 0, 0),
    Vector.new(30, 0, 50),
    Vector.new(-30, 0, 0),
    Vector.new(-30, 0, 20)};

function Sandbox_Initialize(sandbox)

    ...
    
    for i=1, 20 do
        local agent = Sandbox.CreateAgent(
            sandbox, "PathingAgent.lua");

        -- Assign the same path to every agent.
        agent:SetPath(path, true);

        -- Randomly vary speeds to allow agents to pass one 
        -- another.
        local randomSpeed = math.random(
            agent:GetMaxSpeed() * 0.85,
            agent:GetMaxSpeed() * 1.15);

        agent:SetMaxSpeed(randomSpeed);
    end
end

Running the sandbox now, we can see 20 independent agents all following the same predefined path.

Multiple agents following a given path