Статьи

Имитация движения снаряда с помощью ActionScript 3.0

В этом уроке я собираюсь познакомить вас с процессом моделирования основного движения снаряда с помощью Flash и ActionScript. Наше моделирование будет динамически оживлять начальную траекторию снаряда и его последующие отскоки от земли.


Вот быстрый предварительный просмотр того, над чем мы будем работать:


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

В этом уроке я собираюсь познакомить вас с процессом моделирования основного движения снаряда с помощью Flash и ActionScript. Наше моделирование будет динамически оживлять начальную траекторию снаряда и его последующие отскоки от земли. Для простоты я не включил сопротивление воздуха, поэтому в данном примере снаряд будет испытывать только одну силу; сила тяжести. В этом руководстве предполагается, что у вас есть базовые знания в области объектно-ориентированного программирования и ActionScript. Вам также может быть полезно иметь некоторый опыт работы с квадратичными функциями и базовыми понятиями движения, такими как ускорение и скорость . Хотя я в основном собираюсь рассказать о применении этих концепций применительно к Flash и ActionScript, сначала я хочу кратко объяснить их, как они применяются к классической механике.

Классическая механика — это раздел физики, основанный на законах движения Ньютона. В этом уроке мы рассмотрим две концепции классической механики: движение снаряда и коэффициент восстановления. Движение снаряда описывает путь (траекторию) объекта с начальной скоростью, в то же время испытывая ускорение от силы тяжести (и в большинстве случаев сопротивления воздуха). Вы, вероятно, знакомы с историей Ньютона, формулирующего его теорию гравитации, сидя под яблоней. Гравитация играет большую роль в движении снаряда, поскольку она добавляет постоянное ускорение -9,8 м / с 2 в направлении у.

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

Функция положения

Эта функция дает нам позицию снаряда по времени ( d (t) — позиция после того, как прошло t секунд). Маленький треугольник означает «изменение», поэтому Δt означает «количество пройденных секунд».

Чтобы использовать эту функцию, нам нужно знать:

  • Ускорение а снаряда;
  • Начальная скорость (скорость в определенном направлении), v 1 , снаряда;
  • Начальная позиция d 1 снаряда.

Путь снаряда в направлениях x и y не зависит. Это означает, что для вычисления положения в 2-мерном пространстве нам нужно использовать функцию дважды.

Другая концепция, которую мы собираемся рассмотреть, называется коэффициентом реституции . Коэффициент восстановления — это коэффициент для описания разницы в скорости объекта до и после столкновения.

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

Коэффициент реституции

Здесь v 1 — это скорость мяча перед тем, как он коснется земли, а v 2 — это скорость мяча сразу после того, как он упал на землю и снова начал подпрыгивать вверх.

Поскольку коэффициент восстановления различен для каждой комбинации объектов, вам нужно будет найти свой собственный, в зависимости от того, что вы моделируете.


Давайте начнем строить эту вещь! Откройте Flash и создайте новый документ. В этом примере я буду использовать размеры по умолчанию 550 x 400 и частоту кадров 50 FPS. Сохраните этот файл под именем по вашему выбору.

Настройка документа

Далее нам нужно создать класс документа. Создайте новый файл Actionscript и добавьте следующее:

01
02
03
04
05
06
07
08
09
10
11
package
{
    import flash.display.MovieClip;
     
    public class Main extends MovieClip
    {
        public function Main():void{
         
        }
    }
}

Сохраните этот файл в том же каталоге, что и наш FLA. Назовите это Main.as.

Последнее, что нам нужно сделать, это связать класс документа с FLA. Внутри FLA найдите панель « Свойства» . Рядом с Классом документа введите имя класса документа, Main .

Класс документа

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


Теперь, когда наш флеш-документ настроен, мы можем начать кодирование. Первое, что нам нужно сделать, это создать цикл, который будет работать с фиксированным интервалом времени. Говоря в общем, есть две последовательности мысли относительно временных циклов: используя событие ENTER_FRAME или используя класс таймера AS3. Оба метода имеют разные плюсы и минусы, но для этого примера я собираюсь использовать класс Timer .

Нам нужно начать с импорта двух классов: класса Timer и соответствующего ему класса TimerEvent . Внутри Main.as добавьте следующие строки.

1
2
import flash.utils.Timer;
import flash.events.TimerEvent;

Прежде чем мы настроим Timer , я хочу добавить еще один метод, mainLoop, в класс документа. Как следует из названия, этот метод будет служить нашей петлей.

1
2
3
public function mainLoop(event:TimerEvent):void{
 
}

Теперь мы можем настроить таймер! Мы собираемся начать с добавления двух свойств к классу: masterTimer и interval . Назначение masterTimer должно быть очевидным, но свойство interval будет использоваться для хранения длины (в секундах) интервала Timer.

1
2
public var masterTimer:Timer;
public var interval:Number;

Далее мы собираемся заполнить эти свойства внутри нашего метода конструктора Main . На шаге 1 мы устанавливаем частоту кадров нашего документа 50 кадров в секунду. На эту частоту кадров можно ссылаться с помощью stage.frameRate , и она будет использоваться в качестве основы для вычисления интервала таймера.

1
2
3
4
5
6
7
8
// Get the document’s frame rate
var fps:int = stage.frameRate;
 
// The timer interval in seconds
interval = 1/fps;
 
// Create a new instance of the timer, first parameter requires the interval in milliseconds
masterTimer = new Timer(1000/fps);

Timer генерирует событие TimerEvent.TIMER каждый интервал (в нашем случае 50 раз в секунду или один раз каждые 20 миллисекунд). Добавив прослушиватель событий в Timer, мы можем запускать наш метод mainLoop каждый раз, когда генерируется это событие, создавая наш цикл.

1
2
// Connect our loop method to the timer.
masterTimer.addEventListener(TimerEvent.TIMER, mainLoop);

Осталось только запустить таймер.

1
2
// Start the Timer
masterTimer.start();

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

1
2
// Calculate the current time
var currentTime:Number = int((event.target.currentCount * interval) * 1000) / 1000;

Если мы проследим currentTime , мы увидим, как работает таймер.

1
2
// Test the loop by tracing the current time
trace(«Current Time: » + currentTime);

Проверьте фильм. Ваш выходной журнал должен выглядеть примерно так:

Выходной журнал теста таймера

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


Наш следующий шаг — создать класс, представляющий снаряд (баскетбол). Создайте еще один файл ActionScript и еще раз добавьте нашу базовую структуру классов. Сохраните этот файл как Projectile.as .

01
02
03
04
05
06
07
08
09
10
11
package
{
    import flash.display.MovieClip;
     
    public class Projectile extends MovieClip
    {
        public function Projectile():void{
         
        }
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Initial Velocity
private var v1X:Number;
private var v1Y:Number;
 
// Acceleration
private var aX:Number;
private var aY:Number;
 
// Initial Position
private var d1X:Number;
private var d1Y:Number;
 
// Animation-specific
private var startTime:Number;
private var moving:Boolean;

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

Первый метод solveQuadratic возвращает корни квадратичной функции (в стандартной форме: Ax 2 + Bx + C; этот метод возвращает значение x) с использованием квадратной формулы .

01
02
03
04
05
06
07
08
09
10
private function solveQuadratic(a:Number, b:Number, c:Number):Array{
    // Method paramters format: Ax² + Bx + C
    var solution:Array = new Array(2);
    var discriminant:Number = (b * b) — (4 * a * c);
     
    solution[«root1»] = ((-1 * b) + Math.sqrt(discriminant)) / (2 * a);
    solution[«root2»] = ((-1 * b) — Math.sqrt(discriminant)) / (2 * a);
 
    return solution;
}

Второй метод, getComponents , возвращает массив с отдельными компонентами x и y евклидова вектора .

1
2
3
4
5
6
7
8
9
private function getComponents(angleDegrees:Number, magnitude:Number):Array{
    var components:Array = new Array(2);
     
    // Trig functions require angles in radians (1 radian = PI/180 degrees)
    components[«x»] = magnitude * Math.cos(angleDegrees * Math.PI / 180);
    components[«y»] = magnitude * Math.sin(angleDegrees * Math.PI / 180);
     
    return components;
}

Третий метод, getPosition , вернет позицию в данный момент времени. Мы используем формулу позиции из введения; это д (т) .

1
2
3
4
5
6
7
8
private function getPosition(time:Number, acceleration:Number, initialVelocity:Number, initialPosition:Number):Number{
    var position:Number;
     
    // d(t) = (1/2)*a*(t^2) + v_1*t + d_1
    position = (0.5 * acceleration * time * time) + (initialVelocity * time) + (initialPosition);
     
    return position;
}

Четвертый метод, getTimes , является противоположностью getPosition . Возвращает время, необходимое снаряду для достижения заданной позиции.

1
2
3
4
5
6
7
8
private function getTimes(finalPosition:Number, initialPosition: Number, acceleration:Number, initialVelocity:Number):Array{
    var time:Array;
     
    // Solve quadratic position function: 0 = (1/2)*a*(t^2) + v_1*t + d1-d2
    time = solveQuadratic(0.5 * acceleration, initialVelocity, initialPosition — finalPosition);
     
    return time;
}

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

1
2
3
4
5
6
7
8
private function getVelocity(acceleration:Number, initialVelocity:Number, time:Number):Number{
    var velocity:Number;
     
    // Velocity function is first derivative of position function: d’ = a*t + v1
    velocity = acceleration * time + initialVelocity
     
    return velocity;
}

Шестой метод, getVelocityDirection , возвращает направление (угол) скорости в данный момент времени. Другими словами, он возвращает направление движения снаряда в определенный момент.

01
02
03
04
05
06
07
08
09
10
private function getVelocityDirection(accelerationX:Number, accelerationY:Number, initialVelocityX:Number, initialVelocityY:Number, time:Number):Number{
    var angle:Number;
     
    var velocityX:Number = getVelocity(accelerationX, initialVelocityX, time);
    var velocityY:Number = getVelocity(accelerationY, initialVelocityY, time);
     
    angle = Math.atan2(velocityY, velocityX) * 180 / Math.PI;
     
    return angle;
}

На шаге 4 мы добавили несколько свойств в класс Projectile. Если вы заметили, все эти свойства являются частными. Я сделал это, потому что мы не хотим получать доступ к любому из них напрямую. Нам все еще нужен способ установить и предварительно выполнить необходимые вычисления для этих свойств, поэтому мы собираемся добавить метод init для этого.

1
2
3
public function init(velocityDirection:Number, initialVelocity:Number, initialPositionX:Number, initialPositionY:Number, acceleration:Number = 0, accelerationDirection:Number = 0, gravity:Number = -9.8):void{
 
}

Названия разных параметров должны быть достаточно очевидными. Параметр speedDirection принимает угол начальной скорости в градусах. Все параметры ускорения являются необязательными. Если вы установите значение для ускорения , вам нужно будет указать отдельное значение для accelerationDirection, как мы это делали с speedDirection.

Ранее я упоминал, что мы имеем дело с компонентами, однако наш метод init принимает только вектор (величину и направление). Вот где наши удобные методы вступают в игру.

1
2
3
// Get components for our velocity and acceleration vectors
var vComponents:Array = getComponents(velocityDirection, initialVelocity);
var aComponents:Array = getComponents(accelerationDirection, acceleration);

Теперь у нас есть массив, содержащий отдельные компоненты векторов. Теперь мы можем хранить их с соответствующими свойствами.

1
2
3
4
5
6
// Store these vectors in the corresponding properties
v1X = vComponents[«x»];
v1Y = vComponents[«y»];
 
aX = aComponents[«x»];
aY = aComponents[«y»] + gravity;

Точки для начальной позиции не нужно менять.

1
2
3
// Store the initial position
d1X = initialPositionX;
d1Y = initialPositionY;

Прежде чем мы продолжим, мне нужно объяснить специфические для анимации свойства, которые мы создали на шаге 4. Я начну со свойства moving . Поскольку наш снаряд имеет состояние покоя, мы используем свойство перемещения в качестве флага, поэтому цикл анимируется только тогда, когда снаряд находится в движении. Свойство startTime используется для учета разницы между глобальным временем таймера и местным временем функции позиционирования снаряда. Ничего сложного.

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

01
02
03
04
05
06
07
08
09
10
11
12
public function begin(currentTime:Number):void{
    startTime = currentTime;
    moving = true;
}
 
public function end():void{
    moving = false;
}
 
public function isMoving():Boolean{
    return moving;
}

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

1
2
3
4
5
6
7
8
9
public function positionAtTime(currentTime:Number):Array{
    var relativeTime:Number = int((currentTime — startTime) * 1000) / 1000;
    var position:Array = new Array(2);
     
    position[«x»] = getPosition(relativeTime, aX, v1X, d1X);
    position[«y»] = getPosition(relativeTime, aY, v1Y, d1Y);
     
    return position;
}

Если вы посмотрите внутрь этого метода, вы должны заметить пару вещей. Во-первых, это использование переменнойlativeTime. Ранее я упоминал, что нам нужно учитывать разницу во времени между Таймером и Снарядом . Начало траектории снаряда начинается с относительного времени 0, поэтому нас интересует только количество времени, которое прошло на Таймере с момента начала этой траектории. Вот почему мы храним startTime . Второе — это использование нашего метода getPosition из шага 5. Мы используем этот метод дважды, чтобы вычислить положение снаряда по обеим осям.

Сохранить класс снаряда.


Прежде чем мы сможем оживить, нам нужно что-то оживить. Если вы заметили, наш класс Projectile расширяет класс MovieClip , что означает, что мы можем связать его с MovieClip в нашей библиотеке.

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

Создать круг

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

Теперь нам нужно связать наш класс Projectile с MovieClip. В вашей библиотеке щелкните правой кнопкой мыши (Control нажмите на Mac) ваш MovieClip и выберите Linkage …

Выберите связь ...

Установите флажок « Экспорт для ActionScript» . Введите мяч для класса и снаряд для базового класса .

Связь ...

Нажмите ОК . Вы можете увидеть диалоговое окно с сообщением о том, что определение класса не найдено, и оно будет сгенерировано автоматически. Нажмите OK еще раз.

Класс не найден

Наконец, сохраните документ.


У нас есть цикл, мы написали наш класс Projectile и связали его с помощью MovieClip. Пришло время оживить снаряд! Для начала откройте наш класс документов Main.as.

Начнем с добавления еще нескольких свойств.

1
2
3
public var scale:int;
public var ball:Ball;
public var startFlag:Boolean;

Цель этих свойств довольно проста. Свойство scale будет представлять масштаб между положением пикселя Flash и нашим расчетным положением метра. Свойство ball представляет мувиклип, который мы создали на предыдущем шаге. Вы можете заметить, что его тип установлен в Ball , это имя класса, которое мы задали на предыдущем шаге. Наконец, свойство startFlag используется в качестве флага, чтобы сообщить циклу, когда наш снаряд должен начать свою траекторию.

Внутри метода конструктора мы добавим следующие строки для настройки нашего объекта Ball.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// 1 metre = «scale» pixels
scale = 100;
 
// Create a new instance of the Ball
ball = new Ball();
 
// Set the size of the ball, in this case it’s 0.5mx 0.5m
ball.height = 0.5 * scale;
ball.width = 0.5 * scale;
 
// Position the ball on the stage
ball.x = 50;
ball.y = 350;
 
// Add the ball to the stage
stage.addChild(ball);

Если вы тестируете фильм, ваш мяч должен появиться в левом нижнем углу сцены.

Добавить мяч на сцену

Далее нам нужно запустить метод init из шага 6. Если вы посмотрите на объявление метода, то увидите, что для него требуется минимум 4 параметра: speedDirection , initialVelocity , initialPositionX и initialPositionY . Оставаясь в методе конструктора, давайте настроим их!

1
2
// Run the projectile init method to set the properties of our projectile
ball.init(80, 7, ball.x / scale, (stage.stageHeight — ball.y) / scale);

Параметры для начальной позиции требуют некоторых простых вычислений, прежде чем они будут переданы в метод. Методы снаряда все используют метры в качестве единицы расстояния, поэтому мы должны использовать нашу шкалу. InitialPositionY требует дополнительного шага. Так как начало координат Flash существует в верхнем левом углу, а увеличение y перемещает экран вниз , нам необходимо вычесть нашу позицию y из stage.stageHeight , чтобы переместить начало координат в нижний левый угол и позволить увеличить y для перемещения вверх. экран.

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

1
2
// Set the starting flag, so the loop puts the ball in motion
startFlag = true;

Давайте перейдем к настройке цикла. Первое, что нам нужно сделать, это добавить условный оператор, который проверяет startFlag .

1
2
3
4
5
// Start motion if the flag has been flagged
if(startFlag == true){
    ball.begin(currentTime);
    startFlag = false;
}

Есть несколько вещей, которые стоит отметить. Если вы помните, мы уже создали переменную с именем currentTime на шаге 3. Кроме того, поскольку мы хотим вызывать метод begin только один раз, мы возвращаем свойству startFlag значение false .

Было бы бессмысленно, чтобы цикл вычислял положение снаряда, если он находится в покое, поэтому нам нужно добавить еще одно условное утверждение, которое проверяет, движется ли снаряд.

1
2
3
4
// Animate if ball is in motion
if(ball.isMoving() == true){
     
}

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

1
2
// Get the current position
var currentPosition:Array = ball.positionAtTime(currentTime);

Как только мы узнаем позицию, мы просто обновляем позицию MovieClip . Подобно методу init , мы должны принять во внимание наш масштаб и происхождение Flash.

1
2
3
// Move our custom MovieClip
ball.x = currentPosition[«x»] * scale;
ball.y = stage.stageHeight — currentPosition[«y»] * scale;

Мы наконец достигли вехи! Если вы тестируете фильм сейчас, вы можете увидеть основной эффект!


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

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

Идея обнаружения столкновений на основе кадров проста. Внутри петли мы бы проверяли, находится ли y-позиция мяча ниже y-позиции земли. Если условие выполнено, произошло столкновение … звучит хорошо, правда? На самом деле, нет. Скорее всего, столкновение не произойдет в течение определенного интервала таймера . Это означает, что время и позиция отскока будут неточными. Рассмотрим эту схему:

Пример проблемы с обнаружением столкновений на основе кадров

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

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

Прежде всего, нам нужно добавить еще несколько методов в наш класс Projectile . Начнем с timeOfCollision .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function timeOfCollision(ground:Number):Number{
    var time:Number;
    var times:Array;
     
    times = getTimes(ground, d1Y, aY, v1Y);
     
    // We don’t want the negative time result, so use the larger time
    time = Math.max(times[«root1»], times[«root2»]);
     
    // We need to consider the startTime as well since we’re checking the loop
    time = time + startTime
     
    return time;
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function velocityAtTime(currentTime:Number):Number{
    var relativeTime:Number = int((currentTime — startTime) * 1000) / 1000;
     
    var velocity:Number;
     
    var velocityX:Number = getVelocity(aX, v1X, relativeTime);
    var velocityY:Number = getVelocity(aY, v1Y, relativeTime);
     
    // Pythagorean theorem
    velocity = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
     
    return velocity;
}
 
public function velocityDirectionAtTime(currentTime:Number):Number{
    var relativeTime:Number = int((currentTime — startTime) * 1000) / 1000;
 
    var angle:Number;
     
    angle = getVelocityDirection(aX, aY, v1X, v1Y, relativeTime);
     
    return angle;
}

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

1
2
3
public function getStartTime():Number{
    return startTime;
}

Сохраните класс Projectile и вернитесь к классу документа. Первое, что нам нужно сделать, это добавить пару свойств. Свойство collisionTime и свойство cor для представления коэффициента реституции.

1
2
public var collisionTime:Number;
public var cor:Number;

Первое, что нам нужно сделать, это установить наш кор . Мы можем установить это в методе конструктора. Я выбрал 0,8, но не стесняйтесь экспериментировать с различными значениями, чтобы увидеть эффект.

1
2
// Set the coefficient of restitution.
cor = 0.8;

Теперь самое интересное! Внутри нашего основного цикла нам нужно реализовать наши прыгающие методы. Для начала найдите условие для startFlag . Внутри добавьте следующую строку. Это вычислит наше начальное время столкновения.

1
2
// Since the position is the centre point of the ball, the y-position of the ground is actually at half the height of the ball.
collisionTime = ball.timeOfCollision((ball.height / 2) / scale);

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

1
2
3
4
// Check if a collision happened
if(currentTime >= collisionTime){
     
}

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

1
2
var newVelocityDirection:Number = -1 * ball.velocityDirectionAtTime(collisionTime);
var newVelocity: Number = ball.velocityAtTime(collisionTime) * cor;

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

1
2
3
4
// Set the new trajectory
ball.init(newVelocityDirection, newVelocity, ball.x / scale, (stage.stageHeight — ball.y) / scale);
ball.begin(collisionTime);
collisionTime = ball.timeOfCollision((ball.height / 2) / scale);

Мы достигли еще одного рубежа! Если вы тестируете фильм, вы должны увидеть эффект отскакивания.


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

1
var newVelocity: Number = ball.velocityAtTime(collisionTime) * cor;

Я объясню. Предполагая, что cor не равен 0 или 1, скорость нашего снаряда будет приближаться к нулю, становясь все медленнее и медленнее, но фактически никогда не достигая его. Поскольку реальность такова, что наш снаряд останавливается, нам необходимо найти решение этой проблемы.

Вот как. Поскольку cor является постоянным, мы можем рассматривать скачки как геометрический ряд :

  • 1 : время, необходимое для первого отскока
  • a 2 : время, необходимое для второго отскока
  • 3 : время, необходимое для третьего отскока

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

  • S 1 : время, необходимое для первого отскока
  • S 2 : время, необходимое для первого и второго отказов
  • S 3 : Время, затраченное на три первых скачка, всего

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

Формула для суммы бесконечного геометрического ряда

Сама формула проста. Переменная a 1 представляет первый член в серии, в нашем случае, длину первого отскока. Переменная r представляет отношение рядов, в нашем случае cor .

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

1
2
public var stoppingTime:Number;
public var bounce:int;

Нам нужно знать, когда произойдет первый отскок, поэтому свойство bounce будет использоваться в качестве простого счетчика. Внутри условия startFlag цикла (это оператор if : if (startFlag == true) { ) добавьте следующее после нашего объявления cor :

1
2
// Reset the bounce count
bounce = 0;

Затем добавьте следующее в конец нашего условия столкновения.

1
2
3
4
5
6
7
8
// Increase number of bounces
bounce++;
 
// Calculate stopping time on first bounce
if(bounce == 1){
    // Sum of infinite geometric series: S = a_1 / 1 — r
    stoppingTime = (collisionTime — ball.getStartTime()) / (1 — cor) + ball.getStartTime();
}

Каждый раз, когда происходит столкновение, мы должны проверить, прошел ли stoppingTime . Если он есть, мы можем привести снаряд в состояние покоя, вызвав метод end . Цикл все еще собирается изменить положение мяча в последний раз, поэтому мы должны установить его в правильное конечное положение. Цикл уже установил снаряд на новую траекторию, поэтому мы собираемся обойти это, установив currentTime в ball.getStartTime () .

1
// If we reach the stopping time, end the motion if(bounce > 1 && collisionTime > stoppingTime){ ball.end();

Это оно! Снаряд теперь отскакивает и полностью останавливается.


Когда мы играем в фильм, снаряд стреляет немедленно. Это довольно бесполезно для большинства приложений, поэтому давайте немного изменим ситуацию и добавим некоторые базовые элементы управления пользовательским интерфейсом. Для простоты мы добавим две кнопки: кнопку запуска и кнопку сброса.

Наш первый шаг аналогичен шагу 8. В документе FLA нарисуйте две кнопки «Пуск» и «Сброс». Конвертируйте каждую кнопку в символ, на этот раз сделайте их кнопками, а не мувиклипами . На этот раз мы будем держать наши кнопки на сцене, поэтому расположите их по своему желанию. Аналогично шагу 8, откройте свойства связывания для одной из кнопок. Установите флажок « Экспорт для ActionScript» , но на этот раз оставьте имена классов по умолчанию. Нажмите OK и повторите это для другой кнопки.

Свойства связывания для кнопок

На сцене выберите кнопку «Пуск». Откройте панель свойств . Там, где написано <имя экземпляра>, введите startButton . Повторите это для кнопки сброса, называя ее resetButton .

Введите имя экземпляра

Сохраните этот файл и откройте класс документа. Внутри нам нужно импортировать класс MouseEvent .

1
import flash.events.MouseEvent;

Внутри метода конструктора удалите следующие строки.

1
2
3
4
5
// Run the projectile init method to set the properties of our projectile
ball.init(85, 6, ball.x / scale, (stage.stageHeight — ball.y) / scale);
 
// Set the starting flag, so the loop set’s the ball in motion
startFlag = true;

Мы собираемся заменить это добавлением прослушивателей событий для каждой из созданных нами кнопок. Обратите внимание, что мы можем ссылаться на них с помощью <Имя экземпляра>, которое мы установили ранее.

1
2
3
// Add event listeners for our start and reset buttons
startButton.addEventListener(MouseEvent.CLICK, startEvent);
resetButton.addEventListener(MouseEvent.CLICK, resetEvent);

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public function startEvent(event:MouseEvent):void{
    // Don’t start if its already in motion
    if(ball.isMoving() == false){
        // Run the projectile init method to set the properties of our projectile
        ball.init(85, 6, ball.x / scale, (stage.stageHeight — ball.y) / scale);
         
        // Set the starting flag, so the loop set’s the ball in motion
        startFlag = true;
    }
}
 
public function resetEvent(event:MouseEvent):void{
    // End the motion
    ball.end();
     
    // Move ball back to original position
    ball.x = 50;
    ball.y = 350;
}


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

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

Я надеюсь, вам понравился этот урок. Спасибо за прочтение!