50行で作る、HTML5+JavaScriptを使った簡単ライフゲームづくり

おっ立ち野郎が2014年12月29日14:30:07に投稿しました。

JavaScript

13

お気に入り

準備中

Views

このレシピで作るもの

ライフゲームを知っていますか?ライフゲームは世界でとっても有名なシミュレーションゲームです。Wikipediaによると、

1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。

らしいです。

眺めているだけでもさまざまなパターンを観測することができてとても興味深く、魅力的なゲームです。

また、ライフゲームの世界は非常に単純なルールで成り立っています。つまり、僕を含めたプログラミング初心者にはうってつけの題材なのかもしれません。また今回はHTML5とJavaScriptで作ります。"index.html"と"main.js"の2ファイルだけでゲームが完成します。特殊なソフトウェアは必要ありません。

更に50行でコードが完成してしまうので、気軽にプログラミングできるかと思います。


目次

今回作るライフゲームの動作の流れ

書く前にどんなものか少し考えてみましょう。今回の動作はとても少ないです。

  1. ランダムに生きたセルを出現
  2. 生死処理
  3. キャンバスの再描画
  4. 2に戻る

これで終わりです。

早速これを50行で書いていきましょう。

プログラミングの下準備

今回ライフゲームに必要なファイルは以下の2つのみ。

  • index.html
  • main.js

今回はmain.jsをメインに書いていくので、index.htmlをちゃちゃっと書いておきましょう。

index.htmlを作る

index.htmlを以下のように記述してください。

<!DOCTYPE html>
<meta charset="utf-8">
<title>50行ライフゲーム</title>
<script src="main.js"></script>
<style>body{background:black;margin:0;padding:0;}</style>
<canvas id="world" style="display:block;margin:auto;"></canvas>

HTML5の宣言、文字コード、タイトルとJavaScriptの読み込みをし、キャンバス要素を記述します。

htmlタグやheadタグ、bodyタグは省略できます。詳しくは→Google HTML/CSS Style Guide [http://google-styleguide.googlecode.com/svn/trunk/htmlcssguide.xml:bookmark]

それでは下準備ができましたので、早速main.jsを書いていきましょう。50行なのですぐに終わりますよ!

main.jsを書いていく

完成したコードをGistにアップしました。

基本的には上のソースコードと照らしあわせてこの記事を見てください。

コード全体の構成を考える

いきなり白い紙を渡されても書きづらいものなので、引数や内容は考えずに抽象的に構成だけ考えてみましょう。

こんな感じでライフゲームは完成します。

/* グローバル変数の宣言 */


window.onload = function() {
    /* ゲームの初期化処理と開始処理 */
}


function update() {
    /* 世代の更新処理とキャンバスの描画処理 */
    setTimeout(update, ミリ秒); // ミリ秒後に再帰処理をしてループ
}


function draw() {
    /* キャンバスの描画処理。update関数で呼び出される予定。 */
}

もう一度ライフゲームの動作の流れを見てみましょう。

  1. ランダムに生きたセルを出現
  2. 生死処理
  3. キャンバスの再描画
  4. 2に戻る

window.onloadはページ読み込み時に実行される関数です。ここで初期化処理しておいてから、ゲームのメインループを開始します。

update関数は世代の更新、つまり生死処理後にキャンバスを再描画します。再描画の処理はdraw関数を使います。

draw関数は描画処理をする関数です。

ではさっそく具体的に書いていきましょう。

グローバル変数を宣言する

まず定数などをグローバル変数として宣言しましょう。

var SCREEN_SIZE = 500; // キャンバスの幅
var SIDE_CELLS = 200; // 一辺のセルの数
var CELL_SIZE = SCREEN_SIZE / SIDE_CELLS; // セルの幅
var FPS = 10; // フレームレート
var canvas; //= document.getElementById('world');
var context; //= canvas.getContext('2d');
  • SCREEN_SIZEはキャンバスの幅を表し数字の単位はpxです。
  • SIDE_CELLSはキャンバスの一辺におけるのセルの数です。
  • CELL_SIZEは1セルの幅です。
  • FPSはフレームレートです。一秒間に何回世代を更新・描画するかです。
  • canvasには後にキャンバスの要素を代入します。
  • contextには後に描画コンテキストを代入します。

20130911160348.png

※ コンテキストはキャンバスの筆のようなものです。コンテキストを使うと、色を指定してさまざまな形をキャンバスに描くことができます。

ページが読み込まれた時の処理を書こう

ページが読み込まれた時はwindow.onloadに代入された関数が実行されます。

window.onload = function() {
    var field = new Array(SIDE_CELLS*SIDE_CELLS); // フィールド情報
    var tempField = new Array(SIDE_CELLS*SIDE_CELLS); // フィールド情報の一時記憶用
    for (var i=0; i<field.length; i++) field[i] = Math.floor(Math.random()*2); // ランダムに「生」「死」を格納
    canvas = document.getElementById('world'); // canvas要素を取得
    canvas.width = canvas.height = SCREEN_SIZE; // キャンバスのサイズを設定
    var scaleRate = Math.min(window.innerWidth/SCREEN_SIZE, window.innerHeight/SCREEN_SIZE); // Canvas引き伸ばし率の取得
    canvas.style.width = canvas.style.height = SCREEN_SIZE*scaleRate+'px';  // キャンバスを引き伸ばし
    context = canvas.getContext('2d');                // コンテキスト
    context.fillStyle = 'rgb(211, 85, 149)';          // 色
    update(field, tempField);   // ゲームループ開始
}

フィールドの情報は"field"というSIDE_CELLS ✕ SIDE_CELLSの長さの一次元配列で管理します。「生」を1、「死」を0で表現します。

20130914192202.png

"tempField"は"field"と同じ長さを持つfieldの一時記憶用です。update関数で次の世代に移行する時に必要となります。update関数で毎回新しい配列を作成したくなかったのでここで作成しておきました。

そしてキャンバス要素とキャンバスのコンテキストを取得します。context.fillStyleはコンテクスト、つまり筆の色を指定しています。今回はピンクです。今回ライフゲームを作るにあたって、ここで指定するピンク以外のオススメは'rgb(0, 255, 255)'と'rgb(255, 255, 100)'です。はい正直なんでもいいです。キャンバスの横幅、縦幅をCSSで指定することにより、引き延ばすことができます。

最後に再帰処理をする関数、updateを呼び出してゲームのメインループを開始させます。

メインループ処理を書こう

では早速一番重要な関数を書いていきましょう。

この関数がする仕事は、

  1. 生死処理
  2. キャンバスの再描画
  3. 再帰処理で自身を呼び出す

です。

生死処理ではライフゲームのルールにのっとり、処理します。Wikipediaに記述してあるライフゲームのルールは以下です。※ ここでの「隣接」の意味はその回りに存在する8つのセルのことです。

  • 誕生: 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
  • 生存: 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
  • 過疎: 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
  • 過密: 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

これを簡略化すると、

  • 「生」の周りに2つか3つ「生」があれば「生」
  • 「死」の周りに3つ「生」があれば「生」
  • それ以外は「死」

となります。

20130911155451.png

生死処理を終えたあとはdraw関数を呼び出してキャンバスを再描画しましょう。

そして最後に再帰処理をします。一秒は1000ミリ秒なので1000でFPSを割ります。こうすることにより一秒にFPS回だけupdate関数が呼び出されることになります。

function update(field, tempField) {
    var n = 0;                    // 自身のまわりにある「生」の数
    tempField = field.slice(); // 複製
    for (var i=0; i<tempField.length; i++) {
        n = 0;
        for (var s=-1; s<2; s++) {
            for (var t=-1; t<2; t++) {
                if (s==0 && t==0) continue; // 自身はカウントしない
                var c = i+s*SIDE_CELLS+t;   // チェックするセル
                if (c>=0 && c<tempField.length) { // 配列からはみ出していないか(上下の壁判定も兼ねて)
                    if (i<c && c%SIDE_CELLS!=0 || i>c && c%SIDE_CELLS!=SIDE_CELLS-1) { // 左右の壁判定
                        if (tempField[c]) n ++; // 「生」だったらカウント
                    }
                }
            }
        }
        if (tempField[i] && (n==2||n==3)) { // 自身が「生」でカウントが2か3
            field[i] = 1;    // 「生」
        } else if (!tempField[i] && n==3) { // 自身が「死」でカウントが3
            field[i] = 1;    // 「生」
        } else field[i] = 0; // 「死」
    }
    draw(field);                                    // canvasを更新
    setTimeout(update, 1000/FPS, field, tempField); // 再帰
}
ポイント

tempField = field.silce();はfieldをコピーしています。tempField = field;としてしまうと参照を渡しただけになってしまうので注意してください。

20130911155451.png

for文の中にある2つのfor文、for (var s=-1; s<2; s++)とfor (var t=-1; t<2; t++)は周囲のセルを探索するためにあります。

20130911154752.png

sが0でtが0の場合は自分自身なのでカウントをしません。

上下の壁かどうかはチェックする場所が配列からはみだしていないかを考えればよく、左右は以下の図のように考えましょう。

20130914193814.png

これを表すと

// チェックする場所が隣接して"いる"ということをtrueとする時
i<c && c%SIDE_CELLS!=0 || i>c && c%SIDE_CELLS!=SIDE_CELLS-1

setTimeout(呼び出す関数, ミリ秒, 引数1, 引数2, 引数3, …)はミリ秒後に指定した関数をよびだします 。今回はupdate自身を呼び出しています。第三引数、第四引数にupdate関数の引数、"field"と"tempField"を指定しています。呼び出す関数の指定はupdate()としてはいけません。setTimeoutについてはここなんかを参考にしてください。

描画関数を書こう!

最後に描画関数を書いて完成です!

draw関数のすることは、

  1. キャンバスをクリア
  2. フィールド情報に基づいてキャンバスを描画

です。簡単な関数です。

function draw(field) {
    context.clearRect(0, 0, SCREEN_SIZE, SCREEN_SIZE); // 画面をクリア
    for (var i=0; i<field.length; i++) {
        var x = (i%SIDE_CELLS) * CELL_SIZE;             // x座標
        var y = Math.floor(i/SIDE_CELLS) * CELL_SIZE; // y座標
        if (field[i]) context.fillRect(x, y, CELL_SIZE, CELL_SIZE); // 「生」を描画
    }
}

clearRect()で画面をクリアします。context.fillRect(x座標, y座標, 横幅, 縦幅)は先ほど指定した色が塗られた矩形を描きます。

50行ライフゲーム完成!!

どうでしたか?

これで50行ライフゲームが完成しました。お疲れ様でした。

それにしても単純な動作でこんなに魅力的なゲームが作れてしまうことが不思議ですね…。

このレシピを書いたシェフ

おっ立ち野郎

@ottatiyarou

コードレシピの管理人です。
「for文とか関数とかの基礎はやったけど、実際アプリとかどうやって作るのか分からない」を解決するようなサービスにしていきたいです。


このシェフが書いたレシピ