KLabGames Tech Blog

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

連載目次

はじめに

@tenntennです。

前回の記事では,spriteパッケージといくつかのイベントについて扱いました。前回の記事を読んでいただいた方は,Android上で画像の描画と移動,回転,拡大/縮小を行うコードをGoだけで書けるようになったのではないかと思います。

なお,前回の記事を執筆後にevent/configevent/sizeに変更されました。確かにサイズに関する情報しかconfig.Eventには含まれていなかったので,納得できる名前変更です。

また,GoチームによってGo Mobileの情報が徐々に整理されてきました。GoのWikiに導入の記事が追加され,リポジトリにもgomobile bindを使ったSDKアプリケーションのサンプルが追加されました。

さて,今回は前回扱わなかった,event/touchevent/lifecycleについて扱おうと思います。この記事を読めば,Go Mobileでタッチイベントを使った簡単なゲームなら実装できるようになるでしょう。

この記事は,2015年8月16日時点の情報を基に執筆しております。これからもしばらくは,event/configのような破壊的な変更が加わる可能性は十分ありますので,ご注意ください。なお,この記事を執筆当時の最新のコミットはbb4db21edcf86f8e35d2401be92605acd79c8d10です。そのため,記事で参照するGo Mobileのリポジトリのリンクは,このコミットを基準にしております。また,この記事にあるソースコードの一部はGo Mobileのリポジトリから引用したものです。

タッチイベント

第1回目の記事で動かしてみたexample/basicを覚えていますでしょうか?このサンプルでは,画面をタッチするとタッチした位置に,緑の三角形が描画されていたと思います。

example/basicをAndroidで動かしてみた

Go Mobileでは,画面をタッチするとtouch.Eventが送られてきます。このサンプルでは,送られてきたtouch.Eventをハンドルしてタッチした位置を取得し,その位置に三角形を描画しています。

前回の記事で説明した通り,イベントをハンドルするには,app.AppEventsメソッドで取得されるチャネルからイベントを受信します。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            var sz size.Event
            switch e := app.Filter(e).(type) {
                case size.Event:
                    sz = e
                case touch.Event:
                    fmt.Println(e.X, e.Y)
            }
        }
    })
}

タッチイベントを表すtouch.Eventは以下のように定義されています。

// Event is a touch event.
type Event struct {
    // X and Y are the touch location, in pixels.
    X, Y float32

    // Sequence is the sequence number. The same number is shared by all events
    // in a sequence. A sequence begins with a single TypeBegin, is followed by
    // zero or more TypeMoves, and ends with a single TypeEnd. A Sequence
    // distinguishes concurrent sequences but its value is subsequently reused.
    Sequence Sequence

    // Type is the touch type.
    Type Type
}

XYはご想像の通り、タッチした座標です。これらの値はピクセル単位なので,spriteパッケージで使われるgeom.Pt単位にするには,size.Event.PixelsPerPtを基に計算してやる必要があります。

Sequenceは,一連のタッチイベントに付けられるシーケンス番号です。指を同時に画面にタッチすると、タッチイベントが同時に複数発生します。そのため,シーケンス番号は、それらを区別するために利用します。

Typeには,タッチイベントの種類を表す値が入っています。タッチイベントの種類は,TypeBeginTypeMoveTypeEndの3種類が存在し,以下のように定義されています。

const (
    // TypeBegin is a user first touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_DOWN.
    // On iOS, this is a call to touchesBegan.
    TypeBegin Type = iota

    // TypeMove is a user dragging across the device.
    //
    // A TypeMove is delivered between a TypeBegin and TypeEnd.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_MOVE.
    // On iOS, this is a call to touchesMoved.
    TypeMove

    // TypeEnd is a user no longer touching the device.
    //
    // On Android, this is a AMOTION_EVENT_ACTION_UP.
    // On iOS, this is a call to touchesEnded.
    TypeEnd
)

1本の指を画面に置いてから,離すまでの間に複数のタッチイベントが発生します。それらの一連のタッチイベントは同じシーケンス番号が振られ,指がどの状態にあるかはTypeで取得します。画面に指を置いた瞬間にTypeTypeBeginのタッチイベントが発生し,指を動かす度にTypeTypeMoveのイベントが発生します。画面から指を離すと,TypeTypeEndのイベントが発生し,一連のシーケンスのタッチイベントが終了します。

なお,シーケンス番号は常に一意である訳ではなく,そのアプリの起動中に何度も同じシーケンス番号のイベントが送られてきます。現在の実装では,画面に指を置いた順にシーケンス番号を振られ,指を離すとそのシーケンス番号は開放され、再度別の指を置くと同じシーケンス番号が使いまわされます。つまり,新しくシーケンス番号を振る場合は,現在どの指にも振られていない0以上のもっとも小さい数を振るように実装されています。

タッチイベントは基礎的な情報しか提供していませんが,これらをうまく使うことでドラッグやフリックなどを実装することができます。
Webフロントエンドのタッチイベントのライブラリなどを参考にして、高レベルなタッチイベントを行うライブラリを実装しても面白いでしょう。

ライフサイクル

Go Mobileにおいて,アプリのライフサイクルはlifecycle.Eventで表わされます。Go Mobileでは,複数のプラットフォームを一元的に扱うためOnStartOnStopなどのAndroidのActivityのライフサイクルとは一致していません。

lifecycle.Eventは以下のように定義されています。

// Event is a lifecycle change from an old stage to a new stage.
type Event struct {
    From, To Stage
}

ライフサイクル中のある状態をStageという型で表現し,lifecycle.Eventは,とあるStageから別のStageに移ることを表しています。Stageには,StageDeadStageAliveStageVisibleStageFocusedの4つがあり,以下のように定義されています。

const (
    // StageDead is the zero stage. No lifecycle change crosses this stage,
    // but:
    //  - A positive change from this stage is the very first lifecycle change.
    //  - A negative change to this stage is the very last lifecycle change.
    StageDead Stage = iota

    // StageAlive means that the app is alive.
    //  - A positive cross means that the app has been created.
    //  - A negative cross means that the app is being destroyed.
    // Each cross, either from or to StageDead, will occur only once.
    // On Android, these correspond to onCreate and onDestroy.
    StageAlive

    // StageVisible means that the app window is visible.
    //  - A positive cross means that the app window has become visible.
    //  - A negative cross means that the app window has become invisible.
    // On Android, these correspond to onStart and onStop.
    // On Desktop, an app window can become invisible if e.g. it is minimized,
    // unmapped, or not on a visible workspace.
    StageVisible

    // StageFocused means that the app window has the focus.
    //  - A positive cross means that the app window has gained the focus.
    //  - A negative cross means that the app window has lost the focus.
    // On Android, these correspond to onResume and onFreeze.
    StageFocused
)

ステージはStageDeadから始まり,アプリをサスペンドしたり,レジュームしたりすると変わります。各ステージがAndroidのライフサイクルのどの部分にあたるのかは、上記のStageの定義にコメントとして書いてあります。

表にまとめると以下のようになります。なお,「順方向」や「逆方向」は,そのステージ「へ」遷移した(順方向)か,そのステージ「から」遷移した(逆方向)かを表しています。詳細は後述のCrossesメソッドの説明で行います。

ステージ名 意味 対応するAndroidのライフサイクル
StageDead 初期状態 なし
StageAlive アプリの起動している 正方向:onCreate,逆方向:onDestroy
StageVisible ウィンドウの表示されている 正方向:onStart,逆方向:onStop
StageFocused ウィンドウが選択されている 正方向:onResume,逆方向:onFreeze

執筆当時はMacとAndroidでステージの遷移の仕方が違っていました。
たとえば,Androidではステージは以下の表のように変化します。

操作 ステージの遷移
アプリを起動する StageDead->StageFocused
ホームボタンを押す StageFocused->StageAlive
アプリに戻る StageAlive->StageFocused

一方,Macの場合は以下のように遷移します。

操作 ステージの遷移
アプリを起動する StageDead->StageAlive->StageVisible->StageFocused
別のウィンドウを選択 StageFocused->StageVisible
アプリのウィンドウを選択 StageVisible->StageFocused
アプリのウィンドウを閉じる StageFocused->StageAlive->StageVisible
アプリを終了(ウィンドウを閉じてる場合) StageVisible->StageDead
アプリを終了(ウィンドウを開いている場合) StageFocused->StageDead->StageAlive

このようにAndroidの場合はStageVisibleになることはなく,Go Mobileのソースコードを見てもイベントを発生させてる箇所は見つけられませんでした。

以下の図のように,ステージは基本的にStageDead->StageAlive->StageVisible->StageFocusedの順(定数の小さい順)に遷移することを前提としています。
lifecycle.Event.Crossesメソッドを使うと,引数で渡したステージを順方向(定数の小さい順)に通り過ぎてるか(CrossOn),逆方向(定数の大きい順)に通り過ぎているか(CrossOff)が取得できます。
たとえば,図の左側のようにイベントがlifecycle.Event{From: StageAlive, To: StageFocused}の場合,Crosses(StageVisible)CrossOnを返します。

ライフサイクル

少し分かりづらいですね。Crossesメソッドの実装を見る方が分かりやすいかもしれません。なお,Stage型は単なるuint32のエイリアス型であるため,大小比較ができることに注意しましょう。

// Crosses returns whether the transition from From to To crosses the stage s:
//  - It returns CrossOn if it does, and the lifecycle change is positive.
//  - It returns CrossOff if it does, and the lifecycle change is negative.
//  - Otherwise, it returns CrossNone.
// See the documentation for Stage for more discussion of positive and negative
// crosses.
func (e Event) Crosses(s Stage) Cross {
    switch {
    case e.From < s && e.To >= s:
        return CrossOn
    case e.From >= s && e.To < s:
        return CrossOff
    }
    return CrossNone
}

Crossesメソッドを使うことで,プラットフォームによって,とあるステージに遷移しない場合でも,そのステージを「通過」したかどうかが分かります。

たとえば,以下のようにCrossesをメソッドを呼び出した場合,たとえStageVisibleに遷移してなくても,あたかも遷移したかどうかを判断しているように処理ができます。

func main() {
    app.Main(func(a app.App) {
        for e := range a.Events() {
            switch e := app.Filter(e).(type) {
            case lifecycle.Event:
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    // 画面が表示された場合の処理
                case lifecycle.CrossOff:
                    // 画面が表示されなくなった場合の処理
                }
            }
        }
    })
}

Crossesメソッドは一見無駄なように見えますが,Go Mobileでは,複数のプラットフォームの実装を用意する必要があり,Crossesメソッドは,その違いを吸収することができる,よく考えられたメソッドです。

今回は,タッチイベントとライフサイクルについて説明しました。タッチイベントと前回説明したspriteパッケージを使えば簡単なゲームを作ることができるでしょう。また,ライフサイクルイベントを使えば,アプリがサスペンドされたことを検出して,ゲームをポーズさせたりすることができ,より完成度のゲームを作ることができるようになると思います。

次回は,音の再生と各種センサーを扱う方法について説明する予定です。

※この記事は8/10に書かれたものです。現在では最新のGit2.5のWindows用バイナリも公式に公開されていますので、
 自前ビルドせずともGit2.5が利用できますが、次期バージョンでも同様の手順でビルドできると思われます。

@makki_dです。
普段はLinuxを使っていますが、WindowsでGitをビルドしてみたというお話です。
とても簡単にビルドすることができたので紹介したいと思います。

Git 2.5 を使いたいんです。

先日Git2.5が公開されました。
様々な修正のほか機能も多数追加されましたが、個人的には git worktree コマンドに注目しています。

git worktreeとは

あるブランチで作業をしているときそれを中断して、急ぎで別のブランチの修正をしなければならないことってよくありますよね。
そんな時皆さんはどうしていますか?

  1. 今の作業をとりあえずcommitstashしてブランチを切り替える
  2. もうひとつcloneしてきてそこで作業する
  3. git new-workdirで別ディレクトリに作業ツリーを作る
  4. git worktreeで別ディレクトリに作業ツリーを作る ← New!!

git worktreegit new-workdirと同じく、別ディレクトリに作業ツリーを作ることができるコマンドです。
作業ツリーを別に作ることで、今進めている作業をそのまま置いておきながら、同じリポジトリの別ブランチの作業を別ディレクトリで行うことができます。

具体的な使い方はドキュメントの例がわかりやすいです。

これまでもgit new-workdirがありましたが、このコマンドはシンボリックリンクを作成するため、Windowsの場合一般ユーザ権限では利用できませんでした
一方、git worktreeではシンボリックリンクではなく、ファイル中にパスを記録する形で作業ツリーと親リポジトリの関係が管理されるので、一般ユーザでも利用することができます。

公式のWindows用バイナリは1.9.5 (※8/10時点)

早速 Git 2.5 をWindowsでも使ってみたいところですが、公式のDownloadページには、Windows向けバイナリは1.9.5までしか用意されていません。
そして「新しいバージョンが使いたかったら、ソースからビルドしてね」とさらっと書かれています。

Windows上でOSSのビルド環境を整えるというと多くの場合苦行となるような印象がありますが、Gitの場合は驚くほど簡単に開発環境がセットアップできました。

WindowsでのGitビルド環境

Windows用のバイナリと開発環境は、Git for Windowsで入手できます。
紛らわしいですが、「git for windows」等で検索すると上位に出てくる msysGit は 1.9.5 までしかありません。
特に2系を使いたい場合は、間違えずに後継プロジェクトであるGit for Windowsを見てください。

Git for Windows SDKのダウンロード・インストール

インストーラを実行すると、指定したインストールディレクトリ以下に gcc を始めとした開発ツール・環境・ライブラリ一式がダウンロード・インストールされます。
64bit版SDKのデフォルトのインストールパスは C:\git-sdk-64 なのでそのまま記載しますが、変更した場合は適宜読み替えてください。

インストールパス
ネットワークインストール

インストールが終わるとそのまま Git のビルドが始まりますが、私の環境では gettext 関連のビルドでエラーとなりました。

ビルドエラー

エラーとなったコマンドプロンプトを一旦終了し、msysのシェル (C:\git-sdk-64\mingw64_shell.bat) で改めて開き直します。
gitのソースディレクトリ (/usr/src/git) でmakeを実行すると、先ほどのエラーに引っかからずにビルドできます。

ビルド成功

ビルドできたらmake instalすると C:\git-sdk-64\mingw64\bin に実行ファイル一式がインストールされます。
ここにPATHを通すことで、gitコマンドがコマンドプロンプトやcygwinから使えるようになります。

バージョン2.4.6??

執筆時点のmasterブランチでビルドすると、git のバージョンが2.4.6になっていました。

version

これではgit worktreeが使えない!?と一瞬焦りましたが、よくよくコミットグラフを確認すると、v2.5.0タグのブランチがまるごとmergeされてるため、使いたかったgit worktreeコマンドも使えました。

worktree

ちょっとパッチを当ててみる

git worktreeで作った作業ツリーのサブディレクトリの中でalias登録したgitコマンドを呼び出すと、次のようなエラーが出てしまいます。

makiuchi-d@PC-1034 MINGW64 ~/Projects/test/work1/a (work1)
$ git br
fatal: internal error: work tree has already been set
Current worktree: C:/Users/makiuchi-d/Projects/test/work1
New worktree: C:/Users/makiuchi-d/Projects/test/work1/a

これでは不便なので、エラーを出している部分に即席パッチをあててみようと思います。

diff --git a/environment.c b/environment.c
index fb4eda7..8cf1442 100644
--- a/environment.c
+++ b/environment.c
@@ -226,7 +226,7 @@ void set_git_work_tree(const char *new_work_tree)
 {
        if (git_work_tree_initialized) {
                new_work_tree = real_path(new_work_tree);
-               if (strcmp(new_work_tree, work_tree))
+               if (strncmp(new_work_tree, work_tree, strlen(work_tree)))
                        die("internal error: work tree has already been set\n"
                            "Current worktree: %s\nNew worktree: %s",
                            work_tree, new_work_tree);

※あくまで応急措置です

この修正を加えた上でmakemake installすることで、動くようになりました。

動作確認

まとめ

GitはWindowsでも簡単に開発環境をセットアップできます。
いち早く新機能を試したいときや、ちょっとした修正をしたい場合など、ぜひ試してみてください。

P.S.
8/18、Git 2.5のWndows用バイナリが公式に公開されました。

※この記事ではgitのタグ「v2.75」から生成したブランチ上でコードリーディングしています。

Blender標準のレンダラよりもリアルな絵を作る事の出来る「Cycles」というレンダラ。アーティストの方々はクオリティ・リアリティの高い画像や動画などを作る際に利用されている事でしょう。数あるBlenderの機能の中でも注目度が高いのではないでしょうか?

プログラマである私のBlenderの使い道と言えばメッシュを作ったりボーンを仕込んだりしてゲーム開発の際のテスト用モデルを作る事であり、Cyclesのお世話になる事はなかったのですが、コードリーディングをしてみようと思い立ち1から勉強してみることにしました(なんだか目的と手段が逆な気もしますが ^_^; )。

Cyclesはとても奥深い機能であるし、現在も開発が進んでいます。ですのでこのCyclesの記事は1回では終わりませんし、part何までいくか今のところ分かりません。もしかしたら途中で別の機能についての記事を挟んでまた再開するようなこともあるかもしれません。とにかく頑張ってコードリーディングしていこうと思いますのでお付き合いいただければ幸いです。

”Cyclesとは” と書こうとしたんです!

まずはじめに、「Cyclesとは〜〜だ!」と一言で説明されたものを頭に入れておけば後々理解の助けになりそうです。そこでBlender Reference ManualのCyclesのイントロダクション から引用します。

Cycles is a ray tracing renderer focused on interactivity and ease of use, while still supporting many production features.

要するに肝心なところは「Cyclesとはレイトレーシングレンダラだ!」というところですね!
しかしこの「レイトレーシング」、ちょっと調べてみると指しているものが広すぎてもう少し整理しないことには理解の助けにならない事が分かったのです。

ここから先は上記の「レイトレーシング」が何を指すのかもう少しハッキリさせるべく、歴史的なところから調べた事を書いていますが、それぞれのキーワードについて厳密な定義が存在するのかはわかりません。故に私よりも深く勉強されている方からは指摘されるような部分があるかもしれませんが、このように解釈すればCyclesの理解に役立つだろうという部分に主眼を置いて自分なりに整理してみました。

レイトレーシングの歴史

「レイトレーシング」(便宜上、古典的レイトレーシングと呼ぶ事にします。)が考案されたのは1980年代の初頭だったそうです。古典的レイトレーシングは光源から放出される光が物体に直接及ぼす影響しか考慮されていなかったようです。

上記の英文がさす"ray tracing"とは「古典的レイトレーシング」のことなのでしょうか?

実際にCyclesを使ってみるとそうではなさそうだという事が分かります。
Cycles Rendering
Torus Material
Floor Material
床のオブジェクトにトーラスの色が少し映り込んでいるのが分かります。トーラスと床のマテリアル設定はノードの示す通りで光源として設定されている訳ではなく、Diffuseとなっています。つまりCyclesは光源からの直接の影響だけではなく、他の物体が反射した間接的な光も影響するように出来ているようです。このような現象は「間接照明」と呼ばれ、間接照明まで考慮して物体表面の光の挙動をモデル化したものを「グローバルイルミネーション」と呼ぶそうです。

「古典的レイトレーシング」は「間接照明」を考慮していないので「グローバルイルミネーション」には含まれませんが、1984年頃に「古典的レイトレーシング」を発展させる形で「間接照明」まで考慮されたレイトレーシングが考案されたようです。それが「分散レイトレーシング」と呼ばれるアルゴリズムです。「分散レイトレーシング」では1本のレイが物体に入射すると複数のレイが反射されるようです。

しかしこの「分散レイトレーシング」にはレイの数が膨大になる事やその他の欠点があるようで、これらの問題を解消する形で「パストレーシング」というアルゴリズムが1986年頃に考案されました。

その後、1990年代になると視点からのレイも光源からのレイも両方追跡する「双方向パストレーシング」というアルゴリズムが考案されます。

今後書く記事の中で詳しく触れようと思いますが、Cyclesのレンダリング部分のコードを追いかけてみると、kernel_path_traceという名前の関数にたどり着きます。この関数の名前が「パストレース」「双方向パストレース」のどちらを指すのかは今のところわかりませんので後々探っていく事となりますが、ここまでで「レイトレーシングという言葉の指すものが広すぎてもう少し詳しく見ないとCyclesを理解する上で助けにならない」といった意味がおわかりいただけたかと思います。

長い説明になってしまったので、ここまでの私の見解を大まかに図に表しておきます。
RaytraceHistory

レイトレーシングとラスタライジングの違い

さて、ゲームのように秒間に何十フレームもレンダリングする必要のある場合、その技術はリアルタイムレンダリングというカテゴリに分類されて、多くはラスタライジングという手法が採用されます。ラスタライジングはプリミティブ単位でレンダリングする手法です。

一方、何秒・何分・モノによっては何時間もかけて1フレームをレンダリングする事を繰り返し、予め画像や動画などを仕上げておく技術をプリレンダリングと呼びます。レイトレーシングがリアルタイムレンダリングで採用される例は現時点では少なく、多くはプリレンダリングにおいて使われます。

レイトレーシングはラスタライジングと違い、ピクセル単位でレンダリングされます。その事をCyclesのコード上で確認してみましょう。

GPUを使う設定をしていないのであれば、Cyclesでレンダリングをする際に
Sources/cycles_device/Source Files/device_cpu.cppの

for(int y = tile.y; y < tile.y + tile.h; y++) {
    for(int x = tile.x; x < tile.x + tile.w; x++) {
        path_trace_kernel(&kg, render_buffer, rng_state,
                                     sample, x, y, tile.offset, tile.stride);
    }
}

という部分を通ります。

どうやらタイル状に区切ってピクセル単位でレンダリングしているような雰囲気がありますね。レンダリング結果はこうなります。
Default

tile.hとtile.wを2で割ってみたらどのようにレンダリングされているのかわかりやすい絵が出来そうですね。レンダリング結果はこうです。
After
ご覧の通りタイル状に区切られてピクセル単位でレンダリングされています。プリミティブ単位でレンダリングされるラスタライジングとは違うのがわかります。

参考書

レイトレーシングの歴史を調べるにあたり、こちらの本を参考にさせていただきました。

CG Magic:レンダリング
倉地 紀子
オーム社
2007-11

とても親切に書かれている本ですが、学術的な内容であるだけに私にとって容易なものではありませんでした。もし何か間違った解釈をしたままこの記事の中に書いてしまっている箇所があれば、その責任は私にあります。

@fmystB

↑このページのトップヘ