Статьи

Генерация призраков, которые следуют по вашим стопам

Следование по пути — это простая концепция для понимания: объект перемещается из точки A в точку B в точку C и так далее. Но что, если мы хотим, чтобы наш объект шел по пути игрока, как призраки в гоночных играх? В этом уроке я покажу вам, как этого добиться с помощью точек в AS3.


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


Давайте предположим, что игрок перемещает 4 единицы влево и 2 единицы вниз от нашей исходной точки. Чтобы наш призрак оказался в том же месте, ему также нужно переместить 4 единицы влево и 2 единицы вниз из той же точки происхождения. Теперь предположим, что наш игрок движется со скоростью 2; для того, чтобы следующий путь оставался точным, наш призрак также будет иметь скорость 2.

Что если наш игрок решит сделать паузу, прежде чем продолжить? Очевидное решение для призрака — отслеживать точное положение игрока каждый тик — но это потребует хранения большого количества данных. Вместо этого мы просто сохраняем данные каждый раз, когда игрок нажимает разные клавиши — поэтому, если игрок движется вправо в течение десяти секунд, мы будем хранить такое же количество данных, как если бы игрок двигался вправо в течение полсекунды.

Чтобы эта техника работала, наш призрак должен соблюдать следующие правила:

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

Начните с создания нового файла Flash (ActionScript 3.0). Установите ширину 480, высоту 320 и количество кадров в секунду до 30. Оставьте фоновый цвет белым и сохраните файл как CreatingGhosts.fla ; наконец, установите его класс для CreatingGhosts .

Прежде чем перейти к занятиям, нам нужно создать пару мувиклипов. Начните с рисования двух отдельных квадратов размером 20 пикселей без обводки. Преобразуйте первую заливку в мувиклип, установив его регистрацию в центре, присвоив ему имя player и экспортировав его в ActionScript с именем класса Player . Теперь повторите тот же процесс, за исключением того, что замените имя на ghost а класс на Ghost . Уберите эти мувиклипы со сцены.

Создайте свой класс документов с помощью следующего кода:

01
02
03
04
05
06
07
08
09
10
11
package{
    import flash.display.*;
    import flash.events.*;
     
    public class CreatingGhosts extends MovieClip{
        public var player:Player = new Player();
        public function CreatingGhosts(){
            addChild(player);
        }
    }
}

Самоочевидный; Нашим следующим шагом будет настройка класса Player:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package{
    import flash.display.*;
    import flash.events.*;
    import flash.geom.Point;
    import flash.ui.Keyboard;
    import flash.utils.Timer;
    import flash.utils.getTimer;
     
    public class Player extends MovieClip{
        public var startPos:Point;
        public var startTime:int;
        public var speed:Number = 2;
        public var currentLife:int;
        public var keyPressLeft:Boolean = false;
        public var keyPressRight:Boolean = false;
        public var keyPressUp:Boolean = false;
        public var keyPressDown:Boolean = false;
        public function Player(){
             
        }
    }
}

Первые три переменные используются для соответствия правилам; startPos является нашей отправной точкой, startTime — это время, когда игрок был добавлен на сцену, а speed — наша скорость движения. currentLife — это дополнение, используемое для проверки того, сколько раз игрок умер, соответственно, каждый путь сохраняется и может быть получен через это значение. Последние четыре переменные используются для проверки нажатий клавиш.

Пришло время создать класс Ghost :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package{
    import flash.display.*;
    import flash.events.*;
    import flash.geom.Point;
    import flash.utils.getTimer;
    import flash.utils.Timer;
     
    public class Ghost extends MovieClip{
        static public var waypoints:Array = new Array();
        static public var times:Array = new Array();
        public var i:int = 0;
        public var startTime:int;
        public var speed:Number = 2;
        public var selectedLife:int;
        public function Ghost(){
             
        }
    }
}

Две статические переменные, waypoints и times , будут использоваться для хранения массивов; первый будет хранить координаты позиций игрока всякий раз, когда игрок меняет движение, а второй будет хранить время, когда происходило каждое изменение. Другие переменные соответствуют переменным из класса Player .


В конструкторе Player добавьте следующую строку:

1
addEventListener(Event.ADDED_TO_STAGE, init);

Затем создайте функцию init() :

1
2
3
public function init(e:Event){
 
}

Во-первых, нам нужно получить startTime и startTime новый массив времени в массив времен Ghost. (Это немного сбивает с толку; у призрака есть несколько временных массивов, чтобы позволить ему иметь дело с несколькими жизнями в будущем.)

1
2
3
4
startTime = flash.utils.getTimer();
Ghost.times.push(new Array);
currentLife = Ghost.times.length — 1;
Ghost.times[currentLife].push(flash.utils.getTimer() — startTime);

startTime устанавливается на текущее время (значение в миллисекундах); мы добавляем новый дочерний массив в массив Ghost’s times; наш currentLife установлен в индекс этого нового массива; и мы помещаем время, прошедшее во время этой функции, в первый элемент этого нового массива.

Теперь мы устанавливаем начальную позицию:

1
2
3
4
5
startPos = new Point(stage.stageWidth/2, stage.stageHeight/2);
this.x = startPos.x;
this.y = startPos.y;
Ghost.waypoints.push(new Array);
Ghost.waypoints[currentLife].push(startPos);

Наша точка происхождения находится в центре сцены; мы перемещаем нашего игрока в начало координат; новый массив добавляется в массив waypoints в классе Ghost; и первая позиция помещается в этот массив.

Итак, на данный момент Ghost.times[0][0] содержит количество миллисекунд с момента установки SWF (практически нулевого), а Ghost.waypoints[0][0] содержит точку, установленную в центр сцена.

Наша цель состоит в том, чтобы закодировать это так, что если через одну секунду игрок Ghost.times[0][1] клавишу, то для Ghost.times[0][1] будет установлено значение 1000, а для Ghost.waypoints[0][1] будет другая точка , снова установите в центр (потому что игрок еще не будет двигаться). Когда игрок отпускает эту клавишу (или нажимает другую), Ghost.times[0][2] будет установлен на текущее время, а Ghost.waypoints[0][2] будет точкой, соответствующей позиции игрока в это время.

Теперь вот три слушателя событий:

1
2
3
addEventListener(Event.ENTER_FRAME, enterFrame);
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, keyUp);

А пока давайте проигнорируем enterFrame и сосредоточимся на нажатиях клавиш.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public function keyDown(e:KeyboardEvent){
    if (e.keyCode == Keyboard.LEFT && keyPressLeft == false){
        updateWaypoints();
        keyPressLeft = true;
    }else if (e.keyCode == Keyboard.RIGHT && keyPressRight == false){
        updateWaypoints();
        keyPressRight = true;
    }
 
    if (e.keyCode == Keyboard.UP && keyPressUp == false){
        updateWaypoints();
        keyPressUp = true;
    }else if (e.keyCode == Keyboard.DOWN && keyPressDown == false){
        updateWaypoints();
        keyPressDown = true;
    }
 
    if (e.keyCode == Keyboard.SPACE){
        destroy();
    }
}

Всего несколько простых операторов if для предотвращения ошибок при нажатии клавиш и две новые вызываемые функции. updateWaypoints() будет вызываться каждый раз, когда новые точки и времена будут добавляться в массивы призраков, а destroy() используется для удаления игрока и добавления призрака на сцену. Но прежде чем мы перейдем к этим функциям, давайте завершим функции нажатия клавиш.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public function keyUp(e:KeyboardEvent){
    if (e.keyCode == Keyboard.LEFT && keyPressLeft == true){
        updateWaypoints();
        keyPressLeft = false;
    }else if (e.keyCode == Keyboard.RIGHT && keyPressRight == true){
        updateWaypoints();
        keyPressRight = false;
    }
 
    if (e.keyCode == Keyboard.UP && keyPressUp == true){
        updateWaypoints();
        keyPressUp = false;
    }else if (e.keyCode == Keyboard.DOWN && keyPressDown == true){
        updateWaypoints();
        keyPressDown = false;
    }
}

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

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

Но что произойдет, если игрок решит случайно отпустить ключ и снова вызвать изменение? Что ж, мы учитываем это путем обновления путевых точек и времени снова. Если бы это не было сделано, Призрак не смог бы объяснить повороты на 90 градусов; вместо этого он будет двигаться под углом к ​​следующей точке.


Наша updateWaypoints() довольно проста, поскольку состоит из кода, который мы уже написали:

1
2
3
4
public function updateWaypoints(){
    Ghost.times[currentLife].push(flash.utils.getTimer() — startTime);
    Ghost.waypoints[currentLife].push(new Point(this.x, this.y));
}

Функция destroy() так же проста! Путевые точки обновляются, добавляется Ghost, слушатели событий останавливаются, а наш Player удаляется:

1
2
3
4
5
6
7
8
9
public function destroy(){
    updateWaypoints();
    var ghost:Ghost = new Ghost();
    parent.addChild(ghost);
    removeEventListener(Event.ENTER_FRAME, enterFrame);
    stage.removeEventListener(KeyboardEvent.KEY_DOWN, keyDown);
    stage.removeEventListener(KeyboardEvent.KEY_UP, keyUp);
    parent.removeChild(this);
}

Начните с создания функции:

1
2
3
public function enterFrame(e:Event){
     
}

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

01
02
03
04
05
06
07
08
09
10
11
12
if((this.x-(this.width/2)) > 0){
         
}
if((this.x+(this.width/2)) < stage.stageWidth){
         
}
if((this.y-(this.height/2)) > 0){
         
}
if((this.y+(this.height/2)) < stage.stageHeight){
     
}

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

1
2
3
4
5
6
7
8
if(keyPressLeft == true){
    if((this.x-(this.width/2)) <= 0){
        updateWaypoints();
        this.x = this.width/2;
    }else{
        this.x -= speed;
    }
}

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

Точно то же самое сделано для других трех сторон:

1
2
3
4
5
6
7
8
if(keyPressRight == true){
    if((this.x+(this.width/2)) >= stage.stageWidth){
        updateWaypoints();
        this.x = (stage.stageWidth — (this.width/2));
    }else{
        this.x += speed;
    }
}
1
2
3
4
5
6
7
8
if(keyPressUp == true){
    if((this.y-(this.height/2)) <= 0){
        updateWaypoints();
        this.y = this.height/2;
    }else{
        this.y -= speed;
    }
}
1
2
3
4
5
6
7
8
if(keyPressDown == true){
    if((this.y+(this.height/2)) >= stage.stageHeight){
        updateWaypoints();
        this.y = (stage.stageHeight — (this.height/2));
    }else{
        this.y += speed;
    }
}

И с этим мы закончили с классом игрока!


Добавьте следующую строку в конструктор Ghost:

1
addEventListener(Event.ADDED_TO_STAGE, init);

Как и перед созданием функции init() :

1
2
3
4
5
6
7
public function init(e:Event){
    selectedLife = times.length — 1;
    this.x = waypoints[selectedLife][0].x;
    this.y = waypoints[selectedLife][0].y;
    startTime = flash.utils.getTimer();
    addEventListener(Event.ENTER_FRAME, enterFrame);
}

Мы начнем с выбора пути, который мы хотим использовать (по умолчанию он выберет последний массив); затем мы помещаем призрака в начало координат и устанавливаем время начала нашего призрака. Затем создается прослушиватель событий для enterFrame.


Естественно, мы создаем нашу функцию enterFrame:

1
2
3
public function enterFrame(e:Event){
 
}

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

1
2
3
while (i < times[selectedLife].length — 1 && flash.utils.getTimer() — startTime >= times[selectedLife][i]) {
    i++;
}

Следующее, что нужно сделать, это переместить Ghost, если прошедшее время меньше текущего времени из массива:

1
2
3
if (flash.utils.getTimer() — startTime < times[selectedLife][i]) {
    updatePosition();
}

Мы начнем этот шаг с создания функции updatePosition() :

1
2
3
public function updatePosition(){
 
}

Затем добавьте две переменные, чтобы представить разницу и расстояние между старой и новой позицией:

1
2
var diff:Point = waypoints[selectedLife][i].subtract(new Point(this.x, this.y));
var dist = diff.length;

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

1
2
3
4
5
6
7
8
if (dist <= speed){
    this.x = waypoints[selectedLife][i].x;
    this.y = waypoints[selectedLife][i].y;
}else{
    diff.normalize(1);
    this.x += diff.x * speed;
    this.y += diff.y * speed;
}

Сначала мы проверяем, меньше ли расстояние, чем скорость (т. Е. Расстояние, на которое призрак перемещается на каждый тик); если так, мы перемещаем Призрака прямо в точку. Однако, если расстояние меньше, мы нормализуем разницу («означает, что его величина равна 1, при этом сохраняя направление и смысл вектора» — евклидовы векторы во Flash ), и мы увеличиваем положение Призрака в направлении точки.


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

Во-первых, настройка вашего SWF-файла на несжатый в настройках публикации; это приведет к увеличению времени загрузки при запуске, однако производительность будет более плавной. Второй вариант предпочтительнее, если вы планируете компилировать свой проект как исполняемый файл для автономного использования: просто увеличьте частоту кадров примерно до 60.


Спасибо, что нашли время, чтобы прочитать этот учебник! Если у вас есть какие-либо вопросы или комментарии, пожалуйста, оставьте их ниже. А если вам нужна дополнительная задача, попробуйте настроить класс Ghost так, чтобы он следовал по пути в обратном или замедленном режиме.