KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

カテゴリ: その他

しばらく前に、Windows上でTextureMovie機能の実装をしました。TextureMovieというのは動画形式のファイルからフレームを取得し、デコードした結果をOpenGLやDirectX用のテクスチャへ焼きこんでこれらのレンダリングシステムを使って描画するものです。

ここで出たデコードを処理するのはデコーダーで、つまり、何かしらの形式の色情報をRGB(A)に変換するものです。世の中には既成のデコーダーが幾つかありますが、いろいろ事情があって自作という選択肢に至りました。

また、Windowsのことですから、MSが提供している低レベルAPIで何とかなるのかもしれないと考えました。そこで、ほぼRGBとCMYKしか知らない私がいろいろ調べていきました。もし、 色空間の変換やYUV形式に興味がありましたら 、是非お読みください。

まずやりたいことを一言でまとめると、movie.mp4 -> YUVバッファ -> RGBバッファ -> テクスチャという流れです。

最初にWindowsの標準機能であるMicrosoft Media Foundation Transform (MFT)についていろいろ調べました。悪戦苦闘の末、やっと「YUV」でのデータ出力までたどり着きました(MFTに関してはサンプルや資料が極めて少ないため、結構試行錯誤の連続で.mp4からYUVバッファーの出力までできました。それは別の回に改めて紹介することとし、ここでは割愛します)。

いま振り返ると、そこで現れた見知らぬ「YUV」という単語が実に意味深いです。
「YUVって何ぞや、RGBの従兄弟なの?」と思いながら、Wikipediaを開きました。

YUVYCbCrYPbPrとは、輝度信号Yと、2つの色差信号を使って表現される色空間。

とWikipedia先生がこう答えてくれました。

まだよくわからないため、さらにGoogle先生へ答えを求めました。

「人間の目は明るさの変化には敏感だが, 色の変化に は鈍感である」というわけで,色度を抑え、輝度により広い帯域やビット数を割くことにより、少ない損失で効率の良い伝送や圧縮を実現するフォーマット.
デジタル画像の圧縮CODECにおけるフォーマットという観点でまとめる.

つまり、人間の目をうまく欺いて、データの容量を削減するための圧縮フォーマットと考えればいいでしょう。

エンコード(圧縮をかける)したから、デコード(圧縮を解除する)と呼ばれた処理なんだとふっと思いながら、YUVからRGBに変換する方法を探し始めました。

ところが、YUVというのは一つの形式ではなく、幾つかの形式の総称だということに気づきました。MSDNから各形式についてこんな説明があります(以下、図を引用します)。

image04

正直、ぱっと見ただけでは最初に文字も図も今ひとつ分からなかったので、読み飛ばして分かるところまで先に進みました。

まず、そのページの後半にとても重要な計算式が書いてあります(同ページより引用)。

Converting 8-bit YUV to RGB888

From the original RGB-to-YUV formulas, one can derive the following relationships for BT.601.

Y = round( 0.256788 * R + 0.504129 * G + 0.097906 * B) +  16
U = round(-0.148223 * R - 0.290993 * G + 0.439216 * B) + 128
V = round( 0.439216 * R - 0.367788 * G - 0.071427 * B) + 128

Therefore, given:

C = Y - 16
D = U - 128
E = V - 128

the formulas to convert YUV to RGB can be derived as follows:

R = clip( round( 1.164383 * C                   + 1.596027 * E  ) )
G = clip( round( 1.164383 * C - (0.391762 * D) - (0.812968 * E) ) )
B = clip( round( 1.164383 * C +  2.017232 * D                   ) )

where clip() denotes clipping to a range of [0..255]. We believe these formulas can be reasonably approximated by the following:

R = clip(( 298 * C + 409 * E + 128) >> 8)
G = clip(( 298 * C - 100 * D - 208 * E + 128) >> 8)
B = clip(( 298 * C + 516 * D + 128) >> 8)

上記の変換から得たのをもう少し整理すると

R = clip(( 298 * (Y - 16) + 409 * (V - 128) + 128) >> 8)
G = clip(( 298 * (Y - 16) - 100 * (U - 128) - 208 * (V - 128) + 128) >> 8)
B = clip(( 298 * (Y - 16) + 516 * (V - 128) + 128) >> 8)

になります。

これを手に入れたら、あとはYUVの構造さえ分かれば、変換が可能になります。

次に、代表的なフォーマット毎に詳しい説明があります(同ページより引用)。

4:4:4 Formats, 32 Bits per Pixel

AYUV

image06

このフォーマットは輝度毎に色差信号をサンプリングし、アルファ情報もついています。最高クォリティの画質です。

4:2:2 Formats, 16 Bits per Pixel

YUY2

image09

UYVY

image08

このフォーマットはY0とY1が同じUV(U0、V0)を使っています。YUY2とUYVYの差はY、U、Vの並び順だけです。

4:2:0 Formats, 16 Bits per Pixel

※以下のメモリイメージ図はビデオフレームの横幅がY配列の列数と等しく、ビデオフレームの縦幅はY行列の行数と同じです(実際にメモリ上は一次配列扱い。同ページより引用)。

IMC1

image10

IMC3

image05

このフォーマットは四つのYが同じUVを使います。

特徴的な構造として、メモリ上配列でYの値を全部格納してから、Uの配列、Vの配列を置いていることが挙げられます。ただし、UとVの配列はYと同じ幅(行と列の比率、ここはややこしい)なので、ややメモリ利用効率が悪そうです。

また、IMC1とIMC3のメモリ構造はほぼ一緒です。唯一異なるのはUの配列とVの配列どちらを先に並べるかです。

例:

352 * 240のビデオフレームだと

Yの値は352個/行 * 240行(行:列 = 352 : 240 = 22/15)

UとVの値それぞれは176個/行 * 120行(行:列 = 176 : 120 = 22/15)

4:2:0 Formats, 12 Bits per Pixel

これらのフォーマットは全部四つのYが同じUVを使用しています。違いはメモリ上でのデータ格納方法だけです。

IMC2、IMC4

image03
(IMC2)

この形式の特徴はYの値を全部格納してからUとVの値を交替に格納することです。つまり一行の半分はUの値、残り半分はVの値になります。メモリをより効率的に利用できます。IMC2とIMC4の違いはU配列とV配列どちらが先に置かれるかです。また、IMC2はNV12以外で最も使われる形式だと書いてあります(つまり、NV12は最も使われているということで、これは扱いやすさゆえでしょう)。

YV12

image01

正直なところ最初にこの図を見て、IMC1とは何か違うのが分かりませんでした。その後に分かりやすい説明を見つけました(下図は英語版Wikipediaより引用)。

image00

なお、YUV420はYV12の別名です。

簡単にいうと、メモリ上でみた場合にY、U、Vの配列が全部繋がっています。

NV12

image07

同じく、分かりやすい説明が見つかりました(同上)。

image02

NV12のメモリ構造を見ると、最も頻繁に使われる理由が分かります。UVがセットで来るため、ループでとてもまわしやすい構造です。

Yが四つに対し、UVセット一つを使うので、とても自然なプログラムが書けます。

ここまで全部読んだら、一番最初の〇と×の図もちゃんと理解できるようになりました。×はluma = 輝度 = Y、〇はChroma = 色差 = UVということでした。

これで.mp4からテクスチャへの色変換の基礎知識が全部揃いました。計算式とメモリ上のデータ取得部分をプログラムへ組み込み、またMFTで試行錯誤して完成にこぎつけました。

最後に変換部分のコードを抜粋したものを掲載します:

#define CLIP(x) do{if(x < 0){x = 0;} else if(x > 255){x = 255;}} while(0)
#define CONVERT_R(Y, V)    ((298 * (Y - 16) + 409 * (V - 128) + 128) >> 8)
#define CONVERT_G(Y, U, V) ((298 * (Y - 16) - 100 * (U - 128) - 208 * (V - 128) + 128) >> 8)
#define CONVERT_B(Y, U)    ((298 * (Y - 16) + 516 * (U - 128) + 128) >> 8)

void ImplementationMovie::NV12ToRGB(u8* rgbBuffer, u8* yuvBuffer, int width, int height)
{
    u8* uvStart = yuvBuffer + width * height;
    u8 y[2] = { 0, 0 };
    u8 u = 0;
    u8 v = 0;
    int r = 0;
    int g = 0;
    int b = 0;
    for (int rowCnt = 0; rowCnt < height; rowCnt++)
    {
        for (int colCnt = 0; colCnt < width; colCnt += 2)
        {
            u = *(uvStart + colCnt + 0);
            v = *(uvStart + colCnt + 1);

            for (int cnt = 0; cnt < 2; cnt++)
            {
                y[cnt] = yuvBuffer[rowCnt * width + colCnt + cnt];

                r = CONVERT_R(y[cnt], v);
                CLIP(r);
                g = CONVERT_G(y[cnt], u, v);
                CLIP(g);
                b = CONVERT_B(y[cnt], u);
                CLIP(b);
                rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 0] = (u8)r;
                rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 1] = (u8)g;
                rgbBuffer[(rowCnt * width + colCnt + cnt) * 3 + 2] = (u8)b;
            }
        }

        uvStart += width * (rowCnt % 2);
    }
}

Kyo Shinyuu

WindowsアプリにWebブラウザーの機能を簡単に組み込もうとした時、どういう方法を使いますか?案外とどこから着手したら良いか迷う方が多いのではないでしょうか。

特にマルチプラットフォーム開発の時にWindowsでもIEコンポーネントではなく、iOSのUIWebViewやWKWebView、AndroidのWebViewと似た使い方をできる仕組みがあると便利です。

今回はChromiumEmbedded(CEF3)というオープンソースのフレームワークを紹介します。

CEF3以外にもオープンソースの選択肢としてはWebkitがあります。興味がある方はそちらも検討してください。

CEF3はChromiumの機能をアプリケーション内に組み込むためのフレームワークです。Chromiumの比較的新しいバージョンを利用できるため、いわゆるモダンブラウザの機能をWindowsプログラムへ組み込むのに向いています。

しかし残念ながらあまりドキュメントが充実しておらず、特に少しこだわった使い方をしようとするとハマりどころが多くあります。

また、既にOpenGLを使っているWindowsアプリにWebブラウザー機能を組み込もうとすると、お互いの表示が干渉し合う現象が発生します。その点についても後ほど詳しく記述します。

Windows用WebViewとして実装したい機能

ポジションセット
client上任意のポジションにWebViewを設置すること
リサイズ
WebViewの大きさを変えること
可視性
WebViewの表示/非表示
ScalePageToFit
適切な大きさでページを表示
SetCustomHeader
リクエストのヘッダーをカスタマイズ
JavaScript実行
ページ上のボタンを押すなどのイベントで任意のJavaScriptコードを実行

最初の難関

WindowsアプリにWebViewを組み込む需要はあるはずなのに、CEFの話はあまり聞いたことがありません。

その原因の一つは資料や文献はほとんど英語で、サンプルも少ないことだと考えられます。

どこから着手するのかが分からない人もいれば、途中で挫折して諦める人も少なくないでしょう。

ここでは今回の参考資料のリンクを先に貼っておきます。

CEF wiki
概要をまとめたページで、さまざまな関連リンクがあります。
CEF Forum
先人の知恵はたくさんあるので、はまったことがあったらここでトピックを探すのをおすすめ。
CEF Builds
ここからサンプルやプロジェクトファイルを入手できます。動くものがあれば結構良い足がかりになります。
API Doc
API一覧、メソッドのことならここで一目瞭然です。

CEFの特徴

機能がとても充実していて、パワフルです。しかし全体のイメージを掴むのにはやや学習コストが必要です。いきなり大きな機能をつけるよりも、小さいプロジェクトからスタートしてどんどん機能を追加していくスタイルをお勧めします。

コーディング上の特徴や注意点を簡単に挙げておきます。

C++にはC#やJavaの”interface”に直接該当するものがないので、handlerを多重継承で必要なメソッドを生やすことになります。

RTTIやキャストミスを避けるために、継承したクラスのポインタを取得するgetterを全部上書きしています。

純粋仮想関数の定義が漏れないように継承先のメソッド一覧を一度きちんと把握しておいたほうがいいです。

ポインタは全部STLのshared_ptrのようなクラスにラップされるため、オブジェクトのメモリ解放についてはあまり気にする必要がありません。

実際にCEFを使って必要な機能を揃えるまで

Step 0

サンプルプロジェクトの準備と実行確認

  • CEF BuildsからCEF 3.2556.1368.g535c4fb (もしそれより新しいバージョンがでたら、それを使ってください)をダウンロードし、解凍

  • VS2010用の.slnを開くのがおすすめ

  • ソリューションを開くと、三つのプロジェクトが入っていることを確認

  • 最後のラッパー以外の二つそれぞれスタートプロジェクトとして実行してみる

image02

今回の用途においてはCEF3の全体像を把握するのが重要だと考えました。このため出発地点としてはcefclientのほうがいいと判断し、cefsimpleは不要なので削除しました。

cefclientプロジェクトにはファイルが結構な数ありますが、一通り眺めた結果、ClientHandlerClientAppという二つのクラスがとても重要な役割を担っていることがわかりました。その二つのクラスはそれぞれ幾つかのhandlerクラスを継承し、ブラウザーに対する操作を担います。

image00

Step 1

ClientHandlerClientAppをダイエットさせて、最低限必要な機能だけにしました。

いろいろ試行錯誤した結果、

  • ClientHandlerクラスはCefClientCefLifeSpanHandlerを継承

  • ClientAppクラスはCefAppを継承

という構造にしました。さらにMainWindowの子ウィンドウを作ってそこにブラウザーを生成するようにシンプル化しました。

Step 2

とりあえず現段階できた分をアプリに組み込み(やっと本題に入ります)

ヘッダー等々が認識させるように幾つか工夫が必要

  • 実行に必要なdll等のコピー(サンプルプロジェクト実行する時の.gypから出力したログを読んで把握できます)

  • プロジェクトプロパティの修正(lib内の相対パス参照のエラーを解決するため)

クラス定義やインターフェースを実装してから、ウィンドウの親子関係を整理して実行します。

ありがちな問題

WebViewがちらつく(OpenGLとの干渉が発生)

  • この問題はネットで検索すれば結構出ます。

  • WindowsのマルチウィンドウプログラムとOpenGLの相性の問題はありがちのようです。

  • 解消方法も幾つか書いてありますが、残念ながら自分が試した結果いずれもうまく対応できませんでした。

散々試したあげく、何もしないスーバーウィンドウをメインウィンドウとして作って、アプリウィンドウ(本来のメインウィンドウ、OpenGL使用)とWebViewウィンドウの両方を子ウィンドウにした上で、SetWindowPosでwebviewウィンドウを手前に出すという非常に遠回りな方法で解消しました。

Step 3

機能を増やす

  • ポジションセット

  • リサイズ

  • 可視性

SetWindowPosMoveWindowでウィンドウの位置や大きさを設定できます。ShowWindowで可視性を設定できます。

一点気を付けることがあります。WebViewウィンドウとスーパーウィンドウの親子関係の都合上、スーパーウィンドウの位置が変わった場合にはWebViewウィンドウも一緒に移動させる必要があります。

ありがちな問題

ブラウザーの大きさが変わらない(もしくは微妙に小さくなったりする)

  • 大体の場合、原因はbrowserウィンドウのパラメータ変更忘れです

  • WebViewウィンドウの大きさだけを変えた場合、browserウィンドウが大きくなることはないでしょう。

WebViewウィンドウはOpenGLとの干渉問題を解決するための存在で、browserウィンドウの親ウィンドウです。画面上にブラウザーが一つしかない場合、これらのウィンドウの大きさを完全に同じにすればよいです。また、browserウィンドウのポジションはWebViewウィンドウとの相対位置(デフォルトは同位置)なので、特に設定するの必要はありません。

browserウィンドウはbrowser->GetHost()->GetWindowHandle()で取得できます。

image01

Step 4

更に機能追加

  • ScalePageToFit

  • SetCustomHeader

HTTPリクエスト時にカスタムヘッダを付加する仕組み(SetCustomHeader)は割と簡単です。まずClientHandlerの継承にCefRequestHandlerを追加します。

そしてリクエストの生成時にHeaderMapへとkey-valueペアを登録してLoadRequestを呼ぶだけで終わりです。

ScaleToFitPageの対応は若干手間です。

まず、scale = 1.2 ^ zoomLevelという計算式があります(Forumから入手した情報)。

その逆算でzoomLevel = lg(scale) / lg(1.2)が分かります。

一定の基準となるscaleからzoomLevelを計算できたら、SetZoomLevelを読んでコンテンツの大きさを変更できるはずです

が、大きさが全然変わりません

数時間の試行錯誤の後に、SetZoomLevelをリクエストの処理よりも前に呼んでも何も反映されないことがわかりました。

SetZoomLevelの呼び出し位置を変更し、CefRequestHandlerOnBeforeResourceLoadイベントで呼ぶことでうまく反映されます。

Step 5

JavaScript実行

wiki上にはcef / JavaScriptIntegrationというページがあってJSObjectやJSFunctionなどの記述は全部書いてありますので、これらは省略します。

JavaScriptコードの実行はとても簡単で、基本的にExecuteJavaScriptへ引数を渡せば終わりです。

ありがちな問題

Window Bindingをしたい時に、OnContextCreated()メソッドが全く呼ばれない

  • まず、ClientAppクラスはCefRenderProcessHandlerを継承する必要があります。

  • 初期化の時にCefSettings settings; settings.single_process = true;を設定し、singleProcessにしないと各browserのrenderProcessになります。

後はおまけとして、JSObjectJSFunctionが組み合わせて階層化可能で、javascript:window.JSObject.JSFunction()という構造を作れます。

実際にはJSFunctionJSObjectとして生成し、親のJSObjectの下にぶら下げます。

たとえば、JavaScript側からwindow.engine.func()でC++側の指定メソッドを呼び出したい場合、コードは以下のようになります。

void MyWebView::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
{
  CefRefPtr<CefV8Handler> handler = new MyV8Handler(this);
  CefRefPtr<CefV8Value> obj = context->GetGlobal();

  CefRefPtr<CefV8Value> val = CefV8Value::CreateObject(NULL);
  val->SetValue("func", CefV8Value::CreateFunction("func", handler),V8_PROPERTY_ATTRIBUTE_NONE);
  obj->SetValue("engine", val, V8_PROPERTY_ATTRIBUTE_NONE);
}

CreateObjectで作成したJavaScript側のオブジェクトに対してCreateFunctionで作成した関数を割り当て、JavaScript側のglobalオブジェクト(=Webブラウザ上ではwindowオブジェクト)へぶら下げるのがポイントです。

まとめ

今回はCEF3を使ってiOSやAndroidのWebViewに用意されている機能と似た仕組みをWindows上で実現する方法と、その中で遭遇するいくつかの問題への解決策を紹介しました。

個人的には、CEF3があまり普及しない理由はひょっとすると機能が充実しすぎて、規模が大きすぎるためかもしれないと思いました。

本文でも書いたように、最小化したプロジェクトに対して、追加したい機能に応じてhandlerをどんどん継承し構築していく方法で取り組むのをおすすめします。

Kyo Shinyuu

このエントリーは、KLab Advent Calendar 2015 の12/24の記事です。

2015年新卒の本堂です。

この記事ではロシア語テクストを統計的に分析することについて紹介します。

少しでもコンピュータを使って言語を分析することについて興味をもっていただけたら著者として幸せです。

  • ロシア語を少しでも勉強したことのある方
  • ロシア語を数量的に分析することに興味のある方

を読者として想定しています。

はじめに

僕の専門は言語学、特に言語獲得で、さまざまな言語獲得を実験する過程でロシアにも2年ほど住んでいました。

言語獲得というのは、乳幼児の言語習得や外国語習得などです。

乳幼児と大人の言語獲得スピードの差は今だに人類の謎です。

最近はコンピュータを使った数量的な分析を行う計量言語学も(特に英語で)盛んなので、

乳幼児が言語獲得していく過程と、僕が外国語としてロシア語を習得していく過程を数量的に表現したいと思いました。

例えば、乳幼児が生活に密着した語彙から覚えるのに対して、大人は抽象的な語彙から覚えるとか、

乳幼児は覚えた語彙の使用頻度が高いのに対し、大人は滅多に使わない語彙から覚えるなど。

というようなことを数量化できるかもしれない。

プログラミングは知らなかったので、最初は身近にあったMicrosoftのエクセルで始めました。

excel_stats_sample

このように(どうやったかはもう覚えていませんが)一行に一文、一列に一語ずつ入力して、

エクセルの関数を組み合わせて、なんとか目的の語を入力して出現回数を表示させることができました。

しかし、"я"と"мне"が別々にカウントされています。

(яは"私"の意。"мне"は"я"の与格で"私に"の意。)

格ごとに数えたい場合はこれでもいいですが、僕は語を数えたかったのでどちらも"я"としてまとめてほしいわけです。

"я"などはまだ曲用が少ない方なので、格それぞれの出現回数を合計するといいでしょう。

しかし、ご存知のように"идти"のような動詞は形動詞も含めると数十パターンの変化をするので、

これを手作業で行うのは大変です。

エクセルはロシア語の語形変化など知りませんから、教える必要があります。

どうせ自分で教えるなら自分でプログラムを書こう、と勉強を始めました。

ロシア語の自然言語処理について日本語で書かれた情報が全くないので、最初はとても苦労しました。

今後勉強される方のために情報を残しておこうと思います。

自然言語処理とは

コンピュータでロシア語など人の話す言語を処理する分野を総称して「自然言語処理」といいます。

"мне"を"я"に自動で変換することも自然言語処理の一部です。

プログラムもなんらかの言語によって書きます。

この分野ではPython(パイソン)というプログラミング言語がよく使われます。

自然言語処理やPythonについての説明は、すでに優れた書籍や解説記事がいくつもあるのでそれらを参照してください。

入門 自然言語処理という書籍は特におすすめです。自然言語処理だけでなくPythonの入門書としてもわかりやすく、プログラミングの経験がない僕でも楽しく読み進められました。

英語版であればこちらからPDFで読むことができます。

実際にロシア語テクストを分析してみよう・用語編

それでは実際にPythonを使ってチェーホフの戯曲「かもめ」の使用語彙を分析してみましょう。

その前に、これから使う用語を重要なものだけ整理しておきます。

「トークン」は、テキスト中に現れる語そのものです。句読点の間には適宜スペースをいれてあげます。

例えば、"Отчего вы всегда ходите в черном?"という文は、"Отчего"、"вы"、"всегда"、"ходите"、"в"、"черном"、"?"という7つのトークンからなります。

「レマ」は、そのトークンが見出し語として辞書に載っている語形のことだと思って大丈夫です。トークンは必ず1つのレマをもちます。

例えば、トークン"мне"のレマは"я"、トークン"я"のレマも同じく"я"です。トークン"шедщий"のレマは"идти"です。

「レマ化」とは、トークンをレマに変換することです。つまり、トークン"шедщий"をレマ"идти"に変換することです。

「タグ」は、トークンに対しての付加情報です。品詞、態、性別などの形態情報だけでなく意味情報・統語情報なども含みます。

色々な表記方法がありますが、例えば、"Отчего/疑問詞 вы/代名詞 всегда/副詞 ходите/動詞 в/前置詞 черном/形容詞 ?/句読点"というように記します。

「形態素解析」は、トークンに対して品詞、態、性別など形態論的に解析することです。

実際にロシア語テクストを分析してみよう・事前準備編

プログラミング言語にはPythonを使います。

Pythonについては、Macをお使いの場合最初からインストールされていますし、Windowsであればこちらから簡単にインストールできます。

Windowsの場合はPython3.5と書いてあるものをインストールしましょう。

今回の分析にはPython上にさらに2つのソフトウェアをインストールします。

  • nltk 自然言語処理に必要な機能を網羅的に提供してくれます
  • pymorphy2 ロシア語の形態素解析・レマ化をしてくれます

nltkのインストール方法に関しては上述の『入門 自然言語処理』に丁寧な説明がありますが、

どちらもPythonの"pip"というものを使うと簡単にインストールすることができます。

pipはPythonで作られたソフトウェアのインストールを簡単にしてくれるものです。

Macの場合はターミナルを開いて以下のコマンドを実行します。

sudo pip install nltk pymorphy2

Windowsの場合はコマンドプロンプトから以下のコマンドを実行するだけです。

C:¥Python35¥Scripts¥pip.exe install nltk pymophy2

事前準備はこれで終わりです。

実際にロシア語テクストを分析してみよう・実践編

それではいよいよ「かもめ」の使用語彙を分析してみましょう。

まずPythonを起動します。

Python 3.5.0 (default, Sep 23 2015, 04:42:00)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.0.72)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

インターネットからテキストを取得する

ダウンロードに必要なプログラムをロードします。

>>> import urllib.request

テキストをダウンロードします。
テキストはこちらのものを使用します。

>>> downloaddata = urllib.request.urlopen("http://www.lib.ru/LITRA/CHEHOW/chajka.txt")
>>> text = downloaddata.read().decode("koi8-r")

ダウンロードしたテキストを整形する

今回の分析では句読点が不要なので削除します。
replaceを使ってスペースに置き換えています。

>>> text = text.replace(".", " ")
>>> text = text.replace(",", " ")
>>> text = text.replace("?", " ")
>>> text = text.replace("!", " ")
>>> text = text.replace("(", " ")
>>> text = text.replace(")", " ")
>>> text = text.replace("\"", " ")
>>> text = text.replace("'", " ")

ダウンロードしたデータを一行ずつに分解します。
splitを使ってテキストを改行で区切ります。
lenでテキストの行数が分かります。大体3900行ぐらいであれば正しく分解できています。

>>> text = text.split("¥n")
>>> len(text)
3913

39行目までは不要な行なのでカットします。
len(text)で39行減っていることが確認できます。

>>> text = text[39:]
>>> len(text)
3874

1行目はtext[0]で確認できます。見たい行番号 - 1 であることに注意してください。
以下のように本文の一部が表示されたらOKです。

>>> print(text[0])
                            ДЕЙСТВИЕ ПЕРВОЕ

1行をトークンに分割してみる

テキストの1行目をline1に保存します。
line1.split()で1行目をトークンに分割できます。
結果がトークンのリストになっていることが確認できます。

>>> line1 = text[0]
>>> l1_tokens = line1.split()
>>> print(l1_tokens)
['ДЕЙСТВИЕ', 'ПЕРВОЕ']

トークンを集計してみる

テキストからトークンを取り出せるようになったので、実際に集計してみましょう。

まずはテキストをトークンに分割してみましょう。
まず、token_listという空のリストを作って、
テキストをfor文を使って1行ずつトークンに分割し、extendtoken_listに加えていきます。
きちんと分割されているようですね。

>>> token_list = []
>>> for line in text:
...      tokens = line.split()
...      token_list.extend(tokens)
>>> token_list
...
'не',
'любит',
'любит',
'-',
'не',
'любит',
'любит',
'-',
'не',
'любит',
'Смеется',
'Видишь',
'моя',
'мать',
'меня',
'не',
'любит',
'Еще',
'бы',
...]

集計のためにnltkのFreqDistというプログラムをロードします。

>>> from nltk import FreqDist

分割したトークンをFreqDistの中に入れて集計してもらいます。

>>> freqdist = FreqDist(token_list)

集計結果を見てみましょう。
most_commonで見ることができます。上位20位まで表示してみましょう。

>>> freqdist.most_common(20)
[('и', 854),
 ('в', 624),
 ('не', 611),
 ('я', 520),
 ('на', 338),
 ('Я', 274),
 ('что', 263),
 ('с', 254),
 ('Маша', 218),
 ('как', 207),
 ('все', 204),
 ('а', 193),
 ('меня', 180),
 ('мне', 171),
 ('у', 169),
 ('Ирина', 162),
 ('вы', 150),
 ('бы', 147),
 ('-', 146),
 ('то', 134)]

集計できました!
確かによく使われそうな語が上位にきていますね。しかし、"я", "Я", "меня", "мне"が別々にカウントされているようですね。ここまではエクセルでも出来たのでした。
次にいよいよレマ化をしていきます。

トークンをレマ化する

まずは例として先ほどのトークンリストにあった"меня"をレマ化してみましょう。

この"меня"(訳:私を)は代名詞"я"(訳:私)が曲用したものですね。これを"я"に変換しましょう。

pymorphy2を使用するのでロードします。このanalyzerが、ロシア語のトークンをレマ化してくれるものです。

>>> import pymorphy2
>>> analyzer = pymorphy2.MorphAnalyzer()

レマ化します。parseでレマ化することができます。可能性の高い順からリストをくれるので[0]で先頭だけ取り出します。normal_formに実際のレマが入っています。

>>> analyzer.parse("меня")[0].normal_form
'я'

うまくできたようですね。ではすべてのトークンをレマ化してもう一度集計してみましょう。
appendを使ってlemmaを一つずつリストに入れています。

>>> lemma_list = []
>>> for token in token_list:
...     lemma = analyzer.parse(token)[0].normal_form
...     lemma_list.append(lemma)
>>> lemma_list
'не',
'любить',
'любить',
'-',
'не',
'любить',
'любить',
'-',
'не',
'любить',
'смеяться',
'видеть',
'мой',
'мать',
'я',
'не',
'любить',
'ещё',
'бы',
...]

先ほどのトークンリストを比べてみると確かにレマしかなくなっていることが確認できます。

token_list lemma_list
не не
любит любить
любит любить
- -
не не
любит любить
любит любить
- -
не не
любит любить
Смеется смеяться
Видишь видеть
моя мой
мать мать
меня я
не не
любит любить
Еще ещё
бы бы

集計してみましょう。

>>> freqdist_lemma = FreqDist(lemma_list)
>>> freqdist_lemma.most_common(20)
[('я', 1229),
 ('и', 952),
 ('не', 729),
 ('в', 725),
 ('вы', 473),
 ('что', 380),
 ('на', 357),
 ('быть', 334),
 ('он', 332),
 ('а', 313),
 ('с', 304),
 ('весь', 304),
 ('это', 256),
 ('она', 253),
 ('как', 250),
 ('маша', 240),
 ('у', 229),
 ('ты', 220),
 ('мой', 217),
 ('ирина', 195)]

先ほどの"метя", "мне"がすべて集約されて"я"が1位になりましたね!

おわりに

以上簡単でしたが1作品の使用語彙をレマ化して集計するところまで紹介しました。
意外と簡単にできるなと思っていただけたらうれしいです。
NLTKにはまだまだ色々な機能があるので、ほとんどプログラムを書かなくても面白い分析ができます。
ロシア語関連のソフトウェアもたくさんあるので色々触ってみて新しい発見に繋げていただけたら幸いです。
最後まで読んでいただきありがとうございました。
興味はもったが分からない!ということがありましたらお気軽にコメントください。

紹介しきれなかった有用なツールが他にもいくつかあるので最後に簡単に紹介します。

その他ロシア語自然言語処理に有用なツールの紹介

形態素解析・レマ化ツール

Mystem

Mystemはロシアで最大の検索エンジンを作っているYandex製の形態素解析・レマ化ツールです。

商用利用は禁止されており、教育・学術目的のみ利用が許可されています。

Pythonから使うこともできます。

digsolab/pymystem3

TreeTagger

TreeTaggerはロシア語に限らず形態素解析に使われているツールで、日本語でも多くの紹介記事が見つけられます。

商用利用は禁止されています。

インストール方法はやや面倒ですが、こちらに従って丁寧に進めるとインストールできます。

ロシア語にも対応させることができて、

後述のRussian National Corpusで訓練されたパラメータファイルをリーズ大学のSerge Sharoffさんが配布してくれています。

russian.par.gz

Pythonから使うこともできます。

TreeTaggerWrapper

コーパスなど

Национальный корпус русского языка(Russian National Corpus)

コーパスとは、言語データを大量に収集・整理したもので、

収集された生のテキストには、形態情報・意味情報などのメタデータが付与されます。

大量のメタデータを利用して言語を統計的に扱うことを可能にします。

Национальный корпус русского языка(以下、НКРЯ)は現在のところロシア語の唯一にして最大のもっとも権威のあるコーパスです。

1.5億語程度の規模だそうです。

大部分が文語で構成されていて、わずかに口語も含まれています。

プログラム+手動で丁寧にタグ付けされています。

ブラウザからの検索インターフェイスも用意されていて、

形態情報、意味情報など詳細な条件で検索することができます。

商用利用は禁止されており、利用は科学・教育分野に限られています。

また、コーパスの生のデータを取得するには別途許可が必要ですので、

こちらをご覧ください。

OpenCorpora

OpenCorporaは、利用者同士が協力して構築しているロシア語コーパスです。

上サイトからコーパスの生のデータをダウンロードすることができます。

商用利用も可能です。

この記事で使用したpymorphy2はこのコーパスを元に形態素解析・レマ化を行っています。

ロシア語版青空文庫

日本語の青空文庫にあたるものは多数あります。

一番上のБиблиотека Максима Мошкова が一番有名だと思います。

このエントリーは KLab Advent Calendar 2015 の12/23 の記事です。

VoQn です。KLab で現行は主に内製のUnityライブラリ開発をやってます。

今回は特にソフトウェアな話というより、ピープルウェアの話をします。

問題「やっていく気持ち」

サービスでもツールでも、当然 「必要がある」 から、あるいは 「価値がある」 から開発に着手するわけですけれど、
その意義を信じきれるかどうかは個人の心象に依ります。

または、当初は意義のあるミッションを担っていると思っていたとしても、開発が難航したりある程度の目処がついたタイミングで、

  • 「本当にすべきことをしているんだろうか」
  • 「他にすべきことがあるような気がする」
  • 「完成させたところで、そこまで価値のあるモノを出してない気がする」

と、余計な雑念に囚われて生産性が下がったり、完了が遠のいたりしてしまう事があります。

これは最初にどれだけ意欲が高かろうと出会う問題で、当人の意思の問題であろうと、結果的には成果そのものに影響しますし、

「君たち、最近モチベーションが低いぞ。もっと熱意を持って!」 と叱咤激励して解決するものではありません。

対策の方針「(意識が低かろうと)やっていけるようにする」

この記事は、過去に自身が経験した 「やっていく」 プラクティスを紹介しつつ、自戒として、「来年も一年頑張るぞい…」と奮起するような内容です。

[見積もり編] 1.「必要になる工数」より「成果を確認する期日」

無論、無理な締め切りを設けても良くないのですが、全ての課題で正確な作業量が見積もれる事は稀で、プロジェクトの進捗を大きく狂わせないように 「ピボットするための期日」 を予め明確に設けた方が結果的にマシな成果に落ち着きます。

最初は「どんなに軽くても1スプリント(2週間=10日)を最短として提示する」のがコツです。10人日という工数でなく、2週間後に成果物をチェックする、という約束がポイントになります

1営業日は1人日ではない

1 「急ぎでなくていいから」と言われて迷ったとき

こう返しましょう。 「リリースできないと困る時はいつまで?」

2 「要求がデカすぎる」としか思えないとき

「欲を挙げたら一ヶ月も二ヶ月もかかるぞ」 としか思えないトピックに対しては 「2週間待ってくれ、プロトタイプをでっちあげてみせるから」 と一旦そこで合意をとって、その約束は守った上で計画の修正を約束してもらうようにします。

[見積もり編] 2. 「優先度」と「難しさ」と「煩わしさ」は別々に

プランニングポーカーなどで、長期計画のタスクの優先度や難易度をチームで重み付けするにあたって、 『だるさ』(億劫度) は必ず事前に予測するようにします。

実際の業務でも 「技術的な難しさ自体は簡単な方だけれど… とにかく、着手するのは億劫だ」 と感じるモノゴトはあります

  • 技術的 『妥協点』 を求められること
    • 既存の運用との折衝や合意形成を必要とすること
    • 必要な開発コストに対して、求められる期日に明らかな無理がある
    • 要件がそもそも 「夢のような技術」 を求めていること
      • 再度 実現可能な要件に分解する交渉が要る
  • 自動化されてない、膨大で退屈な作業を要するもの
    • OSのバージョン毎の挙動確認を要するコト
    • Webブラウザ毎の挙動確認を要するコト
    • ... etc

スプレッドシートなどに課題をまとめる時、こんな風にします

みっしょん 優先度 難易度 億劫度 備考
複数の端末解像度に対応したUIレイアウトの仕組みを作る 4 2 5 実機テスト必須(iOS 3機種、Android 3機種)
多言語対応でUIアセット切り替えする仕組みを作る 3 3 5 検証用素材を日中韓英ぶん用意せな…

重要なのは、この「煩わしさ」は、「そのタスクを完了した際に、どれだけ体力と意欲を使い果たすか」 の指標であることです。

「技術的には難しくないから、経験の浅い人にまとめてやってもらおう」と、そうした「だるいタスク」を集中させてしまうと、あっという間に人員が疲弊して 最悪離職します

恥ずかしながらこういう経験があります。

  • 「だるさ見積り」した => 予測工数の -5%〜+5% の前倒しor遅延 で済んだ
  • 「だるさ見積り」しなかった => +20%〜40% も遅延した。
    • 終わった後の生産性の低さも本当にもう酷かった。
    • ごめんなさい。。。。

やろう!『だるさ』見積り!本当に大事だよ!

[見積もり編] 3. OKR を意識したバックログ

具体的には Github の issue サマリを記載していく事柄で実践します

  • Objectives : この PullRequest で何ができてほしいのか
    • サマリの見出しに
  • KeyResults : この一連のコミットで具体的に何が変わるのか
    • 箇条書きで

github_issue_sample

Github Issue のフォーマット

また、自分がPullRequestで書く際に意識しているフォーマットは以下の通りです

  • [必須] 目的(どういうことにしたいのか)
  • [必須] TODO(何をするか、したか)
  • [推奨] 留意事項(前方互換性の有無、利用上の注意点、仕様変更)
  • [推奨] レビューポイント(どこを重点的にチェックしてほしいか)
  • [あれば] 検討事項(討議して決めてしまいたいこと)

issue_discussion

[やってく編] 1. ポモドーロテクニック

意識の高い時はこういうシートをコレクト社の 情報カード セクション 5×3 に書いてから退社していました

- [ ] 開発タスクA (8)
  - [ ] モジュールa のテスト追加 (ワークフローHoge と Huga の異常系が不足) (2)
  - [ ] モジュールa のHoge Huga Fooの実装改修 (2)
  - [ ] モジュールb のリファクタリング (2)
  - [ ] モジュールc のインターフェースのDocコメント (2)
- [ ] 頼まれてたPR #XX のコードレビュー (2)
- [ ] 定例の報告書く (1)
- [ ] 休む日の勤怠連絡出す (1)
- [ ] 勉強会資料の草稿書く (2)

丸括弧の中の数字は「そのタスクに今日かけるポモドーロ単位(25分)」を見積りで出します

ポイントは 仕事始めでなく、仕事の終わり に書く癖をつけることで、次の日に出社した時に始めるべき事をちゃんと積み残せる分、ダラダラ深夜まで会社に残るのを防げました

(逆に言えば、こういう習慣を辞めた途端にgdgdになって勤怠が大いに狂ったままになった)

[やってく編] 2. こまめに成果をデモとして見せていく

その時に着手しているコトに対して、一番モチベーションを高く保ちつつも、開発そのものを楽しめるのに一番効果があるやり方でした。

  • 全ての機能が未完了でも、一つでも何か出来たら周りに見せる!
  • 実はバグだらけでも見せる!
  • 普段全然カラミがない人にまで見せて回る!
  • そこでクリティカルなフィードバックを得たり、予想してなかった要件を掘り当てられたりする

プロトタイプやデモアプリケーションや、あるいはスクリーンショットでもいいので、 「今しがた、こんなのが出来た!」 と見せびらかすのは大きな意味がありました。

[やってく編] 3. 未来の自分へありったけ報告

躍起になって開発している最中は自身が抱えてる技術的課題も、それに関する知識も新鮮に記憶していますが、喉元すぎれば熱さ忘れるもので。
開発に関して調べた記事のURLや、読んだ本、キーワードは出来るだけ書き残して、次に同種の悩みにブチ当たった同僚にさっと助け舟が出せるようにしておきます

Git のコミットログに悩み事も綴る

明確に振り返ることが出来ていたプロジェクトでは以下のようなコミットログを意識していました

#${issue_id} ${コミットサマリ}
-- ${何を削ったか}
++ ${何を追加したか、変えたか}
* ${レビューなどでツッコミ入れて欲しいこととか、判断に迷ってる事柄}
-> ${↑ に対して、自分なりに「〜とかでいいのでは」と思っている解決方法}

コードレビュワーに「この件を念頭においてレビューしてほしいんだ」と伝えるための備忘録として使います。

あと、こういう風にコミットログを書けるような程度にコミット粒度を細かくするというのも意識していけます。特に Unity プロジェクトはうっかりすると土石流のようなdiffを産んでしまいがちなので。

定期報告は「手短な項目と冗長な説明」

報告の仕方にもよりますが、定例のミーティングの報告が書面で共有されるのなら、報告は細やかにした方が振り返り時に価値のある資料になります

# とあるタスクについての進捗報告
## Done: 完了させたコト
- (ざっとした報告の概要)
  - (グッダグダした言い訳)
## Doing: 未完了&着手中
- (甘い現状認識)
  - (ダラッダラした言い訳)
## ToDo: 積みタスク
- (雑な課題意識)
  − (意識の低さの言い訳)

グダグダした報告を慎んで、寡黙に「結果を優先してさっさと出そう」としたところ、そっちの方が却って進捗が遅れました。

プロジェクトオーナーからしたら、計画のズレに対し「なぜ?」「どうして?」 が材料として多く持てなければ、 「えっと… 急いで?」と圧(あつ)をかけていく しかなくなるわけで。

「沈黙は金、雄弁は銀」とは言いますが、チームワークにおいて黙ったまま進捗遅れてたら世話ないですし、何より技術的問題を共有しなければ協力し合えないので、手持ちの情報は洗いざらい出しきるつもりでいった方がいいです。

[振り返り編] 1 見積りと実績の差分を計算する

ポモドーロ・テクニックで作業量を記録してあると楽ですし、実態も正確に測れます。
これは、タスクが終わった直後にする方がいいです。

次回に同種の開発項目にあたる際の見積りの精度をより高められれば、未来の自分が苦労しなくて済みます。

数週間すぎてからだと、数字も曖昧になりますし、総括も対策も的確なものでなくなってしまいます。

[振り返り編] 2「学びがあった」ことは広く共有する

成果物以外に 「この開発を経て、何を得たか」 を人に語るのはモチベーションを維持するのにも、自身の成長にも効果があります。

リリースノート以外に、社内のメーリングリストに 「こういうものを作ったよ!こういう風に使えるよ」 と宣伝もかけて流したりしましたが、
それらの技術的なノウハウは(しなかったトピックよりも)忘れずにいられています。

振り返り時に報告するのもいいし、勉強会で発表したり、ブログに書くとかでもいいでしょう。
この記事自体も、そういうことをこまめにやらずに、ひどい下半期を過ごしたことへの反省の意味も込めて書いています。

[各自やっていく編] これらのプラクティスを強要しない

これらは実際にやってみて、確実に効果を実感するものばかりでしたが、その意義を共感させる間もなくチームメイトに啓蒙するのは悪手であるとも感じています。

なぜそうした習慣をはじめるに至ったのか? を省みることもなく、闇雲に強制してしまっては、それが果たしたい目的から逸脱し、ただ漫然と日々の作業を面倒な儀礼として潰してしまうだけです。

これらの習慣は、あくまで 「自分は〜において失態やらかして、そこで導入してみたら効いた」 こととして紹介するに留めています。

むしろ、もっとより簡素で手軽な方法を各自で編み出していいわけですし、そうした小さな事柄でもノウハウとして交換しあえる環境と文化が耕されることが大事かなと思います。

まとめ

各自やっていきましょう

p.s. パズルワンダーランド を応援よろしくおねがいします!

本題には関係ないのですが、最近リリースされた パズルワンダーランド をどうか何卒よろしく応援お願いします!!!

このエントリーは、KLab Advent Calendar 2015 の12/13の記事です。
KLabGames事業部のyasui-sです。よろしくお願いします。

目次

はじめに

最近、サーバとクライアントが常時接続状態で多数の端末を同期させるゲームが増えてきています。
その中で一番の核であり、キモである同時という部分についての考え方を書こうと思います。
蛇足ですが、今回の説明の内容は、要件によって必要度合いが大きく異なります。
要件によっては基本形で十分であることもありますので適時ご判断ください。
本稿ではわかりやすさを重視し、MMOを題材として解説を行い、各通信時間は1秒単位でかかると設定しています。
また、サーバを含めた各端末上での時刻はすべて統一されているとします。

登場端末

  • サーバ
    • 当たり判定ロジック等々殆どの判定を行っているゲームサーバ
      今回の説明ではコマンドの仲介程度しかやっていない
  • クライアントA
    • コマンドの実行者
      ここで言うコマンドとは、移動や攻撃などありとあらゆる行動のことを言う
  • クライアントB
    • クライアントAを見ている第三者
      主にこの端末と先述のクライアントAとで「同時」というものについて考える
    • もちろん、このクライアントがクライアントAの立場になることもあります
    • ちなみにクライアントCなど他の端末が増えても考え方は同じなのでそれについては端折っています

一番の基本形

次のフローをご覧ください。

case1

これは、単純にコマンドの実行を送るフローです。
誰しもが一番初めに作成する形ではないでしょうか。

今回仮定したようなクライアントAからサーバには1秒、サーバからクライアントBには2秒かかるとした場合、
クライアントB上で実行されるのが3秒遅延してしまうということになります。
この3秒が致命的である場合もあり、この3秒をどのようにごまかすかが重要になってきます。

メリット

  • 構造が単純でわかりやすい
  • コーディングが最も簡単

デメリット

  • クライアントAとサーバで実行されるタイミングがずれる
  • クライアントAとクライアントBで実行されるタイミングが大きくずれる

サーバがコマンドを受け取ったことを返した後、実行する

次のフローをご覧ください。

case2

クライアントから送られたコマンドをサーバが全クライアントへ送信しています。

しかし、サーバからクライアントBへの通信時間がサーバからクライアントAへの通信時間より長いため、
結果的にコマンドの実行が1秒ずれてしまっています。
一番の基本形と比べ、クライアントAとクライアントBとのずれが小さくはなっていますが、
それでも要件として満たさない場合も多いはずです。

また、この方法だと致命的な問題があります。
それは、ユーザの操作に対するレスポンスが遅いということです。
上記フローによると、ユーザの入力からコマンドの反映まで2秒かかってしまっており、
その2秒がユーザにとってストレスになってしまう可能性が大いにあります

メリット

  • 構造が単純でわかりやすい
  • コーディングが簡単
  • サーバ上では完全に同期されているため、サーバでの処理が書きやすい

デメリット

  • ユーザレスポンスが悪い
  • サーバと各クライアントで実行されるタイミングが多少ずれる
  • クライアントAとクライアントBで実行されるタイミングが多少ずれる

方向性:実行時刻を送る

ここでコマンドだけの送信・仲介方法を考えることを諦め、
何らかのデータを付け加えて、このずれをどうにかするという方向へ転換してみたいと思います。
今回は実行時刻を加えてみた場合について考えてみます。
しかし、この時刻がくせ者で、サーバを含めたどの端末のどの時点の時刻かによって状況が変わってくるでしょう。
では、それぞれのケースについて詳しく考えてみましょう。

サーバがコマンドの受け取り時刻を加えて返した後、実行する

次のフローをご覧ください。

case3

サーバがコマンドの実行を受け取った際に、サーバ基準の時刻を付け加えて全クライアントへ送信しています。
そして、受け取ったクライアントではそのサーバ基準の時刻でコマンドが実行されたものとして実行します。

  • ここで言うサーバ基準(過去)の時刻に実行されたものとして扱うという言葉は、
    モーションの途中再生を行うなどの表示上のものを指しています。
    これは見た目上不整合が起こることを意味していますが、自分のキャラクタはともかく、
    そもそも他の人のキャラクタをそこまで凝視することは少ないのではないでしょうか。
    そう考えるとこの補間処理は他の人のキャラクタに限定すると問題ないと私は考えています。

これなら各クライアント上で実行開始された(とされる)時刻が同じであるため、
各端末上でのずれはそれほど発生しないでしょう。
しかし、クライアントA上では依然としてユーザの入力からの遅延が発生しており、ストレスが発生する可能性をはらんでいます。

メリット

  • サーバでコマンドを受け取った時間を正としているため、実行タイミングの改ざんを行いにくい
  • 各端末間でずれが生じにくい(ただし、多少のラグは発生する)
  • サーバ上では完全に同期されているため、サーバでの処理が書きやすい

デメリット

  • 時刻を同期する必要がある
  • ユーザレスポンスが若干悪い
    • 時刻を返さない場合と比べると、途中から再生される分ユーザレスポンスが多少改善はされている
    • 自分の操作キャラクタが動作を途中から行っているため、見た目が悪い

コマンドを実行したクライアント上では即時実行し、サーバがコマンドの受け取り時刻を加えて他のクライアントへ返した後、実行する

次のフローをご覧ください。

case4

クライアントA上ではコマンドの入力後、すぐに実行しています。
これにより先ほど発生していた入力に対する遅延は起こらなくなっています。
また、クライアントB上ではサーバで受け取った時刻を元に実行しており、
(補間処理などは行いつつ)クライアントAとクライアントBとのずれは1秒で収まっています。

多くの要件はこの構成で十分なのではないでしょうか。
しかし、逆を言えば1秒もずれているとも言えます。
タイミングが重要な要件であればこのズレが致命的になる可能性があります。

メリット

  • ユーザレスポンスが良い
  • クライアントAを除く、サーバを含めたすべての端末でずれが生じにくい(ただし、多少のラグは発生する)

デメリット

  • 時刻を同期する必要がある
  • クライアントAとサーバを含めた他の端末とでずれが生じる

コマンドを実行したクライアントでコマンドとクライアント基準の実行時刻を送り、サーバがコマンドを他のクライアントへ返した後実行する

次のフローをご覧ください。

case5

クライアントAでは自分の時刻を元に実行時刻を決め、コマンドと共に送っています。
サーバではそれを仲介するだけにし、クライアントBではクライアントA上の実行時刻を元に補間し、実行します。
その結果、クライアントAとサーバ、クライアントBでは実行開始されたタイミングまで同期できました。
しかし、クライアントの自己申告による実行時間を信用するということは、改ざんに対して弱くなることと同義であるため、
悪意のあるクライアントからの通信に対応する必要が発生します。

メリット

  • ユーザレスポンスが良い
  • 各クライアントとサーバとでずれが生じにくい(ただし、多少のラグは発生する)

デメリット

  • 時刻を同期する必要がある
  • クライアントからの通信改ざんに対して脆弱になる

まとめ

以下に今回挙げた項目についてサマリを作ってみました。
個々の方法について比較する一助になれば幸いです。

方法 メリット デメリット 用途例
基本形 構造が単純
コーディングが簡単
各端末でタイミングがずれる タイミングが重要でないもの
例:チャットの吹き出し
サーバがコマンドを受け取ったことを返した後、実行する 構造が単純
コーディングが簡単
サーバ基準で完全に同期している
各端末でタイミングがずれる
ユーザレスポンスが悪い
タイミングが重要でないもの
サーバ側で何らかの判定を行う必要があるもの
例:アイテムの使用
サーバがコマンドの受け取り時刻を加えて返した後、実行する 各端末でずれが生じにくい
サーバ基準で完全に同期している
実行タイミングの改ざんを行いにくい
ユーザレスポンスが悪い
操作キャラクタを含めた見た目が悪い
サーバ側で何らかの判定を行う必要があるもの
例:コマンド方式の場合のスキルの先行入力
コマンドを実行したクライアント上では即時実行し、
サーバがコマンドの受け取り時刻を加えて他のクライアントへ返した後、実行する
実行端末以外の各端末でずれが生じにくい
ユーザレスポンスが良い
実行端末とそれ以外の端末でずれが生じる サーバ側で何らかの判定を行う必要があるもの
ユーザの操作に対するレスポンスが早いほうがいいもの
例:エモーション
コマンドを実行したクライアントでコマンドとクライアント基準の実行時刻を送り
、サーバがコマンドを他のクライアントへ返した後実行する
各端末でずれが生じにくい
ユーザレスポンスが良い
クライアントからの通信改ざんに対して脆弱になる ユーザの操作に対するレスポンスが早い必要があるもの
通信が改ざんされても影響が少ないか異常値をフィルタできるもの
例:コマンド入力方式でない場合の移動処理

これらの手法はどれも一長一短であり、何度も言いますが要件によって最適な選択肢が違います
また、行う通信によってそれぞれの手法を選択するということも有力な候補になるでしょう。
(例えば、移動はクライアントから実行時刻を決めて、攻撃はサーバが実行時刻を決めるなど)

更に今回は割愛しましたが、攻撃対象が死亡しているなど、コマンドが実行できないとサーバが判断した際に
どのようにキャンセルを行うかということも含めて考えていく必要があります。
これらの手法を元に、更に効率のよい方法を日々模索していきたいと思います。

予告

明日は hhatto さんです。お楽しみに。

↑このページのトップヘ