Перейти к основному содержанию

Анимирование

Все мы видели, как игровые интерфейсы умеют не просто открываться, а делать это с некоторыми эффектами. Например инвентарь движется снизу вверх при открытии, а при закрытии сверху вниз. Появление достижений из Java издания игры и изучений из Ancient Wonders и Infinite Forest тоже имеет свою анимацию. В данной главе мы рассмотрим, какие есть подходы реализации похожего поведения, а так же некоторые особенности, которые вам придётся учитывать.

Использование потоков

В одной из прошлых статей мы разбирали потоки, если вы её не видели, обязательно прочитайте. Есть несколько подходов для создания анимации, такие как использование Updatable, android.animation.ValueAnimator и Threading. Мы будем рассматривать только Threading, ведь это позволит нам более удобно разобрать создание анимаций.

Отрисовка

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

Старый способ изменять данные элементов

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

<UI.Window>.content.elements[название элемента][название ключа] = значение;

Давайте рассмотрим другой, более быстрый способ.

Новый способ изменять данные элементов

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

Ключевые методы для работы:

const elements = <UI.Window>.getElements();
const element = elements.get(название ключа); //получаем наш элемент в нужном представлении, который мы регистрировали в <UI.Window>.content.elements

//методы для получения данных вне зависимости от того, открыто ли окно:
element.setBinding(название ключа, значение) //это позволяет менять другие параметры помимо позиции по имени ключа, никогда не меняйте позицию при помощи него
element.getBinding(название ключа) //возвращает значение по названию ключа
element.setPosition(x, y) //позволяет очень быстро менять позиции элементов, это нам сегодня и понадобится

//методы, работающие строго при открытом окне:
const alpha = <UI.Window>.layout.getAlpha(); //получаем прозрачность окна
<UI.Window>.layout.setAlpha(число от 0 до 1); //ставим прозрачность окну
Не все значения элементов меняются через <UI.IElement>.setBinding(key, value)

Если какой-то ключ не получается изменить новым способом, используйте старый.

Комбинируйте способы изменения данных элементов

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

Способы определить, запущена ли анимация в данный момент:

Рассмотрим два подхода, которые используются в разных модификациях.

//Первый подход
Threading.getThread(название потока, мы будем использовать "thread.example.money") != null

//Второй подход
<Object>.inited == true //используем объявленный ключ inited, этот подход будет использоваться в нашем примере
Никогда не стройте анимацию на изменении позиции окна

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

Начинаем писать нашу анимацию

Хорошо, мы определились с инструментами. Давайте определимся с интерфейсом, на основе которого мы будем разбираться в анимировании.

const animator = {
//константы
MAX_HEIGHT: 270, //максимальное число, на которое будут опускаться элементы
TEXT_X: 1000 - (48 * 3) + (45 + 5) + 2,
TEXT_Y: 210,
ICON_X: (1000 - 48 * 3) + 2.5,
ICON_Y: 192.5,
//технические
inited: false, //ключ для определения, запущена ли анимация в данный момент.
queue: [] //очередь, анимация будет повторяться пока она не будет пустой
};
animator.window = new UI.Window({
drawing: [{
type: "background",
color: android.graphics.Color.TRANSPARENT
}],
elements: {
balanceIcon: {
x: this.ICON_X,
y: this.ICON_Y,
type: "image",
width: 40,
height: 40,
bitmap: "manager.balance_icon"
},
balanceText: {
type: "text",
x: this.TEXT_X,
y: this.TEXT_Y,
text: "?",
font: {
size: 15,
color: android.graphics.Color.LTGRAY
}
}
}
});
animator.window.setDynamic(true); //задаём возможность менять данные интерфейса явно.
animator.window.setTouchable(false); //делаем интерфейс не кликабельным. Это значит, что функции клика не будут работать.
animator.window.setAsGameOverlay(true); //задаём интерфейс как игровой оверлей. Это значит, что интерфейс не будет мешать игровым звукам.

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

Пишем вспомогательные функции

Для начала напишем вспомогательные функции:

//функция для безопасного изменения прозрачности окна
animator.setAlpha = function(alpha) {
if(this.window.isOpened()) { //если наше окно открыто
this.window.layout.setAlpha(alpha); //ставим переданную прозрачность
}
}

//функция для сброса значений до начальных
animator.clearValues = function() {
this.setAlpha(1); //убираем прозрачность, чтобы окно было полностью видно
this.setHeight(0); //сбрасываем позиции
}

//функция для изменения позиции элементов по y
animator.setHeight = function(height) {
const elements = this.window.getElements();
const balanceText = elements.get("balanceText");
const balanceIcon = elements.get("balanceIcon");

balanceText.setPosition(this.TEXT_X + height, this.TEXT_Y);
balanceIcon.setPosition(this.ICON_X + height, this.ICON_Y);
}

//функция для безопасного получения значения прозрачности окна
animation.getAlpha = function() {
if(this.window.isOpened()) { //проверяем, открыто ли окно
return this.window.layout.getAlpha(); //возвращаем значение прозрачности окна
}
return 0; //возвращаем 0, если окно закрыто
}
Запускайте потоки только тогда, когда это нужно

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

Приступим к написанию функции запуска нашей анимации для пользователя

animator.init = function(number) {
if(this.inited == true) {
this.queue.push(number);
return;
}
this.inited = true;
this.window.content.elements.balanceText.text = "-" + number; //ставим число списанных денег с нашего импровизированного счёта

if(!this.window.isOpened()) {
this.window.open();
}
this.clearValues();
this.start(number); //эта функция будет запускать поток, её напишем следующей
}
Использование try-catch — хороший подход

Хотя в данном руководстве мы не будем его применять, если вы не уверены в надёжности своего кода, это может стать хорошим решением.

Приступаем к написанию потока для анимации

animator.start = function() {
let height = 0;
Threading.initThread("thread.example.money", () => {
while(true) { //цикл будет выполняться до тех пор, пока анимация естественным образом не завершится
java.lang.Thread.sleep(3); //усыпляет поток на 3 миллисекунды. От этого значения напрямую зависит скорость анимации, чем меньше значение, тем быстрее работает поток.
const alpha = this.getAlpha(); //безопасно получаем значение прозрачности окна
if((height >= this.MAX_HEIGHT && alpha <= 0) || !this.window.isOpened()) { //проверяем, пора ли завершать анимацию
this.inited = false; //сбрасываем значение inited, позволяя нашей анимации быть вновь запущенной
if(this.queue.length > 0) { //пытаемся узнать, есть ли в очереди числа
return this.init(this.queue.shift()); //запускаем анимацию с новыми значениями, если наша очередь не пуста. Это значит, что если мы вызвали функцию init например 3 раза, анимация будет показана 3 раза последовательно
}
this.window.close(); //закрываем окно
return;
}
if(height >= (this.MAX_HEIGHT / 2) && alpha > 0) { //проверяем, пролетела ли анимация полпути
this.setAlpha(alpha - 0.05); //обновляем значение прозрачности
}
if(height < this.MAX_HEIGHT) { //проверяем, что анимация ещё не пролетела нужный путь
this.setHeight(height++); //обновляем и одновременно ставим позиции нашим элементам
}
}
});
}
Удостоверьтесь, что значения никогда не равны undefined

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

На этом всё. Теперь при вызове animator.init(число) у нас появится справа анимация, которая пролетит вниз и плавно исчезнет.

Callback.addCallback("ItemUseLocal", () => {
animator.init(10);
});

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

Не отчаивайтесь!

Если у вас не получается написать анимацию с первого раза, ничего страшного! У многих создателей модификаций не сразу получалось, в том числе и у меня. Главное не сдавайтесь, и у вас точно получится!

Готовые решения

Хотя написание собственных анимаций позволяет вам лучше понять работу потоков, никто не заставляет вас делать это с нуля. Вы можете воспользоваться моей библиотекой для создания любых анимаций, или ScrutinyAPI для создания анимаций подобных изучениям из Ancient Wonders и квестов.

Не бойтесь экспериментировать!

Ещё примеры анимаций:

  1. Обучение
  2. Изучения
  3. Плавное возникновение
  4. Смещение эффектов
  5. Появление и исчезновение эффектов
  6. Анимация 404