Перейти к содержимому

Обратные вызовы

В этом разделе рассматриваются сбор данных, создание функций эффектов и использование класса Meter для запуска этих функций. Здесь не будет подробных объяснений анимаций или специфики обработчиков эффектов — при необходимости обратитесь к разделам Обучение или Справочник API.

Процесс эффективного запуска анимации от начала до конца выглядит следующим образом:

  1. Используйте метод Meter.setValue(value) внутри функции обновления для передачи необработанных данных счётчика в экземпляр класса Meter.
  2. Если Meter оценивает себя как стабильный, он активирует функцию обратного вызова, переданную вторым параметром.
  3. Эта функция обратного вызова содержит условную логику для оценки состояния Meter. Если состояние истинно, ваш эффект должен быть добавлен в массив эффектов (effects.push(new yourEffect())) или в обработчик состояний (steMgr.Push(new yourEffect())).
  4. При каждом запуске функции обновления оценивается весь массив эффектов и последний элемент в обработчике состояний. На этом этапе эффекты отрисовываются на основе переменных состояния.
  5. После отрисовки каждый эффект участвует в проверке своего жизненного срока. Эффекты в массиве эффектов просто итерируют переменную жизненного срока, заданную для экземпляра их класса. Когда жизненный срок равен 0 или меньше, эффект легко удаляется с помощью splice при оценке массива эффектов. Эффекты в обработчике состояний могут удалять себя самостоятельно, поскольку обработчик состояний функционирует как стек, и условная логика для таких эффектов, как правило, более сложная.

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

<head>
<meta meter="health" tags="example" x= ".05" y=".9" width=".189" h="70-140" s="40-100" l="40-100" type="linear"/>
</head>

Здесь создаётся экземпляр класса Meter с именем healthM для активации эффекта «получен урон».

<script>
// Meter instance
var healthM = new Meter(25, healthHelper);
function update () => {
healthM.setValue(health);
window.requestAnimationFrame(update);
}
function healthHelper () => {
// "Took Damage" effect up next
}
// . . .
</script>

Задан достаточно высокий порог стабильности (25) для учёта описанной нестабильности. Прикреплённая функция обратного вызова активируется только если значение счётчика стабильно на протяжении 25 обновлений, что исключает большинство данных, не являющихся реальной полосой здоровья, даже когда игрок стоит неподвижно. Единственный недостаток — растущая задержка эффекта, которая всегда будет ограничивающим фактором в данном сценарии. Для этой конкретной полосы здоровья можно компенсировать это несколькими способами:

  • При попадании часть зелёной полосы мгновенно превращается в красную перед тем, как уменьшиться до нового уровня зелёного здоровья. Если добавить счётчик healthRed, отслеживающий красный цвет в той же зоне, что и счётчик здоровья, можно объединить их значения для более быстрого срабатывания. Условие if ( healthM.decreased && healthRed.increased ) должно обеспечить хорошую точность в данном случае.
  • Также можно объединить показания нескольких линейных счётчиков с одинаковыми настройками цвета. В текущей конфигурации один линейный счётчик расположен в середине полосы здоровья. Добавив ещё два — немного выше и ниже исходного, но всё равно внутри полосы здоровья — близость трёх значений счётчиков можно использовать как дополнительную проверку стабильности. Все три счётчика должны показывать Meter.decreased и находиться в небольшом диапазоне друг от друга для срабатывания эффекта.

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

let healthPrev = 0;
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
effects.push(new healthEffect());
healthPrev = healthM.value;
}
}

Условный оператор требует двух истинных условий: стабильного уменьшения счётчика и несовпадения текущего значения счётчика с ранее записанным. Второе условие добавлено для устранения ошибки. Если проверять только уменьшение, эффект воспроизводился бы многократно. Поскольку Meter.decreased устанавливается после стабилизации счётчика, его значение остаётся неизменным, пока счётчик не изменится. Таким образом, эффект воспроизводится только при уменьшении, отличающемся от последнего стабильного значения.

Далее создаются два эффекта: один для массива эффектов, другой для обработчика состояний.

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

function healthEffect(color, direction, speed){
// The lifetime of the effect should be iterated in the draw function.
this.lifetime = 200;
this.speed = speed;
this.direction = direction;
this.color = color;
this.draw = () => {
// Set fill from parameter
ctx.fillStyle = `hsl(${this.color}, 100%, 50%)`;
// Check direction
if (this.direction == "up"){
ctx.fillRect(0, this.lifetime, width, 30);
} else {
ctx.fillRect(0, height - this.lifetime, width, 30);
}
// As the lifetime decreases by 5 each update, the rectangle moves accordingly.
this.lifetime -= speed;
}
}
function update() {
// Background color
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 320, 200);
// Draw all effects in array and remove them if they are done.
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime == 0){
effects.splice(i, 1);
}
})
window.requestAnimationFrame(update);
}
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Make sure to correctly set the color and direction paremeters.
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
}

Вот как это должно выглядеть в действии:

Благодаря гибкому дизайну функции легко создать и эффект лечения! Просто добавьте дополнительное условие в функцию обратного вызова.

function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Damage effect
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
if (healthM.increased && healthM.value != healthPrev){
// Healing effect
effects.push(new healthEffect(120, "up", 5));
healthPrev = healthM.value;
}
}

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

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

function lobbyAnimation(){
// Instance variables
this.start = new Date().getTime();
this.elapsed = 0;
this.radius = 0;
this.searching = 1000;
// Instance effects array
this.effects = [];
// Process is the method that allows a state effect to remove itself
this.Process = function () {
// If the lobby menu and play button disappear, remove the effect
if (inLobby.decreased && yellowPlayButton.decreased) {
stateMgr.Pop();
// Global boolean to prevent duplicate lobby effects in the state handler
lobbyAnim = false;
}
// If the play button is present, edit this.searching based on its color
yellowPlayButton.value == 1 ? this.searching = 1000 : this.searching = 100;
// Call the Draw function attached to the effect
this.Draw();
};
this.Draw = function(){
// Iterate important variables
this.elapsed = new Date().getTime() - this.start;
this.radius = 50 + (Math.sin(this.elapsed/this.searching)*10);
// Fill the background to overwrite other effects
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 200);
// Draw the main arc
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, 50%)`;
ctx.lineWidth = 20;
ctx.arc(160, 100, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// If the main arc is the right size, push helper effects into the helper array
if(this.radius > 59 && this.elapsed % 2 == 0){
this.effects.push(new lobbyHelper(160, 100, 69));
}
// Play and remove helper effects if necessary
this.effects.forEach((e, i) => {
ctx.lineWidth = 1;
e.draw();
if (e.radius > 360) {
this.effects.splice(i, 1);
}
});
}
}
function lobbyHelper(x, y, radius){
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function(){
// Set the global transparency value. 1 is opaque, 0 is invisible.
ctx.globalAlpha = 1 - this.radius/360;
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, ${50 + (this.radius/7)}%)`;
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// Set the global transparency back to 1 so your other effects are opaque
ctx.globalAlpha = 1;
this.radius++;
}
}
// Declare state handler (found in Snippets)
var stateMgr = new StateHandler();
// Global boolean to prevent duplicate lobby animations
var lobbyAnim = false;
function update(){
// Run the Process function to draw the animation
stateMgr.Process();
}
function lobbyAnimationPlay(){
if(inLobby.value == 1 && yellowPlayButton.value == 1 && !lobbyAnim){
stateMgr.Push(new lobbyAnimation());
lobbyAnim = true;
}
};

Поиск не активен:

Поиск активен:

На этом заканчивается руководство по обратным вызовам, а также официальное руководство для разработчиков SignalRGB. Следите за следующим и финальным дополнением — Обслуживание Lightscript!