Combining Steering Forces : 合并转向力

每一个Steering行为会产生一个力,他们会作用在速度向量上,这个合力的方向和大小会驱动AI个体实现一些行为(如Seek,Flee,Wander等),大致的计算方式如下:

steering = seek(); // this can be any behavior
steering = truncate (steering, max_force)
steering = steering / mass
 
velocity = truncate (velocity + steering , max_speed)
position = position + velocity

因为Steering力是向量,向量能和别的向量叠加。真正神奇的事情是它能和任意数量的向量叠加。

steering = nothing(); // the null vector, meaning "zero force magnitude"
steering = steering + seek();
steering = steering + flee();
(...)
steering = truncate (steering, max_force)
steering = steering / mass

velocity = truncate (velocity + steering , max_speed)
position = position + velocity

整合后的Steering力最终会形成一个代表了所有分力的合力。在上面的代码中,最终的Steering力会使游戏角色在Seek某些东西的同时躲避另一些东西。

看一下下面的合力的例子:

Steering forces combined to produce a single steering force

这种将单个行为整合在一起的方式可以产生非常复杂的运动行为。想象一下如果是通过编码来实现会有多困难,这些计算可能需要计算距离,区域,路径,图结构等,如果你关心的事情还是动态的,那基本上每一帧都要去计算这些东西。

而Steering行为中,所有的力都是动态的,它们本身就是在游戏的每一帧计算的,因此它们能够顺应环境的变化。

为了同时能够方便的使用多个Steering行为,一个用于管理的Manager很有必要。目的是能够创建一个黑盒,使得所有游戏中的个体都能够具备Steering的能力。

Manager会持有游戏个体的一个引用,这个游戏个体姑且称为Host, Manager会提供给这个个体一堆Steering方法,比如Seek 和 Flee。每一次方法被调用,Manager会更新其内部的属性产生一个Steering力。

在Manager处理完所有的调用后,Manager会将Steering力赋给Host作为其速度向量。

Movement manager: plugin architecture.
Movement manager: plugin architecture.插件架构

Manager有一堆方法,每个代表了一个Steering行为,每个行为都需要外部的一些信息来使自己工作,比如Seek行为需要外部提供一个目标点,追踪行为需要目标更多的一些信息,如当前位置和速度。空间中的位置也可以用向量表示,这在大多数游戏引擎中非常常见。

在追踪行为中的目标实际上可以是任何东西。为了让Manager更通用,Manager所接收的target必须遵循一定的规则,提供一些查询接口。

PS:some principles of object-oriented programming,

假定IBoid描述了游戏中可以使用Steering行为的所有个体。只要实现这个接口,就能够被Manager接收。

假设接口IBoid描述了一个实体可以被移动管理控制,只要实现这个接口,任何游戏中的类可以使用转向行为。接口实现如下:

public interface IBoid
{
    function getVelocity() :Vector3D;
    function getMaxVelocity() :Number;
    function getPosition() :Vector3D;
    function getMass() :Number;
}

现在Manager可以以一种通用的方式和个体进行交互了。Manager由两个基本属性和一堆方法构成。

现在Manager可以以一种通用的方式和个体进行交互了。Manager由两个基本属性和一堆方法构成。

public class SteeringManager
{
    public var steering :Vector3D;
    public var host :IBoid;
 
    // The constructor
    public function SteeringManager(host :IBoid) {
        this.host   = host;
        this.steering   = new Vector3D(0, 0);
    }
 
    // The public API (one method for each behavior)
    public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
    public function flee(target :Vector3D) :void {}
    public function wander() :void {}
    public function evade(target :IBoid) :void {}
    public function pursuit(target :IBoid) :void {}
 
    // The update method. 
    // Should be called after all behaviors have been invoked
    public function update() :void {}
 
    // Reset the internal steering force.
    public function reset() :void {}
 
    // The internal API
    private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}
    private function doFlee(target :Vector3D) :Vector3D {}
    private function doWander() :Vector3D {}
    private function doEvade(target :IBoid) :Vector3D {}
    private function doPursuit(target :IBoid) :Vector3D {}
}

Manager在实例化的时候必须接收一个对Host的引用,Manager会在之后改变Host的速度向量。

每个行为被表示为两个方法,一个Public的一个Private的。拿Seek来作为例子:

public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}

Public的Seek方法用来告诉Manager应用这种行为。这个方法没有返回值,它的参数和行为本身有关。在之后的Private方法里会返回一个经过Steering计算的向量值,这个向量值会存放在Manager的属性中。

// The publish method.
// Receives a target to seek and a slowingRadius (used to perform arrive).
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {
    steering.incrementBy(doSeek(target, slowingRadius));
    // incrementBy=> steering = steering + xx;
}

// The real implementation of seek (with arrival code included)
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {
    var force :Vector3D;
    var distance :Number;

    desired = target.subtract(host.getPosition());

    distance = desired.length;
    desired.normalize();

    if (distance <= slowingRadius) {
        desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius);
    } else {
        desired.scaleBy(host.getMaxVelocity());
    }

    force = desired.subtract(host.getVelocity());

    return force;
}

别的行为也是类似的实现方式。

public function pursuit(target :IBoid) :void {
    steering.incrementBy(doPursuit(target));
}

private function doPursuit(target :IBoid) :Vector3D {
    distance = target.getPosition().subtract(host.getPosition());

    var updatesNeeded :Number = distance.length / host.getMaxVelocity();

    var tv :Vector3D = target.getVelocity().clone();
    tv.scaleBy(updatesNeeded);

    targetFuturePosition = target.getPosition().clone().add(tv);

    return doSeek(targetFuturePosition);
}

之前所说的所有行为都可以添加到 behavior() 到 doBehavior();

每次行为方法被调用,返回的向量会被加到Manager的属性steering中。这个属性就代表了所有行为的结果。

在所有行为被调用之后,Manager会把steering的值赋给host的velocity。

然后u执行update()方法;

public function update():void {
    var velocity :Vector3D = host.getVelocity();
    var position :Vector3D = host.getPosition();

    truncate(steering, MAX_FORCE);
    steering.scaleBy(1 / host.getMass());

    velocity.incrementBy(steering);
    truncate(velocity, host.getMaxVelocity());

    position.incrementBy(velocity);
}

上述的方法必须在执行完所有行为后,再施加给对象。

让我们假设一个类叫做Prey使用steering behavior,结构如下。

public class Prey
{
    public var position  :Vector3D;
    public var velocity  :Vector3D;
    public var mass      :Number;

    public function Prey(posX :Number, posY :Number, totalMass :Number) {
        position    = new Vector3D(posX, posY);
        velocity    = new Vector3D(-1, -2);
        mass        = totalMass;

        x = position.x;
        y = position.y;
    }

    public function update():void {
        velocity.normalize();
        velocity.scaleBy(MAX_VELOCITY);
        velocity.scaleBy(1 / mass);

        truncate(velocity, MAX_VELOCITY);
        position = position.add(velocity);

        x = position.x;
        y = position.y;
    }
}

使用这个结构,实例可以使用欧拉积分移动,类似第一个例子。要让其使用manager,需要实现IBoid接口。

public class Prey implements IBoid
{
    public var position  :Vector3D;
    public var velocity  :Vector3D;
    public var mass      :Number;
    public var steering  :SteeringManager;

    public function Prey(posX :Number, posY :Number, totalMass :Number) {
        position    = new Vector3D(posX, posY);
        velocity    = new Vector3D(-1, -2);
        mass        = totalMass;
        steering    = new SteeringManager(this);

        x = position.x;
        y = position.y;
    }

    public function update():void {
        velocity.normalize();
        velocity.scaleBy(MAX_VELOCITY);
        velocity.scaleBy(1 / mass);

        truncate(velocity, MAX_VELOCITY);
        position = position.add(velocity);

        x = position.x;
        y = position.y;
    }

    // Below are the methods the interface IBoid requires.

    public function getVelocity() :Vector3D {
        return velocity;
    }

    public function getMaxVelocity() :Number {
        return 3;
    }

    public function getPosition() :Vector3D {
        return position;
    }

    public function getMass() :Number {
        return mass;
    }
}

update() 方法需要修改为如下:

public function update():void {
    // Make the prey wander around...
    steering.wander();

    // Update the manager so it will change the prey velocity vector.
    // The manager will perform the Euler intergration as well, changing
    // the "position" vector.
    steering.update();

    // After the manager has updated its internal structures, all we must
    // do is update our position according to the "position" vector.
    x = position.x;
    y = position.y;
}

 

可以使用所有的行为。只要在manager调用update之前。

下面的代码显示另外一个版本的更新方法。不过这次回增加一个躲避角色的行为。

public function update():void {
    var destination :Vector3D = getDestination(); // the place to seek
    var hunter :IBoid = getHunter(); // get the entity who is hunting us

    // Seek the destination and evade the hunter (at the same time!)
    steering.seek(destination);
    steering.evade(hunter);

    // Update the manager so it will change the prey velocity vector.
    // The manager will perform the Euler intergration as well, changing
    // the "position" vector.
    steering.update();

    // After the manager has updated its internal structures, all we must
    // do is update our position according to the "position" vector.
    x = position.x;
    y = position.y;
}

hunter狩猎者会追逐prey猎物,如果靠近的话,它会追逐到没体力为止。然后开始漫无目的游荡,直到恢复体力。

Here is the Hunter’s update() method:

public function update():void {
    if (resting && stamina++ >= MAX_STAMINA) {
        resting = false;
    }

    if (prey != null && !resting) {
        steering.pursuit(prey);
        stamina -= 2;

        if (stamina <= 0) {
            prey = null;
            resting = true;
        }
    } else {
        steering.wander();
        prey = getClosestPrey(position);
    }

    steering.update();

    x = position.x;
    y = position.y;
}

public function update():void {
    var distance :Number = Vector3D.distance(position, Game.mouse);

    hunter = getHunterWithinRange(position);

    if (hunter != null) {
        steering.evade(hunter);
    }

    if (distance <= 300 && hunter == null) {
        steering.seek(Game.mouse, 30);

    } else if(hunter == null){
        steering.wander();
    }

    steering.update();

    x = position.x;
    y = position.y;
}

猎物会无限游荡。如果猎人太近就躲避。如果没有猎人,鼠标离的很近,就会追踪鼠标。

代码如下:

public function update():void {
    var distance :Number = Vector3D.distance(position, Game.mouse);

    hunter = getHunterWithinRange(position);

    if (hunter != null) {
        steering.evade(hunter);
    }

    if (distance <= 300 && hunter == null) {
        steering.seek(Game.mouse, 30);

    } else if(hunter == null){
        steering.wander();
    }

    steering.update();

    x = position.x;
    y = position.y;
}

例子会之后给出unity的。

在实现这个代码后,再继续后续教程。

发表评论

邮箱地址不会被公开。