爆速でwh.imのゲームを実装する(じゃんけん編)

# はじめに

こんにちは。普段は情報系の学科で大学生をしている者です。

[この記事](　https://qiita.com/aitaro/items/8a97d320f5586c6e7bb6)に紹介されている、[wh.im](https://wh.im)(ウィム)というサービスの立ち上げに関わっているのですが、その一環でwh.im上で楽しめるゲームを開発しました。

このサービスの特徴として、**誰でもゲームを投稿** できます！そのやり方を知っていただきたく、前回に引き続き記事を書きますので、興味を持った方はぜひゲーム開発を試してみてください！

アプリを公開するまでの一連の流れは、[前回の記事](https://qiita.com/ttttkkkkk31525/items/2a3de27aec88219fed86)にありますので、併せてご覧ください。

Vue.js以外でも実装は可能ですが、今回はVue.jsに限ってご紹介できればと思います！

今回は実際に遊ぶことのできる[じゃんけん](https://github.com/whimRTC/whim-janken)を例に挙げて、実装方法を説明いたします。

#### 5/28更新
- 続編記事「[爆速でwh.imのゲームを実装する(NGワードゲーム編)](https://qiita.com/ttttkkkkk31525/items/702a1259077ab7f80460)」が公開されました。
- ゲームの開発環境が変更となりました。詳細は「[wh.imで自作ゲームを登録するまで](https://qiita.com/ttttkkkkk31525/items/2a3de27aec88219fed86)」をご覧ください。
- [デバッグが可能となりました。](https://qiita.com/ttttkkkkk31525/items/c0e4eec05d7cb3600c0e#このゲームのデータ構造)

#### 6/13更新
- より丁寧に解説した記事「[【Vue.js】半日でできる、ビデオチャット上で動くオリジナルゲームの開発から公開まで。](https://qiita.com/aitaro/items/3a9ae5aba5313d7b9a44)」が公開されています。そちらも併せてご活用ください。

# まずは開発環境で実際に動かしてみる(5/28更新)

実際に[じゃんけん](https://github.com/whimRTC/whim-janken)を開発環境で動かしてみたいと思います。

まず

```bash

$ cd ダウンロードしたいディレクトリ
$ git clone https://github.com/whimRTC/whim-janken.git
```

とします。そして

```bash
$ cd whim-janken
$ yarn # or npm install
$ yarn serve # or npm run serve
```

とします。yarnまたはnpmがインストールされていない場合はインストールしてください。
すると`localhost:8080`にゲームが起動します。

そして、wh.imから「遊び場」へ入室し、そのアドレスの末尾に&develop=trueをつけます。するとwh.imが開発者用のモードとなります。右上のメニューの「アプリを選ぶ」から「開発用(port:8080)」を選ぶことにより、自分の手元でゲームを試すことができます。

![janken.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365308/3b3e6b58-ef6e-8ca9-bc77-067df3f02871.png)

このように表示されれば成功です！


# 実際のコードを見てみる

続いて、実際のコードを使いながら通信方法を説明していきます。ここは全くじゃんけんと同じなので、確認不要な方は飛ばしてしまってかまいま

まず表示される画面が書かれている`src/App.vue`をご覧ください。

```vue:src/App.vue
<template>
  <div id="app">
    <Main class="main" />

    <Player
      v-for="user in $whim.users"
      :key="user.id"
      :class="whimUserWindowClass(user)"
      :displayUser="user"
    />
  </div>
</template>
<script>
export default {
  name: "App",
  components: {
    Main: () => import("@/components/main/Index"),
    Player: () => import("@/components/player/Index")
  },
  computed: {
    users() {
      return this.$whim.users;
    }
  }
};
</script>
```

このゲームではMain画面とPlayer画面に分かれて実装されています。そのため、`App.vue`内で`Main`、`Player`の2つのコンポーネントを呼び出しています。
Mainは画面中央部の画面を、Playerはそれぞれのユーザーのいる場所に表示される画面を表します。
`Main`、`Player`の実装はそれぞれ、 `src/components/main/Index.vue`、`src/components/player/Index.vue`に実装があります。

## wh.imを経由した通信の方法

`App.vue`で`$whim.users`という呼び出しがあります(`this.$whim.users`の省略形です)が、これは`whim-client-vue`というパッケージに入っています。このようにすることで、**`this.$whim`から始まる関数だけで、利用者間の非同期通信まわりは全てできるようになっています。**

ここに扱える関数一覧を示します。scriptタグ内では適宜`this`を先頭に付けてください。

### 状態取得(呼び出すたびに通信する)

| コード           | 型     | 説明                           |
| ---------------- | ------ | ------------------------------ |
| $whim.users      | [User] | ルームに入っているユーザー一覧 |
| $whim.room       | Room   | Room Object                    |
| $whim.accessUser | User   | 現在アクセスしているUser       |
| $whim.state      | State  | ゲームの状態(**自由に設計可能**)          |

### 状態変更
| コード                     | 引数   | 説明                                                         |
| -------------------------- | ------ | ------------------------------------------------------------ |
| $whim.assignState(Object)  | Object | ゲーム情報を追記更新、 
 <br />存在しないキーの場合：追記 <br />存在するキーの場合：更新 |
| $whim.resetState(Object) | Object | ゲーム情報を渡されたObjectにすべて変える                     |


これでは分かりにくいと思うので、後ほどのコードで使っている部分を見ながら、理解していただけると助かります。
より詳細な説明は、[開発者ドキュメント](https://docs.wh.im/developer/whim-client-vue-api/)をご覧ください。

## このゲームのデータ構造
stateはゲームに合わせて**自由に設計**することができます。
今回のゲームでは、ユーザーのIDをキーとしてじゃんけんの手`('rock', 'scissors', 'paper')を値に持つObjectとします。

これらの変数は開発環境ではどんな時でも見ることができます。右上のメニューから「SHOW APP STATE」を選択すると

<img width="200" alt="image.png" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365308/fa40d7a0-3fc8-348f-f937-3dc1ae5bf894.png />

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365308/4b4796b6-7129-92cd-8d1d-305b8199c173.png)


白い部分には私のuserIdが表示されています。

このように、全員のじゃんけんの手を確認することができます。うまくやれば、カンニングすることもできます(友達とやるときはやめましょう)。

## Main
次にMain画面のコードについて説明していきます。Main画面は書くプレイヤー上にに表示されます。じゃんけんゲームでは、各プレーヤーが選択中か選択済みかがボタンを出しています。

```vue:src/components/player/Index.vue
<template>
  <div class="container">
    <a v-if="isAllSelected" class="btn" @click="reset">もう一度</a>
    <Me v-if="!isAccessUserSelected" class="me" />
  </div>
</template>

<script>
export default {
  name: "Main",
  components: {
    Me: () => import("@/components/main/Me")
  },
  computed: {
    users() {
      return this.$whim.users;
    },
    isAllSelected() {
      return (
        this.users.length > 0 &&
        this.users.every(user => this.$whim.state[user.id])
      );
    },
    isAccessUserSelected() {
      return !!this.$whim.state[this.$whim.accessUser.id];
    }
  },
  methods: {
    reset() {
      this.$whim.resetState();
    }
  }
};
</script>
<!-- 以下略 -->
```

ここで`computed`プロパティで`users`と`isAllSelected`の値を定義しています。
じゃんけんでは、`state[user.id]`にじゃんけんの手の状態が入るようになっていて、`isAllSelected`では全員が選択しているかどうかを判定しています。つまり、`state[user.id]`が全員`undefined`でないかどうかを判定しています。`users.length > 0`としているのは、データベースから`user`が取得できていない状態での判定を防ぐためです。
このようにVue内で先ほど定義した関数を用いることでゲームを実装できます。

`Me`コンポーネントについては次で説明します。

## Me

```vue:src/components/player/Me.vue
<template>
  <div>
    <h2 class="subtitle">
      選択してください！
    </h2>
    <div>
      <img src="@/assets/rock.png" class="img" @click="select('rock')" />
      <img
        src="@/assets/scissors.png"
        class="img"
        @click="select('scissors')"
      />
      <img src="@/assets/paper.png" class="img" @click="select('paper')" />
    </div>
  </div>
</template>
<script>
export default {
  name: "Me",
  methods: {
    select(show) {
      this.$whim.setState(this.$whim.accessUser.id, show);
    }
  }
};
</script>
```

じゃんけんの手を選択したときに、`select(show)`メソッドが呼ばれ、 

```js
this.$whim.setState(this.$whim.accessUser.id, show);
```

によってデータベースの`state[accessUser.id]`に自分の手が登録されます。


## Player

次にやや複雑なのですが、Player画面を実装していきます。


```vue:src/components/player/Index.vue
<template>
  <div class="container">
    <Result v-if="isAllSelected" :displayUser="displayUser" />
    <h2 v-else-if="isAccessUserSelected && isSelected" class="subtitle">
      選択済み
    </h2>
    <h2 v-else-if="isAccessUserSelected" class="subtitle">
      選択中
    </h2>
  </div>
</template>
<script>
export default {
  name: "Player",
  components: {
    Result: () => import("@/components/player/Result")
  },
  props: {
    displayUser: {
      type: Object,
      required: true
    }
  }, // 表示されているUserの情報
  computed: {
    users() {
      return this.$whim.users;
    },
    isMe() {
      return this.$whim.accessUser.id === this.displayUser.id;
    },
    isAccessUserSelected() {
      return !!this.$whim.state[this.$whim.accessUser.id];
    },
    isSelected() {
      return !!this.$whim.state[this.displayUser.id];
    },
    isAllSelected() {
      return (
        this.users.length > 0 &&
        this.users.every(user => this.$whim.state[user.id])
      );
    }
  }
};
</script>
```

`computed`の内容を中心にご説明します。

まず、先ほどと同じ`isAllSelected`がすなわち全員が手を選択した状態を表していて、これが`true`のとき`Result`コンポーネントを表示します。

また、`isSelected`で`this.$whim.state[this.displayUser.id]`、すなわちデータベース上の`state[画面の人のID]`を取得していて、その画面の人がどのじゃんけんの手を選んだ状態かどうかが得られています。`this.displayUser`がこの画面のプレーヤーを表すことに注意してください(`displayUser`の情報は`App.vue`で渡されています)。`isSelected`が`true`である、すなわち選んだ状態のとき、画面に「選択済み」が表示されます。

次に、`isMe`でこの画面が自分が映っている画面であるかを取得しています。これが`true`のときに表示される、自分のじゃんけんの手を決める画面は`Me`というコンポーネントで実装されています。

そして、上のいずれの状態でもないときに、「選択中」と表示します。

## Result

```vue:src/components/player/Result.vue
<template>
  <div>
    <h2 class="subtitle">
      結果
    </h2>
    <div>
      <img
        v-if="show === 'rock'"
        src="@/assets/rock.png"
        width="150"
        height="150"
      />
      <img
        v-if="show === 'scissors'"
        src="@/assets/scissors.png"
        width="150"
        height="150"
      />
      <img
        v-if="show === 'paper'"
        src="@/assets/paper.png"
        width="150"
        height="150"
      />
    </div>
  </div>
</template>
<script>
export default {
  name: "Result",
  props: {
    displayUser: {
      type: Object,
      required: true
    }
  },
  computed: {
    show: function() {
      return this.$whim.state[this.displayUser.id];
    }
  }
};
</script>
<style　lang="scss" scoped></style>
```
`state[ユーザーのid]`には手が保存されているので`show`でプレーヤーの手を返します。`displayUser`は呼び出し元の`Player`から渡されています。


以上のコードがあれば、じゃんけんゲームが正常に動きます。

# 最後に

今回は、ゲームの中では最も単純なじゃんけんゲームについて説明しました。次回はNGワードゲームの実装について説明したいと思っています。乞うご期待！
#### 5/28更新
「[爆速でwh.imのゲームを実装する(NGワードゲーム編)](https://qiita.com/ttttkkkkk31525/items/702a1259077ab7f80460)」が公開されました。