LoginSignup
252
224

More than 3 years have passed since last update.

200行のVue.jsでスネークゲームを作った

Last updated at Posted at 2019-09-28

こんにちは、猫チーズです。

snake.gif

社内のVue.js勉強用プログラムとして1時間ほどで簡単なスネークゲームを作ったところ、メンバー各々が魔改造などして、一時的に社内でスネークゲームが流行りました。

(2019/09/29 追記) 第2弾も作りました → 『250行のVue.jsで陣取りゲームを作った』

デモページ

GitHub ソースコード

ゲームルール

スネークゲームは、シンプルで古典的なゲームです。
1. 何も操作しないとヘビはまっすぐ進む
2. 壁か自分自身にぶつかるとゲームオーバー
3. 矢印キーでヘビを方向転換できる
4. リンゴを食べると体が伸びる&スコアUP

これらのルールをVue.jsで作りました。

200行のプログラム

以下の200行のhtmlファイルに全ての機能が纏まっています。
snake.htmlなどの名前で保存して、ブラウザでそのファイルを開くと遊べます。
速さや色、新しいアイテムの追加などをして遊んでみてください。

snake.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Snake</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>

    <style>
        /* グリッドレイアウト */
        #map {
            --grid-size: 10;    /* 10 x 10 マス(CSS変数) */

            display: grid;
            grid-template-columns: repeat(var(--grid-size), 30px);  /* 10列 幅30px */
            grid-template-rows: repeat(var(--grid-size), 30px); /* 10行 高さ30px */
        }

        /* セルの色 */
        .cell {
            border: 1px solid white;
            background: whitesmoke;
        }

        /* ヘビの体の色 */
        .cell.body {
            background: darkgray;
        }

        /* フルーツの色 */
        .cell.fruit {
            background: orangered;
        }

        /* ヘビの頭の色 */
        .cell.head {
            background: dimgray;
        }
    </style>
</head>
<body>
    <div id='app'>
        <p>SCORE: {{ snake.body_indexes.length - 1 }}</p>

        <div id='map'>
            <!-- セルを100個生成して、必要に応じてhead, body, fruitクラスを付ける -->
            <!-- (注意:Vueは v-for="i in 数値" としたとき、iが1から始まる) -->
            <div v-for="i in grid_size * grid_size"
                 :class="{
                     cell: true,
                     head: snake_head_index === i - 1,
                     body: snake.body_indexes.includes(i - 1),
                     fruit: fruit_index === i - 1,
                 }"
            ><!-- {{ i - 1 }} --></div>
        </div>

        <p v-if='is_gameover'>
            GAME OVER<br>
            <button onclick="location.reload()">RETRY</button>
        </p>
    </div>

    <script>
        new Vue({
            el: '#app',

            data: {
                grid_size: 10,  // 10 x 10 マス
                fruit_index: 0, // フルーツの位置インデックス

                // ヘビに関するデータ
                snake: {

                    // 頭の座標(初期値)
                    head_pos: {
                        x: 1,
                        y: 3,
                    },

                    body_indexes: [0],  // 体の位置インデックスたち
                    direction: '',   // 進行方向
                    speed: 400, // 1マス進むのにかかる時間[ms]
                },
            },

            // 初期化
            created() {

                // フルーツの位置をランダムに移動
                this.randomize_fruit_index()

                // キーボード入力のイベントをon_keydownメソッドに投げる
                document.onkeydown = () => {
                    this.on_keydown(event.keyCode)
                }

                // 時間を動かし始める
                this.time_goes()
            },

            watch: {
                // フルーツを食べているか監視
                is_eating_fruit(newValue) {
                    if(!newValue) return    // 食べていなかったら何もしない
                    this.grow_up_snake()
                    this.randomize_fruit_index()
                },
            },

            computed: {

                // ヘビの頭の座標をインデックスに変換
                snake_head_index() {
                    if(this.is_frameout) return null
                    return this.snake.head_pos.y * this.grid_size + this.snake.head_pos.x
                },

                // フルーツ食べてる?
                is_eating_fruit() {
                    return this.snake_head_index === this.fruit_index
                },

                // 自己衝突してる?
                is_suicided() {
                    return this.snake.body_indexes.includes(this.snake_head_index)
                },

                // ヘビの頭は画面外?
                is_frameout() {
                    const head = this.snake.head_pos
                    return head.x < 0 || this.grid_size <= head.x || head.y < 0 || this.grid_size <= head.y
                },

                // ゲームオーバー?
                is_gameover() {
                    return this.is_suicided || this.is_frameout
                },
            },

            methods: {
                // 時間を進める
                time_goes() {
                    if(this.is_gameover) return
                    this.forward_snake()

                    // speedミリ秒後に自分自身を呼び出す
                    setTimeout(this.time_goes.bind(this), this.snake.speed)
                },

                // ヘビを進める
                forward_snake() {

                    // 体の最後尾を頭に持ってくる
                    this.snake.body_indexes.shift()
                    this.snake.body_indexes.push(this.snake_head_index)

                    // 頭を1マス移動
                    switch(this.snake.direction) {
                        case '': this.snake.head_pos.x--; break
                        case '': this.snake.head_pos.y--; break
                        case '': this.snake.head_pos.x++; break
                        case '': this.snake.head_pos.y++; break
                    }
                },

                // ヘビの体を伸ばす
                grow_up_snake() {
                    this.snake.body_indexes.unshift(this.snake.body_indexes[0])
                },

                // フルーツの位置をランダムに移動
                randomize_fruit_index() {
                    this.fruit_index = Math.floor(Math.random() * this.grid_size * this.grid_size)  // 0 〜 99 の乱数
                },

                // キー入力を受け取ってヘビの進行方向を変える(逆方向は不可)
                on_keydown(keyCode) {
                    switch(keyCode) {
                        case 37:    // 「←」キーが押された
                            if(this.snake.direction !== '') { this.snake.direction = ''; }
                            break

                        case 38:    // 「↑」キーが押された
                            if(this.snake.direction !== '') { this.snake.direction = ''; }
                            break

                        case 39:    // 「→」キーが押された
                            if(this.snake.direction !== '') { this.snake.direction = ''; }
                            break

                        case 40:    // 「↓」キーが押された
                            if(this.snake.direction !== '') { this.snake.direction = ''; }
                            break
                    }
                },
            },
        })
    </script>
</body>
</html>

位置インデックスについて補足

スクリーンショット 2019-09-28 11.41.12.png
プログラム内に出てくる 位置インデックス というのは、各セルに割り振った番号のことです。左上の0から始まり、右下まで行くと99です。

上の画像の時点では、
ヘビの頭 snake_head_index === 58
ヘビの体 snake.body_indexes === [44, 54, 55, 56, 57]
フルーツ fruit_index === 99
になっています。

猫チーズ

高校一年の頃からアプリ作りに没頭していて、今はアプリクリエイターとして生きるために奮闘中です。
今年の4月からサイバーブレイン株式会社でデザイナーを始めました。

Twitter | 猫チーズ
https://twitter.com/miyauchoi

Hello 猫チーズ | 猫チーズと擬似会話ができるブログ
https://blog.miyauchi-akira.app/post/20190927/

ポートフォリオ | ミヤウチアキラ
https://miyauchi-akira.app

会社

サイバーブレイン株式会社では、AI Academyというサービスを作っています。

AI Academy
「AIを作りながらAIプログラミングを学ぼう」
https://aiacademy.jp/

Pythonを使った機械学習の教材が無料でたくさんあります。
AIに興味がある方はぜひ。

AI Academyサービス自体のフロントエンド側がVue.jsで作られています。
そのため今回は、社内メンバー(and いずれ来る後輩)のためにVue.jsの教材を作りました。

252
224
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
252
224