20190716のUnityに関する記事は6件です。

MasterMemoryとAirtableでいい感じにマスタを高速に読む

事の発端

きゅぶんずさんがいい感じの記事を書いてるのを見て、Airtableに興味を持ち、いろいろ調べていたらMasterMemoryなるものが出たので、全部一緒に料理してやんよ見たいな感じでUnityでいい感じに使おうと思った記録です。

とりあえず参考までに

きゅぶんずさんの記事
https://qiita.com/kyubuns/items/dc47cf7c9af38f13c9ee

Airtable
https://airtable.com/

Airtableの.NETクライアント
https://github.com/ngocnicholas/airtable.net

↑がJson.NET使ってたので、Utf8Json用に私が作ったもの
https://github.com/yKimisaki/AirtableClient

Utf8Json
https://github.com/neuecc/Utf8Json

起動のたびにAirtableにアクセスすると遅いので、手元にそのデータをバイナリでキャッシュしておく。
そのキャッシュを作ったり、バイナリからテーブルにしてくれる良い物がMasterTable。
https://github.com/Cysharp/MasterMemory

作戦

image.png

↑のようなテーブルを

MasterCache.cs
var appleMaster = MasterCache.TestMaster.FindByID(1);

みたいな形でUnity上から使えるようにする。

TestMasterクラスの作成

自動生成したいけど、いろいろ面倒くさい経験しかないので頑張って書きます。

TestMaster.cs
[MessagePackObject(true)]
[MemoryTable(nameof(MainGameMaster))]
public class TestMaster
{
    [PrimaryKey]
    public int ID { get; set; }

    public string Name { get; set; }

    public int Price { get; set; }
}

注意点としては、PrimaryKey属性がフィールドじゃなくプロパティにしか付けられないので{ get; set; }なりが必須、またUnity用のmpc.exe(MessagePackのリゾルバ生成)はTestMasterのリゾルバは生成してもTestMaster[]、つまりカスタムクラスの配列は自動で生成してくれないので、作っておかないと怒られます。以下で生成されるMasterMemoryResolverを登録しましょう!!!

UnityEditorで必要なものをいい感じに生成する

https://gist.github.com/yKimisaki/9ecc52c87cd7e926423a506bab519de1

image.png

みたいなのを作ってメニューからMessagePackのリゾルバやらAssetBundleやらをそれっぽく生成できるようにしておきます。
できたらMasterMemoryのTablesを生成します。
普通にMasterMemory.Generator.exe自分でたたいてくれてもいいです。

AirTableやバイナリからMasterMemoryに読み込む

https://gist.github.com/yKimisaki/5a3bc7317e4193c38651b8f8ebff1771

みたいな感じで、複数の経路、具体的にはキャッシュのバイナリか、Airtableから直接MasterCacheみたいなメモリ上に展開するクラスに登録できる状態を作っておきましょう。

Airtableからバイナリを作成

あとは、だいぶ前のBuilder.csあたりに

[MenuItem("Build/MasterMemory/Binaries")]
public static void BuildBinaries()
{ 
    var binary = ScriptableObject.CreateInstance<BinaryObject>();

    Task.Run(async () =>
    {
        binary.Value = await new AirtableMasterLoader().LoadAsync();
    }).Wait();

    AssetDatabase.CreateAsset(binary, ClientMasterLoader.BinaryAssetPath);
    AssetDatabase.Refresh();

    Debug.Log($"Build completed. Binary is created on {ClientMasterLoader.BinaryAssetPath}");
}

を追加して実行すると、指定の場所にバイナリを内包したScriptableObjectが生成されます。

image.png

仕上げ

あとはそのバイナリをAssetBundleにしておきましょう。

await MasterCache.RegisterAsync(new ClientMasterLoader());

で、読み込んであげれば、

MasterCache.cs
var appleMaster = MasterCache.TestMaster.FindByID(1);

これが達成できます。

まとめ

雑くてスミマセンが、アプリの起動時間=ログインのしやすさ関わってくる部分だと思っているので、できるだけマスタなどはクライアントにキャッシュしておくことをお勧めします。
で、更新があれば都度AssetBundleなどを経由してDLしてもらうのが、いいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Monobit Unity Networkingを使ってスコアマネージャーを作る【モノビットエンジン入門】

はじめに

以前にMUNを使ったスコアの同期についてちょろっと紹介しました。
今回はその内容を発展させ、マネージャクラスまで作ろうというお話です。

注意点として、ここからはサーバ接続/ルーム作成/ルーム入退室について理解している前提で進めます。
初心者向けの記事なので大丈夫だと思いますが、心配な方は前回の記事をサッと読んでおくと内容が入りやすいと思います。
Monobit Unity Networkingを使ってネットワークマネージャクラスを作る【モノビットエンジン入門】

また、MUNをまだ触ったことがない方は、まずはこちらの記事をご覧ください。
モノビットエンジンでかんたんサーバ接続

環境は以下の通りです
win10 64bit, Unity2018.3.3f1, Monobit Unity Networking2.0 v2.6

作るもの

スコア管理クラスを作ります。
機能は「同期」「変更」「ソート」だけのシンプルなものしかありません。
おおまかな処理の流れとしては、それぞれのクライアントがスコアの変更を要求すると、
ホストがその通知を受け取り、変更の処理が正常に行えればゲスト全員に配る感じです。

実装

ScoreManager

using System.Collections.Generic;
using UnityEngine;
using MonobitEngine;

/** 得点データ構造体 */
public class ScoreData {
    public ScoreData(int id) {
        player_id = id; // MonobitNetwork.player.ID
        score = 0;      // 得点
    }
    public int player_id;
    public int score;
};

public class ScoreManager : MonobitEngine.MonoBehaviour
{
    /** 全員の得点リスト */
    List<ScoreData> m_ScoreList = new List<ScoreData>();

    /// <summary>
    /// 自分の得点データを取得する
    /// </summary>
    public ScoreData GetScoreData() {
        return SearchData(MonobitNetwork.player.ID);
    }

    /// <summary>
    /// 全員の得点データを取得する
    /// </summary>
    public List<ScoreData> GetAllScoreData() {
        return m_ScoreList;
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    public void Init() {
        for (int i = 0; i < MonobitNetwork.room.playerCount; i++) {
            m_ScoreList.Add(new ScoreData(MonobitNetwork.playerList[i].ID));
        }
        // 得点をリセット
        Reset();
    }

    /// <summary>
    /// 得点を変更する通知を送信
    /// </summar
    public void Add(int value) {
        monobitView.RPC("RecvAddScore", MonobitTargets.Host, value, MonobitNetwork.player.ID);
    }

    /// <summary>
    /// 得点加算の通知を受信
    /// </summary>
    [MunRPC]
    void RecvAddScore(int value, int playerID) {
        // 加算する
        int temp = SearchData(playerID).score;
        temp += value;

        // 無効な値かチェック
        if (temp >= 0) {
            // 全クライアントに得点同期通知
            monobitView.RPC("RecvSyncScore", MonobitTargets.All, temp, playerID);
        }
    }

    /// <summary>
    /// 得点を反映する通知を受信する(全クライアントが処理)
    /// </summary>
    [MunRPC]
    void RecvSyncScore(int value, int playerID) {
        // 得点リストを変更
        SearchData(playerID).score = value;

        // 得点リストを降順ソート
        m_ScoreList.Sort((a, b) => {
            if (b.score - a.score < 0) return -1;           // Score値 b < a
            if (b.score - a.score > 0) return 1;            // Score値 b > a
            if (b.player_id - a.player_id > 0) return -1;   // PlayerID b < a (この時点でScore値は等しい)
            if (b.player_id - a.player_id > 0) return 1;    // PlayerID b > a (この時点でScore値は等しい)
            return 0;                                       // この時点でScore値はaとb共に等しく、PlayerIDもaとb共に等しい
        });
    }

    /// <summary>
    /// 得点リストをリセット
    /// </summary>
    void Reset() {
        foreach (ScoreData data in m_ScoreList) {
            data.score = 0;
        }
    }

    /// <summary>
    /// 得点リストからデータを取得する
    /// </summary>
    ScoreData SearchData(int playerID) {
        if (m_ScoreList.Count < 0 || m_ScoreList == null) {
            return null;
        }
        return m_ScoreList.Find(s => s.player_id == playerID);
    }
}

解説

データ構造体

この構造体ではMonobitNetwork.player.IDとセットでスコア値を保持します。
MonobitNetwork.player.IDとは、ネットワーク上に登録された、自分自身のプレイヤーIDを指します。
参考:プレイヤーパラメータ-MUN公式

初期化

public void Init() {
    for (int i = 0; i < MonobitNetwork.room.playerCount; i++) {
        m_ScoreList.Add(new ScoreData(MonobitNetwork.playerList[i].ID));
    }
    // 得点をリセット
    Reset();
}

この初期化関数はルームに入室している状態で呼ぶのが正しいので、全員のプレイヤーが入室し終わったときが良いと思います。
処理としては、ルームにいるプレイヤー全員分のScoreDataListを作っています。

変更

public void Add(int value) {
    monobitView.RPC("RecvAddScore", MonobitTargets.Host, value, MonobitNetwork.player.ID);
}

変更の要求はmonobitView.RPCで行います。関数の第一引数にコールする関数名、第二引数にコール対象、以降はコールする関数へ渡したい値です。参考:RPC(Remote Procedure Call)-MUN公式

[MunRPC]
void RecvAddScore(int value, int playerID) {
    // 加算する
    int temp = SearchData(playerID).score;
    temp += value;

    // 無効な値かチェック
    if (temp >= 0) {
        // 全クライアントに得点同期通知
        monobitView.RPC("RecvSyncScore", MonobitTargets.All, temp, playerID);
    }
}

コールされたホストは、引数のPlayerIDに紐づくスコア値を取得し、計算を行ったのちに無効な値かどうかチェックをします。問題なければ計算結果をクライアント全員に対してRPCで配ります。このときホスト自身も含まれます。

同期とソート

[MunRPC]
void RecvSyncScore(int value, int playerID) {
    // 得点リストを変更
    SearchData(playerID).score = value;

    // 得点リストを降順ソート
    m_ScoreList.Sort((a, b) => {
        if (b.score - a.score < 0) return -1;           // Score値 b < a
        if (b.score - a.score > 0) return 1;            // Score値 b > a
        if (b.player_id - a.player_id > 0) return -1;   // PlayerID b < a (この時点でScore値は等しい)
        if (b.player_id - a.player_id > 0) return 1;    // PlayerID b > a (この時点でScore値は等しい)
        return 0;                                       // この時点でScore値はaとb共に等しく、PlayerIDもaとb共に等しい
    });
}

得点リストを変更すれば、全クライアントで値が同一のものとなるので同期されたことになります。続けて、リストを変更したあとソートを行います。順序規則はゲームの仕様によって様々ですが、今回のケースではまずScore値で比較を行い、もし同一ならPlayer.IDの順で並べるようにしました。

最後に

いかがだったでしょうか。
うまくいかないという方はMUNの基本的な手順が抜けてしまっているかもしれないので、そこを重点的に見直すと良いかもしれません。(MonobitView.csのアタッチ忘れなど)
今後も引き続きMUNの機能を紹介していけたらと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

魔法の杖のジェスチャーを機械学習で判定させてみた

Wiiのようなジェスチャーゲームを作っていたので、そこで学んだことを記述していきたいと思います。

こちらが魔法の杖!

magic_stick.JPG
外装は3Dプリンタで作成。
中にはESPと加速度センサが入っている。
スイッチは外から押せるようにした。
100円ショップのモバイルバッテリーとESPを接続。

魔法の杖から加速度を取得する

こちらは「スイッチを押しているとき、加速度の値をBluetoothで送信する」からコードを少し変えています。
変更点はこちら
1. スイッチを押しているときではなく、常に加速度の値を送信しています。
2. スイッチを押した瞬間、押されたという情報を送信します。

acc_sender.c
#include <SparkFunLSM9DS1.h>
#include "BluetoothSerial.h"

#define LSM9DS1_M 0x1E
#define LSM9DS1_AG 0x6B
#define SW_PIN 7 //pin番号を指定する

LSM9DS1 imu;
BluetoothSerial SerialBT;

int sw_before = 0;
uint16_t connectionState = 0;

void setup()
{
  Serial.begin(115200);
  SerialBT.begin("ESP32"); //Bluetooth接続で表示される名前を指定
  pinMode(SW_PIN, INPUT);

  imu.settings.device.commInterface = IMU_MODE_I2C;
  imu.settings.device.mAddress = LSM9DS1_M;
  imu.settings.device.agAddress = LSM9DS1_AG;
  if (connectionState == 0)
  {
    connectionState = imu.begin();
    while (connectionState == 0)
    {
      Serial.println("Failed to communicate with LSM9DS1.");
      Serial.println("Double-check wiring.");
      Serial.println("Default settings in this sketch will "
                     "work for an out of the box LSM9DS1 "
                     "Breakout, but may need to be modified "
                     "if the board jumpers are.");
      Serial.print("Connection Status: ");
      Serial.println(imu.begin());
      delay(1000);
      connectionState = imu.begin();
      Serial.println("------------------------------------------------------\n");
    }if(connectionState!=1){
      Serial.print("connectionState: ");
      Serial.println(connectionState);
    }
  }
}

void loop()
{
  float x, y, z;
  imu.readAccel();
  x = imu.calcAccel(imu.ax)*10;
  y = imu.calcAccel(imu.ay)*10;
  z = imu.calcAccel(imu.az)*10;
  int sw_state = digitalRead(SW_PIN); // スイッチが押されてるとき1、押されていないと0
  if(sw_state == 1 && sw_before == 0){ // 0だったのが1に切り替わった
    SerialBT.println("clicked");
  }else{
    SerialBT.println("no");
  }
  SerialBT.println(x);
  SerialBT.println(y);
  SerialBT.println(z);
  sw_before = sw_state;
  delay(20);
}

ジェスチャーの予測モデルを作成

PCで加速度を受信し、ジェスチャの予測モデルを作成します。

Bluetoothを接続

ESP側を動かしたままで、PC本体の、
"設定→デバイス→Bluetoothとその他のデバイス→Bluetoothまたはその他のデバイスを追加する→Bluetooth"
の順で探し、自信で指定したESPのデバイス名(今回の場合はESP32_2)をクリック。接続が終わったら完了。

COMポートを探す

そのまま、PC本体の、
"設定→デバイス→Bluetoothとその他のデバイス→その他のBluetoothオプション→COMポート
で、接続されたESPの"発信"側のポート番号をコピー。

コード

上記のポート番号を、12行目の
ser = serial.Serial(.......
の "COM??" に入力する。

今回は3種類のジェスチャーを3回ずつ行い、入力した名前の予測モデルを作成するコードです。ゲームのチューニング程度なので3回にしましたが、ジェスチャーの種類の数と回数、長さ等は仕様に応じて変更してください

model_creater.py
#coding utf-8
import serial #シリアル通信を行うため
import numpy as np #numpyで2次元配列を扱うため
from sklearn.svm import SVC #学習用
import pickle #モデルをファイルとして使用する用

length = 20 #ジェスチャの長さ(連続で受信するデータの個数)
gestures = 3 #ジェスチャの種類の数
times = 3 #各ジェスチャを行う回数

print('Connecting...')
ser = serial.Serial('COM19',115200,timeout=None) #ポート番号19からシリアル受信

print('Input your name:')
name = input()
filename = 'model_' + name + '.sav' #入力した名前の学習モデルのファイル名
X = []
Y = []

for i in range(gestures * times):
    cnt = 0 #受信したデータのカウント用
    mx = [] #各軸ごとの配列を初期化
    my = []
    mz = []
    gesture_num = i % gestures + 1 #ジェスチャ番号を指定

    print('Do gesture No.', gesture_num)

    while True:
        line = ser.readline() #シリアルから1行取得
        switch = line.decode('utf-8') #コード変換
        line = ser.readline()
        x = line.decode('utf-8')
        line = ser.readline()
        y = line.decode('utf-8')
        line = ser.readline()
        z = line.decode('utf-8')

        if switch == 'clicked\r\n' and cnt == 0: #クリックされたらカウント開始
            cnt = 1

        if cnt != 0: #ジェスチャ中
            mx.append(x.rstrip('\r\n')) #シリアルから受け取ってたデータのゴミを取って配列へ追加
            my.append(y.rstrip('\r\n'))
            mz.append(z.rstrip('\r\n'))
            cnt += 1
            if cnt == length: #lengthまでいったら次のジェスチャ待ちへ
                break

    m = mx + my + mz #三軸の加速度を横一列にする
    X.append(m) #学習データ用リストへ追加
    Y.append(str(gesture_num)) #学習ラベル用リストへ追加

ser.close() #シリアルを閉じる

model = SVC(kernel = 'linear', C=1, gamma=1) #学習モデルのパラメータを指定
model.fit(X,np.ravel(Y)) #学習を行う

with open(filename, 'wb') as fp_model: #学習モデルを保存するためのファイルを開く
    pickle.dump(model, fp_model) #モデルを保存

print('Model created.')

これで予測モデルが保存されました!
研究目的ではありませんので、精度検証は行わず早速ジェスチャーを判定してみましょう。

ジェスチャーを判定し続ける

PCで加速度を受信し、ジェスチャーを判定し続けます。
以下のプログラムで、ポート番号を適切に変えて実行するだけです。

gesture_judger.py
import serial #シリアル通信を行うため
import pickle #モデルをファイルとして使用する用

length = 20 #ジェスチャの長さ(連続で受信するデータの個数)
print('Connecting...')
ser = serial.Serial('COM19',115200,timeout=None) #ポート番号19からシリアル受信

print('Input your name:')
name = input()
filename = 'models/model_' + name + '.sav' #入力した名前の学習モデルのファイル名

print('Loading...')
with open(filename, 'rb') as fp_model: #学習モデルファイルを開く
    loaded_model = pickle.load(fp_model) #モデルをロード
print('Do any gesture!')

try:
    while True:
        cnt = 0 #受信したデータのカウント用
        mx = [] #各軸ごとの配列を初期化
        my = []
        mz = []

        while True:
            line = ser.readline() #シリアルから1行取得
            switch = line.decode('utf-8') #コード変換
            line = ser.readline()
            x = line.decode('utf-8')
            line = ser.readline()
            y = line.decode('utf-8')
            line = ser.readline()
            z = line.decode('utf-8')

            if switch == 'clicked\r\n' and cnt == 0: #クリックされたらカウント開始
                cnt = 1

            if cnt != 0: #ジェスチャ中
                mx.append(x.rstrip('\r\n')) #シリアルから受け取ってたデータのゴミを取って配列へ追加
                my.append(y.rstrip('\r\n'))
                mz.append(z.rstrip('\r\n'))
                cnt += 1
                if cnt == length: #lengthまでいったら次のジェスチャ待ちへ
                    break

        m = mx + my + mz #三軸の加速度を横一列にする
        m2 = [] #リスト型にするため
        m2.append(m) #リストへ追加

        pre = loaded_model.predict(m2) #ジェスチャの判定
        print('This gesture is No.', pre[0][0])

except KeyboardInterrupt: # Ctrl-C を捕まえたら終了
    print('Close!')

ser.close() #シリアルを閉じる

できましたでしょうか?
ゲームで応用する場合は、判定したものをソケット通信で送信し、Unity側のC#で受け取れば実装できます。処理もなかなか素早くできますよ。
ご不明点等あればお気軽に質問してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

unityでshaderに動的に数値を設定しても動かなかった話

先に結論

materialがinstanceされてて
数値指定は非instanceのマテリアルに当たってた。

症状

・unityからuniformでshaderにsetfloatで数値を渡していた
・挙動が切り替わる変数と切り替わらない変数があった
・getfloatで数値が切り替わってることは確認していた

原因(ポカ)

・数値を切り替えるスクリプトが複数あり、
 起動後にマテリアルを取得するものと、手動でアタッチしているものがあった。
 前者はうまく動き、後者がうまく行っていなかった
・glslだったため、実機でテストしていてinstance化に気付いていなかった

対応

・スタートでマテリアルを取得するようにした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity]Oculus Questへのインストールが失敗した際に見直したこと

はじめに

友人から「OculusQuest向けのビルドは通るがインストールでエラーを吐く」
と相談された時に確認した項目と最終的な解決策を記事としてまとめておきます。
他に参考になりそうな記事がパッと見た感じなかったのでお役に立てれば。

環境
OS : Windows 10
Unityバージョン : 2018.2.5f1

エラー内容

Performing Streamed Install
adb: failed to install ~~\test.apk: Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION: Failed to parse /data/app/vmdl57234186.tmp/base.apk: AndroidManifest.xml]

吐かれたログの中から原因を示しているであろうエラーはこんな内容でした。

見直したこと

  • SDKのパス
    (Android StudioのSDKマネージャーで確認できるパスと、
    Unityに設定しているパスが一致しているか、と環境変数のsdk/toolsのパス)
  • JDKのパージョン(1.8~ かどうか)
  • NDKのパージョン
  • Adbのインストールの確認と再インストール
    参考サイト: https://tech.la-fra.com/2018/07/29/post-204/

他にもこちらの記事を参考にPlayer Settingsなども一通り見直しましたが何故かインストール出来ませんでした。

試しに

一度普通のAndroid端末でもビルドしてみましたが、
こちらでも何故かビルドの段階でこの様なエラーが
(Build SettingのBuild SystemはGradle)

FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring root project 'gradleOut'.
No toolchains found in the NDK toolchains folder for ABI with prefix: mips64el-linux-android

最終的な解決策

ここでどうもNDKが悪さをしているのでは?と思い、
Android Studio側のNDKをアンインストールしてみた所、無事インストールが通りました。
その流れでQuestでもインストールしてみると、なんとすんなり通った...
※ ただBuild and Runではどちらでも何故かエラーが発生するため、Build後にapkを直接adbコマンドでインストールしました。

どうもAndroid Studio側のNDKとUnity側のNDKが競合していた...?
詳しくはイマイチ分からず仕舞いですが、同じエラーが起きた方は
一度Android StudioのNDKを切ってみると良いかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SteamVR2.0のInteractionシステムが超便利だから使ってみよう

はじめに

こんにちは、ZeniZeniです。
みなさんSteamVRのバージョンは何を使っていますか?SteamVRはバージョンが2.0になってから大幅に仕様が変わり、それを忌避して今もver1.xを使っている人は多いんじゃないんでしょうか。(僕もそうでした)

しかし!SteamVR2.0にはめちゃくちゃ便利なコンポーネント群があって、SteamVRを入れるだけで基本的なVRゲームのシステムを実装できます。

この記事では、SteamVR2.0での主要な便利コンポーネントの使い方を紹介していきます。(めちゃくちゃ種類があるので、少しずつ紹介を増やしていきます。)
こちらの公式ドキュメントにも詳しいことが載っているので、ぜひ読んでみてください。

コントローラのinput周りの仕様(binding UI)については、これまでにたくさん記事が出ているので、そちらを参照してください。
フレームシンセシスさんのサイト がわかりやすくておすすめです。

で、どう便利なんですか?

まずはSteamVR2.0を導入して、フレームシンセシスさんのサイトに書いてある通りに設定をしましょう。
その後、Asset/SteamVR/InteractionSystem/Samples/Interactions_Example.unityを開きましょう。
するとこのようなシーンが開きます。
bandicam 2019-07-15 16-25-04-511.jpg

このシーンを起動すると、テレポート移動しながら、Steamが用意してくれる多様なInteraction Systemを体験できます。

弓矢や感覚的に押せるボタンがあったり
bandicam 2019-07-15 16-30-08-802.jpg

回せる円盤やレバーがあったりします。
bandicam 2019-07-15 16-30-19-271.jpg

ラジコンもありました。
bandicam 2019-07-15 16-31-56-666.jpg

もうここだけでも1時間は遊べますね!

このシーンからわかる通り、SteamVRには基本的な「物をつかむ」、「物を投げる」、「テレポートする」以外の多様な機能が標準実装されています。

さらにこれらはVRでなくても遊べるようになっているのがすごいんです。
SteamVR2.0にはfallbackという機能が付いていて、下図の2D Debugを押すとキーボードとマウスでVRと同様のことができます。
チーム開発でVR環境のない人もデバッグができるのは素晴らしいですね。
bandicam 2019-07-15 22-20-13-160.jpg

大前提

基本的には、Playerコンポーネントを持つクラスが存在し、インタラクション対象にはInteractableコンポーネントを付け、コントローラにはHandコンポーネントを付けます。
SteamVRのInteraction Systemはこれらを参照して、様々な命令を行っています。
Player.prefabがCore/Prefabsにあるので、それを使うのが簡単です。これはCameraRigのより発展的な物で、InteractiveSystemを使うために必要な設定がされています。
bandicam 2019-07-16 00-40-35-737.jpg

bandicam 2019-07-15 16-59-52-250.jpg
bandicam 2019-07-15 17-00-06-926.jpg
インスペクターの項目が多すぎてうへーっってなりますが、項目をひとつずつ簡単に説明します。

Player.cs

シングルトン的な扱いになっているので、シーンに二つ以上置けません。
GetComponentしなくてもどこからでも呼び出せます。(便利!)

基本的には他のクラスはこのPlayerコンポーネントを利用して、HandやCameraを参照します。
特にこのインスペクターをいじるということは少ないと思います。

Interactable.cs

インタラクションしたいオブジェクトには必ずこのコンポーネントを付けてください。
このコンポーネントの様々なメソッドを利用して、複雑なインタラクションシステムを構成していきます。

変数名                                                 型                役割 
Activate Action Set On Attach   SteamVR_ActionSet SteamVR Inputに登録したAction Setを有効にします。
自前で作成したAction Setで操りたい場合はここにそのAction Setを設定します。
Hide Hand On Attach bool trueだと物を掴んだりするとき(Hand.AttachObject()が呼ばれるとき)に、コントローラも手のモデルも消します。逆に物を離す時(Hand.DettachObject()が呼ばれるとき)には出現します。
Hide Skeleton On Attach bool Hide Hand On Attachと違い手だけが消えるようになります。
Hide Skeleton On Attach bool Hide Hand On Attachと違いコントローラだけが消えるようになります。
Hand Animation On Pickup int 物を掴む際の手のアニメーションを指定します。手のアニメーターコントローラはint型のanimationStateという変数でアニメーションを切り替えていて、指定した値のアニメーションが物を掴む際に実行されます(後述)。 ...のはずなんですけど0以外を指定するとエラーがめちゃめちゃ出る。というか手の形を指定する場合はSteamVR_Skeleton_Poserを使うので(後述)、これどう使うのかわからない。サンプルはみんな0だし。
Set Range Of Motion On Pickup SkeletalMotionRangeChange 手の形をコントローラを握ってる形に合わせるか合わせないかを決めます。どうでもいいならNoneにします。
Use Hand Object Attachment Point bool オブジェクトを掴む際の手の位置を、Hand.ObjectAttachmentPointのTransformを参照するかHand自体のTransformを参照するかを決めます。SteamVR_Skeleton_Poserで手の位置と形を指定しているとfalseになることに注意。
Attach Ease In bool 物を掴む際に、オブジェクトがゆっくり手に収まるか、即座に手に収まるかを決めます。
Snap Attach Ease In Time float Attach Ease In がtrueのとき、オブジェクトが手に収まるまでの時間を指定します。
Snap Attach Ease In Completed bool Attach Ease In がtrueになっていて、オブジェクトがゆっくり移動している間はfalse。移動が完了するとtrueになります。それ以外の時にtrueになっているかfalseになっているかは不定(確認用だからいじることはない)
Hand Follow Transform bool falseのとき、物を掴むと、掴んだ瞬間のオブジェクトと手の相対距離のまま、オブジェクトが手に追従します。trueならばUse Hand Object Attachment Pointで指定したTransformの位置に手が来ます。SteamVR_Skeleton_Poserで手の位置と形を指定しているとtrue、false関係なくSteamVR_Skeleton_Poserで指定した手と位置と形になります。
Highlight On Hover bool Handがオブジェクトに触れているときに、オブジェクトにハイライトを付けるかを決めます。
Hide Highlight GameObject[] ここに設定された子オブジェクトはHighlightされなくなります。

Hand.cs

コントローラにつけるコンポーネントですね。

変数名                                                 型                役割 
Other Hand Hand 反対の手を設定します。
Hand Type SteamVR_Input_Sources Inputシステムでのコントローラの入力を取るかを指定します。
Tracked Object SteamVR_Behaviour_Pose 基本的に自身が入ります。
Grab Pinch Action SteamVR_Action_Boolean 物を掴む際に、Inputシステムのどの入力で掴むかを決めます。
Grab Grip Action SteamVR_Action_Boolean 物を掴む際に、Inputシステムのどの入力で掴むかを決めます。
Haptic Action SteamVR_Action_Vibration コントローラを振動させるときのOutアクションを指定します。
UI Interact Action SteamVR_Action_Boolean UIを操作する際に、Inputシステムのどの入力で操作するかを決めます。
Use Hover Sphere bool インタラクションする際の手の効果範囲(緑色)を使用するかしないかを決めます。falseにするとGizmoが消えます。
Hover Sphere Transform Transform インタラクションする際の手の効果範囲(緑色)の原点の座標をTransformで指定します。
Hover Sphere Radius float インタラクションする際の手の効果範囲(緑色)を設定します。
Hover Layer Mask float インタラクションしたくないものの属するlayerのチェックを外すことで、それらのオブジェクトに干渉しなくなります。
Hover Update Interval float オブジェクトに触れているかいないかの更新頻度を設定します。
Use Controller Hover Component bool インタラクションする際の手の効果範囲(青色)を使用するかしないかを決めます。falseにするとGizmoが消えます。
Controller Hover Component string インタラクションする際の手の効果範囲(緑色)の原点の座標をstringで指定します。なにゆえstring?と思うかもしれませんが、これはviveコントローラのモデル(SteamVR_RenderModel.csのついたもの)の子オブジェクトの任意の部位の座標を指定するためです。他に下図のようなものがあります。bandicam 2019-07-16 01-50-44-177.jpg
Controller Hover Radius float インタラクションする際の手の効果範囲(青色)を設定します。
Use Finger Joint Hover bool インタラクションする際の手の効果範囲(黄色)を使用するかしないかを決めます。falseにするとGizmoが消えます。
Finger Joint Hover SteamVR_Skeleton_JointIndexEnum インタラクションする際の手の効果範囲(黄色)の原点の座標を手のモデルの部位で指定します。
Finger Joint Hover Radius float インタラクションする際の手の効果範囲(黄色)を設定します。
Object Attachment Point Transform 物を掴む際む際の手の位置を指定します。Interactableコンポーネントの設定によっては全く使わないです。
No Steam VR Fallback Camera Camera Fallback用の機能
No Steam VR Fallback Max Distance No Item float Fallback用の機能
No Steam VR Fallback Max Distance With Item float Fallback用の機能
Show Debug Text bool trueだとコントローラの横に下図のようなデバッグ用テキストが出る(便利)bandicam 2019-07-16 02-07-47-183.jpg
Spew Debug Text bool trueだとShow Debug TextのtextがEditerのConsoleに出力されます。
Show Debug Interactables bool trueだとインタラクション対象まで緑色の線が引かれます。Debug.DrawLineで出した線なのでSceneビューじゃないと確認できません。

インタラクションする方法

インスペクターに出てる機能を理解したところで、さっそくインタラクションできるオブジェクトを作ってみましょう。
例として、SteamVR側がInteractableExample.csを作成しており、それを参考に動作を理解してみましょう。

To Be Continued!

開発、執筆にあたり、下記のサイト様を参考、引用させていただきました。

  1. フレームシンセシス
  2. SteamVR Interaction System from The Lab
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む