【1週間】Web ゲームを爆速で開発するために実践したこと🐧個人開発向け

(内心)
作ったゲームを沢山の人に触ってもらってリアクションが欲しいなぁ～
そうだ、記事投稿して宣伝しよう！

といった**下心で書いた記事です**。すみませんでした 遊んでください😆

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Web Game 処女作🎉<br>シンプルでストレス発散になるゲームが爆誕！<br>敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆<a href="https://t.co/h8w574aGGQ">https://t.co/h8w574aGGQ</a><br><br>★アドバイス急募★<br>『こうしたらもっと面白くなる』など、ご意見ご感想を頂けると嬉しいっす(〃´∪｀〃)ゞ<a href="https://twitter.com/hashtag/%E4%BB%8A%E6%97%A5%E3%81%AE%E7%A9%8D%E3%81%BF%E4%B8%8A%E3%81%92?src=hash&amp;ref_src=twsrc%5Etfw">#今日の積み上げ</a> <a href="https://twitter.com/hashtag/100DaysOfCode?src=hash&amp;ref_src=twsrc%5Etfw">#100DaysOfCode</a> DAY 21 <a href="https://t.co/HJvFj87zTf">pic.twitter.com/HJvFj87zTf</a></p>&mdash; hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a href="https://twitter.com/hikaru_firecamp/status/1273289660186292224?ref_src=twsrc%5Etfw">June 17, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

> ペンギンがサメさんを倒すゲーム
> 敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆
> https://games.westa.io/

## 0⃣ 結論 / 爆速で開発するために実践したこと
- １週間のカウントダウンタイマーを設置する
- 適当な企画書を作って全体を把握しならが作業する
- 新しいことは１個までに制限して知っているものを使う
- こだわりたい部分を絞り他を捨てる覚悟を持つ

## 1️⃣ この記事の対象読者

この記事では**１週間で Web ゲームを作る為に何を考え何をしたのか**を書きました。
以下に当てはまる人達に参考になれば嬉しいです。

- 好きなことへのこだわりが強く**いつまでも作品が完成しない**人
- いつも**ダラダラ期限を伸ばしてしまう**人
- このゲームがどんなライブラリ使っているか知りたい人
- 奇特な人

## 2️⃣ なぜ１週間なの？
個人開発の一番の敵って**いかにモチベーションを保つか**だと思いませんか？

私の場合１ヶ月もたてば他に興味あることが出てきてしまうのでモチベーションが移ってしまいます。
でも、**１週間なら全力で頑張れるちょうどよい長さ**かな～と考えました。

(それと、１週間ならクオリティ低くても言い訳になるかな と)

## 3⃣ １週間という短い期間で終わらせるために意識したこと

### (1) １週間のカウントダウンタイマーを設置する

いつもズルズル伸ばしてしまう癖があるので **Twitter で公開日を宣言**しました。
そして公開日までの**カウントダウンタイマーを常に目に入る位置に設置**しておきました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">次に出す Web アプリのリリース日を 6/17 12:00 頃に決めました🎉<br>メリハリ付けるために中途半端でもこのタイミングで出します<br><br>写真はちょうどいいカウントダウンタイマーが無かったので作ってみました<br><br>▪コードはこちら<a href="https://t.co/HiGjiROP4R">https://t.co/HiGjiROP4R</a><br>(8,9行目を編集すれば自分用に使えるよ！) <a href="https://t.co/HnjXxktDLb">pic.twitter.com/HnjXxktDLb</a></p>&mdash; hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a href="https://twitter.com/hikaru_firecamp/status/1269807592533979138?ref_src=twsrc%5Etfw">June 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このカウントダウンタイマー、**すっごく効果があった**ように思います。度々目に入るので**程よい緊張感と無機質な圧力**をくれます。
さらにシレっと**周囲の人たちに公開日を宣言**できるのでかなりお勧めです。

> ⚠ 補足 ⚠
> さも予定を守ったように言ってますが結局半日ほど遅く公開してしまいました
> ほ ん と す み ま せ ん で し た 😗


### (2) 適当な企画書を作って全体を把握しならが作業する

企画書などが無い状態でコーディングすると目に付いた場所から手を付けてしまいがちで**視野が局所的になりやすく作業の優先度決めが難しく**なります。

簡単なものでも**全体像を把握できるもの**があると、次にどの部分を実装するか考えるときに『最低でもここを実装しないとだから優先度高めで』みたいな**スケジュール管理が容易**になります。

今回は以下の様な簡易仕様書をあらかじめ書いておりました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これから作るミニゲームの簡易仕様書！<br>よくあるクリックゲームですが、ちゃんと作りきれるか不安😇<br><br>１週間がんばります〜<br>(あ、奇しくも一人 web1week みたいになってる)<br><br>全然関係ないけど最近 iPad でお絵描きの練習始めました！スマブラやったことあればピンとくるはず😎 <a href="https://t.co/oVo5xAUeBe">pic.twitter.com/oVo5xAUeBe</a></p>&mdash; hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a href="https://twitter.com/hikaru_firecamp/status/1270280818116583425?ref_src=twsrc%5Etfw">June 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


これのおかげで実装段階での仕様変更が減り、**一直線にゴールに向かって実装できるので結構大切**なものだと思います。

### (3) 新しいことは１個までに制限して知っているものを使う

初めて使う道具が多いと勉強することが増えたり些細なことにはまったりして時間を消費しがちなので、**使い慣れた開発環境**と**使った事のあるライブラリ**を中心に選定しました。

#### 使い慣れたもの
- **VS-Code** ... 開発環境
- **TypeScript** ... 開発言語
- **UIKit** ... UI コンポーネント

#### 以前から実験レベルで遊んでいたもの
- **Three.js** ... 3D 描画エンジン
- **Cannon.js** ... 3D 物理エンジン

####  初めて使うもの
- **Audio API** ... BGM や効果音の再生


今回は遊べる Web ゲームを**短期間で作ることが目的**だったので冒険をしない制約を設けましたが、完成度が低くても**未知なる技術を沢山学びたいならどんどん新しい技術を使ってみたらよい**と思います。

**目的次第でやり方を臨機応変にする**ことが大事なのかなと😎


### (4) こだわりたい部分を絞り他を捨てる覚悟を持つ

こだわりが強いと味のあるイイものができるけど、その代わり**完成が遅くなる**傾向にあると思います。

今回は『ローポリキャラ達が**物理演算で予想外の挙動**をする』部分だけこだわりましたが、操作 UI の見た目などはブラウザ標準のプログレスバーを使っていたりと**大部分はかなり適当**です。

個人的にはゲームの中に**ブラウザの DOM を混ぜることに違和感**とアレルギーを感じるのですが『**こだわる部分を絞って他は適当**』と大胆に割り切っちゃいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/ef433b3e-8d54-62b9-94b9-83f98f528ade.png)

他にも、ペンギン/サメ/コイン/木 の当たり判定はすべて立方体(正六面体)で手抜きをしましたが、これは予想に反して『**サメさんやコインが地面に刺さってる**』ように見えたり『木々があらぶってる』感じになったり**ヘンテコな世界観の演出**に一役買ったように思います。
![2020-06-18_00h32_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/a1a1764b-8e99-17a7-de03-a410ae195d96.png)


## 4⃣ おまけ: 実装 Tips

唐突なおまけの実装 Tips です。

### (1) Cannon.js / サメさんの Z 位置(奥行)を固定

Cannon.js は 3 次元物理演算なので XYZ 軸分の動きがあるのですが、サメさん(敵)に関しては奥行方向に動かれると攻撃を当てられなくなるので Z 軸を固定しています。

```ts
// CannonJs: サメさんの物理演算用の剛体を生成
const sharkBody = new CANNON.Body({ ＜省略＞ });

// ThreeJs: アニメーションループを開始
renderer.setAnimationLoop(() => {
    // CannonJs: 1フレーム分の物理演算を実行
    world.step(＜省略＞);

    // ★★ サメさんのZ位置を固定する ★★
    sharkBody.position.set(
        sharkBody.position.x,
        sharkBody.position.y,
        0); // Z軸を0に変更
});
```

### (2) Cannon.js / 摩擦設定・反発設定

初期値だとほとんど滑ることは無く、滑らせた方が面白そうだったので摩擦と反発の設定をいじっています。

モデルごとに摩擦係数や反発係数を設定できれば直感的だったのですが、Cannon.js では『モデル１とモデル２に対しての摩擦・反発を設定』といった具合に設定が必要でした。

```ts
// 
// 物理演算ワールドを初期化
// 

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // m/s²

const floorBodyMaterial = new CANNON.Material(`FloorModel`);
const penguinBodyMaterial = new CANNON.Material(`PenguinModel`);
const sharkBodyMaterial = new CANNON.Material(`SharkModel`);

// 摩擦反発設定: 床とペンギン
world.addContactMaterial(new CANNON.ContactMaterial(
    floorBodyMaterial,
    penguinBodyMaterial,
    {
        friction: 0.01, // 摩擦設定 (ペンギンが床を滑るように)
        restitution: 0.8, // 反発設定
    }
));

// 摩擦反発設定: 床とサメ
world.addContactMaterial(new CANNON.ContactMaterial(
    floorBodyMaterial,
    sharkBodyMaterial,
    {
        friction: 0.05, // 摩擦設定
        restitution: 0.3, // 反発設定
        contactEquationStiffness: 1e8,
        contactEquationRelaxation: 3,
    }
));

// 摩擦反発設定: ペンギンとサメ
world.addContactMaterial(new CANNON.ContactMaterial(
    penguinBodyMaterial,
    sharkBodyMaterial,
    {
        friction: 0.01, // 摩擦設定
        restitution: 2.0, // 反発設定 (攻撃を受けたサメが吹っ飛びやすいように)
    }
));
```

### (3) Three.js / 同じモデルは使いまわして効率化

今回ゲーム内では**同じモデルが大量に出現**します。その際に**毎回モデルをロードしていると実行効率が悪い**ので、モデル管理クラスが必要になります。

こういった目的の管理クラス系にはシングルトン実装が最適かと思いますが、TypeScript では **`module` を使うと簡単にシングルトン実装が可能**です。

```ts
/** モデル名の型定義 */
export type ModelName = `PENGUIN` | `SHARK` | `TREE` | `COIN`;

/** 
 * ゲーム内のモデルを管理するモジュール
 *　 シングルトン実装
 * 　モデルをあらかじめロードして、ロード済みのモデルを使いまわすことで効率化
 */
export module ModelManager {
    /** ロード済みのモデルを保持 */
    const modelMap = new Map<ModelName, THREE.Object3D>();

    /** 各種モデルをロードする */
    export async function load() {
        await loadModel(`PENGUIN`, `models/PenguinJumping.glb`);
        await loadModel(`SHARK`, `models/Shark.glb`);
        await loadModel(`TREE`, `models/Tree.glb`);
        await loadModel(`COIN`, `models/Coin.glb`);
    }

    /** モデルを複製して取得する */
    export function getModel(modelName: ModelName) {
        return modelMap.get(modelName)!.clone();
    }

    /** ロード済みモデルを開放する */
    export function dispose() {
        // ThreeJs: ロードしたモデルをすべて解放
        modelMap.forEach((obj3D, key) => {
            GameUtils.disposeObject3D(obj3D);
        });
    }

    async function loadModel(modelName: ModelName, path: string) {
        // ThreeJs: モデル読み込み
        const obj3D = await GameUtils.loadGltfModel(path);

        // リストに追加
        modelMap.set(modelName, obj3D);
    }
}
```

使い方

```ts
// あらかじめすべてのモデルをロードする
await ModelManager.load();

// ペンギンモデルを取得する (内部的にはロード済みのモデルを複製しているので効率的)
const penguin1 = ModelManager.getModel(`PENGUIN`);
const penguin2 = ModelManager.getModel(`PENGUIN`);
const penguin3 = ModelManager.getModel(`PENGUIN`);
```

## 最後に

ゲーム開発中に Twitter でいいねやコメントなどでリアクションをくれた方、開発中のゲームを試してヒントをくれた友人方、本当にありがとうございました。
大変励みになりモチベーションになりました、重ねてお礼申し上げます😆

あと、ここまで読んでまだプレイしていない人！！
↓ やってからリアクションをクレクレ厨😗

> ペンギンがサメさんを倒すゲーム
> 敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆
> https://games.westa.io/

## 宣 伝 😆

最近書いた記事です。よかったら合わせて見てくれると嬉しいです(〃´∪｀〃)ゞ
[【239 種無料】Everything というゲームの 3D モデルがオープン化されました🎉モデルの分離方法を紹介](https://qiita.com/alclimb/items/ceccf46dfaf8546c6a93)