このサイトでは、分析、カスタマイズされたコンテンツ、および広告に Cookie を使用します。このサイトを引き続き閲覧すると、Cookie の使用に同意するものと見なされます。
Hi, Developers,
straightapps.com ロゴ
作成 September 25, 2020、最終更新 October 9, 2020
トップページ > Web 開発トップ > マップ内のすべてのフラッグを集めろ
line
Web 開発
line

ボールをうまく転がして、マップ内のすべてのフラッグを集めるゲームです。

※ 表示が正しくないと思う場合は、JavaScript を有効にしてください。



マップ内のすべてのフラッグを集めろ!

PC などのキーがある環境では、上下左右キーを押して、その方向にボールを転がせます。 マップ上に点在しているフラッグを、すべて集めましょう! ボールは壁に当たっても止まりません!

左上のミニマップに、残りのフラッグの位置が表示されています。

スマホやタブレットなど、キーがない場合は、(マップ内で)ボールを進めたい側をタップしてください。

ボールの近くをタップ(クリック)すると、一時停止できます。

現時点で、別の面はありません。 クリアして、再開したい場合は、ページをリロードしてください。


現時点でのテスト済みブラウザは、

  • Windows 10 PC の Edge, Chrome, FireFox
  • です。 Windows 10 PC の Internet Explorer では、ゲーム自体は動作しますが、文字が表示されません。

    スマホやタブレットで開く場合は、 この QR コード ( URL の QR コード
    https://www.straightapps.com/web/js-flag-rally.html
    )
    が、このページの URL になっています。 QR コード読み取りアプリで読み取ると便利です。


    この手のゲームには、さらにいろいろな仕組みが考えられますが、 このページの目的は、複雑なコードではなく、JavaScript の練習なので、やりすぎません。

    ▲ページ先頭へ

    ここから先は開発情報です

    投稿 September 25, 2020

    ここから先は、上記、canvas を使ってマップをスクロールさせ、フラッグをゲットするプログラム開発を行った際に調べたりした JavaScript コードについての情報です。

    作者背景については、「素因数分解トレーニング」のページの 「ここから先は開発情報です」をご覧ください。

    まずは、HTML 部です。

    <canvas id="canvas-main"></canvas>
    
    <script type="text/javascript" src="js/flagrally.js"></script>
    
    <button onclick="speed(2)" title="低速"> スロー </button>
    <button onclick="speed(4)" title="通常"> 通常速度 </button>
    <button onclick="speed(8)" title="高速"> クイック </button>
    

    canvas id="canvas-main" は、ゲーム盤面を描画する、グラフィック領域です。

    script の読み込みのあとにあるボタンは、押されたときに JavaScript 関数を呼び出すようにしています。 環境(デバイス)によって動作速度が遅い、または速いと感じられる場合に、調整可能にします。

    以降のセクションで、それぞれ詳細を確認していきます。

    なお、ご参考までに、JavaScript ソースコードそのものを、拡張子 js を拡張子 txt に変えて、次のリンクより開けるようにしてあります。
    flagrally-init.txt
    flagrally.txt
    ※ 作成時はタブを4文字としていますので、環境によってはタブが8文字のため、 コメント等がずれて見えるかも知れません。
    ※ 念のため書き添えますが、このソースコードをこのまま転載・公開することはご遠慮ください。

    今後、それぞれ見ていきますが、現時点では動作するところまでとさせてください。 近日中に、追記の形で情報を載せていきます。

    なお、本サイトのご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。

    ▼ セクション一覧

    JavaScript コードのロード位置
    ページ読み込み時の処理
    マップ配列をスキャンする
    メインループ
    キー入力の受付け
    マウスクリック(タップ)の受付け
    向きを変える処理
    そのままの向きを継続する
    通れるかの判定とフラッグのゲット
    マップのスクロール処理
    ミニマップを描画する
    速度設定ボタン処理


    ▲ページ先頭へ

    JavaScript コードのロード位置

    投稿 October 6, 2020

    flagrally-init.js は、 <head> 〜 </head> タグ内でロードしていますが、 この場所で読み込む場合は、ページ本体のデータ(body 部)が読み込まれる前に、ロードされるようです。

    なので、この中で、あとで定義されている canvas id="canvas-main" にアクセスしようとしても、アクセスできません。

    var scr = new Object();					// 画面のサイズ情報を保持するオブジェクト
    scr.width = screen.width;				// 画面のサイズ
    scr.innerWidth = window.innerWidth;			// ウィンドウ内側の幅
    scr.innerHeight = window.innerHeight;			// ウィンドウ内側の高さ
    scr.clientWidth = document.documentElement.clientWidth;	// 基本的にはinnerHeightと同じ?
    

    flagrally-init.js にあるのは、 同じく canvas でゲームを展開する moveball-init.js と同様、 上記のものだけです。 利用できる画面サイズを scr に設定しています。 詳しくは、 「ボールを転がしてゴールを目指す」の 「JavaScript コードのロード位置」 をご覧ください。

    ▲ページ先頭へ

    ページ読み込み時の処理

    投稿 October 6, 2020

    body が読み進まれていき、script の行まで到達すると、src で指定されたファイルが読み込まれます。

    <script type="text/javascript" src="js/flagrally.js"></script>
    

    flagrally.js は、 canvas id="canvas-main" にアクセスしますので、 この script の行は、使用する要素の定義よりあとに記述されていなくてはなりません。 一般的には </body> の直前でいいようですが、 このページでは、読みにくくならないように、 必要な HTML 要素の定義が終わった直後、 つまり canvas の定義の直後に置いています。 そのあと、このソースにある関数を呼び出す button を定義していますが、 ボタンが押されて初めて解析されるため(そのときにはもう読み込まれている)、これで問題ないようです。

    なお、ただ単純に、ブラウザがその行を解析するときに読む、というだけのようですから、 script 行が複数あれば、それぞれそのタイミングで読み込まれるようです。

    flagrally.js の先頭部分は、次のような流れになっています。

    まずは、1マスのサイズを保持するための情報を設定しています。

    // ブロック情報
    var block = new Object();
    block.w = 13;				// 横方向の表示コマ数
    block.h = 13;				// 縦方向の表示コマ数
    block.offsetx = 6;
    block.offsety = 6;
    block.size = 0;				// 1コマの表示サイズ(ピクセル・正方形)
    

    オブジェクト block に情報を入れることとします。 Object 変数は、C/C++ で言う構造体に似たものです。 メンバー変数と言える、ピリオドの後に続く変数名は、 事前に定義することなく、自由に作成し値を代入できます。 自由すぎて危険に思えます

    block.wblock.h は、一度に画面に表示するマスの数です。 大きなマップのうち、ボールのまわり計 13 × 13 の領域だけを、 画面(canvas 内)に表示することとします。 ユーザーが操作するボールは、常に画面の中央にあることとしますので、 block.offsetxblock.offsety ともに 6 を入れ、(6, 6) の位置に描画します。

    block.size は1マスのサイズとしますが、 canvas のサイズが決まらないと決まらないので、仮に 0 を入れています。 1マスのサイズを先に決め、そこから canvas のサイズを決定すると、 スマホでは画面に収まらないとか、PC だとすごく小さいとか、不都合が起きるでしょう。

    次に、canvas のサイズを決定しています。 ブラウザの描画可能領域までを限度とし、 上記 block に設定した、13 × 13 マスを表示できるサイズとしています。

    画面が十分広い場合は、最大 520 x 520 ピクセルまでに制限している以外は、 「ボールを転がしてゴールを目指す」の canvas のサイズを決定 している部分と同じですので、必要に応じてご参照をお願いします。 内容としては、block.size に1マスのサイズを設定し、 決まった canvas のサイズを canvas.widthcanvas.height に設定している、です。

    canvas に描画するためのコンテキストの取得はいつも同じです。

    var ctx = canvas.getContext('2d');
    

    次に、ボール情報を保持するオブジェクトを作成しています。

    var ball = new Object();
    ball.img = new Image();
    ball.img.src = 'jsimg/pin.png';			// 通常
    ball.imgF1 = new Image();
    ball.imgF1.src = 'jsimg/zfr_flash1.png'		// フラッグを獲った直後
    ball.cx = 24;					// キャラクタベースの座標
    ball.cy = 23;
    

    変数 ball に、新しいオブジェクトを作成します。 最初にあった block と同じように、事前の定義は必要なく、 ピリオドのあとに自由に変数名を付け、値を代入可能です。

    その img メンバーに、新しい Image クラスのオブジェクトを作成します。 Image のオブジェクト img の src プロパティにファイル名を設定し、ボールの画像データとします。 あとでコンテキストの drawImage 関数でボールを描画するときに参照します。

    同じように、imgF1 には、フラッグを獲った直後に描画するボール画像を設定しています。

    ball.cx と ball.cy はボールの位置を、マップ全体のうちの座標で保持するものです。 初期位置を設定しています。

    今度はフラッグの情報です。

    var treasure = new Object();
    treasure.img = new Image();
    treasure.img.src = 'jsimg/zfr_treasure.png';
    treasure.num = 0;				// 残りフラッグ数(自動計算)
    treasure.flash = 0;				// フラッグ取得後のカウンタ
    

    treasure.img に画像を設定しています。 treasure.num には 0 を入れていますが、あとでフラッグをマップ内に自由に配置し、 ゲーム開始前にマップをスキャンして、残りのフラッグの個数をこの変数にセットしています。

    treasure.flash は、フラッグを獲ったあとにボールの色を変えるためのカウンタとしています。

    続いてキー入力用のオブジェクトを定義しています。

    ボールは移動を始めたらもう止まりません。 壁に当たっても、(なるべく直前に移動していた向きに)向きを変えて進み続けるようになっています。 また、曲がれる部分より前に 移動方向を指示 ( 矢印キー、またはクリック(タップ)で指示します。 ) した場合、次にその方向に行けるようになったときに曲がるように、予約します。 いわゆる「キーの先行入力」のための用意です。 ただ、このプログラムでは、うまく動いていないケースがあるのかも知れません。

    var key = new Object();
    key.reserve = '';				// 次に移動する方向
    key.push = '';					// 今移動している方向
    key.prev = '';					// 前に移動していた方向
    

    いよいよマップを用意します。

    var map = new Object();
    
    map.map =[	"9999999999999999999999999999999999999999999999999",	// 1
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",	// 5
    		"9999999999999999999999999999999999999999999999999",
    		"9999991000000011100000000111000000000010000999999",
    		"9999991010020011100011100011000020011110020999999",
    			<!-- 同じような定義は省略 -->
    		"9999990020110111011011110001111011011001010999999",
    		"9999990000110000020000000000200000000001020999999",	// 25
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",
    		"9999999999999999999999999999999999999999999999999",	// 30
    		"9999999999999999999999999999999999999999999999999"
    ];	//	 0123456789abcdefghij0123456789abcdefghij012345678
    

    このような直接的な初期化は C/C++ に似ていますが、 全体を中カッコで囲うのではなく、 大カッコで囲います ( しかもイコールより右側だけでよく、map[] のようには書きません。 ) ので、注意が必要です。

    map.map[] は、31 要素の文字列を持つ1次元配列で、 1要素は 49 文字で構成されています。 文字列なので数字である必要はありませんが、数字で構成しています。 すべての要素の文字数が同じであれば、 31 要素でなくても、49 文字でなくても問題は起きません。

    9 は、場外に出られなくするための外枠です。 上下左右ともに6列あれば、中央にいるボールのまわりが配列外を参照することはありません。 1 は、場内にある障害物(壁)です。 通り抜けはできません。 0 は通れる部分で、2 はフラッグです。 フラッグは、このあと checkMap 関数でスキャンするので、 いくつ配置しても正しくカウントされます。

    map.h = map.map.length;			// マップの縦のデータ数
    map.w = map.map[0].length;			// マップの横のデータ数
    map.cx = ball.cx - block.offsetx;		// キャラクタベースの座標
    map.cy = ball.cy - block.offsety;
    map.x = 0;					// ピクセルベースの座標
    map.y = 0;
    map.move = 0;					// 移動量(ピクセル)
    

    map.hmap.w には、 map 配列の縦方向(1次元目)の要素数と、 横方向(1要素)の文字数を設定しています。 上記のように、length プロパティを参照して値を得ていますので、 map 配列定義の時には、要素数も文字数も自由に定義しても問題はない、となります。 なお、1次元配列について、より詳細には、 「素因数分解トレーニング」の 「【基礎】可変サイズの配列と操作」に書いています。

    map.cxmap.cy には、 マップを画面に描画するときの、左上のマスの座標を、マス単位で設定しています。 ball の座標がボール位置で、画面中央に表示するための値が block.offset に入っていますので、 ここから計算しています。

    残りの3つは、マップを滑らかにスクロールさせるためのもので、あとで設定されます。

    マップはこのようにコード内でじかに設定していますので、1面や2面などの区別はありません。

    マップ情報が設定されましたので、マップを解析します。

    checkMap 関数の詳細は、次のセクションで見ていきます。 内容としては、マップを確認して配置されているフラッグの数を数え、treasure.num に設定する、です。

    checkMap();
    

    ゲーム状態を設定します。

    var state = 'ready';				// ready/play/pause/clear
    var oneStep = 4;				// 1回で4ピクセル移動
    

    state には、現在の状態を記録しています。 ゲーム開始前は ready、 プレー中は play が設定されることとします。 すべてのフラッグを獲得したら、clear にします。 また、ゲーム中にボールをクリック(タップ)したら一時停止できるとし、 その間は pause にします。

    最後に、キー入力を受け付けるためのイベントリスナーを設定します。 keydown イベントの第3引数が、 「ボールを転がしてゴールを目指す」のときと 違います ( ただ false を指定していました。 ) が、今回の処理の場合、この指定でないと、上下キー入力でページ全体がスクロールしてしまいました。

    addEventListener("keydown", keydownfunc, { passive: false });
    

    canvas 上でのクリック(タップ)を得るためのイベントリスナーも設定します。

    canvas.addEventListener('click', onClick, false);
    

    このあと、ずっとループし続ける main 関数を用意し、 addEventListener 関数で、HTML 読み込み後に実行されるようにしています。 以降は関数の定義しかありませんので、 この flagrally.js の実行は終わりで、続きの HTML が読み込まれていくことになります。

    function main()
    {
    	<!-- 処理の詳細はここでは省略 -->
    	requestAnimationFrame( main );
    }
    
    addEventListener('load', main(), false);
    
    ▲ページ先頭へ

    マップ配列をスキャンする

    投稿 October 6, 2020

    マップ配列 map.map[] をスキャンして、 自由に配置されたフラッグの数をかぞえる関数です。

    function checkMap() {
    	var n;
    
    	for (var y = 0; y < map.h; y++) {
    		for (var x = 0; x < map.w; x++) {
    

    変数 y のループで、1次元配列 map.map[] をすべて確認することとしています。 その中で、変数 x のループで、1要素の文字列を、1文字ずつ確認しています。

    			n = map.map[y][x];
    

    変数 n に、チェック対象とする1文字を取得します。 map.map[] は1次元配列なのですが、それぞれの要素が文字列を持っていますので、 このように2次元配列にアクセスするような形で、参照することができます。 ただし、このような記述に対して、値を設定して部分書き換えすることはできませんでした。

    			switch (n) {
    				case '1':			// 壁
    					break;			// 上下左右を見て角を丸める?
    				case '2':			// フラッグ
    					treasure.num++;		// 数をカウント
    					break;
    				case '9':			// 外側
    					break;
                }
            }
        }
    }
    

    マップの文字が 1 なら壁(通れない)ですので、カウントしません。 何もしていないので判断する必要はないのですが、 上下左右のマップを見て、角を丸めた画像にしたりできるよう、入れてあります。

    マップの文字が 2 ならフラッグですので、treasure.num を加算しています。

    マップの文字が 9 なら外側です。 これも、通れる道との接触ならラインを引くとか、壁との接触ならどうするとか、 将来の拡張時に忘れないように、入れてあります。

    今はまだ、コードで直接マップを設定しているので関係しませんが、 マップが増えて選べるようになったなら、ボールのスタート位置や、スタート時の移動方向も、 マップで指定可能になっているといいかもしれません。

    ▲ページ先頭へ

    メインループ

    投稿 October 7, 2020

    HTML が読み込み完了してから起動される main 関数は、 ウェブページが表示されているうちはずっとループしている処理となります。

    function main()
    {
    	ctx.fillStyle = "rgb( 230, 255, 230 )";
    	ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    	showMap();
    

    canvas のコンテキスト ctx を使用して、 使用する色を fillStyle に設定しています。 この場合は rgb 指定ですので、赤、緑、青の順に、0 〜 255 の値を渡し、「明るい緑」にしています。 fillRect 関数で、canvas 全体を塗りつぶしています。

    showMap 関数は、画面を描画する関数として実装してあります。 あとに書いています。

    main ループを実行する前に、変数 state には ready を設定していますので、 ページを読み込んだときには、この「ゲーム開始前」の状態になります。

    	if (state === 'ready') {
    		ctx.font = "bold 36px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif";
    		ctx.fillStyle = '#faf';
    
    		var str = 'CLICK TO START!';
    		var tx = (canvas.width - ctx.measureText(str).width) / 2;	// センタリング
    		var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent;
    		var ty = (canvas.height - th) / 2;
    		ctx.fillText(str, tx, ty);			// canvas 領域の座標(y座標は文字の下を指している)
    		ctx.strokeText(str, tx, ty);
    	}
    

    canvas のコンテキスト ctx を使用して、font に使用フォントを設定し、fillStyle で色を指定しています。 色指定は、canvas を塗りつぶした rgb の値ではなく、ウェブデザインでよく使われる、 # のあとに R、G、B 値を 16 進数 0 〜 f で3つ並べる書き方を使っています。 この場合は、明るいピンクになります。

    変数 str に、表示したい文字列を設定しています。 これはこのあと、measureText 関数を使ってサイズを計算し、stillText 関数などで描画するためです。

    表示したい文字列 str を、指定済みのフォントで描画した場合のサイズを measureText 関数で取得し、その幅 width を参照して、 横方向に canvas の中央となる x 座標、tx を決定しています。

    高さも同じように、measureText の height で得たいところですが、そうはいかないようです。 テキストの描画では、y 方向にはベースラインという基準となる水平ラインがあり、 そこから上に actualBoundingBoxAscent ピクセル移動したところが、 テキストの上端になるようです。 同じように、actualBoundingBoxDescent ピクセル移動したところが、 テキストの下端になるようです。 なので、これらを足すとテキストの高さになるので、いったん変数 th に入れています。 これをもとに、縦方向の中央、ty を計算しています。

    そして fillText 関数でテキストを描画しています。 指定したフォントのベースラインはテキストの下端にあるようで、 ここで指定した ty は、テキストの下端の座標となり、実際には中央より少し上にテキストが表示されますが、 画面的にはボールに重ならなく都合がいいので、そのままにしてあります。 次の strokeText 関数は、テキストのふちを描画する関数です。

    次は、ゲームクリア後の描画です。

    	else if (state === 'clear') {
    	    ctx.font = "48px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif";
    	    ctx.fillStyle = '#ffa';
    
    	    var str = 'FINISHED !';
    	    var tx = (canvas.width - ctx.measureText(str).width) / 2;
    	    var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent;
    	    var ty = (canvas.height - th) / 2;
    	    ctx.fillText(str, tx, ty);
    	}
    

    すべてのフラッグを集めると、変数 state には clear と設定されるようになっています。 フォントの設定、色指定、そのあとのセンタリングのための計算など、 先ほどの、ゲーム開始前のテキストのセンタリングと同じです。

    ボール付近がクリック(タップ)されると、一時停止となります。 一時停止中は、変数 state に文字列 pause が設定されています。

    	else if (state === 'pause') {
    		ctx.font = "48px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif";
    		ctx.fillStyle = '#fff';
    
    		var str = 'PAUSE';
    		var tx = (canvas.width - ctx.measureText(str).width) / 2;
    		var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent;
    		var ty = (canvas.height - th) / 2;
    		ctx.strokeText(str, tx, ty);
    		ctx.fillText(str, tx, ty);
    	}
    

    表示する文字列以外は、ゲーム開始前のテキスト描画と同じです。

    ゲーム中は、変数 state に文字列 play が入っていますが、 それ以外はここまで見てきていますので、「それ以外」の判断としています。

    	else {
    	    if (map.move === 0) {
    	        makeTurn();
    	    }
    	    else if (map.move > 0) {
    	        keepMoving();
    	    }
    	}
    

    map.move は、マップを滑らかにスクロールさせるための移動量で、 これが 0 のとき、「滑らかな移動中」ではなく、マスぴったりの位置にいることを意味します。 makeTurn 関数で方向キー(あるいはタップ)を検出し、向きを変えます。 makeTurn 関数については、「向きを変える処理」に書いています。

    ぴったりの位置にいない場合は方向転換はさせないで、 keepMoving 関数で、今移動している方向への移動をキープします。 keepMoving 関数については、「そのままの向きを継続する」に書いています。

    最後にボールの描画です。 通常時と、フラッグを獲った直後の2パターン用意しています。

    	if (treasure.flash < 1) {					// フラッグ獲得直後ではないとき
    	    ctx.drawImage(ball.img, block.offsetx * block.size, block.offsety * block.size, block.size, block.size);
    	}
    

    マップがスクロールしているので転がっているように見えるボールですが、 常に画面の中央に描画されているだけです。

    通常時は、treasure.flash が 0 ですので、 それを判定し、drawImage 関数で、 main 関数に入る前に設定している ball.img を描画します。 最初の引数2つは、描画する位置の左上の座標です。 block.offsetx や block.offsety はマス単位の座標が設定してありますので、 1マスのサイズ block.size を掛けて、ピクセル単位の座標に変換しています。 続く引数2つは、サイズです。 ball.img のサイズが何であっても、このサイズに拡大あるいは縮小されて描画されます。 この場合は、1マスにぴったり収まるように、1マスのサイズを指定しています。

    	else {	// フラッグ獲得直後のとき
    		treasure.flash--;
    		if (treasure.flash > block.size / 4) {
    			ctx.drawImage(treasure.img, block.offsetx * block.size, block.offsety * block.size, block.size, block.size);
    			ctx.fillStyle = '#ff6';
    		}
    		else{
    			ctx.fillStyle = '#400';
    		}
    
    		ctx.font = "36px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif";
    		var str = 'あと' + treasure.num + '個!';
    		var tx = (canvas.width - ctx.measureText(str).width) / 2;
    		var th = ctx.measureText(str).actualBoundingBoxAscent + ctx.measureText(str).actualBoundingBoxDescent;
    		var ty = (canvas.height - th) / 2;
    		ctx.fillText(str, tx, ty);
    
    		ctx.drawImage(ball.imgF1, block.offsetx * block.size, block.offsety * block.size, block.size, block.size);
        }
    

    treasure.flash が 1 以上に設定されているとき、ここを通ります。

    まずは、忘れずに treasure.flash をカウントダウンしておきます。 この値が1マスのサイズの 1/4 より大きいとき、treasure.img に読み込んだフラッグを描画しています。 比較する値に特に意味はありませんが、PC で適当な値として設定しています。 そしてこのあと描画するテキストの色を指定しています。

    上記以外の場合、つまりフラッグを獲って少ししたら、 フラッグは描画せず、このあと描画するテキストの色を変えています。

    そのあとは、残りのフラッグ数を画面に描画するものです。 「あと 2 個!」のような文字列が、画面に表示されます。 テキストの描画については、これより前の部分とほぼ同じです。

    最後には、色が違うボール画像 ball.imgF1 を描画します。 透過ありの png 画像ですので、先に描画したフラッグの上に重ねて描画されます。

    	requestAnimationFrame( main );
    }
    

    これはいつもの、main ループを続けるための関数です。

    ▲ページ先頭へ

    キー入力の受付け

    投稿 October 6, 2020

    メインループ直前の addEventListener 関数により、 キーダウン時には keydownfunc 関数が呼び出されるようにしています。

    function keydownfunc( event )
    {
    	var key_code = event.keyCode;
    
    	if ( key_code === 37 ) key.reserve = 'left';
    	if ( key_code === 38 ) key.reserve = 'up';
    	if ( key_code === 39 ) key.reserve = 'right';
    	if ( key_code === 40 ) key.reserve = 'down';
    
    	if ( key_code >= 37 && key_code <= 40 ){	// 使用するキーのみに限定
    		event.preventDefault();
    	}
    }
    

    引数で渡された event に、キー情報が入っています。 変数 key_code に入れている event.keyCode に、 押されたキーのコードが入っています。 おそらく1バイトの ASCII コードです。

    矢印キーだけを判断し、key.reserve に 押されたキーを記録しています。 キーコード 37 なら左矢印ですので、left という文字列をセットしています。 他のキーコードについても同様です。 なお、イコール記号3個で比較しているのは、C/C++ にはない表記ですが、型まで一致を意味しています。 イコール記号2つで比較すると、文字列の "37" でも真と判断されます。

    最後の if 文で、処理した4キーに限定し、 preventDefault 関数を呼び出しています。 これにより、特に上下キーについて、画面をスクロールさせる「デフォルトの処理」を行わないようにします。 これがないと、ゲーム中に上下キーを押したとき、 進む方向が変わるのと同時に、ページがスクロールしてしまいます。

    ▲ページ先頭へ

    マウスクリック(タップ)の受付け

    投稿 October 8, 2020

    メインループ直前の canvas.addEventListener 関数により、 マウスならクリック、タッチスクリーンならタップ時に onClick 関数が呼び出されるようにしています。

    PC で操作している場合は、クリックでゲームを開始させています。 また、ボール付近のクリックで一時停止と解除を行います。 操作は矢印キーです。

    スマホなどでも、ゲーム開始や一時停止・解除は同じです。 タップはマウスクリック(左クリック)と同じ扱いになりますので、特別なコードは必要ありません。 操作は、キーがないデバイスのため、どこをタップしたかでキー入力の代わりとしています。

    function onClick( event )
    {
    	// ready 状態でクリックされた場合、ゲームを開始します。
    	if (state === 'ready') {
    
    		// 自動的に上に走行開始
    		key.push = 'up';
    		map.cy -= 1;
    		map.move = Math.abs(1 * block.size);
    
    		state = 'play';
    		return;
    	}
    

    まずは state が ready のとき、 すなわちゲーム開始前、ページロード直後です。 ゲームを開始にあたり、変数 key.push に up を設定し、上に転がり始めるように設定しています。 キー入力時に設定している key.reserve は、 次に曲がれるときに有効になる「先行入力」用ですが、ここでは実際に移動する方向を直接セットしています。 スタート位置の上のマスが「通れない」に設定されてしまうと、「刺さった」ような状態になるでしょう。

    座標としては y 方向に -1 となり、 滑らかにスクロールさせるための変数 map.move に、1マスのサイズを、ピクセル単位で設定しています。 絶対値 ( 符号を外した値です。1 でも -1 でも 1、2 でも -2 でも 2 です。 ) を求める Math.abs 関数を使う意味がありませんが、他の部分と書き方を合わせるために、こうしています。

    ボールの動きを設定したら、state を play、すなわちゲーム中に書き換えて、クリック関数を終了します。

    	// ポーズ中にクリックされた場合は、ゲームを再開します。
    	if (state === 'pause') {
    		state = 'play';
    		return;
    	}
    

    一時停止中state に pause が入っています。 単純に、state を play に書き換えて、クリック関数を終了します。

    	//  クリア後は、クリックは何もしません。
    	if (state === 'clear') {
    		return;
    	}
    

    フラッグを全部集めてクリア状態になると、 state には clear が設定されています。 このとき、盤面はすべて停止状態となっていますので、クリックでも何もしません。

    もう一度最初から始めたい場合は、ページをリロードする必要があります。 ここで初期化して、再スタートにしても良さそうです。

    	// ゲーム中以外の想定外の場合、何もしません。
    	if (state != 'play') {
    		return;
    	}
    

    あとは state には play 以外は設定されませんが、 念のため、play 以外の文字列が入っている場合は、何もしないようにしています。

    	// ゲーム中にクリックされた場合、スマホやタブレットだと矢印キーがないのでタップで操作します。
    	var rect = event.target.getBoundingClientRect();	// Canvas の絶対座標位置を取得
    	var mx = Math.floor(event.clientX - rect.left);	// rectが実数で返されているため
    	var my = Math.floor(event.clientY - rect.top);
    
    	var bx = Math.floor(mx / block.size) - 6;		// マス単位の座標に変換(13x13座標)
    	var by = Math.floor(my / block.size) - 6;		// -6 して、-6 〜 0 〜 +6
    

    ゲーム中は、 引数 event を参照して、どこがクリック(タップ)されたのかを特定します。 event.target は、イベントが発生した要素で、この場合は canvas です。 その getBoundingClientRect 関数を呼び出して、 canvas 自体が画面のどこに配置されているのかを取得しています。

    クリック(タップ)された座標は、event の clientXclientY に入っています。 これは、画面をベースとした座標のようですので、 canvas のどこがクリックされたのかを知るために、 canvas の左上の座標を引いて、canvas の左上を (0, 0) になるようにしています。

    canvas の座標をピクセル単位で計算した、mx、my を、 マス単位の座標に変換するため1マスのピクセル単位のサイズ block.size で割り、 ボールを中心とする、縦横 -6 〜 +6 の座標にしています。 (0, 0) がボールの位置となります。

    	// 中央部近く(ボールまわり1マス以内)ならポーズにします。
    	if (bx >= -1 && bx <= 1  && by >= -1 && by <= 1 ) {
    		state = 'pause';
    		return;
    	}
    

    ボールとその周辺の1マスがクリック(タップ)された場合、 state を pause に書き換え、一時停止となるようにしています。

    	// 操作可能領域?
    	if (bx < 0 && Math.abs(bx) > Math.abs(by)) {
    		key.reserve = 'left';
    	}
    	else if (bx > 0 && bx > Math.abs(by)) {
    		key.reserve = 'right';
    	}
    	else if (by < 0) {
    		key.reserve = 'up';
    	}
    	else if (by > 0) {
    		key.reserve = 'down';
    	}
    }
    

    ボールの周辺ではない場合、 ボールからの距離を調べ、遠い方向に進むように設定します。

    bx と by には、ボールを中心としたマス単位の座標 -6 〜 +6 が入っています。 1つ目の if 文で、横方向がマイナス、つまりボールより左で、 横方向の距離が縦方向の距離より遠い場合、左矢印キーが押されたのと同様に、 key.reserve に left を設定しています。

    他の3方向も同様で、ボールの右のほうなら右に、上のほうなら上に、下のほうなら下に、 次の移動方向を予約するようにします。

    ▲ページ先頭へ

    向きを変える処理

    投稿 October 9, 2020

    ゲーム中、メインループで、マップがスクロール「ぴったり」になった場合、 先行入力されたキーを有効とするために、この makeTurn 関数が呼び出されます。 「ぴったり」とは、キー処理は1マス単位でしか処理を受け付けませんが、 マップのスクロールは1マスを複数回にわけて更新して滑らかに見せていますので、 その「分けてスクロールさせてい途中」ではない状態を言っています。 具体的には、map.move が 0 のときのことです。

    function makeTurn()
    {
    	var copy_key_push = key.push;
    
    	// 移動方向を決定します。
    	var dx = 0, dy = 0;
    
    	if ( key.reserve === 'left' ) {
    		dx = -1;
    		key.push = 'left';
    	}
    	else if ( key.reserve === 'up' ) {
    		dy = -1;
    		key.push = 'up';
    	}
    	else if ( key.reserve === 'right' ) {
    		dx = 1;
    		key.push = 'right';
    	}
    	else if ( key.reserve === 'down' ) {
    		dy = 1;
    		key.push = 'down';
    	}
    

    今進んでいる方向は、key.push に入っています。 キー入力があっても、その方向に進めない場合に戻せるように、copy_key_push にコピーしています。

    key.reserve に、ここまでに入力されたキーによる方向が設定されていますので、 実際に進む方向 key.push を設定し、x、y の差分を dx、dy に設定しています。

    これですぐに採用すると、壁に向かっていても進んでしまいますので、 通れる方向かどうかを判断しなくてはいけません。 なお、ちょっと見にくいですが(直したほうがいいです)、 キー入力がなかった場合は dx も dy も 0 なので、if 文は通らず、 return false で終わります。

    	// キーが押されたとき
    	if (dx != 0 || dy != 0){
    		map.cx += dx;
    		map.cy += dy;
    		map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size );
    

    キーが押されていると、移動方向が dx、dy に設定されていますので、 マス単位の座標 map.cx または map.cy が更新されます。 滑らかに移動させるための移動量 map.move も1マス分に設定します。 面倒な式になっていますが、実際には block.size にしかなりません。

    		var n = canGoThrough(block.offsety + map.cy, block.offsetx + map.cx);
    
    		// 通れない場所の場合
    		if (n == 1) {
    		    map.cx -= dx;		// 計算を戻します
    		    map.cy -= dy;
    		    key.prev = key.push;
    
    			// 元の動きをキープします。
    			key.push = copy_key_push;
    			map.move = 0;
    			return false;
    		}
    

    別に用意した canGoThrough 関数で、 ボールの進む先のマップ情報を取得し、変数 n に設定しています。 map.cx、map.cy は canvas 左上に表示するマップの座標が設定されていますので、 block の offsetx と offsety を加算して、ボールに位置に合わせています。

    ボールの進む先が 1 の場合、通れない部分ですので、加算した座標や進む方向をもとに戻します。 保存しておいた、進んでいた方向を書き戻して、関数を終了します。 map.move を 0 にしていますので、 (正確には次のメインループで)直進を継続することになります。

    		else {
    			key.reserve = '';	// 予約を解除
    			if (n == 2) {
    				getFlag(block.offsety + map.cy, block.offsetx + map.cx);
    			}
    		}
    		return true;
    	}
    

    通れる部分なら、キー予約 key.reserve をクリアします。 フラッグの場合は、getFlag 関数を呼び出し、 フラッグの獲得処理を行います。

    最後は、キーが押されていなかった場合のコードです。 自動走行中にキー入力があったかどうかを判断するため、 キー入力がなかったことを示す false を返しています。

    	// キーは押されていませんでした。
    	return false;
    }
    
    ▲ページ先頭へ

    そのままの向きを継続する

    投稿 October 9, 2020

    メインループで、マップがマス「ぴったり」の位置までスクロールした場合、 キー入力の参照が優先されますが、キー入力がなかった場合は、 この keeyMoving 関数が呼び出されます。

    function keepMoving()
    {
    	// 移動量(ピクセル)を決定して、移動します。
    	var nowMove = Math.min(oneStep, map.move);
    	map.move -= nowMove;
    
    	// 移動している方向により、座標を調整します。
    	if ( key.push === 'left' )	map.x -= nowMove;
    	if ( key.push === 'up' )	map.y -= nowMove;
    	if ( key.push === 'right' )	map.x += nowMove;
    	if ( key.push === 'down' )	map.y += nowMove;
    
    	// まだ移動途中の場合は、ここで終わります。
    	if ( map.move > 0 ){
    		return;
    	}
    

    1マスは正方形ですので、縦方向・横方向の移動中に関わらず、 一定のピクセル数 oneStep だけスクロールさせます。 oneStep の初期値は 4 で、「スロー」や「クイック」ボタンで値を変えられます。

    マップの移動量 map.move は必ず 0 ぴったりで終わりたいので、Math.min 関数で、 最大の移動量を map.move までとしています(2つの値の小さいほうを選択)。 つまり、端数がでた場合は、端数だけを処理する、ということになります。

    今、移動している方向は、key.push に入っていますので、 それにより、マップのピクセル単位の座標 map.x または map.y を更新します。

    まだスクロール途中であるなら、つまりマス「ぴったり」の位置まで到達していないなら、この関数の処理はここで終わります。

    	// ぴったりの座標では、キー入力による向き変えを優先します。
    	if ( makeTurn() == true ){
    		return;
    	}
    

    マスぴったりの座標になったときの処理です。

    まずはキー入力があったかどうかを調べます。 makeTurn 関数は、キー入力があればその向きを設定して true を返します。 詳細は、「向きを変える処理」にあります。 false が返った場合は、キー入力がなかった場合です。

    	// キー入力がなかった場合、今の向きで移動を続けます。
    	var dx = 0, dy = 0;
    
    	if ( key.push === 'left' )	dx = -1;
    	if ( key.push === 'up' )	dy = -1;
    	if ( key.push === 'right' )	dx = 1;
    	if ( key.push === 'down' )	dy = 1;
    
    	// とりあえず進みます。
    	map.cx += dx;
    	map.cy += dy;
    	map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size );
    

    変数 dx、dy に、今移動している方向により、移動量を設定します。 マップのマス単位の座標を更新し、移動量 map.move を設定します。 この場合も、実際には block.size しか入りませんので、式自体はあまり意味がありません。

    	// 移動先を確認します。
    	var n = map.map[block.offsety + map.cy][block.offsetx + map.cx];
    
    	// 通れる場合は、そのまま進みます。
    	if ( n == 0 ){
    		return;
    	}
    
    	// フラッグの場合は、ゲットします。
    	if (n == 2) {
    		getFlag(block.offsety + map.cy, block.offsetx + map.cx);
    		return;
    	}
    

    ボールの移動先のマップ情報を取得し、変数 n に設定します。 map は文字列を持つ1次元配列ですが、このように2次元配列的にアクセス可能です。 map.cx や map.cy は、canvas の左上のマップの座標なので、 block.offsetx や offsety を加算して、ボールの座標に変換しています。

    通れる場合は値が 0 ですので、そのまま進めます。 なお、実際には n は1文字を表しますが、イコール記号2つだと型判断なく比較できますので、これで問題ありません。 これでエラーにならない、というより正しい書き方(の1つ)であるため、注意力が下がりそうです。

    フラッグの場合は値が 2 ですので、進むと同時にフラッグをゲットします。 処理は、getFlag 関数で行い、完了したら、この関数も終わります。

    以降は通れない場所、値 1 と値 9 の場合です。 領域の外側か内側かだけですので、この2つの値に処理上の違いはありません。

    	// 余計に移動したぶんを戻します。
    	map.cx -= dx;
    	map.cy -= dy;
    	map.move = 0;
    	key.reserve = '';
    
    	dx = 0;
    	dy = 0;
    
    	var n1, n2;
    

    進めないので、ひとまずもとの位置まで戻します。 移動量 map.move もいったんクリアし、キーの先行入力も解除します。 他の変数も、先に用意しています。

        // 左に進んでいた場合
    	if (key.push === 'left') {
    		n1 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx);	// 上のマス
    		n2 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx);	// 下のマス
    		if (key.prev === 'down') {		// 下を優先したい場合
    			n1 = canGoThrough(block.offsety + map.cy + 1, block.offsetx + map.cx);	// 下のマス
    			n2 = canGoThrough(block.offsety + map.cy - 1, block.offsetx + map.cx);	// 上のマス
    		}
    

    左に進んでいた場合は、上または下に(自動的に)曲がります。 上のマスと下のマスが通れるかどうかを canGoThrough 関数で判定し、 変数 n1 と n2 に入れていますが、 この順に判断するので、下に進むことを優先するなら、逆に設定します。 変数 key.prev に、今の方向に向く前に進んでいた方向をキープしているつもりですが、 このあたりは少々怪しい感じもあります。

    		if (n1 == 0 || n1 == 2) {		// 上が通れる場合
    			if (key.prev === 'down') {	// 下を優先したい場合
    				key.push = 'down';
    				dy = 1;
    			}
    			else {
    				key.push = 'up';
    				dy = -1;
    			}
    			if (n1 == 2) {
    				getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx);	// フラッグなら取る
    			}
    			key.prev = 'left';
    		}
    

    ちょっとコメントがわかりにくいですが、 n1 には、上に進みたい場合は上の、下に進みたい場合は下のマップの情報が入っています。

    通れるので、key.push にこのあと移動する方向を設定し、dy にマス単位の移動量を設定しています。 そして進む先がフラッグの場合は、getFlag 関数を呼び出して、獲得処理を実行します。

    		else if (n2 == 0 || n2 == 2) {
    			if (key.prev === 'down') {	// 下を優先したい場合
    				key.push = 'up';
    				dy = -1;
    			}
    			else{
    				key.push = 'down';
    				dy = 1;
    			}
    			if (n2 == 2) {
    				getFlag(block.offsety + map.cy + dy, block.offsetx + map.cx + dx);	// フラッグなら取る
    			}
    			key.prev = 'left';
    		}
    

    第1のオプションに進めなかった場合、第2のオプションを調べます。 処理内容は、先ほどと基本的に同じです。

    		else {
    			key.push = 'right';	// バウンドは、必ずできる
    			dx = 1;
    		}
    	}
    

    上にも下にも進めない場合、そこは袋小路ですので、折り返されないといけません。 ここまで左に進んできたので、右に戻ることは、チェックしなくても大丈夫です。

    これと同じように、上、右、下に進む処理がありますが、内容的には同様ですので、ここでは省略します。

    	map.cx += dx;
    	map.cy += dy;
    	map.move = Math.abs( dx * block.size ) + Math.abs( dy * block.size );
    }
    

    設定した移動量 dx、dy をマップに反映させ、 またピクセル単位のスクロール量を map.move にセットして、終了します。

    ▲ページ先頭へ

    通れるかの判定とフラッグのゲット

    投稿 October 9, 2020

    指定のマスが通れるか通れないかを調べるための関数 canGoThrough は、 次のように定義されています。

    通れるマスのときは 0 を、フラッグのときは 2 を、 通れないマスなら 1 でも 9 でも区別なく、1 を返します。

    map は文字列の1次元配列ですが、このように2次元配列的に参照することができます。

    function canGoThrough( y, x )
    {
    	var n = map.map[ y ][ x ];
    	if ( n == 0 ){
    		return 0;		// 通れる
    	}
    	else if (n == 2) {
    		return 2;		// 通れる
    	}
    	return 1;			// 通れない
    }
    

    フラッグをゲットする処理は、次の getFlag 関数で行っています。

    function getFlag(y, x)
    {
    	map.map[y] = replaceValue(map.map[y], x, 0);
    	treasure.num--;
    	if (treasure.num == 0) {
    		state = 'clear';
    	}
    	else {
    		treasure.flash = Math.floor(block.size / 2);
    	}
    }
    

    フラッグのあるマスを、別途用意した replaceValue 関数で、 値 0 に置き換えて、ゲットしたこととします。 詳細は、「ボールを転がしてゴールを目指す」に書いています。

    マップ上の残りのフラッグ数 treasure.num をマイナス1し、 残りがなくなったら state を clear に書き換えて、クリア状態にします。

    まだある場合は、treasure.flash に適当な値を設定し、 少しの間、残りのフラッグ数を表示したり、ボールの色を変えたりする処理を行います。

    ▲ページ先頭へ

    マップのスクロール処理

    投稿 October 9, 2020

    このゲームでは、ボールは固定の位置にいるだけで、 マップがスクロールすることにより、ボールが移動しているように見せています。

    マップの描画は、メインループの最初のほうで呼び出される、showMap 関数が担当しています。

    function showMap()
    {
    	// 端数となるピクセル数を計算します。
    	//   x または y のどちらかのみが端数があり、どちらかは必ずぴったりになります。
    	var zx = 0, zy = 0;
    	if ( key.push === 'left' )	zx = -map.move;	// 左に移動中
    	if ( key.push === 'up' )	zy = -map.move;
    	if ( key.push === 'right' )	zx = map.move;	// 右に移動中
    	if ( key.push === 'down' )	zy = map.move;
    

    key.push に、現在移動している方向が設定されています。 また、map.move には、最大1マスまでの、ピクセル単位の移動量が入っています。 例えば1マスが 24 ピクセルなら、map.move は 24 からスタートし、 1ループごとに、20、16、12、8、4、0 と、(標準速度の場合)4 ずつ減っていきます。

    移動方向と map.move を参照し、マスぴったりの位置から、どちらにどれだけずれているかを、zx、zy に設定しています。

    大事なことは、上または下に進んでいるときにマップの端数が出るのは y 方向のみで、 x 方向にはマスぴったりであるようにしている、というところです。 左または右に進んでいるときは、マップの端数がでるのは x 方向のみで、 y 方向にはマスぴったりとし、いつでも自由に端数を出せるようにはしていません。

    	// 端数を表示するため、上下・左右で各1マスおおきくループしています。
    	for (var y = -1; y < block.h + 1; y ++){
    
    		if ( map.cy + y < 0 || map.cy + y >= 31 ){
    			continue;
    		}
    

    まずは、y 方向のループを設定しています。 画面は 13 × 13 マスで構成しており、block.h には 13 が入っています。 端数部分(スクロール途中でマス全体が表示されない部分)があるため、 上下とも1マスずつ大きめの値でまわし、 map 配列への不正なアクセスを行わないよう、範囲外を除外しています。

    continue は、C/C++ と同じように、 現在あるループの残りを行わないで、次のループに進むためのコマンドです。

    		for (var x = -1; x < block.w + 1; x ++){
    
    			// 念のため、範囲外を除外
    			if ( map.cx + x < 0 || map.cx + x >= 49 ){
    				continue;
    			}
    

    同じように、横方向、x 方向にもループを行っています。 block.w には、13 が入っています。 この2つのループの組み合わせで、描画したい全マスを網羅します。

    			// マス描画位置
    			var px = x * block.size + zx;
    			var py = y * block.size + zy;
    
    			// 項目別に描画
    			var n = map.map[map.cy + y][map.cx + x];
    
    			// 【マス】描画数による速度の変化を減らす目的で
    			if ( n == 0 ){
    				ctx.fillStyle = '#d81';
    				ctx.fillRect(px, py, block.size, block.size);
    			}
    
    			// 【マス】マップ外側
    			if ( n == 9 ){
    				ctx.fillStyle = '#444';
    				ctx.fillRect(px, py, block.size, block.size);
    			}
    
    			// 【マス】グリーン(壁)
    			else if ( n == 1 ){
    				ctx.fillStyle = '#8F8';
    				ctx.fillRect(px, py, block.size, block.size);
    			}
    
    			// 【マス】フラッグ
    			else if ( n == 2 ){
    				ctx.fillStyle = '#d81';
    				ctx.fillRect(px, py, block.size, block.size);
    				ctx.drawImage( treasure.img, px, py, block.size, block.size );
    			}
    		}
    	}
    

    変数 px、py に、 1マスのサイズ block.size を掛けた「基本の描画位置」に、 端数分 zx、zy を加味した座標 px、py を決定します。

    変数 n に、描画すべきマップ情報を設定します。 たびたびですが、map は文字列の1次元配列ですが、このように2次元配列的に参照することができます。

    そして項目別に描画します。 通れる部分は黄土色 #d81 で、マップの外側は暗いグレー #444 で塗りつぶしています。 通れない部分は グリーン #8F8 です。 この色の表記方法は、ウェブでよくみられる、R、G、B、すなわち赤、緑、青の順に、 強さを 16 進数1桁、0 〜 f で表したものです。

    背景色として、通れる部分を canvas のクリアで塗れば、マスごとに全部描画する必要はなくなりますが、 描画するマスの数が変動することで、スクロールする速度が安定しないと嫌なので、 無駄に見えますが、描画を行っています。

    フラッグについては、treasure.img に設定しておいた画像を描画しています。

    このあと、このまま下記「ミニマップを描画する」に続きます。

    ▲ページ先頭へ

    ミニマップを描画する

    投稿 October 9, 2020

    画面には全体マップのうちの 13 × 13 の部分しか見えませんので、 残りのフラッグが少なくなると、探すのが難しくなります。 これを解消するため、画面左上に小さい全体マップを表示しています。

    	var dotsize = Math.max(2, Math.floor(block.size / 10));	// 最低2ピクセル
    	ctx.fillStyle = '#FFF';
    	ctx.fillRect(20 - 2, 20 - 2, dotsize * (map.w - 12) + 4, dotsize * (map.h - 12) + 4);
    	for (var y = 6; y < map.h - 6; y++) {
    		for (var x = 6; x < map.w - 6; x++) {
    

    まず、dotsize に、ミニマップの1粒のサイズを決めています。 1マスのサイズの 1/10 を基本としていますが、 小さすぎると意味をなしませんので、最低 2 ピクセルに制限しています。 Math.floor は、引数の値を超えない最大の整数を返す関数です。 Math.max は、2つの引数のうち、大きいほうの値を返す関数です。

    領域全体を、#FFF、すなわち白で塗りつぶしています。 このとき、本当に描画するマップより、上下左右各2ピクセルずつ大きく領域を取っています。

    そのあと、すべてのマスを網羅するループを作成していますが、 マップの上下左右各6マスは「外側」として 9 を並べていますので、それは参照・描画しないようにしています。

    			var sx = 20 + (x-6) * dotsize;
    			var sy = 20 + (y-6) * dotsize;
    
    			// 項目別に描画
    			var n = map.map[y][x];
    
    			// マスに関係なく、自分の位置を描画
    			if (x == map.cx + 6 && y == map.cy + 6) {
    				ctx.fillStyle = '#ff8';
    				ctx.fillRect(sx, sy, dotsize, dotsize);
    				continue;
    			}
    

    sx、sy に描画位置を設定しています。 左上に各 20 ピクセルを固定値で設定し、事前に決めた dotsize ピクセルで描画します。

    変数 n にマップ情報を取得します。 ボールの位置では、マス n に関係なく、#ff8 で、つまり明るい黄色で描画しています。

    			// 【マス】描画数による速度の変化を嫌う
    			if (n == 0) {
    				ctx.fillStyle = '#200';
    				ctx.fillRect(sx, sy, dotsize, dotsize);
    			}
    
    			// 【マス】マップ外側(描画領域外)
    			else if (n == 9) {
    				ctx.fillStyle = '#444';
    				ctx.fillRect(sx, sy, dotsize, dotsize);
    			}
    
    			// 【マス】グリーン(壁)
    			else if (n == 1) {
    				ctx.fillStyle = '#484';
    				ctx.fillRect(sx, sy, dotsize, dotsize);
    			}
    
    			// 【マス】フラッグ
    			else if (n == 2) {
    				ctx.fillStyle = '#f88';
    				ctx.fillRect(sx, sy, dotsize, dotsize);
    			}
    		}
    	}
    }
    

    あとはマス別に、通れるなら #200、暗い赤で描画します。 同様に、外側(除外しているのでありませんが、設定自体は可能なため)なら暗いグレーで、 壁ならグリーンで、フラッグなら赤で粒を描画しています。

    ▲ページ先頭へ

    速度設定ボタン処理

    投稿 October 9, 2020

    デバイスの処理速度が画面サイズにより、ボールの動きが早すぎたり遅すぎたりして、 ゲーム性が下がる可能性があります。

    このプログラムでは、ウェイト処理などはなく、とにかく目いっぱいにメインループを回しますので、 あらかじめ処理速度がわかれば、初期値を調整すべきです。

    スロー」ボタンでは引数 2 で、 「標準速度」ボタンでは、初期値と同じ引数 4 で、 「クイック」ボタンでは引数 8 で、 この関数が呼び出されます。

    function speed( step )
    {
    	oneStep = step;
    }
    

    引数の値をそのまま oneStep に設定しています。 oneStep は、1回のループでスクロールさせる量(ピクセル)なので、 スローだと1マスぶん移動するまでの描画回数が標準の2倍になります。 クイックでは、標準の倍で移動します。

    ▲ページ先頭へ
    line
    関連トピックス
    line


    その他のおすすめ

    ウェブ開発に関するトピックは、「ウェブ開発トップ」にまとめられています。



    © 2017-2021 StraightApps.com 無断転載を禁じます。No reproduction without permission.