このサイトでは、分析、カスタマイズされたコンテンツ、および広告に Cookie を使用します。このサイトを引き続き閲覧すると、Cookie の使用に同意するものと見なされます。
Hi, Developers,
straightapps.com ロゴ
作成 September 18, 2020、最終更新 October 5, 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-move-ball.html
    )
    が、このページの URL になっています。 QR コード読み取りアプリで読み取ると便利です。


    この手のゲームには、いろいろな仕組みが考えられますが、 このページの目的は、複雑なコードではなく、JavaScript の練習なので、 メモにとどめます。

    壁のバリエーションとして、以下のようなものが基本でしょう。

  • 一方通行の壁(または通れる方向が決まっている)
  • 一度ぶつかると壊れる壁
  • 複数回ぶつかると壊れる壁
  • ある決まった時間だけ現れる壁
  • 床のバリエーションとしても、基本的なものがあります。

  • ブレーキがかかる床(ぶつかるのではなく上で止まる)
  • 矢印の向きに勝手に方向転換する床
  • 別の場所にワープする床
  • ゴールも、複数のバリエーションが考えられます。

  • (得点の異なる)複数のゴールがある
  • 順番に複数のゴールを巡る
  • もっと複雑にもできます。

  • 複数のボールがあり、それぞれ決まったゴールに誘導する
  • トンネルや橋、「上の段」などの3D化
  • ▲ページ先頭へ

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

    投稿 September 17, 2020

    ここから先は、上記、canvas を使ってボールを転がすプログラム開発を行った際に調べたりした JavaScript コードについての情報です。

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

    まずは、HTML 部です。

    <p id="message"></p>
    
    <canvas id="canvas-main"></canvas>
    
    <!-- 扱う要素が揃ったあとに読み込み -->
    <script type="text/javascript" src="js/moveball.js"></script>
    
    <button onclick="reset(0)" title="ボールリセット"> ボールを初期位置に戻す </button>
    <button onclick="reset(1)" title="マップリセット"> マップリセット </button>
    <button id="dev-button" onclick="devMode()" title="開発者モード"> 開発者モードにする </button>
    

    p id="message" には、ゴールしたとき「おめでとう」を表示するテキスト行です。

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

    script の読み込みのあとにあるボタン類は、押されたときに JavaScript 関数を呼び出すようにしています。

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

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

    ▼ セクション一覧

    JavaScript コードのロード位置
    ページ読み込み時の処理
    マップの初期化(配列操作)
    メインループ
    キー入力の受付け
    マウスクリック(タップ)の受付け
    ボールの移動処理
    ボタン処理
    開発者モードでのクリック(タップ)受付け


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

    ▲ページ先頭へ

    JavaScript コードのロード位置

    投稿 October 4, 2020

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

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

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

    moveball-init.js にあるのは上記のものだけです。 変数 scr に、新しく作成した Object クラスのオブジェクト ( インスタンスと言ったりします。 ) は、C/C++ で言うところの構造体のようなものです。 そのあとすぐに値を設定していますが、 メンバー変数を定義することなく、自由に利用できるので簡単ですが、 正しく利用しないと発見が難しいバグが生まれることとなるでしょう。 意図した動作にならないときは、変数名を疑う必要がありそうです。

    ほぼすべての機能が実装されている moveball.js は、 body タグ内でロードされています。 これは、HTML の body を先頭から読み込んでいき、 そのコードに到達したときに読み込まれる、というものです。

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

    ここでは、これより前で定義された p id="message"canvas id="canvas-main" にアクセスできます。

    なお、JavaScript のコードは、head や body タグ内に直接記述することも可能です。 機能テストには有効かもしれませんが、ごちゃごちゃになるので、おすすめできる方法ではありません。 また、HTML タグの onClick などに直接コードを書くこともできるようですが、小規模でない限りは、混乱のもとになるかと思います。

    <script type="text/javascript">
    <!--
    	<!-- JavaScript コードを記述 -->
    // -->
    </script>
    <noscript>
    	<!-- JavaScript が無効の場合の注意書きなどを記述 -->
    </noscript>
    

    コードの内容としては、 scr のメンバーに、ブラウザの描画可能領域の幅と高さを、ピクセル単位でセットしています。 innerWidth と innerHeight が Internet Explorer で動作しない、という記述がネットにありましたので、 clientWidth も取得していますが、IE 11 では問題なく動作しているようです。

    ▲ページ先頭へ

    ページ読み込み時の処理

    投稿 October 5, 2020

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

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

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

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

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

    まずは、canvas のサイズを決定しています。 固定のピクセル数にしてしまうと、PC では小さくなったり、 あるいはスマホやタブレットでは画面からはみ出す、と不便になってしまいます。

    var canvas = document.getElementById('canvas-main');
    var w = scr.innerWidth;				// クライアント領域の幅
    var h = scr.innerHeight;			// クライアント領域の高さ
    

    まずは変数 canvas に、canvas 要素を取得します。 また、このあと画面に合わせて調整するため、 変数 w にブラウザの描画可能領域の幅を、 変数 h にブラウザの描画可能領域の高さを設定します。 scr オブジェクトは、moveball-init.js で定義・設定しています。

    var rect = canvas.getBoundingClientRect();	// Canvas の絶対座標位置を取得
    w -= Math.floor( rect.left );			// rectが小数で返されている
    w = Math.min( 480, w );				// 十分広い場合は 480 ピクセルまでとします
    

    canvas の getBoundingClientRect 関数は、 canvas が描画される領域を、rect、すなわち左、右、上、下の座標で返す、というものです。 詳しくは知らないのですが、ブラウザのクライアント領域に対する座標が返るようです。 コメントには「絶対座標位置」と書いていますが、表現は正しくないような気がします・・・。

    left と top は すでに決まっています ( right と bottom はサイズ未指定のため、デフォルトの値となると思われますから、何になるかわかりません。 ) ので、横幅として、ブラウザ(スマホの場合は画面)の右端までに収まるサイズは、 left の位置を引いたものになります。 ここで、rect には小数を含む値が返されているため(OpenGL のような座標系を変換したのでしょうか?)、 Math.floor 関数で、 小数部を切り捨てて ( 正の値の場合は切り捨てですが、負の値の場合はそうではありませんので、ご注意ください。 ) います。

    最後に、もし PC など画面がかなり広いケースに描画サイズが大きくなりすぎないよう、 480 ピクセルまでに制限しています。 Math.min 関数で、もとの w と 480 の小さいほうの値が w に設定されます。 スマホなどの場合で、横が 480 ピクセルより小さい場合は、その値が採用されます。 これで、スマホでも画面内に収まるようになります。

    var blockSize = Math.floor( w / 24 );		// 24コマ表示可能とします
    w = blockSize * 24;
    

    変数 blockSize に、1マスのサイズを入れています。 1マスは正方形とし、canvas 内にヨコ24マス、タテ24マス表示できるサイズとします。 1マスのサイズが整数となるように、Math.floor 関数を使用しています。

    if (h > blockSize * 24){			// 高さ方向が十分なサイズなら
    	h = blockSize * 24;			// 24コマ表示できればOK
    }
    else if (h < blockSize * 24){			// 高さが不足の場合
    	blockSize = Math.floor( h / 24 );	// 縦に24コマ表示可能とします
    	h = blockSize * 24;
    	w = blockSize * 24;			// 横も24コマサイズに合わせます
    }
    
    canvas.width = w;				// canvas の幅設定
    canvas.height = h;				// canvas の高さ設定
    

    1マスのサイズから、canvas の高さ h を決定しますが、 横は収まるものの縦に収まらない可能性(ブラウザの描画可能領域が横長の場合)を考慮し、 if 文で判断しています。

    変数 h には、今、描画可能領域の高さが入っていますので、 24マス表示するのに十分なサイズであれば、24マス表示のサイズに設定します。

    ぴったり、または不足している場合は、else if 文での判断になります。 ここでは不足している場合だけを判断し、不足しているなら 1マスのサイズを縦方向から再計算し、幅を再設定して、小さいほうに合わせています。

    ぴったりの場合はそのままでいいので、計算できた w と h を canvas に設定し、 canvas のサイズを決定しています。

    canvas に描画するためのコンテキストの取得はいつもこうですので、先に進みます。

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

    var ball = new Object();
    ball.img = new Image();
    ball.img.src = 'jsimg/pin.png';
    ball.cx = 0;					// キャラクタベースの座標
    ball.cy = 0;
    ball.x = 0;					// ピクセルベースの座標
    ball.y = 0;
    ball.width = 100;				// pin.png のサイズ
    ball.height = 100;
    ball.move = 0;
    

    変数 ball に、新しいオブジェクトを作成します。 Object は、C/C++ で言う構造体と同じようなものです。

    その img メンバーに、新しい Image クラスのオブジェクトを作成します。 改めて、Object は事前にメンバー変数の定義などは必要なく、 自由に作成・参照できる、大変便利ですが、大変危険なものです。

    Image のオブジェクト img の src プロパティにファイル名を設定し、画像データとします。 あとでコンテキストの drawImage 関数でボールを描画するときに参照します。 設定したときにすぐにイメージが読み込まれているのか、初回アクセス時に読み込まれているのかは、 詳しく調べていないため、わかりません。

    以降は変数の初期化です。cx、cy はキャラクタベース、すなわちマス単位の座標としています。 x、y はピクセル単位の座標としています。滑らかに動かすために、別にしています。 width と height は、pin.png の画像サイズです。 img からわかるかもしれませんが、動作優先のため、自分で設定しています。 move は、「滑らかな移動量」です。

    続く goal オブジェクトも、ゴールを定義するためで同様です。

    ここではそのあとの「キー入力用オブジェクト」を参照します。

    var key = new Object();
    key.up = false;
    key.down = false;
    key.right = false;
    key.left = false;
    key.push = '';
    

    このプログラムでは、キー入力は即時に参照するわけではなく、 「滑らかな移動」の「区切りがいいところ」だけで有効となります。 ですが、区切りがいいところでキーが押されたときのみ有効では、操作が難しくなります。 ですので、「滑らかな移動」中にキーが押された場合に、これらのフラグにセットするようにしています。 変数 push は、滑らかに移動している方向を保持します。

    次はマップの定義です。 文字列を持つ 1次元配列 ( C/C++ 同様、map[0] や map[1] のようにアクセスできるものです。 ) を定義し、2次元配列的に扱っています。

    1文字が1マスの情報を表し、1要素が24文字で24マスを表します。 それが24要素ありますので、全体で縦横24マスが表されます。 あとで初期状態の壁を設定しますので、すべて0、つまり通れるマスとしています。

    var map = [	"000000000000000000000000",	// 1
    		"000000000000000000000000",
    		"000000000000000000000000",
    		"000000000000000000000000",
    		"000000000000000000000000",	// 5
    <!-- 同じような定義は省略 -->
    		"000000000000000000000000",
    		"000000000000000000000000",
    		"000000000000000000000000",	// 24
    ];
    

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

    変数 developerMode を false に設定し、 マップを編集できる「開発者モード」ではないことを示しています。

    var developerMode = false;
    

    最後に、このあとに用意した makeMap 関数を呼び出して、マップ map[] を初期状態にします。

    makeMap();
    

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

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

    マップの初期化(配列操作)

    投稿 October 5, 2020

    冒頭部では、1次元配列 map を24要素用意し、 それぞれの要素は24文字の文字列となっていました。 本来は2次元配列で作りたいのですが、 知識不足によりうまく作成・設定する方法が見つかりませんでしたので、 比較的簡単に扱える、1次元配列としています。

    まずは上下左右の4辺を壁にして、ボールが盤面から出ないようにします。 すべてを 0 で初期化していて、それは「通れる」と決めましたので、 通れない壁は 1 とすると決めます。 実際には文字列ですから、数字でなくても問題ないはずです。 今は、通れる部分をスペースにすれば、もっと見やすかったなと思います。

    function makeMap()
    {
    	for (var y = 0; y < 24; y ++){
    		if (y == 0 || y == 23){
    			map[y] = "111111111111111111111111";	// すべて壁
    		}
    		else{
    			map[y] = "100000000000000000000001";	// クリア
    		}
    	}
    

    1次元配列へのアクセスは、C/C++ と同じように、map[0] のようにします。 インデックス値 ( 大カッコ内に書く数字のことです。 ) は [0] から始まり、24個用意した場合は最終が [23] です。

    文字列を入れている、それぞれの map 要素は、C/C++ でいう char [24] ではなく、 MFC ( Microsoft Foundation Classes。Windows デスクトップアプリ開発用の C/C++ のマイクロソフト提供のクラス群。 ) CString ( 便利な文字列操作クラスです。Android アプリ用には、「C/C++ による文字列操作 CString を代用」で触れています。 ) に似ていますので、上記のように新しい値を直接代入可能です。

    一番上と一番下のはすべて 1、すなわち壁とし、その間の中間部分は左端と右端だけ壁になるようにしています。

    ここからは、面ごとに変わるべきマップの初期化なのですが、 今はまだ1面も2面もなく、1種類しかありません。

    	ball.cx = 1;					// キャラクタベース座標
    	ball.x = ball.cx * blockSize;			// ピクセルベース座標
    	ball.cy = 1;
    	ball.y = ball.cy * blockSize;
    

    ボールの初期位置を cx、cy にセットしています。 面によっては、別の場所からスタートできるように、この関数内に入れています。

    x、y は、ボールを滑らかに移動させるためのピクセル単位の座標で、 初期状態はマスの位置とぴったりになるように設定しています。

    次の部分のコードでは、マップの指定のマスを壁に書き換えています。

    	map[1] = replaceValue( map[1], 12, 1);
    	map[1] = replaceValue( map[1], 17, 1);
    	map[2] = replaceValue( map[2], 2, 1);
    	<!-- 同様の処理は省略 -->
    	map[18] = replaceValue( map[18], 12, 1);
    	map[19] = replaceValue( map[19], 2, 1);
    	map[20] = replaceValue( map[20], 17, 1);
    	//map[12][2] = '1';				// 参照はできるが設定はできない。
    

    まず先に、最後の行にあるように、2次元配列のような指定では、部分書き換えはできません。 JavaScript としては、あくまで「文字列を持つ1次元配列」なので当然にも思えますが、 char[] を持つ1次元配列なら C/C++ では設定可能ですし、 何より JavaScript でも、この形で読み出しはできるので、なんだか微妙です。

    とにかく2次元配列のような書き方による書き換えはできませんでしたので、 replaceValue 関数を用意しました。

    function replaceValue( mapstr, pos, value )
    {
    	var m = mapstr.substr(0,pos) + value + mapstr.substr(pos + 1);
    	return m;
    }
    

    replaceValue 関数は、 引数で文字列 mapstr、書き換える文字の位置 pos、書き換える新しい値 value を受け取り、 書き換えた文字列を返します。 JavaScript の関数は、C/C++ でいう 参照渡し ( 引数を func( int& n ) と定義して n を書き換えれば、呼び出し側から func( a ); とするだけで、呼び出し側の a の値が更新されるものです。 ) ができませんので、こうなっています。

    内容は、変数 m に、mapstr の「対象の文字より左側」を substr 関数で設定し、 それに新しい文字を連結、さらに「対象の文字より右側」を substr 関数で連結して返します。 value は数字として渡されていますが、連結時に型が揃えられて、文字列として連結されます。 返された文字は map[] に書き戻されますので、結果として部分更新されたことになります。

    なお、ここでは使用していませんが、 1次元配列の場合は、map.length に要素数が入っています。 2次元配列の場合では、map.length で1次元目の要素数が設定されているようです。 1次元配列については、 「素因数分解トレーニング」の 「【基礎】可変サイズの配列と操作」に書いています。

    最後に、ゴールの座標を設定して、終わります。

    	goal.cx = 10;
    	goal.cy = 10;
    	map[goal.cy] = replaceValue( map[goal.cy], goal.cx, 2);
    }
    

    改めて、map 配列の座標 10,10 は、画面で言えば左上から 10,10 番目ではなく、11,11 番目となります。

    ▲ページ先頭へ

    メインループ

    投稿 October 5, 2020

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

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

    まず canvas 全体を塗りつぶしています。 fillStyle 関数で、背景色を指定しています。 この場合は、rgb のカッコ内に、R、G、B、すなわち赤、緑、青の要素順に、 それぞれ 0 〜 255 の値を指定できます。 この場合は全体に明るく、緑が強い色となり、この色が、通れる部分になります。 塗りつぶしを実行している fillRect 関数には、 左上の座標と、右下の座標を指定しています。 canvas のサイズが width と height に入っていますので、これを使い、 canvas 全体としています。

    続いて、通れないマス(壁)と、ゴールに色を付けています。

    	for (var y = 0; y < 24; y ++){
    		for (var x = 0; x < 24; x ++){
    			if (map[y][x] == 1){
    				ctx.fillStyle = '#fee';
    				ctx.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
    			}
    			if (map[y][x] == 2){
    				ctx.fillStyle = '#f00';
    				ctx.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
    			}
    		}
    	}
    

    y のループは、1次元配列 map[y] のループです。 ループして全マスの値を参照し、壁やゴールなら色を付ける、としています。 すぐある x のループは、文字列 map[y] の1文字1文字を調べ、色付けを行うためのものです。

    ここで、map[y] は24文字の文字列を持つ1次元配列の1要素です。 上記のように、map[y][x] のように、文字列に対して配列の表記を行うと、 map[y] の x 文字目 ( 一般的な日本語でいう1文字目は x=0、2文字目は x=1 と指定します。 ) にアクセス可能です。 ただし、先に書いたように、ここに値を代入して文字列を書き換えることはできませんでした。

    また、文字列の1文字を取り出しているので本来は文字列で比較すべきですが、 JavaScript には型がありませんので、数値で比較しています。 実は内部的にはある型の一致まで比較するには、イコールを3個並べて、 map[y][x] === '1' のように記述すべきだそうです。

    この部分のコードとしては、ループでマスを1つづつ確認し、 1 なら #fee の色で、2 なら #f00 の色でマスを塗りつぶす、というものです。 1 は壁、2 はゴールと、事前に決めています。 この場合の色指定は、ウェブでよく使われている書き方で、 # の文字の後、R、G、B、すなわち赤、緑、青の強さを、16 進数1文字、0 〜 f で指定するものです。

    	addEventListener("keydown", keydownfunc, false);
    	addEventListener("keyup", keyupfunc, false);
    

    ここでは、キーダウンイベント(キーが押された)で、 別途用意した keydownfunc 関数を、 キーアップイベント(キーが離された)で、 別途用意した keyupfunc 関数を呼び出すというものです。

    ループ内に入れてありますが、この記述はループの外にあってもいいようです。 ループの外に置いた場合は、ここを通ったあと、 ハンドラー ( ここでは keydownfunc や keyupfunc、「取り扱う関数」のことです。 ) がロードされる前に実行されてしまうと、きっとエラーになると思います。 小さいコードでは試せないので、どうなるか確実ではありませんが。

    ボールの移動に関する2ブロックはあとで触れることとして、最後の部分を確認しておきます。

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

    先ほどのキーイベントと同じで、 クリック(マウスなら左ボタンが押されて離された、タップなら画面に触れて離された)されたとき、 別途用意した onClick 関数 を呼び出すというものです。 ただし、canvas に対しての設定ですので、canvas 上でのクリックのみが対象となるようです。

    	ctx.drawImage( ball.img, ball.x, ball.y, blockSize, blockSize );
    	//ctx.drawImage( ball.img, ball.x, ball.y );
    

    canvas のコンテキスト ctx を用いて、 drawImage 関数でボールを描画します。

    ball.img にはあらかじめ pin.png が設定されていますので、 ball.x、ball.y の位置に、blockSize サイズの正方形に収まるように拡大あるいは縮小されて、描画されます。 透過部分ありの png 画像なら、ちゃんと透過されます。

    	requestAnimationFrame( main );
    }
    

    最後に、ループを無限に続行するように記述して、main 関数は終わりです。

    ▲ページ先頭へ

    キー入力の受付け

    投稿 October 5, 2020

    メインループ内で、addEventListener 関数により、 キーダウン時には keydownfunc 関数が、 キーアップ時には keyupfunc 関数が呼び出されるようにしました。

    キーが押されたときの関数は、次のように記述されています。

    function keydownfunc( event )
    {
    	var key_code = event.keyCode;
    	if( key_code === 37 ) key.left = true;
    	if( key_code === 38 ) key.up = true;
    	if( key_code === 39 ) key.right = true;
    	if( key_code === 40 ) key.down = true;
    	goal.point = 0;							// ゴールしたあとの再ムーブ可能
    	event.preventDefault();
    }
    

    引数で受けられる event にキー情報が入っていますので、event.keyCode で押されたキーの情報を取得できます。

    カーソルキー(矢印キー)は ASCII コードで 37 〜 40 に割り当てられていますので、 それぞれを判断し、おされていた場合は対応する key オブジェクトのメンバーに true を設定しています。 これはいつでも受け付けていますが、実際にボールの動きに反映されるのは、 ボールが壁にぶつかって止まってからとなります。

    最後にある preventDefault 関数呼び出しは、ブラウザによるかもしれませんが、 上下キーだとページをスクロールさせる動作になっていたりしますので、 それを抑止する、というものです。 つまり、キー入力を先にもらって、ブラウザにはデフォルトの処理をさせない、ということです。

    なお、この書き方は良くないようで、 canvas が画面外に出たとしても、(この説明部分を)上下キーでスクロールさせることはできませんし、 (Chrome ブラウザの場合)F5 キーでページを再読み込みすることもできません。 必要なキーが押されたときのみ、デフォルトの動作を抑止すべきです。

    設定された値の参照は、あとで解説します。

    キーが離されたときの関数は、次のように記述されています。

    function keyupfunc( event )
    {
    	var key_code = event.keyCode;
    	if( key_code === 37 ) key.left = false;
    	if( key_code === 38 ) key.up = false;
    	if( key_code === 39 ) key.right = false;
    	if( key_code === 40 ) key.down = false;
    }
    

    押されたときと同様、event.keyCode にキー情報が入っていますので、 カーソルキーを判定し、フラグをクリアしています。

    つまり、ボールが移動中にキーが押されても、止まる前に離されれば、キーは効かない、ということです。 間違えて押しても、すぐに離せばキャンセルになる、というイメージです。

    ▲ページ先頭へ

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

    投稿 October 5, 2020

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

    PC ではキーがあるのでクリック処理は不要ですが、 キーだけだとスマホやタブレットなどのキーがないデバイスでは操作できません。

    関数には、引数でクリック情報が渡されますので、 それを参照して、処理を行います。

    function onClick( e )
    {
    	var rect = e.target.getBoundingClientRect();	// Canvas の絶対座標位置を取得
    	mx = Math.floor( e.clientX - rect.left );	// rectが小数で返されている
    	my = Math.floor( e.clientY - rect.top );
    
    	var cx = Math.floor(mx / blockSize);
    	var cy = Math.floor(my / blockSize);
    

    ここでは仮引数名が e となっていますが、event で統一しても良さそうです。

    まずは rect にクリックされたターゲットの描画領域を取得しています。 canvas と e.target が同じだと思いますが、スクロール時に値が変わるのか、 スクロールさせても変わらないのかは、未確認です。

    変数 mx と my に、canvas 領域内の基準としたクリック位置を設定しています。 rect が小数で返っていますので、floor 関数で、整数化しています。 それを1マスのサイズ blockSize で割って、 変数 cx、cy にマス単位の座標を取得しています。

    	// 開発者モードの場合は、クリック(タップ)で壁をトグルさせます。
    	if (developerMode == true){
    		<!-- ここでは省略します。あとで触れます。 -->
    
    		// クリック(タップ)による移動は行いません。
    		return;
    	}
    

    ソースでは、ここに「開発者モード」用の処理が入っています。 開発者モードでは、マップをクリックすることで、自由に壁を作ったり、壁を除去したりできます。 詳しくは、あとで書きます。

    	// スマホやタブレットだと矢印キーがないのでタップで操作します。
    	var xdiff = cx - ball.cx;
    	var ydiff = cy - ball.cy;
    

    クリックされた位置がボールより左なら xdiff がマイナスの値に、 ボールより右なら xdiff がプラスの値になります。 同様に、クリックされた位置がボールより上なら ydiff がマイナスの値に、 ボールよりしたら ydiff がプラスの値になります。

    	key.left = false;
    	key.right = false;
    	key.up = false;
    	key.down = false;
    

    キー入力のフラグを、設定前にクリアします。 連続クリックで、2つのフラグが一度に true になることを防いでいます。

    	if (Math.abs(xdiff) > Math.abs(ydiff)){	// 横に移動
    		if (xdiff < 0)			key.left = true;
    		else if( xdiff > 0 )	key.right = true;
    		goal.point = 0;				// ゴールしたあとの再ムーブ可能
    	}
    	else if (Math.abs(xdiff) < Math.abs(ydiff)){	// 縦に移動
    		if (ydiff < 0)			key.up = true;
    		else if (ydiff > 0)		key.down = true;
    		goal.point = 0;				// ゴールしたあとの再ムーブ可能
    	}
    }
    

    Math.abs 関数は、絶対値を求める関数です。 つまり 1 でも -1 でも 1 が、2 でも -2 でも 2 が返るものです。

    最初の if 文で、ボールからクリックされた位置までの距離が、 横方向のほうが縦方向より遠かった場合は、左、または右キーが押されたのと同じ動作とします。 goal.point については、ゴール時の処理で触れます。

    次の else if 文では、縦方向のほうが横方向より遠かった場合の処理で、 上、または下キーが押されたのと同じ動作としています。

    key オブジェクトのフラグについては、 キーと同じように合わせていますので、処理の区分けはありません。

    ▲ページ先頭へ

    ボールの移動処理

    投稿 October 5, 2020

    いよいよ、メインループで実施している、ボールの移動処理についてです。 前半はキー入力の受け付け、後半は移動処理です。

    まずはキー入力の受け付け処理ですが、キーが有効なのはボールが停止しているときのみです。 ボールが移動中は ball.move に1以上の値が入っていることとしていますので、 この値が 0 のときのみ、処理が実行されることになります。

    	if ( ball.move === 0 ) {
    		var dx = 0, dy = 0;
    		if ( key.left === true ) {
    			dx = -1;
    			key.push = 'left';
    			key.left = false;
    		}
    		else if ( key.up === true ) {
    			dy = -1;
    			key.push = 'up';
    			key.up = false;
    		}
    		else if ( key.right === true ) {
    			dx = 1;
    			key.push = 'right';
    			key.right = false;
    		}
    		else if ( key.down === true ) {
    			dy = 1;
    			key.push = 'down';
    			key.down = false;
    		}
    

    key オブジェクトのメンバーでフラグを確認し、 これから移動しようとしている方向 dx または dy を設定しています。 同時に、key.push に移動方向を設定し、 その方向に進める場合は、壁にぶつかるまで進み続けるようにしています。

    		if (dx != 0 || dy != 0){
    			ball.cx += dx;
    			ball.cy += dy;
    			ball.move = blockSize;
    
    			var n = map[ball.cy][ball.cx];		// 進む先の情報
    			if (n == 2){				// ゴール位置
    				var elem = document.getElementById('message');
    				elem.innerHTML = '<span class="red strong" style="font-size: 200%;">おめでとう!</span>';
    				goal.point = 1;
    			}
    			else if (n != 0){			// 空欄以外
    				ball.cx -= dx;
    				ball.cy -= dy;
    				ball.move = 0;
    
    				key.push = "";
    			}
    		}
    	}
    

    キー入力があった場合、dx または dy が設定されますので、入力があった場合のみの処理となります。 キー入力がなければ、何もしません。

    いったん、ボールの位置 ball.cx と ball.cy に、キー入力があった方向に1マス進んだ値を設定します。 そしてピクセル単位の移動量 ball.move に、1マス分のピクセル数 blockSize を設定しています。

    変数 n に、進む先のマップを取得しています。 文字列の1次元配列の場合、このように2次元配列的にデータにアクセスすることができます(書き換えはできません)。

    まずは先が 2、すなわちゴールだった場合、p id="message" の要素を取得し、 その innerHTML を書き換えることにより、「おめでとう」表示をしています。 innerHTML によるテキスト要素の書き換えについて詳しくは、 「【基礎】HTML 要素のテキストの書き換え」 に書いています。

    逆に、移動先が 0 ではない、つまり壁であるなら、加算した移動方向を戻し、移動量 ball.move も 0 に戻します。 key.push もクリアして、停止状態を継続します。

    後半は、ボールの移動処理です。 残りのボールの移動量を表す ball.move が 1 以上の場合にのみ、実行されます。

    	if (ball.move > 0) {
    
    		// 最大4ピクセル移動します。
    		var nowMove = 4;
    		if (ball.move < 4){
    			nowMove = ball.move;
    		}
    		ball.move -= nowMove;
    
    		if ( key.push === 'left' )	ball.x -= nowMove;
    		if ( key.push === 'up' )	ball.y -= nowMove;
    		if ( key.push === 'right' )	ball.x += nowMove;
    		if ( key.push === 'down' )	ball.y += nowMove;
    

    1マスのサイズにかかわらず、いちどに4ピクセル移動することを基本としています。 1マスのサイズによっては4の倍数になっていないため、残りが4未満のときは、 そのピクセル数だけの移動として、ball.move が 0 になるようにします。

    移動量を決定したら、ball.move から差し引くと同時に、 key.push に設定している移動方向を参照し、ボールのピクセル位置 ball.x または ball.y を更新します。

    以下の部分では、「ゴールする前」かつ 移動量が 0 になった、すなわちマスぴったりまで移動し終わるときにのみ、実行とします。 マスとマスの間にいるときの移動処理は、下記を実行せずに終わりです。

    		if (ball.move == 0 && goal.point == 0){
    			var px = ball.cx;
    			var py = ball.cy;
    			if ( key.push === 'left' ){
    				px --;
    			}
    			else if ( key.push === 'up' ){
    				py --;
    			}
    			else if ( key.push === 'right' ){
    				px ++;
    			}
    			else if ( key.push === 'down' ){
    				py ++;
    			}
    

    変数 px と py にボールの位置をコピーし、今進んでいる方向により、次に(自動的に)進む座標を設定します。

    			if (px != ball.cx || py != ball.cy){
    				if (map[py][px] == 0){				// 進む先が 0(空欄)
    					ball.cx = px;
    					ball.cy = py;
    					ball.move = blockSize;
    				}
    				else if (map[py][px] == 2){			// 進む先がゴール
    					ball.cx = px;
    					ball.cy = py;
    					ball.move = blockSize;
    
    					var elem = document.getElementById('message');
    					elem.innerHTML = '<span class="red strong" style="font-size: 200%;">おめでとう!</span>';
    					goal.point = 1;
    				}
    			}
    		}
    	}
    

    念のため移動があることを確認したあと、進む先のマスを確認します。

    進む先が通れるマスであれば、その座標を採用して ball.cx、ball.cy に設定し、移動量を ball.move に設定します。 key.push は書き換えませんので、今と同じ方向に進むことになります。

    進む先がゴールなら、その座標を採用して ball.cx、ball.cy に設定し、移動量を ball.move に設定します。 同時に p id="message" の innerHTML を書き換え、「おめでとう」を表示します。 改めて、innerHTML によるテキスト要素の書き換えについて詳しくは、 「【基礎】HTML 要素のテキストの書き換え」 に書いています。

    ▲ページ先頭へ

    ボタン処理

    投稿 October 5, 2020

    canvas の下に、3つのボタンを用意しています。

    ボールを初期位置に戻す」ボタンを押すと、reset(0) 関数を呼び出し、ボタンを初期位置に戻します。

    マップリセット」ボタンを押すと、reset(1) 関数を呼び出し、(開発者モードで編集された)マップを初期状態に戻します。

    function reset(flag)
    {
    	// 動いていない時のみ有効とします
    	if ( ball.move === 0 ) {
    		ball.cx = 1;						// キャラクタベースの座標
    		ball.cy = 1;
    		ball.x = blockSize;					// ピクセルベースの座標
    		ball.y = blockSize;
    
    		var elem = document.getElementById('message');
    		elem.innerHTML = "";
    	}
    
    	// flag がセットされているときは、マップも初期化します。
    	if (flag == 1){
    		makeMap();							// マップのリセット
    	}
    }
    

    ボールが動いているときに初期位置に戻すとおかしなことも起きそうですので、止まっているときのみにします。 ここで直接 ball.cx と ball.cy を設定してしまっているので、 makeMap 関数でのみ初期位置を変更すると、正しい位置になりません。 makeMap で初期位置を設定するとき、この関数を呼ぶなど、更新するべきです。

    このときすでにゴール済みであれば、「おめでとう」が表示されてしまっていますので、 表示を消しておきます。

    「マップリセット」ボタンでは、引数の flag に 1 が設定されていますので、 次の if 文が成立します。 この場合は、ページのロード時と同じ「完全リセット」ですので、makeMap 関数を呼び出します。 ボールの位置なども、makeMap 関数の設定している位置で上書きされることになります。

    もう1つ、「開発者モードにする」ボタンを押すと、devMode 関数を呼び出し、マップを自由に編集できる、開発者モードにします。 開発者モードでは、ボタンのラベルが「開発者モードを終わる」に変わりますので、 開発者モードを終了させることができます。

    function devMode()
    {
    	var elem = document.getElementById('dev-button');
    
    	if (developerMode == true){
    		developerMode = false;
    		elem.innerText = ' 開発者モードにする ';
    	}
    	else{
    		developerMode = true;
    		elem.innerText = ' 開発者モードを終わる ';
    	}
    }
    

    開発者モードであるかどうかは、developerMode で判断できます。

    今開発者モードであるときは、フラグをオフし、ボタンのラベルを「開発者モードにする」に書き換えます。 ボタンに表示するテキストは、<button></button> に書いたものになりますので、innerText の書き換えで実現できます。

    今は開発者モードでないとき、フラグを設定し、ボタンのラベルを「開発者モードを終わる」に書き換えます。

    この関数はフラグを書き換えるのみで、実際の処理は次のセクション、クリック処理に実装されています。

    ▲ページ先頭へ

    開発者モードでのクリック(タップ)受付け

    投稿 October 5, 2020

    変数 developerMode が true であるとき、開発者モードになっています。 開発者モードとは、盤面のマスをクリック(タップ)することで、 通れる部分なら壁に、壁なら通れる部分に書き換えられる機能です。

    なお、現状ではそれを保存することはできません。そのときのみです。

    処理は、クリック処理関数 onClick の途中に実装されています。

    	// 開発者モードの場合は、クリック(タップ)で壁をトグルさせます。
    	if (developerMode == true){
    		var toggle = true;
    
    		if (cx == 1 && cy == 1){				// ボールの初期位置は除外
    			toggle = false;
    		}
    		if (cx == 0 || cy == 0 || cx == 23 || cy == 23){	// ふちは除外
    			toggle = false;
    		}
    
    		if (toggle == true){
    			if (map[cy][cx] == 0){
    				map[cy] = replaceValue( map[cy], cx, '1');
    			}
    			else if (map[cy][cx] == 1){
    				map[cy] = replaceValue( map[cy], cx, '0');
    			}
    		}
    
    		// クリック(タップ)による移動は行いません。
    		return;
    	}
    

    壁にする、壁を消す、壁にする、壁を消す・・・のように、オンとオフを繰り返すような処理を「トグルする」と呼んでいます。 トグルさせるかどうかを表す変数 toggle を、初期状態で true に設定しています。

    ただし、いくつかの特殊な場所を除外しています。

    まずは、固定のスタート位置 (1, 1) は、壁にできないようにしています。 壁にできないようにするには、toggle を false に設定します。

    また、上下左右の縁は、壁を消すと盤面外に出られてしまいますので、不許可としています。

    トグルしていい場所のとき、それが 0、すなわち通れるマスなら 1、すなわち壁に書き換えます。 書き換えには、makeMap 関数で使用した、replaceValue 関数を使います。 今度は第3引数に文字列を渡していますが、型のない JavaScript なので、どちらでも関係ありません。

    クリック関数はここで終了とし、ボールの位置と比較して進む方向を決める処理には進みません。 つまり、開発者モードでは、クリック(タップ)でのボールの移動はできません。 矢印キーによる移動は可能ですので、PC では編集しながらボールを動かすことができます。

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


    その他のおすすめ

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



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