вторник, 27 апреля 2010 г.

Управление поведениями (Steering behaviors). Часть 2.

Продолжение перевода Управление поведениями (Steering behaviors). Часть 1.

Теперь посмотрим на реализацию различных поведений, начнем с поиска (seek). Каждое из поведений будет определено в своем методе класса SteeredVehicle. Некоторые из поведений потребуют добавления новых свойств класса или дополнительных методов для своей реализации, мы их рассмотрим по мере необходимости.



Шаг 6. Поиск (seek)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/SeekTest.swf, 550, 400[/SWF]

Как мы уже говорили, поведение поиск заставляет персонаж двигаться в определенную точку. Вот как это выглядит:

public function seek(target:Vector2D):void
{
var desiredVelocity:Vector2D = target.subtract(_position);
desiredVelocity.normalize();
desiredVelocity = desiredVelocity.multiply(_maxSpeed);
var force:Vector2D = desiredVelocity.subtract(_velocity);
_steeringForce = _steeringForce.add(force);
}


Сначала мы определяем требуемую скорость, которая бы переместила нашего персонажа в нужную нам точку. Мы получаем эту скорость путем вычитания вектора позиции персонажа из вектора позиции требуемой точки (см рис.1).

Рисунок 1. Красный треугольник – персонаж, серый круг – точка, в которую хочет попасть персонаж
Рисунок 1. - Красный треугольник – персонаж, серый круг – точка, в которую хочет попасть персонаж



Теперь мы нормализуем требуемую скорость и умножаем этот вектор на значение _maxSpeed. Это даст нам вектор, который все еще направлен в требуемую точку, но его длина ограничена значением максимальной скорости для данного персонажа.

Мы получили требуемую скорость, но у нашего персонажа уже есть собственная скорость. При вычитании собственной скорости из требуемой скорости получим вектор, при добавлении которого к текущей скорости, мы будем двигаться в требуемую точку.

Это тот самый вектор, который мы добавим в управляющее воздействие. Напомним, что его значение ограничивается в методе update() с помощью свойства _maxForce. Таким образом, мы не развернемся к нужной точке мгновенно, а сделаем это с течением времени (см рис.2).

Рисунок 2. – Получение вектора управляющего воздействия
Рисунок 2. – Получение вектора управляющего воздействия



Ниже представлен пример поиска в действии:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class SeekTest extends Sprite
{
private var _vehicle:SteeredVehicle;

public function SeekTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_vehicle = new SteeredVehicle();
addChild(_vehicle);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_vehicle.seek(new Vector2D(mouseX, mouseY));
_vehicle.update();
}
}
}


Это очень простой пример. Мы создаем нового управляемого персонажа, добавляем его на сцену и в каждом кадре персонаж ищет позицию указателя мыши и обновляет свою позицию. Изменяя значения maxSpeed и maxForce в персонаже, или его массу в свойстве mass, мы можем хорошо прочувствовать, как эти параметры влияют на управление нашим персонажем. Вы также можете посмотреть, как поиск работает для неподвижной точки, оставляя указатель мыши неподвижным. Или если хочется приключений можно добавить еще одного персонажа и заставить первого искать второго.

Перейдем к следующему поведению.

Шаг 7. Избегание (flee)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/FleeTest.swf, 550, 400[/SWF]

Поведение избегание полностью противоположно поведению поиск. Реализация почти та же самая, кроме последней строчки, где мы вычитаем вектор текущего управляющего воздействия от суммарного вектора управляющих воздействий на данного персонажа.

public function flee(target:Vector2D):void
{
var desiredVelocity:Vector2D = target.subtract(_position);
desiredVelocity.normalize();
desiredVelocity = desiredVelocity.multiply(_maxSpeed);
var force:Vector2D = desiredVelocity.subtract(_velocity);
_steeringForce = _steeringForce.subtract(force);
}



Мы не будем опять все подробно разбирать, т.к. все тоже, что и в поиске, а лучше посмотрим пример:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class FleeTest extends Sprite
{
private var _vehicle:SteeredVehicle;

public function FleeTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_vehicle = new SteeredVehicle();
_vehicle.position = new Vector2D(200, 200);
_vehicle.edgeBehavior = Vehicle.BOUNCE;
addChild(_vehicle);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_vehicle.flee(new Vector2D(mouseX, mouseY));
_vehicle.update();
}
}
}



Как видите, наш персонаж пытается укрыться от указателя мыши в углу.

Теперь, когда у нас есть два противоположных поведения, давайте создадим два персонажа, одному присвоим поведение поиск, а другому поведение избегание и посмотрим на их взаимодействие:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class SeekFleeTest1 extends Sprite
{
private var _seeker:SteeredVehicle;
private var _fleer:SteeredVehicle;

public function SeekFleeTest1()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_seeker = new SteeredVehicle();
_seeker.position = new Vector2D(200, 200);
_seeker.edgeBehavior = Vehicle.BOUNCE;
addChild(_seeker);

_fleer = new SteeredVehicle();
_fleer.position = new Vector2D(400, 300);
_fleer.edgeBehavior = Vehicle.BOUNCE;
addChild(_fleer);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_seeker.seek(_fleer.position);
_fleer.flee(_seeker.position);
_seeker.update();
_fleer.update();
}
}
}


Здесь мы создаем два персонажа: _seeker и _fleer. Один персонаж убегает от другого, который его преследует. Попробуйте поменять параметры обоих персонажей и посмотрите, что получиться.

Теперь, когда у нас есть больше чем одно поведение, мы можем применить много поведений к одному персонажу. В следующем примере персонаж А ищет персонажа В, который ищет персонажа С, который, в свою очередь ищет персонажа А. В тоже время, каждый персонаж и убегает от того персонажа, который его ищет. Посмотрим код:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class SeekFleeTest2 extends Sprite
{
private var _vehicleA:SteeredVehicle;
private var _vehicleB:SteeredVehicle;
private var _vehicleC:SteeredVehicle;

public function SeekFleeTest2()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_vehicleA = new SteeredVehicle();
_vehicleA.position = new Vector2D(200, 200);
_vehicleA.edgeBehavior = Vehicle.BOUNCE;
addChild(_vehicleA);

_vehicleB = new SteeredVehicle();
_vehicleB.position = new Vector2D(400, 200);
_vehicleB.edgeBehavior = Vehicle.BOUNCE;
addChild(_vehicleB);

_vehicleC = new SteeredVehicle();
_vehicleC.position = new Vector2D(300, 260);
_vehicleC.edgeBehavior = Vehicle.BOUNCE;
addChild(_vehicleC);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_vehicleA.seek(_vehicleB.position);
_vehicleA.flee(_vehicleC.position);

_vehicleB.seek(_vehicleC.position);
_vehicleB.flee(_vehicleA.position);

_vehicleC.seek(_vehicleA.position);
_vehicleC.flee(_vehicleB.position);

_vehicleA.update();
_vehicleB.update();
_vehicleC.update();
}
}
}


Переходим к следующему поведению.

Шаг 8. Прибытие (arrive)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/ArriveTest.swf, 550, 400[/SWF]

Поведение прибытие, при всем к нему уважении, это почти тоже, что и поведение поиск. Фактически используется тот же алгоритм и персонаж управляется тем же способом. Отличие в том, что персонаж при прибытии в определенную точку постепенно замедляется и полностью останавливается, когда достигает этой точки.

Чтобы увидеть, почему поведение прибытие необходимо, вернемся к классу SeekTest. Переместите указатель мыши в любую позицию и подождите, пока персонаж туда не переместится. Вы увидите, что персонаж проскакивает через позицию указателя, затем понимает свою ошибку, поворачивается обратно и опять проскакивает. Этот процесс может продолжаться бесконечно, т.к. персонаж перемещается с максимальной скоростью.

Поведение прибытие решает эту проблему путем замедления персонажа по мере приближения его к объекту:

public function arrive(target:Vector2D):void
{
var desiredVelocity:Vector2D = target.subtract(_position);
desiredVelocity.normalize();

var dist:Number = _position.dist(target);
if(dist > _arrivalThreshold)
{
desiredVelocity = desiredVelocity.multiply(_maxSpeed);
}
else
{
desiredVelocity = desiredVelocity.multiply(_maxSpeed * dist / _arrivalThreshold);
}

var force:Vector2D = desiredVelocity.subtract(_velocity);
_steeringForce = _steeringForce.add(force);
}


Первые две строки делают тоже, что и в поведении поиск. Но перед умножением требуемой скорости на значение _maxSpeed мы проверяем расстояние до объекта. Если расстояние больше, чем определенный порог заданный в переменной _arrivalThreshold, то мы умножаем вектор требуемой скорости на значение _maxSpeed. После выполнения блока условий метод продолжает свое выполнение также как и в поведении поиск.

Напротив, если расстояние до цели меньше, чем определенный порог, мы должны что-то предпринять. Вместо умножения вектора требуемой скорости на значение _maxSpeed, мы умножаем его на отношение _maxSpeed * dist / _arrivalThreshold. Если расстояние меньше, чем значение порога то мы получим значение отношения dist / _arrivalThreshold меньше единицы. И следовательно значение требуемой скорости будет меньше значения _maxSpeed. При уменьшении расстояния до нуля, требуемая скорость также примет нулевое значение. См рис.3


Рисунок 3



Конечно, теперь класс SteeredVehicle нуждается в свойстве, в котором будет храниться значение порога, поэтому добавим его.

private var _arrivalThreshold:Number = 100;


Добавим также сеттер/геттер для записи/чтения этого свойства:

public function set arriveThreshold(value:Number):void
{
_arrivalThreshold = value;
}
public function get arriveThreshold():Number
{
return _arrivalThreshold;
}



Протестируем наше новое поведение:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class ArriveTest extends Sprite
{
private var _vehicle:SteeredVehicle;

public function ArriveTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_vehicle = new SteeredVehicle();
addChild(_vehicle);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_vehicle.arrive(new Vector2D(mouseX, mouseY));
_vehicle.update();
}
}
}


Единственное отличие от класса SeekTest в строке метода onEnterFrame, где вместо метода seek() вызывается метод arrive(). Как видите, при перемещении мыши персонаж ведет себя также как и при поведении поиск, но как только он приближается к указателю мыши, он сразу же замедляет свое движение.

Шаг 9. Преследование (pursue)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/PursueTest.swf, 550, 400[/SWF]

Еще одно поведение, которое очень похоже на поведение поиск. Фактически преследование использует поиск в своей реализации. Суть поведения преследование в том, что осуществляется поиск не текущей позиции объекта, а позиции, в которой он будет в будущем при текущей скорости. Очевидно, что это поведение применяется для преследования движущихся объектов. Поэтому можно сказать, что преследуемый объект должен быть экземпляром класса Vehicle, или экземпляром класса SteeredVehicle, т.к. он наследует от Vehicle.
Как же предсказать где будет цель? Для этого мы используем текущую скорость преследуемого объекта и найдем точку, где будет находиться этот объект через определенное время. Но на какой временной промежуток делать предсказание? Хороший вопрос! Если вы будете определять точку в далеком будущем, вы можете промахнуться, если в недалеком будущем – будете все время отставать. Фактически поведение преследование – это поведение поиск со значением временного промежутка равным нулю.

Одна из стратегий основывается на расчете временного промежутка, основанном на расстоянии между двумя этими объектами (который преследует и который убегает). Если вы далеко от цели, то временной промежуток будет большим и чем вы ближе к цели, тем временной промежуток будет меньше. Код:

public function pursue(target:Vehicle):void
{
var lookAheadTime:Number = position.dist(target.position) / _maxSpeed;
var predictedTarget:Vector2D = target.position.add(target.velocity.multiply(lookAheadTime));
seek(predictedTarget);
}


Первое, что мы здесь делаем, это определяем временной промежуток lookAheadTime из отношения расстояния между объектами и максимальной скорости. Это дает нам число кадров, за которое наш преследующий объект догонит преследуемого, если тот не будет двигаться. Мы можем предсказать позицию, где будет находиться преследуемый объект через это число кадров при его скорости. И наконец, мы применяем к преследующему персонажу поведение поиск этой позиции. См рис.4


Рисунок 4



Теперь посмотрим на все это в действии. На этот раз мы создадим три персонажа. Один будет экземпляром класса Vehicle а два других – экземплярами класса SteeredVehicle (один с поведением поиск, другой с поведением преследование). Если все получилось, то персонаж с поведением преследование должен победить.

Итак, код:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class PursueTest extends Sprite
{
private var _seeker:SteeredVehicle;
private var _pursuer:SteeredVehicle;
private var _target:Vehicle;

public function PursueTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_seeker = new SteeredVehicle();
_seeker.x = 400;
addChild(_seeker);

_pursuer = new SteeredVehicle();
_pursuer.x = 400;
addChild(_pursuer);

_target = new Vehicle();
_target.position = new Vector2D(200, 300);
_target.velocity.length = 15;
addChild(_target);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_seeker.seek(_target.position);
_seeker.update();

_pursuer.pursue(_target);
_pursuer.update();

_target.update();
}
}
}


Заметим, что оба объекта SteeredVehicle стартуют с одной позиции, но ищущий объект движется в ту точку, где находится цель, а преследующий – в точку, где цель будет в будущем. Как видим, преследующий достиг цели раньше.

Шаг 10. Уклонение (evade)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/EvadeTest.swf, 550, 400[/SWF]

Как вы уже наверное догадались, поведение уклонение это полная противоположность поведения преследование. И также как преследование в своей реализации использует поиск, так и уклонение в своей реализации использует избегание.

По существу, поведение уклонение предсказывает позицию, где будет находиться преследующий объект через определенный промежуток времени и старается увести персонаж от этой позиции подальше. Все принципы, обсужденные в описании поведения преследование применимы и здесь. Фактически это тот же метод преследование за исключением последней строчки:

public function evade(target:Vehicle):void
{
var lookAheadTime:Number = position.dist(target.position) / _maxSpeed;
var predictedTarget:Vector2D = target.position.subtract(target.velocity.multiply(lookAheadTime));
flee(predictedTarget);
}


Не думаю, что здесь нужно что-то объяснять. Вот код теста:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class PursueEvadeTest extends Sprite
{
private var _pursuer:SteeredVehicle;
private var _evader:SteeredVehicle;

public function PursueEvadeTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_pursuer = new SteeredVehicle();
_pursuer.position = new Vector2D(200, 200);
_pursuer.edgeBehavior = Vehicle.BOUNCE;
addChild(_pursuer);

_evader = new SteeredVehicle();
_evader.position = new Vector2D(400, 300);
_evader.edgeBehavior = Vehicle.BOUNCE;
addChild(_evader);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_pursuer.pursue(_evader);
_evader.evade(_pursuer);
_pursuer.update();
_evader.update();
}
}
}



Шаг 11. Блуждание (wander)

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/04/27/steering_behaviors/WanderTest .swf, 550, 400[/SWF]

Название этого поведение говорит само за себя. Персонаж с таким поведением перемещается по экрану без какой-либо цели. Это поведение очень часто моделируется в играх и симуляциях.

Реализация поведения блуждание сопряжена с некоторыми трудностями. Мы могли бы применить к персонажу какое-нибудь случайное движение, броуновское, например, но выглядеть это будет несколько хаотично и неправдоподобно. Нам же нужно что-то более гладкое и реалистичное. Обычно это достигается помещением перед персонажем воображаемого круга, выборе случайной точки на этом круге и использовании этой точки в качестве цели. В каждом кадре эта точка может случайным образом перемещаться. И т.к. эта точка не может быть вне данного круга, то и воздействие на персонаж в текущем кадре не будет сильно отличаться от воздействия в предыдущем кадре. См. рис 5


Рисунок 5



Можно изменять ряд факторов, чтобы получить разные виды блуждания: размер круга, на каком расстоянии он размещается от персонажа и на сколько смещается цель в каждом кадре.

Код метода wander():

public function wander():void
{
var center:Vector2D = velocity.clone().normalize().multiply(_wanderDistance);
var offset:Vector2D = new Vector2D(0);
offset.length = _wanderRadius;
offset.angle = _wanderAngle;
_wanderAngle += Math.random() * _wanderRange - _wanderRange * .5;
var force:Vector2D = center.add(offset);
_steeringForce = _steeringForce.add(force);
}


Сначала мы определяем центр круга. Вектор скорости персонажа указывает нам точку перед ним. Нормализацией вектора скорости персонажа и умножение его на значение переменной _wanderDistance, мы получаем вектор определяющий центр нашего воображаемого круга. Теперь добавим другой вектор, назовем его offset - он представляет случайную точку на окружности. Длина этого вектора будет равна значению переменной _wanderRadius а его угол будет равен _wanderAngle. Угол wanderAngle изменяется случайно в диапазоне _wanderRange. Сумма векторов центра воображаемого круга center и смещения offset даст нам вектор управляющего воздействия для данного вида поведения. Теперь мы можем его добавить к вектору суммарного управляющего воздействия.

Заметим, что появились новые переменные в классе SteeredVehicle. Добавим их в класс и установим им значения по умолчанию.

private var _wanderAngle:Number = 0;
private var _wanderDistance:Number = 10;
private var _wanderRadius:Number = 5;
private var _wanderRange:Number = 1;


И добавим сеттеры/геттеры для этих переменных:

public function set wanderDistance(value:Number):void
{
_wanderDistance = value;
}
public function get wanderDistance():Number
{
return _wanderDistance;
}

public function set wanderRadius(value:Number):void
{
_wanderRadius = value;
}
public function get wanderRadius():Number
{
return _wanderRadius;
}

public function set wanderRange(value:Number):void
{
_wanderRange = value;
}
public function get wanderRange():Number
{
return _wanderRange;
}



Наконец, сам пример поведения блуждание:

package
{

import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;

public class WanderTest extends Sprite
{
private var _vehicle:SteeredVehicle;

public function WanderTest()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

_vehicle = new SteeredVehicle();
_vehicle.position = new Vector2D(200, 200);
addChild(_vehicle);

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

private function onEnterFrame(event:Event):void
{
_vehicle.wander();
_vehicle.update();
}
}
}



Мы закончили обзор простых поведений, перейдем к более сложным и начнем с избегания препятствий.

Исходники

2 комментария:

  1. Эту тоже осилил)) переходим к 3-й части))
    (статья просто офигенная)
    Весь код переписываю ручками)) (кроме сетеров и гетеров) чтоб лучше понять)) и то, понимается с трудом(

    ОтветитьУдалить
  2. Огромнейшее спасибо - пригодилось необыкновенно. Кучу времени сэкономил, чтобы самому не выдумывать.

    ОтветитьУдалить