vue3+typescript реализует ролевую игру середины осени

внешний интерфейс Vue.js разработка игр
vue3+typescript реализует ролевую игру середины осени

«Я участвую в творческом конкурсе Праздника середины осени, смотрите:Творческий конкурс «Праздник середины осени»"

предисловие

Снова выходные, мне нечего делать дома, я потратил два дня на то, чтобы придумать и сделать страницу, связанную с Праздником середины осени, прежде всего, стек технологий обоснован и соответствует современным новым технологиям, поэтому я рассматриваю возможность использованияVue3+Typescript, а затем тема Праздника середины осени, я думаю об истории о том, как Чанъэ летит на Луну. Поскольку это Чанъэ летит на Луну, страница должна быть интересной и игривой. Поэтому я, наконец, решил сделать страница с похожим стилем.

ChMkJ1tpIkuINzThAAUqitDPvVkAAqf4wHB5i0ABSqi715.jpg

После выбора стека технологий и темы и стиля производства

GIF.gif

Сначала поговорим о сценарии.春光灿烂猪八戒из后羿(二牛)а также嫦娥персона плюс东成西就из大理段王爷Секция моста вознесения.Также есть последний эффект вознесения призраков и животных.Сначала скажу,что это потому,что я действительно не нашел доступного материала,поэтому могу обойтись только этой анимацией,найденной в интернете. o(╥﹏╥)o Хорошо, тогда давайте начнем разговор, как я добился эффекта анимации страницы в этом типе игры.

организация страницы

Страница создана с помощью vite, структура файла такая

image.png

Поскольку на странице есть только одна сцена, вся страница помещается вAPP.vueнаписано в.interfaceВ папке хранятся некоторые определенные объекты интерфейса.В компоненте 4 компонента, за которыми следуют

  1. dialogBox: Компонент нижнего диалога
  2. lottie: Вход咒语После компонента эффекта взрыва яйца
  3. spriteкомпонент анимации спрайтов
  4. typedвойти咒语Компонент «Эффекты ввода»

Итак, давайте поговорим об этом по очереди в соответствии с анимационными эффектами, которые появляются на странице.

спрайт анимация

Страница начинается с二牛Анимация персонажа, идущего по мосту слева. Давайте сначала разберем эту анимацию.帧动画, то есть эффект действия ходьбы, за которым следует анимация смещения ходьбы по мосту слева.Тогда поговорим сначала об этом帧动画

Кадровая анимация

«Покадровая анимация — это распространенная форма анимации (Frame By Frame). Ее принцип заключается в разложении анимационных действий на «непрерывные ключевые кадры», то есть отрисовке разного контента кадр за кадром на каждом кадре временной шкалы, чтобы Анимация непрерывного воспроизведения

image.png

Возьмите мой проект в качестве примера,二牛На самом деле анимация ходьбы — это картинка, в нашем интерфейсе эта картинка также называется雪碧图,На картинке 4 действия.При постоянном переключении 4 действий у нас на глазах формируется эффект шагающего движения.Хорошо принцип объяснен понятно,тогда давайте теперь посмотрим на код

  <div ref="spriteBox">
    <div ref="sprite" class="sprite"></div>
  </div>

Структура страницы очень простая, всего три строчки.htmlкод, завернутыйhtmlНа самом деле, раньше это делалось位移动画б/у, внутриspriteпросто делать帧动画, Давайте посмотрим на код javascript

// 样式位置
export interface positionInterface {
    left?: string,
    top?: string,
    bottom?: string,
    right?: string
}

export interface spriteInterface {
  length: number, // 精灵图的长度
  url: string, // 图片的路径
  width: number, // 图片的宽度
  height: number, // 图片的高度
  scale?: number, // 缩放
  endPosition: positionInterface // 动画结束站的位置
}

import { Ref } from "vue";
import { positionInterface, spriteInterface } from "../../interface";

/**
 * 精灵图实现逐帧动画
 * @param spriteObj 精灵对象
 * @param target 精灵节点
 * @param wrap 精灵父节点 [控制精灵移动]
 * @param callback 图片加载好回调函数
 * @param moveCallback 移动到对应位置的回调函数
 */
export function useFrameAnimation(
  spriteObj: spriteInterface,
  target: Ref,
  wrap: Ref,
  callback: Function,
  moveCallback: Function
) {
  const { width, length, url, endPosition } = spriteObj;
  let index = 0;

  var img = new Image();
  img.src = url;
  img.addEventListener("load", () => {
    let time;
    (function autoLoop() {
      callback && callback();
      // 如果到达了指定的位置的话,则停止
      if (isEnd(wrap, endPosition)) {
        if (time) {
          clearTimeout(time);
          time = null;
          moveCallback && moveCallback();
          return;
        }
      }
      if (index >= length) {
        index = 0;
      }
      target.value.style.backgroundPositionX = -(width * index) + "px";
      index++;
      // 使用setTimeout, requestFrameAnimation 是60HZ进行渲染,部分设备会卡,使用setTimeout可以手动控制渲染时间
      time = setTimeout(autoLoop, 160);
    })();
  });

  // 走到了对应的位置
  function isEnd(wrap, endPosition: positionInterface) {
    let keys = Object.keys(endPosition);
    for (let key of keys) {
      if (window.getComputedStyle(wrap.value)[key] === endPosition[key]) {
        return true;
      }
    }
    return false;
  }
}

параметр

useFrameAnimationэто帧动画функция, параметры функции передаются первыми精灵图Объект описания, который в основном описывает изображение спрайта, состоит из нескольких действий, что такое адрес изображения, объект изображения на узле DOM и функция обратного вызова, передаваемая родителю вызывающей функции после перехода к На самом деле, комментарии в коде также описывают это очень четко.

загрузка изображения

Мы используем это изображение, чтобы сделать帧动画Когда изображение загружено, оно должно быть сначала обработано после загрузки изображения, поэтому мы должны сначалаnew Image, а затем присвойте емуsrc, затем следите за егоloadмероприятие,

Перебирать анимации

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

Добавить обработчик функции обратного вызова

Когда изображение загружено, вызовитеcallbackФункция, которая сообщает внешнему изображению, что загрузка завершена.Если есть что-то, что нужно сделать для загрузки изображения, вы можете написать это в этой функции обратного вызова.Есть также код вisEndфункция судить位移Была ли анимация завершена, если анимация смещения завершена, остановить帧动画петля, дайте ей постоять и стать картинкой.Затем выполнитеmoveCallbackСообщите родителю вызывающей функции, что анимация смещения закончилась.Это примерно то, что делает эта функция.

Анимация смещения

Анимация смещения относительно проста, давайте сначала посмотрим на код:

<script lang="ts">
import {
  computed,
  defineComponent,
  defineEmit,
  PropType,
  reactive,
  ref,
  toRefs,
  watchEffect,
} from "vue";
import { spriteInterface } from "../../interface";
import { useFrameAnimation } from "./useFrameAnimation";

export default defineComponent({
  props: {
    action: {
      type: Boolean,
      default: false,
    },
    spriteObj: Object as PropType<spriteInterface>,
  },
  defineEmit: ["moveEnd"],
  setup(props, { emit }) {
    const spriteBox = ref(null);
    const sprite = ref({ style: "" });
    const spriteObj = reactive(props.spriteObj || {}) as spriteInterface;
    const { width, height, url, length } = toRefs(spriteObj);
    watchEffect(() => {
      if (props.action) {
        useFrameAnimation(
          spriteObj,
          sprite,
          spriteBox,
          () => {
            triggerMove();
          },
          () => {
            emit("moveEnd");
          }
        );
      }
    });
    // 给宽度后边加上单位
    const widthRef = computed(() => {
      return width.value + "px";
    });
    // 给高度后边加上单位
    const heightRef = computed(() => {
      return height.value + "px";
    });
    // 给背景图片连接添加url
    const urlImg = computed(() => {
      return `url("${url.value}")`;
    });
    // 移动到目标位置
    function triggerMove() {
      if (spriteObj.scale || spriteObj.scale === 0) {
          spriteBox.value.style.transform = `scale(${spriteObj.scale})`;
      }
      if (spriteObj.endPosition) {
        Object.keys(spriteObj.endPosition).forEach((o) => {
          if (spriteBox.value && sprite.value.style) {
            spriteBox.value.style[o] = spriteObj.endPosition[o];
          }
        });
      }
    }
    return {
      widthRef,
      heightRef,
      urlImg,
      length,
      sprite,
      spriteBox,
      triggerMove,
    };
  },
});
</script>

Главное в коде этоwatchEffect, судя по использованию精灵组件Передайте props.action, чтобы начать решать, начинать ли帧动画, после вызова того, о чем мы говорили в предыдущем пунктеuseFrameAnimationПосле функции четвертый параметр функции обратного вызова заключается в том, что изображение загружается Когда изображение загружается, мы можем сделать это здесь.位移动画, то есть,triggerMove,triggerMoveФункция на самом деле вставляетspriteObjНекоторые настроенные позиции и информация о масштабе помещаются в соответствующиеDOMНа узле, если вы хотите говорить об анимации, это на самом делеcssДелать в прослушивании位移动画После окончания передать родителю amoveEndпользовательское событие.

<style lang="scss" scoped>
.sprite {
  width: v-bind(widthRef);
  height: v-bind(heightRef);
  background-image: v-bind(urlImg);
  background-repeat: no-repeat;
  background-position: 0;
  background-size: cover;
}
</style>

CSS здесь описывает только о精灵图ширина, высота и путь к изображению, выше написанное таким образомv-bindЭто способ, который можно использовать после vue3, так что динамические переменные могут быть записаны непосредственно в CSS, и все, кто его использовал, говорят, что это хорошо~精灵图Настоящий анимационный эффект записывается вAPP.vueВнутри CSS

  .boy {
    position: absolute;
    bottom: 90px;
    left: 10px;
    transform: translate3d(0, 0, 0, 0);
    transition: all 4s cubic-bezier(0.4, 1.07, 0.73, 0.72);
  }
  .girl {
    position: absolute;
    bottom: 155px;
    right: 300px;
    transform: translate3d(0, 0, 0, 0);
    transition: all 4s cubic-bezier(0.4, 1.07, 0.73, 0.72);
  }

описано выше二牛а также嫦娥Исходное положение и динамические эффекты.

диалоговый компонент

существует二牛идти嫦娥рядом после,APP.vueчерез вышеупомянутыйmoveEndПользовательское событие знает, что анимация закончилась, а затем после окончания анимации появляется диалоговое окно.Для диалога вам действительно нужно подумать о сценарии диалога и формате сценария диалога.

сценарий диалога

const dialogueContent = [
  {
    avatar: "/images/rpg_male.png",
    content: "二牛:嫦娥你终于肯和我约会了, 哈哈",
  },
  {
    avatar: "/images/rpg_female.png",
    content: "嫦娥:二牛对不起,我是从月宫来的,我不能和人间的你在一起!",
  },
  {
    avatar: "/images/rpg_female.png",
    content:
      "嫦娥:今天是中秋节,我只有今天这个机会可以重新回月宫",
  },
  {
    avatar: "/images/rpg_female.png",
    content:
      "嫦娥:回月宫的条件是找到真心人,让他念起咒语,我才能飞升!",
  },
  {
    avatar: "/images/rpg_female.png",
    content: "嫦娥:而你就是我的真心人,你可以帮我嘛?",
  },
  {
    avatar: "/images/rpg_male.png",
    content: "二牛:好的,我明白了! 我会帮你的.",
  },
  {
    avatar: "/images/rpg_female.png",
    content: "嫦娥:好的。 谢谢你!",
  },
];

Это сценарий моей маленькой игры, потому что кто-то сказал сначала абзац, я сказал другой абзац, или кто-то другой сказал абзац, а затем сказал абзац В этом случае просто запишите это в порядке диалога, а затем мы в Код может отображаться один за другим по порядку, нажав время взаимодействия.Структура диалога в основном人物头像а также人物内容,Здесь я показываю имена персонажей прямо в содержании,чтобы не заморачиваться.На самом деле при необходимости их можно подтянуть.

структура

Давайте сначала посмотрим на это.htmlструктура

  <div v-if="isShow" class="rpg-dialog" @click="increase">
    <img :src="dialogue.avatar" class="rpg-dialog__role" />
    <div class="rpg-dialog__body">
      {{ contentRef.value }}
    </div>
  </div>

Структура на самом деле очень простая, внутри есть аватарка и контент, используемisShowДля управления отображением и скрытием диалога используйтеincreaseПерейти к следующему разговору.

логическая реализация

    function increase() {
      dialogueIndex.value++;
      if (dialogueIndex.value >= dialogueArr.length) {
        isShow.value = false;
        emit("close");
        return;
      }
      // 把下个内容做成打字的效果
      contentRef.value = useType(dialogue.value.content);
    }

существуетincreaseСпособ тоже очень простой, после клика заявленный индекс (по умолчанию 0)+1, если индекс равен длине скрипта, закройте диалоговое окно, а затем дайтеAPP.vueОдинcloseПользовательское событие, если оно меньше длины скрипта, перейти к следующему содержимому скрипта и начать с打字Эффект представлен.useTypeметод.

/**
 * 打字效果
 * @param { Object } content 打字的内容
 */
export default function useTyped(content: string): Ref<string> {
  let time: any = null
  let i:number = 0
  let typed = ref('_')
  function autoType() {
    if (typed.value.length < content.length) {
      time = setTimeout(() =>{
        typed.value = content.slice(0, i+1) + '_'
        i++
        autoType()
      }, 200)
    } else {
      clearTimeout(time)
      typed.value = content
    }
  }
  autoType()
  return typed
}

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

Компонент Typebox (заклинание)

Закончив сценарий,APP.vueполучит компоненты и закончатсяcloseпользовательское событие, в которое мы можем поместить诅咒组件показывать,

структура

<div v-if="isShow" class="typed-modal">
    <div class="typed-box">
      <div class="typed-oldFont">{{ incantation }}</div>
      <div
        @input="inputChange"
        ref="incantainerRef"
        contenteditable
        class="typed-font"
      >
        {{ font }}
      </div>
    </div>
  </div>

Проклятый компонент, здесьhtmlСтруктура, мы можем взглянуть, она используется внутриcontenteditableЭто свойство, после установки этого свойства,divЕго можно изменить, чтобы он был похож на поле ввода, мы можем напрямуюdivПриведенный выше текст можно свободно изменять, поэтому нам необходимо отслеживать изменения пользователя при его изменении.inputмероприятие.incantationЭто наконечник внизу咒语, fontВвод - это то, что нам нужно ввести咒语.

логическая реализация

export default defineComponent({
  components: {
    ClickIcon,
  },
  emits: ["completeOver"],
  setup(props, { emit }) {
    const isShow = ref(true);
    const lottie = ref(null);
    const incantainerRef = ref(null);
    const defaultOption = reactive(defaultOptions);
    const incantation = ref("Happy Mid-autumn Day");
    let font = ref("_");

    nextTick(() => {
      incantainerRef.value.focus();
    });

    function inputChange(e) {
      let text = e.target.innerText.replace("_", "");
      if (!incantation.value.startsWith(text)) {
        e.target.innerText = font.value;
      } else {
        if (incantation.value.length === text.length) {
          emit("completeOver");
          font.value = text;
          isShow.value = false;
          lottie.value.toggle();
        } else {
          font.value = text + "_";
        }
      }
    }

    return {
      font,
      inputChange,
      incantation,
      incantainerRef,
      defaultOption,
      lottie,
      isShow,
    };
  },
});
</script>

Когда компонент всплывает, мы используемincantainerRef.value.focus();Сделайте его автоматическим фокусом.inputChangeВ случае, мы идем судить ввод咒语ли и подскажите咒语то же самое, если отличается, вы не можете продолжать печатать и оставаться на правильном вводе咒语Вкл., если все введено правильно, он автоматически закроется咒语всплывающее окно, и всплывающее подобное恭喜通过Эффект фейерверка.completeOverспециальное событие дляAPP.vue.

Тема страницы APP.vue

Страница фактически как режиссер.После получения различных отзывов от актеров будет организован следующий актер

  setup() {
    let isShow = ref(false); // 对话框窗口开关
    let typedShow = ref(false); // 咒语窗口开关
    let girlAction = ref(false); // 女孩动作开关, 导演喊一句action后,演员开始演绎
    const boy = reactive(boyData);
    const girl = reactive(girlData);
    const dialogueArr = reactive(dialogueContent);
    // 男孩移动动画结束
    function boyMoveEnd() {
      isShow.value = true;
    }
    // 完成输入咒语
    function completeOver() {
      girlAction.value = true;
    }
    function girlMoveEnd() {}
    // 对话窗口关闭
    function dialogClose() {
      // 对话框关闭后,弹出咒语的窗口,二牛输入咒语后,嫦娥开始飞仙动作
      typedShow.value = true;
    }
    return {
      dialogueArr,
      boy,
      girl,
      isShow,
      boyMoveEnd,
      girlMoveEnd,
      girlAction,
      dialogClose,
      typedShow,
      completeOver,
    };

Просто взгляните на него, сказать особо нечего.

напиши в конце

Я не буду говорить об этом эффекте фейерверка, потому что моя последняя статьяКак использовать Лотти в VueКаждая деталь была подробно объяснена. И на этот раз этот компонент на самом деле является самоинкапсулированным компонентом, о котором я говорил в этой статье. Основные эффекты таковы. Если вам интересно, вы можете обратиться к моей статье. На основе , добавьте в него некоторые детали. Например, добавьте云彩анимация, добавить水波Динамические эффекты и т.д. Если вам нужен исходный код, вы можетекликните сюдаСмотри, насыщенный день проходит так быстро~ Увидимся в следующий раз, всем желаю заранее中秋节快乐!.