Reader small image

You're reading from  Learning Game AI Programming with Lua

Product typeBook
Published inNov 2014
Reading LevelBeginner
PublisherPackt
ISBN-139781783281336
Edition1st Edition
Languages
Right arrow
Author (1)
David Young
David Young
Right arrow

Chapter 8. Perception

In this chapter, we will cover the following topics:

  • Creating, sending, and receiving events

  • Giving sight and sound senses to agents

  • Creating teams of competing agents

  • Updating behaviors with event messaging

  • Managing agent communications through messaging

Currently, our agents have a near-omnipotent view of the sandbox. Through the use of direct enemy position selection, our agents respond so quickly and precisely that players might call this cheating. To create more believable agent interactions and responses, we need to implement perception. Essentially, this means giving our agents the ability to see, hear, and communicate with each other.

Events


So far, each of our agents run within their own virtual machine and communicating between them is not easy. To alleviate this, we can create a data structure called an event, which can be passed between agents or the sandbox itself.

Attributes

Creating an event is as simple as creating a table within Lua with a few restrictions. As events are serialized by the sandbox and passed to each virtual machine, only simple table attributes can be added to the event.

Each supported attribute is listed as follows:

Event attribute type

Description

boolean

A true or false Lua value

number

Any Lua number internally represented as a float value

object

A code object, the sandbox, sandbox object, or an agent

string

Any Lua string value

vector

Any vector created from Vector.new

Note

Events might not have nested tables within them; any unsupported attribute type will be discarded during the serialization. Currently, this is a limitation of event serialization of the C++ side of the...

Managing events


As the sandbox broadcasts events to every sandbox object that registers a callback function, we'll create an agent communication system that provides commonly used functionalities such as event type registration and event filtering.

Assigning agent teams

So far, our agents fend for themselves in a free-for-all style; now, we'll assign teams to utilize team-based communication as well as the beginnings of cooperative strategy. By changing the initialization of the agent, we can assign the agent to either team1 or team2 and assign a different character model accordingly:

IndirectSoldierAgent.lua:

function Agent_Initialize(agent)
    Soldier_InitializeAgent(agent);
    agent:SetTeam("team" .. (agent:GetId() % 2 + 1));

    soldierUserData = {};

    if (agent:GetTeam() == "team1") then
        soldierUserData.soldier = Soldier_CreateSoldier(agent);
    else
        soldierUserData.soldier = 
            Soldier_CreateLightSoldier(agent);
    end
    
    ...
    
end

Even with a...

Creating agent senses


With basic agent communication out of the way, we can give our agents the ability to determine visibility with other agents.

Initializing senses

Initializing our agent's senses will follow a functional paradigm that is very similar to what we've used previously. Passing our userData table inside our initialization function will provide any custom per agent data we'll need going forward:

AgentSenses.lua:

function AgentSenses_InitializeSenses(userData)
end

Updating senses

Updating agent senses is nearly identical to initialization, except that we pass in the sandbox and a deltaTimeInMillis time difference, so we throttle the update rate of individual senses:

AgentSenses.lua:

function AgentSenses_UpdateSenses(
    sandbox, userData, deltaTimeInMillis)
end

Agent visibility


Implementing agent visibility begins with casting a number of raycasts into the sandbox to see whether the agent can visibly detect another agent without objects blocking our agent's sight. To cast a ray, the only requirements are a starting and ending point within the sandbox. The results of the raycast are returned as a Lua table that contains a result attribute indicating whether anything was hit; it also contains an optional object attribute of the sandbox object that the ray intersected with, if the raycast successfully intersected with an object.

Note

When casting rays, take care about selecting a starting position that isn't already intersecting with an object, otherwise the results returned might not be the expected outcome.

To case a ray we'll use the RayCastToObject function provided by the sandbox.

local raycastResult = Sandbox.RayCastToObject(
    sandbox, startPoint, endPoint);

In the following screenshot, you can see various rays that represent agent visibility...

Agent sighting events


Now that we can detect when another agent is visible, we can create specific messages about an agent sighting, a dead enemy, or even a dead friend that can be sent to teammates. By extending our EventType table, we can create the three new event types for which we'll be sending out events:

AgentCommunications.lua
AgentCommunications.EventType.DeadEnemySighted =
    "DeadEnemySighted";
AgentCommunications.EventType.DeadFriendlySighted =
    "DeadFriendlySighted";
AgentCommunications.EventType.EnemySighted = "EnemySighted";

New enemy sighted event

To send out a new enemy sighted event, we can create a wrapper that handles all the event-specific information such as the sighted agent, where it was sighted, as well as the time at which it was sighted. The last information about when they were sighted will come in handy when dealing with stale information:

AgentSenses.lua:

function SendNewEnemyEvent(
    sandbox, agent, enemy, seenAt, lastSeen)

    local event = {
        agent...

Handling new agent sightings


As visibility can be updated very often, we only want to send out events for new agent sightings. We can create a helper function that will take a list of known agents indexed by their agent ID numbers and a list of currently visible agents. By storing the lastSeen time of a sighting, we can determine whether an event should be sent out.

Throttling events in this fashion prevents an abundance of redundant events from being propagated. As events are sent to every registered object, this can have an impact on performance when massive amounts of events are being sent:

AgentSenses.lua:

local function HandleNewAgentSightings(
    userData, knownAgents, spottedAgents)
    
    local newAgentSightings = {};
    
    for key, value in pairs(spottedAgents) do
        local agentId = value.agent:GetId();
        local lastSeen =
            value.lastSeen - knownAgents[agentId].lastSeen;
    
        if (knownAgents[agentId] == nil or lastSeen > 500) then
           ...

Agent auditory senses


As agents have no way of hearing something, we can simulate what information they can receive by sending events when auditory actions occur, such as a bullet shot or bullet impact.

Auditory events


Auditory events are created from the sandbox itself and are sent to all agents. It's up to the agent to determine what to do with the information, such as disregarding the event based on the distance.

The BulletShot event

To send an event when bullets are shot, we can update the ShootBullet function to send out an event based on the bullet's shot position:

AgentCommunications.lua:

AgentCommunications.EventType.BulletShot = "BulletShot";

Soldier.lua:

local function SendShootEvent(sandbox, shootPosition)
    local event = { position = shootPosition };
    
    AgentCommunications_SendMessage(
        sandbox,
        AgentCommunications.EventType.BulletShot,
        event);
end

local function ShootBullet(sandbox, position, rotation)

    ...    

    SendShootEvent(sandbox, position);
end

The BulletImpact event

An event similar to the BulletShot event can be sent out when a bullet impacts a location:

AgentCommunications.lua:

AgentCommunications.EventType.BulletImpact = "BulletImpact...

Handling auditory events


Handling auditory events requires you to add additional time-to-live information to each stored event. As a large number of bullet shots and impacts events are emitted, the time-to-live information will be used to prune out old events:

AgentSenses.lua:

local function HandleBulletImpactEvent(
    userData, eventType, event)
    
    local blackboard = userData.blackboard;
    local bulletImpacts = blackboard:Get("bulletImpacts") or {};
    
    table.insert(
        bulletImpacts,
        { position = event.position, ttl = 1000 });
    blackboard:Set("bulletImpacts", bulletImpacts);
end

local function HandleBulletShotEvent(userData, eventType, event)
    local blackboard = userData.blackboard;
    local bulletShots = blackboard:Get("bulletShots") or {};
    
    table.insert(
        bulletShots,
        { position = event.position, ttl = 1000 });
    blackboard:Set("bulletShots", bulletShots);
end

We can now add our auditory event handlers during the AgentSenses initialization...

Decaying blackboard events


When we update our agent senses, we can use the deltaTimeInMillis to decrement the time-to-live value of each stored event. If the time-to-live drops below 0, we'll remove the stored event:

AgentSenses.lua:

local function PruneEvents(events, deltaTimeInMillis)
    local validEvents = {};
    
    for index = 1, #events do
        local event = events[index];
        event.ttl = event.ttl - deltaTimeInMillis;
        
        if (event.ttl > 0) then
            table.insert(validEvents, event);
        end
    end
    
    return validEvents;
end

Providing a helper function to wrap pruning events is useful, as most auditory events will require being pruned during the normal agent's update loop:

AgentSenses.lua:

local function PruneBlackboardEvents(
    blackboard, attribute, deltaTimeInMillis)
    
    local attributeValue = blackboard:Get(attribute);
    
    if (attributeValue) then
        blackboard:Set(
            attribute,
            PruneEvents(attributeValue...

Decaying auditory events


Now, we can update our AgentSenses update loop to prune both the bulletImpacts as well as bulletShots entries that are being stored on the blackboard. Pruning out old events prevents Lua from consuming large amounts of data, as both of these event types occur frequently:

AgentSenses.lua:

function AgentSenses_UpdateSenses(
    sandbox, userData, deltaTimeInMillis)

    PruneBlackboardEvents(
        userData.blackboard,
        "bulletImpacts",
        deltaTimeInMillis);
    
    PruneBlackboardEvents(
        userData.blackboard,
        "bulletShots",
        deltaTimeInMillis);
    
    ...

end

Team communications


So far, team communication has revolved around agent visibility; we can now extend these communications to include behavioral logic selections such as new enemy selection, position updates, and retreat positions.

The EnemySelection event

To coordinate attacks, our agents need to notify their teammates when they've selected a new enemy. Sending out an event when a new enemy is being pursued requires you to add a new EventType string. With a specific EventType, we can create a wrapper function that will message the team about the enemy:

AgentCommunications.lua:

AgentCommunications.EventType.EnemySelection = "EnemySelection";

SoldierActions.lua:

local function SendEnemySelection(sandbox, agent, enemy)
    AgentCommunications_SendTeamMessage(
        sandbox,
        agent,
        AgentCommunications.EventType.EnemySelection,
        { agent = agent });
end

function SoldierActions_PursueInitialize(userData)
    
        ...
        
        userData.controller:QueueCommand...

Updating agent behaviors


Now that we have a number of different senses, team-based communications, and knowledge about the environment, we can update our agent's blackboard selections to account for the additional information.

Enemy selection

With additional visibility information, we can update our enemy selection to take into account only agents that aren't teammates as well as ones that have been visible within the last second. As teammates send out enemy sightings and essentially share their visibility about the environment, the response time for our agents to pursue a newly spotted enemy is rather quick.

Tip

Currently, our agents share immediate knowledge with each other. With only a small amount of work, you can delay knowledge propagation or even preference agents in order to select enemies they've seen firsthand.

Creating a new function for choosing the best enemy will now only process known visible agents that are stored on the blackboard.

SoldierKnowledge.lua:

function SoldierKnowledge_ChooseBestVisibleEnemy...

Summary


Now that we've grounded our agents within their environment, limiting what they can see and hear, and adding a layer of team-based communication, we can start building up more believable and strategic choices for them. Going forward, we can start building up tactics and influencing how our agents choose to move throughout the environment and not just their destinations and closest enemies. In the next chapter, we'll introduce a new spatial data structure that provides tactical information about the sandbox.

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Learning Game AI Programming with Lua
Published in: Nov 2014Publisher: PacktISBN-13: 9781783281336
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)