62
|
1 ## 継続を使用する並列分散フレームワークのUnity実装
|
|
2
|
64
|
3 ---
|
62
|
4
|
68
|
5 author: Ryo Yasuda, Shinji Kono
|
|
6 profile: 琉球大学理工学研究科情報工学専攻 河野研究室
|
|
7
|
|
8 ### 概要
|
|
9 * オンラインゲームにおける通信にはクライアントサーバ方式が主流
|
|
10 * データの共有はサーバを経由するため低速
|
|
11 * 当研究室で開発を行っているChristie の分散計算を使用することで、高速かつ安全に通信できると考えた
|
|
12 * Christie をUnity で使用するためにC# で書き換えを行った
|
|
13 * 実装としては、localDataGearManager を用いた同一プロセスで複数インスタンス立ち上げによる通信が可能
|
|
14
|
|
15 ---
|
|
16
|
62
|
17 ### オンラインゲームにおけるデータ通信
|
68
|
18 * オンラインゲームは複数のプレイヤーが関与する分散プログラム
|
64
|
19 * 分散プログラムを正しく書くことは難しい
|
|
20 * 攻撃の標的になる場合が多い
|
68
|
21 * クライアントの負荷軽減やチート対策のため、クライアントサーバ方式が主流
|
|
22 * データの同期にはサーバを経由するため低速
|
62
|
23
|
64
|
24 ---
|
|
25
|
|
26 ### オンラインゲームにおけるデータ通信
|
|
27 * 当研究室では並列分散通信フレームワークChristie を開発中である
|
68
|
28 * 型のあるDataGear とKey を持つストリーム、DataGearManager として格納している
|
64
|
29 * 他のノードはDGM のproxyを持っており、proxy に書き込むことで通信を実現している
|
|
30 * DGM はトポロジーマネージャーによって自動的に構築される
|
|
31 * プログラム自体はDGM の名前を知っていれば良い
|
|
32 * 他のノードのIP addressなどを知る必要はない
|
|
33
|
|
34 * ネットワークが切断されてもゲームは継続可能
|
|
35 * ノードが接続している対象を直接知ることはできない
|
|
36 * チートに対する耐性がある
|
63
|
37
|
|
38
|
68
|
39 * 本研究ではJava で書かれたChristieとC# で書き換えを行ったChristie #を説明し、その機能と実装の差について考察を行う
|
64
|
40
|
|
41 ---
|
|
42
|
|
43 ### Christie の基礎概念
|
63
|
44 * Christie は当研究室で開発をしている並列分散通信フレームワークである
|
64
|
45 * 同じく当研究室で開発しているGearsOS に導入予定のため次のような概念を持っている
|
|
46
|
|
47 * CodeGear (クラスやスレッド)
|
|
48 * DataGear (変数データ)
|
|
49 * CodeGearManager (CG,DG,DGMを管理)
|
|
50 * DataGearManager (DGを管理,localとremoteの2種類がある, put操作によりDGを格納)
|
|
51
|
|
52 ---
|
|
53
|
|
54 ### Christie の基礎概念
|
68
|
55 <center><img src="https://i.imgur.com/ZvpoXGd.png" alt="message" width="450" height="300"></center>
|
64
|
56 <center>
|
|
57 Christie を同一プロセスで複数インスタンス立ち上げた際の接続の構造図
|
|
58 </center>
|
|
59
|
68
|
60
|
64
|
61 * 全てのCGM はThreadPool と他のCGM をList として共有している
|
|
62 * ThreadPool はCPU に合わせた並列度でqueue に入ったThread を逐次実行していく
|
|
63 * 1つのThreadPool で処理を行うことでCPU のコア数に適したThread を管理でき、並列度を下げ流ことを防ぐ
|
|
64 * ThreadPoolを共有することメタレベルで全てのCG/DG にアクセス可能
|
|
65
|
|
66 ---
|
|
67
|
|
68 ### Christie の基礎概念 annotationについて
|
|
69 DG を取り出すためにCG内に宣言した変数データにannotation をつける。annotationには以下の4つがある。
|
|
70
|
|
71 * Take
|
|
72 * 先頭のDG を読み込み、そのDG を削除する
|
|
73 * DG が複数ある場合Take を使用する
|
|
74 * Peek
|
|
75 * 先頭のDG を読み込むがDG を削除しない
|
|
76 * 操作をしない場合は同じデータを参照し続ける
|
|
77 * TakeFrom
|
|
78 * Take と同じ動作だが、remote 先のDGMを指定できる
|
|
79 * PeekFrom
|
|
80 * Peek と同じ動作だが、remote 先のDGMを指定できる
|
|
81
|
|
82 ---
|
|
83
|
|
84 ### Topology Manager
|
68
|
85 * Christie 上でNetwork Topology を形成する
|
64
|
86 * 参加を表明したノードに名前を与える
|
|
87 * 必要があればノード同士の配線を自動で行う
|
|
88
|
|
89 * 静的Topology と動的Topology 2種類がある
|
|
90
|
|
91 ---
|
|
92
|
67
|
93 ### Topology Manager
|
|
94 * 静的Topology は以下のようなdot ファイルを与えることでNode の関係を構築できる
|
|
95 * それぞれのNode への通信にはIP address などは使用せずright というlabel を使用することで接続できる
|
|
96
|
|
97 ```ring.dot
|
|
98 digraph test {
|
|
99 node0 -> node1 [label="right"]
|
|
100 node1 -> node2 [label="right"]
|
|
101 node2 -> node0 [label="right"]
|
|
102 }
|
|
103 ```
|
|
104
|
|
105 <center><img src="https://i.imgur.com/pCCHo2W.png" alt="message" width="200" height="300"></center>
|
|
106 <center></center>
|
|
107
|
|
108
|
68
|
109 <!---
|
67
|
110
|
|
111 ---
|
|
112
|
64
|
113 ### Christie のコード例
|
|
114
|
|
115 ``` java:StartHelloWorld.java
|
|
116 public class StartHelloWorld extends StartCodeGear {
|
|
117
|
|
118 public StartHelloWorld(CodeGearManager cgm) {
|
|
119 super(cgm);
|
|
120 }
|
|
121
|
|
122 public static void main(String[] args){
|
|
123 CodeGearManager cgm = createCGM(10000);
|
|
124 cgm.setup(new HelloWorldCodeGear());
|
|
125 cgm.setup(new FinishHelloWorld());
|
|
126 cgm.getLocalDGM().put("helloWorld","hello");
|
|
127 cgm.getLocalDGM().put("helloWorld","world");
|
|
128 }
|
|
129 }
|
|
130 ```
|
62
|
131
|
64
|
132 ```java:HelloWorldCodeGear.java
|
|
133 public class HelloWorldCodeGear extends CodeGear {
|
|
134
|
|
135 @Take String helloWorld;
|
|
136
|
|
137 @Override
|
|
138 protected void run(CodeGearManager cgm) {
|
|
139 System.out.print(helloWorld + " ");
|
|
140 cgm.setup(new HelloWorldCodeGear());
|
|
141 cgm.getLocalDGM().put(helloWorld,helloWorld);
|
|
142 }
|
|
143 }
|
|
144 ```
|
|
145
|
|
146 ```java:FinishHelloWorld.java
|
|
147 public class FinishHelloWorld extends CodeGear {
|
|
148 @Take String hello;
|
|
149 @Take String world;
|
|
150
|
|
151 @Override
|
|
152 protected void run(CodeGearManager cgm) {
|
|
153 cgm.getLocalDGM().finish();
|
|
154 }
|
|
155 }
|
|
156 ```
|
68
|
157 --->
|
|
158
|
|
159 ---
|
|
160
|
|
161 ### Java からの変更点
|
|
162 * Java とC# は基本的に書き方は変わらない
|
|
163
|
|
164
|
|
165 ```java:ex.java
|
|
166 Java
|
|
167 public class StartHelloWorld extends StartCodeGear { }
|
|
168
|
|
169 @Override
|
|
170 protected void run(CodeGearManager cgm) { }
|
|
171
|
|
172 @Take String helloWorld;
|
|
173 ```
|
|
174
|
|
175 ```cs:ex.cs
|
|
176 C#
|
|
177 public class StartHelloWorld : StartCodeGear { }
|
|
178
|
|
179 public override void Run(CodeGearManager cgm) { }
|
|
180
|
|
181 [Take] string helloWorld;
|
|
182 ```
|
64
|
183
|
|
184 ---
|
|
185
|
|
186 ### Christie \# のコード例
|
|
187 ```cs:StartHelloWorld.cs
|
|
188 public class StartHelloWorld : StartCodeGear {
|
|
189
|
|
190 public StartHelloWorld(CodeGearManager cgm) : base(cgm) { }
|
|
191
|
|
192 public static void Main(string[] args) {
|
|
193 CodeGearManager cgm = CreateCgm(10000);
|
|
194 cgm.Setup(new HelloWorldCodeGear());
|
|
195 cgm.Setup(new FinishHelloWorld());
|
|
196 cgm.GetLocalDGM().Put("helloWorld", "hello");
|
|
197 cgm.GetLocalDGM().Put("helloWorld", "world");
|
|
198 }
|
|
199 }
|
|
200 ```
|
|
201
|
|
202 ```cs:HelloWorldCodeGear.cs
|
|
203 public class HelloWorldCodeGear : CodeGear {
|
|
204 [Take] string helloWorld;
|
|
205
|
|
206 public override void Run(CodeGearManager cgm) {
|
|
207 Console.Write(helloWorld + " ");
|
|
208 cgm.Setup(new HelloWorldCodeGear());
|
|
209 cgm.GetLocalDGM().Put(helloWorld, helloWorld);
|
|
210 }
|
|
211 }
|
|
212 ```
|
|
213
|
|
214 ```cs:FinishHelloWorld.cs
|
|
215 public class FinishHelloWorld : CodeGear {
|
|
216 [Take] private string hello;
|
|
217 [Take] private string world;
|
|
218
|
|
219 public override void Run(CodeGearManager cgm) {
|
|
220 cgm.GetLocalDGM().Finish();
|
|
221 }
|
|
222 }
|
|
223 ```
|
62
|
224
|
68
|
225 1. Main関数でCGM のインスタンス生成
|
|
226 2. 2つのCG をsetupして待ち状態にする
|
|
227 3. key:hellowWorld data:"hello" がTake される
|
|
228 4. 変数が揃ったためStartHelloWorld のRun が実行される
|
|
229 5. "hello" がprintされ、再び待ち状態になる。 key:hellow data:"hello"がput される
|
|
230 6. key:hellowWorld data:"world" がTake され、4,5と同様に処理される
|
|
231 7. 変数hello とworld がput され揃ったため、FinishHelloWorld のRun が実行され、プログラムは終了する
|
|
232
|
|
233
|
|
234
|
|
235
|
62
|
236 ### Unity
|
64
|
237 * UnityはUnity Technologies が開発を行っているゲームエンジンである
|
|
238 * 世界で最も使用されているゲームエンジン
|
|
239 * 非常に軽く、スペックが低いノートPCでもゲーム開発が可能
|
|
240 * プログラミング言語にはC# が採用されている
|
68
|
241 * C# のAPI やUnity 向けに拡張されたAPIも使用可能
|
|
242 * 開発した機能をUnity に組み込むことも可能
|
64
|
243
|
|
244 ---
|
62
|
245
|
65
|
246 ### Christie \# on Unityのコード例
|
64
|
247 ```cs:UnityStartHelloWorld.cs
|
|
248 public class StartHelloWorld : StartCodeGear {
|
|
249
|
|
250 public StartHelloWorld(CodeGearManager cgm) : base(cgm) { }
|
62
|
251
|
64
|
252 public void RunCodeGear(CodeGearManager cgm) {
|
|
253 cgm.Setup(new HelloWorldCodeGear());
|
|
254 cgm.Setup(new FinishHelloWorld());
|
|
255 cgm.GetLocalDGM().Put("helloWorld", "hello");
|
|
256 cgm.GetLocalDGM().Put("helloWorld", "world");
|
|
257 }
|
|
258 }
|
|
259 ```
|
|
260
|
|
261 ```cs:UnityHelloWorld.cs
|
|
262 public class HelloWorld : MonoBehaviour {
|
|
263 void Start() {
|
|
264 CodeGearManager cgm = StartCodeGear.CreateCgm(10000);
|
|
265 var helloWorld = new StartHelloWorld(cgm);
|
|
266 helloWorld.RunCodeGear(cgm);
|
|
267 }
|
|
268 }
|
|
269 ```
|
68
|
270 * HelloWorldCodeGearと、FinishHelloWorld はそのまま使用
|
|
271 * StartHelloWorld をUnity で使用できるように書き換え
|
|
272 * Unity ではMonoBehaviour 継承したクラスが動作可能
|
|
273 * ゲーム開始時に1度だけ呼ばれるStart 関数
|
|
274 * Start 関数でCGM のインスタンスを生成
|
|
275 * Main 関数を名前を変えたRunCodeGear 関数を実行
|
64
|
276
|
|
277 ---
|
|
278
|
|
279 ### Take annotationの実装
|
|
280 * Christie ではDGを取得するためにannotation を使用している
|
65
|
281 * C# ではannotation と同様の機能にattribute があり、Take をattribute で実装した
|
|
282
|
|
283 * Take はフィールド変数に対して適用する
|
|
284
|
64
|
285
|
|
286 ```java:Take.java
|
|
287 @Target(ElementType.FIELD)
|
|
288 @Retention(RetentionPolicy.RUNTIME)
|
|
289 public @interface Take { }
|
|
290 ```
|
|
291
|
|
292 ```cs:Take.cs
|
|
293 [AttributeUsage(AttributeTargets.Field)]
|
|
294 public class Take : Attribute { }
|
|
295 ```
|
62
|
296
|
65
|
297 ---
|
|
298
|
67
|
299 ### MessagePackの相違点
|
68
|
300 * Christie ではMessagePack を使用してデータを圧縮し送受信している
|
67
|
301 * インスタンス内のpublic 変数に対して圧縮可能
|
|
302 * バージョンが古いため、現在はサポートされていない
|
|
303 * そのため、最新版とは記述方法が異なる
|
|
304
|
68
|
305 * 圧縮するクラスには@Message annotatoinをつける
|
67
|
306 * MessagePack インスタンスを作成後、write、read することでデータの圧縮解凍が可能
|
|
307 * 圧縮されたデータはbyte[] 型になる
|
|
308
|
|
309 ```java:MessagePackEx.java
|
|
310 public class MessagePackExample {
|
|
311 @Message
|
|
312 public static class MyMessage {
|
|
313 public String name;
|
|
314 public double version;
|
|
315 }
|
|
316
|
|
317 public static void main(String[] args) throws Exception {
|
|
318 MyMessage src = new MyMessage();
|
|
319 src.name = "msgpack";
|
|
320 src.version = 0.6;
|
|
321
|
|
322 MessagePack msgpack = new MessagePack();
|
|
323 // Serialize
|
|
324 byte[] bytes = msgpack.write(src);
|
|
325 // Deserialize
|
|
326 MyMessage dst = msgpack.read(bytes, MyMessage.class);
|
|
327 }
|
|
328 }
|
|
329 ```
|
|
330
|
|
331 ---
|
62
|
332
|
67
|
333 ### MessagePackの相違点
|
|
334 * C# のMessagePack は複数存在している
|
|
335 * java 版と似たような書き方をするMessagePack-CSharp を選択した
|
|
336
|
|
337 * 圧縮を行いたいクラスに対してMessagePackObject attribute を付ける
|
|
338 * 圧縮する変数に対してkey を設定できる
|
|
339 * 解凍時にjson として展開できる
|
|
340 * データの圧縮にはMessagePackSerializer.Serialize 関数を用い、byte[] に圧縮される
|
|
341 * データの解凍にはMessagePackSerializer.Deserialize 関数を使用する
|
|
342 * Deserialize 関数はジェネリスク関数であるため<>内に解凍するデータの型情報を記述する
|
|
343
|
|
344 ```cs.MessagePackEx.cs
|
|
345 [MessagePackObject]
|
|
346 public class MyClass {
|
|
347 [Key(0)]
|
|
348 public int Age { get; set; }
|
|
349 [Key(1)]
|
|
350 public string FirstName { get; set; }
|
|
351 [Key(2)]
|
|
352 public string LastName { get; set; }
|
|
353
|
|
354 static void Main(string[] args) {
|
|
355 var mc = new MyClass {
|
|
356 Age = 99,
|
|
357 FirstName = "hoge",
|
|
358 LastName = "huga",
|
|
359 };
|
|
360
|
|
361 byte[] bytes = MessagePackSerializer.Serialize(mc);
|
|
362 MyClass mc2 = MessagePackSerializer.Deserialize<MyClass>(bytes);
|
|
363
|
|
364 // [99,"hoge","huga"]
|
|
365 var json = MessagePackSerializer.ConvertToJson(bytes);
|
|
366 Console.WriteLine(json);
|
|
367 }
|
|
368 }
|
|
369 ```
|
65
|
370
|
|
371 ---
|
|
372
|
|
373 ### ThreadPoolからTaskへの書き換え
|
|
374 * Christie ではThreadPool を使用していた
|
|
375 * Christie # ではThreadPoolより高機能なTask を用いて書き換えを行った
|
|
376
|
|
377 * Task は複雑な非同期処理を通常のコーディングと同じ感覚で直感的に記述できる
|
|
378
|
|
379 * 裏でThreadPool が動くようになっている
|
|
380 * 大きく動作は変わらない
|
|
381
|
|
382 ---
|
|
383
|
62
|
384 ### ThreadPoolからTaskへの書き換え
|
|
385
|
65
|
386 ```java:PriorityThreadPoolExecutors.java
|
|
387 public class PriorityThreadPoolExecutors {
|
|
388
|
|
389 private static class PriorityThreadPoolExecutor extends ThreadPoolExecutor {
|
|
390 private static final int DEFAULT_PRIORITY = 0;
|
|
391 private static AtomicLong instanceCounter = new AtomicLong();
|
|
392
|
|
393 public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
|
|
394 int keepAliveTime, TimeUnit unit) {
|
|
395 super(corePoolSize, maximumPoolSize, keepAliveTime, unit, (BlockingQueue) new PriorityBlockingQueue<ComparableTask>(10,
|
|
396 ComparableTask.comparatorByPriorityAndSequentialOrder()));
|
|
397 }
|
|
398
|
|
399 @Override
|
|
400 public void execute(Runnable command) {
|
|
401 super.execute(command);
|
|
402 }
|
|
403 }
|
|
404 }
|
|
405 ```
|
|
406
|
|
407
|
|
408
|
|
409 ```cs:ThreadPoolExecuters.cs
|
|
410 public class ThreadPoolExecutors {
|
|
411
|
|
412 public ThreadPoolExecutors(int nWorkerThreads, int nIOThreads) {
|
|
413 ThreadPool.SetMinThreads(nWorkerThreads, nIOThreads);
|
|
414 }
|
|
415
|
|
416 public void Execute(CodeGearExecutor command) {
|
|
417 Task.Factory.StartNew(() => command.Run());
|
|
418 }
|
|
419 }
|
|
420 ```
|
|
421
|
|
422 ---
|
|
423
|
62
|
424 ### Unityで使用されているライブラリとの比較
|
65
|
425 Unityで使用されている既存のライブラリとして、Photon Unity Networking 2(PUN2)、MLAPIと、Christie # の比較を行う。
|
|
426
|
|
427 | | Christie # | PUN2 | MLAPI |
|
|
428 |--- |--- | --- |---|
|
|
429 |通信方式 |p2p |クライアントサーバ方式 | クライアントサーバ方式|
|
|
430 |プロトコル | TCP | TCP | TCP |
|
|
431 |特徴 |通信のためのIP address がプログラム直接記述されていない |Photon Cloud でサーバを自前で用意する必要がない | Unity公式でサポートされている RPC が使用可能|
|
|
432
|
67
|
433 ---
|
65
|
434
|
67
|
435 ### チート対策について
|
|
436 * オンラインゲームにおいてチート対策は必須
|
|
437 * 通常のオンラインゲームでのチート対策
|
|
438 * クライアントをモニタリングする
|
|
439 * ダメージ計算などは全てサーバで行う
|
|
440 * ユーザからの通報
|
|
441
|
|
442 * Christie では型があるDataGear をkey と合わせてDGMに格納する方式を取っている
|
|
443 * 他のノードとの通信にはDGM のporxy に書き込むことで可能
|
|
444 * DGM の構成にはTopology Manager が自動的に構成する
|
|
445 * Topology Manager を使用することでクライアントは接続先を直接知る必要がない
|
|
446 * IP address などチートに使用される情報をプログラムに含めることなく通信可能
|
|
447
|
|
448 <center><img src="https://i.imgur.com/L8GVFdL.png" alt="message" width="450" height="260"></center>
|
|
449 <center>label を使用したデータ通信</center>
|
62
|
450
|
|
451 ### 実装の現状
|
67
|
452 * Local DGMを使用してUnity 上でデータ通信を行うことができている
|
|
453 * Scketo とMessagePack を用いた通信に関しては、書き換え途中
|
68
|
454 * 独自クラスをMessagePack でserialize できない
|
67
|
455 * 今後の予定
|
|
456 * Christie で実装されている例題
|
|
457 * Alice からChristie に書き換えた際に取り除かれた機能の洗い出しを行う
|
69
|
458 * Unity でChristie #の検証として100人規模のFPS の作成
|
62
|
459
|
67
|
460 ---
|
|
461
|
|
462 ### まとめ
|
68
|
463 * Christie をUnity で使用するためにC# に書き換えを行った
|
|
464 * 書き換え方針としては、attribute やMessagePack などC# 独自の機能に対応しつつ元のソースコードと同一になるようにした
|
|
465 * 実装としては、localDataGearManager を用いた同一プロセスで複数インスタンス立ち上げによる通信が可能
|
|
466 * Remote DataGearManager を使用した複数台の通信については書き換え途中であり、引き続き行っていく
|
69
|
467 * Christie の検証のためUnity で100人規模のFPS を作成する
|
67
|
468
|
|
469
|
|
470
|
|
471
|