継続を使用する並列分散フレームワークのUnity実装

継続を使用する並列分散フレームワークのUnity実装


author: Ryo Yasuda, Shinji Kono profile: 琉球大学理工学研究科情報工学専攻 河野研究室

# 概要

  • オンラインゲームにおける通信にはクライアントサーバ方式が主流
    • データの共有はサーバを経由するため低速
  • 当研究室で開発を行っているChristie の分散計算を使用することで、高速かつ安全に通信できると考えた
  • Christie をUnity で使用するためにC# で書き換えを行った
  • 実装としては、localDataGearManager を用いた同一プロセスで複数インスタンス立ち上げによる通信が可能

# オンラインゲームにおけるデータ通信

  • オンラインゲームは複数のプレイヤーが関与する分散プログラム
    • 分散プログラムを正しく書くことは難しい
    • 攻撃の標的になる場合が多い
  • クライアントの負荷軽減やチート対策のため、クライアントサーバ方式が主流
    • データの同期にはサーバを経由するため低速

# オンラインゲームにおけるデータ通信

  • 当研究室では並列分散通信フレームワークChristie を開発中である

    • 型のあるDataGear とKey を持つストリーム、DataGearManager として格納している
    • 他のノードはDGM のproxyを持っており、proxy に書き込むことで通信を実現している
    • DGM はトポロジーマネージャーによって自動的に構築される
      • プログラム自体はDGM の名前を知っていれば良い
      • 他のノードのIP addressなどを知る必要はない
  • ネットワークが切断されてもゲームは継続可能

  • ノードが接続している対象を直接知ることはできない

    • チートに対する耐性がある
  • 本研究ではJava で書かれたChristieとC# で書き換えを行ったChristie #を説明し、その機能と実装の差について考察を行う


# Christie の基礎概念

  • Christie は当研究室で開発をしている並列分散通信フレームワークである

    • 同じく当研究室で開発しているGearsOS に導入予定のため次のような概念を持っている
  • CodeGear (クラスやスレッド)

  • DataGear (変数データ)

  • CodeGearManager (CG,DG,DGMを管理)

  • DataGearManager (DGを管理,localとremoteの2種類がある, put操作によりDGを格納)


# Christie の基礎概念

message
Christie を同一プロセスで複数インスタンス立ち上げた際の接続の構造図
  • 全てのCGM はThreadPool と他のCGM をList として共有している
  • ThreadPool はCPU に合わせた並列度でqueue に入ったThread を逐次実行していく
    • 1つのThreadPool で処理を行うことでCPU のコア数に適したThread を管理でき、並列度を下げ流ことを防ぐ
  • ThreadPoolを共有することメタレベルで全てのCG/DG にアクセス可能

# Christie の基礎概念 annotationについて

DG を取り出すためにCG内に宣言した変数データにannotation をつける。annotationには以下の4つがある。

  • Take
    • 先頭のDG を読み込み、そのDG を削除する
    • DG が複数ある場合Take を使用する
  • Peek
    • 先頭のDG を読み込むがDG を削除しない
    • 操作をしない場合は同じデータを参照し続ける
  • TakeFrom
    • Take と同じ動作だが、remote 先のDGMを指定できる
  • PeekFrom
    • Peek と同じ動作だが、remote 先のDGMを指定できる

# Topology Manager

  • Christie 上でNetwork Topology を形成する

    • 参加を表明したノードに名前を与える
    • 必要があればノード同士の配線を自動で行う
  • 静的Topology と動的Topology 2種類がある


# Topology Manager

  • 静的Topology は以下のようなdot ファイルを与えることでNode の関係を構築できる
  • それぞれのNode への通信にはIP address などは使用せずright というlabel を使用することで接続できる
1
2
3
4
5
digraph test {
	node0 -> node1 [label="right"]
	node1 -> node2 [label="right"]
	node2 -> node0 [label="right"]
}
message

# Java からの変更点

  • Java とC# は基本的に書き方は変わらない
1
2
3
4
5
6
7
Java
public class StartHelloWorld extends StartCodeGear { }

@Override
protected void run(CodeGearManager cgm) { }

@Take String helloWorld;
1
2
3
4
5
6
C#
public class StartHelloWorld : StartCodeGear { }

public override void Run(CodeGearManager cgm) { }

[Take] string helloWorld;

# Christie # のコード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class StartHelloWorld : StartCodeGear {
    
    public StartHelloWorld(CodeGearManager cgm) : base(cgm) { }

    public static void Main(string[] args) {
        CodeGearManager cgm = CreateCgm(10000);
        cgm.Setup(new HelloWorldCodeGear());
        cgm.Setup(new FinishHelloWorld());
        cgm.GetLocalDGM().Put("helloWorld", "hello");
        cgm.GetLocalDGM().Put("helloWorld", "world");        
    }
}
1
2
3
4
5
6
7
8
9
public class HelloWorldCodeGear : CodeGear {
    [Take] string helloWorld;
    
    public override void Run(CodeGearManager cgm) {
        Console.Write(helloWorld + " ");
        cgm.Setup(new HelloWorldCodeGear());
        cgm.GetLocalDGM().Put(helloWorld, helloWorld);
    }
}
1
2
3
4
5
6
7
8
public class FinishHelloWorld : CodeGear {
    [Take] private string hello;
    [Take] private string world;

    public override void Run(CodeGearManager cgm) {
        cgm.GetLocalDGM().Finish();
    }
}
  1. Main関数でCGM のインスタンス生成
  2. 2つのCG をsetupして待ち状態にする
  3. key:hellowWorld data:“hello” がTake される
  4. 変数が揃ったためStartHelloWorld のRun が実行される
  5. “hello” がprintされ、再び待ち状態になる。 key:hellow data:“hello"がput される
  6. key:hellowWorld data:“world” がTake され、4,5と同様に処理される
  7. 変数hello とworld がput され揃ったため、FinishHelloWorld のRun が実行され、プログラムは終了する

# Unity

  • UnityはUnity Technologies が開発を行っているゲームエンジンである
    • 世界で最も使用されているゲームエンジン
    • 非常に軽く、スペックが低いノートPCでもゲーム開発が可能
  • プログラミング言語にはC# が採用されている
    • C# のAPI やUnity 向けに拡張されたAPIも使用可能
    • 開発した機能をUnity に組み込むことも可能

# Christie # on Unityのコード例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class StartHelloWorld : StartCodeGear {
    
    public StartHelloWorld(CodeGearManager cgm) : base(cgm) { }

    public  void RunCodeGear(CodeGearManager cgm) {
        cgm.Setup(new HelloWorldCodeGear());
        cgm.Setup(new FinishHelloWorld());
        cgm.GetLocalDGM().Put("helloWorld", "hello");
        cgm.GetLocalDGM().Put("helloWorld", "world");
    }
}
1
2
3
4
5
6
7
public class HelloWorld : MonoBehaviour {
    void Start() {
        CodeGearManager cgm = StartCodeGear.CreateCgm(10000);
        var helloWorld = new StartHelloWorld(cgm);
        helloWorld.RunCodeGear(cgm);
    }
}
  • HelloWorldCodeGearと、FinishHelloWorld はそのまま使用
  • StartHelloWorld をUnity で使用できるように書き換え
    • Unity ではMonoBehaviour 継承したクラスが動作可能
    • ゲーム開始時に1度だけ呼ばれるStart 関数
    • Start 関数でCGM のインスタンスを生成
    • Main 関数を名前を変えたRunCodeGear 関数を実行

# Take annotationの実装

  • Christie ではDGを取得するためにannotation を使用している

    • C# ではannotation と同様の機能にattribute があり、Take をattribute で実装した
  • Take はフィールド変数に対して適用する

1
2
3
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Take { }
1
2
[AttributeUsage(AttributeTargets.Field)]
public class Take : Attribute { }

# MessagePackの相違点

  • Christie ではMessagePack を使用してデータを圧縮し送受信している

    • インスタンス内のpublic 変数に対して圧縮可能
  • バージョンが古いため、現在はサポートされていない

    • そのため、最新版とは記述方法が異なる
  • 圧縮するクラスには@Message annotatoinをつける

  • MessagePack インスタンスを作成後、write、read することでデータの圧縮解凍が可能

    • 圧縮されたデータはbyte[] 型になる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class MessagePackExample {
    @Message
    public static class MyMessage {
        public String name;
        public double version;
    }
 
    public static void main(String[] args) throws Exception {
        MyMessage src = new MyMessage();
        src.name = "msgpack";
        src.version = 0.6;
 
        MessagePack msgpack = new MessagePack();
        // Serialize
        byte[] bytes = msgpack.write(src);
        // Deserialize
        MyMessage dst = msgpack.read(bytes, MyMessage.class);
    }
}

# MessagePackの相違点

  • C# のMessagePack は複数存在している

    • java 版と似たような書き方をするMessagePack-CSharp を選択した
  • 圧縮を行いたいクラスに対してMessagePackObject attribute を付ける

  • 圧縮する変数に対してkey を設定できる

    • 解凍時にjson として展開できる
  • データの圧縮にはMessagePackSerializer.Serialize 関数を用い、byte[] に圧縮される

  • データの解凍にはMessagePackSerializer.Deserialize 関数を使用する

    • Deserialize 関数はジェネリスク関数であるため<>内に解凍するデータの型情報を記述する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[MessagePackObject]
public class MyClass {
    [Key(0)]
    public int Age { get; set; }
    [Key(1)]
    public string FirstName { get; set; }
    [Key(2)]
    public string LastName { get; set; }

    static void Main(string[] args) {
        var mc = new MyClass {
            Age = 99,
            FirstName = "hoge",
            LastName = "huga",
        };

        byte[] bytes = MessagePackSerializer.Serialize(mc);
        MyClass mc2 = MessagePackSerializer.Deserialize<MyClass>(bytes);

        // [99,"hoge","huga"]
        var json = MessagePackSerializer.ConvertToJson(bytes);
        Console.WriteLine(json);
    }
}

# ThreadPoolからTaskへの書き換え

  • Christie ではThreadPool を使用していた

    • Christie # ではThreadPoolより高機能なTask を用いて書き換えを行った
  • Task は複雑な非同期処理を通常のコーディングと同じ感覚で直感的に記述できる

  • 裏でThreadPool が動くようになっている

    • 大きく動作は変わらない

# ThreadPoolからTaskへの書き換え

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class PriorityThreadPoolExecutors {

    private static class PriorityThreadPoolExecutor extends ThreadPoolExecutor {
        private static final int DEFAULT_PRIORITY = 0;
        private static AtomicLong instanceCounter = new AtomicLong();

        public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                int keepAliveTime, TimeUnit unit) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, (BlockingQueue) new PriorityBlockingQueue<ComparableTask>(10,
		ComparableTask.comparatorByPriorityAndSequentialOrder()));
        }

        @Override
        public void execute(Runnable command) {
                super.execute(command);
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class ThreadPoolExecutors {
    
    public ThreadPoolExecutors(int nWorkerThreads, int nIOThreads) {
        ThreadPool.SetMinThreads(nWorkerThreads, nIOThreads);
    }
    
    public void Execute(CodeGearExecutor command) {
        Task.Factory.StartNew(() => command.Run());
    }
}

# Unityで使用されているライブラリとの比較

Unityで使用されている既存のライブラリとして、Photon Unity Networking 2(PUN2)、MLAPIと、Christie # の比較を行う。

Christie # PUN2 MLAPI
通信方式 p2p クライアントサーバ方式 クライアントサーバ方式
プロトコル TCP TCP TCP
特徴 通信のためのIP address がプログラム直接記述されていない Photon Cloud でサーバを自前で用意する必要がない Unity公式でサポートされている RPC が使用可能

# チート対策について

  • オンラインゲームにおいてチート対策は必須

  • 通常のオンラインゲームでのチート対策

    • クライアントをモニタリングする
    • ダメージ計算などは全てサーバで行う
    • ユーザからの通報
  • Christie では型があるDataGear をkey と合わせてDGMに格納する方式を取っている

    • 他のノードとの通信にはDGM のporxy に書き込むことで可能
    • DGM の構成にはTopology Manager が自動的に構成する
  • Topology Manager を使用することでクライアントは接続先を直接知る必要がない

    • IP address などチートに使用される情報をプログラムに含めることなく通信可能
message
label を使用したデータ通信

# 実装の現状

  • Local DGMを使用してUnity 上でデータ通信を行うことができている
  • Scketo とMessagePack を用いた通信に関しては、書き換え途中
    • 独自クラスをMessagePack でserialize できない
  • 今後の予定
    • Christie で実装されている例題
    • Alice からChristie に書き換えた際に取り除かれた機能の洗い出しを行う
    • Unity でChristie #の検証として100人規模のFPS の作成

# まとめ

  • Christie をUnity で使用するためにC# に書き換えを行った
  • 書き換え方針としては、attribute やMessagePack などC# 独自の機能に対応しつつ元のソースコードと同一になるようにした
  • 実装としては、localDataGearManager を用いた同一プロセスで複数インスタンス立ち上げによる通信が可能
  • Remote DataGearManager を使用した複数台の通信については書き換え途中であり、引き続き行っていく
  • Christie の検証のためUnity で100人規模のFPS を作成する
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy