バスキュール技術ブログ

バスキュールが得意とするインタラクティブエンジニアリングを、あますことなくお届け!

Socket.ioでデバイスを繋いでアナログメタバースを作る

f:id:bascule-dev:20210609212821p:plain

エンジニアの桟(@katapad)です。

PC・スマホだけでなく、さまざまなデバイスとやりとりするにはSocket.ioやOSCのようなリアルタイムメッセージ通信が必要になる。いつもSocket.ioの書き方を忘れるので、2021年におけるSocket.ioの実装例をここに残しておく。

小さなサンプルを作るだけならつまらないので、なんらかの体験のプロトレベルのものを作ることにした。

謎の会社であるバスキュールをアナログかつバーチャル訪問するサンプルを作る。落とし穴付きで。

バスキュールが謎会社すぎるとの声をたくさん聞くようになったので、気軽にバーチャル会社訪問でもしてくれたらなあという動機で作ってみる。

TOIOでバスキュール社内のリアル空間を動き回ってもらう

コロナ禍で注目されたどうぶつの森やフォートナイトといったメタバース。それをアナログに置き換えるとどうなるのか?というテーマを掲げ、ユーザーがリモートでアバター(TOIO)を動かせるようにし、バスキュール社内に設置したリアル空間を動き回ってもらう。

f:id:bascule-dev:20210609213100j:plain
TOIOをアバターに

ブラウザでリモートからTOIOを操作して社内の空間を歩き回ってもらう

f:id:bascule-dev:20210609213343g:plain

○✗クイズの落とし穴をObnizで作る

会社を知るには○✗クイズというのが鉄板(?)である。さらに鉄板を重ねるならば、不正解時には床を落とすことも必要だろう。ここではObnizでサーボモーターを制御し、TOIOを落とし穴にハメるギミックを作る。

このようにして、バスキュール社内をウロウロして社内の空気に触れながら、クイズで理解を深めてもらい、床から転落してもらうといった、誰も見たことのない体験をしてもらうことにした。

f:id:bascule-dev:20210609215929g:plain

構成

f:id:bascule-dev:20210609212302p:plain

ブラウザの画面も載せる(コントローラー、落とし穴とか)

  • Node.js
    • Socket.ioサーバーをたてる
    • toio.jsでTOIOを制御する
  • ブラウザ
    • TOIOを操作する
    • 落とし穴を操作する
  • Unity
    • TOIOの位置情報を取得
    • AR的に情報をオーバーレイする
  • デバイス
    • Obnizでサーボを制御し、床を落とす
    • ObnizはSocket.ioを介さずブラウザから直接操作することもできるが、最大接続数の制限があるのでサーバーで一括管理した。

といった連携をSocket.ioでやっている。

使ったもの

なお、この試作には入っていないが、PythonでもSocket.io v4系が使えるライブラリがある。

そもそもSocket.ioで何ができるの?

主にWebSocketを用いてリアルタイムで双方向メッセージングをする技術。チャットなどが最も適当な例に当たる。

Socket.ioにはWebSocketのみでは実現できない便利機能がたくさんある。

  • room, namespace
    • room: 同じ空間にいくつか部屋が分かれているイメージ
      • 空間に一斉にメッセージ可
      • 特定の部屋のみにメッセージ可
    • namespace: 空間がそもそも違う
  • 接続が切れても自動で再接続してくれる
  • メッセージが届くまでリトライしてくれる
  • volatileを使うと、送りすぎて不要になったメッセージを捨ててくれる

他、簡単に扱えるメッセージング技術としてはOSCもあるが、ブラウザではUDPが使えないためWebSocketで実装されたOSCのライブラリを使う必要がある。(WebRTCを使うとUDPが使えるが、PeerToPeer接続が面倒)

最近はブラウザでも気軽にUDPが使えるWebTransportなる技術が作られてる最中なので、NodeやUnityなどで使える日を気長に待とう。

サンプルコード

Node.js Server

全部書くと長くなるので、かいつまんで書いていく

// fastifyでサーバー作成
const fastify = require('fastify')({ logger: true })
fastify.register(require('fastify-cors'))
fastify.register(require('fastify-socket.io'), {
  allowEIO3: true, // Socket.io v3も対応させる
  cors: {
    origin: "*",
    methods: ["GET", "POST"],
    allowedHeaders: ["Origin, X-Requested-With, Content-Type, Accept"],
  }
})

const start = async () => {
  try {
    await fastify.listen(8080, '0.0.0.0')
    fastify.io.on('connect', socket => {
      // クライアントが接続時にクエリを投げるようにしたので、クエリにあわせてroomにjoinさせる
      switch (socket.handshake.query.client) {
        // ブラウザのTOIOコントローラー
        case 'controller':
          socket.join('controller')
          break
        // TOIOを制御する別のNode.jsプロセス
        case 'toio':
          socket.join('toio')
          break  
                //.....
                // というような部屋の割り振り記述が続く
                //.....
      }
      // ブラウザからTOIOを動かす指示がきたら、TOIOのroomに揮発メッセージで送る
      socket.on('move', data => {
        socket.to('toio').volatile.emit('toioMove', data)
      })

      // TOIOから自己位置の情報が来たら、ブラウザとUnityに揮発メッセージで送る
      socket.on('toio/position', data => {
        socket.to('controller').volatile.emit('toio/position', data)
        socket.to('unity').volatile.emit('toio/position', data)
      })
    })
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

JavaScript Client

以下はブラウザからTOIOを操作する画面のコードをかいつまんだもの。

ブラウザからのメッセージを受けて、実際にTOIOデバイスを操作するのはtoio.js を使っている。こちらもだいたい同じようなコードになる。

import { io } from 'socket.io-client'
const SOCKET_HOST = 'http://localhost:8080'

class SocketManager {
  connect() {
        // Socketの
    this.socket = io(SOCKET_HOST, { query: { client: 'controller' } })
    this.socket.connect()
    this.socket.on('connect', (socket) => {
      console.log('connected')
    })
    this.socket.on('toio/position', (data) => {
      // TOIOデバイスから来た自己位置を取得
    })
  }

  disconnect() {
    if (!this.socket) return
    this.socket.off()
    this.socket.disconnect()
    this.socket = null
  }

    // TOIOを動かすメッセージを送る。送り過ぎたら不要になるのでvolatileで送る。
  move(left, right, duration = 100, ottikiId = 0) {
    this.socket.volatile.emit('move', { left, right, duration })
  }
}

export default new SocketManager()

Unityのコード

Socket.ioを実装したライブラリがGithubにいくつも上がっているが、だいたいが開発停止している。

ただひとつ、Best HTTP/2 というアセットが Socket.io v3に対応している。

  • サーバーをv4にしても動作していた
  • namespace, room対応
  • 型を渡すと On時にパースしてくれるようになった
  • volatile にも対応
// Connect
SocketOptions options = new SocketOptions();
options.AdditionalQueryParams = new PlatformSupport.Collections.ObjectModel.ObservableDictionary<string, string>();
options.AdditionalQueryParams.Add("client", "unity"); // Connect時にパラメータを渡せる
_manager = new SocketManager(new Uri("http://localhost:8080"), options);

// 受信
// 型を渡すとパースしてくれる
_manager.Socket.On<CubeInfo>("toio/position", (CubeInfo data) =>
{
    Debug.Log($"CubeInfo: {data.index}, {data.x}, {data.y}, {data.angle}");
});

// 送信
_manager.Socket.Emit('hoge')

アナログメタバースの知見

こんな小さな実験でもいくつかの知見が得られた。

アナログアバターに魂が乗る

f:id:bascule-dev:20210610105019g:plain
アバターを撫でる

リモートユーザーから「リアルに現場にいるひとにからアバターを撫でられるとどうなるのか?」との問いがあった。撫でられると「ちょっとうれしい」らしい。逆に横から叩いてなぎ倒してみると、やはりつらい思いがあったらしい。実在する人物から叩かれると心理的な影響が大きいのかもしれない。ゲームのメタバースで同じことをしても、ここまでの感情になるのだろうか?

リアル側の人間がリアルのアバターを見ると、言葉は通わせないが意思疎通はできる小動物とみなせる

f:id:bascule-dev:20210610105010g:plain
ボールを持ってきてもらう

アバターにボールを持ってこさせるという実験をした。リアル側の人間から見ると、きちんと意思の疎通ができる小動物と遊んでいる感覚があった。だがそれは人間でも犬でもない、別の生物。珍妙このうえない気持ちになる。

まとめ

Socket.ioはすごく簡単かつ、確実にメッセージを届けられるライブラリ。OSCと併用するともっと大量のメッセージのやりとりもできるようになる。

エンジニア募集中

バスキュールではコードを書くだけではなく、自分が信じるおもしろさを考え、実現するエンジニアを募集しています!フロントもバックエンドも募集中。

bascule.co.jp www.green-japan.com

で、もうすぐ別のおもしろコンテンツが出ます

まもなく、こんなモッククオリティではない、バスキュールらしいおもしろコンテンツがリリースされます。乞うご期待!

参考リンク

fokaさんのUnityとTOIOを連動させる記事
WindowsのUnityからBLEアプリ経由してtoioで遊ぶ - Qiita

Obnizでサーボ動かしてる記事
kintoneで反応速度ゲームを作ってみた