UFOを飛ばそう

ここからは、いよいよUFOゲームを作っていきます。

こういったゲームを作るとき、大きく分けて以下のふた通りの方法があります。

  1. UFOやキャノン砲をDOM要素として作成し、その位置をJavaScriptで動かす。
  2. 絵を描く領域としてキャンバス(canvas)を用意し、そこにJavaScriptで自力で絵を描く。

今回のUFOゲームは、別にどちらの方法でも作れると思いますが、今回は2番目の、キャンバスに自力で絵を描く方法とします。ゲームを作るのなら、こちらの方が応用の幅が広いのでは、と思います。

canvasを貼る

自由に絵を描くための領域として、HTMLにはcanvas要素という要素があります。ここでは、幅200ピクセル、高さ100ピクセルのcanvas要素を貼ってみましょう。

リスト1: canvas要素
<canvas width="200px" height="100px" style="background-color:black;"></canvas>

width(ウィドゥス:幅)属性、height(ハイト:高さ)属性で、幅と高さを指定しています。デフォルトでは背景色が白で、白地のページに貼ったのでは見えないので、背景色として黒を指定しています。閉じタグは省略できません。

width属性とheight属性の値には、「200px」のように「px」が付いていますが、これはピクセル(pixel)を意味します。ピクセルというのは、パソコンの画面を構成する点(ドット)のことです。パソコン(テレビやスマホでも同じですが)の画面は、たくさんの光る点で構成されていて、いまどきの普通のWindowsパソコンなら横方向に1920、縦方向に1080ぐらいのドットで構成されていると思います(この数字を解像度と言います)。

上のHTMLで、実際に貼ったのがこちら。

図1: canvas要素を貼ってみた

ではここに絵を描きます。まずは手始めに、長方形を書いてみましょう。

リスト2: 長方形を書いてみる
  1: const canvas = document.getElementById("fig2");
  2: const context = canvas.getContext("2d");
  3: context.strokeStyle = "white";
  4: context.strokeRect(20, 20, 30, 50);

下のcanvasに長方形が描画されていると思います。単に画像を貼ったように見えるかもしれませんが、下の図2は、実際にJavaScriptで描画したcanvasです。

図2: 長方形を描いたcanvas

このcanvasにはfig2というid属性が付けられているので、リスト2の1行目でそのcanvasを取得し、2行目で、そのcanvasからgetContext("2d")により2次元描画用のコンテキスト(context)を取得しています。3行目でそのコンテキストのstrokeStyleに「白」という色を指定し、4行目で長方形を描画しています。なお、ここでは色をwhiteという色名で指定していますが、RGB(red, green, blue)の値を0~255で指定する方法もあります。リスト4で実演します。

4行目のstrokeRect()ですが、まずrect(レクト)はrectangleの略で、長方形という意味です。stroke(ストローク)は、「ペンや筆で、ひと筆で書いた動き」のような意味があります。なのでstrokeRect()で長方形が描けるわけです。

このstrokeRect()の引数は、20, 20, 30, 50となっていますが、これはそれぞれ、この長方形の左上のX座標、Y座標、この長方形の幅、高さです。

図3: canvasの座標系

上の図3で、うすいグレーの領域がcanvasです。canvasで位置を指定するときに使う座標系は、左上が原点です。算数や数学で、正比例のグラフとかを書くときに使う座標系では、Y軸は上を向いていますが、canvasの座標系では下を向いていることに注意してください。

リスト2で描画している長方形は、左上の点のX座標とY座標がそれぞれ20、長方形の幅が30、高さが50であることがわかるでしょう。

「あれ? strokeStyleとやらで色に白を指定したはずなのに、図2の長方形、灰色に見えない?」と思った人がいるかもしれません。鋭いです。

実はcanvasの座標系では、X座標やY座標を整数にしたとき、その座標は、ドットとドットの間を意味します。そのため、切りのいい整数の座標で線を引くと、その線は、画面上の2ドットにまたがります。

図4: 座標系の整数は、ドットとドットの間にある

2ドットにまたがるからといって、幅2ドットで線を引いてしまうと太く見えるので(線幅は自由に指定できますが、デフォルトは1ドット分です)、細く見せるために白を灰色にしているのです。よって、位置を0.5だけずらしてやれば、正しく1ドット分の線になり、灰色ではなく白で描画されます。以下の例では、長方形の左上が、X座標、Y座標ともに20だったのを、20.5にしました。

図5: 0.5だけ位置をずらした

「それはさておき、canvasの最初の例がなんで長方形なの? 単純に、線を引いてみるとかじゃダメなの?」という声も聞こえてきそうですが、線を引くのはちょっと面倒だからです。まあ、やってみましょう。

リスト3: 線を引く
  1: const canvas = document.getElementById("fig4");
  2: const context = canvas.getContext("2d");
  3: context.beginPath();
  4: context.moveTo(10, 10);
  5: context.lineTo(50, 80);
  6: context.lineTo(80, 10);
  7: context.moveTo(100, 80);
  8: context.lineTo(140, 10);
  9: context.lineTo(180, 80);
 10: context.strokeStyle = "white";
 11: context.stroke();
図6: 線を引いたcanvas

リスト3にあるように、線を引くには、contextのbeginPath()メソッドで、まずパス(path)を開始します(3行目)。「begin」(ビギン)というのは、「始める」という意味です。その後、moveTo()メソッドで「ペン」を移動し(4行目)、lineTo()メソッドで線を描画します(5~6行目)。moveTo()を使えば、ペンを上げた状態で移動できるので、図6のように途中が途切れた線も引けます。ただし、lineTo()の時点ではまだ線は引けていなくて、11行目のstroke()の呼び出しで実際に線が引かれます。

別段難しくはないと思いますが、「線ぐらい、単純に始点と終点の座標を与えるだけで引けてもいいだろう」と思う人はいるように思います。実際、長方形は左上の座標と幅と高さを与えるだけで描けたわけですから、なぜ線を引くだけのことに「パス」などというものを使わなければいけないのか疑問に思うのは無理もありません。

ただ、パスには直線だけではなくて円弧も追加できますし、中を塗りつぶすこともできます。こういうのは、パスを使わないとうまく描けないでしょう。

図7: ちょっと複雑な図形を描く
リスト4: ちょっと複雑な図形のプログラム
  1: const canvas = document.getElementById("fig5");
  2: const context = canvas.getContext("2d");
  3: context.beginPath();
  4: context.moveTo(10, 10);
  5: context.lineTo(40, 10);
  6: // 円弧を描く。引数は順に中心X,Y座標、半径、始点の角度(ラジアン)、終点の角度、
  7: // 最後のtrueは円弧を反時計回りで書く場合に指定(省略すると時計回り)。
  8: context.arc(90, 10, 50, Math.PI, 0, true);
  9: context.lineTo(170, 10);
 10: context.lineTo(170, 80);
 11: context.lineTo(10, 80);
 12: context.closePath();
 13: // 色をRGB値で指定する
 14: context.strokeStyle = "rgb(200, 200, 255)";
 15: // 線の太さを指定する
 16: context.lineWidth = 3;
 17: // 輪郭線を描く
 18: context.stroke();
 19: // 塗りつぶしの色をRGB値で指定する
 20: context.fillStyle = "rgb(120, 120, 255)";
 21: // 塗りつぶす
 22: context.fill();

contextって何だ

初めてcanvasを使ってみた人にとって、パス以上にその意味がわからないのは、「context」だと思います。canvasに絵を描くなら、strokeRect()とかlineTo()とかのメソッドがcanvasについていればいいのに、なんでわざわざcontextなるものを持ってこなければいけないのか、と。

まず、ここのプログラムでは「canvas.getContext("2d")」としてcontextを取得しています。この「2d」は2次元という意味で、このcontextを使えば2次元の図が描画できるということです。getContext()の引数には他にwebglとかwebgl2とかbitmaprendererが指定でき、たとえばwebglを指定すれば3次元のグラフィックスを表示できます。canvasにはこのように色々な方法で描画できるので、strokeRect()とかlineTo()とかのメソッドを直接canvasに付けるわけにはいきません。

context(コンテキスト)という名前も謎です。実のところこういうものをcontextと呼ぶのは、JavaScriptに限った話ではなくて、たとえばWindowsでC言語とかでグラフィックを扱うときはDevice Context(デバイスコンテキスト)というものを使いますし、LinuxなどのUNIXで使われているX WindowにはGraphics Context(グラフィックスコンテキスト)というものがあります。JavaScriptのcontextがstrokeStylelineWidthで現在の色とか線幅を保持しているのと同様、X WindowのグラフィックスコンテキストやWindowsのデバイスコンテキストも、そういった状態を保持します。英語でcontextといえば、文脈とか状況とかの意味があります。「現在の色」のようなものを文脈とか状況とかだと考えれば、この名前でよいのかもしれません(正直、私はよくわかっていませんが……)。

ところで、contextが現在の色とか線幅とかを覚えていてくれるものなら、contextを複数使えれば、場合によっては便利だと思います。ただ、JavaScriptのcanvasのgetContext()は、2回呼んでも、2回目に返すcontextは、1回目のcontextと同じものなので、複数を並行して使うことはできないようです。

アンチエイリアシング

図6で斜めの線を引いていますが、これを拡大すると、こんな感じになっています。

図8: アンチエイリアシング

黒地に白で線を引いたはずが、灰色の点がかなりあります。これは、黒地に対して白1色で斜めの線を引くとドットのギザギザ(ジャギーといいます)が目立つので、灰色の点を打つことで目立たなくしているのです。この処理をアンチエイリアシング(anti-aliasing)と呼びます。

図2で、線が灰色になったのも、このアンチエイリアシングの効果です。

画像を表示する

長方形や線を描くのもよいですが、UFOゲームを作るならUFOの画像を表示できる必要があるでしょう。

まずUFOの画像を用意します(どっかで見たような形ですが…… オマージュです)。

UFO

私はこれをWindowsに付属のペイントで描きました。スタートメニューのWのところから、「Windowsアクセサリ」→「ペイント」で起動できます。

この画像のサイズは79×31ピクセルです。ペイントでは、(わかりにくいですが)メニューの「ファイル」の下の「プロパティ」から、画像のサイズの確認や変更ができます。また、画面右下のスライダで表示の拡大/縮小ができます。このサイズの絵を描くのなら、最大に拡大(800%)にするのがよいでしょう。背景は黒く塗っています。ゲームの画面の背景が黒いからです。透過画像にすれば、背景部分を透明にできますが、ペイントではそのような画像は作れません。

図9: ペイントでUFOを描く

UFOが描けたら、「ファイル」→「名前を付けて保存」から、ufo.pngというファイル名で保存します。「ファイルの種類」のところが「PNG(*.png)」になっていることを確認してください。

図10: 画像ファイルはPNGで保存する。

こちらのページでは、パンケーキの写真を「.jpg」という拡張子で保存しています。それに対し、今回は「.png」です。拡張子が.jpgの画像はJPEG(ジェイペグ)というフォーマットで、.pngの画像はPNG(ピング)というフォーマットで保存されています。JPEGは写真に向いたフォーマットですが、この手の小さなドット絵はPNGの方が向いています(JPEGだと、ブロックノイズというノイズが乗って、ちょっと色が変になります)。

まあ、このUFOでよければ、絵を自分で描かなくても、目次のところからダウンロードできるzipファイルに.pngファイルを同梱していますが……

さて、では、この画像を、canvasに表示します。

画像をcanvasに表示するには、まず画像を読み込んで、それをcontextのdrawImage()メソッドでcanvasに表示します。具体的には以下のように書く――のですが、後で説明する通り、これではうまくいきません。

リスト5: lesson04_1.html(画像表示失敗版)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOを表示する(多分失敗)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="200px" height="100px" style="background-color:black;"></canvas>
  9: <script>
 10: // UFOの画像を読み込む
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: 
 14: // canvasに描画する
 15: const canvas = document.getElementById("canvas");
 16: const context = canvas.getContext("2d");
 17: context.drawImage(ufoImage, 10, 10);
 18: </script>
 19: </body>
 20: </html>

11~12行目で、UFOの画像を読み込んでいます。11行目で、「new Image()」と書くことで、画像の要素を作っています(HTMLにimg要素を書いて作られるものと同じです)。「newって何だ?」と思うかもしれませんが、ここでは深入りしません。12行目で、その画像のsrcプロパティに画像ファイルのパスを与えることで、画像の読み込みが開始されます。「./ufo.png」のようにカレントディレクトリを指定しているので、HTMLと同じフォルダにufo.pngを置いてください。

こうすれば、17行目のdrawImage()の呼び出しで、画像が表示される――はずなのですが、実際には、おそらく表示されなかったと思います。12行目で画像のパスを指定して、画像の読み込みが開始されましたが、まだ終わっていないためです。ブラウザでWebページを表示するとき、画像はちょっと遅れて表示されたりするでしょう。あれと同じことです。

これを解決するには、画像の読み込みが終了し、onloadメソッドが呼び出されたタイミングで画像を表示します。具体的には以下のようになります。

リスト6: lesson04_2.html(画像表示成功版)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOを表示する(今度は成功)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="200px" height="100px" style="background-color:black;"></canvas>
  9: <script>
 10: // UFOの画像を読み込む
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: ufoImage.onload = drawUfo;
 14: 
 15: function drawUfo() {
 16:   // canvasに描画する
 17:   const canvas = document.getElementById("canvas");
 18:   const context = canvas.getContext("2d");
 19:   context.drawImage(ufoImage, 10, 10);
 20: }
 21: </script>
 22: </body>
 23: </html>

13行目で、画像のonloadに、drawUfo()関数を設定しています。これにより、画像のロードが終わったら、drawUfo()関数が呼び出されるようになります。ボタンのonclickに関数を設定したらボタンをクリックしたときにその関数が呼び出されたのと同じことです。そして、drawUfo()の中で、画像の描画を行います。

これで画像が表示されたと思います。

――ただし、この入門では、この「正しい方法」は取らず、あえて失敗した方の、lesson04_1.htmlの方法で進めようと思います。ここで作ろうとしているのはUFOゲームであり、UFOゲームではUFOが動きます。動くということは、場所を少しずつ変えながら何度も何度も書き直す、ということであり、何度も書いているうちに画像のロードは終わるので、すぐ表示されるようになるからです。最初の一瞬、何回かは表示されなかったのかもしれませんが、そんなのは人間の目にはばれないので、問題ありません。

なお、上のプログラムでは、drawImage()に3つの引数を渡しています。ひとつめは画像のオブジェクト、後の「10, 10」は画像の左上のX座標とY座標です。この座標を変えて何度も表示してやれば、UFOを動かすことができるわけです。

図11: drawImage()の座標系

UFOを左から右に動かす

ではさっそくUFOを動かしてみます。

UFOゲームでは、UFOはランダムに(でたらめに)飛び回る予定ですが、いきなりそれは難しいので、まずは単純に、左から右に動かすことを考えます。

drawImage()の引数でX座標、Y座標を渡しているのなら、for文で、X座標を増やしながら回してやれば、動かすことができるのでは?」と思うかもしれません。もっともです。

ものはためし、やってみましょう。19行目からのforループで、xを0から720まで(canvasの幅が800で、UFOの画像幅が79なので、右端の直前で止めています)変化させています。

リスト7: lesson04_3.html(UFOを動かす 失敗版)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOを動かす(失敗版)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="800px" height="100px" style="background-color:black;"></canvas>
  9: <script>
 10: // UFOの画像を読み込む
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: ufoImage.onload = drawUfo;
 14: 
 15: function drawUfo() {
 16:   const canvas = document.getElementById("canvas");
 17:   const context = canvas.getContext("2d");
 18: 
 19:   for (let x = 0; x <= 720; x += 10) {
 20:     context.drawImage(ufoImage, x, 10);
 21:   }
 22: }
 23: </script>
 24: </body>
 25: </html>

ここをクリックすると、実行結果が開きます。こんなのが表示されたはずです。

図12: lesson04_3.htmlの実行結果

UFOは動かず、右端にひとつだけ表示され、途中には、なにやら残骸が見えます。この「残骸」は、UFOを描いた後、消さずに次の座標にUFOを描いたので、前のUFOが一部残っているのです。

それはさておきUFOが動いて見えないのはなぜでしょうか。「いまどきのコンピュータは高速で動作するから、一瞬で右端まで行ってしまった」からでしょうか。それはまあ事実ではあるのですが、それだけでもありません。ためしに、左右に永久に動き続けるようにプログラムを変更してみましょう。

リスト8: lesson04_4.html (UFOを動かす もっと深刻な失敗版)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOを動かす(もっと深刻な失敗版)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="800px" height="100px" style="background-color:black;"></canvas>
  9: <script>
 10: // UFOの画像を読み込む
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: ufoImage.onload = drawUfo;
 14: 
 15: function drawUfo() {
 16:   const canvas = document.getElementById("canvas");
 17:   const context = canvas.getContext("2d");
 18: 
 19:   for (;;) {
 20:     for (let x = 0; x <= 720; x += 10) {
 21:       context.drawImage(ufoImage, x, 10);
 22:     }
 23:     for (let x = 720; x >= 0; x -= 10) {
 24:       context.drawImage(ufoImage, x, 10);
 25:     }
 26:   }
 27: }
 28: </script>
 29: </body>
 30: </html>

ここをクリックすると別ウインドウで実行されます(後述の通り、ブラウザが固まるので注意。タブを閉じれば終わります)。

いったん右端まで移動したら、次は右端から左端に動くように、そしてさらにそれを無限ループにしてみました。これなら、「左右にすごいスピードで動き続けるUFO」を見ることができるでしょうか。

動かしてみるとわかりますが、ブラウザが固まってしまうだけで、UFOは、動くどころか、ただのひとつも描画されません。これは「目にも止まらないスピードでUFOが動いている」わけではありません。仮にそうだとしたら「残骸」くらいは見えてもよさそうなものです。それすら見えないということは、そもそもUFOの描画は行われていないのです。

これはブラウザでJavaScriptを動かす時の特性なのですが、JavaScriptでcanvasに何かを描いたり、あるいはDOMをいじって画面表示を変更したとき、実際に画面が変わるのは、「JavaScriptの処理が終わってブラウザに制御が戻った時」です。なので、このように無限ループでUFOとかを動かすことはできません。もともとJavaScriptというのは、動いている間はブラウザが固まってしまうので、できるだけ早く処理を終わらせてブラウザに制御を戻さなければいけないのです。これはもうそういうものだと思っていただくしかありません。

UFOゲームのようなシューティングゲームは、以下のような無限ループで作れそうに思えます。

for (;;) {
  ①敵とか自機とかビームとかを描画する
 ②弾が当たったかどうかを判定する。
 ③敵とか自機とかビームとかを動かす
}

実際、言語とか環境によっては、こんな感じでゲームが作れる場合もあります。そして、この無限ループをメインループと呼んだりします。

しかし、JavaScriptではこの方法は使えません。ではUFOを動かすにはどうするのかといえば、setTimeout()という関数を使います。

setTimeout()という関数は、引数をふたつ取り、ひとつ目に関数を、ふたつ目に、タイムアウトの時間をミリ秒(千分の一秒)単位で指定します。そうしておくと、指定した時間が経過した時点で、ひとつ目の引数で指定した関数を呼び出してくれます。使ってみましょう。

リスト9: lesson04_5.html(UFOを動かすことに成功)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOを動かす(成功)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="800px" height="100px" style="background-color:black;"></canvas>
  9: <script>
 10: "use strict";
 11: // UFOの画像を読み込む
 12: const ufoImage = new Image();
 13: ufoImage.src = "./ufo.png";
 14: 
 15: const canvas = document.getElementById("canvas");
 16: const context = canvas.getContext("2d");
 17: 
 18: let x = 0;
 19: 
 20: mainLoop();
 21: 
 22: function mainLoop() {
 23:   context.clearRect(0, 0, 800, 100);
 24:   context.drawImage(ufoImage, x, 10);
 25:   if (x < 720) {
 26:     x += 10;
 27:     // 20ミリ秒後に、mainLoop()が呼び出されるように仕掛ける
 28:     setTimeout(mainLoop, 20);
 29:   }
 30: }
 31: </script>
 32: </body>
 33: </html>

ここをクリックすると、実際に動くページが開きます。

16行目まで、UFOの画像を読み込んだり、canvasやcontextを取得するところまでは今まで通りです。

18行目で、UFOのX座標を0に設定したうえで、20行目でmainLoop()関数を呼び出します。

mainLoop()関数の中では、まず23行目のclearRect()で、画面全体をクリアしています。これにより、このプログラムからは「残骸」が残らなくなりました。clearRect()の引数は、クリアする領域の左上のX座標、Y座標、幅、高さです。黒でfillRect()するのと同じでは? と思えますが、性能がよかったりはするようです

24行目で、xで指定したX座標でUFOを描画したうえで、右端に達していなければxに10加算し(26行目)、setTimeout()により、mainLoop()を20ミリ秒後に呼び出すように設定します。その後は<script>要素が終わってしまうので、いったんここでブラウザに制御が返り、UFOが本当に描画されます。そして、20ミリ秒後にブラウザがmainLoop()を呼び出してくれるので、さっきより10ピクセル右にUFOを再描画し、以後繰り返しです。この繰り返しはループではありませんが、実質ループと同じなので、関数名をmainLoop()としました。

あとは、このmainLoop()の中に、以下のようなプログラムを組み込んでいけば、UFOゲームの出来上がり、ということになります。取り消し線で消していないところが、今後のやること一覧です。

UFOをランダムに動かす

それでは、UFOをランダムに(でたらめに)動かす方法を考えます。

ゲームのレベルで「でたらめに」何かを動かしたいのなら、JavaScriptでは、Math.random()関数を使うのが定石です(ゲームのレベルで、というのは、暗号化とか、セキュリティに関する箇所で使えるほどちゃんとした乱数ではないということです)。

以下のプログラムで、Math.random()関数の動きを見てみます。

リスト10: Math.random()関数の実験
  1: for (let i = 0; i < 10; i++) {
  2:   console.log("Math.random().." + Math.random());
  3: }

ここをクリックすると動きます。

2行目で、console.log()Math.random()の結果を出力しています。F12で開発者ツールを起動し、「コンソール」を選んで結果を見てください。

Math.random()..0.2251262342865572
Math.random()..0.47936193586632236
Math.random()..0.248704014690877
Math.random()..0.778698981864336
Math.random()..0.24831053958583182
Math.random()..0.14857993961913496
Math.random()..0.3276603568694041
Math.random()..0.9700096313507545
Math.random()..0.06810971855332393
Math.random()..0.7677122567357675

今回、for文で10回表示したので、10個、ランダムな数値が表示されています。何度も実行すれば、そのたびに違う値が表示されます。上の実行結果を見ればわかるように、Math.random()の返す値は、0以上1未満の値です。

そこまではわかったとして、では、これを使ってUFOをランダムに飛ばすには、どうすればよいでしょうか。

ひとつ考えつく方法として、以下のようなものがあります。

この方法だと、X座標もY座標も必ず増えるか減るかするので、UFOは常に斜めに動きます(画面端にぶつからない限り)。また、X座標、Y座標ともに10ずつ増やしたり減らしたりしているのは、一度にこれくらい動かしても十分動いているように見えるし、setTimeout()で20ミリ秒を指定した状態ではこれぐらいでちょうどよいスピードで動くからです。

上の考え方を、実際にプログラムにしたものが以下です。

リスト11: lesson04_6.html UFOをランダムに動かす(その1)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOをランダムに動かす(その1)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="800px" height="600px" style="background-color:black;"></canvas>
  9: <script>
 10: "use strict";
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: 
 14: const canvas = document.getElementById("canvas");
 15: const context = canvas.getContext("2d");
 16: 
 17: // UFOの初期位置は、とりあえず左上とする
 18: let ufoX = 0;
 19: let ufoY = 0;
 20: 
 21: mainLoop();
 22: 
 23: function mainLoop() {
 24:   context.clearRect(0, 0, 800, 600);
 25:   context.drawImage(ufoImage, ufoX, ufoY);
 26: 
 27:   if (Math.random() < 0.5) {
 28:     if (ufoX > 0) { // 画面をはみ出してしまわないためのチェック
 29:       ufoX -= 10;
 30:     }
 31:   } else {
 32:     if (ufoX < 720) {
 33:       ufoX += 10;
 34:     }
 35:   }
 36:   if (Math.random() < 0.5) {
 37:     if (ufoY > 0) {
 38:       ufoY -= 10;
 39:     }
 40:   } else {
 41:     if (ufoY < 500) { // 下端はキャノン砲の分、ちょっと空けておく
 42:       ufoY += 10;
 43:     }
 44:   }
 45:   setTimeout(mainLoop, 20);
 46: }
 47: </script>
 48: </body>
 49: </html>

27行目で乱数を発生させて、それが0.5より小さければ、X座標を10減らしています。この時、画面をはみ出すようなら、減らすのをやめています(28行目のif文)。そうでなければX座標を10増やしています(33行目)。こちらでもはみ出しチェックを行います。以下同様です。

ここをクリックすると、動くページが別タブで開きます。

――どうでしょうか。UFOは「ランダムに飛び回る」というより、なんだかぶるぶる震えているように見えます。いくらJavaScript入門のためのサンプルのゲームであっても、これはさすがにちょっとあんまりだと思います※1。もうちょっとそれらしい飛び方になる方法を考えなければいけません。

なお、ここで、UFOをランダムに動かすために、「乱数が0.5より小さければX座標を10減らす」といった方法を考えましたが、このように、プログラムで何かを実現するときの、その方法のことをアルゴリズム(algorithm)と呼びます。今回の、UFOを飛ばすアルゴリズムはちょっと出来が悪かったので、もうちょっとマシなアルゴリズムを考えます。

次のアルゴリズムでは、以下の方針とします。

この方法なら、「目標点」まではUFOはそこに向けて動くので、前のアルゴリズムのように「ぶるぶる震えているだけ」にはならないでしょう。

図13: UFOを動かすアルゴリズムその2

図にあるように、UFOは、X座標とY座標の両方が目標点と異なるうちは、X座標とY座標の両方を変化させて、斜めに動きます。どちらかが一致すれば、水平または垂直の飛行に移行します。今回、canvasは幅800高さ600ですが、目標点は、X座標は(UFOの幅が79ピクセルなので)0以上720以下、Y座標は、キャノン砲の場所も考えてちょっと余裕を取って、0以上500以下とします。これはそれぞれ以上、以下なので、0とか720とかの値(こういう値を境界値と呼びます)を含みます。

このアルゴリズムをプログラムにしたのが以下です。

リスト12: lesson04_7.html UFOをランダムに動かす(その2)
  1: <!DOCTYPE html>
  2: <html lang="ja">
  3: <head>
  4: <meta charset="UTF-8">
  5: <title>UFOをランダムに動かす(その2)</title>
  6: </head>
  7: <body>
  8: <canvas id="canvas" width="800px" height="600px" style="background-color:black;"></canvas>
  9: <script>
 10: "use strict";
 11: const ufoImage = new Image();
 12: ufoImage.src = "./ufo.png";
 13: 
 14: const canvas = document.getElementById("canvas");
 15: const context = canvas.getContext("2d");
 16: 
 17: // UFOの初期位置は、とりあえず左上とする
 18: let ufoX = 0;
 19: let ufoY = 0;
 20: // 「目標点」の初期位置も、左上とする
 21: let ufoTargetX = 0;
 22: let ufoTargetY = 0;
 23: 
 24: mainLoop();
 25: 
 26: function mainLoop() {
 27:   context.clearRect(0, 0, 800, 600);
 28:   context.drawImage(ufoImage, ufoX, ufoY);
 29: 
 30:   if (ufoX === ufoTargetX && ufoY === ufoTargetY) {
 31:     // UFOの位置と目標点が一致しているので、新たな目標点を設定
 32:     ufoTargetX = Math.floor(Math.random() * 73) * 10;
 33:     ufoTargetY = Math.floor(Math.random() * 51) * 10;
 34:   }
 35:   // UFOを目標点に近づける
 36:   if (ufoX < ufoTargetX) {
 37:     ufoX += 10;
 38:   } else if (ufoX > ufoTargetX) {
 39:     ufoX -= 10;
 40:   }
 41:   if (ufoY < ufoTargetY) {
 42:     ufoY += 10;
 43:   } else if (ufoY > ufoTargetY) {
 44:     ufoY -= 10;
 45:   }
 46:   setTimeout(mainLoop, 20);
 47: }
 48: </script>
 49: </body>
 50: </html>

18~19行目でUFOの初期位置を決めると同時に、21~22行目で、目標点も同じ座標にしています。そして、30行目で、UFOの位置と目標点が完全に同じなら、目標点を再設定しています。つまりmainLoop()の最初の実行では、UFOの位置と目標点が一致しているので必ずここを通ります。

このプログラムでは、UFOは10ピクセル単位で動くので、目標点も10ピクセル単位にしてやらないと、UFOの位置と目標点が完全には一致しません。そこで、たとえば32行目では、以下のようにして目標点のX座標を決めています。

  1. Math.random()は0以上1未満の値を返すので、まずこれに73をかけます。そうすると、0以上73未満の値となります。
  2. その値に対し、Math.floor()関数をかけます。Math.floor()は、小数点以下を切り捨てる関数です。これにより、0~72の値となります。floor(フロア)って何だ、床か? と思うかもしれませんがその通りで、切り捨てて下の値を取るから「床」です。反対に切り上げる関数はMath.ceil()で、ceilとは「天井」という意味です。
  3. 最後にそれに10をかけます。これにより、最小は0、最大は720の、10刻みの値が取得できます。

目標点のY座標も同様にして、最小は0、最大は500の、10刻みの値を取得します。

その上で、UFOの座標を目標点に近づけます。UFOのX座標が目標点より小さければ10増やし、大きければ10減らします。等しければ何もしません(36~40行目)。これをX座標、Y座標それぞれについて行うことで、UFOは目標点に近づいていきます。

ここをクリックすると、動くページが別タブで開きます。

斜めに動いている間は、水平/垂直に動くときよりも速く動いている(X座標、Y座標両方が変化するのだから当たり前です。√2倍(約1.41421356倍)のスピードで動きます)、とか、気になるところはありますが、今回のUFOゲームのサンプルとしては、まずはこれでよいのではないでしょうか。

残りのやること一覧は以下です。次はキャノン砲を表示して動かします。

公開日: 2021/06/20



前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る