All Articles

JS シューティングゲーム

JSでシューティングゲームを作ったのでメモ。

bf108b83f7f20c18069939c0ec8964bd

こちらのサイトを参考にして作りました。ほとんど、こちらのサイトのコードですが、今のところ一箇所のみ(敵キャラの動きを一つ追加した)自分で追加しました。今後、さらに改造を加える予定です。

f39b116eef40ff6b3405418c19b8e1db

とあるので、この記事に自分が手打ちしたコードを置いておきます。手打ちしたので、コメントやコードの位置を自分で分かるように編集しています。

参考サイトに細かい解説はあるので、この記事では解説しません。自分と同じように参考サイトを見て、作ってみたい方は途中から変数の宣言の説明の省略等があるので、注意してください。自分はそこでハマりました。

このプログラムを通していろいろ学びました。

例えばこれ。

switch(true){
    case i = 0:
    break;
    case j = 0:
    break;
}

switchを最初からtrueにし、caseで条件分けしている。こうする事でより柔軟に場合分けできます。

他には、今まである変数がtrueかfalseで場合分けするとき以下のように書いていました。

if(whichType === true){
    //処理
}else{
    //処理
}

しかし先ほどのswitchのコードと似ていますが、このようにする事で簡略化できます。

if(whichType){
    //処理
}else{
    //処理
}

今後はこのように書くように統一したいと思います。

プログラムを書いている中で、自分の数学の知識が足りていないことを痛感させられました。例えば、アイキャッチ画像(最初の画像)のBossの動きです。これは理解出来ていないので、sin cos の復習を今後したいと思います。↓

以下コードです。

index.html

<!DOCTYPE html>
<html>
<head>
<scriptsrc=”common.js”></script>
<scriptsrc=”main.js”></script>
<scriptsrc=”character.js”></script>
<scriptsrc=”boss.js”></script>
<style>
canvas {
border: 1pxsolidgray;
}
</style>
</head>
<body>
<canvasid=”screen”></canvas>
<pid=”info”></p>
</body>
</html>
**main.js**
//global--------------------------------------------------
var screenCanvas, info;
var run = true;
var fps = 1000 / 30;
var mouse = new Point;
var ctx; //canvas2d コンテキスト格納用
var fire = false;
var counter = 0;
var score = 0;
var message = '';

// const--------------------------------------------------
var CHARA_COLOR = 'rgba(0, 0, 255, 0.75)';
var CHARA_SHOT_COLOR = 'rgba(0, 255, 0, 0.75)';
var CHARA_SHOT_MAX_COUNT = 10;
var ENEMY_COLOR = 'rgba(255, 0, 0, 0.75)';
var ENEMY_MAX_COUNT = 10;
var ENEMY_SHOT_COLOR = 'rgba(255, 0, 255, 0.75)';
var ENEMY_SHOT_MAX_COUNT = 100;
var BOSS_COLOR = 'rgba(128, 128, 128, 0.75)';
var BOSS_BIT_COLOR = 'rgba(64, 64, 64, 0.75)';
var BOSS_BIT_COUNT = 5;

//main----------------------------------------------------
window.onload = function () {
    //何にかの変数
    var i, j;
    var p = new Point();

    //スクリーンの初期化
    screenCanvas = document.getElementById('screen');
    screenCanvas.width = 256;
    screenCanvas.height = 256;

    //自機の位置(マウスの位置)を真ん中あたりに設定
    mouse.x = screenCanvas.width / 2;
    mouse.y = screenCanvas.height - 20;

    //2dコンテキスト
    ctx = screenCanvas.getContext('2d');

    //イベントの登録
    screenCanvas.addEventListener('mousemove', mouseMove, true);
    screenCanvas.addEventListener('mousedown', mouseDown, true);
    window.addEventListener('keydown', keyDown, true);

    //その他のエレメント関連
    info = document.getElementById('info');

    //自動初期化
    var chara = new Character();
    chara.init(10);

    var charaShot = new Array(CHARA_SHOT_MAX_COUNT);
    for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
        charaShot[i] = new CharacterShot()
    }

    var enemy = new Array(ENEMY_MAX_COUNT);
    for (i = 0; i < ENEMY_MAX_COUNT; i++) {
        enemy[i] = new Enemy();
    }

    var enemyShot = new Array(ENEMY_SHOT_MAX_COUNT);
    for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
        enemyShot[i] = new EnemyShot();
    }

    var boss = new Boss();

    var bit = new Array(BOSS_BIT_COUNT);
    for (i = 0; i < BOSS_BIT_COUNT; i++) {
        bit[i] = new Bit();
    }

    //レンダリング処理を呼び出す
    (function () {
        //カウンター追加
        counter++;

        //HTMLを更新
        info.innerHTML = mouse.x + ':' + mouse.y;

        //screenをクリア
        ctx.clearRect(0, 0, screenCanvas.width, screenCanvas.height);

        //自機------------------------------

        //パスの設定を開始
        ctx.beginPath();

        //自機の位置を設定
        chara.position.x = mouse.x;
        chara.position.y = mouse.y;

        //自機を描くパスを設定
        ctx.arc(chara.position.x, chara.position.y, chara.size, 0, Math.PI * 2, false);

        //自機の色を設定する
        ctx.fillStyle = CHARA_COLOR;

        //自機を描く
        ctx.fill();

        //fireフラグの値により分岐
        if (fire) {
            //全ての自機ショットを調査する
            for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
                //自機ショットが既に発射されているかチェック
                if (!charaShot[i].alive) {
                    //自機ショットを新規にセット
                    charaShot[i].set(chara.position, 3, 5);

                    //ループを抜ける
                    break;
                }
            }
            //フラグを降ろしておく
            fire = false;
        }

        //自機の動きとショット------------------------------------------

        //パスの設定を開始
        ctx.beginPath();

        //全ての自機ショットを調査する
        for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
            //自機ショットが既に発射されているかチェック
            if (charaShot[i].alive) {
                //自機ショットを動かす
                charaShot[i].move();

                //自機ショットを描くパスを設定
                ctx.arc(
                    charaShot[i].position.x,
                    charaShot[i].position.y,
                    charaShot[i].size,
                    0, Math.PI * 2, false

                );

                //パスをいったん閉じる
                ctx.closePath();
            }
        }

        //自機ショットの色を設定する
        ctx.fillStyle = CHARA_SHOT_COLOR;

        //自機ショットを描く
        ctx.fill();

        //カウンターの値によってシーンを分岐
        switch (true) {
            //カウンターが70より小さい
            case counter < 70:
                message = 'READY...';
                break;

            //カウンターが100より小さい
            case counter < 100:
                message = 'GO!!';
                break;

            //カウンターが100以上
            default:
                message = '';

                //以下ゲーム処理
                //エネミーの出現管理-----------------------------------

                //100フレームに一度出現させる
                if (counter % 100 === 0 && counter < 1000) {
                    //全てのエネミーを調査する
                    for (i = 0; i < ENEMY_MAX_COUNT; i++) {
                        //エネミーの生存フラグをチェック
                        if (!enemy[i].alive) {

                            //サイズを決定
                            var enemySize = 15;

                            if (counter < 500) {
                                //タイプを決定するパラメータを算出
                                j = (counter % 200) / 100;

                                //タイプに応じて初期位置を決める(type 0 or 1)                             
                                p.x = -enemySize + (screenCanvas.width + enemySize * 2) * j
                                p.y = screenCanvas.height / 2;

                                //エネミーを新規にセット
                                enemy[i].set(p, enemySize, j);
                            } else {
                                //タイプ2の初期位置を決める
                                p.x = screenCanvas.width / 2;
                                p.y = -enemySize * 2;
                                //タイプ3エネミーを新規にセット
                                enemy[i].set(p, enemySize, 2);
                            }
                            //一体出現させたのでループを抜ける
                            break;
                        }
                    }
                }else if (counter === 1000) {
                    //1000フレーム目にボスを出現させる
                    p.x = screenCanvas.width / 2;
                    p.y = -80;
                    boss.set(p, 50, 30);

                    //同時にビットも出現させる
                    for (i = 0; i < BOSS_BIT_COUNT; i++) {
                        j = 360 / BOSS_BIT_COUNT;
                        bit[i].set(boss, 15, 5, i * j);
                    }
                }

                //エネミー動きとショット(描画はなし)---------------------------------

                //パスの設定を開始
                ctx.beginPath();

                //全てのエネミーを調査する
                for (i = 0; i < ENEMY_MAX_COUNT; i++) {
                    //エネミーが既に発射されているかチェック
                    if (enemy[i].alive) {
                        //エネミーを動かす
                        enemy[i].move();

                        //エネミーを描くパスを設定
                        ctx.arc(
                            enemy[i].position.x,
                            enemy[i].position.y,
                            enemy[i].size,
                            0, Math.PI * 2, false

                        );

                        //ショットを打つかどうかパラメータの値からチェック
                        if (enemy[i].param % 30 === 0) {
                            //エネミーショットを調査する
                            for (j = 0; j < ENEMY_SHOT_MAX_COUNT; j++) {
                                if (!enemyShot[j].alive) {
                                    //エネミーショットを新規にセットする
                                    p = enemy[i].position.distance(chara.position);
                                    p.normalize();
                                    enemyShot[j].set(enemy[i].position, p, 5, 5);

                                    //1個出現させたのでループを抜ける
                                    break;
                                }
                            }
                        }

                        //パスをいったん閉じる
                        ctx.closePath();
                    }
                }

                //エネミーの色を設定する
                ctx.fillStyle = ENEMY_COLOR;

                //エネミーを描く
                ctx.fill();

                //エネミーショットの描画-----------------------------------

                //パスの設定
                ctx.beginPath();

                // 全てのエネミーショットを調査
                for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
                    // エネミーショットがあるか確認
                    if (enemyShot[i].alive) {
                        // エネミーショットを動かす
                        enemyShot[i].move();

                        //ショットの設定
                        ctx.arc(
                            enemyShot[i].position.x,
                            enemyShot[i].position.y,
                            enemyShot[i].size,
                            0, Math.PI * 2, false
                        );

                        // 一旦パスを閉じる
                        ctx.closePath();
                    }
                }

                //ショットの色を指定
                ctx.fillStyle = ENEMY_SHOT_COLOR;

                //描画
                ctx.fill();

                //ボス----------------------------------------------
                //パスの設定を開始
                ctx.beginPath();

                //ボスの出現フラグをチェック
                if (boss.alive) {
                    //ボスを動かす
                    boss.move();

                    //ボスを描くパスを設定
                    ctx.arc(
                        boss.position.x,
                        boss.position.y,
                        boss.size,
                        0, Math.PI * 2, false
                    );

                    //パスを一旦閉じる
                    ctx.closePath();
                }

                //ボスの色を設定する
                ctx.fillStyle = BOSS_COLOR;

                //ボスを描く
                ctx.fill();

                //ビット--------------------------------------------
                //パスの設定を開始
                ctx.beginPath();

                //全てのビットを調査する
                for (i = 0; i < BOSS_BIT_COUNT; i++) {
                    //ビットの出現フラグをチェック
                    if (bit[i].alive) {
                        //ビットを動かす
                        bit[i].move();

                        //ビットを描くパスを設定
                        ctx.arc(
                            bit[i].position.x,
                            bit[i].position.y,
                            bit[i].size,
                            0, Math.PI * 2, false
                        );

                        //ショットを打つかどうかパラメータをチェック
                        if (bit[i].param % 25 === 0) {
                            //エネミーショットを調査する
                            for (j = 0; j < ENEMY_SHOT_MAX_COUNT; j++) {
                                if (!enemyShot[j].alive) {
                                    //エネミーショットを新規にセットする
                                    p = bit[i].position.distance(chara.position);
                                    p.normalize();
                                    enemyShot[j].set(bit[i].position, p, 4, 1.5);

                                    //一個出現させたのでループを抜ける
                                    break;
                                }
                            }
                        }

                        //パスを一旦閉じる
                        ctx.closePath();
                    }
                }

                //ビットの色を設定する
                ctx.fillStyle = BOSS_BIT_COLOR;

                //ビットを描く
                ctx.fill();

                //衝突判定------------------------------------------
                //エネミーと自機ショットの衝突判定
                //全ての自機ショットを調査する
                for (i = 0; i < CHARA_SHOT_MAX_COUNT; i++) {
                    //自機ショットの生存フラグをチェック
                    if (charaShot[i].alive) {
                        //自機ショットとエネミーとの衝突判定
                        for (j = 0; j < ENEMY_MAX_COUNT; j++) {
                            //エネミーの生存フラグをチェック
                            if (enemy[j].alive) {

                                //エネミーと自機ショットとの距離を計測
                                p = enemy[j].position.distance(charaShot[i].position);
                                //衝突判定
                                if (p.length() < enemy[j].size) {
                                    //生存フラグを降ろす
                                    charaShot[i].alive = false;
                                    enemy[j].alive = false;

                                    //スコアを加算
                                    score++;

                                    //衝突があったのでループを抜ける
                                    break;
                                }
                            }
                        }
                        //自機ショットとボスビットとの衝突判定
                        for (j = 0; j < BOSS_BIT_COUNT; j++) {
                            //ビットの生存フラグをチェック
                            if (bit[j].alive) {
                                //ビットと自機ショットとの距離を計測
                                p = bit[j].position.distance(charaShot[i].position);
                                //衝突判定
                                if (p.length() < bit[j].size) {
                                    //耐久値をデクリメントする
                                    bit[j].life--;

                                    //自機ショットの生存フラグを降ろす
                                    charaShot[i].alive = false;

                                    //耐久値がマイナスになったらビットの生存フラグを降ろす
                                    if (bit[j].life < 0) {
                                        bit[j].alive = false;
                                        score += 3;
                                    }

                                    //衝突があったのでループを抜ける
                                    break;
                                }
                            }
                        }

                        //ボスの生存フラグをチェック
                        if (boss.alive) {
                            //自機ショットとボスの衝突判定
                            p = boss.position.distance(charaShot[i].position);
                            if (p.length() < boss.size) {
                                //耐久値をデクリメント
                                boss.life--;

                                //自機ショットの生存フラグを降ろす
                                charaShot[i].alive = false;

                                //耐久値がマイナスになったらクリア
                                if (boss.life < 0) {
                                    score += 10;
                                    run = false;
                                    message = 'CLEAR!!';
                                }
                            }
                        }
                    }
                }
                //自機とエネミーショットの衝突判定
                //全てのエネミーショットを調査する
                for (i = 0; i < ENEMY_SHOT_MAX_COUNT; i++) {
                    //エネミーショットの生存フラグをチェック
                    if (enemyShot[i].alive) {
                        //自機とエネミーショットとの距離を計測
                        p = chara.position.distance(enemyShot[i].position);

                        //衝突判定
                        if (p.length() < chara.size) {
                            //自機の生存フラグを降ろす
                            chara.alive = false;

                            //衝突があったのでパラメータを変更してループを抜ける
                            run = false;
                            message = 'GAME OVER!!';
                            break;
                        }
                    }
                }

                break;
        }

        //スコア表示更新
        info.innerHTML = 'SCORE: ' + (score * 100) + ' ' + message;

        //フラグにより再帰呼び出し
        if (run) { setTimeout(arguments.callee, fps); }
    })();
};

//event---------------------------------------------------
function mouseMove(event) {
    //マウスカーソル座標の更新
    mouse.x = event.clientX - screenCanvas.offsetLeft;
    mouse.y = event.clientY - screenCanvas.offsetTop;
}

function mouseDown(event) {
    //フラグを立てる
    fire = true;
}

function keyDown(event) {
    //キーコードを取得
    var ck = event.keyCode;

    //Escキーが押されていたらフラグを降ろす
    if (ck === 27) { run = false; }
}

common.js

function Point(){
    this.x = 0;
    this.y = 0;
}

Point.prototype.distance = function(p){
    var q = new Point();
    q.x = p.x - this.x;
    q.y = p.y - this.y;
    return q;
};

Point.prototype.length = function(){
    return Math.sqrt(this.x * this.x + this.y * this.y);
};

Point.prototype.normalize = function(){
    var i = this.length();
    if(i>0){
        var j = 1 / i;
        this.x *= j;
        this.y *= j;
    }
};

character.js

function Character() {
    this.position = new Point();
    this.size = 0;
}

Character.prototype.init = function (size) {
    this.size = size;
}

function CharacterShot() {
    this.position = new Point();
    this.size = 0;
    this.speed = 0;
    this.alive = false;
}

CharacterShot.prototype.set = function (p, size, speed) {
    //座標をセット
    this.position.x = p.x;
    this.position.y = p.y;

    //サイズ、スピードをセット
    this.size = size;
    this.speed = speed;

    //生存フラグを立てる
    this.alive = true;
};

CharacterShot.prototype.move = function () {
    //座標を真上にspeed分だけ移動させる
    this.position.y -= this.speed;

    //一定以上の座標に到達していたら生存フラグを降ろす
    if (this.position.y < - this.size) { this.alive = false; } } function Enemy() { this.position = new Point(); this.size = 0; this.type = 0; this.param = 0; this.alive = false; } Enemy.prototype.set = function (p, size, type) { //座標をセット this.position.x = p.x; this.position.y = p.y; //サイズ、タイプをセット this.size = size; this.type = type; //パラメータをリセット this.param = 0; //生存フラグを立てる this.alive = true; }; Enemy.prototype.move = function () { //パラメータをインクリメント this.param++; //タイプに応じて分岐 switch(this.type) { case 0: //x方向へまっすぐ進む this.position.x += 2; //スクリーンの右端より奥に到達したら生存フラグを降ろす if (this.position.x > this.size + screenCanvas.width) {
                this.alive = false;
            }
            break;
        case 1:
            //マイナスx方向へまっすぐ進む
            this.position.x -= 2;

            //スクリーンの左端より奥に到達したら生存フラグを降ろす
            if (this.position.x < -this.size) { this.alive = false; } break; case 2: //画面下(yの+方向)にまっすぐ進む this.position.y +=2; //スクリーン下に到達したら生存フラグを降ろす if(this.position.y > this.size + screenCanvas.height){
                this.alive = false;
            }
            break;
    }
};

function EnemyShot(){
    this.position = new Point();
    this.vector = new Point();
    this.size = 0;
    this.speed = 0;
    this.alive = false;
}

EnemyShot.prototype.set = function(p,vector,size,speed){
    //座標、ベクトルをセット
    this.position.x = p.x;
    this.position.y = p.y;
    this.vector.x = vector.x;
    this.vector.y = vector.y;

    //サイズ、スペードをセット
    this.size = size;
    this.speed = speed;

    //生存フラグを立てる
    this.alive = true;
};

EnemyShot.prototype.move = function(){
    //座標をベクトルに応じてspeed分だけ移動させる
    this.position.x += this.vector.x * this.speed;
    this.position.y += this.vector.y * this.speed;

    //一定以上の座標に到達していたら生存フラグを降ろす
    if(
        this.position.x < -this.size ||
        this.position.y < -this.size || this.position.x > this.size + screenCanvas.width ||
        this.position.y > this.size + screenCanvas.height 
    ){
        this.alive = false;
    }
}

boss.js

//boss-----------------------------------------
function Boss() {
    this.position = new Point();
    this.size = 0;
    this.life = 0;
    this.param = 0;
    this.alive = false;
}

Boss.prototype.set = function (p, size, life) {
    //座標をセット
    this.position.x = p.x;
    this.position.y = p.y;

    //サイズ、耐久値をセット
    this.size = size;
    this.life = life;

    //パラメータをリセット
    this.param = 0;

    //生存フラグを立てる
    this.alive = true;
};

Boss.prototype.move = function () {
    var i, j;

    //パラメータをインクリメント
    this.param++;

    //パラメータに応じて分岐
    switch (true) {
        case this.param < 100:
            //下方向にまっすぐ進む
            this.position.y += 1.5;
            break;
        default:
            //パラメータからラジアンを求める
            i = ((this.param - 100) % 360) * Math.PI / 180;

            //ラジアンから横移動量を算出→サイン、コサイン勉強必要!
            j = screenCanvas.width / 2;
            this.position.x = j + Math.sin(i) * j;
            break;
    }
}

//boss bit-------------------------------------------------------
function Bit(){
    this.position = new Point();
    this.parent = null;
    this.size = 0;
    this.life = 0;
    this.param = 0;
    this.alive = false;
}

Bit.prototype.set = function(parent,size,life,param){
    //母体となるボスをセット
    this.parent = parent;

    //サイズ、耐久値をセット
    this.size = size;
    this.life = life;

    //パラメータに初期値をセット
    this.param = param;

    //生存フラグを立てる
    this.alive = true;
};

Bit.prototype.move = function(){
    var i,x,y;

    //パラメータをインクリメント
    this.param++;

    //パラメータからラジアンを求める
    i = (this.param % 360) * Math.PI / 180;

    //ラジアンから横移動量を算出
    x = Math.cos(i) * (this.parent.size + this.size);
    y = Math.sin(i) * (this.parent.size + this.size);
    this.position.x = this.parent.position.x + x;
    this.position.y = this.parent.position.y + y;
}