すごい勢いでモックを作るノウハウを共有しよう【Webゲーム開発】

この記事で伝えたいこと

Web上でゲーム作る際に、モックづくりで役立つ設定や注意点とかとかとか

つくったもの

f:id:s_gozaru:20171219060344g:plain

はじめに

皆さんゲーム作ってますか? クライアントワーク事業部のポロです。
最近はゲームっぽいWebサイトを作る日々を送っております。

ゲーム制作のような規模の大きい制作では、いきなり本制作に着手すると様々な要因で爆死する危険があります。
爆死の危険を回避するのに使えるのが、モック制作です。
本制作に着手する前に、確認したい機能やデザインを工数かけずにさくっと作ってしまいましょう。
(この記事では、モックとかプロトタイプとかの言葉を細かく使い分けません。ご了承を。)

モックづくりのノウハウはあまり言語化されていないことが多いので、
この記事を叩き台に、はてブなどでみなさんのノウハウやアドバイスやコメントなどもらえると励みになります。 このエントリーをはてなブックマークに追加

やったこと

  • package.json を設定して
  • 画面表示物つくって
  • ゲームの挙動を設定して
  • 新しく使うライブラリをAPI確認しながら導入して
  • 効果音探して
  • 適当にパラメタ調整して感想をもらう

作りはじめる際に注意していること

  • なにを確認するつもりで作るのか決めておく。
  • 割込みが起きないように周辺タスクを全て片付けておく。
  • 短い時間でつくる。
    • 1日に収まる規模で作るのが好きです(タイムアタック宣言すると楽しい)
  • 確認に不要な細かな部分に執着しないよう注意する。
  • いつ捨てるのか決めておく。
    • モックがそのまま本制作にならないように、事前にコードを捨てて書き直す旨を宣言しておきます。

package.json

タスクランナーのたぐいは仰々しいと思ってるみなさん、watchifyだけ使うとシンプルなのでオススメです。

{
  "name": "mock",
  "version": "1.0.0",
  "author": "",
  "license": "MIT",
  "scripts": {
    "start": "watchify ./src/app.js -o ./bundle.js -v"
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "babel": {
    "presets": [
      "es2015"
    ]
  },
  "devDependencies": {
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.22.0",
    "babelify": "^7.3.0",
    "watchify": "^3.9.0"
  }
}

import export を使いたいだけなので、シンプルな構成です。
あとは Exponentiation operator を使いたかったら es2016 を、
Async Await 使いたかったら es2017 をといった具合に使いたい機能を随時追加します。

https://github.com/tc39/proposals/blob/master/finished-proposals.md

画面表示物をつくる

円形のゲージが、色を変えながら減っていくことだけ決まっていたので、工数書けずに作りました。

色については緑を128に固定しつつ、他の2つの平均を128にすると見ため綺麗でコスパが良いです。
色見本を付けておきます。 (iOS11っぽいですね)
f:id:s_gozaru:20171219061823p:plain

ゲームの挙動を設定

ここは手慣れているので、ごりごりっと作ります。
過去に作ったマウス or タッチ判定のラッパーなどを見直しつつ使いまわしていきます。

const eventList = {
  pointerstart: new Set(),
  pointermove: new Set(),
  pointerend: new Set(),
};

function initHandler(elm) {
  let isTouch = false;
  let didTouch = false;
  function onPointer(e, evtType, inputType) {
    if (didTouch && inputType === 'mouse') {
      return;
    }
    if (!didTouch && inputType === 'touch') {
      didTouch = true;
    }
    if (evtType === 'pointermove' && isTouch === false) {
      return;
    }
    const rect = e.target.getBoundingClientRect();
    const offsetX = (e.clientX - rect.left) * (elm.width / elm.clientWidth);
    const offsetY = (e.clientY - rect.top) * (elm.height / elm.clientHeight);
    if (evtType === 'pointerstart') { isTouch = true; }
    if (evtType === 'pointerend') { isTouch = false; }
    eventList[evtType].forEach((fn) => {
      fn(offsetX, offsetY);
    });
  }
  function touchHandler(evt) {
    evt.preventDefault();
    const evtType = (() => {
      switch (evt.type) {
        case 'touchstart':
          return 'pointerstart';
        case 'touchend':
          return 'pointerend';
        case 'touchmove':
          return 'pointermove';
        default:
          return null;
      }
    })();
    onPointer(evt.changedTouches[0], evtType, 'touch');
  }
  function mouseHandler(evt) {
    evt.preventDefault();
    const evtType = (() => {
      switch (evt.type) {
        case 'mousedown':
          return 'pointerstart';
        case 'mouseup':
          return 'pointerend';
        case 'mousemove':
          return 'pointermove';
        default:
          return null;
      }
    })();
    onPointer(evt, evtType, 'mouse');
  }
  elm.addEventListener('touchstart', touchHandler, { passive: false });
  elm.addEventListener('touchend', touchHandler, { passive: false });
  elm.addEventListener('touchmove', touchHandler, { passive: false });

  elm.addEventListener('mousedown', mouseHandler, { passive: false });
  elm.addEventListener('mouseup', mouseHandler, { passive: false });
  elm.addEventListener('mousemove', mouseHandler, { passive: false });
}
function addEventStart(evt) {
  eventList.pointerstart.add(evt);
}
function removeEventStart(evt) {
  eventList.pointerstart.delete(evt);
}

function addEventEnd(evt) {
  eventList.pointerend.add(evt);
}
function removeEventEnd(evt) {
  eventList.pointerend.delete(evt);
}

function addEventMove(evt) {
  eventList.pointermove.add(evt);
}
function removeEventMove(evt) {
  eventList.pointermove.delete(evt);
}

export default {
  initHandler,
  addEventStart,
  removeEventStart,
  addEventEnd,
  removeEventEnd,
  addEventMove,
  removeEventMove,
};

新しく使うライブラリの導入

今回は効果音の再生に howler.js を使うので、 その仕様確認と動作確認を行いました。
howler.js はドキュメントが見やすく過保護すぎない素直な挙動をします。みなさんもガンガン使いましょう。

f:id:s_gozaru:20171219065123p:plain

効果音探し

Googleで「適当なクエリ + 効果音」とかで探します。
同人制作クラスタ出身だと、サイト名を見ただけで商用利用やクレジットの可否が判定できるらしいですね。

利用した素材は利用元をファイルにまとめておくと便利です。
今回の場合は、seフォルダ内に以下のように記載しています。

drum02.mp3 ドラム系02 http://www.kurage-kosho.info/others.html
small-bell02.mp3 鈴02 http://www.kurage-kosho.info/others.html
cicada01.mp3 セミ01 http://www.kurage-kosho.info/nature.html

これをしとかないと、あとで権利関係を確認するときに大きな工数がかかります。

スワイプ操作音に蝉の鳴き声を選定できたのが個人的には最高にCoolだったと思っています。
くらげ工匠さん。素晴らしい効果音を自由に使わせていただき、ありがとうございます。)

パラメタ調整 & フィードバック

収穫フェイズです。
うごくものがあるので、 色んな人に見せてフィードバックをもらいます。
(以下の内容は記憶を頼りに書いてるので、正確な内容じゃないです。ご注意を)

  • 「押したと思ったのにMissが出るとストレスなんで、判定広めにしたほうがいいですよ」(音ゲーマー①)
  • 「スワイプのやつ。始点と終点を同時タップで取れました」(音ゲーマー①)
  • 「メータの色いいかんじ」(デザイナー)
  • 「シンクロニカかosuか」(音ゲーマー②)
  • 「いい感じ、あとはちゃんと譜面を作れば面白くなりそう」(ディレクター)

参考文献の情報や、おおまかな方針としてはブレがなさそうなことを確認できました。
また本実装の際に、当たり判定や譜面を柔軟に変更できるよう警戒できました。

個人的に重要な確認事項

あと個人的に重要な確認事項として以下2点があります。

  • 設計に無理がないか

    • 新規導入するライブラリはすんなり設計に収まってくれそうです。
    • updateの返り値で成功失敗消滅など返してみましたが、弱点が多かったので本実装では方針を変えました。
    • 本実装では非同期演出によるフェイズ進行が厄介そうなので、もう少し防御的にコードを組み直しました。
  • パラメタ調整次第で、誰にもクリア不可能と誰でもクリア可能に設定できる

    • 「調整次第でクリアできそうでできないラインに設定できる」ことを経験して確信しておくことで、ゲームの仕組み自体の面白さは疑わずにゲーム開発を進めることができます。

おわりに

いかがだったでしょうか。

使い捨て型プロトタイピングは、迅速さを失わない限り、利点が沢山あるのでどんどん導入していきましょう。
みなさんのモックづくりのノウハウもお待ちしております。
何かコメントなどありましたら、はてブなどから叩きつけてやってください。 このエントリーをはてなブックマークに追加
Web開発者なのでフィードバックをこよなく愛しています。

また、面白法人カヤックではブラウザゲーム開発が好きな方や、生JSを愛してやまない方を募集しております。 www.kayac.com www.kayac.com

明日は手がけたサイトは毎度素晴らしいクオリティを叩き出すふかぽんさんによる
「【WebGL】スプライトアニメーションさせるシェーダー」です。乞うご期待!