macOS 環境で Stereo Speaker + Sound Slayer の 7.1ch サウンド環境をつくる

背景

自宅にあるデスクトップPC(Windows)は生活スペースに置きたくないため*1macOSからリモート接続*2して使用しています。有線のネックスピーカーであるSound Slayerを購入したはいいものの、リモートのWindowsへの接続方法が課題でした。

更に、生活スペースにはStereo Speakerが置いてあります。Stereo Speaker(2ch)とSound Slayer(5.1ch)を組み合わせて7.1chサウンド環境をつくれないか?と思ってしまったわけです。スピーカーの位置による没入感はどうなのか?という点はいったん脇に置いて、まずは実現可能なのかを検証しました。

最終的に、無料での構築は断念しましたが、追加料金1000円で構築できました。本記事は、その備忘録です。色々と試行錯誤している時に困っていた現象が突然なくなり、その理由はよくわかっていません。記録に漏れがあり、再現できない可能性はあります。ご了承ください。

構成

図のとおり、PCと周辺機器は以下で構成されます。

また、以下のソフトウェアを使用します。

本記事では、ParsecとDual Senseに関わる部分は扱いません。Parsecは低遅延のリモートデスクトップアプリケーションです。ゲームパッドの入力もHostであるWindows PCに送信できます。音声出力も受信できますが、ステレオ(2ch)しか対応していません。

Windows側の構築

はじめに、VB-Audio VoiceMeeterからVoice Meeterのインストーラーをダウンロードして、Voice Meeterをインストールします。インストール手順は特別なことが無かったので省略します。

インストールが完了するといくつかのアプリケーションが追加されます。(画像ではVoice Meeter Bananaがインストールされていますが、通常のVoice Meeterで構築できたのでBananaは不要です。)

アプリケーションの他にもサウンドバイスが追加されます。追加されたサウンドバイスの設定を7.1chに変更します。

まず、CABLE Input デバイスの構成を選択し、7.1サラウンドを選択して閉じます。

次に、Voicemeeter Input デバイスの構成を選択し、7.1サラウンドを選択して閉じます。このダイアログでのテストは最後に実行します。デバイスを規定値に設定したら次に進みます。

Voicemeeter x64 アプリケーションを起動します。右側にA MAIN OUT、B VIRTUAL OUTと書かれているので、A(A1)をWindowsのスピーカー用に、B(A2)を配信用に設定することにします。VIRTUAL INPUTの列にあるAとBのトグルボタンはBのみを有効にします。有効にしたOUTに音声は送られます。B VIRTUAL OUTの縦に並んだボタン(破線の部分)は全て無効にしておきます。

HARDWARE OUTのA2を選択すると以下のダイアログが表示されます。左側で KS(Kernel Streaming)を選択したうえで、CABLE Input デバイスを選択します。さきほど7.1サラウンドに変更したので8chと表示されています。選択したらダイアログは閉じて次に進みます。

VBANを選択すると以下のダイアログが表示されます。左上のボタンでVBANをONにします。そして、下の赤枠の部分を編集します。さきほどBのVIRTUAL OUTを配信用に設定することにしたので、SourceはBUS Bです。Stream Nameは任意です。IP Address ToはmacOS側のIPアドレスを調べて設定します。Chを8にします。Net Qualityはネットワークの状況に応じて設定します。私のWi-Fi環境ではFastにするとノイズが入るようだったのでMediumにしています。編集が終わったらダイアログは閉じて次に進みます。

Menuを選択すると以下のプルダウンメニューが表示されます。アプリケーションが実行されていないと音声の転送が行われないため、System TrayとRun on Windows Startupを有効にします。これにより、Windows起動時にアプリケーションが実行され、アプリケーションはシステムトレイに常駐します。

macOS側の構築

macOS側では、まず機器セットを構成します。そのために、標準アプリケーションのひとつであるAudio MIDI設定アプリケーションを起動します。以下のアイコンのアプリケーションです。Sound Slayerを接続していると、一覧にHeadphone、Microphone In、Speakerが表示されます。ダイアログ左下の「+」を選択し、機器セットを選択します。機器セットの設定画面が表示されるので、使用するデバイスチェックボックスをONにします。UGREEN-40759P(Stereo Speaker)、Speaker(Sound Slayer)、Microphone In(Sound Slayerのマイク)をONにしています。中央上のクロックのソースでSpeakerを選択すると、音ずれ補正の破線の部分が自動的に変更されます。ここは他のデバイスを選択しても大丈夫です。出力チャンネルの部分は編集可能なので画像のように編集しておくと良いと思います。画像では縦に並んでいますが、横に並んでいて見づらければ右上のボタンで表示を変更できます。

右下の「スピーカーを構成」を選択すると以下のダイアログが表示されます。構成を7.1リアサラウンドに変更し、ストリームとチャネルを適切に選択します。テストを行い、各スピーカーから再生されることを確認します。適切に設定できていたら適用し、完了して次に進みます。

macOSサウンド設定で出力をさきほど設定した機器セットに設定します。

最後にVBAN Receptorアプリケーションを起動します。以下のアイコンのアプリケーションです。アプリケーションを起動すると以下のダイアログが表示されます。Stream NameをクリックするとWindows側で設定したStream Nameが表示されるので選択します。また、ダイアログ下部でHARDWARE OUTPUTに機器セットを選択します。

チャネルを調整する

ここまでの設定を行ったうえで、Voicemeeter Inputデバイスの構成からテストを行うとテストで再生したスピーカーと実際に音が鳴るスピーカーが異なります。

Windows側とmacOS側でチャネルが異なることで発生するようなので、Windows側で8x8 Output Matrixアプリケーションを使って調整します。

アプリケーションを実行したら以下のダイアログが表示されます。左上のSUB Bを選択します。Windows側で設定したBのVIRTUAL OUTにおけるチャネル毎の音量を設定します。画像のとおりに設定します。この設定ではチャネル3と5を、4と6を入れ替えています。数字の変更は変更したい数値を上下にドラッグします。キーボードでの入力が1回だけできたのですがそれ以降成功せず、数値の変更方法が分からず苦労しました。

アプリケーションを終了してしまうとチャネルの入れ替えが有効になりません。アプリケーションを常駐させる必要があるため、タイトルバーを右クリックしてメニューを表示します。System Tray、Show App On Startupを有効にし、Windows起動時にアプリケーションを起動しシステムトレイに常駐させます。

再度、Voicemeeter Inputデバイスの構成からテストを行うとテストで再生したスピーカーと実際に音が鳴るスピーカーが同じになります。

以上で全ての設定は終了です。おつかれさまでした。

スピーカーの音量バランスが悪い場合は8x8 Output Matrixアプリケーションで調整するのが簡単だと思います。macOS側では機器セットの音量調整ができません。サウンド設定の音量設定が不可能になります。ミュートやマスターボリューム設定はVBAN Receptorで行うと楽かなと思います。

追記:2025-03-31

背景にて「色々と試行錯誤している時に困っていた現象が突然なくなり、その理由はよくわかっていません。」と書きましたが、困った現象が再発しています。困った現象とは、全てのチャネルの音がフロントのStreo Speakerから再生される現象と再生されるチャネルが変更されてしまう現象です。全てのチャネルの音がフロントのStereo Speakerから再生される現象はParsecMinFrameRateが関係している様子で、Windowsの音量ミキサーでParsecMinFrameRateを消音すると再生されなくなります。

*1:生活スペースとなっている部屋が比較的暖かいのと、埃が多いためです。

*2:無線と有線のどちらでも使える様にしていますが、大抵はケーブルが邪魔なので無線です。

macOS 用 UnityHub および Unity アプリケーションが起動しなくなった件

2023-10-25 UnityHub をアップデートしたところ、UnityHub および Unity アプリケーションが起動しなくなりました。

何とか起動する様になったので、行ったことの備忘録です。まずは注意事項です。

  • この記事に書かれた方法により何らかの問題が発生しても当方では責任を負えません。
  • この記事に書かれた方法で必ずしも解決するとは限りません。
  • この記事に書かれた方法の全てを行わなくても解決するかもしれません。

まず、最終的に解決に至るきっかけとなった操作を記します。

  • キーチェーンアクセスにおいて Unity で検索し、結果として表示された 2 項目を削除した。
    • 項目名はメモしていないが、片方はアカウントに関する情報だったと思う。

試行錯誤している段階で UnityHub と Unity を完全に(?)アンインストールしています。以下のディレクトリの内容を全て削除しました。

アプリケーションが起動しない時にアクティビティモニタを確認したところライセンス関係のプロセスが起動していたので、ライセンス認証まわりで問題があったのではないかと想像しています。

Kotlin コーディング規約

概要

Kotlin コーディング規約から個人的に忘れそうな部分を抜き出した備忘録です。 ここに記載されている以外の内容も有益なので原文の参照をおすすめします。

目次

参考情報

備忘録

Source code organization

Source file organization

複数の宣言を同じ1つのファイルに配置することは、以下の条件を全て満たす場合のみ推奨されます。宣言には、classes, top-level functions, top-level properties などが該当します。

  • これらの宣言が意味的に密接に関連している
  • ファイルサイズが妥当な範囲内(数百行を超えない)

Class layout

  1. Property declarations and initializer blocks
  2. Secondary constructors
  3. Method declarations
  4. Companion object

以下を行ってはいけません。

  • メソッドをアルファベット順に並べる
  • メソッドを可視性で並べる
  • 通常のメソッドと拡張メソッドを分離する

nested classes はそのクラスを使用するコードの次に置きます。nested classes が外部で使用されることを意図している場合は companion object の後に置きます。

Naming rules

package の命名では小文字のみを使用し _ は使用しません。複数語から成る名前は通常は推奨されませんが、使用する必要がある場合は単語を連結する (nameexample) か camel case (nameExample) を使用します。

Function names

クラスのインスタンスを生成する factory functions の場合は戻り値の型と同じ名前を使用します。例えば、戻り値の型が Foo の場合は fun Foo(): Foo { ... } とします。

Property names

以下の val properties には大文字を _ で接続した名前 (EXAMPLE_NAME) を使用します。

  • const が付けられている
  • top-level か object で、カスタムの get function を持たず、変更不可能なデータを保持している

シングルトンオブジェクトへの参照を持つ properties には object 宣言と同じ規則で名前を付けます。つまり、クラスと同様の名前を付けます。

enum constants は以下のいずれかの名前を使います。

  • uppercase underscore-separated names (例えば、RED, BLUE, GREEN)
  • upper camel case names (例えば、Red, Blue, Green)

Names for backing properties

プレフィックスに _ を付けます。

Choose good names

名前の一部に頭文字を使用している場合は以下の規則に従います。

  • 2文字の場合は全て大文字 (例えば、IOStream であり IoStream ではない)
  • 3文字以上の場合は先頭のみ大文字 (例えば、HttpInputStream であり HTTPInputStream ではない)

Formatting

Horizontal whitespace

// の後には空白を入れること。

水平方向で揃えることは避けること。識別子の名前の長さが変わった場合に影響を受けるべきではありません。

Colon

以下の場合は : の前に空白を入れます。

  • type と super type を分ける (例えば、class FooImpl : Foo())
  • 他の constructor に委譲する (例えば、constructor(x: Int) : this(x))
  • object キーワードの後 (例えば、object : IFoo)

Class headers

長いため省略。原文を参照のこと。

Modifiers order

長いため省略。原文を参照のこと。

Annotations

引数ありの注釈を複数並べるときは改行します。引数なし注釈を複数並べるときは1行に並べます。引数なし注釈が1つの場合は宣言と同じ行に書きます。

File annotations

ファイルコメントの後、package 宣言の前に書きます。但し、package 宣言との間に空行を入れ、package ではなくファイルに対する注釈であることを強調します。

Lambdas

パラメーターリストが長すぎて1行に収まらない場合は、-> を別の行に配置します。

Trailing commas

Kotlin スタイルガイドでは宣言時の引数末尾へのコンマ記載を推奨しています。

Documentation comments

通常は @param@return などのタグの使用は避けます。文中に [paramName] と記載します。

Avoid redundant constructs

「わかりやすくするため」だけに不要な構文要素をコードに残さない様にします。例えば、戻り値の型の Unitセミコロン、string template 中の {} などは不要な場合にはなくします。

Idiomatic use of language features

Lambda parameters

短く、ネストされていないラムダでは、it の使用を推奨します。

Named arguments

以下の場合には named argument の使用を推奨します。

  • 同一の基本型を複数とる
  • Boolean 型 (全ての引数が文脈より絶対的に明確である場合を除き)

Nullable Boolean values in conditions

nullable Boolean の比較は value == truevalue == false で比較する。

Loops

基本的には通常の for ループの使用を推奨します。但し、以下の場合は forEach 関数を使用します。

  • forEach のレシーバーが nullable
  • 長い call chain の一部として使う

Functions vs properties

以下の場合は functions より properties を優先して使用します。

  • 例外を投げない
  • 計算が安価、もしくは、一度だけ計算してキャッシュしている
  • オブジェクトの状態が変更されない限り、呼び出しの度に同じ結果を返す

原神(Genshin Impact)をDualSenceでプレイする

概要

原神はDualSenceを無線(Bluetooth)接続した場合に限りDualSenceを使用してプレイできると書かれた記事が見受けられます。 しかし、PC環境によって原神がコントローラーを認識したり、しなかったりといった状況がある様です。 認識されない場合の対処方法にはいくつかの方法がありますが、本記事ではSteamのBig Pictureモードを使用する方法を説明します。

尚、この記事の内容は動作を保証するものではありません。試す場合には自己責任でお願いします。 また、本記事の方法でも動作しない場合、当方では対応しかねます。

(2021-10-06追記:Ver.2.2でDualSenceに正式対応するとのことなので、こちらの方法は不要かもしれません。)

目次

確認環境

  • Windows 10 Pro (21H1)
  • Steam ビルド Sep 16 2021
  • 原神(Genshin Impact) 2.1.0

参考情報

解説

事前にBluetoothのペアリングは済ませてある前提で話を進めます。ペアリングが済んでいない場合には、他の記事を検索すると説明している記事が見つかると思います。以下の画像の様に、「Wireless Controller」が「ペアリング済み」になっている必要があります。

Steamを管理者として実行する

Steamを起動する際には「管理者として実行」を選んで起動する必要があります。

ライブラリに原神を登録する

ライブラリメニューから、「非Steamゲームを追加」を選択して Genshin Impact を追加します。

「参照」ボタンを押下して、exeファイルを選択するウィンドウを開きます。

C:\Program Files\Genshin Impact\Genshin Impact Game フォルダにある GenshinImpact.exe を選択します。

そして、「選択したプログラムを追加」ボタンを押下します。

以上で原神アプリがライブラリに追加されます。

Big Picture モードに切替え、コントローラを設定する

ゲームを起動する際には Big Picture モードから起動します。

まずは、Big Picture モードに切り替えます。

歯車アイコンから設定メニューを開き、コントローラー設定を開きます。

PlayStation 設定サポート」を有効にします。 ついでに「Big Picture 終了時に電源を切る」を有効にすると、Big Picture を終了した際にコントローラーの電源が切れます。

以上で設定は完了です。

ゲームを起動する

ゲームを起動する際には、Big Picture モードのライブラリメニューから GENSHINIMPACT を選択します。

ゲームが開始されるとコントローラーでプレイできる思います。

(管理者として実行されていれば Big Picture モードでなくてもプレイできるみたいです。)

Pythonのプログラムでピボットテーブルを使う

概要

PythonからExcelのピボットテーブルを使いたいが、難しいプログラムを記述したくない場合の方法です。 ピボットテーブルの設定はExcelで行い、Pythonのプログラムではピボットテーブルの値の範囲のみを変更します。

目次

確認環境

参考情報

解説

テンプレートとなるExcelファイルを作成する

まずピボットテーブルを含んだExcelファイルを用意します。 今回は疑似個人情報データ生成サービスを使って生成したExcelファイルに対してピボットテーブルを作成します。 ピボットテーブルの作り方は検索して調べてください。

作成したExcelファイルは以下の通りになっているとします。

  • ピボットテーブルは pivot シートに含まれている
  • データは dummy_data シートに含まれている
  • Excelファイルの名前はPivotTemplate.xlsx

Pythonでピボットテーブルの値の範囲を設定する

Pythonのプログラムで以下の2つを合わせます。

  • PivotTemplate.xlsxに含まれているピボットテーブル
  • 別途作成したデータシート

合わせるためのプログラムは以下のとおりです。

import openpyxl

# データシートを作成する
new_wb = openpyxl.Workbook()
ws = new_wb.create_sheet('data')

# ... wsにデータを追加する ...


def add_pivot(cache_id, data_sheet, destination_book):
    template_wb = openpyxl.load_workbook('PivotTemplate.xlsx')

    pivot_sheet = template_wb['pivot']  # ピボットテーブルが含まれたシート
    pivot = pivot_sheet._pivots[0]
    pivot.name = data_sheet.title + '_pivot'  # ピボットテーブルに名前を付ける
    pivot.cacheId = cache_id  # IDが重複するとエラーになる
    pivot.cache.refreshOnLoad = True  # 読み込み時にピボットテーブルを更新
    source = pivot.cache.cacheSource.worksheetSource
    print(source)

    print(data_sheet.dimensions)
    print(data_sheet.title)
    source.ref = data_sheet.dimensions
    source.sheet = data_sheet.title

    pivot_sheet = destination_book.create_sheet(pivot.name)
    pivot_sheet.add_pivot(pivot)
    template_wb.close()


add_pivot(1000, ws, new_wb)
new_wb.save('PivotSample.xlsx')

まず、はじめにdataという名前のシートをnew_bookに追加して、 このdataシート(ws)にデータを用意しています。

次に、add_pivot関数でピボットテーブルを含むシートを追加しています。 cache_idはピボットテーブルのIDです。 他のピボットテーブルと重複しない値にします。 ここでは重複しないであろう値として適当に1000にしています。 data_sheetはデータを含むシートです。 ここでは最初の段階で作成したdataシート(ws)を指定しています。 destination_bookはピボットテーブルを含むシートを追加する先のブックです。 ここではnew_wbを指定しています。

add_pivot関数で行っている内容を説明します。 まず、テンプレートとなるPivotTemplate.xlsxファイルを開き、ピボットテーブルが含まれるシートからピボットテーブルの情報を取得します。 そして、このピボットテーブルの情報(pivot)を変更しています。 nameは設定しなくて構いませんが、ここではシート名に基づいて名付けています。 cacheIdは他のピボットテーブルと重複しない値を設定する必要があります。 refreshOnLoadTrueに設定しておくとファイルを開いたときにピボットテーブルが更新されます。 次に、ピボットテーブルが参照するデータの範囲を変更します。 refにデータの範囲、sheetにシート名を設定することで行えます。 data_sheetのデータ範囲(data_sheet.dimensions)とシート名(data_sheet.title)を設定しています。 最後に、新規シートにピボットテーブルを追加します。

以上を行ってファイルを保存すれば、ピボットテーブルを含んだファイルが作成されます。

Unity Android Plugin の手間を軽減する管理方法(改)

概要

Unity Android Plugin の解説記事では JAR, AAR ファイルを手動コピーする例が多く見受けられます。 手動コピーでは依存ライブラリのバージョンを管理しづらくなるため、出来る限り手動コピーは避けたいです。 この記事では Gradle の Local リポジトリを経由して Android Plugin を Unity に公開する方法を説明します。

目次

確認環境

  • macOS 10.15.7
  • Android
    • Android Studio 4.1.2
    • minSdkVersion 19
    • buildToolsVersion 30.0.3
    • Gradle 6.5
  • Unity 2019.4.18f1
  • Rider 2020.3.2

参考情報

解説

Android Plugin (AAR) をビルドする

Android Library モジュールを作成し、モジュールの build.gradle を以下の様に記述します。 この例ではモジュール名を sample にしています。

plugins {
    id 'com.android.library'
    id 'kotlin-android'
    id 'maven-publish'
}

ext {
    groupId = "com.example"
    versionName = "1.0"
    unityVersion = "2019.4.18f1"
    unityJar = "/Applications/Unity/Hub/Editor/$unityVersion/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar"
}

group = groupId
version = versionName

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    compileOnly files(unityJar)
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

// see: https://developer.android.com/studio/build/maven-publish-plugin?hl=ja
afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release
            }
            debug(MavenPublication) {
                from components.debug
                artifactId = "${project.name}-debug"
            }
        }
    }
}

ポイントは以下の2点です。

  • Maven Publish Plugin を使用する
  • Unity の classes.jar をコピーせずに直接指定する

1つめのポイントとして、 Maven Publish Plugin を使用するために pluginsid 'maven-publish' を追加しています。 また、afterEvaluate 内に publishing の設定を記述しています。 この例の設定では、ビルドバリアント(debug, release)毎に AAR ファイルを生成します。 具体的には、sample-1.0.aar (release) ファイルと sample-debug-1.0.aar (debug) ファイルが生成されます。

2つめのポイントとして、 compileOnly files(unityJar) によって Unity の classes.jar を参照しています。 Windows の場合は unityJar のパスを適切なパスに変更してください。 Unity のバージョンを変える場合には unityVersion を変更します。 implementation ではなく compileOnly にしている理由は、 Unity から AAR ファイルを参照する際に Unity の classes.jar が重複して参照されることを防ぐためです。 implementation にすると Unity でビルドする際にエラーになります。

ビルド設定は以上です。

ビルドを実行する前に、今回は以下のサンプルプログラムを作成します。

package com.example.unity.sample

import com.unity3d.player.UnityPlayer

class KotlinObject {

    fun getActivityName(): String = UnityPlayer.currentActivity.localClassName

}

Android Plugin をビルドし、Local リポジトリに保存する

以下のコマンドを Terminal で実行するとビルドが行われ、その結果が Local リポジトリに保存されます。

project % ./gradlew publishToMavenLocal

Gradle のデフォルトでは、Local リポジトリ~/.m2/repository になっています。 この repository ディレクトリに以下のファイルといくつかの関連ファイルが作成されます。

  • com/example/sample/1.0/sample-1.0.aar
  • com/example/sample-debug/1.0/sample-debug-1.0.aar

以上で Android Plugin の作成と公開は終了です。

アプリに Android Plugin への依存を追加する

Unity で Project Settings > Player > Android > Publishing Settings > Build と辿っていくと以下の設定が表示されます。

f:id:nosix:20210225172356p:plain

画像の様に以下の2箇所にチェックを付けます。

  • Custom Main Gradle Template
  • Custom Base Gradle Template

チェックを付けることで Assets/Plugins/Android ディレクトリに以下の2つのファイルが作成されるはずです。 (作成されない場合は Build を実行してみると作成されると思います。)

  • mainTemplate.gradle
  • baseProjectTemplate.gradle

この2つのファイルを編集します。

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN

apply plugin: 'com.android.library'
**APPLY_PLUGINS**

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.example:sample:1.0'
**DEPS**}

// ... 以下省略
// ... 以上省略
    repositories {**ARTIFACTORYREPOSITORY**
        google()
        jcenter()
        mavenLocal()
        flatDir {
            dirs "${project(':unityLibrary').projectDir}/libs"
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

mainTemplate.gradle には implementation 'com.example:sample:1.0' を追加しています。 これは groupId:artifactId:version の形式になっており、groupId と version は Android Plugin の build.gradle で指定した値になっています。 artifactId はモジュール名が使われており、今回は sample (release) か sample-debug (debug) です。

baseProjectTemplate.gradle には mavenLocal()repositories に追加しています。 ファイル内に repositories が2箇所あるので注意して下さい。 buildscript 内の repositories はビルドスクリプト(Gradle)が使用するライブラリのリポジトリを指定しています。 buildscript ではない方の repositories は Android アプリをビルドする際に使用するライブラリのリポジトリ指定です。 ここに Local リポジトリを参照する指定 mavenLocal() を追加します。

アプリをビルドする

今回は以下の Script を作成して動作確認をしました。

using UnityEngine;
using UnityEngine.UI;

public class AndroidExecutor : MonoBehaviour
{
    [SerializeField] private Text result;

    void Start()
    {
        using (var o = new AndroidJavaObject("com.example.unity.sample.KotlinObject"))
        {
            result.text = o.Call<string>("getActivityName");
        }
    }
}

Androidバイスを PC に接続するか Android Emulator を起動した上で、 Build Settings で Android Platform に Switch して、 Run Device にデバイスか Emulator を指定します。 File メニューの Build And Run を選択すると APK ファイルのファイル名を訊ねられないので、 Build Settings の Build And Run は使わずに閉じて File メニューから実行しています。

実行するとText に以下の様に表示されます。

f:id:nosix:20210225175148p:plain

Sharing

概要

NRSDK (Nreal Light の Software Developer Kit) の Observer View を理解するために、 サンプルとして提供されている Sharing (Scene) のソースコードを解読してみます。 個人的な読解結果であるため誤りが含まれている可能性があることをご了承ください。

目次

確認環境

  • NRSDK 1.2.1

解説

Sharing (Scene) の Hierarchy とそこで使われている Scripts を網羅して確認しました。

Sharing 機能は NetWorkSession (Prefab) を Hierarchy に追加して使用します。 NetWorkSession (Prefab) には SharingManager (Component) が設定されており、SharingManager のプロパティでは Player と ObjectPool を設定します。 Player, ObjectPool のいずれにも NetworkBehaviour (Component) を設定した Prefab を設定します。

ObjectPool には共有データを定義した Prefab を登録します。 共有データを NetObject と呼ぶとすると、他ユーザーから受け取る NetObject と自分から送信する NetObject を登録します。 Player には自分が生成する NetObject を定義した Prefab を登録します。

ObjectPool には複数の Prefab が登録されます。 登録する際には NetworkBehaviour を継承したクラスを作成し、作成したクラスを Component にもつ Prefab を作成します。 ObjectPool は NetworkBehaviour を継承したクラスのクラス名を Key、Prefab を Value とする辞書を保持します。 Key は NetObjectInfo.Key として使われます。

Player には単一の Prefab が登録されます。 ObjectPool に登録した Prefab のいずれか 1 つを登録します。 ObjectPool に複数の Prefab を登録するケースは理解できませんでした。 SharingManager は NetWorkSession のシングルトンインスタンスを使用しているため、SharingManager を複数使用してはいけないのではないかと思います。 そのため、Player に登録される Prefab は 1 つだけです。

NetworkBehaviour を継承したクラスの例として TestNetBehaviour が用意されています。 TestNetBehaviour では、共有データを管理します。 共有データはシリアライズして送受信します。 SynObject を継承させたクラスを独自に定義すれば任意のデータを共有できます。 標準では以下の SynObject を継承したクラスが用意されています。

  • SynTransform
  • SynInt
  • SynVector2
  • SynVector3
  • SynQuaternion

また、NetworkBehaviour.IsOwner は自身のセッションで作成した NetObject である場合には true になります。 つまり、Player に指定して生成されたインスタンスでは IsOwner が true になります。

Hierarchy

  • Sharing

    • Main Camera
      • [C] Camera
      • [C] Audio Listener
    • Directional Light
      • [C] Light
    • NetWorkSession (Prefab)
  • NetWorkSession (Prefab) (in Scripts/Sharing/Prefabs)

  • Player (Prefab) (in Demos/Sharing/Prefabs)

Scripts

  • SharingManager (in Scripts/Sharing/Scripts)

    • MonoBehaviour を継承している
    • Inspector の設定項目
      • Player : NetworkBehaviour (Demos/Sharing/Prefabs/Player)
      • Object Pool : NetWorkObjectPool (Scripts/Sharing/Prefabs/NetWorkObjectPool)
    • OnEnable
    • Start
      • Wraper.Initialize
      • ObjectPool.Init
      • NetObjectManager.Init
      • NetWorkSession.SearchLocalServer
        • LocalServerSearcher.Search
          • IP 255.255.255.255 port 1989 UDP に RequestForServerIP を送信する
          • IP:port を受信する
          • LocalServerSearcher.m_LocalServer に IPEndPoint(IP, port) を設定する
    • NetWorkSession.OnGetServerIP
      • EasyClient.ConnectAsync(EndPoint)
    • NetWorkSession.OnClientConnected
      • MsgGroup.LoginEvent.Request(GUID)
      • SharingManager.OnNetConnected
    • OnNetConnected
      • Player が設定されている場合は CreateNetObjRequest(NetworkBehaviour) を呼び出す
    • CreateNetObjRequest
      • MsgGroup.CreateNetObjectEvent.Request(NetworkBehaviour.GetType().Name)
      • OnCreateNetObjectResp
        • NetObjectManager.Create
          • NetWorkObjectPool から NetObjectInfo.Key で NetworkBehaviour を検索する
          • NetworkBehaviour が見つかったら NetworkBehaviour を Instantiate して、Initialize を呼び出す
            • NetObjectInfo.Owner が NetWorkSession.GUID と一致したら、NetworkBehaviour.IsOwner を true にする
          • Instantiate で生成された NetworkBehaviour の GameObject の name を NetObjectInfo.Key に設定する
        • SynContextRequest
          • MsgGroup.SynContextEvent.Request();
    • SynContextRequest
      • MsgGroup.SynContextEvent.Request()
      • OnSynContextResp
        • NetObjectManager.SynObjects
          • 受け取った NetObjectInfo 毎に以下を行う
            • NetObjectInfo.Identify 毎に NetObjectManager.Create を呼び出して NetworkBehaviour インスタンスを生成する
              • NetObjectInfo.Identify が NetworkBehaviour インスタンスの識別子になる
              • 既にインスタンスが生成されている場合は NetObjectManager.Create を呼び出さない
    • SynDataRequest
      • MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Others)
      • OnSynDataResp
        • NetMsgType が SynValue なら NetworkBehaviour.DeserializeData
        • NetMsgType が Commond なら NetworkBehaviour.ReplyCommond
  • MsgGroup

    • SynContextEvent
    • CreateRoomEvent
    • CreateNetObjectEvent
    • DestroyNetObjectEvent
    • GetRoomStateEvent
    • JoinRoomEvent
    • LeaveRoomEvent
    • LoginEvent
    • DownLoadCommondListEvent
    • UpLoadCommondListEvent
    • SynDataEvent
  • TestNetBehaviour (in Demos/Sharing/Scripts)

    • NetworkBehaviour を継承している
    • Inspector の設定項目
      • SynTransform
      • SynInt
      • SynVector2
      • SynVector3
      • SynQuaternion
    • Update
      • IsOwner が true ならば SynObject を更新する
      • Space キー押下時、MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Everyone))
  • NetworkBehaviour (in Scripts/Sharing/Scripts/RunTime)

    • Updater コルーチン
      • MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Everyone))
  • NetWorkObjectPool (in Scripts/Sharing/Prefabs)

    • ScriptableObject を継承している
    • Inspector の設定項目
      • NetObjects : List
    • NetworkBehaviour (のサブクラス) の Type Name をKey、NetworkBehaviour (のサブクラス) を Component に持つ GameObject を Value とした Dictionary を持つ