🐱

Scratchで汎用ネットワークイベントを組んでみた話

に公開

まえがき

Scratchという教育向けビジュアルプログラミング言語があります。

https://scratch.mit.edu

私はこの言語がけっこう好きで、中学生のころにはソフトウェアレンダラをずっと書いていました。

(小~中学生のときのプロジェクト説明が恥ずかしすぎるのでリンクは割愛しますが、AABBボックスや頂点と辺データで定義されるアニメーション可能なワイヤフレーム描画やサターン的なテクセルベースのテクスチャマッピングなどを作っていました)

さて、今回はScratch標準の機能である"クラウド変数"を用いてネットワークイベントの送受信をできるようなプログラムを書いてみたので、記事にしました。

作成したプロジェクト

https://scratch.mit.edu/projects/1283020733/

スペースを押すと8x8で書いたデータベースくんが押したユーザーのUIDを発言します。

後に書きますが、認証ユーザーでないとクラウド変数が使えないため、昔Scratchに登録した覚えがなければ続く記事と見比べながらソースを読んでみてください。

参考にした情報

Griffpatch氏によるマルチプレイヤープラットフォーマーのチュートリアル動画
こちらの動画ではキャラクターの絶対座標をスナップショット同期するチュートリアルが示されています。
https://www.youtube.com/watch?v=1JTgg4WVAX8&list=PLy4zsTUHwGJIw6-ra80IMuxiRW4XHiGqf

クラウド変数とは

まずScratchの同期をするにあたって避けては通れないクラウド変数の仕組みと仕様について解説します。

クラウド変数の特徴

クラウド変数には次のような特徴があります。

  • 更新されたクラウド変数は全クライアントにリアルタイムで配信される
  • 最新の変数はサーバーに保存される

一方、次のような制約があります。

  • 認証済み(Scratcher)ユーザーしか更新できない
  • 数値(10進数)しか扱えず、長さも256桁まで
  • 0.1秒未満の更新は無視され、ローカルにのみ反映される
  • 10個までしか設定できない
  • ローカル変数にできない(当然と言えば当然)
  • 自由なチャットを組むと規約違反[1]

一般的に、クラウド変数はプロジェクトのハイスコアを保存したり、投票システムを作ったりするのに使われます。

イベントシステムの概要

今回作成したイベントシステムは、上記の制約をある程度吸収し、イベントの送受信に特化させたものです。
「誰が」「何のイベントを」「どのようなデータと共に」送信したのかを数値情報にエンコード・デコードする処理を持っているため、ユーザーはイベントの内容にのみ気を配れば済むようになっています。

設計

データフォーマット

クラウド変数に格納するデータは"4桁 = 1ブロック"として管理します。[2]
具体的な設計は後述しますが、これにより可変長データの切り出しや管理が容易になります。

[ブロック0]  [ブロック1]  [ブロック2] ...
1234        5678        9012        ...

各ブロックには、
送信者UID(2ブロック), イベントヘッダ, イベントデータ
の情報が入ります。

(例:

[イベントヘッダ0], [データ0], [データ1], [イベントヘッダ1]

また、クラウド変数の桁数制限から、送信できる最大データ長はUIDを含めて64ブロックになります。

UID

UIDはセッションごとに乱数を用いて生成しています。
ユーザー名を39進数の数値化したものを8桁に丸める方式も用意してありますから、お好きなほうを選んでください。

UIDは送信データの先頭に来る仕様なので8桁のままならUIDは 10000000~99999999 に制限されます。[3]

イベントヘッダ

イベントヘッダは次のようなデータから成ります。

[イベントID(2桁)][データスロット(1桁)][データ長(1桁)]

データ長:
ブロック数で表され、最大9ブロック(= 36桁)。

イベントID:
イベント固有の識別子

データスロット:
たくさんのデータを必要としないイベントのデータ長を節約するための自由なスロット。
(例: 移動方向のみ同期すればよい移動イベント, 攻撃失敗イベント, など)

文字列

文字列は、

a-z, 0-9, -(ハイフン), _(アンダースコア)
(Scratchのユーザー名に使える文字すべて)

を39進数化した数値で表され、必ず5文字(39^5 = 90,224,199通り, 2ブロック)で区切られます。
(4桁で39進数化しても2文字しか入らず、データ長を全く圧縮できず腹が立ったためこのような仕様にしました)

受信側

クラウド変数はポーリングされており、編集されたときパースされて リストNETWORK_EVENT に登録されます。

登録される際、ブロックごとに項目が分けられ、
UID(8桁), イベントID(2桁), データスロット(1桁), データ(4桁*9) の順に並び、1イベントは必ず12項になります。
ID:00,データ長:0, データスロット:0のイベントを送信した場合のスクショ1項目にUID, 2項目に00, 3項目が0で、4項目から12項目が空白

クラウド変数対策

送信レート制限

送信レートは変数sync_freqで制御できます。
Scratchには[2000年からの日数]というunix時間なのか何なのかよくわからないブロックがあり、かなり細かいfloatで管理されているため、これを86400倍すると[2000年からの秒数]ブロックになります。
前回送信時間を変数_timer_oldに保存して、現在の時間との差がsync_freq以上になったフレームで送信を行います。

今の実装を聞いてもらえばわかる通りsync_freqは厳密な送信頻度を保証しません。

キューイング

送信するイベントは一度リスト_send_queueにまとめられ、256桁を超えない範囲で送信フレームごとに統合されます。
256桁を超えるイベントは送信をキャンセルされ、次の送信フレームに繰り越されます。

まとめ

かなりシンプルな実装になりましたが、汎用性を考えるとこんなものになるんじゃないかといった印象。
実は今まで何個かこういった同期イベントシステムを作ったことがあり、再送命令など作ろうとしたことがありますが、"再送イベント"という形になってしまうので、ユーザーイベントと処理がバラけて気持ちが悪かったため断念しました。

そういう同期の発展的なところはゲーム側で一緒に作ってしまうほうがよいというところで、以上Scratchでネットワークイベントを組んでみた話でした。

脚注
  1. safe-chatという単語組み合わせによるチャットシステムを組んでいる人もいるが、これはグレー ↩︎

  2. ブロックサイズについて: 8桁や2桁に比べてデータ量とブロック数のバランスが良い。なお文字列ベースの同期をしたい場合は、39進数化した5文字がほぼぴったり入る8桁区切りの方が効率的。 ↩︎

  3. クラウド変数は256桁すべてを有効な数値とするために文字列として扱われるため、Scratchの貧弱な文字列操作でも扱いやすくすべく先頭にUIDを配置した。 ↩︎

Discussion