iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0
Modern Web

派對動物嗨起來!系列 第 19

D19 - 是誰偷按確定?

  • 分享至 

  • xImage
  •  

本系列文已改編成書「甚麼?網頁也可以做派對遊戲?使用 Vue 和 babylon.js 打造 3D 派對遊戲吧!」

書中不只重構了程式架構、改善了介面設計,還新增了 2 個新遊戲呦!ˋ( ° ▽、° )

新遊戲分別使用了陀螺儀與震動回饋,趕快買書來研究研究吧!ლ(╹∀╹ლ)

在此感謝深智數位的協助,歡迎大家前往購書,鱈魚感謝大家 (。・∀・)。

助教:「所以到底差在哪啊?沒圖沒真相,被你坑了都不知道。(´。_。`)」

鱈魚:「你對我是不是有甚麼很深的偏見啊 (っ °Д °;)っ,來人啊,上連結!」

Yes


現在玩家出現了,訊號也進來了,讓畫面產生互動效果吧!

玩家頭像對話泡泡

完派對遊戲的時候,常常發生不知道是誰在選單偷按的問題,現在讓我們在玩家頭像中,新增「說出觸發按鍵」的功能,以後誰偷按都無所遁形啦!╮(´∀` )╭

前往 player-avatar 設計說出對話泡泡的功能,首先設計資料。

src\components\player-avatar.vue

const messageInfo = reactive({
  id: '',
  text: '',
});

接著新增顯示與隱藏泡泡用的 function。

const hideBalloon = debounce(() => {
  messageInfo.text = '';
}, 2000);

function showBalloon(text: string) {
  const id = nanoid();

  messageInfo.id = id;
  messageInfo.text = text;

  hideBalloon();
}

再新增一個映射按鈕名稱與顯示 Icon 的 function。

const keyToIcon = [
  {
    keyName: KeyName.UP,
    icon: 'arrow_drop_up'
  },
  {
    keyName: KeyName.LEFT,
    icon: 'arrow_left'
  },
  {
    keyName: KeyName.RIGHT,
    icon: 'arrow_right'
  },
  {
    keyName: KeyName.DOWN,
    icon: 'arrow_drop_down'
  },
  {
    keyName: KeyName.CONFIRM,
    icon: 'done'
  },
]
function keyNameToIconName(name: string) {
  const target = keyToIcon.find(({ keyName }) => keyName === name);
  return target?.icon ?? 'question_mark';
}

最後透過 defineExpose 提供外部使用。

...
<script setup lang="ts">
import { debounce } from 'lodash-es';
import { nanoid } from 'nanoid';
import { computed, reactive } from 'vue';
...
const messageInfo = reactive(...);

const hideBalloon = debounce(...);

function showBalloon(text: string) {...}

const keyToIcon = [...]
function keyNameToIconName(name: string) {...}

defineExpose({
  playerId: props.playerId,
  showBalloon
});
</script>
...

最後完成 template 與 CSS 內容。

<template>
  <q-avatar
    :color="color"
    text-color="white"
    size="6rem"
  >
    {{ props.codeName }}

    <div class="balloon-box">
      <transition name="balloon">
        <div
          v-if="messageInfo.text !== ''"
          :key="messageInfo.id"
          class="balloon"
        >
          <q-icon
            color="black"
            :name="keyNameToIconName(messageInfo.text)"
          />
        </div>
      </transition>
    </div>
  </q-avatar>
</template>
...
<style scoped lang="sass">
.balloon-enter-active, .balloon-leave-active
  transition-duration: 0.4s
  transition-timing-function: cubic-bezier(0.150, 1.535, 0.625, 1.015)
.balloon-leave-active
  transition-timing-function: cubic-bezier(1.000, 0.005, 0.150, 1.005)
.balloon-enter-from, .balloon-leave-to
  transform: translateY(100%) rotate(-30deg) !important
  opacity: 0 !important
.balloon-leave-to
  transform: translateY(100%) scale(0.4) !important

.balloon-box
  position: absolute
  top: 0
  left: 50%
  transform: translate(-50%, -100%)
  width: 100%
  height: 80%
.balloon
  position: absolute
  background: white
  box-shadow: 5px 5px 10px rgba(#000, 0.1)
  border-radius: 9999px
  padding: 1rem 2rem
  font-size: 2rem
  &::before
    content: ''
    width: 2rem
    height: 2rem
    position: absolute
    left: 30%
    bottom: 0
    transform: translateX(-40%) rotate(30deg)
    background: white
    box-shadow: 5px 5px 10px rgba(#000, 0.01)
</style>

以上準備好對話泡泡的功能了!( ‧ω‧)ノ╰(‧ω‧ )

現在讓我們回到大廳,實際觸發對話泡泡看看,首先透過 ref 取得所有的玩家頭像。

src\views\game-console-lobby.vue

<template>
  ...
  <div class="absolute inset-0 flex">
    <div class="flex w-full h-full">
      <!-- 選單 -->
      <div class="w-1/3 flex flex-col p-12">
        ...
        <!-- 玩家清單 -->
        <transition-group ... >
          <player-avatar
            v-for="player in playersInfo"
            ref="players"
            ...
          />
        </transition-group>
      </div>
      ...
    </div>
  </div>
</template>

<script setup lang="ts">
...

const playersInfo = computed(() => { ... });

const players = ref<InstanceType<typeof PlayerAvatar>[]>([]);
...
</script>
...

接著試試看在 onGamepadData 事件中觸發 player-avatar 的 showBalloon()。

ezgif-2-030b0663db.gif

成功讓玩家說話了!◝( •ω• )◟

使用搖桿控制主選單

現在讓我們透過搖桿選擇主選單按鈕,新增 use-gamepad-navigator 用於儲存按紐並觸發相關控制。

首先制定控制元件基本介面,用來說明、限制此功能能夠支援使用的控制元件。

src\composables\use-gamepad-navigator.ts

import { ref } from 'vue';

export interface ControlElement {
  click(): void;
  hover(): void;
  leave(): void;
  isHover(): boolean;
}

export function useGamepadNavigator<T extends ControlElement>() {
  const controlElements = ref<T[]>([]) as Ref<T[]>;

  return {
  }
}

想了解泛型加上限制的說明可以參考此連結:https://ithelp.ithome.com.tw/m/articles/10266542

接著加入各類 function。

import { computed, onMounted, Ref, ref } from 'vue';
...
export function useGamepadNavigator<T extends ControlElement>() {
  ...
  /** hover 指定元件 */
  function hoverElement(index: number) {
    controlElements.value.forEach((el) => el.leave());
    controlElements.value?.[index]?.hover();
  }

  /** 目前 hover 元件的 index */
  const currentIndex = computed(() =>
    controlElements.value.findIndex(({ isHover }) => isHover())
  );

  /** 上一個元件 */
  function prev() {
    if (currentIndex.value < 0) {
      return hoverElement(0);
    }

    let targetIndex = currentIndex.value - 1;
    if (targetIndex < 0) {
      targetIndex += controlElements.value.length;
    }

    return hoverElement(targetIndex);
  }

  /** 下一個元件 */
  function next() {
    if (currentIndex.value < 0) {
      return hoverElement(0);
    }

    const targetIndex = (currentIndex.value + 1) % controlElements.value.length;
    return hoverElement(targetIndex);
  }

  /** 點擊目前 hover 元件 */
  function click() {
    if (currentIndex.value < 0) {
      hoverElement(0);
      return controlElements.value?.[0]?.click();
    }

    const targetIndex = currentIndex.value;
    hoverElement(targetIndex);
    return controlElements.value[targetIndex].click();
  }

  /** 自動 hover 第一個元件 */
  onMounted(() => {
    controlElements.value?.[0]?.hover();
  });

  return {
    mountElement,
    next,
    prev,
    click,
  }
}

回到大廳中引用 use-gamepad-navigator,新增綁定元件用的 function 並綁定至想要被控制元件的 ref 中。

src\views\game-console-lobby.vue

<template>
  ...
  <div class="absolute inset-0 flex">
    <div class="flex w-full h-full">
      <!-- 選單 -->
      <div class="w-1/3 flex flex-col p-12">
        <div class="flex flex-col flex-1 justify-center items-center gap-14">
          ...
          <btn-base
            :ref="mountElement"
            label="開始遊戲"
            ...
          >
            ...
          </btn-base>

          <btn-base
            :ref="mountElement"
            label="結束派對"
            ...
          >
            ...
          </btn-base>
        </div>
        ...
      </div>
      ...
    </div>
  </div>
</template>

<script setup lang="ts">
...
import { useGamepadNavigator } from '../composables/use-gamepad-navigator';
...
function mountElement(el: any) {
  const controlElement = el as InstanceType<typeof BtnBase>;
  gamepadNavigator.mountElement(controlElement)
}
...
</script>
...

會忽然發現 TypeScript 警告引數指派類型錯誤。Σ(ˊДˋ;)

Untitled

這是因為 btn-base 組件對外提供的介面不符合 use-gamepad-navigator 規定的 ControlElement 介面,現在讓我們新增 btn-base 的 defineExpose,讓介面相符吧。( ´ ▽ ` )ノ

src\components\btn-base.vue

...
<script setup lang="ts">
...
import { ControlElement } from '../composables/use-gamepad-navigator';
...
defineExpose<ControlElement>({
  click: handleClick,
  isHover: () => state.hover,
  hover: handleMouseenter,
  leave: handleMouseleave,
});
</script>
...

現在回到 game-console-lobby,就會發現剛剛的錯誤消失了。ヽ(≧∀≦)ノ

現在讓我們在 onGamepadData 中呼叫 use-gamepad-navigator 的控制功能吧。

src\views\game-console-lobby.vue

...
<script setup lang="ts">
function init() {
  ...
  gameConsole.onGamepadData((data) => {
    ...
    if (action === 'up') {
      gamepadNavigator.prev();
      return;
    }

    if (action === 'down') {
      gamepadNavigator.next();
      return;
    }

    if (action === 'confirm') {
      gamepadNavigator.click();
      return;
    }
  });
}
init();
</script>
...

現在可以透過搖桿控制選單了!ლ(´∀`ლ)

ezgif-2-aff7d30598.gif

但是有一個小問題,就是觸發 click 沒有觸發 btn-base 點擊動畫,讓我們改進一下 handleClick。

src\components\btn-base.vue

<script setup lang="ts">
...
import { promiseTimeout } from '@vueuse/core';
...
function handleClick(showEffect = false) {
  emit('click');

  if (showEffect) {
    processClick();
  }
}
async function processClick() {
  state.hover = true;
  state.active = true;

  await promiseTimeout(200);

  state.active = false;
}
...
defineExpose<ControlElement>({
  /**
   * @param effect 點擊效果,預設 true
   */
  click(effect = true) {
    handleClick(effect)
  },
  ...
});
</script>

現在可以透過搖桿隔空觸發按鈕點擊動畫了!✧*。٩(ˊᗜˋ*)و✧*。

ezgif-2-a3c1a98fe1.gif

再來準備讓我們進入重頭戲,開發企鵝遊戲的部分了!( •̀ ω •́ )✧

總結

  • 完成玩家頭像對話框框功能
  • 完成透過搖桿控制大廳選單功能

以上程式碼已同步至 GitLab,大家可以前往下載:

GitLab - D19


上一篇
D18 - 玩家一起粗乃玩!
下一篇
D20 - I want to play a game
系列文
派對動物嗨起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言