🕹️

PixiJSで簡単なゲームを作ってみた

2024/02/22に公開

これです:

まあ、かなりシンプルなゲームですが、PixiJSで何ができるかを感じることができるので、この記事を書きました。

そうだ、挨拶しないと! ¡Hola! こんにちは! テラーノベルのオスカルです。Webの開発をしています。いつも言うんだけど、日本語はまだまだ勉強中なので、応援してください!

PixiJSってなに?

ウェブサイトを確認するなら、こう言っています:「The HTML5 Creation Engine」、日本語では「HTML5創造エンジン」かな?。

👆あまり良い説明ではありません... 周りを調べてみると、これが私が見つけたものです:

  • HTMLキャンバス要素内でレンダリングされるインタラクティブなグラフィックを作成するためのライブラリです。
  • それ自体はゲームライブラリではありません。アセットのローディング、アニメーション、タイミング、およびブレンディングモードを処理します...
  • WebGLを使用します

ちょっと待って、WebGLってなに?

ブラウザー内でプラグインを必要とせずに、2D / 3D グラフィックスを直接レンダリングするための JavaScript API です。コンピューターの GPU へのアクセスを可能にするため、高性能なレンダリングを提供します。すでにすべての最新のブラウザでサポートされているし基本的には、コンピューターのGPUパワーを使用してキャンバス要素にレンダリングするため、非常に強力です。

PixiJSや他のライブラリはそれをサポートしています。しかし、バージョン8(まだベータ版です)では、PixiJSはWebGPUもサポートしています

なるほど、でもね。。。WebGPUってなに?

あ、はいはい、そうですね、ごめん、自分の言葉で説明させてください🙏

WebGLの後継であり、Direct3D 12AppleのMetal、またはVulkanなどの現代のGPU APIとの互換性を提供します。

キャンバスを使用する必要もなく、HTMLも必要ありません。GPU計算能力を直接利用するためのAPIを提供します。例えば、機械学習やそのような高コストの操作に使用されます。

しかし、その力とは対照的に、より高速な操作が可能ですが、まだ非常に新しいため、ブラウザの互換性はまだ開発中です。

(👆 結婚赤いですね。。。😢)

PixiJSの代替手段

もしより本格的なゲームを作成する予定であれば、ゲームプログラミングにはより適したPixiJSの代替手段があることに注意してください: ThreeJSPhaser, BabylonJS。。。

Phaserでいくつかのクイックプロトタイプを作成しようとしましたが、PixiJSに切り替えました。セットアップがはるかに簡単で、すぐに開発を開始できると感じたからです。しかし、Phaserはゲーム開発フレームワークですので、本格的なゲーム開発にはおそらくはるかに適していると考えています。

英語ですが、この記事ではPixiJSPhaserの両方を比較しています。興味があればぜひチェックしてください。

Ok、十分な紹介です!ゲームを作成しましょう!

ドキュメントを見ると、かなり簡単になるはずです... さて、確認してみましょう。Pixi-hotwireというリポジトリがあります。これはボイラープレートのはずです。それをクローンして、確認してみましょう。

クローンした後、package.jsonファイルを確認すると、かなりシンプルで、ほとんどWebpackTypeScript、そしてPixiJSの依存関係です:

[..]
  "dependencies": {
    "pixi.js": "^7.2.4"
  },
  "devDependencies": {
    "copy-webpack-plugin": "^11.0.0",
    "html-webpack-plugin": "^5.5.3",
    "npm-run-all": "^4.1.5",
    "rimraf": "^5.0.1",
    "terser-webpack-plugin": "^5.3.9",
    "ts-loader": "^9.4.4",
    "typescript": "^5.1.6",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
}

そして、ほとんど空のindex.ejsファイルがあります:

  <head>
    <title>Pixi Hotwire</title>
    <meta
      id="viewport" name="viewport"
      content="width=device-width, minimum-scale=1.0, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover"
    />
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100%;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <noscript>Please enable JavaScript</noscript>
  </body>
</html>

アクションは、index.tsファイルで始まります。そこでは、PixiからApplicationオブジェクトをインポートし、ゲームの一般的なプロパティでインスタンス化し、ドキュメントボディにアタッチします:

import { Application} from "pixi.js";

const app = new Application<HTMLCanvasElement>({
  resolution: window.devicePixelRatio || 1,
  autoDensity: true,
  backgroundColor: 0x6495ed,
  width: 640,
  height: 480,
});

document.body.appendChild(app.view);

こちらで、さまざまな初期化設定をすべて確認できます。

yarnした後yarn startを実行し、ブラウザでデフォルトの localhost:1234を開くと、指定された背景のキャンバス要素が表示されます。

1- 我々の愛すべきテノちゃんをメインキャラクターとして追加しましょう。

staticフォルダ内にtenochan.pngファイルを追加し、Spriteクラスを使用してキャラクターをキャンバスに追加する方法をかなりすぐに理解しました。

import { Application, Sprite } from "pixi.js";

[..]

document.body.appendChild(app.view);

// PNGファイルからスプライトを作成
const tenochan: Sprite = Sprite.from("tenochan.png");

// スプライトの中心を定義
tenochan.anchor.x = 0.5;
tenochan.anchor.y = 0.5;

// キャンバスの中央に配置
tenochan.x = app.screen.width / 2;
tenochan.y = app.screen.height / 2;

app.stage.addChild(tenochan);

anchor はキャラクターの中心を定義するために使用され、それはテノちゃんを配置、回転、または移動するための原点として使用されます。後、テノちゃんのx座標とy座標をcanvasサイズの中心に設定した後、spriteオブジェクトをアプリケーションのstageに追加します。StagePixiアプリケーションのメインビューであり、さまざまなシーンやオブジェクトが存在するルートです。

ブラウザをチェックすると:

2- アニメーションを追加する方法を見つけましょう!

これはRemotionを思い出させます。Tickerは、レンダリング前にフレームごとに関数を呼び出すオブジェクトです。したがって、自動的にテノちゃんを移動させるために次のようなものを追加できます:

app.ticker.add(() => {
  tenochan.rotation += 0.05;
});

ちょっとかわいそうだけど、こうなる:

では、Tickerに相対的に自動的に物体を移動させる方法を知っています。これは、自分自身で動く敵キャラクターには適していますが、テノちゃんを自分で動かしたい場合もあります。JavaScriptのevent listenerを使用すると、これも非常に簡単に実行できます:

const onKeyDown = (e: KeyboardEvent) => {
  if (e.key === "ArrowRight") tenochan.x += 10;
  if (e.key === "ArrowLeft") tenochan.x -= 10;
  if (e.key === "ArrowUp") tenochan.y -= 10;
  if (e.key === "ArrowDown") tenochan.y += 10;
};

document.addEventListener("keydown", onKeyDown);

今、カーソルキーでキャンバス上をテノちゃんを動かすことができます!

左に移動すると少し奇妙に見えるので、そのように見えるようにしました。テノちゃんの反転バージョンを用意しました:

Pixiでこれを行う方法は、メインキャラクター用の1つのSpriteを定義することですが、左向きと右向きの2つのTextureも用意することです。

const left = Texture.from("tenochan left.png"); // 左
const right = Texture.from("tenochan.png"); // 右

const tenochan = new Sprite(right);

そして、左または右のカーソルキーが押され、Spriteが移動するときにTextureを変更します:

const onKeyDown = (e: KeyboardEvent) => {
  if (e.key === "ArrowRight") {
    tenochan.x += 10;
    tenochan.texture = right;
  }
  if (e.key === "ArrowLeft") {
    tenochan.x -= 10;
    tenochan.texture = left;
  }
  if (e.key === "ArrowUp") tenochan.y -= 10;
  if (e.key === "ArrowDown") tenochan.y += 10;
};

今、もう少し自然に見えます!

じゃ、テノちゃん運動いっぱいしてるのでお腹すいただろう?動いてる魚を入れましょう!

fish.png

とりあえず、空から落ちてきて、x軸上のランダムな位置に現れるようにしましょう:

// 新しいスプライトを宣言し、キャンバスの上部に配置し、ランダムな水平位置に配置します。
const food: Sprite = Sprite.from("fish.png");
food.x = Math.random() * app.screen.width;
food.y = 0;
app.stage.addChild(food);

// メインループにおいて、y座標にランダムな量を加えて落下しているように見えるようにし、
// 魚がキャンバスの底に到達したかどうかを検出します。そうであれば、それを上部に移動させて
// 位置を再設定します。
app.ticker.add(() => {
  food.y += Math.random() * 10;
  if (food.y > app.screen.height) {
    food.y = 0;
    food.x = Math.random() * app.screen.width;
  }
});

魚が降ってきています!

3- 衝突を検出

さて、お察しの通り、ゲームの目標はすでに定義されています: テノちゃんでできるだけ多くの魚を捕まえることです。

これを行うためには、テノちゃんが魚に触れたかどうかを知る方法を見つける必要があります。 この関数を使って、両方のスプライトの境界をチェックできます:

const isColliding = (a: Sprite, b: Sprite) => {
   const ab = a.getBounds();
   const bb = b.getBounds();
   return (
     ab.x + ab.width > bb.x &&
     ab.x < bb.x + bb.width &&
     ab.y + ab.height > bb.y &&
     ab.y < bb.y + bb.height
   );
 };

コードは追いやすく、1つのスプライトを囲む四角形の一部が2番目のスプライトを囲む四角形の一部と同じ座標にある場合、ヒットが発生します。

そして、これが起こったときに何をするかを決める必要があります。この場合、私はテノちゃんを少し太らせ、魚の位置をリセットして、それが食べられたように見えるようにし、新しい魚が空から落ちてくるようにしました。

これを行うには、メインループにisCollidingチェックを追加し、trueの場合、テノちゃんの幅に10を追加し、魚の位置をリセットします。


app.ticker.add(() => {
  food.y += Math.random() * 10;
  if (food.y > app.screen.height) {
    food.y = 0;
    food.x = Math.random() * app.screen.width;
  }
  if (isColliding(tenochan, food)) {
    // テノちゃんを太らせる 😂
    tenochan.width += 10;

    // 魚の位置をリセット
    food.y = 0;
    food.x = Math.random() * app.screen.width;
  }
});

遊びましょう!

爆発しそう 😂

4- もう少し詳細な例

いくつかの改善を加えて、このシンプルなゲームが少し良く見えるようにしました。最初に行ったことは、より自然な魚の動きを試みることでした。それは、魚がランダムな間隔と速度で左右に動くことを可能にすることで行いました:

[..]
// 左右バージョンの魚
const leftFish = Texture.from("fish.png");
const rightFish = Texture.from("fish right.png");
const food: Sprite = Sprite.from(leftFish);

// 2つの間のランダムな数値を生成する関数
const randomIntFromInterval = (min: number, max: number): number => {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

// 現在の魚の移動方向と速度、およびそれらを変更するための関数
let directionX = "left";
let directionY = "down";
let speed = randomIntFromInterval(1, 10);

const changeXDirection = () => {
  if (directionX === "left") directionX = "right";
  else directionX = "left";
};

const changeYDirection = () => {
  if (directionY === "down") directionY = "up";
  else directionY = "down";
};

const changeSpeed = () => {
  speed = randomIntFromInterval(1, 5);
};

// 魚をランダムに移動させるためのメイン関数。
// ランダムなタイミングで更新されるため、時々魚が停止することもあり、
// これは良いことです。
const randomlyMoveFish = () => {
  if (directionX === "left") {
    food.x -= Math.random() * speed;
    food.texture = leftFish;
  } else {
    food.x += Math.random() * speed;
    food.texture = rightFish;
  }
  if (directionY === "down") food.y += Math.random() * speed;
  else food.y -= Math.random() * speed;

  // 方向や速度をたまにしか変更しない
  if (randomIntFromInterval(1, 500) > 495) changeXDirection();
  if (randomIntFromInterval(1, 500) > 495) changeYDirection();
  if (randomIntFromInterval(1, 500) > 495) changeSpeed();
};

// キャンバス内に魚があることを確認する関数
const ensureFishInBounds = () => {
  if (food.y > app.screen.height - 40) food.y = app.screen.height - 40;
  if (food.y < 40) food.y = 40;
  if (food.x > app.screen.width - 100) food.x = app.screen.width - 100;
  if (food.x < 60) food.x = 60;
};

魚の動きはこうなります:

次に行ったことは、テノちゃんが魚を捕まえると、彼のサイズを増やし、幅だけでなく、次に現れる新しい魚のサイズをランダムに変更しました。

const onTenochanFeeding = () => {
  food.x = Math.random() * app.screen.width;
  food.y = 0;
  food.width = randomIntFromInterval(20, 80);
  food.height = food.width;
  tenochan.width += 10;
  tenochan.height += 10;
};

テノちゃんのサイズが十分に大きい場合は、メインループを停止し、魚を見えなくし、テノちゃんのサイズを大きくします。

const onGameOver = () => {
  app.ticker.stop();
  tenochan.width = 500;
  tenochan.height = 500;
  tenochan.x = app.screen.width / 2;
  tenochan.y = app.screen.height / 2;
  food.visible = false;
};

const onTenochanFeeding = () => {
[..]
 if (tenochan.width > 200) {
    onGameOver();
  }
}

そして、メインループは次のようになります:

app.ticker.add(() => {
  if (isColliding(tenochan, food)) {
    onTenochanFeeding();
  } else {
    randomlyMoveFish();
    ensureFishInBounds();
  }
});

以上です! こんな感じになりました:

ぜひ、こちから遊んでみてください!ああ、ブラウザのスクロールをトリガーするカーソルキーの代わりに、「wsad」を使用して、てのちゃんを移動してください。

これはかなりシンプルなゲームですが、PixiJSを使ってこの種の開発がどれほど簡単で迅速に行えるかを感じていただければ幸いです。

Clone the code from Github!!

あ!コードはここにあります、是非cloneして遊んでください!

関連するリソース

PixiJS UIという、直接使用できるPixiJS用のコンポーネントライブラリがあります。

また、PixiJSのドキュメントはかなり良いですね!

Examples
PixiJS tutorial
PixiJS Elementals

React向けのライブラリバージョンもあります。

こちらで、PixiJSで開発されたいくつかの本格的なゲームを見つけることができます。

Bubbo bubbo
Top games made with PixiJS

終わりに

PixiJSをすぐに使えることに大いに驚きました。また、これらの点も気に入りました:

  • TypeScriptのサポートがあります!
  • コードはかなりきれいで理解しやすく、例もすぐに理解して適用できます。
  • オープンソースです、これは本当に素晴らしいです。
  • すぐに動作するものを持つのは簡単です。

でも、より本格的なゲームを作るための最良の選択肢ではないかもしれません。

ま、僕楽しかった! 🤭

もしゲームを作ってみたらコメントで教えてください!🙏

テラーノベル テックブログ

Discussion