Статьи

Используйте векторные области для реализации поля зрения во флэш-игре

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


Давайте посмотрим на конечный результат, к которому мы будем стремиться. Нажмите на башню в нижней части сцены, чтобы начать симуляцию.


Я уверен, что большинство читателей использовали камеры. Каждая камера имеет угол обзора, определяемый типом объектива. Есть узкие и широкие углы обзора. Углы обзора ограничивают поле зрения в секторе. Сверху вниз они выглядят как на схеме ниже. Если вы сделаете снимок, все в пределах затененной области будет захвачено.

Поле зрения: узкое и широкое

Поле зрения турели в нашей симуляции похоже на камеру. Если в поле зрения находится враг, охранник ответит (подайте сигнал тревоги, прицелитесь, стреляйте и т. Д.).


Математические условия для определения предмета в пределах поля зрения

Диаграмма выше показывает поле зрения башни. Обычно угол обзора будет одинаковым как слева, так и справа. Радиус также будет одинаковым во всем секторе. Таким образом, чтобы проверить, находится ли враг в поле зрения турели, можно использовать эти два математических условия:

  1. Расстояние между башней и противником меньше радиуса.
  2. Угол от линии визирования башни до противника составляет менее 30 °.

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

  • Сравните величины vLine2 и vLine3, чтобы проверить условие 1 из шага 2.
  • Сравните угол, зажатый между vLine2 и vLine3, чтобы проверить условие 2 из шага 2.

Используя формулу скалярного произведения между векторами, мы можем найти угол между двумя векторами. Я включил презентацию Flash выше, чтобы облегчить ваше понимание. Нажмите на кнопки внизу, чтобы пролистать рамки.

Вот Vector2D Actionscript в Vector2D (который я использовал в предыдущих уроках, как этот ), который выполняет эту работу. Обратите внимание, что линия 257 помогает определить, находится ли угол на отрицательной или положительной стороне. Однако это не очень нам поможет, так как направление не важно. Больше объяснений по этой теме в следующей части этой серии.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * Method to obtain the smaller angle, in radian, sandwiched from current vector to input vector
 * @param vector2 A vector to bound the angle
 * @return Angle in radian, positive is clockwise, negative is anti-clockwise
 */
public function angleBetween(vector2:Vector2D):Number
{
    //get normalised vectors
    var norm1:Vector2D = this.normalise();
    var norm2:Vector2D = vector2.normalise();
     
    //dot product of vectors to find angle
    var product:Number = norm1.dotProduct(norm2);
    product = Math.min(1, product);
    var angle:Number = Math.acos(product);
     
    //sides of angle
    if (this.vectorProduct(vector2) < 0) angle *= -1
    return angle;
}

Ниже я включил презентацию, которая реализует концепцию поля зрения. Не стесняйтесь нажимать и перетаскивать большие серые круги вокруг. Осмотрите область, покрытую полем зрения, обозначенным более темными точками.


Если вы хотите увидеть ActionScript для презентации выше, не стесняйтесь открывать AppFan.as из исходной загрузки — это прокомментировано для облегчения понимания. Я приведу здесь только важный фрагмент, который проверяет условия.

Я выделил условное утверждение, что каждая из маленьких точек на сцене сравнивается с ним, чтобы увидеть, находится ли он внутри выделенной области.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
//Calculate the magnitude and angle
var vLine2:Vector2D = new Vector2D(b2.x — b1.x, b2.y — b1.y);
var vLine3:Vector2D = new Vector2D(b3.x — b1.x, b3.y — b1.y);
var ang:Number = Math.abs(vLine2.angleBetween(vLine3)) //Eliminate directional feature of angle
var mag:Number = vLine2.getMagnitude();
 
for each (var item:Ball in sp) {
    var vParticle1:Vector2D = new Vector2D(item.x — b1.x, item.y — b1.y);
     
    //Checking if falls within sector
    //Condition: Magnitude less than mag, angle between particle ang vLine2 less than ang
     if(Math.abs(vLine2.angleBetween(vParticle1)) <ang
     && mag> vParticle1.getMagnitude()){
        item.col = 0x000000;
    }
    //if outside of segment, original color
    else item.col = 0xCCCCCC;
    item.draw();
}

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

Хорошо, возможно, вы не уверены, так что вот еще один пример другого использования. Охранник несет короткий меч, лук и стрелы. Когда враг слишком далеко, он может просто хотеть пристально следить за ним. Когда враг попадает в стрельбище, он стреляет стрелами. Когда враг подходит слишком близко, он сражается с коротким мечом. Вы можете даже использовать различные типы охранников: лучники и мечники. С врагами в пределах стрельбища борются лучники, в то время как с близкого расстояния — мечники.

Понимая условия, представленные в шаге 2, можно внести изменения в нашу симуляцию или игру.


Я включил презентацию Flash ниже, чтобы продемонстрировать различные случаи и соответствующий им набор условий. Нажмите на кнопки, чтобы проверить различные случаи.


Ниже приведен фрагмент кода для реализации условий, изложенных в шаге 7. Вы можете просмотреть весь исходный код в Region2.as чтобы проверить всю реализацию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//Checking done every frame
private function move(e:MouseEvent):void
{
    //Calculate Vector from guard to enemy
    var g_e:Vector2D = new Vector2D(enemy.x — guard.x, enemy.y — guard.y);
    var angle:Number = r3.angleBetween(g_e);
    //Conditions
    var withinSector:Boolean = Math2.degreeOf(Math.abs(angle)) < sector;
    var withinR3:Boolean = g_e.getMagnitude() < r3.getMagnitude();
    var withinR2:Boolean = g_e.getMagnitude() < r2.getMagnitude();
    var withinR1:Boolean = g_e.getMagnitude() < r1.getMagnitude();
     
    //Difference example cases
    if (example == 0) {
        if (withinSector && withinR3) {
            t1.text = «Within FOV»
        }
        else t1.text = «Beyond FOV»
    }
    else if (example == 1) {
        if (withinSector && withinR3 && !withinR1) {
            t1.text = «In between \nfar and near attenuation»
        }
        else t1.text = «Beyond FOV»
    }
    else if (example == 2) {
        if (withinSector) {
            if (withinR1) t1.text =»Sword attack»
            else if (withinR2) t1.text = «Arrow shoot»
            else if (withinR3) t1.text = «Keep observe»
        }
        else t1.text = «Beyond FOV»
    }
}
 
//Swapping example cases in response to changes in context menu
private function swap(e:ContextMenuEvent):void
{
    //swap example
    if (e.target.caption == &quot;Basic FOV&quot;) example = 0;
    else if (e.target.caption == &quot;Far/Near Attenuation&quot;) example = 1;
    else if (e.target.caption == &quot;Observe/Arrow/Sword&quot;) example = 2;
     
    //Redraw region indicating detection area
    drawRegion();
}

Вот реализация идей, объясненных на шаге 6. Нажмите на черный кружок и перетащите его вокруг сцены, чтобы проверить, находится ли он в видимой области. Щелкните правой кнопкой мыши по сцене, чтобы открыть контекстное меню, затем выберите «Базовое поле зрения», «Затухание вдали / рядом» и «Наблюдать / Стрелка / Меч», чтобы просмотреть различные примеры.


Теперь, чтобы поместить в контекст все, что мы узнали, мы собираемся создать симуляцию. Вот сценарий:

Башня установлена ​​на одном конце сцены. Его роль состоит в том, чтобы уничтожить как можно больше войск, которые вторгаются в его пространство. Конечно, турель должна будет видеть эти войска (в поле зрения), чтобы стрелять лазерами — и пока прощайте войска. Поскольку в любой момент он может стрелять только лазерным лучом, он выберет ближайшего противника в поле зрения.

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

Изображение, изображающее сценарий 1

Реализация этого примера будет жестко закодирована. Основные настройки, как показано ниже. При инициализации мы нарисуем графику элементов (река, войска, турель) и расположение затем красиво. При нажатии на фиолетовую башню, анимация начнется. На каждом кадре мы увидим их анимацию в соответствии с поведением каждого элемента.

01
02
03
04
05
06
07
08
09
10
11
12
13
public function Scene1() {
    makeTroops();
    makeRiver();
    makeTurret();
    turret.addEventListener(MouseEvent.MOUSE_DOWN, start);
    function start ():void {
        stage.addEventListener(Event.ENTER_FRAME, move);
    }
}
private function move(e:Event):void {
    behaviourTroops();
    behaviourTurret();
}

Ниже приведены переменные этого класса.

01
02
03
04
05
06
07
08
09
10
private var river:Sprite;
         
private var troops:Vector.<Ball>;
private var troopVelo:Vector.<Vector2D>;
 
private var turret:Sprite;
private var fieldOfView:Sprite;
 
private var lineOfSight:Vector2D = new Vector2D (0, -300);
private var sectorOfSight:Number = 20 //Actually half of sector, in degrees

Рисование и позиционирование реки. Довольно просто

01
02
03
04
05
06
07
08
09
10
11
private function makeRiver():void {
    river = new Sprite;
     
    //Specify the location & draw graphics of river
    with (river) {
        x = 0;
        graphics.beginFill(0x22BBDD, 0.2);
        graphics.drawRect(0, 0, 500, 50);
        graphics.endFill();
    }
}

Рисование войск будет простым. Тем не менее, я хотел «V» формирования на войсках. Поэтому я сначала располагаю отряд в нижней части буквы «V», а затем войска по обе стороны его крыла. Вы можете отрегулировать его центральное положение через center и расстояние между войсками через xApart и yApart . Обратите внимание, что troops и соответствующие troopVelo имеют одинаковый индекс. Все войска движутся на юг.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private function makeTroops():void {
    troops = new Vector.<Ball>;
    troopVelo = new Vector.<Vector2D>;
     
    //local variables
    var center:Vector2D = new Vector2D(stage.stageWidth * 0.5, 150);
    var xApart:int = 20;
     
    //Locating troops & velocities
    var a:Ball = new Ball;
    ax = center.x;
    //troops heading south
    var aV:Vector2D = new Vector2D(0, 1);
     
    for (var i:int = 1; i < 11; i++) {
        var b:Ball = new Ball;
        bx = center.x + i * xApart;
        var bV:Vector2D = new Vector2D(0, 1);
         
        var c:Ball = new Ball;
        cx = center.x — i * xApart;
        var cV:Vector2D = new Vector2D(0, 1);
    }
}

Здесь мы нарисуем, расположим и сориентируем башню и ее поле зрения лицом к северу к врагу. Обратите внимание, что линия визирования направлена ​​на север.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private function makeTurret():void {
    //instantiate, locate, orient turret
    turret = new Sprite;
    turret.x = stage.stageWidth * 0.5,
    turret.y = stage.stageHeight;
    turret.rotation = -90;
    turretRot = 2;
     
    //Draw turret graphics
    var w:int = 30;
    turret.graphics.beginFill(0x9911AA);
    turret.graphics.lineStyle(2);
    turret.graphics.lineTo(w, -h / 2);
    turret.graphics.lineTo(0, h / 2);
    turret.graphics.endFill();
     
    //Setting data for field of view’s graphics
    var point1:Vector2D = new Vector2D(0, 0);
    var point2:Vector2D = new Vector2D(1, 0);
    var point3:Vector2D = new Vector2D(0, 0);
    point1.polar(lineOfSight.getMagnitude(), Math2.radianOf(sectorOfSight));
    point2.setMagnitude(lineOfSight.getMagnitude()/Math.cos(Math2.radianOf(sectorOfSight)));
    point3.polar(lineOfSight.getMagnitude(), Math2.radianOf(-sectorOfSight));
     
    //instantiate, locate, orient field of view
    fieldOfView = new Sprite;
    fieldOfView.x = turret.x;
    fieldOfView.rotation = -90;
     
    //draw turret’s field of view
    fieldOfView.graphics.beginFill(0xff9933, 0.1);
    fieldOfView.graphics.lineStyle(1);
    fieldOfView.graphics.moveTo(0, 0);
    fieldOfView.graphics.lineTo(point1.x, point1.y);
    fieldOfView.graphics.curveTo(point2.x, point2.y, point3.x, point3.y);
    fieldOfView.graphics.lineTo(0, 0);
    fieldOfView.graphics.endFill();
}

Небольшая деталь здесь на чертеже прямой видимости. Я включил изображение ниже для пояснения:

Изображение для уточнения точек на рисунке FOV

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//troops’ behaviour
private function behaviourTroops():void
{
    //for each troop
    for (var i:int = 0; i < troops.length; i++) {
         
        //If troop reach bottom of screen, respawn on top of screen
        if (troops[i].y > stage.stageHeight) {
            troops[i].y = 0;
        }
        //if wading through river, slow down
        //else normal speed
        if (river.hitTestObject(troops[i])) troops[i].y += troopVelo[i].y*0.3;
        else troops[i].y += troopVelo[i].y
         
        //If troop is dead ( alpha < 0.05 ), respawn on top of screen
        if (troops[i].alpha < 0.05) {
            troops[i].y = 0;
            troops[i].col = 0xCCCCCC;
            //stage.removeChild(troops[i]);
            //troopVelo.splice(i, 1);
        }
    }
}

Башня будет охранять свой пост, поворачивая свое поле зрения вокруг, но в определенных ракурсах. Здесь я определил угол панорамирования от -135 до -45 (используя углы вспышки). Если в поле зрения есть враг, он нападет на него. Но если противника больше, он выберет ближайшего к атаке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//turret’s behaviour
private function behaviourTurret():void
{
    //rotate turret within boundaries of -135 & -45
    if (turret.rotation > -45) turretRot = -2
    else if (turret.rotation < -135) turretRot = 2
     
    //shoot closest enemy within sight
    graphics.clear();
    if (enemyWithinSight() != null) {
         
        //closest enemy in sight
        var target:Ball = enemyWithinSight();
        target.col = 0;
        target.alpha -= 0.2;
         
        //orient turret towards enemy
        var turret2Target:Vector2D = new Vector2D(target.x — turret.x, target.y — turret.y);
        turret.rotation = Math2.degreeOf(turret2Target.getAngle());
         
        //draw laser path to enemy
        graphics.lineStyle(2);
    }
     
    //no enemy within sight, continue scanning
    else { turret.rotation += turretRot }
 
    //turn field of view and line of sight of turret according to turret’s rotation
    fieldOfView.rotation = turret.rotation;
    lineOfSight.setAngle(Math2.radianOf(turret.rotation));
}

Башня найдет ближайшего противника в поле зрения и отреагирует, выпустив в него лазер. Чтобы увидеть, как он находит ближайшего врага, ознакомьтесь с реализацией ActionScript ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//return the closest enemy within sight
private function enemyWithinSight():Ball {
    var closestEnemy:Ball = null;
    var closestDistance:Number = lineOfSight.getMagnitude();
     
    for each (var item:Ball in troops) {
        var turret2Item:Vector2D = new Vector2D(item.x — turret.x, item.y — turret.y);
         
        //check if enemy is within sight
        //1.
        //2.
        //3.
        var c1:Boolean = Math.abs(lineOfSight.angleBetween(turret2Item)) < Math2.radianOf(sectorOfSight) ;
        var c2:Boolean = turret2Item.getMagnitude() < lineOfSight.getMagnitude();
        var c3:Boolean = turret2Item.getMagnitude() < closestDistance;
         
        //if all conditions fulfilled, update closestEnemy
        if (c1 && c2&& c3){
            closestDistance = turret2Item.getMagnitude();
            closestEnemy = item;
        }
    }
    return closestEnemy;
}

Теперь вы можете нажать Ctrl + Enter в FlashDevelop и наблюдать за этим моделированием. Нажмите на башню, чтобы начать демонстрацию ниже.


Мы можем использовать это понимание, чтобы:

  • Реализуйте поле зрения для врагов.
  • Реализуйте больше турелей.
  • Внесите изменения в поле зрения, как описано в шаге 9.

…и так далее.

Надеюсь, это вызовет некоторые идеи и, возможно, поможет в вашей следующей симуляции или игре.

Спасибо за прочтение. Как обычно, напишите комментарий, чтобы сообщить мне, было ли это полезно для вас. Я напишу следующий урок, чтобы проверить, как враги могут оставаться вне поля зрения турели, «прячась» за препятствиями. Будьте на связи.