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

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

Чтобы добиться желаемого эффекта, мы будем использовать 3 типа переходов / анимаций:

Перечислить переходы:

  • Движение скольжения плитки
  • Пункты игрового меню

Условная отрисовка:

  • Наложение конца игры

Переходы между состояниями:

  • Оценка меню игры
  • Раскрытие плитки при объединении

Чтобы понять основы переходов и анимации, вот два отличных руководства, которые я написал - они познакомят вас со всеми основными концепциями переходов Vue.js и способами их создания.

Https://medium.com/babystep/vuejs-transitions-3842d8b633ae https://medium.com/babystep/vuejs-animations-javascript-md-fa496111d200

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

Движение скольжения плитки

Итак, имея в виду основы переходов Vue.js, давайте погрузимся в них. Давайте сначала попробуем реализовать скользящий переход плитки. Этот переход является наиболее важным для игры, и без него игра практически не воспроизводится.

Концептуально, когда мы думаем о доске, мы могли бы представить ее как 2-мерный массив, используя индексы каждого массива, чтобы получить координаты x, y тайла. Это отличная структура для манипулирования плиткой, но когда мы попытаемся применить переходы к этой структуре, мы столкнемся с некоторыми проблемами. Во-первых, group-transition в Vue.js работает с (плоским) списком. Двумерный массив на самом деле представляет собой список списков. Это отразится в group-transition из group-transitions. Это может изначально не поднимать никаких флагов, но если мы подумаем о том, как запускаются group-transition, становится очень очевидно, что любой вид вертикального перехода будет почти невозможен с этой структурой. Это потому, что единственный способ вызвать эффект перехода - это войти, выйти или переместиться. Перемещение плитки с board[0][0] на board[1][0] будет означать, что ей придется покинуть список board[0] и войти в список board[1]. Это можно сделать с помощью javascript, но для обработки досок разного размера, скольжения разной длины и направления скольжения потребуются значительные вычислительные ресурсы.

Не волнуйтесь, есть более простой способ справиться с переходами. Возвращаясь к более раннему моменту, group-transition в Vue.js работает с (плоским) списком. Давайте снова подумаем о плате, действительно ли это должен быть 2d-массив? Для расчета платы в движении да, имеет смысл работать с 2d-массивом, но для рендеринга платы это не имеет значения. Структуру сетки можно реализовать с помощью причудливого стиля флексбокса. На самом деле было бы проще работать с API Vue.js, если бы это был плоский массив. Итак, стратегия здесь может быть 1 из 2 вариантов:

  • Сохраните доску как плоский массив и разбейте ее на 2d-массив в любое время, когда нам нужно рассчитать ход.
  • Сохраните доску как вложенный массив и сведите ее в 1d-массив для рендеринга.

Вариант, который я выбрал, был первым вариантом. Я считаю, что этот вариант предпочтительнее, потому что он кажется более простым, но поскольку мы отключаем каждый элемент с помощью идентификатора, оба метода будут работать нормально. У Vue не будет проблем с обнаружением изменений в массиве и новой позиции элементов.

Здесь вы можете спросить себя - конечно, но все переходы списков, которые я видел до сих пор, были вертикально организованными. Как может один group-transition справиться как с вертикальным, так и с горизонтальным перемещением?

Если вы немного изучите документацию по переходам Vue.js, вы в конечном итоге столкнетесь с этим разделом:

Https://vuejs.org/v2/guide/transitions.html#List-Move-Transitions

В этом разделе они показывают пример под названием Lazy Sodoku. Здесь показано, что один transition-groupelement может обрабатывать многомерные переходы сетки. Как? Все, что вам нужно сделать, это организовать сетку так, как вам нравится с помощью CSS, и убедиться, что вы применили класс v-move к каждому элементу в группе.

Помня об этих концепциях, давайте посмотрим на код, благодаря которому это происходит:

// game.js
<transition-group name="tile" tag="div" class="board">
  <tile v-for="tile in board" :tile="tile" :key="tile.id"></tile>
</transition-group>
/* styles.css */
.board {
  display: flex;
  flex-wrap: wrap;
  width: 23em;
  padding: 6px;
  border-radius: 6px;
  background-color: rgba(58, 41, 76, 0.5);
}
.tile-move {
  transition: transform .09s ease;
}

Это так просто. Мы создаем transition-group с именем tile. Используя это, мы можем определить наш v-move класс - .tile-move. Это создаст эффект скольжения. Мы также определяем наш класс board, который представляет собой гибкий блок, который будет обертываться. Есть много других способов заставить работать сетку, но это быстрый и простой пример.

Пункты игрового меню и счет в игровом меню

С самым важным переходом под нашим поясом давайте посмотрим на другие group-transition в нашем списке, переход на увеличение количества баллов и переход между состояниями самого счета. Цель здесь состоит в том, чтобы каждый раз, когда мы набираем очки в игре, мы хотим, чтобы общее количество очков увеличивалось на единицу до тех пор, пока не достигнет нового значения, а также отображать уведомление, которое исчезает, показывая + <point_number> над нашим счетом. Поскольку мы только что имели дело с group-transition, давайте сначала разберемся с этим. Вот что мы хотим сделать, так это посмотреть счет. Каждый раз, когда счет меняется, мы можем наблюдать его новое и старое значение и брать разницу между ними, чтобы узнать, сколько очков набрал игрок. Кроме того, зная, насколько увеличилось количество очков, мы можем отобразить это число как набранные очки и анимировать результат от его предыдущего результата к новому результату. Итак, давайте посмотрим на код, чтобы это произошло.

// game-menu.js
watch: {
  score(newValue, oldValue) {
    const self = this
    if (newValue > 0) {
      // must clone deep because when mutating (NOT replacing) an Array, 
      // the old value will be the same as new value because they reference the same Array
      let oldPoints = _.cloneDeep(self.pointsIncrease)
      oldPoints.push(newValue - oldValue)
      self.pointsIncrease = oldPoints
    }
  },
  pointsIncrease(newPoints, oldPoints) {
    if (newPoints.length > oldPoints.length) {
      setTimeout(() => {
        this.pointsIncrease.pop()
      }, 200)
    }
  }
},

// game-menu.js
// <template>
<transition-group name="points" tag="div" class="points">
  <div v-for="(pointIncrease, index) in pointsIncrease" :key="index">+ {{ pointIncrease }}</div>
</transition-group>

/* styles.css */
.points-enter-active, .points-leave-active {
  transition: all 100ms;
}
.points-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
.points-enter {
  transform: translateY(30px);
}

Идея здесь в том, что мы ведем список увеличений баллов. Когда мы наблюдаем увеличение количества очков, мы помещаем его в список, чтобы мы могли его отобразить. Это сделано для того, чтобы мы могли обрабатывать любое количество очков, увеличивающих потоковую передачу в игровое меню. Для этого мы делаем две вещи внутри наблюдаемых свойств. Сначала смотрим значение оценки. Когда оно изменится, и новое значение будет положительным целым числом, мы добавим разницу в список - pointsIncrease. Одна важная проблема при просмотре массивов во Vue заключается в том, что вы должны заменить массив, иначе наблюдаемое значение не будет знать, какое было старое значение. Это потому, что ссылки в памяти на новый и старый массивы одинаковы. Вот почему мы используем метод cloneDeep, чтобы убедиться, что мы создали новую копию массива. Затем внутри нашего pointsIncrease наблюдателя мы обнаруживаем любое увеличение длины массива и устанавливаем тайм-аут для удаления всего, что было добавлено в список. Это приведет к срабатыванию переходов входа / выхода в нашем списке и убедитесь, что мы не оставляем никаких уведомлений об увеличении баллов.

Теперь перейдем к следующей анимации, собственно партитуру. Повторюсь, эффект, которого мы хотим достичь, - это увеличить счет на 1 в течение некоторого времени, в конечном итоге достигнув окончательного результата. Это забавная анимация, которая придаст игре вид аркады. Стратегия здесь будет заключаться в том, чтобы «подвести» оценку от текущего значения к желаемому, попутно обновляя и устанавливая промежуточное значение. Давайте посмотрим на код для этого.

// game-menu.js
data() {
  return {
    animatedScore: 0,
    pointsIncrease: [],
  }
},
watch: {
  score(newValue, oldValue) {
    const self = this
    if (newValue > 0) {
      // must clone deep because when mutating (NOT replacing) an Array, 
      // the old value will be the same as new value because they reference the same Array
      let oldPoints = _.cloneDeep(self.pointsIncrease)
      oldPoints.push(newValue - oldValue)
      self.pointsIncrease = oldPoints
    }
    function animate () {
      if (TWEEN.update()) {
        requestAnimationFrame(animate)
      }
    }
    new TWEEN.Tween({ tweeningNumber: oldValue })
      .easing(TWEEN.Easing.Quadratic.Out)
      .to({ tweeningNumber: newValue }, 500)
      .onUpdate(function () {
        self.animatedScore = this.tweeningNumber.toFixed(0)
      })
      .start()
    animate()
  },
  
  ...

Здесь мы видим тот же код, что и раньше, но с добавлением двух функций, функции анимации и функции Tween. Вы можете видеть, что функция Tween начинается с oldValue и изменяется до newValue в течение 500 мс. И onUpdate, или на каждом тике, мы устанавливаем атрибут данных в компоненте игрового меню animatedScore, равный числу анимации движения, округленному до целого числа. Это суть анимации!

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

// game-menu.js
...
// <template>
<transition name="fade">
  <div v-if="gameOver" class="modal">
    <h1>Game Over!</h1>
    <a class="button button-black" @click="newGame()">Try again</a>
  </div>
</transition>
...
// <Component>
props: {
  gameOver: {
    type: Boolean,
    required: true,
  }
},
...
/* styles.css */
.fade-enter-active {
  transition: opacity 2s
}
.fade-leave-active {
  transition: opacity .5s
}
.fade-enter, .fade-leave-to {
  opacity: 0
}

Плитка всплывает при слиянии

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

// tile.js
watch: {
  value(newVal, oldVal) {
    if (newVal > oldVal) {
      setTimeout(() => {
        Velocity(this.$el, {scale: 1.2}, {duration: 50, complete: () => {
          Velocity(this.$el, {scale: 1}, {duration: 50})
        }})
      }, 50)
    }
  }
},

Здесь мы просто наблюдаем значение любого тайла, и когда newValue больше oldValue, мы будем масштабировать элемент вверх и вниз на 20% в течение 100 мс.

Вот и все анимации! Теперь игра должна выглядеть отлично и отзывчивой!