KLabGames Tech Blog

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

このエントリーは、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 の12/19の記事です。 やまだです。 昨年はWebGLをつかったソートを実装してみましたが今年はWebGLを使った画像圧縮を実装してみます。

はじめに

WebGLのみならずアプリ開発において画像のフォーマットの選択は重要です。 とくにゲームの大部分を占める画像データはゲームのダウンロード時間に直結するため可能な限り圧縮しておきたいものです。

WebGLではGPUにテクスチャをアップロードする際にはgl.texImage2Dを使用します。 gl.texImage2DTexImageSourceをアップロードできます。 このTexImageSourceというのが面白くてテクスチャデータとしてHTMLImageElementが使えます。 すなわちGIF/JPEG/PNGといったフォーマットだけでなくGoogleが開発した高圧縮率フォーマットであるWebPも使用可能です。 さらにはHTMLVideoElementもテクスチャフォーマットとして使用できるため、動画さえもテクスチャにできます。 ダウンロードサイズの削減が目的であればGIF/JPEG/PNG/WEBPから適切なフォーマットを選ぶのがよいでしょう。

一方で今日ではモバイルデバイスもターゲットとなってくるため、 ビデオメモリの容量も気にしたいところで、画素あたりのビット数も削減したいところです。 WebGLにおいてはPNGなどを用いた場合、1画素あたり32bit使用することが多いでしょう。 その場合、1024x1024のテクスチャ1枚当たり4MByteのサイズとなります。 そうなってくると選ばれるフォーマットはETCやPVRTCでしょう。 例えばETC2の8bit/pixelを用いれば1024x1024のテクスチャも1/4のサイズである1MByteまで削減できます。 ですが、これらはWebGLでも使用可能ですがWebGL 1.0ではエクステンションのため使用できる保証がありません。 ETC2はWebGL 2.0で使用可能とのことなので今後はETC2を積極的に使用していきたいところです。 ETCやPNG等を使用するのも手ではありますが前述のとおりいくつかの無視できない課題が存在するため、 今回はこれらとは異なるアプローチで画像圧縮を試みてみました。

YCgCo色空間による圧縮

Unity向けではありますがこちらYCgCo色空間を使用して画像の圧縮を試みている方がいらっしゃいました。 記事で解説されている通りではありますが画像をLuma(明度)とChroma(彩度)に分割し、 人間の目はLumaに比べてChromaの変化に対して鈍感である性質を利用してChromaの情報量を落として圧縮しています。

元画像

lena

Lumaのみの画像

luma_only

Chromaのみの画像

chroma_only

人間の目はLumaのみの画像に対してChromaの変化に鈍感であることが実感できると思います。 このアイディアそのままにWebGLによる再実装を行いました。

色空間を変換するシェーダ

RGBAをYCgCoAに変換する

YCgCoとRGBの色空間は行列による変換が可能です。なのでシェーダとも相性がよくシェーダでも行列を使用して変換を行います。 1つだけ注意しないといけないところはYCgCoのYの値域は[0, 1]に対してCgCoの値域は[-0.5, +0.5]のためテクスチャに保存する際に0.5を加算します。 この加算含めて4x4行列にまとめるとRGBAをYCgCoAに変換するシェーダは以下のように書けます。

元となる実装と同じようにYをAlpha8に割り当て、CgCoAをRGB565のフォーマットに書き込むことを考えて swizzleを使用して(Y,Cg,Co,A) => (A,R,B,G)のように割り当てます。 さらに変換後の画像をさらにRGBとAに分割し、RGB画像の解像度を1/4に縮小します。 結果1画素あたりのビット深度はAlpha8(8bit) + RGB565(16bit) / 4 = 12bitとなります。

precision mediump float;

uniform sampler2D u_map;

varying vec2 v_uv;

void main(void) {
    mat4 m = mat4(
        0.25, -0.25, 0.5, 0.0,
        0.5, 0.5, 0.0, 0.0,
        0.25, -0.25, -0.5, 0.0,
        0.0, 0.5, 0.5, 1.0
    );

    vec4 c = texture2D(u_map, v_uv);
    vec3 ycc = (m * vec4(c.rgb, 1.0)).xyz;
    float alpha = c.a;
    vec4 ycca = vec4(ycc, alpha);

    gl_FragColor = ycca.ywzx;
}

Yのみのアルファ画像

lena_y

縮小したCgCoA画像

lena_cca

YCgCoAをRGBAに変換する

YCgCoAに変換した2枚のテクスチャをシェーダで合成し、RGBAに変換します。 その際にVRAMの節約のためにCgCoA画像はRGB565のテクスチャで、Y画像はAlpha8のテクスチャとしてアップロードします。

// Y画像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, gl.ALPHA, gl.UNSIGNED_BYTE, image);
// CgCoA画像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_SHORT_5_6_5, image);

シェーダで合成する際はCgCoに0.5を加算しているため行列をかけるまえに0.5ひいてやります。

precision mediump float;

uniform sampler2D u_tex0; // CgCoA画像
uniform sampler2D u_tex1; // Y画像

varying vec2 v_uv;

void main(void) {
    mat3 m = mat3(
        1, 1, 1,
        -1, 1, -1,
        1, 0, -1
    );
    vec3 cca = texture2D(u_tex0, v_uv).xyz;
    float y = texture2D(u_tex1, v_uv).w;
    vec4 c = vec4(y, cca.x, cca.z, cca.y) - vec4(0.0, 0.5, 0.5, 0.0);
    gl_FragColor = vec4(m * c.xyz, c.a);
}

合成結果

result

比較

左:元画像、右:合成画像

compare

ほぼ劣化がないように見えます。 念のためちゃんと画像が劣化しているのかを確認するために こちらを参考に差分をとってみます。

$ composite -compose difference lena.png result.png diff.png
$ identify -format "%[mean]" diff.png
428.525

確かに差分が発生しているので画像は劣化してはいるようです。 差分画像を正規化した物がこちらです。

diff_normalized

全体的に差分が発生していることが確認できます。 Chromaのビット数を削減しているので納得できる結果となりました。

最後に

YCgCoによる圧縮は見た目上は効果的であるということはわかりましたが、いくつかの問題点があります。

1つはテクスチャフェッチが2回必要となること。 テクスチャフェッチは重い処理なのでできれば少なくしたいところです。

もう1つはアルファのみを保存する適切なフォーマットが見つからないこと。 Y画像は8bit深度のアルファ画像なのですがPNG画像でアルファ付き画像として保存すると、 32bit深度で保存されてしまい少しもったいないです。

また、原因の特定はできなかったのですがGPUによってはうまく描画できない例も見受けられました。

得られる画像は非常に良いものではあるのですが、現状のWebGLにおいてでは画像の圧縮はダウンロードサイズの削減が目的であればPNG/JPEG/WebPを使用し、 ビデオメモリの節約であればETC2に望みを託す、というのが今はよいかもしれません。

以上です。

この記事は、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業界を盛り上げましょう!

↑このページのトップヘ