LoginSignup
5
3

More than 3 years have passed since last update.

[連載]スーパーマリオ的なゲームをjavascriptで作ってみる 初級編 〜6章〜 テキがいてこそ

Last updated at Posted at 2020-12-31

本連載について

本章の概要

やっぱりゲームといえば、主人公以上に重要な役割を担うと言っても過言ではないのが”敵”の存在ですよね!
ってことで、敵を登場させたいと思います

  • 本章の内容は大きく4ステップです
    • その1 〜とりあえず登場させるの巻〜
    • その2 〜あったたらだめよの巻〜
    • その3 〜ジャンプで踏みつければOKの巻〜
    • その4 〜増殖させようの巻〜
  • 各ステップごとに実際のソースをQiita上に記載しています
  • 上記と同じくソースの実態を保存しているgitのリポジトリも記載しています
    リンクにアクセスして実際のソースをダウンロードすることができます
    ぜひダウンロードして動かしながら試してみてください!

その1 〜とりあえず登場させるの巻〜

ゴール

  • とりあえず動く敵を登場させる

前提

  • 画像は作っておく必要があります
  • 着地判定や、ブロックから落ちたら自由落下する部分などは、既存ロジックを流用して実現します

やること

  • 敵の画像ファイルを作成し、配置します ([参考]ドッド絵を作成する)
  • 敵の位置を算出します
  • 敵の画像を描画します
  • サンプルソースでは以下のファイル構成としています
└┬─ src    ── ...
 │         
 └─ images ┬─ character-01 ── ...
           └─ character-02 ── base.png
           └─ ground-01    ── ...

実装内容

index.js

...(省略)

// 敵の情報のパラメータ宣言 & 初期化
var enemyX = 550;
var enemyY = 0;
var enemyIsJump = true;
var enemyVy = 0;

...(省略)

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {

  // 画面全体をクリア
  ctx.clearRect(0, 0, 640, 480);

  // アップデート後の敵の座標
  var updatedEnemyX = enemyX;
  var updatedEnemyY = enemyY;

  // 敵は左に固定の速度で移動するようにする
  updatedEnemyX = updatedEnemyX - 1;

  // 敵の場合にも、主人公の場合と同様にジャンプか否かで分岐
  if (enemyIsJump) {
    // ジャンプ中は敵の速度分だけ追加する
    updatedEnemyY = enemyY + enemyVy;

    // 速度を固定分だけ増加させる
    enemyVy = enemyVy + 0.5;

    // ブロックを取得する
    const blockTargetIsOn = getBlockTargetIsOn(
      enemyX,
      enemyY,
      updatedEnemyX,
      updatedEnemyY
    );

    // ブロックが取得できた場合には、そのブロックの上に立っているよう見えるように着地させる
    if (blockTargetIsOn !== null) {
      updatedEnemyY = blockTargetIsOn.y - 32;
      enemyIsJump = false;
    }
  } else {
    // ブロックの上にいなければジャンプ中の扱いとして初期速度0で落下するようにする
    if (
      getBlockTargetIsOn(enemyX, enemyY, updatedEnemyX, updatedEnemyY) === null
    ) {
      enemyIsJump = true;
      enemyVy = 0;
    }
  }

  // 算出した結果に変更する
  enemyX = updatedEnemyX;
  enemyY = updatedEnemyY;

  // 敵の画像を表示
  var enemyImage = new Image();
  enemyImage.src = "../images/character-02/base.png";
  ctx.drawImage(enemyImage, enemyX, enemyY, 32, 32);

  ...(省略)

}

※ 実際のソースコードは こちら からダウンロードできます

説明

  • 主人公の座標算出のために行っていることとほぼ同じことを実施してあげればOKです
  • キー入力による制御がないため、固定で左方向に移動する && ブロックの境でも進んでそのまま落下する ものとしています
  • だいぶ煩雑になってきてしまいましたね、、本当はメソッド切り出しとかすべきなのですが本章ではこのまま進んでしまいます
  • とりあえず、敵が動く状態までで、当たり判定はその2で行います

CodePenのサンプル

See the Pen mario-game-tutorial-01-06-01 by taku7777777 (@taku7777777) on CodePen.

その2 〜あったたらだめよの巻〜

ゴール

  • 敵にあたったら、ゲームオーバーとなるようにする

前提

  • あたり判定は、簡易的に更新後の座標のみを用いて行います (着地判定のように、更新前の座標は用いない)

やること

  • 更新後の主人公の座標と敵の座標から当たり判定を行います
  • あたっていたら、ゲームオーバーとすします

実装内容

index.js

...(省略)

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {

  ...(省略)

  // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある
  if (!isGameOver) {
    // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする
    var isHit = isAreaOverlap(updatedX, updatedY, 32, 32, updatedEnemyX, updatedEnemyY, 32, 32);

    if (isHit) {
      // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える
      isGameOver = true;
      vy = -10;
    }
  }

  ...(省略)

}

...(省略)

/**
 * 2つの要素(A, B)に重なる部分があるか否かをチェックする
 * 要素Aの左上の角の座標を(ax, ay)、幅をaw, 高さをahとする
 * 要素Bの左上の角の座標を(bx, by)、幅をbw, 高さをbhとする
 */
function isAreaOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
  // A要素の左側の側面が、Bの要素の右端の側面より、右側にあれば重なり得ない
  if (bx + bw < ax) {
    return false;
  }
  // B要素の左側の側面が、Aの要素の右端の側面より、右側にあれば重なり得ない
  if (ax + aw < bx) {
    return false;
  }

  // A要素の上側の側面が、Bの要素の下端の側面より、下側にあれば重なり得ない
  if (by + bh < ay) {
    return false;
  }
  // B要素の上側の側面が、Aの要素の下端の側面より、上側にあれば重なり得ない
  if (ay + ah < by) {
    return false;
  }

  // ここまで到達する場合には、どこかしらで重なる
  return true;
}

※ 実際のソースコードは こちら からダウンロードできます

説明

  • 2つの要素が重なる部分があるかチェックするメソッド(isAreaOverlap)を追加します
  • 重なりがあるか否かのチェックは、重ならない可能性を排除してって、最後まで排除されなかったらもう重なっているしかないよね。って感じのロジックにしています
  • 主人公と敵の更新後の座標を特定し終えた後に、主人公と敵の座標とで重なりがあるかチェックします
  • 重なりがある場合にはゲームオーバーとします

CodePenサンプル

See the Pen mario-game-tutorial-01-06-02 by taku7777777 (@taku7777777) on CodePen.

その3 〜ジャンプで踏みつければOKの巻〜

ゴール

  • ジャンプして敵を踏みつけたら、敵をやっつけられるようにする

前提

  • 厳密には敵の上側の側面からあたった場合に敵を倒すことができる、とすべきであるがここでは簡易的にジャンプ中かつ下向きに進んでいる状態で敵にあたったら敵を倒すことができるとします(実際にはほぼ問題なし)

やること

  • 当たり判定を行った後に、ジャンプ中 && 下向きに進んでいた場合には敵を倒す(見えなくする && あたり判定の対象から除外する)ようにします

実装内容

index.js

...(省略)

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {

  ...(省略)

  // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある
  if (!isGameOver) {
    // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする
    var isHit = isAreaOverlap(updatedX, updatedY, 32, 32, updatedEnemyX, updatedEnemyY, 32, 32);

    if (isHit) {
      if (isJump && vy > 0) {
        // ジャンプしていて、落下している状態で敵にぶつかった場合には
        // 敵を消し去る(見えない位置に移動させる)とともに、上向きにジャンプさせる
        vy = -7;
        enemyY = 500;
      } else {
        // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える
        isGameOver = true;
        vy = -10;
      }
    }
  }

  ...(省略)

}

※ 実際のソースコードは こちら からダウンロードできます

説明

  • -7とか、マジックナンバー入っちゃっていますが、ここでは一旦無視で、、、動かしてみてそれっぽく動く値を入れています

CodePenサンプル

See the Pen mario-game-tutorial-01-06-03 by taku7777777 (@taku7777777) on CodePen.




その4 〜増殖させようの巻〜

ゴール

  • 敵を量産する

前提

  • 初期画面表示時に、複数の敵を登場させるようにします。
  • 時間立つごとにどんどん敵が増えてく、みたいな感じにしてもよいのですが、制御めんどうなのでここでは予め敵を配置しておくようにします

やること

  • 複数の敵の情報を保持できるようにします
  • 初期位置を決めます
  • 座標の計算・あたり判定・敵の描画を敵ごとに実施するようにします

実装内容

index.js

...(省略)

// 敵の情報のパラメータ宣言 & 初期化
var enemies = [
  { x: 550, y: 0, isJump: true, vy: 0 },
  { x: 750, y: 0, isJump: true, vy: 0 },
  { x: 300, y: 180, isJump: true, vy: 0 },
];

...(省略)

// 画面を更新する関数を定義 (繰り返しここの処理が実行される)
function update() {

  // 画面全体をクリア
  ctx.clearRect(0, 0, 640, 480);

  // 敵情報ごとに、位置座標を更新する
  for (const enemy of enemies) {
    // アップデート後の敵の座標
    var updatedEnemyX = enemy.x;
    var updatedEnemyY = enemy.y;
    var updatedEnemyInJump = enemy.isJump;
    var updatedEnemyVy = enemy.vy;

    ...(省略ほぼ既存ロジックと同じ)

    // 算出した結果に変更する
    enemy.x = updatedEnemyX;
    enemy.y = updatedEnemyY;
    enemy.isJump = updatedEnemyInJump;
    enemy.vy = updatedEnemyVy;
  }

  ...(省略)

  // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある
  if (!isGameOver) {
    // 敵情報ごとに当たり判定を行う
    for (const enemy of enemies) {
      // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする
      var isHit = isAreaOverlap(x, y, 32, 32, enemy.x, enemy.y, 32, 32);

      if (isHit) {
        if (isJump && vy > 0) {
          // ジャンプしていて、落下している状態で敵にぶつかった場合には
          // 敵を消し去る(見えない位置に移動させる)とともに、上向きにジャンプさせる
          vy = -7;
          enemy.y = 500;
        } else {
          // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える
          isGameOver = true;
          vy = -10;
        }
      }
    }
  }

  ...(省略)

  // 敵情報ごとに当たり判定を行う
  for (const enemy of enemies) {
    ctx.drawImage(enemyImage, enemy.x, enemy.y, 32, 32);
  }

  ...(省略)

}

※ 実際のソースコードは こちら からダウンロードできます

説明

- 以下の4点において、複数データを扱えるようにします
1. 複数の敵の情報を保持できるように、変数の持ち方を変える(配列で保持するようにする)
2. 敵座標の計算を敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)
3. 主人公との当たり判定も敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)
4. 敵の描画を敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)

CodePenのサンプル

See the Pen mario-game-tutorial-01-06-04 by taku7777777 (@taku7777777) on CodePen.

終わりに

お疲れまです!!!

初級編は一旦このへんで...

これ以上は、ちゃんとリファクタしながら進めてあげないとソースがカオスになってしまうので、
(もうだいぶカオスですが)
次回以降はtypescriptを用いてリファクタしながら進めます。
導入のしやすさをとってjavascriptを使ってましたが、
実際には圧倒的にtypescriptのほうが開発楽なので、typescriptに頼っていきます!!

ここまで来たあなたら、typescriptの利用はメリットしかないはずです!!!!!!!!!



5
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3