プログラミング初心者のための「HTML・CSS・Javascript」で作るシンプルゲームアプリ開発入門

「HTML・CSS・Javascript」を使用して、「シンプルなゲームアプリ」を作りながら、

  • ゲーム進行制御
  • ローカルストレージ
  • クリック検知
  • 時間管理

など、ゲームアプリ開発に必要なスキルを身に付けていきましょう。

前回、作成したアニメーションアプリを元に作成をしていきますので、アニメーション部分については、

→「プログラミング初心者のためのHTML・CSS・Javascriptで作るアニメーションアプリ開発入門」

を先に読んでおいてください。

今回作成するのは下の動画のようなゲームアプリです。

「START」ボタンをクリックするとゲームが開始し、ボールが動き出しますので、ボールをクリックしてボールを削除していき、「ボールを全て削除するまでのタイムを競う」という「シンプルなゲームアプリ」です。

「クリアタイム」は、ローカルストレージに保存をしているため、ブラウザを閉じても再度同じブラウザでページを開くと、ローカルストレージ内の「スコアの一覧」データが表示されます。

ゲームに必要な要素の作成

前回作成したアニメーション要素にゲーム要素を追加するために、

var timeScr = document.getElementById('time'); // タイム表示要素
var startTime;             // ゲーム開始時間
var gameStatus = false;    // ゲームステータス
var scores = new Array();  // タイムスコア

の変数を新たに追加しています。

  • timeScr → ゲーム中の経過時間を表示する「タイム表示要素」のDOMの取得
  • startTime→ ゲーム開始時間を記録する変数
  • gameStatus → ゲームの状態を管理する変数
  • scores → タイムスコアの保存用配列変数

ボールの当たり判定

Ballクラスに関しては、「クリックした際にボールに触れているか?」を調べるために「isHit」メソッドを追加します。

class Ball{

       ・
       ・
       ・
       ・
              
    isHit(clickPosX, clickPosY){
        if(Math.sqrt(Math.pow(clickPosX - this.posX,2) + Math.pow(clickPosY - this.posY,2)) < 15){
            return true;   
        } else {
            return false;
        }
    }
}

クリックした位置がボールと触れているかは下図のように「クリック点とボールの距離」を計算します。

ボールのクリック判定

この計算値が、ボールの半径より小さければ「ボールをクリックしている」と判定しボールを削除します。

「クリック点とボール中心の距離」は「ピタゴラスの定理」で導くことができますが、「ピタゴラスの定理」を忘れてしまった方は、

→「ピタゴラスの定理」

を参照してみてください。

ゲームの初期化処理

ゲーム開始時の初期化処理には、「タイムスコア」を「ローカルストレージ」から読み込む処理等が追加されています。

// コンテキストの取得可否チェック
if(canvas.getContext){
    // canvas要素のコンテキストを取得
    context = canvas.getContext('2d');
    initBallObj();          //ボールの初期化
    var ret = loadScores(); //タイムスコアのロード
    if(ret !== null){ 
        scores = ret;
        refleshShowScores(); //タイムスコアを更新
    };
}

新しいメソッドがいくつか追加されていますね。

  • initBallObj
  • loadScores
  • refleshShowScores

のメソッドの内容は、次のようになります。

/**
 * ボールの初期化
 */
function initBallObj(){
    // new Ball(X座標,Y座標,横幅,高さ,移動速度,回転有無,画像ファイル名)
    objArray = new Array();
    objArray.unshift(
        new Ball(150,75,30,30,false,0.4,"./img/image1.png"),
        new Ball(70,120,30,30,true,0.6,"./img/image2.png"),
        new Ball(45,115,30,30,true,0.8,"./img/image3.png"),
        new Ball(53,45,30,30,true,1.0,"./img/image4.png"),
        new Ball(178,103,30,30,false,1.2,"./img/image5.png"),
        new Ball(223,141,30,30,false,1.4,"./img/image6.png"),
        new Ball(255,111,30,30,false,1.6,"./img/image7.png"),
        new Ball(215,121,30,30,true,1.8,"./img/image8.png")
    ); 
}

「initBallObj」メソッドは、前回作成した「アニメーション処理」にも存在していた「ボールのインスタンス(実体)」を作っている部分をメソッドに切り出しています。

「ゲーム開始時」と2回目以降のゲームを開始する「初期化処理」でこのメソッドを使用しています。

次に、今回新しい要素として出てきている「スコアのロード(読み込み」と「スコア表示更新」のメソッドは下記のようになります。

/**
 * タイムスコアをローカルストレージからロード
 */
function loadScores(){
    var loadScores = localStorage.getItem('scores');
    return JSON.parse(loadScores);
}

/**
 * タイムスコア一覧の表示を更新
 */
function refleshShowScores(){
    var html = '';
    document.getElementById('scores').innerHTML = '';
    html += 'RankTime';
    for( var i = 0; i < scores.length; i++){
        html += '' + (i + 1) + '' + showTime(Number(scores[i])) + '';
    }
    document.getElementById('scores').innerHTML = html; 
}

これまでのタイムスコアは「ローカルストレージ」に保存しているため、「loadScores」メソッドでは、「ローカルストレージ」からスコアを読み込んでいます。

「タイムスコアデータ」は、「JSON」形式で保存されているため、「JSON.parse」メソッドでJSONデータをオブジェクトに変換しています。

「ローカルストレージ」の詳しい使い方は、

→「ローカルストレージ」

を参照してみてください。

ボールクリック時の処理

ボールをクリックした場合の処理は、CANVASオブジェクトの「onclickプロパティ」に「無名関数オブジェクト」を定義し、代入します。

/**
 * CANVASクリック時の処理
 */
canvas.onclick = function(e) {
    // CANVAS要素の座標位置を取得
    rect = e.target.getBoundingClientRect();
                
    // クリック位置がボールに触れているかをすべてのボール対して判定
    for( var i = 0; i < objArray.length; i++){
        if(objArray[i].isHit((e.clientX - rect.left), (e.clientY - rect.top)) === true){
            // ボールに触れていれば、ボール格納用配列からボールを削除
            objArray.splice(i,1);
        }
    }
                
    if(objArray.length === 0){
        // すべてのボールを削除した場合の処理(ゲームの終了処理)
        gameStatus = false;
        clearInterval(timerID); // タイマーを停止
        context.clearRect(0, 0, canvas.width, canvas.height); // CANVASを初期化
                    
        // クリアタイムを表示
        context.font = "24px 'MS Pゴシック'";
        var timeTP = getGameTP();
        context.fillStyle = "gray";
        context.fillText("クリアタイム", (canvas.width / 2) - 73, 65);
        context.fillText(showTime(timeTP), (canvas.width / 2) - 45, 95);
                    
        // タイムスコア配列にスコアを追加
        scores.unshift(timeTP); 
                    
        // タイムスコアを昇順に並び替え
        scores.sort(function(a,b){ return (a < b ? -1 : 1); } );  
                    
        // タイムスコアを「ローカルストレージ」に保存
        saveScores(scores); 
                    
        refleshShowScores(); // タイムスコアの表示を更新
        timeScr.innerHTML = "Time";
        initBallObj(); // ボールの初期化
    }
}

新たに「saveScores」メソッドが登場していますが、「ローカルストレージ」にタイムスコアを保存しています。

/**
 * タイムスコアをローカルストレージにセーブ
 */
function saveScores(scores){
    var saveScores = JSON.stringify(scores);
    localStorage.setItem('scores', saveScores);
}

タイムスコアを「ローカルストレージ」に保存するためにはオブジェクトをJSONに変換するため「JSON.stringify」メソッドを使用します。

タイムスコアの時間管理法

ゲーム内の時間は「ミリ秒(1000分の1秒)」で管理をしています。

基準時刻を「1970年1月1日0時0分0秒」とした「UNIXタイムスタンプ」という値を用いて、ゲームでの経過時間を計算しています。

ゲーム開始時のタイムスタンプを「startTime」変数に格納しておき、ゲーム開始後に、その時点でのタイムスタンプとの差を求めることで、ゲームでの経過時刻が求められます。

その時点でのタイムスタンプは「Dateオブジェクト」の「now」メソッドで取得することができます。

startTime = Date.now();

ゲーム中のある時点での時刻と「startTime」変数に格納された値の差を求める処理は「getGameTP」メソッドの中で行っています。

/**
 * ゲーム開始時刻からの経過時刻(タイムスタンプ)を取得
 */
function getGameTP() {
    return Number(Date.now()) - Number(startTime);
}

これでゲームに必要な一通りの機能は揃いましたので、完成したコードは下記のようになります。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="robots" content="noindex,nofollow">
        <title>WEB SCREEN APP</title>
        <link rel="stylesheet" type="text/css" href="web_game_app.css">
    </head>
    <body>
        <canvas id="screen"></canvas>
        <div id="control">
            <button id="start_btn">START</button>
            <span id="time">time</span>
        </div>
        <table id="scores">
            <tr>
                <th>Rank</th>
                <th>Time</th>
            </tr>
        </table>
        <script>
            // canvas要素のDOM(Document Object Model)を取得
            const INTERVAL = 1000;
            var canvas = document.getElementById('screen');
            var context;
            var rect;
            var objArray;
            var obj;
            var timerID;
            var timeScr = document.getElementById('time'); // タイム表示要素
            var startTime;             // ゲーム開始時間
            var gameStatus = false;    // ゲームステータス
            var scores = new Array();  // タイムスコア

            class Ball{
                constructor(x, y, width, height, rotFlg, speed,image_fname ) {
                    this.posX = x;                 // CanvasのX座標
                    this.posY = y;                 // CanvasのY座標
                    this.width = width;            // 画像の横幅
                    this.height = height;          // 画像の縦幅
                    this.posXmoveDirection = true; // true : X座標増加, false : X座標減少
                    this.posYmoveDirection = true; // true : Y座標増加, false : Y座標減少  
                    this.moveSpeed = speed;        // 移動速度
                    this.rotateAngle = 0;          // 回転角度
                    this.rotateFlag = rotFlg;      // 回転フラグ
                    
                    // Imageオブジェクト生成
                    var img = new Image();     
                    
                    // 画像ファイルを読み込み
                    img.src = image_fname;     
                    
                    // 画像ファイルをセット
                    this.image = img;          
                }
                
                move(){  
                    if( this.posXmoveDirection === false ) {
                        this.posX -= this.moveSpeed;
                    } else {
                        this.posX += this.moveSpeed;  
                    }
       
                    if( this.posYmoveDirection === false ) {                 
                        this.posY -= this.moveSpeed;
                    } else {               
                        this.posY += this.moveSpeed;       
                    }
                }
                
                isHit(clickPosX, clickPosY){
                    if(Math.sqrt(Math.pow(clickPosX - this.posX,2) + Math.pow(clickPosY - this.posY,2)) < 15){
                        return true;   
                    } else {
                        return false;
                    }
                }
            }
            
            // コンテキストの取得可否チェック
            if(canvas.getContext){
                // canvas要素のコンテキストを取得
                context = canvas.getContext('2d');
                initBallObj();          //ボールの初期化
                var ret = loadScores(); //タイムスコアのロード
                if(ret !== null){ 
                    scores = ret;
                    refleshShowScores(); //タイムスコアを更新
                };
            }
            
            function move(){
                timeScr.innerHTML = showTime(getGameTP());  //表示タイムを更新
                context.clearRect(0, 0, canvas.width, canvas.height);
                objArray.forEach(function( obj ) {
                
                    if(obj.posX >= canvas.width - (obj.width / 2)){
                        obj.posXmoveDirection = false;
                    }
                
                    if(obj.posX <= (obj.width / 2)){
                        obj.posXmoveDirection = true;
                    }
                
                    if(obj.posY >= canvas.height - (obj.height / 2)){
                        obj.posYmoveDirection = false;
                    }
                
                    if(obj.posY <= (obj.height / 2)){
                        obj.posYmoveDirection = true;
                    }
                
                    obj.move();

                    context.beginPath();
                     
                    if(obj.rotateFlag === true){
                        context.save();
                        context.translate(obj.posX, obj.posY);
                        obj.rotateAngle++;
                        context.rotate(( obj.rotateAngle % 360 ) / 180 * Math.PI);
                        context.translate(-obj.width/2, -obj.height/2);
                        context.drawImage(obj.image, 0, 0);
                        context.restore();
                    } else {
                        context.drawImage(obj.image, obj.posX - (obj.width/2), obj.posY - (obj.height/2));
                    }
                });
            }
            
            /**
             * ゲーム開始
             */
            function startGame(){
                if( gameStatus === false ){
                    gameStatus = true;
                    startTime = Date.now();
                    timerID = setInterval(move, 33);
                }
            }
            
            /**
             * ボールの初期化
             */
            function initBallObj(){
                // new Ball(X座標,Y座標,横幅,高さ,移動速度,回転有無,画像ファイル名)
                objArray = new Array();
                objArray.unshift(
                    new Ball(150,75,30,30,false,0.4,"./img/image1.png"),
                    new Ball(70,120,30,30,true,0.6,"./img/image2.png"),
                    new Ball(45,115,30,30,true,0.8,"./img/image3.png"),
                    new Ball(53,45,30,30,true,1.0,"./img/image4.png"),
                    new Ball(178,103,30,30,false,1.2,"./img/image5.png"),
                    new Ball(223,141,30,30,false,1.4,"./img/image6.png"),
                    new Ball(255,111,30,30,false,1.6,"./img/image7.png"),
                    new Ball(215,121,30,30,true,1.8,"./img/image8.png")
                ); 
            }

            /**
             * CANVASクリック時の処理
             */
            canvas.onclick = function(e) {
                rect = e.target.getBoundingClientRect();
                
                for( var i = 0; i < objArray.length; i++){
                    if(objArray[i].isHit((e.clientX - rect.left), (e.clientY - rect.top)) === true){
                        objArray.splice(i,1);
                    }
                }
                
                if(objArray.length === 0){
                    gameStatus = false;
                    clearInterval(timerID);
                    context.clearRect(0, 0, canvas.width, canvas.height);
                    context.font = "24px 'MS Pゴシック'";
                    var timeTP = getGameTP();
                    context.fillStyle = "gray";
                    context.fillText("クリアタイム", (canvas.width / 2) - 73, 65);
                    context.fillText(showTime(timeTP), (canvas.width / 2) - 45, 95);
                    scores.unshift(timeTP);
                    scores.sort(function(a,b){ return (a < b ? -1 : 1); } );
                    saveScores(scores)
                    refleshShowScores();
                    timeScr.innerHTML = "Time";
                    initBallObj();
                }
            }
            
            /**
             * 表示タイムの生成
             */
            function showTime(time){
                mSec = time % 1000;
                sec = Math.floor(time / 1000);
                return sec + "秒" + mSec;
            }
            
            /**
             * ゲーム開始時刻からの経過時刻(タイムスタンプ)を取得
             */
            function getGameTP() {
                return Number(Date.now()) - Number(startTime);
            }
            
            /**
             * タイムスコア一覧の表示を更新
             */
            function refleshShowScores(){
                var html = '';
                document.getElementById('scores').innerHTML = '';
                html += '<tr><th>Rank</th><th>Time</th></tr>';
                for( var i = 0; i < scores.length; i++){
                    html += '<tr><td>' + (i + 1) + '</td><td>' + showTime(Number(scores[i])) + '</td></tr>';
                }
                document.getElementById('scores').innerHTML = html; 
            }
            
            /**
             * タイムスコアをローカルストレージにセーブ
             */
            function saveScores(scores){
                var saveScores = JSON.stringify(scores);
                localStorage.setItem('scores', saveScores);
            }
            
            /**
             * タイムスコアをローカルストレージからロード
             */
            function loadScores(){
                var loadScores = localStorage.getItem('scores');
                return JSON.parse(loadScores);
            }
            
            document.getElementById('start_btn').addEventListener('click', startGame, false);
        </script>
    </body>
</html>

実際の動きは、下記リンクから確認することができます。

サンプル(Google Chrome動作確認済)

今回は「ボールのみ」をクラスにしましたが、「ゲームの進行」や「タイムスコア」などさまざまなクラスを作り、「オブジェクト間で連携する処理」を作ることで、「中・大規模なゲーム」を作ることもできます

「Three.js」などの「3D描画ライブラリ」を使うことで、「3Dゲーム」を作ることもできますので、さまざまなゲームを作りながらプログラミングスキルを身に付けるのも楽しいのではないかと思います。

さまざまな「ゲームアイデア」を考えながら、ぜひ「オリジナルのゲームアプリ開発」にチャレンジしてみてください。

HOMEへ