📶
Rails の ActionCable と Godot の WebSocketPeer で通信する
Rails と Godot 間での WebSocket 通信がとりあえずローカル環境で動いたので、備忘のため残します。
試したバージョンは以下です。
- Rails 8.0.2
- Godot 4.4.1
動かした動画です。(※Godotのキー入力でメッセージを行って来いしてるだけの動画です。)
Rails 側の設定
Rails 以外からも接続を受け付けるようにする
デフォルトでは接続を受け付けないので、config.action_cable.disable_request_forgery_protection
を true にする。
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 から追加された機能らしいです。
参考
Godot 側の設定
ActionCable の仕組み
ActionCable は JSON でメッセージのやり取りをしています。
どういうメッセージを渡していたかは、Javascript 側の実装 を見ながら確認しました。
以下概要
- WebSocket open
- => Rails からの
{ "type": "welcome", ... }
メッセージをクライアント側で受信 - <= Rails に
{ "command": "subscribe", "identifier": {"channel": "XxxChannel", ... } }
をクライアント側からメッセージ送信。ここで識別に必要な情報を一緒に渡す。 - Rails の
XxxChannel
のsubscribed
メソッドが呼ばれる。識別するため、stream_from
を設定 - 接続確立
- => Rails から
broadcast
で渡したメッセージは、{ "message": ... }
の形式でクライアント側で受信 - <= Rails にメッセージを渡す際は、
{ "command": "message", "identifier": {"channel": "XxxChannel", ... }, "data": { "action": "yyy", ... } }
の形式で、クライアントから渡したい情報はdata
に入れる。action
には実行したいメソッドを指定。 - Rails 側では、
XxxChannel#yyy
が実行される
以上を GDScript 側でも同じように処理すれば、クライアントとして動作します。
ActionCable.tscn
ActionCable でのやりとりを抽象化するため、Node を継承したシーンを作成し、ActionCable
と名前をつける。
以下の GDScript をアタッチする。
url
と channel_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
向けのスクリプトを書くとしたら、
- ActionCable.tscn を継承して、
FooChannel.tscn
を作成 - インスペクターから、
channel_name
にFooChannel
を設定 - 以下の 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