LoginSignup
65
48

More than 5 years have passed since last update.

Rails5のActionCableでイカゲームもどきを作ってみた

Last updated at Posted at 2016-10-03

Rails5から追加されたActionCableを使って某人気イカゲームもどきを作ってみました。ActionCableを使えばかなり簡単にWebSocketのリアルタイムな機能が作れます。

できあがりのイメージ

ika2.gif

サンプルのソースコードはGitHubに上がっているので参考にしてみてください。

バトル画面の実装

とりあえずクライアント側もRailsでHTMLとして作るので、バトル用の画面を作ります。

$ rails g controller battle

routes.rb

root to: "battle#index"

battle_controller.rb

class BattleController < ApplicationController

  def index
  end

end

ActionCableを設定・作成

ジェネレータが用意されているので g channel でチェンネルを作ります。こいつがActionCableのロジック本体になります。

$ rails g channel battle attack

battle_channel.rb

class BattleChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def attack
  end
end

続いてActionCableをRailsサーバと一緒に使えるようにマウントしてあげます。実際、本番環境だと別プロセスでやるのが王道なのかな?

routes.rb

mount ActionCable.server => '/cable'

デフォルトだとActionCableは別ホストからは接続できないので、その制限を外してあげます。

development.rb

config.action_cable.disable_request_forgery_protection = true

生成された battle_channel.rb をカスタマイズします。 stream_from でユーザが購読するチャンネル名を指定します。ここでは battle_channel としました。

attack メソッドでは、ブロードキャストで battle_channel 宛に message: "hoge" を送信しています。こうすることで、 battle_channel を購読している全てのユーザにメッセージを送ることが出来ます。

battle_channel.rb

class BattleChannel < ApplicationCable::Channel
  def subscribed
    stream_from "battle_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def attack
    ActionCable.server.broadcast "battle_channel", message: "hoge"
  end
end

WebSocketのサーバ側は上記でOKなので、次はクライアント側を確認します。クライアント側は、assets/javascript ディレクトリに channel が追加されています。中身は、WebSocketからのコールバック関数が定義されています。ここで接続時や切断時、データを受け取った時…などのアクションが組み込めます。

先程作った attack も追加されています。 @perform これを使うことで battle_channel.rbattack メソッドを叩くことが出来ます。

デバッグしやすいように receivedconsole.log を追加しておきます。

javascript/channels/battle.coffe

App.battle = App.cable.subscriptions.create "BattleChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    console.log data

  attack: ->
    @perform 'attack'

まずはテストとして attack メソッドをブラウザから叩いてみます。App.battleはグローバル空間にあるので、簡単に叩けます。 rails s -b 0.0.0.0 としてサーバを起動しておきます。せっかくなので、複数のブラウザを立ち上げておきます。

Chrome / Safariの開発ツール

App.battle.attack()

これで message: "hoge" を受け取れていることが確認できると思います。簡単ですね。基本はこれだけで、あとはゲームっぽい演出を加えていきます。

クリック位置の座標をブロードキャスト

ゲームのやり方として、マウスなどでクリックした場所にインクを飛ばすようにしようと思います。なので、まずはクリック位置の座標を全員に送る必要があります。

通常の battle.coffee にクリックイベントを仕込みます。

javascripts/battle.coffee

$ ->
  $(window).click (e)->
    position = { x: e.pageX, y: e.pageY }
    App.battle.attack(position)

クライアントから送られた座標を全員にブロードキャストします。

channels/battle.coffee

App.battle = App.cable.subscriptions.create "BattleChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    console.log data

  attack: (position) ->
    @perform 'attack', position: position

battle_channel.rb

  def attack(position)
    ActionCable.server.broadcast "battle_channel", battle: position
  end

クリック位置にドットを表示するように実装

最後に受け取った座標を元に描画します。例だと、単なる1pxのドットなので全然見分けがつきませんが、これをインクの画像にすればOKです。

channels/battle.coffee

  received: (data) ->
    console.log data
    attack_point = $('<div>')
    attack_point.css('position', 'absolute')
    attack_point.css('top', data.battle.position.y)
    attack_point.css('left', data.battle.position.x)
    attack_point.css('width', '1px')
    attack_point.css('height', '1px')
    attack_point.css('background', '#f00')
    $('body').append attack_point

以上で、イカゲームの基礎は出来上がったも同然です。簡単ですね。あと以下のような機能を実装すればよりゲームらしく感じると思います。GitHubにアップしているソースコードでは荒いですが実装しているので参考にしてみてください。

  • マッチング
  • タイム機能
  • 勝敗判定

気になった・はまった所紹介

マッチング

対戦させるためにマッチングが必要になりますが、こんかいは1つの固定の部屋で戦うようにしたのでChannelは1つです。ただ、普通は複数の部屋で戦うと思うので、ちょっとした工夫が必要になります。

例としてはこんな感じです。1対1を想定しています。まず、マッチング用のSeekモデルを作って、Redisのリストにユーザをぶち込みます。誰か既にいたらマッチング成功。ゲームスタート。そして、専用の部屋を作成し、RoomIDをそれぞれのChannelに通知して互いに対戦します。

class GameChannel < ApplicationCable::Channel
  def subscribed
    stream_from "game_player_#{current_user.id}"
    Seek.create(current_user.id)
  end
end
class Seek
  def self.create(user_id)
    if opponent = REDIS.spop("seeks")
      #TODO: ここでマッチング成立ゲーム開始
      Rails.logger.debug "マッチング成功: user_id: #{user_id} / opponent_id: #{opponent}"
      Game.start(user_id, opponent)
    else
      REDIS.sadd("seeks", user_id)
    end
  end

  def self.remove(user_id)
    REDIS.srem("seeks", user_id)
  end

  def self.clear_all
    REDIS.del("seeks")
  end
end
class Game
  def self.start(user_id1, user_id2)

    # ここで対戦用のChannelを保存、Channel名をそれぞれのプレイヤーに通知する
    room = Room.create(name: "room_#{user_id1}_#{user_id2}")

    ActionCable.server.broadcast "game_player_#{user_id1}", { action: "battle_start", room_id: "#{room.id}" }
    ActionCable.server.broadcast "game_player_#{user_id2}", { action: "battle_start", room_id: "#{room.id}" }
  end
end

インクの描画

インクはSVG画像を使っています。色んな色を使いたかったので、CSSでSVGの色を塗りつぶして再利用することにしました。が、SVGファイルだと外部CSSが効かないというアレがあるので、Ruby側でインライン化したりしました…。地味に面倒。

.material_svgs
  - (1..12).each do |i|
    div id="ink-#{i}"
      == File.read(Rails.root.join("app/assets/images/#{i}.svg"))

勝敗判定

今回のイカゲームは、塗りつぶした領域が多いほうが勝ちになります。なので、ゲーム終了後にどの色が一番多いかを判定しなくちゃいけないんですが、実装の時間がなかったのでサーバ側でログ判定せずに、雑にクライアントのブラウザ側で画像を合成して、サーバに投げてImageMagickで解析しています。

  • インクのSVGファイルを全て<img>タグに変換
  • <img><canvas>に追加
  • <canvas>をBASE64でサーバに投げる
  • サーバはImageMagickでどの色が多いかを解析し、クライアントに返す
  • 受け取ったクライアントはそれを表示

このロジック、けっこう穴があって、全ユーザが上記の処理を行うため、ブラウザの大きさに寄って勝敗が変わってきてしまいます。まぁそこらへんはご愛嬌ということで。あと、SVGでやっちゃったけど、最初から全部canvasでやれば負荷とかも軽くなったのかもしれない…。

65
48
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
48