KLabGames Tech Blog

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

カテゴリ: Unity

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

22番手も緊張しますね。KLabGamesの基盤エンジニアのkenseiです。よろしくお願いします。

プロローグ

ある日突然事は起こります。それまで順調にビルドされていたjenkinsのandroidビルドが突然のエラー。ログを見るとstderrの項目に

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

の文字が。そう、Androidはファイル内でコードによって呼び出すことができる参照の総数で65535を超える事ができないのです。

64K を超えるメソッドを使用するアプリの設定

いつかは戦わないといけないのですが、いざその日が来ると憂鬱ですね。ちなみに宗教上の理由でmulti dexはご法度です。

まずは計測

dexのメソッド数を計測してみます。ありがたいことにツールが公開されているので、それを使用します。

dex-method-counts

$ ./gradlew assemble
$ ./dex-method-counts path/to/target.apk > method-count.txt

吐かれたログを見ると

.
.
android: 16833
com: 19806
  facebook: 3263
  google: 14115
    android: 13911
    gms: 13911 <= gpgs
.
.

androidはしょうがないとして、うむ、、GPGSでかいな。。てことで、ここを重点的に潰す事にします。

aarとの戦い

最近のandroid libraryはAAR(Android Archive)で提供されています。ただのzipですが、jarは中に入ってしまってるので解凍しないといけません。という事でみんな大好き、shellの出番です。

本当はgradleでスマートにやりたかったのですが、aarをzipTreeするとpermissionエラーを吐いてgradleが死にました。googleさん、中をゴニョゴニョしたい人の事考えてassembleして下さい。。

準備

unzipするので、Assetsと同じフォルダにディレクトリを作ります。そこにshellを作っていきます。

$ mkdir -p Target/shrink-jar-task && touch shrink-jar.sh && chmod u+x shrink-jar.sh

まずはおまじない

#!/bin/bash

# 変数の準備
WORKSPACE=$(pwd)
WORK_DIR="${WORKSPACE}/tmp"
GRADLE_WORK_DIR="${WORKSPACE}/lib"
PLUGIN_DIR="${WORKSPACE}/../Assets/Plugins/Android"

# 優しさ
while getopts v OPT
do
  case $OPT in
    v ) DEBUG="true"
        ;;
  esac
done

# おしおき
if [ "${JAVA_HOME-undefined}" = "undefined" ]; then
    echo "JAVA_HOME not set"
    echo "alias emacs='vim'" >> .bash_profile
    exit 1
fi
if [ "${ANDROID_HOME-undefined}" = "undefined" ]; then
    echo "ANDROID_HOME not set"
    echo "alias emacs='vim'" >> .bash_profile
    exit 1
fi

# ワーク作る
if [ -d $WORK_DIR ]; then
  rm -rf $WORK_DIR
fi
mkdir $WORK_DIR
if [ -d $GRADLE_WORK_DIR ]; then
  rm -rf $GRADLE_WORK_DIR
fi
mkdir $GRADLE_WORK_DIR

だいたいコメントの通りです。途中でJAVA_HOMEとANDROID_HOMEを設定せずにビルドをしようとする不届き者にemacsを使えなくするお仕置きをしています。vimを使ってればお仕置きされずに済んだのに。。

準備その2

shellでやっていますが、一部の処理はgradleを使用するのでhomebrew等でインストールを済ませておいて下さい。あ、言うの遅くなりましたが、動作はmacでしか確認していません。

上記で用意したフォルダにbuild.gradleファイルを作成し、下記の内容を設定します。

$ vim build.gradle
task wrapper(type: Wrapper) {
    gradleVersion = '2.14.1'
}

設定後にbuild.gradleファイルのある場所で

$ gradle wrapper

を実行します。

aarを解凍

Assets/Plugins/Androidの下にあるaarを解凍していきます。

echo "unziping..."
find $PLUGIN_DIR -type f -name "*.aar" -exec basename {} \; | sed -e "s/.aar//" | xargs -I{} unzip ${PLUGIN_DIR}/{}.aar -d ${WORK_DIR}/{} >/dev/null 2>&1
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | xargs -I{} rm -rf ${PLUGIN_DIR}/{}

ちなみにmaxdepthオプション付けてディレクトリをfindすると、rootが入って来てしまうのですが、mindepthを重ねがけするとrootを除外できるのを今回発見しました。他のスマートなやり方あるかもしれないので、もっと綺麗なshellを書けるようになりたい。。

aarを削除

もう使用しないので、aarを消していきます。

echo "remove plugin aar..."
if [ "$DEBUG" == "true" ] ; then
    find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec echo "  "{} \;
fi
find $PLUGIN_DIR -type f -name "*.aar*" -not -name 'appcompat-v7*.aar' -not -name 'support-v4-*.aar' -exec rm {} \;

この時にappcompatとsupportはいったん除きます。理由は後で面倒くさいからです!

classes.jarを取り出す

解凍したaarの中からjarを取り出していきます。

echo "move classes.jar"
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -not -name 'appcompat-v7*' -not -name 'support-v4-*' -exec basename {} \; | xargs -I{} mv ${WORK_DIR}/{}/classes.jar ${GRADLE_WORK_DIR}/{}.jar
find $PLUGIN_DIR -type d -name "firebase*" -exec basename {} \; | xargs -I{} mv ${PLUGIN_DIR}/{}/libs/classes.jar ${GRADLE_WORK_DIR}/{}.jar
if [ "$DEBUG" == "true" ] ; then
  find $GRADLE_WORK_DIR -type f -name "*.jar"  -exec echo "  "{} \;
fi

ファイルが同名なので、元のフォルダ名+"jar"に名前を変更しています。また、AARファイルになっていないAndroidLibraryのjarも持ってきています。今回はfirebaseを指定しています。

classes.jarを一つのjarにまとめる

renameしたlibフォルダのjarファイル達をunzipして一つのjarファイルにまとめてしまいます。build.gradleを編集します。

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'lib', include: ['*.jar'])
}

jar {
    from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    archiveName = 'google.jar'
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.14.1'
}

shellにgradleのjarタスクを実行させます。

echo -n "make aggregation jar"
./gradlew -b build.gradle jar

-b でgradle 設定ファイルを指定しています。

full-gpgs-jar

jd-guiで生成されたjarを確認した所 一個のjarファイルにまとまりました。

proguard ruleを生成

使用していないメソッドをstripするためにproguardをかけていきます。一からproguardのruleファイルを書いていくのは面倒なので、aarの中に入ってるproguard.txtを再利用します。また、アプリ独自の設定を書いていくようにproguard-rules.proファイルを用意します。

$ touch proguard.base

shellにproguard ruleファイルを生成させます。

echo "generate proguard rule"
cat $ANDROID_HOME/tools/proguard/proguard-android.txt > proguard-rules.pro
find $WORK_DIR -type f -name "proguard.txt" | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cat {} >> proguard-rules.pro
cat proguard.base >> proguard-rules.pro

ちなみにデフォルトのproguard-android.txtもまとめて1ファイルにしてしまってます。gradleのproguardタスクでgetDefaultProguardFile関数が使えなかったので。。

jarファイルダイエット

proguardを使用して、未使用のメソッドをjarから抹殺します。androidのpluginとjavaのpuluginでタスクがかち合うので、別名のgradle設定ファイルにします。今回はproguard.gradleとします。

まずはandroidプラグインを使ってビルドを通すためにダミーのAndroidManifest.xmlを作成します。

$ mkdir -p src/main && vim src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.klab.tekitou" >
    <uses-sdk android:minSdkVersion="7" />
</manifest>

次にgradle設定ファイルを作成します。

$ vim proguard.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'com.android.library'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 23
    }
}

task clearJar(type: Delete) {
    delete '../Assets/Plugins/Android/google.jar'
}
task proguard(type: proguard.gradle.ProGuardTask) {
    configuration 'proguard-rules.pro'
    injars 'build/libs/google.jar'
    outjars '../Assets/Plugins/Android/google.jar'
    libraryjars System.getenv("JAVA_HOME") + '/lib/rt.jar'
    libraryjars System.getenv("JAVA_HOME") + '/lib/jce.jar'
    libraryjars System.getenv("ANDROID_HOME") + '/platforms/android-23/android.jar'
    libraryjars System.getenv("ANDROID_HOME") + '/extras/android/m2repository/com/android/support/support-annotations/23.4.0/support-annotations-23.4.0.jar'
    libraryjars 'tmp/appcompat-v7-23.1.1/classes.jar'
    libraryjars 'tmp/support-v4-24.0.0/classes.jar'
    libraryjars '../Assets/Plugins/Android/firebase-common-9.8.0/libs/classes.jar'
}
proguard.dependsOn(clearJar)

injarsにclasses.jarをまとめたgoogle.jar、libraryjarsにjarが依存している物を指定しています。

※適当に必要そうなのを足していったので、結局どれが本当に必要なのか分かってない

appcompatとsupportは最初に作業フォルダに持ってきたけど、excludeしていたjarを使用します。support-annotationsはversionがpathに入っていますが、target sdk versionの中で一番新しいのを適当に設定しています。android.jarはproguard.gradleで指定したcompileSdkVersionを指定しています。

shellにgradleのjarタスクを実行させます。

echo "shurink jar"
./gradlew -b proguard.gradle proguard

ここはビルドに必要なlibraryjars不足によるエラーとの戦いになるので、何回もgradlewコマンドを叩いて通るまで頑張ります。

minimam-gpgs-jar

できあがったミニマムなjarファイルです。

aarの中のresourceを戻す

解凍したaarの中のリソースを戻していきます。Assets/Plugins/Androidフォルダ直下に置いて、packagingをUnityに任せます。

echo "copy unziped android resources"
if [ "$DEBUG" == "true" ] ; then
  find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo "  "${WORK_DIR}/{}
fi
cat <<EOS > project.properties
target=android-14
android.library=true
generated=true
EOS
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp project.properties ${WORK_DIR}/{}/
find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} cp -r ${WORK_DIR}/{} ${PLUGIN_DIR}/{}
if [ "$DEBUG" == "true" ] ; then
  find $WORK_DIR -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | grep -v appcompat-v7 | grep -v support-v4 | xargs -I{} echo "  "${PLUGIN_DIR}/{}
fi

project.propertiesを追加しないとUnityにAndroidResourceとして認識されないので、追加しています。targetはPlayer SettingsのMinimum API Levelを設定すれば大丈夫だと思います。generated=trueとしてるのは、aarのバージョンアップ時にフォルダを消すためのマーキングです。

きちんとAndroidResourceとして認識されているかはUnityAppPath/Temp/StagingArea/android-librariesを見ると確認できます。

proguardファイルの設定

上記で生成したjarファイルはミニマム過ぎて動きません。試しにfirebaseを全部stripから覗いてみます。

$ vim proguard.base
-dontwarn android.support.v4.app.**
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**

-keep public class com.google.firebase.**

ついでにwarningがうるさいので切っています。

  • aarはresourceが未コンパイルなので、R.classがないエラーが出るので。
  • R.$のエラーだけ出るようになるまではdontwarnは設定しない

実行してみると

add-firebase

firebaseが復活しているのが分かります。

ここからはUnityでAndroidビルド&&実機で動かしながら動作確認 => 落ちた所でログを確認して、必要なクラスをproguard.baseに足してshellを流す の作業の繰り返しになります。

エピローグ

vimを使いましょう

明日はTech Blogなのに、泣かせるので評判な@hhattoです。

この記事は、KLab Advent Calendar 2016 の17日目の記事です。はじめまして、morikenです。

昨日はjukey17さんのVRのゲームを作ってみて気をつけたことでした。

2016年はVR元年

さて、2016年は年始から盛んに「今年はVR元年だ」と言われていましたが、確かに一気にVRが普及したように感じます。

家庭用のVR機器がいくつも発売され、またVRを体験できる場所や機会も増えたことにより、VRの魅力を知った方も増えたと思います。

私自身も、VR ZONEでVRを体験してその魅力に取り憑かれ、PlayStation®VRももちろん購入し、日々バーチャル空間でサイリウムを振ったり、家庭教師の仕事をしたりして楽しんでいます。

ところで、面白いゲームをプレイすると、自分でも作ってみたくなるのがゲーム開発者の性です。そこで、VR元年が終わる前に、自分でもVRのゲームを作ってみることにしました。

VRゲームを作る前に

今回は初めてということで、普段から慣れ親しんだUnityで、スマートフォン向けVRゲームを作ることにしました。

ところが、スマホ向けのVRゲームを作る上で、ある問題に直面しました。それは、プレイヤーにどのようにゲームを操作させるか、です。

というのも、スマホVRでは専用のゴーグルにスマホをセットし、それを覗いてコンテンツを楽しみます。そのため、タッチパネルによる操作はほぼ行うことができません。

それゆえ、VRコンテンツの開発者たちは、ゴーグルに磁石を取り付けてスイッチ代わりにしたり、視線でUIを選択できるようにしたり、首を縦横に振る動きを利用したりと、色々工夫をしてきました。しかし、これらの操作方法では、複雑な操作をさせることは難しいです。

そんな時、自宅にむかし購入したBluetoothのキーボードがあることを思い出しました。Bluetoothなので、スマホとも接続できるはずです。キーボードを使うことができれば、プレイヤーに複雑な操作をさせることができます。

そして、キーボードを使ったゲームと言えば、思い浮かぶものは1つしかありません。

そう、タイピングゲームですね。

VRタイピングゲームの作成手順

手順は以下の通りです。

  1. タイピングゲームを作ります。

  2. 1.で作ったものをVRに対応します。

以上の2ステップです。簡単ですね。

タイピングゲームの作成

今回は、フィールドの中心にいるプレイヤーに向かって、周囲から襲いかかってくるモンスターをタイピングによって撃退するという、ベーシック(?)なタイピングゲームを作成しました。

プレイヤーを定点カメラにするのは、VRのデモ作品などによくある形式ですね。この形式は、周囲を 360° 見渡せるためVRと相性が良く、またカメラの移動がないため酔いづらい、というメリットがあります。

リソースにはUnity Technologies製のアセット、Survival Shooterを利用しました。

非VRのタイピングゲーム

VR対応

タイピングゲームがあらかた完成したら、次はVRに対応させます。

VR対応には、Google VR SDK for Unityを使用しました。Googleは以前、Google Cardboard SDKというSDKを配付していましたが、今年の8月に発表されたDaydreamに対応され、リニューアルされたようです。

VR対応はとても簡単で、UnityにインポートしたSDKに含まれるGvrViewerMain.prefabというPrefabをシーンに配置するだけです。これだけで下の画像のように、ゲーム再生時に自動で右目・左目用のカメラを配置し、VRゴーグルを通して見ることで、ゲーム内のオブジェクトが立体的に見えるようになります。

VR対応のデモ

VRでのUIの注意点

さて、VRゲームでは、非VRのゲームとは少しUIを変えなければいけません。

というのも、非VRゲームと同じようにUIを配置しても、UIが適切に表示されないためです。

例えばこのように、ゲーム内テキストと体力ゲージを模したUIを配置します。

オーバーレイのUI

ところが、これを再生すると、下のようにUIが左目、右目の領域にはみ出して表示されてしまいます。

オーバーレイのUI再生時

この問題を解決するため、カメラ画像にオーバーレイさせていたUIを、3D空間上に配置しましょう。

CanvasコンポーネントのRender Modeは、デフォルトではScreen Space - Overlayになっていますが、これをWorld Space に変更します。

Canvasコンポーネントの設定

すると、Canvas以下のUIコンポーネントが通常の3Dオブジェクトと同じようにワールド空間上に配置されるようになり、左目用・右目用の両方のカメラに描画されるようになりました。

VR対応されたUI

完成

こうして、無事にVRタイピングゲームが完成しました。

VR対応されたゲームのGIF

実際にプレイしてみた

さて、実際にプレイする上で、いくつか必要なものがあります。

まずは、Bluetooth接続できるキーボードです。このゲームはタイピングゲームなので、キーボードは必須です。

キーボード

次に、プレイ中はVRゴーグルを手で支えることができません。そのため、頭に固定できるタイプのものを用意する必要があります。

VRゴーグル

またモンスターは360°さまざまな方向から襲いかかってくるため、素早く周囲を見渡せるよう立ってプレイするのが最適です。その際にキーボードを手元に固定できるように、画板のようなものがあると便利です。

画板のようなもの

全て装備するとこのようになります。(本当は某ゲーム機を背負いたかったのですが、怒られそうなので諦めました。)

装備した様子

プレイしている様子です。遊んでいる本人はVRの世界に没入できてとても楽しいのですが、周りからは不審な目で見られるので、人目がないところでプレイするのが良さそうです。

プレイの様子

終わりに

ゲームのクオリティはさておき、VR対応ゲームはこのように簡単に作ることができます。興味を持たれた方はぜひチャレンジしてみてください。

一緒にこれからのVR業界を盛り上げましょう!

KLab Advent Calendar 16日目の記事です

はじめまして、jukey17です
Advent Calendarを書くのは初めてです
よろしくお願いします!

この記事に書いてあること

この記事では、jukey17がVRゲームを初めて作ってみて、気をつけた内容を簡単にまとめています

これからVRのゲームを作ってみようと思っている方の参考に少しでもなってもらえれば幸いです

VRのゲームを作ろうと思ったキッカケ

VR元年と言われた2016年 、個人でも楽しむことができるVR製品が次々に発表・発売されていき「VR製品買いました」「VR楽しい!」「VR凄い」という声が開発仲間の周りから少しずつ聞こえてくるようになりました

私自身全く興味がなかったわけではなく最初は体験ブースなどを見つけたら軽く遊んでみる程度の興味はありました
が、今一歩踏み込めないままでいました

そうこうしているうちについにはゲーム開発とはゆかりのない仕事をしている友人からもVRの話題が聞こえるようになってきてやっと ちょっとこれは実際に作ってみたほうがいいのでは? と思うようになりました

気がついたら今年も残りわずか、 このまま踏み込まないまま1年が終わってしまうのも勿体無い! と思い、VRゴーグルを購入しゲーム開発をはじめました

作ったゲーム 『World Jab VR』

image

正面から飛んでくるジャブ(パンチ)を左右に避けるゲームです
少し昔、TVで話題になっていた某ボクシング3兄弟の父親が行っていた練習の1つをモチーフにしました

このゲームは Google VR SDK for Unity(※以下SDK)を使ってUnityで作りました

ジャイロセンサーを使用して横の傾きを検出し、プレイヤー自身を左右に動かしてジャブを避けます
何回連続で避けられるかを競うゲームとなっています

初めてのVRゲーム開発だったのですが、実際に開発を進めていく中で気をつけたことの中でやってよかったと思った内容がありました

以下にその内容を紹介します

気をつけて良かったこと

先にUnityEditor上で遊べるようにした

私はまず初めに VRのゲームを作ろう! と勢い良くSDKのサンプルシーンを起動し、実機に転送して遊んで見るところからスタートしました

しかし、プロジェクトをビルドして、端末にバイナリを転送し、起動した状態で端末をVRゴーグルに装着して…
というのを 開発中に何度も繰り返すのはとても大変 だとすぐに気づきました

そこでまずはSDKの導入は一旦せず、UnityEditor上のみで動作が確認できるように開発をはじめました

とはいっても、SDKは簡単にゲームのVR対応ができるように GvrViewerMain.prefab をシーン上に配置するだけでVR対応が済むような構造になっているので、この対応は非常に簡単です

初めはUnity上で開発を進めていき、 ある程度遊べるようになってきたら対象のプレハブをひょいとシーンに置けば良いのです

但し、今回作った World Jab VR はゲームの仕様上プレハブの設置とセットでジャイロセンサーの入力機構も実装しなければなりません

ある程度遊べるようになってからも膨大な微調整の時間がかかるのは容易に想像できます

そこで更にもうひと工夫することにしました

入力操作方法を切り替えられるようにした

今回は3つの入力操作方法を切り替えられるようにしました

  • キーボード操作

PC(UnityEditor)想定
※先程のgifアニメーションがPC操作

  • ジャイロセンサー

スマホ(VR)想定

vr_play

  • タッチ操作

スマホ(非VR)想定

touch_play

最後のタッチ操作はキーボードとジャイロセンサーの出し分けを作ったときに「幾らでも新しいパターンが追加できるな」と思い、サクッと作ってみてしまった副産物です

この3つの入力操作方法は実行前に設定を切り替えられるようにしました

これのおかげで下記のような対応がとてもしやすくなりました

  • VRで遊んでいる間に問題が発覚!
    1. UnityEditorで動作を確認と問題を修正の繰り返し
    2. 修正が完了したらVRで再度遊ぶ
  • 新しい機能を追加したい!
    1. UnityEditorで機能の実装と微調整の繰り返し
    2. 実装が終わったらVRで最終チェック

一度VR対応したせいで細かい動作確認が億劫になり、開発スピードが落ちていく…なんてことは防げるはず!
(モチベーション維持は大事です)

今後の展望

UI上からのパラメータ調整・設定切り替え

いわゆるデバッグ機能です
現在はUnityEditor上のインスペクタから設定できるようにしています

しかしこれだと実機で動作確認したときに微調整をするために一旦UnityEditorに戻らなければなりません
大きな問題の修正や新機能の追加などプログラムを書き換えなければならない内容では仕方ないことですが、パラメータの調整やオプションの切り替えだけで済む内容なのであればなるべくUnityEditorに戻りたくありません

VRゴーグルを装着しているときは 視点ポインターで選択 できるようにしないといけないため歯応えがありそうな内容です

追加したい機能のアイデアももう思い浮かんでおりドンドンと実装をしたいところ...
もあるのですが、こういった 何度も繰り返し時間の掛かる部分は積み重なっていくとかなりの時間を消費していて開発スピードを下げる原因になる ため、足回りの部分の整備も怠らずに進めていきたいところです

ジャブを繰り出す側もプレイヤーにしてしまう

現状はAIが繰り出すジャブをひたすら避けるゲームになっていますが、通信機能を使って2人で遊べるように したら見栄えも含めてもっと面白くなりそうです

避ける側はVRゴーグルを装着、ジャブをする側はスマホにフリック操作してジャブを繰り出す

フェイント機能 などがあると駆け引きが増えて盛り上がりそうです

(※操作方法は暫定)

最後に

初めてのVRのゲームを作ってみましたが、どんな内容のゲームをつくるかを考える際に『どんな場所で遊ぶのか』『遊んでいる人は外から見てどのように見えているのか』など、 ゲームの内容の外側のことも気にしたりしながら作ったので予想以上に難しく・そして楽しかったです

今回は開発を始めていく中での気をつけておくと良いことを紹介しましたが、今後はそういった空間を含めたゲームデザインや新デバイスならではの技術に触れるような内容にもチャレンジしたいと思いました

以上です

この記事は KLab Advent Calendar 2016 の14日目の記事で @kakkun61 が担当します。

compile SDK version や build-tools version を Unity アプリで指定するにはどうすればいいの?という話をします。

この記事の内容は OS X 10.11.6 で確認しています。

Android アプリケーションのビルド時に指定するバージョンについて

Android アプリケーションのビルド時に指定するバージョンについては「基本をおさえる!Androidアプリで指定するバージョンについて(compileSdk,buildToolsVersion,....) - Qiita」にまとまっています。簡単に引用・要約します。

Android アプリケーションのビルド時に指定するバージョンは下記7つあります。

  • SDK バージョン
    • コンパイル SDK バージョン
    • 最小 SDK バージョン
    • 最大 SDK バージョン
    • ターゲット SDK バージョン
  • build-tools バージョン
  • Gradle の Android プラグインのバージョン
  • Gradle のバージョン
SDK バージョン
コンパイル SDK バージョン
コンパイル時に使用される Android.jar のバージョンでここにない API はコンパイルできません。build.gradle に記載します。値としては API レベルです。次の3つも同様です。
最小 SDK バージョン
インストールできる OS の下限で、条件付きコンパイル等しない限りこれより古い API は利用できません。build.gradle に記載します。
最大 SDK バージョン
インストールできる OS の上限です。build.gradle に記載します。
ターゲット SDK バージョン
OS がこの値を見て処理をし分けます。例えば UI テーマの変更や実質メニューキーの表示非表示があります。引用元記事にスクリーンショットがあります。build.gradle に記載します。
build-tools バージョン
その名の通り build-tools のバージョンで aaptaidldxjack.jarjill.jar などが含まれます。build.gradle に記載します。
Gradle の Android プラグインのバージョン
プロジェクトの build.gradle に記載します。
Gradle のバージョン
gradle/wrapper/gradle-wrapper.properties にダウンロード元 URL として記載します。

Unity 3D から生成した Android プロジェクトを Gradle でビルドする

Unity 3D を使って直接 APK を生成する場合、コンパイル SDK バージョンと build-tools バージョンを指定することができず、ビルドするコンピューターにインストールされている最新版が使用されます。これでは、ビルドサーバーを複数案件で共用している場合、案件ごとでこれらバージョンを固定してビルドすることができなくなります。なので、Unity 3D から Android プロジェクトを生成し、Gradle でビルドすることを考えます。

ただ、これではまだ問題があります。Unity 3D の生成する Android プロジェクトの形式が ADT 形式なのです。Google による ADT のサポートは終了しています。

これを Gradle でビルドするために Gradle ビルドスクリプトに少し工夫を加えます。

処理の流れ

  1. Unity エディターから Android プロジェクト(ADT 形式)を出力
  2. Gradle でビルドできるよう Gradle 関連ファイルのリンクを作成
  3. Gradle でビルドして APK 生成

ディレクトリー構成

下記のようなディレクトリー構成とします。サンプルリポジトリーはこちら

  • Assets/
    • Editor/
      • BatchBuild.cs
  • ProjectSettings/
  • gradle/
    • gradle/wrapper
    • build.gradle
    • gradlew
  • make-gradle-buildable.bash

BatchBuild.cs

バッチビルドでは Build() 関数を呼び出せば build-adt ディレクトリーに Android プロジェクトが生成されます(処理の流れ 1)。BuildOptions.AcceptExternalModificationsToPlayer を指定することで Android プロジェクトの生成を指定しています。その他のパラメーターは適宜変更してください。

using UnityEngine;
using UnityEditor;
using System.Linq;

public static class BatchBuild
{
    public static void Build()
    {
        EditorApplication.Exit(DoBuild());
    }

    static int DoBuild()
    {
        Debug.Log("Build Start");
        var options = BuildOptions.SymlinkLibraries
                      | BuildOptions.Development
                      | BuildOptions.AllowDebugging
                      | BuildOptions.AcceptExternalModificationsToPlayer; // このオプションでプラットフォームのプロジェクトが生成される

        var scenes = EditorBuildSettings.scenes.Where(s => s.enabled).ToArray();

        PlayerSettings.bundleIdentifier = "com.example.gradletest";
        PlayerSettings.bundleVersion = "1";
        PlayerSettings.productName = "Gradle Test";
        PlayerSettings.Android.bundleVersionCode = 1;
        PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel22;

        var errorMessage = BuildPipeline.BuildPlayer(scenes, "build-adt", BuildTarget.Android, options);
        if (string.IsNullOrEmpty(errorMessage))
        {
            Debug.Log("Build Success");
            return 0;
        }
        Debug.LogError("Build Failure");
        Debug.LogError(errorMessage);
        return 1;
    }

    [MenuItem("File/Export Android Project #%a")]
    public static void MenuBuild()
    {
        DoBuild();
    }
}

make-gradle-buildable.bash

処理の流れ 1 で、build-adt ディレクトリーに Android プロジェクトが生成されるので、Gradle でのビルドができるようにそこにファイルのリンクを置きます(処理の流れ 2)。prod_nameBatchBuild.csPlayerSettings.productName に指定されたものです。

#!/usr/bin/env bash -ex

prod_name=$(ls -1 build-adt)
ln -sFv "$(pwd)/gradle/"* "build-adt/$prod_name"

gradle/build.gradle

処理の流れ 3 のADT 形式のプロジェクトを Gradle でビルドするためのビルドスクリプトです。sourceSets の部分がキモで、各ソースファイルの位置を ADT 形式に合うように指定しています。他の部分は一般的な Android のビルドスクリプト同様に適宜変更してください。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.example.gradletest"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    // Lint エラーが出てもビルドを継続するように指示
    // Unity の生成物が Lint にひっかかるので必須
    lintOptions {
        abortOnError false
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-unity.txt'), 'proguard-rules.pro'
        }
    }

    // ADT 形式のディレクトリー構成に対応するための記述
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.4.0'
}

ビルド手順

以上の準備をすると、下記コマンドでビルドができるようになります。生成された APK は build-adt/$prod_name/build/outputs/apk 以下にあります。./make-gradle-buildable.bash./gradlew build の実行は、Unity の post process build にしてしまうのもありだと思います。

/Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -quit -executeMethod BatchBuild.Build
./make-gradle-buildable.bash
pushd "build-adt/$(ls -1 build-adt)"
  ./gradlew build
popd

こんにちは、makki_dです。 今回はKLabでも利用しているSQLCipherについて、自前ビルドすることでapkファイル内のライブラリサイズを50%(当社比)削減する方法を紹介したいと思います。

暗号化機能付きSQLite、SQLCipher

SQLiteはファイルベースの軽量なRDBで、クライアント(iPhone/Android)内にステージや敵の基本情報(マスタデータ)等を保持するのにとても便利です。 ですが、SQLiteのデータファイルを平文のまま保持していると、改ざんされたり非公開のデータが覗き見られたりしてしまうので、暗号化しておきたいものです。 そんな時には、SQLiteに暗号化機能を加えたSQLCipher(※)がとても手軽で、KLabでも活用しています。

この記事では、Unity製のAndroid向けアプリで、SQLCipherを依存ファイルを少なくシンプルに使うための方法を紹介します。

※ここで扱うSQLCipherは、オープンソースなCommunity Editionを利用しています。

SQLCipherの普通の使い方 (Android)

SQLCipherのサイトにはAndroidへの導入手順のページもありますし、 Community Editionのバイナリパッケージも用意されています。 Unity5以降であれば、aarファイルをAssets/Plugins/Android以下に入れることでライブラリ自体は簡単に導入できます。

しかしこのライブラリはJavaから利用することを前提とするものですので、Javaのクラスに依存していますし、Unityから使う時もJavaの世界を経由して呼び出すことになってしまいます。

さらに、3.4.0まではICU (International Compornents for Unicode) という文字コード変換ライブラリが組み込まれているため、 icudt46.zipという2.2MBのファイルがもれなく含まれていました。 GooglePlayのAPKファイルサイズ制限的にも苦しいので、依存をなくしてしまいたいところです。

ネイティブ共有ライブラリとして使う方法

さて、ここでUnityでSQLiteを使う時を思い出したいと思います。

C#には、DllImport属性を使ってネイティブ共有ライブラリの関数を直接呼び出すことができます。 (MSDN, Mono)

Unityでもこの機能を使って、Plugins以下に配置したlibsqlite3.soの関数を呼びだすことでSQLiteが使えます。 SQLCipherもネイティブ共有ライブラリとしてビルドすれば、同様にDllImportできるはずです。

ネイティブ共有ライブラリのビルド方法

実際にARMv7向けのネイティブ共有ライブラリをビルドしてみたいと思います。 ビルド環境は Ubuntu 14.04、Android NDK r12b を想定してます。

前準備として次のように環境変数を設定します。 コマンド中のパスやAPI Levelは適宜読み替えてください。

export ANDROID_NDK_ROOT=$HOME/Android/android-ndk-r12b
export SYSROOT=$ANDROID_NDK_ROOT/platforms/android-14/arch-arm
export TOOLCHAIN=$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
export PATH=$TOOLCHAIN:$PATH

SQLiteの場合

ここでまたSQLiteを思い出したいと思います。 SQLiteのビルドは、amalgamationされた単一のcソース(sqlite3.c)からコンパイルするのが簡単です。 SQLiteのDownloadsページよりソースを入手した後、 Android NDKを用いて次のようにビルドします。

unzip sqlite-amalgamation-3140100.zip
cd sqlite-amalgamation-3140100
arm-linux-androideabi-gcc --sysroot=$SYSROOT -march=armv7-a -shared sqlite3.c -o sqlite3.so

sqlite3.cは公式サイトからも入手できますが、生のソースコードツリーから生成することもできます。 configureしたあと、make sqlite3.c とするだけです。

unzip sqlite-src-3140100.zip
cd sqlite-src-3140100
./configure
make sqlite3.c

SQLCipherの場合

さて、SQLCipherではどうでしょう。 configureオプションで頑張ってクロスコンパイルすることもできるかもしれませんが、 SQLiteと同じようにamalgamationされたsqlite3.cからコンパイルするのが簡単です。

SQLCipherではsqlite3.c単体では提供されていないので、自分で生成します。 ソースをcloneしてきたら、configureしてmake sqlite3.cです。sqliteと同じですね。

git clone https://github.com/sqlcipher/sqlcipher.git
cd sqlcipher
./configure
make sqlite3.c

続いてNDKでビルドするのですが、その前にOpenSSLも用意する必要があります。 SQLCipherは暗号化にOpenSSLのlibcryptoを利用しているのですが、Androidが持つlibcrypto.soは使えませんので静的リンクします。 (Android6以降のlibcrypto.soはOpenSSLではなくBoringSSLのもので、ドキュメントにもリンクしてはならないと書かれてます。)

同じNDKでlibcrypto.aをビルドします。 (実際には使っていない機能を組み込まないために、このあたりを参考にConfigureオプションを設定します)

git clone -b openssl_1_0_2-stable https://github.com/openssl/openssl.git
cd openssl
./Configure dist
./Configure --cross-compile-prefix=$TOOLCHAIN/arm-linux-androideabi- --sysroot=$SYSROOT android-armv7
make build_libcrypto 

libcrypto.aがビルドされました。いよいよ生成したsqlite3.cからsqlcipherをビルドします。

cd sqlcipher
arm-linux-androideabi-gcc --sysroot=$SYSROOT -march=armv7-a -shared -DSQLITE_HAS_CODEC -I../openssl/include sqlite3.c -o libsqlcipher.so ../openssl/libcrypto.a

できあがったlibsqlcipher.soの使い方

こうしてAndroid用のlibsqlcipher.soを作ることが出来ました。 これは普通のネイティブ共有ライブラリなので、DllImportで直接呼び出すことができます。 Javaやicudt46.zipへの依存もありません。 既存のSQLite向けの実装のDllImportだけ書き換えれば使えるはずです。

またaarファイルでSQLCipherを導入した場合、Javaとのブリッジのためのコードの他、ARMv5向けのバイナリもapkファイルに含まれてしまいます。 Unity本体がARMv5をサポートしていないので、ARMv7とx86のネイティブ共有ライブラリのみに絞ることでapkファイルのサイズを節約できます。 空プロジェクトにライブラリのみ追加して試してみたところ、apkファイルサイズは次のようになりました。

ライブラリ導入方法 apkファイルサイズ ライブラリサイズ(差分)
ライブラリ無し 17.6 MB --
aar ver 3.4.0 24.7 MB 7.1 MB
aar ver 3.5.3 20.0 MB 2.4 MB
自前ビルド 18.8 MB 1.2 MB

この記事ではARMv7のAndroid向けバイナリのビルド方法を掲載しましたが、x86のAndroidやWindows向けのクロスコンパイルも基本的には同じ方法でできるはずです。 ぜひ挑戦してみてください。

↑このページのトップヘ