Phoenix の Channel で MMORPG的チャットを作ってみる

この記事は [Elixir Advent Calendar 2016](http://qiita.com/advent-calendar/2016/elixir) 18日目の記事です。

# はじめに

ElixirではなくPhoenixによった話になります。

僕は青春時代の大半をMMORPG(メイプルストーリー)の中で過ごした人間です。
僕がMMORPGで好きなのところはゲーム部分はもちろんですけど、一番楽しかったのはチャットで、
ほとんどチャットのためだけにダラダラとログインする日々でした。
みなさんもそんな経験があるのではないでしょうか？

PhoenixにはChannelという機能がありまして、簡単にリアルタイム通信をすることができます。
この機能をつかって、あの一番楽しかった場所を再現することにしました。

# Phoenixとは？

ElixirのRuby on Rails風のWebアプリケーションフレームワークです。
本当にRails"風"なだけで全然違うなぁと思っています。

個人的にはRailsは暗黙の了解的に様々なものを省略して覚えゲーだなぁと思うことも多いのですが、
Phoenixは明示的に指定するのでわかりやすく、コードを追いやすいなぁという気がしています。

http://www.phoenixframework.org/

詳しくは以下を読むと良いかもしれません。

* http://qiita.com/mizchi/items/e38d3f0461fe316021f7
* http://qiita.com/mserizawa/items/f32469bcac61aa6fa491

# Channelとは？

Phoenixの機能で、簡単にWebsocketによる通信が実現できるやつです。
公式のドキュメントを観て試しに動かしてみるのが一番わかり易いかと思います。

http://www.phoenixframework.org/docs/channels

クライアントの実装はJSのみならず、iOS, Android, C# もあるそうです。

使ってみると本当に拍子抜けするくらい簡単でした。
夢が広がります。

# MMORPG的なチャットを作る

MMORPG的なチャットは特徴として以下を持ちます。

* 参加している人たちが見える
* 移動することができる
* チャットすることができる

## 成果物

https://powerful-mesa-92106.herokuapp.com/

![game.gif](https://qiita-image-store.s3.amazonaws.com/0/40562/a2421299-7aa5-0f7a-87f4-6be8e63bd8f3.gif)

* 自分のコマは赤色、他のプレイヤーは青色で表示されます。
* キーボードの w(上), s(下), a(左), e(右)で移動できます。
* 上部のフォームでテキストを入力すると文字が表示されます。

## サーバー側実装

```elixir
defmodule ChannelSample.RoomChannel do
  use ChannelSample.Web, :channel

  @init_x 100
  @init_y 100

  def join("room:lobby", _, socket) do
    socket = assign(socket, :user_id, user_id(socket))
    {:ok, %{body: %{user_id: user_id(socket)}}, socket}
  end

  # ユーザーがログインした事をbroadcast
  def handle_in("login", _map, socket) do
    user = %{x: @init_x, y: @init_y, user_id: user_id(socket)}
    ChannelSample.ExistingUsers.record(user)
    broadcast! socket, "login", %{body: user}
    {:noreply, socket}
  end

  # メッセージが来たことをbroadcast
  def handle_in("new_message", %{"body" => body}, socket) do
    broadcast! socket, "new_message", %{body: %{user_id: user_id(socket), text: body["text"]}}
    {:noreply, socket}
  end

  # 現在ログインしているユーザーの一覧を返す
  # ChannelSample.ExistingUsers はAgentとして実装しています。
  def handle_in("fetch_all", _map, socket) do
    users = ChannelSample.ExistingUsers.all
    |> Enum.reject(fn {k, v} -> k == user_id(socket) end)
    |> Map.new
    {:reply, {:ok, %{body: %{users: users}}}, socket}
  end

  # ユーザーが動いた事をbroadcast
  def handle_in("move", %{"body" => body}, socket) do
    user = Map.merge(convert_body(body), %{user_id: user_id(socket)})
    ChannelSample.ExistingUsers.record(user)
    broadcast! socket, "move", %{body: user}
    {:noreply, socket}
  end

  # ユーザーの接続が切れたらlogoutしたことをbroadcast
  def terminate(_map, socket) do
    broadcast! socket, "logout", %{body: %{user_id: user_id(socket)}}
    ChannelSample.ExistingUsers.remove(user_id(socket))
    {:stop, :shutdown, socket}
  end

  defp convert_body(body) do
    %{x: body["x"], y: body["y"]}
  end

  defp user_id(socket) do
    Map.get(socket.assigns, :user_id) || Integer.to_string(:rand.uniform(1000000))
  end
end
```

## クライアント側実装

phina.jsを使用して作成しました。

http://phinajs.com/

すごく省略するとこんな形です。
なんとなくイメージはわかるかと思います。

### 初期化処理

```javascript
        ...
        // チャンネルに接続
        channel.join()
            .receive("ok", resp => {
                // ユーザーidを取得する
                this.user_id = resp.body.user_id;
            })

        // ログインする
        channel.push("login");

        channel.push("fetch_all")
            .receive("ok", payload => {
                // 現在ログインしているユーザーの分のオブジェクトを作成する
            });

        channel.on("move", payload => {
            // idを指定して動かす
        });

        channel.on("login", payload => {
            // ユーザーのオブジェクトを作成する
        });

        channel.on("logout", payload => {
            // 該当するユーザーオブジェクトを削除する
        });

        channel.on("new_message", payload => {
            // 該当するユーザーのコメントを表示させる
        });
        ...
```
### 各フレームの処理

```javascript
        // 左右移動
        if (keyboard.getKey('a')) {
            this.users[this.user_id].x -= 8;
        }
        if (keyboard.getKey('e')) {
            this.users[this.user_id].x += 8;
        }
        // 上下移動
        if (keyboard.getKey('w')) {
            this.users[this.user_id].y -= 8;
        }
        if (keyboard.getKey('s')) {
            this.users[this.user_id].y += 8;
        }

        if (keyboard.getKey('a') || keyboard.getKey('e') || keyboard.getKey('w') || keyboard.getKey('s')) {
            channel.push("move", {body: {x: this.users[this.user_id].x, y:this.users[this.user_id].y}});
        }
```
### メッセージフォームの処理

```javascript
var messageFormButton = document.getElementById("message-form-button");
messageFormButton.addEventListener("click", () => {
    let messageFormText = document.getElementById("message-form-text");
    channel.push("new_message", {body: {text: messageFormText.value}});
    messageFormText.value = "";
})
```

# まとめ

* PhoenixはElixirのWebアプリケーションフレームワークです。
* PhoenixのChannelを使用すると簡単にリアルタイム通信ができます。
* Channelを使用してMMORPG風のチャットを作りました。

サーバー側の実装はそんなに時間はかからなかったのですが、javascript(というかphina.js)で悩む時間が多かった・・・

リアルタイム通信を使ったアプリケーションが簡単に作れるなんて素晴らしい時代ですね。
アイデア次第で面白いことができそうです。