LoginSignup
3
2

More than 1 year has passed since last update.

TypeScriptでブロック崩しを作ろう(Excalibur.jsを添えて)

Last updated at Posted at 2022-07-16

概要

本記事はTypeScriptを使って...というより
Excalibur.jsを使ってブロック崩しを簡単に作ろう、という記事。
あわよくば、Excalibur.js信者を増やす作戦なのだ...フフフ。

Excalibur.jsって❓

JavaScriptの2Dゲームエンジン。
TypeScriptとの親和性が非常に高い。

同類にはPhaserとか、melonJSとかがある。

筆者は断然Excalibur.js推し。
Phaserは型情報がよく分からんときがあった。melonJSは使ったことない。
Excalibur.jsは型情報がバッチリ完備されてて、VSCodeでサクサクプログラミングできるのがお気に入り。

なお、 日本語の情報は皆無 。っていうか英語でもほぼない。
でも、必要なことは公式のDocDiscussionに載ってるので調べてみよう。
特にDiscussionで質問すると速攻で作者から返信が来るぞ❗Erik氏すげえ❗

環境構築

まず、エディタは何でもOK。以下、VSCodeを想定。
Node.jsも当然インストール済みだとする。

次。拙作のテンプレがあるのでそれを clone する。

git clone https://github.com/tenpaMk2/excalibur-parcel2-vscode-debuggable-template

次。 clone したフォルダをVSCodeで開いて、

npm install

する。
これでExcalibur.jsやらTypeScriptやらParcel2やら必要なものがインストールされる。

Parcel2って何❓って人はググってね。詳しい説明は省くよ(筆者もよくわかってないので...)。

この状態で

npm run start

して、↓のような表示が出れば成功。

> start
> parcel serve ./src/index.html

Server running at http://localhost:1234
✨ Built in 1.54s

これで、テンプレ既存のデモゲーム(ゲームじゃないけど)がトランスパイルされて、
おまけに閲覧用のサーバーも立ち上がる。
試しにアクセスすればゲームが動いているのが確認できるはず。
もしくは、適当なtsファイルを開いて、F5でデバッグ開始してもOK。

最後に、テンプレでしか使わないファイルを削除する。
↓を削除。

src
├── assets
│   └── roguelikeChar_transparent.png
├── config.ts
├── files.d.ts
├── objects
│   ├── ball.ts
│   └── ground.ts
├── resource.ts
└── scenes
    └── game-scene.ts

つまり、 index.htmlstyle.cssindex.ts だけ残せばOK。

作る

最初の一歩

ここからは公式docのGetting Startedに沿って進める。
いじるのは index.ts のみ。

まずは中身を全部削除して、↓のようにする。

import { DisplayMode, Engine } from "excalibur";

const game = new Engine({
  width: 800,
  height: 600,
  displayMode: DisplayMode.FitScreen,
  canvasElementId: "game",
});

engine.start();

いきなり公式docと違う部分があるけど目を瞑ってほしい。些末なことなので。
ブラウザで http://127.0.0.1:1234 にアクセスして、青い画面が表示されれば成功。

棒をつくる

続いて、ブロック崩しの『棒』部分を作る。
engine.start() のあとに↓を続ける。

const paddle = new Actor({
  x: 150, // 横位置
  y: game.drawHeight - 40, // 縦位置
  width: 200, // 幅
  height: 20, // 高さ
  color: Color.Chartreuse, // 色
  collisionType: CollisionType.Fixed, // 衝突判定タイプ
});
game.add(paddle); // これでゲームに登録される。忘れがちなので注意。

// 棒がマウスの動きに追従するようにする
game.input.pointers.primary.on("move", (evt) => {
  paddle.pos.x = evt.worldPos.x;
});

↓のように棒が表示されてれば成功。

スクリーンショット 2022-07-16 13.48.42.png

なお、↓のクラスはファイル上部でインポートすること。以後も新しいクラスはインポートするのを忘れないように。

  • Actor
  • Color
  • CollisionType

VSCodeで作業してるなら、タイプしたときに補完候補が↓のように表示されるはず。Tabで選択すれば自動で import してくれるぞ❗

スクリーンショット 2022-07-16 13.40.23.png

Excalibur.jsでは、ゲーム上に出現するオブジェクトはたいてい Actor クラスで実現する。
この Actor に位置や幅、高さ、衝突判定タイプなどの情報を与えてゲームに登録する。
で、各イベントごとに動作を制御してやると、ゲームとして動くというシロモノ。

今回も棒の動きの制御には move イベントを使う。
これは、マウスが動いたときに発火するイベント。
このイベントのオブジェクトに、マウスの位置が格納されるので、
それをそのまま棒の位置として使ってやればOK。簡単だね❗
なお、マウスが動く限り毎フレームごとにイベントは発火する。

球をつくる

const ball = new Actor({
  x: 100,
  y: 300,
  radius: 10, // 球半径
  color: Color.Red,
  collisionType: CollisionType.Passive,
});
game.add(ball);

// 1秒後に右下へ移動開始。
setTimeout(() => {
  ball.vel = new Vector(100, 100);
}, 1000);

Actorradius の情報を与えると球になる。
逆に言うと、 widthheight を与えると長方形になる。

球の衝突判定タイプは Passive
詳細は公式docを見てほしいが、ざっくり↓の認識でOK。

特徴 タイプ
衝突によって動かない Fixed
衝突によって動く Active
衝突を検知する Passive
衝突しない PreventCollision

分からなければあれこれ試してみて動きを見てね。

ボールの動きを制御するには .vel プロパティを書き換えればOK。
今回は重力も摩擦の設定もしてないので、等速直線運動する。
なお、 y座標は下向きが正なので注意

球が画面端で反射するようにする


ball.on("postupdate", () => {
  // 球が画面左にぶつかったら反射する
  if (ball.pos.x < ball.width / 2) {
    ball.vel.x = ballSpeed.x;
  }
  // 球が画面右にぶつかったら反射する
  if (game.drawWidth < ball.pos.x + ball.width / 2) {
    ball.vel.x = -ballSpeed.x;
  }

  // 球が画面上にぶつかったら反射する
  if (ball.pos.y < ball.height / 2) {
    ball.vel.y = ballSpeed.y;
  }
});

.on() でイベントに処理を紐付ける。
postupdate イベントは毎フレームの更新処理ごとに呼ばれる。
流れとしては以下↓。

  1. preupdate イベントの処理
  2. update() の処理
  3. postupdate イベントの処理
  4. (1フレームごとに)1から繰り返し

ball.pos.x は球の横位置。
横位置は球の中心なので注意
なお、この中心をどこにするかは anchor プロパティで制御できる。

画面端の処理を追加したけど、実際に画面端にボールを弾けるようになるのはこのあと。

ブロックを作る

const padding = 20;
const xoffset = 65;
const yoffset = 20;
const columns = 5;
const rows = 3;

const brickColors = [Color.Violet, Color.Orange, Color.Yellow];

const brickWidth = game.drawWidth / columns - padding - padding / columns; // px
const brickHeight = 30; // px
const bricks: Actor[] = [];
for (let j = 0; j < rows; j++) {
  for (let i = 0; i < columns; i++) {
    const brick = new Actor({
      x: xoffset + i * (brickWidth + padding) + padding,
      y: yoffset + j * (brickHeight + padding) + padding,
      width: brickWidth,
      height: brickHeight,
      color: brickColors[j % brickColors.length],
      collisionType: CollisionType.Active,
    });
    game.add(brick);
    bricks.push(brick);
  }
}

基本的には棒をつくるのと基本は変わらない。
位置や幅の計算がめんどくさいだけ。説明省略。

球が弾かれるようにする

let colliding = false;
ball.on("collisionstart", function (ev) {
  // 衝突開始時の処理

  if (bricks.indexOf(ev.other) > -1) {
    ev.other.kill(); // ブロックにあたっていたら消去する
  }

  // MTV(Minimum Translation Vector)を正規化するらしい。このへんは謎。
  const intersection = ev.contact.mtv.normalize();

  if (!colliding) {
    colliding = true;
    if (Math.abs(intersection.x) > Math.abs(intersection.y)) {
      ball.vel.x *= -1; // MTVを利用して、反射を処理する
    } else {
      ball.vel.y *= -1; // MTVを利用して、反射を処理する
    }
  }
});

ball.on("collisionend", () => {
  // 衝突終了時の処理

  colliding = false;
});

衝突時のイベントの処理順も公式docから画像を引用。

スクリーンショット 2022-07-16 14.51.30.png

collisionstart イベントが衝突開始時のイベントで、
collisionend イベントが衝突終了時のイベントだね。
これ以外には衝突中に毎フレーム発火し続ける precollision とか postcollision イベントもある

MTVの説明は...省略させて...。筆者もよくわかってない。
precollision イベントオブジェクトには上下左右のどこにぶつかったのかを判定するプロパティがあるので、そっちを使ったほうが良いと思うのだがなあ。
気が向いた人はそっちで書き直してみよう(丸投げ)。

ゲームオーバーを作る

ball.on("exitviewport", () => {
  alert("You lose!");
});

これだけ。 exitviewport イベントは、画面の外に Actor が出たら発火するイベント。
画面の上と左右は球を反射するようにしているので、下から出たときだけゲームオーバーになる、という寸法だね。

できあがり

スクリーンショット 2022-07-16 15.05.11.png

以上、これでできあがり。
どうだろう。意外と簡単だったんじゃないだろうか?
要は↓を記述さえすればゲームになる❗簡単❗

  • どこにどういう Actor がいて、
  • どういうとき(イベント発火時)にどういう動きをするのか

Excalibur.jsどう❓

使いやすくない❓いい感じじゃない❓
githubのstarが1000しかないなんて信じられない。皆もっと使おう。

宣伝

他にも↓みたいなゲームが作れるよ。皆も作ろう❗筆者も作ったんだからさ。

最後に

この記事でExcalibur.js信者が増えれば幸い。
ご意見ご質問あればコメントお願いします。

3
2
0

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
3
2