4. JS魔法修行: (2) ゲーム開発の基本
前の章ではプログラムを書くことのちょっとしたイントロをやりました。学ぶことは山ほどあるのですが、それらのうち、他のプログラミング言語でも 「イベントループ」 と呼ばれているものを私たちの旅の前半で学ぶ題材にしてみたいと思います。いってみればゲームの最中にずっと ぐるぐる回す ことになる、一種の処理のカタチです。
人間とそのかわりに仕事をする機械たちとの間でいったい何が起きているのか、この "イベントループ" という仕組みから大体のイメージを掴んでもらうことができればと思います。
4-1. 自由に関数を作ってみる
現在の JS はこうなっています。第四章なので app_04.js とします。
if (navigator.onLine) {
console.log('yes!');
} else {
console.log('no...');
}
ここに check という関数を作ってみます。
まずは何も考えず、こんなふうに書き換えてみて下さい。
まずは何も考えず、こんなふうに書き換えてみて下さい。
check();
function check() {
if (navigator.onLine) {
console.log('yes!');
} else {
console.log('no...');
}
}
やっていることを見てみましょう。
三行目を見てみましょうか。function というのが出てきました。
function は日本語で 関数 という意味です。function を使うと "自分だけの関数" を定義することができます。ここでは function を使って check という関数を作りました。 またさっきの数行はこの中に移動しました。
一番上を見てみます。
check();
check の後ろに () が付いていますね。こうすると 「check が 実行できる」 のです。
ファイル保存し、ブラウザを更新してみます。
今までとおなじく yes! とログ出力されます。ページが読み込まれると同時に app_04.js が読み込まれ、今定義した check が実行されるからです。
4-2. ボタンでプログラム実行
さて、「ボタンを押したら check が実行される」 というのをやってみましょう。
JS はいったん忘れ、HTML に戻ります。これが現在の HTML です。第四章なので web_04.html とします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Test</title>
<link href="style_04.css" rel="stylesheet">
<script type="text/javascript" src="app_04.js"></script>
</head>
<body>
<div class="extra-large">
<div class="color-1">Hello</div>
<div class="color-2">World</div>
<div class="color-3">You</div>
<div class="color-4">Are</div>
<div class="color-5">Beautiful</div>
</div>
</body>
</html>
ここに ボタンを追加 してみます。ボタンは <button> というタグで定義します。<body> が始まる真下です。
ちなみに onclick="check()" というの書かれていますが、これは 見なかったこと にしましょう。何も考えずに以下の HTML を書き写して下さい。
<body>
<button onclick="check()">Check</button>
<div class="extra-large">
<div>...</div>
<div>...</div>
....
HTML の方はこれでオッケーです。ボタンが配置されます。
また JS に戻ります。app_04.js を修正します。
今は一番上で check が実行されていますが、これが 実行されないようにするため に、まずはこの部分を 「コメントアウト」 します。これには // を使います。通常はプログラムの中でコメントを書くために使うものですが、これで check が実行されないようにできます。
// check();
function check() {
if (navigator.onLine) {
console.log('yes!');
} else {
console.log('no...');
}
}
これで app_04.js が読み込まれても check が実行されなくなる のです。
それではさっき "無視" した onclick="check()" について考えてみましょう。
<button onclick="check()">Check</button>
onclick という名前から想像します。"on" という英語は日本語だと 「〜のとき」 ですね。"click" は 「クリック」 です。そうすると "onclick" は 「クリックしたとき」 になります。これに check を指定しているので 「クリックしたときに check が実行される」 ことになります。
ボタンを押してみましょう。
開発者ツールに yes! とログ出力されます。
何度も押してみましょう。
押された "回数" が yes! の横に表示されます。
これは開発者ツールの仕様です。
押された "回数" が yes! の横に表示されます。
これは開発者ツールの仕様です。
4-3. イベントループ、それと let と const
慣れてきましたか? ちょっと複雑なことをしてみましょう。
現在はボタンを押すと check が一回だけ実行 されます。これを、何度も何度も 「永遠に実行される」 ようにしましょう。
(a) 処理ループを開始する
web_04.html を修正します。つまり HTML の方です。
まずは以下の <button> を消しましょう。
まずは以下の <button> を消しましょう。
そして新しい <button> を書きます。
<div> で囲んでいますが、これは現時点では意味はありません。
Start というボタンが表示されます。
次に app_04.js を書いていきましょう。新しく startCheck という関数を追加します。
// チェックを開始する関数
function startCheck() {
setInterval(check, 500);
}
// 実際にチェックする関数
function check() {
if (navigator.onLine) {
console.log('yes!');
} else {
console.log('no...');
}
}
startCheck 関数の中をみると setInterval というのが新たにが登場しました。これはいったい何でしょうね。
意味を推測する材料として以下があるでしょう:
- setInterval という英語
- setInterval(check, 500) という記述
setInterval 関数には二つの 「引数 (ひきすう)」 を渡しています。
第一引数は check を呼んでいるんだろう、ということが想像できます。
第二引数の方はちょっと想像力が必要です。
何でしょうね?
第二引数の方はちょっと想像力が必要です。
何でしょうね?
実はこれは "秒数" です。
といっても単位は 「ミリ秒」 です。
1ミリ秒というは一秒の 1/1000 のこと。
したがって 500 とすれば 0.5秒 を指定していることになります。
1ミリ秒というは一秒の 1/1000 のこと。
したがって 500 とすれば 0.5秒 を指定していることになります。
さて問題の setInterval とは何でしょう。
これは 「指定された関数を、指定されたミリ秒ごとに実行するもの」 です。
これは 「指定された関数を、指定されたミリ秒ごとに実行するもの」 です。
したがって今回のコードは
「0.5 秒に一回、check 関数を実行しろ」
という命令になります。
という命令になります。
ファイルを保存し、ブラウザを更新して下さい。
0.5秒に一回、yes! がログ出力されることを確認できます。
(b) 処理ループを解除する
せっかくなので続けてプログラムを完成させましょう。
まだ余裕はありますか?
まだ余裕はありますか?
今度はボタンを二つ にします。
まだ Start ボタンしかないので、今度は Stop ボタンを追加しましょう。
まだ Start ボタンしかないので、今度は Stop ボタンを追加しましょう。
web_04.html を修正します。
見た目はこんな感じになります。
Start ボタンの真下に Start ボタンが表示されるようになります。
HTML の方はこれで準備ができました。
次は JS を修正するので app_04.js を開きます。
stopCheck という関数を追加しましょう。
(いろいろ新しい記述が出てきますが無視して読み進めましょう)
次は JS を修正するので app_04.js を開きます。
stopCheck という関数を追加しましょう。
(いろいろ新しい記述が出てきますが無視して読み進めましょう)
let pokemon = null;
function startCheck() {
pokemon = setInterval(check, 500);
}
function stopCheck() {
if (pokemon) {
clearInterval(pokemon);
pokemon = null;
}
}
function check() {
if (navigator.onLine) {
console.log('yes!');
} else {
console.log('no...');
}
}
さてさて、ここでまた clearInterval という不可解な関数が登場しました。
でもこれはさっきの setInterval に似ていますね?
でも実はもっと重要なのが "プログラムの一番上" にあるコレです。
let pokemon = null
まあ、少しずつ見ていきましょう。
でも・・・
もうそろそろ、このあたりが 新たな挫折ポイント になりそうですか?
もうそろそろ、このあたりが 新たな挫折ポイント になりそうですか?
なんか複雑なことをやっている "気" がする・・・ もうついていけないと弱気になりそうです。ですが何度も言うように 知らないことを学んでいるのだから知らないのは当たり前 なのです。
一つずつ丁寧にみていけば 「あ、なんだ。そんなことか」 となるはずです。
(c) let と const
まずは let から見てみましょう。
これは 「変数」 を作るのに使います。
「変数」 というのは何でも中に入れられる、いってみれば "いれもの" のようなものです。
「変数」 というのは何でも中に入れられる、いってみれば "いれもの" のようなものです。
これを "宣言" するのに使います。
直感的に理解しやすそうな例を見てみましょう。
let secret = 9902; // 秘密のパスワード
if (secret === 9902) {
// もしパスワードが 9902 だったら金庫室に侵入できる
console.log('Success!');
} else {
// そうでなければ死亡
console.log('Game Over...');
}
何となく解りますか?
そして let でつくった変数は、途中で中身を "変更" することができます。
これも例をみてみましょう。
これも例をみてみましょう。
let girl = 'Tracy';
console.log(girl); // --> Tracy
girl = 'Kisha';
console.log(girl); // --> Kisha
girl = 'Diana';
console.log(girl); // --> Diana
girl = 9999;
console.log(girl); // --> 9999
さて、ややこしいことを言いました。
「 let でつくった変数は中身を変更できる」 ってどういうことでしょう?
実は 「中身を変更できない変数もある」 のです。
それは const で宣言した変数です。
const は中身を変更 できない 変数になります。
const は中身を変更 できない 変数になります。
const を使った例を見てみましょう。
const で宣言した変数をあとから変更しようとすると "怒られ" ます。
const で宣言した変数をあとから変更しようとすると "怒られ" ます。
const girl = 'Tracy';
girl = 9999; // ここで怒られる!
ややこしいですね・・・
- let ・・・ あとで 「変更する」 変数を作るときに使う
- const ・・・ あとで 「変更しない」 変数を作るときに使う
こんなふうに覚えておいて下さい。
そういうわけで girl はあとから中身を変更しますから、こっちは let で宣言するしかありません。
でもその前の let secret = 9902 があったじゃないですか。こっちは中身をあとから変更していますか? 変更していませんよね? ということはこっちは const で宣言してもいい訳です。
これは const secret = 9902 と書いてもいい、ということです。
これは const secret = 9902 と書いてもいい、ということです。
const secret = 9902;
if (secret === 9902) {
console.log('Success!');
} else {
console.log('Game Over...');
}
secret は途中で中身を変更しないので、const で宣言しても怒られません。
変数を宣言するときに、最近は const の方がよく使われます。
「やたらと中身を変えない」 というプログラミングの方法が人気だからです。
(これは Functional Programming と呼ばれます)
「やたらと中身を変えない」 というプログラミングの方法が人気だからです。
(これは Functional Programming と呼ばれます)
(d) 「登録番号」 について
今回のプログラムに戻りましょう。
let pokemon = null;
これがプログラムの一番上にあります。
これは pokemon という変数を "宣言" しています。
またそれと同時に null というのを入れています。
null とはいったい何でしょうか?
これは pokemon という変数を "宣言" しています。
またそれと同時に null というのを入れています。
null とはいったい何でしょうか?
難しいことを抜きにいえば null は 「何も入ってない」 という意味です。だから今回のプログラムでは let で pokemon を宣言しつつ、そこに 「何も入ってない、を入れている」 ことになります。
さてプログラムに戻ります。
let pokemon = null;
function startCheck() {
pokemon = setInterval(check, 500);
}
確か Start ボタンを押すと startCheck が実行されるのでしたね?
あれ?
startCheck の中身がさっきのとちょっと違います。
さっきは
さっきは
setInterval(check, 500)
だけでした。
でもこれが
だけでした。
でもこれが
pokemon = setInterval(check, 500)
になっています。
になっています。
実は setInterval を実行すると登録の結果として 「登録番号」 を返してくれ ます。
そしてここでは 返された 「登録番号」 を pokemon に入れている のです。
そしてここでは 返された 「登録番号」 を pokemon に入れている のです。
プログラムの開始時は pokemon に 「何も入っていない」 を入れています。しかし Start ボタンを押した瞬間、この中に 「登録番号」 が入るのです。そしてなんと、pokemon の中には ボタンが押されるまで、ずーっとおなじ登録番号が入ってる ことになります。
続きを見てみましょう。stopCheck 関数をみてみます。
function stopCheck() {
if (pokemon) {
clearInterval(pokemon);
pokemon = null;
}
}
ここでやっと clearInterval の説明に戻って来ることができます。
clearInterval とは何でしょうか?
想像力を働かせることにします。
setInterval はミリ秒単位で実行したい関数を登録するための関数でした。なのでおそらく clearInterval はそれを解除するもの なのでしょう。
clearInterval の "一つ手前" を見て下さい。
if (pokemon) {
これは条件分岐です。pokemon にちゃんと中身が入っているかを確認しています。
そしてこうなっています。
そしてこうなっています。
if (pokemon) {
clearInterval(pokemon);
pokemon に何かが入っているときだけ、clearInterval を実行されます。
clearInterval には pokemon という引数を渡しています。です。中に何が入っているんでしたっけ? 「登録番号」 でしたね。clearInterval がこれを受け取ると、その "登録番号" が登録されているかどうかを確認し、もし存在していれば 登録された処理ループを解除 します。
(e) 自分のケツを拭く
実はまだやることがあります。
pokemon という変数は われわれが勝手に作った変数 です。だからその中身は ちゃんと自分で空っぽにする 必要があります。「何も入ってない」 にもどす、つまり最初のときのようにまた pokemon に null を入れておくのです。
pokemon = null
これによって、次回 Stop ボタンが押され、stopCheck が実行されても、もう pokemon の中身は何も入っていません。中身に何も入っていないため、もう if (pokemon) という分岐処理には入って来れません。そうすると、登録された処理がないのに無駄に clearInterval が実行されることがなくなります。
ファイルを保存し、ブラウザを更新してみましょう。
Start ボタンを押すと 0.5秒 に一回 yes! とログ出力されます。
ネットワークに繋がっているからです。
ネットワークに繋がっているからです。
ネットワーク接続を切ってみましょう。
0.5秒 に一回 no... と出力されます。
どうでしょうか?
プログラムらしいプログラムになりました!
長い解説でしたけれど、、、
長い解説でしたけれど、、、
でもその価値はあったと思いません?
(f) バグを修正する
でもこのプログラム、実は重大なバグがあります・・・
Start ボタンを二回押すと、ミリ秒単位の関数実行が2つ登録されるのです。
二回目が押されるとき、確かに pokemon には一番最後 (二つ目) の登録番号が入ります。でもひとつ前 (一つ目) の登録番号は上書きされ、この世から消えてしまいます。
そして Stop ボタン。
Stop ボタンを押すと、最後に登録されたほうは確かに解除されます。
Stop ボタンを押すと、最後に登録されたほうは確かに解除されます。
でも最初のやつは、ずっとぐるぐる回ったままです。
だってもう一つ目のほうの登録番号はこの世から消えて、誰も覚えていないのですから。
これではどうやっても一つ目の方を解除する事はできません、、、
だってもう一つ目のほうの登録番号はこの世から消えて、誰も覚えていないのですから。
これではどうやっても一つ目の方を解除する事はできません、、、
let pokemon = null;
function startCheck() {
// 何度も pokemon が上書きされてしまう
pokemon = setInterval(check, 500);
}
修正版はこんな感じになります。
let pokemon = null;
function startCheck() {
if (!pokemon) {
pokemon = setInterval(check, 500);
}
}
「Start ボタンが押されたとき、すでに登録がないときだけ、登録する」」 という条件を入れました。
こうすれば、Stop ボタンが押されたとき、一つ前の登録番号が消されることはなくなります。
こうすれば、Stop ボタンが押されたとき、一つ前の登録番号が消されることはなくなります。