2018/06/19

[JS]ゲーム内に重力を追加し自然なジャンプモーションを実装する(3パターンで実装)

アクションゲームになくてはならない要素、それはジャンプモーション!
今回はゲーム内に重力を加え、自然なジャンプモーションを実装する。

方法としては3通りあるので、それぞれ解説していく。

  • 三角関数(sin)を使う
    • 月面でジャンプしているようなふんわりしたモーション
  • 速度に加速度を足しこむ
    • 簡単で自然なジャンプモーション
  • 鉛直投げ上げの公式(y = v0t - 0.5gt^2)を使う
    • より自然界に近いジャンプモーション


三角関数を使ったジャンプモーション


sin関数は引数にとるラジアンの値が大きくなるにつれ、0→1→0→-1→0のように値が変化する。その値をy軸に当てはめることでジャンプモーションが実装できる。

サイン波のような軌道を描くので、月面でジャンプしたようなモーションになる。
// 1秒間(60frame)で着地する
const radPerFrame = Math.PI * 2 / 60

// Canvas上では左上が原点になるため、ジャンプ方向を反転させるため最後に-1をかける
// x10は係数でジャンプの高さを調整する
y += (Math.sin(radPerFrame * frame) * 10) * -1
frame++


ラジアンは2πで360°となる。滞空時間を1秒間(60フレーム)にしたい場合は、1フレームにつき角度が2π÷60分大きくなれば良いので、radPerFrame * frameと実装している。



速度に加速度を足し込んだジャンプモーション


速度に加速度を足しこんでそれをy軸に設定する。これだけでかなり自然なジャンプモーションが実装できる。
// 重力
let gravity = 0
// y軸方向の速度
let yv = 0

// ジャンプ中
gravity = 0.4

// ジャンプをやめたら
gravity = -0.4

/**
 * ジャンプ処理
 */

 // y軸方向の速度に重力を足し込む
yv += gravity
// 速度yvをy軸に足し込む
y += yv
ジャンプ中は重力をプラスに、頂点に達して落下してくるときは重力をマイナスに変換することで、自然なジャンプモーションが実装できる。

ただし大きなジャンプなど時間が立てば経つほど値が正確でなくなってくる。



鉛直投げ上げの公式を使ったジャンプモーション


高校物理で習う鉛直投げ上げの公式を使ったジャンプモーションだ。
y = v0t - 1/2gt^2 (t: 時間, v0: 初速度, g: 重力加速度)

詳しい説明は省くが、先ほど紹介した加速度を足し込む式を積分するとこの公式になる。
// 重力加速度(ゲーム内用に調整)
const gravity = 0.4;

// ジャンプ中の経過時間
let time = 0;

// y軸方向の初速度
const yv = 10;

/**
 * ジャンプ処理
 */
// 投げ上げの公式(※y軸方向に反転 + 地面につくようにcanvasの高さを足す)
y = 0.5 * gravity * time * time - yv * time + (canvas.height - h);
time++;
地球上における重力加速度は9.8m/s^2なのだが、ゲーム用に調整している。

ジャンプ中の時間をtimeで保持しておき、鉛直投げ上げの公式を使ってy軸に設定する。
ただし、CanvasAPIは左上が原点(0,0)なので、y軸方向に反転させ、かつ地面につくように調整している。

この公式を使ったジャンプモーションが一番自然界に近くなる。


サンプルプログラムは以下のとおり。
<div id="app"></div>
<button id="stop">stop</button>
class Player {
  constructor() {
    this.x = 0;
    this.y = 0;
    this.w = this.h = 32;
    this.xv = 5;
    this.yv = 10;
    this.gravity = 0.4;
    this.jumping = false;
    this.jumpTimer = 0;
  }

  draw() {
    game.ctx.fillStyle = 'red'
    game.ctx.fillRect(this.x, this.y, this.w, this.h);
  }

  update(game) {
    if (game.keyboard.includes("ArrowLeft")) {
      if (this.x <= 0) {
        this.x = 0;
      } else {
        this.x -= this.xv;
      }
    }
    if (game.keyboard.includes("ArrowRight")) {
      if (this.x + this.w >= game.canvas.width) {
        this.x = game.canvas.width - this.w;
      } else {
        this.x += this.xv;
      }
    }
    if (game.keyboard.includes("Space")) {
      if (!this.jumping) {
        this.jumping = true;
        this.jumpTimer = 0;
      }
    }

    // 投げ上げの公式(※y軸方向に反転)
    this.y = (0.5 * this.gravity * this.jumpTimer * this.jumpTimer - this.yv * this.jumpTimer) + (game.canvas.height - this.h);
    this.jumpTimer++;

    if (this.y > game.canvas.height - this.h) {
      this.y = game.canvas.height - this.h;
      this.jumping = false
    }
  }
}

class Game {
  constructor(el) {
    this.timer = 0;
    this.items = [];
    this.keyboard = [];
    this.setEventListener();

    this.canvas = document.createElement("canvas");
    this.canvas.width = 320;
    this.canvas.height = 240;
    this.ctx = this.canvas.getContext("2d");
    document.querySelector(el).appendChild(this.canvas);
  }
  setEventListener() {
    document.addEventListener("keydown", e => {
      this.keyboard.push(e.code);
    });
    document.addEventListener("keyup", e => {
      this.keyboard = this.keyboard.filter(a => a !== e.code);
    });
  }

  add(item) {
    this.items.push(item);
  }

  start() {
    this.render();
  }

  stop() {
    cancelAnimationFrame(this.timer);
  }

  render() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.items.forEach(a => {
      a.update(this);
      a.draw(this);
    });

    this.timer = requestAnimationFrame(this.render.bind(this));
  }
}

const player = new Player();
const game = new Game("#app");
game.add(player);
game.start();


以上

written by @bc_rikko

0 件のコメント :

コメントを投稿