サンプルプログラム/ICSを利用した双方向グリッド間通信

Last-modified: 2019-04-21 (日) 00:07:40

Inter-grid communication systemを利用した双方向のグリッド間通信を行うスクリプトのサンプルです。


はじめに

グリッド間通信とは

Space Engineersでは直接繋げて建設されたブロックのひとまとまりを「グリッド」、ピストンやコネクターによって接続された空間を「サブグリッド」と呼びます。
実行中のプログラマブルブロックと同じグリッド、もしくはサブグリッドに所属するブロックはGridTerminalSystemによって簡単に呼び出すことが出来ますが、独立したグリッド同士が情報をやり取りする為には、アンテナを介したグリッド間通信が必要になります。

Inter-grid communication system

Inter-grid communication systemはUpdate 1.189で追加された新たなグリッド間通信の方法です。
従来は「グリッド間通信ガイド」で解説されているように、アンテナ主体でブロードキャストを行いグリッド間通信を実現していました。しかし、この方法は「アンテナ送受信やプログラマブルブロックの実行は1チックに一度のみ」という仕様上の制限により、相互通信のようなある程度複雑な通信を実現する為には、メッセージキューやコールバックといった仕組みを自力で実装しなければならず、非常に面倒でした。
Inter-grid communication systemではこれらの仕組みが標準で利用できる他、ユニキャスト(1対1通信)や文字列以外の送受信なども可能です。

概要

電源のほかに必要なブロックと設定

種類と数備考
Programmable Block×2名前は任意で区別しやすいものを
Antenna又はLaser Antenna×2それぞれのグリッドに一つずつ。互いに電波が届く範囲にあること

配置

2つのプログラマブルブロックを別のグリッドにする為、以下のように配置します。


GridCommunicationSampleMin.jpg

スクリプトの完成形

const string Unicast_Receive = "Unicast_Receive";
const string Broadcast_Reply = "Broadcast_Reply";
const string Callback_Unicast = "Callback_Unicast";
const string Callback_Broadcast = "Callback_Broadcast";
public Program() {
    IGC.UnicastListener.SetMessageCallback(Callback_Unicast);
    IGC.RegisterBroadcastListener(Broadcast_Reply).SetMessageCallback(Callback_Broadcast);
}
public void Main(string argument, UpdateType updateSource) {
    if ((updateSource & (UpdateType.Terminal | UpdateType.Trigger)) != 0) {
        IGC.SendBroadcastMessage(Broadcast_Reply, argument);
        return;
    }
    if ((updateSource & UpdateType.IGC) != 0 && argument == Callback_Unicast) {
        var unicastListener = IGC.UnicastListener;
        while (unicastListener.HasPendingMessage) {
            var msg = unicastListener.AcceptMessage();
            string resultMsg;
            switch (msg.Tag) {
                case Unicast_Receive:
                    var sourceMsg = msg.As<ImmutableArray<string>>();
                    resultMsg = string.Format("{0} from {1} [{2}]", msg.Tag, sourceMsg[0], sourceMsg[1]);
                    break;
                default:
                    resultMsg = string.Format("Tag \"{0}\" is not defined", msg.Tag);
                    var newMsg = new string[] { Me.CustomName, resultMsg };
                    IGC.SendUnicastMessage(msg.Source, Unicast_Receive, newMsg.ToImmutableArray());
                    break;
            }
            Echo(resultMsg);
        }
        return;
    }
    if ((updateSource & UpdateType.IGC) != 0 && argument == Callback_Broadcast) {
        var broadcastListener = IGC.RegisterBroadcastListener(Broadcast_Reply);
        while (broadcastListener.HasPendingMessage) {
            var msg = broadcastListener.AcceptMessage();
            var sourceMsg = msg.As<string>();
            var newMsg = new string[] { Me.CustomName, sourceMsg };
            IGC.SendUnicastMessage(msg.Source, Unicast_Receive, newMsg.ToImmutableArray());
            var resultMsg = string.Format("{0} [{1}]", msg.Tag, sourceMsg);
            Echo(resultMsg);
        }
        return;
    }
}

両方のプログラマブルブロックに上記と同じものを貼り付け、どちらか一方のプログラマブルブロックのターミナルを開き、Argumentに好きなメッセージを入力してRunを押してください。それぞれのターミナルの右枠に下記のような文字列が表示されればグリッド間通信は成功です。

 

送信側:

Unicast_Receive from 相手のブロック名 [入力したメッセージ]

受信側:

Broadcast_Reply [入力したメッセージ]

解説

大まかな流れ

二つの同じプログラムが役割を変えて通信するので大まかな流れを掴んでいないと混乱するかもしれません。
プログラマブルブロックの一方をA、もう一方をBとすると以下のようになります。

Aを手動操作 ⇒ A(10~13行目ブロードキャスト⇒ B(34~最終行ユニキャスト⇒ A(14~33行目メッセージ出力

1~4行目:タグ文字列の定義

const string Unicast_Receive = "Unicast_Receive";
const string Broadcast_Reply = "Broadcast_Reply";
const string Callback_Unicast = "Callback_Unicast";
const string Callback_Broadcast = "Callback_Broadcast";

通信識別用のタグの定義です。メッセージを送受信する時に、この文字列をタグとして使用します。
自由に付けられるので好きな名前にして構いません。

5~8行目:リスナー登録とコールバックの設定

public Program() {
    IGC.UnicastListener.SetMessageCallback(Callback_Unicast);
    IGC.RegisterBroadcastListener(Broadcast_Reply).SetMessageCallback(Callback_Broadcast);
}

コンストラクタでメッセージ受信の為の前準備を行います。

上記2行目でユニキャストのコールバックを、3行目でブロードキャストのリスナー登録とコールバックを設定しています。リスナー登録とはtwitterのフォローのようなもので、登録するとそのタグ(ここではBroadcast_Reply)に届くメッセージを受信出来るようになります。またコールバックを設定しておくことで、登録したタグにメッセージが届くと、プログラマブルブロックがゲーム側から自動的に実行されるようになります。その際、argumentにはコールバックで設定した文字列(Callback_UnicastかCallback_Broadcast)が、updateSourceにはUpdateType.IGCがそれぞれ代入されます。

10~13行目:ブロードキャスト送信

if ((updateSource & (UpdateType.Terminal | UpdateType.Trigger)) != 0) {
    IGC.SendBroadcastMessage(Broadcast_Reply, argument);
    return;
}

ターミナルから手動で実行した時(UpdateType.Terminal)と、ボタンやセンサー等を介して実行した時(UpdateType.Trigger)に、argumentの文字列にBroadcast_Replyタグを付けてブロードキャストしています。受信側がアンテナの範囲外などでいなかった場合、これ以上なにも起こりません。

14~33行目:ユニキャスト受信

if ((updateSource & UpdateType.IGC) != 0 && argument == Callback_Unicast) {

ユニキャストの場合に32行目までが実行されます。5~8行目で解説したコールバックメッセージを使って判別しています。

 
var unicastListener = IGC.UnicastListener;

受信したメッセージを保持しているIMyMessageProviderを取得します。ユニキャストの受信先は自分ただ一人だけなのでタグは指定しません。

 
while (unicastListener.HasPendingMessage) {
    var msg = unicastListener.AcceptMessage();

受信したメッセージはメッセージキューという形で保持されています。これは先に入れたメッセージを先に取り出すデータ構造で、机に積み上げた手紙を下から読んでいくのと同じです。AcceptMessage()を呼び出すたびにキューの中で最も古いメッセージが返され、同時にそのメッセージはキューから削除されます。HasPendingMessageによってキューにメッセージが残っているか否かがわかるので、未読のメッセージを順次取り出し処理をループさせています。

メッセージを受信するとすぐさまコールバックが実行されるにも拘わらず、何故この処理が必要かというと、同じチックに2通以上のメッセージが届いても、コールバックが実行されるのは1度だけだからです。

 
string resultMsg;
switch (msg.Tag) {
    case Unicast_Receive:
        var sourceMsg = msg.As<ImmutableArray<string>>();
        resultMsg = string.Format("{0}! from {1} [{2}]", msg.Tag, sourceMsg[0], sourceMsg[1]);
        break;
    default:
        resultMsg = string.Format("Tag \"{0}\" is not defined", msg.Tag);
        var newMsg = new string[] { Me.CustomName, resultMsg };
        IGC.SendUnicastMessage(msg.Source, Unicast_Receive, newMsg.ToImmutableArray());
        break;
}

Unicast_Receiveタグのついたユニキャストを受信したら、受信したメッセージを出力します。

AcceptMessage()で取得したmsgはTag、Data、Sourceで構成されています。Tagは任意の文字列で、送信時に設定されたものが入っています。Dataはメッセージ本体ですがobject型で保持されているのでmsg.As<型名>()で型変換を行います。わざわざAs<型名>()を使わなくても内部的にはただのキャストなので、普通にキャストしても構いません。ImmutableArray<string>は見慣れない型ですが、送信できるデータ型に制限がある為に、文字列の配列を変換したものです。

Sourceには送信元のプログラマブルブロックの固有IDが入っています。IDに関しては補足にて説明しています。

default以下は存在しないタグを指定して送信された場合の処理ですが、今回のサンプルではプログラムの打ち間違い以外でこの処理が実行されることはありません。内容は相手側のUnicast_Receiveへの送信が行われていて重要ですが、ブロードキャストの処理で同じ事をしているのでそちらで解説します。

 
Echo(resultMsg);

ターミナル右枠への文字列の表示。簡単なデバッグとかに便利です。

34~最終行:ブロードキャスト受信

if ((updateSource & UpdateType.IGC) != 0 && argument == Callback_Broadcast) {

ブロードキャストの場合に最後のreturnまでが実行されます。

 
var broadcastListener = IGC.RegisterBroadcastListener(Broadcast_Reply);

基本はユニキャストと同じですが、ブロードキャストではどのタグのメッセージキューを取得するか指定します。

サンプルではBroadcast_Replyしかリスナー登録をしていないため、決め打ちで書いていますが、複数のタグにリスナー登録している場合、例えば緊急通信用の回線などを用意して使い分けたい場合、switchやifで条件分けします。その場合、5~8行目の処理も増えます。

 
while (broadcastListener.HasPendingMessage) {
    var msg = broadcastListener.AcceptMessage();

ユニキャストと同じです。

 
var sourceMsg = msg.As<string>();
var newMsg = new string[] { Me.CustomName, sourceMsg };
IGC.SendUnicastMessage(msg.Source, Unicast_Receive, newMsg.ToImmutableArray());
var resultMsg = string.Format("{0} [{1}]", msg.Tag, sourceMsg);

10~13行目で送信されたDataは単なるstring型なので、ここでもstring型として復元します。string型は変換しなくても送信できるデータ型です。

上記2行目で返信用のDataを作成しています。自分のブロック名(Me.CustomName)と受信したメッセージを配列にパッケージして次の行で送信しています。この際、配列型はそのままでは送信出来ないので、ToImmutableArray()で型変換します。

ユニキャストでの送信には相手の固有ID、タグ、メッセージが必要になります。ここではブロードキャスト送信者のIDに向けてユニキャスト(つまり送信者に返信)しています。サンプルでは考慮していませんが、送信相手がアンテナの範囲外にいたり、IDが無効だった場合にはSendUnicastMessage()はfalseを返します。また範囲外かどうかを判別する為にはIGC.IsEndpointReachable()も使えます。

補足

ブロックの固有ID

全てのブロックにはそれぞれ固有のIDが割り当てられています。実態はlong型の数字で、.EntityIdもしくは.GetIdによって取得でき、ユニキャスト送信にはこの値を使用します。
IDはブロックが設置された瞬間に生成され、以後破壊されるまで基本的には変化しません。セーブとロードを行ってもずっと同じ値です。

しかし、壊していないのに値が変化する場合が存在します。それは「Merge Blockによるグリッドの結合」です。これはマージブロックが、小さいグリッドを大きい方のグリッドの一部として再生成しているからです。もしもIDをキャッシュしてルーティングテーブルの様なものを作る場合は、その事に注意してください。

送信できるデータ型

自分で定義したクラスは送信不可です。object型にしようとも問答無用でブロックされます。
許可されているのはプリミティブ型とゲームの処理によく使われる汎用型で、許可されていない型の場合、コードチェックでは問題なくとも実行時に以下のようなエラーが出力されます。

Caught exception during execution of script:Message type 型名 is not allowed! at...

許可されているデータ型一覧
  • int, uint
  • short, ushort
  • long, ulong
  • bool
  • byte, sbyte
  • char
  • double
  • float
  • string
  • Ray, RayD
  • Line, LineD
  • Color
  • Plane, PlaneD
  • Point
  • MyQuad, MyQuadD
  • Matrix, MatrixD, MatrixI, Matrix3x3
  • Capsule, CapsuleD
  • Vector2, Vector2D, Vector2B, Vector2I
  • Vector3, Vector3D, Vector3B, Vector3I, Vector3L, Vector3S, Vector3UByte, Vector3Ushort, Vector3I_RangeIterator
  • Vector4, Vector4D, Vector4I, Vector4UByte
  • MyShort4, MyUShort4
  • MyBounds
  • CubeFace
  • Quaternion, QuaternionD
  • Rectangle, RectangleF
  • BoundingBox, BoundingBoxI, BoundingBoxD
  • BoundingBox2, BoundingBox2I, BoundingBox2D
  • BoundingSphere, BoundingSphereD
  • MyTransform, MyTransformD
  • CurveTangent
  • CurveLoopType
  • CurveContinuity
  • ContainmentType
  • MyBlockOrientation
  • Base6Directions.Axis, Base6Directions.Direction, Base6Directions.DirectionFlags
  • Base27Directions.Direction
  • MyOrientedBoundingBox, MyOrientedBoundingBoxD
  • PlaneIntersectionType
  • CompressedPositionOrientation
  • HalfVector2, HalfVector3, HalfVector4
  • MyTuple
  • ImmutableArray
  • ImmutableList
  • ImmutableQueue
  • ImmutableStack
  • ImmutableHashSet
  • ImmutableSortedSet
  • ImmutableDictionary
  • ImmutableSortedDictionary

コメント