JavaScript
vue
Firebase
FirebaseRealtimeDatabase
nuxt

Nuxt.jsで時間泥棒確定ゲーム作ってみました

最近英語でも記事を書くことにしたしゅーいっちです。

いろんなところにハマってかなり時間がかかり、作った自分の時間が泥棒に持っていかれてしまいました(すみません)。その時のメモです。

経緯

年末にokinawa.rbのミートアップにかなり久しぶりに行った時に、勉強のために作りかけで放ったらかしだったゲームの話をしてしまい、有言実行状態にするためにウェブ上で動作するところまで作りました。

できたもの

Cat or Dog, not a cow! (https://neko-inu.com)
ネコまたはイヌ!ウシではない

Cat or Dog, not a cow!

名前

将来的にまかり間違って流入増加することを願って人気の検索ワードを盛り込んでます。ビッグワードすぎて競合に勝つことはないのですが夢があったほうがいいので。最初は Pizza or hamburger でしたがいらすとやさんのイラストを選ぶ時に図柄に合わせて変更しました。

遊び方

まずサウンドはなるべくオンにしてください。音のフィードバックがないとUX的につらいです。

  • たぬきを一匹クリックする(ゆっくりやさしく)
  • ネコまたはイヌが出れば、1ポイントゲット
  • ウシが出たら、ウシの頭をゲット
  • ウシの頭が3つになるとゲームオーバー
  • ハイスコア更新するとお花畑に行きます。
  • ハイスコア未満で終了したら地獄に落ちます。
  • World Recordを更新すると今流行りの #月に行きます。

スマホにしか最適化しなかったので、画面の細さをキープするためパソコンで見ると火星に着陸した飛行機の中な感じになります。

使ったモノ

作る前の自分に教えてあげたいこと(aka 学んだこと)

フレームワークってのは

構造をゼロから作らなくてすむけど決まりがあるよ、 フレームワークごとの決まりを学ぶ必要があるよ。

そのデータがどの範囲から呼び出されるのかをある程度は予測しておくべき(スコープ)

別のコンポーネントからリソースを呼ぼうと思ったら、importしないと呼べない場所にあった。

  • ハイスコアになったよという状態 isHighScore
  • 同じ音の再生/停止を複数コンポーネントに渡ってやろうと思ったら、同じ名前の別インスタンスになって制御できなかった。

どちらもVuexのところで宣言したらできた。(これがベストなやり方かどうかはだれか教えてください)

/store/index.js
const createStore = () => {
  return new Vuex.Store({
    state: {
      isNewHighScore: false,
      bgSong: {}
    },
    mutations: {
      setIsNewHighScore(state) {
        state.isNewHighScore = true;
      },
      setBgSong(state) {
        state.bgSong = new Audio("lucky_game.mp3");
        state.bgSong.loop = true;
        state.bgSong.volume = 0.3;
      }
   }
}

初回アクセス時にVue側で呼び出す。

pages/index.vue
methods: {
    init() {
      this.initSongs();
    },
    initSongs() {
      this.$store.commit("setBgSong");
      // Other songs and sound etc..
    }
}

ハイスコア状態なら表示

<div v-if="$store.state.isNewHighScore">
ここ表示!
</div>

ちなみに自分の最高得点は50点です。

spaは普通のhtmlと違うよ

  • サーバーサイドとクライアントサイドがあってそのコードがどっちで動いているか把握する必要ある。
  • Firebase storageの本家のサンプルコードがすでに動かない!途方に暮れてstackOverFlowで聞いたら「それクライアントのライブラリだよ」と。結局画像は/staticassetsに入れた。
  • googleBotは、すべてのjavascriptをレンダリングするわけではないため、サーバーサイドで事前に書き出されたページ以外はインデックスしない → 検索結果に出てこない。
  • URL移動してもコンテンツがロードされない問題(未解決)。遷移先で新しい通信が発生しない場合?コンテンツがリフレッシュされない?nuxt-link$router.push(path)など。このissueを参考にいろいろやってみたが、3日調べてわからなかったのでモーダルウィンドウで済ませた。

ブラウザは思ったとおりに動かないことがある

  • モダルウィンドウ内で画像をcssアニメーションで動かしたら、モバイルブラウザだけ動かなかった。動き出しのタイミングを0.5s遅らせることで動いた。
  • 音の出るタイミングは難しい。setTimeOutと絡めると挙動不審になったので分離。
  • 画像読み込むのに時間がかかる。特にモバイルは。モダル内の背景画像が初回表示時に遅れて出てくる。preloadというのをする必要があるよ(やってない)。
  • モバイルブラウザ向けの開発してる人秘伝のタレまみれですごそう。

そもそも今回のはcanvasでやるべきなんじゃないだろうか。

firebase hosting のホスティングは簡単

  • 最初のセットアップ済ませたら、npm run build && npm run generate && firebase deploy であっさりデプロイされた。ドメインとのリンクも一瞬。neko-inu.com でアクセスできます。

音重要

  • 効果音がないとタップのフィードバックがないので、画面内で何が起こってるのか伝わらない。
  • 音楽が流れていると楽しい。久しぶりにgarage band使った。できる人にmidiキーボードで演奏してもらって、あとから自分で音色変えたり構成変えたりする作業が楽しかった。

絵重要

  • 最初は material design の icon だけでやっていた。ふと、いらすとやさんのイラストを思い出して差し替えてみたら一気に説得力がアップした。いらすとやさんすごい。

Math.randomは使ってはいけない(追記2019.1.12 0:11)

@angeart さんからのコメント

グローバル関数ですのでMath.random = () => 0等で簡単にすげ替えることができ、ずっと当たりを出せるようになってしまいます

やってみたら100%の確率で猫が出るようになりました。こんな危険な関数があっていいのかと思いましたが(ねこかわいいけど)いろいろ調べ回った結果、最終的にsecure-randomというモジュールを使いました。

https://github.com/jprichardson/secure-random

import secureRandom from "secure-random";

// 略

setRandomAnimal() {
   let ranNum = secureRandom(1)[0] % this.animals.length;
   this.selectedAnimal = this.animals[ranNum];
   return this.selectedAnimal;
},

これで安全に配列の長さの整数をランダムに返せるようになりました。

符号なし整数値という言葉を知りました。

Math.randomはそのままでは使ってはいけない(追記2019.1.13 9:31)

@taqm さんよりコメント

// 事前に退避しておく
const random = Math.random;

// 上書きを無効化する
Math = Object.freeze(Math);

こちらのやり方のほうが使われてる気がしますね。

ちなみに、secure-random入れたせいで、以前はたまにでていた高得点が出なくなったようで、ゲーム的には渋い感じになりました。20点がかなり遠いです。

作ったあとの自分にやってほしいこと(aka TODO)

  • pwa化
  • Cloud messageでWorld Record更新時に希望者に通知。

やったチュートリアル