iT邦幫忙

2022 iThome 鐵人賽

DAY 23
2

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

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

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

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

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

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

Yes


讓企鵝動起來吧!

由於在建立冰塊時,已經將摩擦力(friction)設定為 0,所以企鵝只要動起來就不會停下來。

要模擬出在光滑表面上操作的感覺很簡單,只要讓玩家的方向操作,是在企鵝上加「加速度」,而不是「速度」就可以了。

讓我們新增一個移動用的 method。

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

...
export class Penguin {
  ...
  /** 指定移動方向與力量 */
  walk(force: Vector3) {
    if (!this.mesh) {
      throw new Error('未建立 Mesh');
    }

    // 受力
    this.mesh.physicsImpostor?.applyImpulse(force, Vector3.Zero());
  }
}

接著我們先用鍵盤控制看看,建立鍵盤事件。

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

<script>
async function init() {
  ...
  const penguin = await createPenguin('', 1);

  scene.onKeyboardObservable.add((keyboardInfo) => {
    if (keyboardInfo.type !== KeyboardEventTypes.KEYDOWN) return;

  });
  ...
}
...
</script>

現在來讓企鵝走路,但是要怎麼判斷方向呢?

已知 babylon.js 坐標系是左手坐標系,而:

  • 正 X 方向是畫面右方
  • 正 Z 方向是畫面前方
  • 正 Y 方向是往上

所以程式如下。

<script>
async function init() {
  ...
  scene.onKeyboardObservable.add((keyboardInfo) => {
    if (keyboardInfo.type !== KeyboardEventTypes.KEYDOWN) return;

    switch (keyboardInfo.event.key) {
      case 'ArrowLeft': {
        penguin.walk(new Vector3(-1, 0, 0));
        break;
      }
      case 'ArrowUp': {
        penguin.walk(new Vector3(0, 0, 1));
        break;
      }
      case 'ArrowRight': {
        penguin.walk(new Vector3(1, 0, 0));
        break;
      }
      case 'ArrowDown': {
        penguin.walk(new Vector3(0, 0, -1));
        break;
      }
    }
  });
  ...
}
...
</script>

現在可以用鍵盤方向鍵移動企鵝了。

ezgif-1-caf1905c08.gif

我們得到一隻急速漂移的企鵝!ᕕ( ゚ ∀。)ᕗ

為了避免企鵝跑太快,讓我們加個速度限制吧。

...
export class Penguin {
  ...
  private readonly maxVelocity = new Vector3(6, 6, 6);
  ...
  async init() {
    ...
    // 持續呼叫
    this.scene.registerBeforeRender(() => {
      this.limitMaxVelocity();
    });

    return this;
  }
  ...
  private limitMaxVelocity() {
    if (!this.mesh || !this.mesh.physicsImpostor) return;

    const velocity = this.mesh.physicsImpostor.getLinearVelocity();
    if (!velocity) return;

    const currentSpeed = velocity.length();
    if (currentSpeed > this.maxVelocity.length()) {
      const newVelocity = velocity.normalize().multiply(this.maxVelocity);
      this.mesh.physicsImpostor?.setLinearVelocity(newVelocity);
    }
  }
}

助教:「只有這樣不給力捏 (´・ω・`)」

鱈魚:「那就讓企鵝轉個方向吧!ԅ(´∀` ԅ)」

只要求出力向量與人物向量夾角,再讓企鵝轉到指定角度即可。

兩向量夾角可以利用向量夾角公式求得,公式為:

Untitled

依公式設計一個計算受力角度的 method。

...
export class Penguin {
  ...
  /** 取得力與人物的夾角 */
  private getForceAngle(force: Vector3) {
    if (!this.mesh) {
      throw new Error('未建立 Mesh');
    }

    const forceVector = force.normalize();
    /** 企鵝面相正 Z 軸方向 */
    const characterVector = new Vector3(0, 0, 1);
    const deltaAngle = Math.acos(Vector3.Dot(forceVector, characterVector));

    /** 反餘弦求得角度範圍為 0~180 度,需要自行判斷負角度部分。
     *  力向量 X 軸分量為負時,表示夾角為負。
     */
    if (forceVector.x < 0) {
      return deltaAngle * -1;
    }

    return deltaAngle;
  }
}

得到很神經質的企鵝!ᕕ( ゚ ∀。)ᕗ

ezgif-1-6eeede8dbd.gif

助教:「能不能再給力一點?(´・ω・`)」

鱈魚:「那就讓轉向加上動畫吧!( ´ ▽ ` )ノ」

使用 ActionManager 與 InterpolateValueAction 達成補幀效果。

...
export class Penguin {
  ...
  /** 負責人物轉向動畫 */
  private rotateAction?: InterpolateValueAction;
  ...
  private initActionManager() {
    if (!this.mesh) return;

    this.mesh.actionManager = new ActionManager(this.scene);

    this.rotateAction = new InterpolateValueAction(
      ActionManager.NothingTrigger,
      this.mesh,
      'rotation',
      new Vector3(0, 3, 0),
      300
    );
    this.mesh.actionManager.registerAction(this.rotateAction);
  }
  ...
  async init() {
    ....
    // 初始化 actionManager
    this.initActionManager();

    this.scene.registerBeforeRender(...);
    return this;
  }

  /** 指定移動方向與力量 */
  walk(force: Vector3) {
    ...
    // 轉向
    const targetAngle = this.getForceAngle(force);
    if (this.rotateAction) {
      this.rotateAction.value = new Vector3(0, targetAngle, 0);
      this.rotateAction.execute();
    }
  }
  ...
}

旋轉吧!企鵝!

ezgif-1-1bab3f94f1.gif

助教:「轉是會轉了,不過為甚麼最後從左邊轉到下面的時候會轉那麼大圈啊!╭(°A ,°`)╮」

鱈魚:「我也想和你解釋,但我只是隻魚 (. ❛ ᴗ ❛.)」

助教:(拿出菜刀)

鱈魚:「冷靜冷靜 Σ(ˊДˋ;),讓我們來探討一下。」

探討以下例子,以企鵝向上看為 0 度位置:

  • 若起始角度為 180 度(向下),往左轉時應該要轉到 270 度才會自然(轉 90 度)。
  • 若起始角度為 -90 度(向左),往下轉時應該要轉到 -180 度才會自然(轉 -90 度)。

但是 getForceAngle 求得的角度 a 之範圍為 180 ≤ a < -180,所以結果會變成:

  • 若起始角度為 180 度(向下),往左轉時轉到 -90 度,轉了 -270 度。
  • 若起始角度為 -90 度(向左),往下轉時轉到 180 度,轉了 270 度。

看起來就是轉了很大一圈。

這裡有個很簡單的解決辦法,就是判斷如果旋轉角度大於 180 度,就先讓企鵝瞬間轉到目標角度 a 的兩倍補角位置,這樣考慮以上案例就會是:

  • 若起始角度為 -90 度(向左),目標轉到 180 度(向下),先瞬間轉到 270 度(看起來也是向左)再轉到目標,結果就是從 270 度轉到 180 度,轉了 -90 度。

看圖可能會比較好理解。

D23 - 旋轉企鵝 (3).png

轉向變自然了!✧*。٩(ˊᗜˋ*)و✧*。

ezgif-2-876037f34b.gif

總結

  • 完成企鵝移動功能
  • 完成更好的轉向功能

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

GitLab - D23


上一篇
D22 - 企鵝登場:建立企鵝 Class
下一篇
D24 - 完全體企鵝
系列文
派對動物嗨起來!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ShawnL
iT邦新手 1 級 ‧ 2022-10-07 14:22:30

不會轉向急速漂移的企鵝有種惡趣味遊戲才會出現的操作 XD
有笑有推

鱈魚 iT邦新手 1 級 ‧ 2022-10-07 14:25:29 檢舉

感謝捧場 XD

我要留言

立即登入留言