🎮

PixiJSを使って簡単なゲームを作る

2024/12/10に公開

はじめに

こんにちは!今回はウェブゲーム開発の経験を元に、みなさんにPixiJSを使って簡単にH5ゲームを作る方法をご紹介します!

PixiJS

PixiJSは、ブラウザ上で軽量かつ高速な2Dレンダリングを実現するためのJavaScriptライブラリです。HTML5の<canvas>を使って、ゲームやインタラクティブなアプリケーションを手軽に構築することができます。今回はこれを使ってvampire survivors風ゲームを作ってみましょう。

準備

今回はシンプルにindex.htmlとmain.js、そしてPixiJSだけ用意します。
PixiJSはv7.4.2 CDN形式で使います。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Vampire Survivors-like Game</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.4.2/pixi.min.js"></script>
</head>
<body>
    <canvas id="game-canvas"></canvas>
    <script src="src/main.js"></script>
</body>
</html>
main.js
// ここにコードを作成していきます。

PixiJSの初期化

まずは、pixijsを初期化します。

main.js
const app = new PIXI.Application({
    view: document.getElementById('game-canvas'),   // canvas要素を指定
    width: 800,                                     // 横サイズ
    height: 600,                                    // 縦サイズ
    backgroundColor: 0x000000                       // 背景色
});

用意したcanvasを指定して、ゲームのサイズと背景色を設定します。

ゲームステージができました

プレイヤーを作成

プレイヤーのキャラクターを作ります。今回は画像なしでpixiの描画機能を使います。

main.js
const player = new PIXI.Graphics(); // pixi.jsの描画オブジェクト
player.beginFill(0xffffff);         // 白色で塗り開始
player.drawCircle(0, 0, 10);        // 中心(0, 0)に半径10の円を描画
player.endFill();                   // 塗り終了
player.x = app.view.width / 2;      // x座標を画面中央に
player.y = app.view.height / 2;     // y座標を画面中央に
app.stage.addChild(player);         // ステージに追加


プレイヤーができました

キャラクターを動かせるように

キーボードの矢印キーとWASDキーでキャラクターを移動させます。

main.js
const playerSpeed = 3;    // プレイヤーの移動速度
const keys = { up: false, down: false, left: false, right: false };
window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
    if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
});
window.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
    if (e.key === 'ArrowDown' || e.key === 's') keys.down = false;
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
});

// pixijsのアニメーションループに登録
app.ticker.add(() => {
    // プレイヤー移動
    if (keys.up) player.y -= playerSpeed;
    if (keys.down) player.y += playerSpeed;
    if (keys.left) player.x -= playerSpeed;
    if (keys.right) player.x += playerSpeed;
});


キーボードの入力でキャラクターが移動します

フレームレートに依存しないように

既存のコードではユーザーのモニターのフレームレートによりキャラクターの移動速度が異なります。
これはpixijsのtickerが1フレームごとに更新されるためです。なので、1フレームごとの移動量ではなく1秒ごとの移動量に変更します。

main.js
+ const playerSpeed = 180; // プレイヤー速度1秒あたり180ピクセル移動
- const playerSpeed = 3;    // プレイヤーの移動速度

// pixijsのアニメーションループに登録
app.ticker.add(() => {
+    const deltaMS = app.ticker.deltaMS; // 前フレームからの経過ミリ秒
+    const deltaSec = deltaMS / 1000;    // 秒換算

    // プレイヤー移動
+    let moveX = 0;
+    let moveY = 0;
+    if (keys.up) moveY -= playerSpeed * deltaSec;
+    if (keys.down) moveY += playerSpeed * deltaSec;
+    if (keys.left) moveX -= playerSpeed * deltaSec;
+    if (keys.right) moveX += playerSpeed * deltaSec;
-    if (keys.up) player.y -= playerSpeed;
-    if (keys.down) player.y += playerSpeed;
-    if (keys.left) player.x -= playerSpeed;
-    if (keys.right) player.x += playerSpeed;
+    player.x += moveX;
+    player.y += moveY;
});

敵を作る

画面の端付近からランダムに出現させます。そしてプレイヤー方向に移動するようにします。

main.js
const enemies = [];         // 敵の配列
let spawnTimer = 0;         // 敵出現の経過時間
const spawnInterval = 1000; // 1秒(1000ms)ごとに敵出現
const enemySpeed = 60;      // 敵の移動速度1秒あたり60ピクセル移動

app.ticker.add(() => {
    ...省略
    // 敵出現の時間更新
    spawnTimer += deltaMS;
    if (spawnTimer > spawnInterval) {
        spawnTimer = 0;

        const enemy = new PIXI.Graphics();  // 敵オブジェクト
        enemy.beginFill(0xff0000);  // 赤色で塗り開始
        enemy.drawCircle(0, 0, 8);  // 中心(0, 0)に半径8の円を描画
        enemy.endFill();

        // 画面外もしくは端付近からランダム出現
        const side = Math.floor(Math.random() * 4);
        if (side === 0) {
            // 画面上端から出現
            enemy.x = Math.random() * app.view.width;
            enemy.y = -10;
        } else if (side === 1) {
            // 画面右端から出現
            enemy.x = app.view.width + 10;
            enemy.y = Math.random() * app.view.height;
        } else if (side === 2) {
            // 画面下端から出現
            enemy.x = Math.random() * app.view.width;
            enemy.y = app.view.height + 10;
        } else {
            // 画面左端から出現
            enemy.x = -10;
            enemy.y = Math.random() * app.view.height;
        }

        app.stage.addChild(enemy);
        enemies.push(enemy);
    }

    // 敵移動
    enemies.forEach((enemy) => {
        const dx = player.x - enemy.x;
        const dy = player.y - enemy.y;
        const dist = Math.sqrt(dx*dx + dy*dy);
        const ux = dx / dist;
        const uy = dy / dist;

        if (dist > 0) {
            const vx = ux * enemySpeed * deltaSec;
            const vy = uy * enemySpeed * deltaSec;
            enemy.x += vx;
            enemy.y += vy;
        }
    });
});


敵が出現し、プレイヤーに近づいてきます

弾丸を発射させる

プレイヤーが敵を倒すための弾丸を発射させます。弾丸は敵オブジェクトと衝突したら敵オブジェクトを破壊し、消えます。

main.js
const bullets = [];         // 弾丸の配列
let bulletTimer = 0;        // 弾発射の経過時間
const bulletInterval = 500; // ミリ秒(0.5秒)ごとに弾発射
const bulletSpeed = 300;    // 弾丸の移動速度1秒あたり300ピクセル移動

app.ticker.add(() => {
    ...省略
    // 弾発射
    bulletTimer += deltaMS;
    if (bulletTimer > bulletInterval) {
        bulletTimer = 0;

        const bullet = new PIXI.Graphics();
        bullet.beginFill(0x00ff00); // 緑色で塗り開始
        bullet.drawCircle(0, 0, 4); // 中心(0, 0)に半径4の円を描画
        bullet.endFill();
        bullet.x = player.x;
        bullet.y = player.y;
        app.stage.addChild(bullet);
        bullets.push(bullet);
    }

    // 弾移動
    bullets.forEach((b) => {
        b.y -= bulletSpeed * deltaSec;
    });

    // 衝突判定(弾 vs 敵)
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        for (let j = bullets.length - 1; j >= 0; j--) {
            const bullet = bullets[j];
            const dx = enemy.x - bullet.x;
            const dy = enemy.y - bullet.y;
            const dist = Math.sqrt(dx*dx + dy*dy);
            // 弾と敵の距離が8(弾半径4 + 敵半径8)未満なら衝突
            if (dist < (8 + 4)) {
                app.stage.removeChild(enemy);   // 敵をステージから削除
                enemies.splice(i, 1);           // 配列から削除

                app.stage.removeChild(bullet);  // 弾をステージから削除
                bullets.splice(j, 1);           // 配列から削除
                break;
            }
        }
    }
});


弾丸が上に発射され、敵とぶつかったら敵が消えます

マウスの位置に弾丸を発射するように

main.js
// ターゲット位置(マウスで指定された位置)
// 初期値は画面中央上部
+ let targetX = app.view.width / 2;
+ let targetY = 0;

// マウスが動くたびにターゲット位置を更新
+ app.view.addEventListener('pointermove', (e) => {
+    const rect = app.view.getBoundingClientRect();  // canvasの位置とサイズを取得
+    const mouseX = e.clientX - rect.left;           // canvas内のマウスのx座標
+    const mouseY = e.clientY - rect.top;            // canvas内のマウスのy座標
+    targetX = mouseX;
+    targetY = mouseY;
+ });

app.ticker.add(() => {
    ...中略
    // ターゲット位置に向かって弾を連射する
    // 一定間隔で弾を生成
    bulletTimer += deltaMS;
    if (bulletTimer > bulletInterval) {
        bulletTimer = 0;

        // プレイヤー位置(player.x, player.y)から、ターゲット位置(targetX, targetY)へのベクトルを算出
+        const dx = targetX - player.x;
+        const dy = targetY - player.y;
+        const dist = Math.sqrt(dx*dx + dy*dy);

        // ターゲット位置への単位ベクトル
+        const ux = dx / dist;
+        const uy = dy / dist;

        // 弾の速度ベクトル
+        const vx = ux * bulletSpeed;
+        const vy = uy * bulletSpeed;

        // 弾を生成
        const bullet = new PIXI.Graphics();
        bullet.beginFill(0x00ff00);
        bullet.drawCircle(0, 0, 4);
        bullet.endFill();
        bullet.x = player.x;
        bullet.y = player.y;
+        bullet.vx = vx;
+        bullet.vy = vy;
        app.stage.addChild(bullet);
        bullets.push(bullet);
    }

    // 弾移動
    bullets.forEach((b) => {
        // 1秒あたりの移動距離
+        const moveX = (b.vx * deltaMS) / 1000;  
+        const moveY = (b.vy * deltaMS) / 1000;
+        b.x += moveX;
+        b.y += moveY;
    });
    ...中略
});


マウスカーソルに向かって弾丸が発射されます

スコアを追加

ゲームで一番大事なスコア表示を追加してみましょう。

main.js
+ let score = 0; // スコア
+ const scoreText = new PIXI.Text('Score: 0', { fill: 0xffff00 });    // スコア表示用テキストオブジェクト、黄色
+ scoreText.x = 10;   
+ scoreText.y = 10;
+ app.stage.addChild(scoreText);

...中略
    // 衝突判定(弾 vs 敵)
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        for (let j = bullets.length - 1; j >= 0; j--) {
            const bullet = bullets[j];
            const dx = enemy.x - bullet.x;
            const dy = enemy.y - bullet.y;
            const dist = Math.sqrt(dx*dx + dy*dy);
            // 弾と敵の距離が8(弾半径4 + 敵半径8)未満なら衝突
            if (dist < (8 + 4)) {
+                score += 1;                         // スコア加算
+                scoreText.text = `Score: ${score}`; // スコア表示更新
                app.stage.removeChild(enemy);   // 敵をステージから削除
                enemies.splice(i, 1);           // 配列から削除

                app.stage.removeChild(bullet);  // 弾をステージから削除
                bullets.splice(j, 1);           // 配列から削除
                break;
            }
        }
    }
...中略


左上にスコアが表示され、敵を倒すたびに加算されていきます

全体のコード

main.jsの全体コードです。

main.js
const app = new PIXI.Application({
    view: document.getElementById('game-canvas'),   // canvas要素を指定
    width: 800,                                     // 横サイズ
    height: 600,                                    // 縦サイズ
    backgroundColor: 0x000000                       // 背景色
});

const playerSpeed = 180; // プレイヤー速度1秒あたり180ピクセル移動
const player = new PIXI.Graphics(); // pixi.jsの描画オブジェクト
player.beginFill(0xffffff);         // 白色で塗り開始
player.drawCircle(0, 0, 10);        // 中心(0, 0)に半径10の円を描画
player.endFill();                   // 塗り終了
player.x = app.view.width / 2;      // x座標を画面中央に
player.y = app.view.height / 2;     // y座標を画面中央に
app.stage.addChild(player);         // ステージに追加

const enemies = [];         // 敵の配列
let spawnTimer = 0;         // 敵出現の経過時間
const spawnInterval = 1000; // ミリ秒(1秒)ごとに敵出現
const enemySpeed = 60;      // 敵の移動速度1秒あたり60ピクセル移動

const bullets = [];         // 弾丸の配列
let bulletTimer = 0;        // 弾発射の経過時間
const bulletInterval = 500; // ミリ秒(0.5秒)ごとに弾発射
const bulletSpeed = 300;    // 弾丸の移動速度1秒あたり300ピクセル移動

// ターゲット位置(マウスで指定された位置)
// 初期値は画面中央上部
let targetX = app.view.width / 2;
let targetY = 0;

let score = 0;                                                      // スコア
const scoreText = new PIXI.Text('Score: 0', { fill: 0xffff00 });    // スコア表示用テキストオブジェクト、黄色
scoreText.x = 10;   
scoreText.y = 10;
app.stage.addChild(scoreText);

const keys = { up: false, down: false, left: false, right: false };
window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
    if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
});
window.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
    if (e.key === 'ArrowDown' || e.key === 's') keys.down = false;
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
});

// マウスが動くたびにターゲット位置を更新
app.view.addEventListener('pointermove', (e) => {
    const rect = app.view.getBoundingClientRect();  // canvasの位置とサイズを取得
    const mouseX = e.clientX - rect.left;           // canvas内のマウスのx座標
    const mouseY = e.clientY - rect.top;            // canvas内のマウスのy座標
    targetX = mouseX;
    targetY = mouseY;
});

app.ticker.add(() => {
    const deltaMS = app.ticker.deltaMS; // 前フレームからの経過ミリ秒
    const deltaSec = deltaMS / 1000;    // 秒換算

    // プレイヤー移動(秒基準)
    let moveX = 0;
    let moveY = 0;
    if (keys.up) moveY -= playerSpeed * deltaSec;
    if (keys.down) moveY += playerSpeed * deltaSec;
    if (keys.left) moveX -= playerSpeed * deltaSec;
    if (keys.right) moveX += playerSpeed * deltaSec;
    player.x += moveX;
    player.y += moveY;

    // 敵出現の時間更新
    spawnTimer += deltaMS;
    if (spawnTimer > spawnInterval) {
        spawnTimer = 0;

        const enemy = new PIXI.Graphics();
        enemy.beginFill(0xff0000);  // 赤色で塗り開始
        enemy.drawCircle(0, 0, 8);  // 中心(0, 0)に半径8の円を描画
        enemy.endFill();

        // 画面外もしくは端付近からランダム出現
        const side = Math.floor(Math.random() * 4);
        if (side === 0) {
            // 画面上端から出現
            enemy.x = Math.random() * app.view.width;
            enemy.y = -10;
        } else if (side === 1) {
            // 画面右端から出現
            enemy.x = app.view.width + 10;
            enemy.y = Math.random() * app.view.height;
        } else if (side === 2) {
            // 画面下端から出現
            enemy.x = Math.random() * app.view.width;
            enemy.y = app.view.height + 10;
        } else {
            // 画面左端から出現
            enemy.x = -10;
            enemy.y = Math.random() * app.view.height;
        }

        app.stage.addChild(enemy);
        enemies.push(enemy);
    }

    // 敵移動
    enemies.forEach((enemy) => {
        const dx = player.x - enemy.x;
        const dy = player.y - enemy.y;
        const dist = Math.sqrt(dx*dx + dy*dy);
        const ux = dx / dist;
        const uy = dy / dist;

        if (dist > 0) {
            const vx = ux * enemySpeed * deltaSec;
            const vy = uy * enemySpeed * deltaSec;
            enemy.x += vx;
            enemy.y += vy;
        }
    });

    // ターゲット位置に向かって弾を連射する
    // 一定間隔で弾を生成
    bulletTimer += deltaMS;
    if (bulletTimer > bulletInterval) {
        bulletTimer = 0;

        // プレイヤー位置(player.x, player.y)から、ターゲット位置(targetX, targetY)へのベクトルを算出
        const dx = targetX - player.x;
        const dy = targetY - player.y;
        const dist = Math.sqrt(dx*dx + dy*dy);

        // ターゲット位置への単位ベクトル
        const ux = dx / dist;
        const uy = dy / dist;

        // 弾の速度ベクトル
        const vx = ux * bulletSpeed;
        const vy = uy * bulletSpeed;

        // 弾を生成
        const bullet = new PIXI.Graphics();
        bullet.beginFill(0x00ff00);
        bullet.drawCircle(0, 0, 4);
        bullet.endFill();
        bullet.x = player.x;
        bullet.y = player.y;
        bullet.vx = vx;
        bullet.vy = vy;
        app.stage.addChild(bullet);
        bullets.push(bullet);
    }

    // 弾移動
    bullets.forEach((b) => {
        // 1秒あたりの移動距離
        const moveX = (b.vx * deltaMS) / 1000;  
        const moveY = (b.vy * deltaMS) / 1000;
        b.x += moveX;
        b.y += moveY;
    });

    // 衝突判定(弾 vs 敵)
    for (let i = enemies.length - 1; i >= 0; i--) {
        const enemy = enemies[i];
        for (let j = bullets.length - 1; j >= 0; j--) {
            const bullet = bullets[j];
            const dx = enemy.x - bullet.x;
            const dy = enemy.y - bullet.y;
            const dist = Math.sqrt(dx*dx + dy*dy);
            // 弾と敵の距離が8(弾半径4 + 敵半径8)未満なら衝突
            if (dist < (8 + 4)) {
                score += 1;                         // スコア加算
                scoreText.text = `Score: ${score}`; // スコア表示更新
                app.stage.removeChild(enemy);   // 敵をステージから削除
                enemies.splice(i, 1);           // 配列から削除

                app.stage.removeChild(bullet);  // 弾をステージから削除
                bullets.splice(j, 1);           // 配列から削除
                break;
            }
        }
    }
});

最後に

今回はPixiJSを使ってゲームを作ってみました。簡単なゲームですが、誰でも作れるぐらい難しくないので、皆さんもぜひ1回作ってみましょう!

GMOメディアテックブログ

Discussion