本节书摘来异步社区《Lua游戏AI开发指南》一书中的第2章,第2.1节,作者: 【美】David Young(杨) 译者: 王磊 责编: 陈冀康,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.10 智能体的属性
现在已经可以创建智能体了,我们回过头来看看智能体都有哪些属性,以及它们的意义是什么。
2.10.1 朝向
每当需要返回智能体的朝向时,最简单的方法是使用前向向量,它通常代表了智能体的运动方向。朝向的左向量和上向量也可以访问到。每当你需要改变智能体的方向时,只需简单地设置它的前向向量。
1.前向轴
为了获取和设置智能体的前向向量,我们可以使用内建的GetForward和SetForward辅助函数。
local forwardVector = agent:GetForward();
Agent.SetForward(agent, forwardVector);
2.左向轴
可以使用GetLeft辅助函数获取左向量。
local leftVector = agent:GetLeft();
3.向上的轴
类似的,可以使用GeUp辅助函数获取上向量。
local upVector = agent:GetUp();
2.10.2 定位
智能体的位置是它的胶囊表象的质心在物理模拟中的位置。如果你需要确定智能体的原点,只需简单地返回它的位置,然后用y分量减去它的高度的一半。
位置
可以使用GetPosition和SetPosition辅助函数获取和设置智能体的位置。
local positionVector = agent:GetPosition();
agent:SetPosition(positionVector);
2.10.3 大小
智能体和它们的胶囊表现的大小是由高度和半径来代表的。修改这两个值的时候,物理模拟也会进行调整,并根据修改来为智能体创建一个新的表现形式。
1.高度
可以通过GetHeight和SetHeight函数来存取智能体的高度。
local height = agent:GetHeight();
agent:SetHeight(height);
2.半径
可以通过GetRadius和SetRadius函数来获取和修改智能体的半径。
local radius = agent:GetRadius();
agent:SetRadius(radius);
2.10.4 物理
虽然智能体都会参与物理模拟,但并不是智能体所有的物理参数都会在物理模拟层使用。比如,智能体的质量和在物理模拟中使用的质量相同,但智能体的MaxForce和MaxSpeed函数只会被它自身使用。这两个参数分别代表智能体能对自身施加的最大的力和在不受任何外部影响时能达到的最大速率。
为什么在处理智能体的物理模拟时做这样的区分是有意义的呢?一个直观的例子就是对重力的处理。当智能体加速到它的最大速率时,我们仍然希望重力能让智能体在下落时向下加速。这个加速度能让智能体达到比它的最大速率属性更大的速率。
1.质量
可以使用智能体的GetMass和SetMass辅助函数来获取和修改它的质量。
local mass = agent:GetMass();
agent:SetMass(mass);
2.最大作用力
智能体的最大作用力可以使用GetMaxForce和SetMaxForce函数来获取和设置。
local maxForce = agent:GetMaxForce();
agent:SetMaxForce(maxForce);
3.最大速率
为了设置和获取智能体的最大速率属性,可以使用GetMaxSpeed和SetMaxSpeed函数。
local maxSpeed = agent:GetMaxSpeed();
agent:SetMaxSpeed(maxSpeed);
4.速率
可以通过GetSpeed和SetSpeed函数来设置和获取智能体的速率。
local speed = agent:GetSpeed();
agent:SetSpeed(speed);
5.速度
类似地,可以通过GetVelocity和SetVelocity函数来获取和设置智能体的速度。
local velocityVector = agent:GetVelocity();
agent:SetVelocity(velocityVector);
2.10.5 知识
智能体本身保存有一个基本的知识集,使得外部的Lua脚本(比如沙箱脚本)可以让它作为代理来保存一定的数据。例如,有时我们会创建一个智能体并让它移动到一个目标位置,我们可能想让沙箱来设置这个位置,而不是让智能体自己来确定它的目标。
1.目标
智能体的目标是一个向量位置。通常,智能体使用目标来作为它们想到达的位置或者另一个智能体的已知位置。
local targetVector = agent:GetTarget();
agent:SetTarget(targetVector);
2.目标半径
目标半径数量值是用来判定智能体是否足够靠近它的目标,而不必精确地处于目标的位置上。这个值可以帮助解决智能体在靠近它的目标时,由于微小的数目偏差而不断地在目标位置打转的问题。
local targetRadius = agent:GetTargetRadius();
agent:SetTargetRadius(targetRadius );
3.路径
智能体的路径是一系列的向量点。在路径追踪过程中,智能体内部会利用这些点来确定要移动到的位置。让智能体记住它们的路径是一个小小的优化,能避免在路径追踪计算过程中把路径数据传来传去。当把路径设置给智能体时,还可以传递一个布尔值参数,用来指定该路径是否循环。
local pathTable = agent:GetPath();
local hasPath = agent:HasPath();
local path = {
Vector.new(0, 0, 0),
Vector.new(10, 0, 10),
Vector.new(0, 0, 10) };
agent:SetPath(path, cylic);
2.10.6 智能体的移动
沙箱中所有的智能体都会通过物理系统自动进行物理模拟,因此有必要了解一下基本的牛顿物理学。
1.质量
当智能体相互碰撞时,它的质量就会起作用了。智能体的质量决定了当它被施加一个力时,它的加速度会是多少。
2.速率
速率定义了智能体移动的快慢而不考虑其移动的方向。在沙箱中速率值的单位都是米/秒,代表速度向量的模。
3.速度
速度代表了智能体的移动速率和方向。它的单位是米/秒,用一个向量来表示的。
4.加速度
沙箱中的加速度单位是米/秒2,它代表了智能体的速度变化的快慢。
5.力
力是推动智能体四处移动的主要因素,它的单位是牛顿。
Newtons = kilograms * meters / second 2
一旦受力,物体就会而加速或减速,加减速的快慢则取决于它的质量。
理解沙箱中使用的这些概念非常重要,它可以让你对于速度、距离和质量等概念有一个直观的认识。
2.10.7 智能体转向力
我们的沙箱有了基本的智能体属性和成熟的物理系统的支持,可以让智能体在力的作用下真实地移动了。这种运行系统通常被称为基于驾驶的运动系统。Craig Reynold的论文《Staring Behaviors For Autonomous Characters》很好地描述了移动角色的驾驶系统。转向力能支持多种类型的移动,还能支持在一个角色上施加多个力。
由于沙箱使用OpenSteer库来做转向计算,在Lua中请求一个转向力就很容易了。既然OpenSteer处理了转向计算,对力的应用则留在Lua脚本中处理。
1.探索
探索是核心的转向力之一,它计算一个让智能体向目标移动的力。OpenSteer会合并探索和抵达转向力,让智能体在接近目标位置时缓慢减速。
local seekForce = agent:ForceToPosition(destination);
2.对智能体应用转向力
创建一个探索型智能体需要两个主要组件。一个是在更新智能体的前进方向时对其施加力的计算,另一个就是把智能体的水平方向的速度限制在它的最大速率属性范围内。
每当我们对智能体施加力时,我们都会使用最大的力。这会让智能体在最短的时间内达到最大速度。如果不施加最大的力,则一些较小的力将来不及对智能体产生影响。这一点非常重要,因为系统不会比较转向力的大小。
首先,我们将实现Agent_ApplyForce函数来处理对物理系统施加转向力,并在智能体的速度方向有变化时更新智能体的前进方向。
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
在施加任何转向力之前,我们会移除所有y轴方向的力。如果你的智能体能够飞行的话,这可能不太合适。不过在沙箱中模拟的主要是类人形的智能体,而人除了跳跃是不能在y轴方向运动的。接下来,我们会把转向力归一化为一个单位向量,这样就可以把这个转向力缩放到被允许的最大的力。
虽然并不需要在所有的转向计算中使用最大的力,但它能产生理想的结果。你可以自由地选择如何对智能体施加力计算来体验不同的转向行为。
一旦计算好了力,我们只需通过ApplyForce函数来施加这个力。现在我们使用力来计算智能体加速度的变化。有了加速度,我们可以把加速度乘上deltaTimeInSeconds参数来得到速度的变化量。速度变化量再加到智能体的当前速度上,得到向量的就是它的前进方向。
这只是对智能体施加力计算的许多种方法中的一种,它对我们的智能体在运动方向和速率变化方面的表现做了很多的假设。稍后我们会对它进行进一步的优化,比如平滑方向的变化等。
要记住,所有的计算单位都是米、秒和千克。
3.限制智能体的水平速率
接下来,我们想要限制智能体的速率。如果考虑施加到智能体上的力,你会想到,加速度的存在会让智能体的速度很快超过它们允许的最大速率。
在限制速度时,我们只想限制智能体的横向速度而忽略由重力产生的纵向速度。为了达到这种效果,我们先取出智能体的速度,把y轴方向的速度设为0,然后把速度的大小限制到最大速率范围内。
在把速度设置回智能体前,我们会把原来y轴方向的速度再设置回去。
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
为了精确计算智能体的最大速率,我们限制了智能体的所有水平方向的速度,这还会造成一个重要的副作用。如果智能体已经达到它的最大速率,则所有能对它产生影响的外部力(比如推动智能体的物理对象)都不会对它的实际速度产生影响。
4.创建一个探索智能体
有了力的应用和最大速率的限制,我们可以创建移动的智能体了。探索智能体会计算一个推动力来让目标移动到目标位置,当达到目标位置的目标半径之内时,它将出发移动到一个新的目标。
首先,计算一个推动移动到目标位置的探索转向力并使用Agent_ApplyPhysics SteeringForce函数来施加到智能体上。接下来,调用Agent_ClampHorizontalSpeed函数来限制任何可能超限的速率。
在每一帧中,还会绘制一些额外的调试信息,以显示智能体的移动方向以及目标的半径大小。如果智能体移动到目标半径以内,则会随机计算一个新的目标位置,然后智能体就开始向这个新的目标移动,如图2-5所示。
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
重命名Lua文件。
src/my_sandbox/script/Agent.lua to
src/my_sandbox/ script/SeekingAgent.lua
5.追逐
创建追逐智能体和创建探索智能体的方法很相似,只是追逐智能体会预测另一个目标移动智能体的位置。首先创建一个新的PursuingAgent.Lua脚本,实现基本的Agent_Cleanup、 Agent_HandleEvent、 Agent_Initialize和Agent_Update函数。
创建Lua文件如下:
src/my_sandbox/script/PursuingAgent.lua
追逐智能体需要有一个被追逐的敌人,我们将在Initialize和Update函数范围之外创建一个持久型Lua变量enemy。在智能体的初始化过程中,我们会查询沙箱中当前的所有智能体,然后把第一个智能体设置为敌人。
相比于探索智能体,我们对追逐智能体的一个小改变是使用PredictFuturePosition函数来计算它的未来位置。我们把预测的时间秒数作为参数传递,以计算追逐智能体的目标位置。
我们甚至可以让追逐智能体的移动速度比它们的敌人更慢,但当敌人改变方向时,它们仍然会试图在新的预测位置追赶上来。
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
由于追逐智能体需要一个敌人,我们会在追逐智能体初始化后在沙箱中创建这个敌人。
Sandbox.lua:
function Sandbox_Initialize(sandbox)
...
Sandbox.CreateAgent(sandbox, "SeekingAgent.lua");
Sandbox.CreateAgent(sandbox, "PursuingAgent.lua");
end
运行沙箱,你会看到一个追逐智能体在追逐一个探索智能体,如图2-6所示。
6.逃跑
创建一个逃避行为基本上和创建一个探索行为类似。唯一区别在于智能体是从它的目标逃开,而不是向目标移动。可以通过Agent.ForceToFleePosition函数来获取一个逃避的力。
local forceToFlee = agent:ForceToFleePosition(position);
7.躲避
躲避是一种让智能体躲开另一个智能体的转向行为。这种行为和追逐行为正好相反。我们从敌人将要去的位置逃开而不是向那个位置移动。
local forceToEvade = agent:ForceToFleePosition(
enemy:PredictFuturePosition(timeInSeconds));
8.漫游
漫游行为本质上就是在智能体的前进方向上施加一个切向的转向力。漫游会对智能体的移动添加偏离,它本身不会被用作一个转向力。把一个时间片段的毫秒值作为参数传入,可以得到一个恒定速率变化的漫游力。
local forceToWander = agent:ForceToWander(deltaTimeInMillis);
9.目标速率
为了调整智能体的速率到想要的目标速率,可以使用ForceToTargetSpeed函数来计算一个加速或者减速的转向力。
local forceToSpeed = agent:ForceToTargetSpeed(targetSpeed);
10.路径追踪
一个路径追踪智能体需要实现两种不同的转向行为。一个称为ForceToStayOn Path,用于将智能体保持在路径上,另一个叫ForceToFollowPath,用于让智能体沿着路径移动。
11.创建一个路径追踪智能体
创建路径追踪智能体和在SeekingAgent.lua中实现的探索型智能体很相似。 首先,创建一个新的Lua脚本PathingAgent.lua。
创建如下的Lua文件:
src/my_sandbox/script/PathingAgent.lua
这一次,我们将使用DebugUtilities函数来绘制一条路径。这个函数位于src/demo_ramework/ script/DebugUtilities.lua文件中。当初始化寻路智能体时,我们将把这条路径赋给它。路径默认是循环的,因此智能体会一直绕着一个大圆转圈。
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
Agent_Update函数添加了一些新的代码,展示了如何把两个转向力加到一起。ForceToFollowPath和ForeceToStayOnPath都以一个与StayOnPath力相关的较小的权重加到一起。寻路智能体中还添加了一个ForceToTargetSpeed函数以确保它的速度不会小于一个最小速度。
在沙箱中创建寻路智能体和创建其他智能体类似,只不过这一次我们将创建20个速度各异的智能体,它们会跟随同一条路径。运行沙箱,你会看到这些智能体会互相碰撞而不能相互超越。对于一个行为良好的路径追踪来说,我们还缺少避免碰撞的功能。
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
现在运行沙箱,我们会看到20个独立的智能体沿着同一条预先定义的路径运动,如图2-7所示。
2.10.8 规避
规避转向行为是避免移动的智能体和其他智能体或对象相互碰撞。当智能体相互靠近时,ForceToAvoidAgents函数会在潜在智能体的切向方向上计算一个避免碰撞力。我们会使用预测性的移动计算来判断两个智能体是否会在给定的时间内相互碰撞。
另一方面,障碍物避免会使用球体来近似模拟沙箱对象,使用智能体的预测移动来创建一个在潜在碰撞切向方向上的转向力。
1.避免碰撞
我们可以使用ForceToAvoidAgents函数来计算避免碰撞所需要的力,函数的参数是与其他智能体可能相撞的最小时间。
local avoidAgentForce =
agent:ForceToAvoidAgents(minTimeToCollision);
2.规避障碍物
类似地,ForceToAvoidObjects函数可以用来计算避免与其他移动的障碍物相撞的力。
local avoidObjectForce =
agent:ForceToAvoidObjects(minTimeToCollision);
2.10.9 规避障碍物和其他智能体
修改SeeingAgent.lua脚本,添加ForceToAvoidAgents和ForceToAvoidObjects函数的加权合计值,可以让探索智能体规避可能的碰撞。运行沙箱,试着向在路径上移动的智能体发射箱子,观察它是如何避开箱子的。
SeekingAgent.lua
function Agent_Update(agent, deltaTimeInMillis)
local destination = agent:GetTarget();
local deltaTimeInSeconds = deltaTimeInMillis / 1000;
local avoidAgentForce = agent:ForceToAvoidAgents(1.5);
local avoidObjectForce = agent:ForceToAvoidObjects (1.5);
local seekForce = agent:ForceToPosition(destination);
local targetRadius = agent:GetTargetRadius();
local radius = agent:GetRadius();
local position = agent:GetPosition();
local avoidanceMultiplier = 3;
-- Sum all forces and apply higher priority to avoidance
-- forces.
local steeringForces =
seekForce +
avoidAgentForce * avoidanceMultiplier +
avoidObjectForce * avoidanceMultiplier;
AgentUtilities_ApplyForce(
agent, steeringForces, deltaTimeInSeconds);
...
end
Sandbox.lua:
require "DebugUtilities";
function Sandbox_Update(sandbox, deltaTimeInMillis)
-- Grab all Sandbox objects, not including agents.
local objects = Sandbox.GetObjects(sandbox);
-- Draw debug bounding sphere representations for objects with
-- mass.
DebugUtilities_DrawDynamicBoundingSpheres(objects);
end
现在运行沙箱,我们可以向探索智能体发射箱子,观察它是如何绕开每个对象的,如图2-8所示。
2.10.10 群组移动
群组移动可以拆分成3种主要的转向行为:对齐、聚拢和分离。对齐转向力让一个群组中的智能体都面朝同样的方向。聚拢转向力则保持群组中的智能体聚集在一起。分离与聚拢相反,它让群组中的智能体彼此之间都能保持一个最小的距离。
使用这3种行为的组合(也被称为群聚),可以构造一群智能体,它们一起移动,同时又不会互相穿透和重叠,如图2-9所示。
1.对齐
使用ForceToSperate函数可以计算一个转向向量,用来把智能体与群组中的其他智能体对齐。
local forceToAlign =
agent:ForceToSeparate(maxDistance, maxAngle, agents);
2.聚拢
使用ForceToCombine函数则可以计算一个聚集的力,用来保持智能体和群组中的其他智能体待在一起。
local forceToCombine =
agent:ForceToCombine(maxDistance, maxAngle, agents);
3.分离
使用ForceToSperate函数可以计算一个力来让智能体与群组中的其他智能体分开。
local forceToSeparate =
agent:forceToSeparate(minDistance, maxAngle, agents);
2.10.11 创建一群追随者
在这个示例中,我们将创建另一种AI类型,称为追随者代理。这里,一群追随者会聚集在一起并向它们的领导者移动。另一方面,领导者就是探索类型的智能体,它会在沙箱中随机地四处移动,完全不顾后面的追随者。
为创建追随者,我们对这些智能体使用多个转向力以让它们聚拢、分离和对齐它们追随的领导者。
创建一个Lua文件如下:
src/my_sandbox/script/FollowerAgent.lua
FollowerAgent.lua:
require "AgentUtilities";
local leader;
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)));
-- Assign the first valid agent as the leader to follow.
local agents = Sandbox.GetAgents(agent:GetSandbox());
for index = 1, #agents do
if (agents[index] ~= agent) then
leader = agents[index];
break;
end
end
end
function Agent_Update(agent, deltaTimeInMillis)
local deltaTimeInSeconds = deltaTimeInMillis / 1000;
local sandboxAgents = Sandbox.GetAgents(agent:GetSandbox());
-- Calculate a combining force so long as the leader stays
-- within a 100 meter range from the agent, and has less than
-- 180 degree difference in forward direction.
local forceToCombine =
Agent.ForceToCombine(agent, 100, 180, { leader } );
-- Force to stay away from other agents that are closer than
-- 2 meters and have a maximum forward degree difference of
-- less than 180 degrees.
local forceToSeparate =
Agent.ForceToSeparate(agent, 2, 180, sandboxAgents );
-- Force to stay away from getting too close to the leader if
-- within 5 meters of the leader and having a maximum forward
-- degree difference of less than 45 degrees.
local forceToAlign =
Agent.ForceToSeparate(agent, 5, 45, { leader } );
-- Summation of all separation and cohesion forces.
local totalForces =
forceToCombine + forceToSeparate * 1.15 + forceToAlign;
-- Apply all steering forces.
AgentUtilities_ApplyPhysicsSteeringForce(
agent, totalForces, deltaTimeInSeconds);
AgentUtilities_ClampHorizontalSpeed(agent);
local targetRadius = agent:GetTargetRadius();
local position = agent:GetPosition();
local destination = leader:GetPosition();
-- Draw debug information for target and target radius.
Core.DrawCircle(
position, 1, Vector.new(1, 1, 0));
Core.DrawCircle(
destination, targetRadius, Vector.new(1, 0, 0));
Core.DrawLine(position, destination, Vector.new(0, 1, 0));
end
追随者智能体和寻路智能体很相似;首先,它会在初始化过程中找到一个领导者,然后使用聚合和对齐转向力来尝试尽可能地接近并保持与领导者的距离,同时使用一个分离的力来保持与其他智能体以及领导者至少两米的距离。
Sandbox.lua:
function Sandbox_Initialize(sandbox)
...
-- Create a pursuing agent to follow the seeking agent.
Sandbox.CreateAgent(sandbox, "PursuingAgent.lua");
-- Create a group of followers that follow the seeking agent.
for i=1, 5 do
Sandbox.CreateAgent(sandbox, "FollowerAgent.lua");
end
...
end
创建5个追随者并绘制围绕它们的调试圆以显示它们的分离区域, 现在就能很容易地看到每个应用到智能体移动上的力了,如图2-10所示。
2.10.12 转向力合计
目前为止,我们已经把一些加权的转向力加到一起,并且当某些条件满足时来施加这些力。但这些对于智能体的运动究竟有什么用呢?把各种不同的转向力叠加到一起有两种通用的方法:加权合计法或者基于优先级的方法。
1.加权合计
加权合计法总是把所有转向力纳入计算中,每个转向力根据与其他力的比较而得到一个固定的系数。当转向力数量比较少时,这种方法非常直观。但当数量庞大时,要平衡众多互相对抗的力就会非常困难了。
通常这种方法是让智能体移动的首选方法,但当需要处理更复杂的情况时,最好采用基于优先级的方法。
2.基于优先级的力
基于优先级的方法只会基于某种优先级或条件来使用某些转向力。例如,你可以忽略所有小于某个阀值的力,或者采用循环的方法,每次只让一个转向力在一段时间内起作用。这种循环的方法可以避免加权合计法可能造成的一个问题,即不同的转向力相互抵消,导致智能体无法移动。
加权合计法和基于优先级的方法都不完美。为了让智能体能按设想的方式运动,这两种方法都需要做大量细致的调整工作。