📶

Rails の ActionCable と Godot の WebSocketPeer で通信する

に公開

Rails と Godot 間での WebSocket 通信がとりあえずローカル環境で動いたので、備忘のため残します。

試したバージョンは以下です。

  • Rails 8.0.2
  • Godot 4.4.1

動かした動画です。(※Godotのキー入力でメッセージを行って来いしてるだけの動画です。)
https://youtu.be/wTefTLuv8EQ

Rails 側の設定

Rails 以外からも接続を受け付けるようにする

デフォルトでは接続を受け付けないので、config.action_cable.disable_request_forgery_protectiontrue にする。

config/environmetns/development.rb
Rails.application.configure do
  # 略
  # Uncomment if you wish to allow Action Cable access from any origin.
  config.action_cable.disable_request_forgery_protection = true

チャネルを作成

$ rails g channel Xxx でチャネルを作成する

app/channels/foo_channel.rb
class FooChannel < ApplicationCable::Channel
  def subscribed
    stream_from stream_name
  end

  # GDScript 側の `perform` で実行したものを受信
  def foo(data)
    puts data
    broadcast bar: 1 # 送信テスト
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  private
    def stream_name
      "foo_#{params[:name]}"
    end

    def broadcast(data)
      ActionCable.server.broadcast stream_name, data
    end
end

Redis か solid_cable を使う

メッセージの送信を rails console などから行えるようにするには、ActionCable デフォルトのアダプタの設定ではなく、Redis か SolidCable にする必要があります。

SolidCable って知らなかったんですが、Rails 8 から追加された機能らしいです。
参考
https://techracho.bpsinc.jp/hachi8833/2024_11_11/146390

Godot 側の設定

ActionCable の仕組み

ActionCable は JSON でメッセージのやり取りをしています。
どういうメッセージを渡していたかは、Javascript 側の実装 を見ながら確認しました。

以下概要

  1. WebSocket open
  2. => Rails からの { "type": "welcome", ... } メッセージをクライアント側で受信
  3. <= Rails に { "command": "subscribe", "identifier": {"channel": "XxxChannel", ... } } をクライアント側からメッセージ送信。ここで識別に必要な情報を一緒に渡す。
  4. Rails の XxxChannelsubscribed メソッドが呼ばれる。識別するため、stream_from を設定
  5. 接続確立
  6. => Rails から broadcast で渡したメッセージは、{ "message": ... } の形式でクライアント側で受信
  7. <= Rails にメッセージを渡す際は、{ "command": "message", "identifier": {"channel": "XxxChannel", ... }, "data": { "action": "yyy", ... } } の形式で、クライアントから渡したい情報は data に入れる。action には実行したいメソッドを指定。
  8. Rails 側では、XxxChannel#yyy が実行される

以上を GDScript 側でも同じように処理すれば、クライアントとして動作します。

ActionCable.tscn

ActionCable でのやりとりを抽象化するため、Node を継承したシーンを作成し、ActionCable と名前をつける。

以下の GDScript をアタッチする。
urlchannel_name@export をつけて、インスペクタから編集できるようにしておく。

action_cable.gd
extends Node
class_name ActionCable

@export var url := "ws://localhost:3000/cable"
@export var channel_name := 'SomeChannel'
@onready var ws := WebSocketPeer.new()
var identifier = ''


func _ready():
    var err = ws.connect_to_url(url)
    if err == OK:
        print_debug("Connecting to %s..." % url)
    else:
        push_error("Unable to connect.")
        set_process(false)


func _process(_delta):
    ws.poll()
    match ws.get_ready_state():
        WebSocketPeer.STATE_OPEN:
            while ws.get_available_packet_count():
                var data = ws.get_packet().get_string_from_utf8()
                handle(JSON.parse_string(data))
        WebSocketPeer.STATE_CLOSING: pass
        WebSocketPeer.STATE_CLOSED:
            var code = ws.get_close_code()
            print_debug("Closed: %d. Clean: %s" % [code, code != -1])
            set_process(false)


func handle(json: Dictionary) -> void:
    if json.has('type'):
        match json['type']:
            'welcome': subscribe()
            'ping': pass
            'confirm_subscription': pass
            _: print_debug("< Received: %s" % json)
    else:
        if json.has('message'):
            _handle(json['message'])


func subscribe() -> void:
    print_debug("> Subscribe: %s" % channel_name)
    var app_name = ProjectSettings.get('application/config/name')
    identifier = '{"channel": "%s", "name": "%s"}' % [channel_name, app_name]
    send_command('subscribe')


func send_command(command: String, payload: Dictionary = {}) -> void:
    var msg := {
        "command": command,
        "identifier": identifier,
    }
    if payload: msg.merge(payload)
    ws.send_text(JSON.stringify(msg))


func perform(action: String, payload: Dictionary) -> void:
    var data = { 'action': action }
    data.merge(payload)
    send_command('message', { 'data': JSON.stringify(data) })


func _handle(payload: Dictionary) -> void:
    pass # override this method

XxxChannel.tscn

上記で作成した、ActionCable.tscn を継承したシーンを作成して処理を書く。

例えば、上で書いた FooChannel 向けのスクリプトを書くとしたら、

  1. ActionCable.tscn を継承して、FooChannel.tscn を作成
  2. インスペクターから、channel_nameFooChannel を設定
  3. 以下の GDScript をアタッチ
foo_channel.gd
extends ActionCable

# 受信処理
func _handle(payload: Dictionary) -> void:
    print_debug('_handle: %s' % payload) # => _handle: { "bar": 1.0 }

# 送信処理(とりあえず実行した画面上でキーを押すと実行)
func _input(event: InputEvent) -> void:
    if event is InputEventKey:
        if ws.get_ready_state() == WebSocketPeer.STATE_OPEN:
            perform('foo', { 'bar': true }) # => Rails の FooChannel#foo を実行

以上です。

Discussion