Следование по пути — это простая концепция для понимания: объект перемещается из точки A в точку B в точку C и так далее. Но что, если мы хотим, чтобы наш объект шел по пути игрока, как призраки в гоночных играх? В этом уроке я покажу вам, как этого добиться с помощью точек в AS3.
Окончательный результат предварительного просмотра
Нажмите на SWF, затем используйте клавиши со стрелками для перемещения. Нажмите пробел, чтобы переключиться на призрака, который будет следовать пути, который вы создали.
Логика позади пути следования
Давайте предположим, что игрок перемещает 4 единицы влево и 2 единицы вниз от нашей исходной точки. Чтобы наш призрак оказался в том же месте, ему также нужно переместить 4 единицы влево и 2 единицы вниз из той же точки происхождения. Теперь предположим, что наш игрок движется со скоростью 2; для того, чтобы следующий путь оставался точным, наш призрак также будет иметь скорость 2.
Что если наш игрок решит сделать паузу, прежде чем продолжить? Очевидное решение для призрака — отслеживать точное положение игрока каждый тик — но это потребует хранения большого количества данных. Вместо этого мы просто сохраняем данные каждый раз, когда игрок нажимает разные клавиши — поэтому, если игрок движется вправо в течение десяти секунд, мы будем хранить такое же количество данных, как если бы игрок двигался вправо в течение полсекунды.
Чтобы эта техника работала, наш призрак должен соблюдать следующие правила:
- Призрак и игрок имеют одинаковую точку происхождения.
- Призрак должен следовать по тому же пути, что и игрок.
- Призрак должен двигаться с той же скоростью, что и игрок.
- Призрак должен хранить текущее время каждый раз, когда изменяется движение игрока.
Шаг 1: Настройка
Начните с создания нового файла 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
.
Шаг 2: инициализация проигрывателя
В конструкторе 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);
|
Шаг 3: Ключевые события
А пока давайте проигнорируем 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 градусов; вместо этого он будет двигаться под углом к следующей точке.
Шаг 4: Обновление и уничтожение
Наша 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);
}
|
Шаг 5: игровой кадр игрока
Начните с создания функции:
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;
}
}
|
И с этим мы закончили с классом игрока!
Шаг 6: Инициализация Призрака
Добавьте следующую строку в конструктор 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.
Шаг 7: входящий кадр Призрака
Естественно, мы создаем нашу функцию 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();
}
|
Шаг 8: Обновление позиции призрака
Мы начнем этот шаг с создания функции 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 ), и мы увеличиваем положение Призрака в направлении точки.
Шаг 9: примечание
Что следует отметить в этом методе, так это то, что он использует много ресурсов ЦП для непрерывной загрузки времени и точек, а иногда может привести к некоторой задержке, даже если логика верна. Однако мы нашли два способа противостоять этому!
Во-первых, настройка вашего SWF-файла на несжатый в настройках публикации; это приведет к увеличению времени загрузки при запуске, однако производительность будет более плавной. Второй вариант предпочтительнее, если вы планируете компилировать свой проект как исполняемый файл для автономного использования: просто увеличьте частоту кадров примерно до 60.
Вывод:
Спасибо, что нашли время, чтобы прочитать этот учебник! Если у вас есть какие-либо вопросы или комментарии, пожалуйста, оставьте их ниже. А если вам нужна дополнительная задача, попробуйте настроить класс Ghost так, чтобы он следовал по пути в обратном или замедленном режиме.