iT邦幫忙

2022 iThome 鐵人賽

DAY 24
2

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

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

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

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

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

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

Yes


助教:「都沒動作的移動很像幽靈欸,能不能再給力一點?」

鱈魚助教:「都沒動作的移動很像幽靈欸,能不能再給力一點?」

鱈魚:「那就來點動畫吧。(´,,•ω•,,)」

讓我們呼叫模型中的動畫,利用動畫權重混合功能,讓兩種動畫之間切換滑順。

原理很簡單,就是把播放狀態從改成以 0 到 1 的權重呈現,原有動畫為 1,目標動畫為 0。

動畫切換時,原有動畫逐步減少至 0,目標動畫逐步增加至 1,如此便可以在兩種動畫之間平滑切換。

參考資料:babylon.js - Advanced Animating Methods

新增功能。

...
export class Penguin {
  ...
  async init() {
    ...

    this.setState('idle');

    this.scene.registerBeforeRender(...);

    return this;
  }

  /** 指定移動方向與力量 */
  walk(force: Vector3) {
    ...
    if (this.rotateAction) {
      ...
      /** 播放走路動畫 */
      this.animation.walk?.start(true);
      this.setState('walk');
      /** 在停止走路後,過 500ms 時切換為 idle 狀態 */
      this.setIdleStateDebounce();
    }

  }
  ...
  /** 設定人物狀態 */
  private setState(value: State) {
    this.processStateAnimation(value);
    this.state = value;
  }
  /** 處理狀態動畫 */
  private processStateAnimation(newState: State) {
    if (newState === this.state) return;

    const playingAni = this.getAnimationByState(this.state);
    const targetAni = this.getAnimationByState(newState);

    this.state = newState;
    if (!targetAni || !playingAni) return;

    /** 攻擊動畫不循環播放 */
    const loop = this.state !== 'attack';
    /** 切換至攻擊動畫速度要快一點 */
    const offset = this.state === 'attack' ? 0.3 : undefined;

    this.scene.onBeforeRenderObservable.runCoroutineAsync(this.animationBlending(playingAni, targetAni, loop, offset));
  }
  /** 動畫混合 */
  private * animationBlending(fromAni: AnimationGroup, toAni: AnimationGroup, loop = true, offset = 0.1) {
    let currentWeight = 1;
    let targetWeight = 0;

    toAni.play(loop);

    while (targetWeight < 1) {
      targetWeight += offset;
      currentWeight -= offset;

      toAni.setWeightForAllAnimatables(targetWeight);
      fromAni.setWeightForAllAnimatables(currentWeight);
      yield;
    }

    fromAni.stop();
  }
  /** 根據 state 取得動畫 */
  private getAnimationByState(value: State) {
    return this.animation[value];
  }
  /** 停止呼叫後 500ms 設為 idle 狀態 */
  private setIdleStateDebounce = debounce(async () => {
    this.setState('idle');
  }, 500)
}

ezgif-2-977141b6a5.gif

助教:「看起來不錯,但好像少了點甚麼?(´・ω・`)」

鱈魚:「那就讓企鵝增加攻擊技能吧!ヽ(●`∀´●)ノ 」

增加攻擊功能,並加入以下限制:

  • 每 2 秒才能攻擊一次
  • 攻擊時無法變換方向和移動
  • 攻擊需要至少耗費 1 秒
...
export class Penguin {
  ...
  /** 指定移動方向與力量 */
  walk(force: Vector3) {
    ...
    // 攻擊時舞法移動
    if (this.state === 'attack') return;

    ...
  }
  ...
  /** 攻擊,限制攻擊頻率,2 秒一次 */
  attack = throttle(() => {
    this.setState('attack');
    this.leaveAttackStateDebounce();
    this.setIdleStateDebounce.cancel();
  }, 2000, {
    leading: true,
    trailing: false,
  })
  /** 攻擊結束後 1 秒時,回到 idle 狀態 */
  private leaveAttackStateDebounce = debounce(() => {
    this.setState('idle');
  }, 1000, {
    leading: false,
    trailing: true,
  })

  /** 取得力與人物的夾角 */
  ...
}

接著透過空白建發動攻擊吧。

src\games\the-first-penguin\game-scene.vue

...
<script>
async function init() {
  ...
  scene.onKeyboardObservable.add((keyboardInfo) => {
    ...
    switch (keyboardInfo.event.key) {
      ...
      case ' ': {
        penguin.attack();
        break;
      }
    }
  });
  ...
}
</script>

現在我們得到可以幹架的完全體企鵝了!◝(≧∀≦)◟

ezgif-5-9da212cf46.gif

接著讓我們實作被攻擊效果吧,新增一個被攻擊的 method 並微調部分程式。

概念很簡單,就是被攻擊時會施加一個指定的力量即可。

src\games\the-first-penguin\penguin.ts

...
export class Penguin {
  ...
  private readonly assaultedForce = new Vector3(20, 20, 20);

  constructor(name: string, scene: Scene, params?: PenguinParams) {
    ...
    /** 設定預設值 */
    this.params = defaultsDeep(params, this.params);
  }

  ...
  /** 被攻擊
   * @param direction 移動方向
   */
  assaulted = throttle((direction: Vector3) => {
    if (!this.mesh) {
      throw new Error('未建立 Mesh');
    }

    // 計算力量
    const force = direction.normalize().multiply(this.assaultedForce);
    this.mesh.physicsImpostor?.applyImpulse(force, Vector3.Zero());
  }, 500, {
    leading: true,
    trailing: false,
  })
  ...
}

現在來實作攻擊判定,回到場景,稍微調整一下建立企鵝 function 內容,建立一隻沙包企鵝。

src\games\the-first-penguin\game-scene.vue

...
<script>
...
let scene: Scene;
const penguins: Penguin[] = [];
...
async function createPenguin(id: string, index: number) {
  const penguin = await new Penguin(`penguin-${index}`, scene, {
    position: new Vector3(5 * index, 10, 0),
    ownerId: id,
  }).init();

  return penguin;
}

async function init() {
  ...
  createIce(scene);

  const result = await Promise.allSettled([
    createPenguin('', 0),
    createPenguin('', 1),
  ])
  result.forEach((data) => {
    if (data.status !== 'fulfilled') return;
    penguins.push(data.value);
  });

  scene.onKeyboardObservable.add((keyboardInfo) => {
    ...
    const penguin = penguins[0];

    switch (keyboardInfo.event.key) { ... }
  });
  ...
}
</script>

現在畫面上有兩隻企鵝了。

Untitled

接著加入攻擊碰撞偵測。

src\games\the-first-penguin\game-scene.vue

<script setup lang="ts">
...
async function createPenguin(id: string, index: number) {...}

// 偵測企鵝碰撞事件
function detectCollideEvents(penguins: Penguin[]) {
  const length = penguins.length;
  for (let i = 0; i < length; i++) {
    for (let j = i; j < length; j++) {
      if (i === j) continue;

      const aMesh = penguins[i].mesh;
      const bMesh = penguins[j].mesh;
      if (!aMesh || !bMesh) continue;

      if (aMesh.intersectsMesh(bMesh)) {
        handleCollideEvent(penguins[i], penguins[j]);
      }
    }
  }
}

function handleCollideEvent(aPenguin: Penguin, bPenguin: Penguin) {
  if (!aPenguin.mesh || !bPenguin.mesh) return;

  const aState = aPenguin.state;
  const bState = bPenguin.state;
  // 沒有企鵝在 attack 狀態,不須動作
  if (![aState, bState].includes('attack')) return;

  const direction = bPenguin.mesh.position.subtract(aPenguin.mesh.position);
  if (aState === 'attack') {
    bPenguin.assaulted(direction);
  } else {
    aPenguin.assaulted(direction.multiply(new Vector3(-1, -1, -1)));
  }
}

async function init() {
  ...

  /** 持續運行指定事件 */
  scene.registerAfterRender(() => {
    detectCollideEvents(penguins);
  });

  /** 反覆渲染場景,這樣畫面才會持續變化 */
  ...
}
...
</script>

現在企鵝可以兇殘的擊飛同類了!( •̀ ω •́ )y

ezgif-5-294ca6ced1.gif

總結

  • 完成企鵝攻擊與被攻擊效果
  • 完成企鵝攻擊碰撞偵測

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

GitLab - D24


上一篇
D23 - 爆走企鵝
下一篇
D25 - 建立類比控制搖桿
系列文
派對動物嗨起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言