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

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

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

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

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

Отрисовка

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

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

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

окно.content.elements.название_элемента.название_ключа = <значение>;

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

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

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

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

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

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

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

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

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

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

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

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

// первый, используя поток
Threading.getThread("mymod.example.money") != null

// второй, используя собственный объект, этот подход будет использоваться в нашем примере
if (объект.initiated) {
// ...
}
Никогда не стройте анимацию на изменении позиции окна

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

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

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

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,
// технические переменные
initiated: 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.initiated == true) {
this.queue.push(number);
return;
}
this.initiated = 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", function() {
while (true) { // цикл будет выполняться до тех пор, пока анимация естественным образом не завершится
java.lang.Thread.sleep(3); // усыпляем поток на 3 миллисекунды; от этого значения напрямую зависит скорость анимации, чем оно меньше, тем быстрее работает поток
const alpha = this.getAlpha(); // безопасно получаем значение прозрачности окна
if ((height >= this.MAX_HEIGHT && alpha <= 0) || !this.window.isOpened()) { //проверяем, пора ли завершать анимацию
this.initiated = false; // сбрасываем значение иниализации, позволяя нашей анимации быть вновь запущенной
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 и квестов.

Появление достижений из Java издания игры и изучений из Ancient Wonders и Infinite Forest тоже имеет свою анимацию. Или, например, более сложная анимация.

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

Некоторые примеры анимаций

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