MRが楽しい

MRやVRについて学習したことを書き残す

自作のQuestアプリをMetaストアで公開する手順をまとめる

本記事では自作のQuestアプリをMetaストアで公開する手順をまとめる手順の記事をまとめます。

Metaストア

QuestデバイスではMetaストアを通じて様々なアプリをダウンロードして体験することができます。
Metaストアに個人開発のアプリを公開するには幾つかの申請作業が必要になります。
www.meta.com

公開までの大きな流れは以下の通りです。
1.アカウントの設定とアプリ枠の作成
2.アプリパッケージの作成
3.ストア情報の入力と申請
4.審査とリリース

ストアへの公開手順

以下の記事で手順を記事にまとめています。

アカウントの設定とアプリ枠の作成

1.ダッシュボードのログイン
bluebirdofoz.hatenablog.com

2.組織の認証
bluebirdofoz.hatenablog.com

3.契約文書の署名と関心事項の入力
bluebirdofoz.hatenablog.com

4.プラットフォームの要件
bluebirdofoz.hatenablog.com

5.アプリの年齢層の自己証明とシェア設定
bluebirdofoz.hatenablog.com

6.プライバシーポリシー
bluebirdofoz.hatenablog.com

7.アプリの出品タイプと価格の設定
bluebirdofoz.hatenablog.com

アプリパッケージの作成

8.アプリビルドのアップロード
bluebirdofoz.hatenablog.com

9.アプリビルドの設定
bluebirdofoz.hatenablog.com

ストア情報の入力と申請

10.アプリレビューの申請
bluebirdofoz.hatenablog.com

11.アプリメタデータの名前と説明文
bluebirdofoz.hatenablog.com

12.アプリメタデータのスペック
bluebirdofoz.hatenablog.com

13.アプリメタデータの詳細
bluebirdofoz.hatenablog.com

14.アプリメタデータのアセット
bluebirdofoz.hatenablog.com

15.アプリメタデータのコンテンツレーティング
bluebirdofoz.hatenablog.com

16.アプリレビューの申請
bluebirdofoz.hatenablog.com

審査とリリース

17.アプリレビューの差し戻し
bluebirdofoz.hatenablog.com

18.アプリのリリース
bluebirdofoz.hatenablog.com

参考ページ

公式ドキュメント
developers.meta.com

ファイルまたはフォルダ作成時にWindowsで利用不可なファイル名かチェックする

本日はC#の小ネタ枠です。
ファイルまたはフォルダ作成時にWindowsで利用不可なファイル名かチェックする方法です。

Windowsのファイル名の命名規則

Windowsのファイル名には利用不可能な予約文字や予約名が存在します。
これらを含むファイル名は通常利用できず、何らかの要因で誤って利用した場合問題が発生することがあります。
learn.microsoft.com

例えばWindowsで利用不可な予約文字は以下の通りです。

< (より小さい)
> (より大きい)
: (コロン)
" (二重引用符)
/ (スラッシュ)
\ (円記号)
| (縦棒またはパイプ)
? (疑問符)
* (アスタリスク)

利用不可な予約名は以下の通りです。

CON、PRN、AUX、NUL、
COM0、COM1、COM2、COM3、COM4、COM5、COM6、COM7、COM8、COM9、COM¹、COM²、COM³、
LPT0、LPT1、LPT2、LPT3、LPT4、LPT5、LPT6、LPT7、LPT8、LPT9、LPT¹、LPT²、LPT³

サンプルスクリプト

これらの利用不可なファイル名かチェックを行う以下のサンプルスクリプトを作成しました。
・FileNameCheckTest2.cs

using System;
using System.IO;
using UnityEngine;

public class FileNameCheckTest2 : MonoBehaviour
{
    /// <summary>
    /// 作成するファイル名
    /// </summary>
    [SerializeField]
    private string fileName = default;
    
    private string DocumentFolderPath => System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    
    /// <summary>
    /// ファイル名で利用不可なWindowsのファイルシステムの予約文字
    /// </summary>
    private static readonly char[] InvalidFileNameCharsWindows = new Char[]
    {
        '<', '>', ':', '"', '/', '\\', '|', '?', '*'
    };

    /// <summary>
    /// ファイル名で利用不可なWindowsのファイルシステムの予約ファイル名
    /// </summary>
    private static readonly string[] InvalidFileNameWindows = new string[]
    {
        "CON", "PRN", "AUX", "NUL",
        "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³",
        "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³"
    };
    
    // ファイルを作成する
    [ContextMenu("CreateFile")]
    public void CreateFile()
    {
        // ファイル名に利用不可なファイルシステムの予約文字が含まれているか確認する
        if (fileName.IndexOfAny(InvalidFileNameCharsWindows) >= 0)
        {
            Debug.LogError($"Invalid FileName: {fileName}");
            return;
        }
        
        // 予約ファイル名のチェックは拡張子を除いたファイル名で行う
        // 英字の大文字小文字は区別しないため、大文字に変換してから検索する
        var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpper();
        
        // ファイル名が利用不可なファイルシステムの予約ファイル名か確認する
        if (Array.Exists(InvalidFileNameWindows, x => x == fileNameWithoutExtension))
        {
            Debug.LogError($"Invalid FileName: {fileName}");
            return;
        }
        
        // ファイルが存在しない場合はファイルを作成する
        var fileFolderPath = Path.Combine(DocumentFolderPath, "ScriptTest");
        var newFilePath = Path.Combine(fileFolderPath, "newFile.txt");
        if (!File.Exists(newFilePath)) File.Create(newFilePath).Close();
        Debug.Log($"Create File: {newFilePath}");
    }
}

利用不可なファイル名を指定した場合、エラーメッセージを返してファイルを作成しません。

nulフォルダまたはファイルを削除する

本日はWindowsの小ネタ枠です。
nulフォルダまたはファイルを削除する方法についてです。

nul

Windowsのフォルダまたはファイル名にはいくつかの使用不可能な予約名が存在します。
nulフォルダまたはファイル名も予約名の1つです。
learn.microsoft.com

CON、PRN、AUX、NUL、COM0、COM1、COM2、COM3、COM4、COM5、COM6、COM7、COM8、COM9、COM¹、COM²、COM³、LPT0、LPT1、LPT2、LPT3、LPT4、LPT5、LPT6、LPT7、LPT8、LPT9、LPT¹、LPT²、LPT³。 また、これらの名前の直後に拡張子を付けないでください。たとえば、NUL.txt と NUL.tar.gz はどちらも NUL と同じです。

通常nulという名前のフォルダは作成できませんが、スクリプト実行などの要因で作成されてしまうことがあります。

一度作成してしまったnulフォルダは通常の方法ではアクセスできず削除できません。

直接アクセスのプレフィックス

予約名のファイルにアクセスする場合はファイルシステムを介さないように直接アクセスのプレフィックスを用いてアクセスします。
learn.microsoft.com

\\.\

コマンドプロンプトを起動します。

以下のようにnulフォルダのパスをプレフィックスを付けて指定して名前変更コマンドを実行します。

rename \\.\D:\Unity\TestProject\ScriptTest2021\ScriptTest\nul foldername

以下の通りフォルダの名前をnulから別の名前に変更できました。

予約名でなくなれば通常通りファイルシステムからアクセス可能です。

参考ページ

www.web-dev-qa-db-ja.com

Path.Combineで文字列の順序を誤った場合の動作

本日はC#の小ネタ枠です。
Path.Combineで文字列の順序を誤った場合の動作の注意点についてです。

Path.Combine

Path.Combine関数は文字列の配列を結合してパスを生成します。
learn.microsoft.com

string[] paths = {@"d:\archives", "2001", "media", "images"};
string fullPath = Path.Combine(paths);
// パス:d:\archives\2001\media\images が生成される

途中にルートパスが含まれた場合の動作

Path.Combineを利用する際は配列の順番に注意が必要です。
配列の途中にルートパスが含まれていた場合、結合操作がリセットされます。

string[] paths = {"2001", "media", @"d:\archives", "images"};
string fullPath = Path.Combine(paths);
// パス:d:\archives\images が生成される

途中のルートパスによるリセットが発生してもPath.Combineはエラーを返しません。
意図しないパスが生成された場合は配列の順序を確認してみる必要があります。

サンプルスクリプト

以下のサンプルスクリプトを作成して動作を確認してみます。
・PathCombineTest.cs

using UnityEngine;

public class PathCombineTest : MonoBehaviour
{
    [ContextMenu("RunTest")]
    public void RunTest()
    {
        // 複数のディレクトリ名を結合する
        string[] paths = new string[] { "C:\\", "Alpha", "Beta", "Gamma", "Delta", "Epsilon" };
        var fullpath = System.IO.Path.Combine(paths);
        Debug.Log($"Path.Combine: {fullpath}");
    }
    
    [ContextMenu("RunTest2")]
    public void RunTest2()
    {
        // 途中にルートパス名を含む複数のディレクトリ名を結合する
        string[] paths = new string[] { "Alpha", "Beta", "Gamma", "C:\\", "Delta", "Epsilon" };
        var fullpath = System.IO.Path.Combine(paths);
        Debug.Log($"Path.Combine: {fullpath}");
    }
}

以下の通り順番を誤って途中にルートパス名を含む配列の場合、その前の文字列が消去されて結合されています。

C#でディレクトリの生成/削除/移動を行う

本日はC#の小ネタ枠です。
C#ディレクトリの生成/削除/移動を行う方法です。

System.IO.Directory

System.IO.Directoryの各種メソッドを利用してディレクトリの生成/削除/移動を行うことができます。
learn.microsoft.com

ディレクトリの生成にはCreateDirectoryメソッドを利用します。

var newDirectory = Path.Combine(DocumentFolderPath, "new");
System.IO.Directory.CreateDirectory(newDirectory);

ディレクトリの削除にはDeleteメソッドを利用します。

var targetDirectory = Path.Combine(DocumentFolderPath, "target");
System.IO.Directory.Delete(targetDirectory);

ディレクトリの移動にはMoveメソッドを利用します。

var targetDirectory = Path.Combine(DocumentFolderPath, "target");
var moveDirectory = Path.Combine(DocumentFolderPath, "move");
System.IO.Directory.Move(originDirectory, moveDirectory);

サンプルスクリプト

マイドキュメントフォルダでディレクトリの生成/削除/移動を行う以下のサンプルスクリプトを作成しました。
・DirectoryControl.cs

using System.IO;
using UnityEngine;

public class DirectoryControl : MonoBehaviour
{
    private string DocumentFolderPath => System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    private string documentFolderPath;
    
    // フォルダを作成する
    [ContextMenu("CreateFolder")]
    public void CreateFolder()
    {
        // ディレクトリが存在しない場合はディレクトリを作成する
        var newDirectory = Path.Combine(DocumentFolderPath, "AlphaDirectory");
        if (!Directory.Exists(newDirectory)) System.IO.Directory.CreateDirectory(newDirectory);
        Debug.Log($"Create Folder: {newDirectory}");
    }
    
    // フォルダを削除する
    [ContextMenu("DeleteFolder")]
    public void DeleteFolder()
    {
        // ディレクトリが存在している場合はディレクトリを削除する
        var targetDirectory = Path.Combine(DocumentFolderPath, "AlphaDirectory");
        if (Directory.Exists(targetDirectory)) System.IO.Directory.Delete(targetDirectory);
        Debug.Log($"Delete Folder: {targetDirectory}");
    }
    
    // フォルダを移動(名前変更)する
    [ContextMenu("MoveFolder")]
    public void MoveFolder()
    {
        // ディレクトリが存在している場合はディレクトリを移動する
        var originDirectory = Path.Combine(DocumentFolderPath, "AlphaDirectory");
        var moveDirectory = Path.Combine(DocumentFolderPath, "BetaDirectory");
        if (Directory.Exists(originDirectory) && !Directory.Exists(moveDirectory))
            System.IO.Directory.Move(originDirectory, moveDirectory);
        Debug.Log($"Rename Folder: {originDirectory}");
    }
}

各メソッドを実行するとマイドキュメントフォルダでディレクトリの生成/削除/移動を実行できます。

UnityでPCのドキュメントフォルダやピクチャフォルダにアクセスする

本日はUnityの小ネタ枠です。
UnityでPCのドキュメントフォルダやピクチャフォルダにアクセスする方法です。

System.Environment.SpecialFolder

System.Environment.SpecialFolder列挙型を使って特別なフォルダへのディレクトリパスを取得可能です。
learn.microsoft.com

一例として以下のようなユーザディレクトリのパスを取得できます。

識別子 対象パス
MyDocuments マイドキュメントフォルダ
MyPictures マイピクチャフォルダ
MyVideos マイビデオフォルダ
MyMusic マイミュージックフォルダ
// マイドキュメントフォルダのパスの取得例
var folderType = System.Environment.SpecialFolder.MyDocuments;
var documentFolderPath = System.Environment.GetFolderPath(folderType);

サンプルスクリプト

以下のコンテンツメニューからパス取得の動作確認を行うサンプルスクリプトを作成しました。
・SpecialFolderAccessor.cs

using UnityEngine;

public class SpecialFolderAccessor : MonoBehaviour
{
    // ドキュメントフォルダのパスを取得する
    [ContextMenu("AccessDocumentFolder")]
    public void AccessDocumentFolder()
    {
        // ドキュメントフォルダのパスを取得
        Debug.Log(GetSpacialFolderPath(System.Environment.SpecialFolder.MyDocuments));
    }
    
    // ピクチャーフォルダのパスを取得する
    [ContextMenu("AccessPictureFolder")]
    public void AccessPictureFolder()
    {
        // ピクチャーフォルダのパスを取得
        Debug.Log(GetSpacialFolderPath(System.Environment.SpecialFolder.MyPictures));
    }
    
    // ビデオフォルダのパスを取得する
    [ContextMenu("AccessVideoFolder")]
    public void AccessVideoFolder()
    {
        // ビデオフォルダのパスを取得
        Debug.Log(GetSpacialFolderPath(System.Environment.SpecialFolder.MyVideos));
    }
    
    // ミュージックフォルダのパスを取得する
    [ContextMenu("AccessMusicFolder")]
    public void AccessMusicFolder()
    {
        // ミュージックフォルダのパスを取得
        Debug.Log(GetSpacialFolderPath(System.Environment.SpecialFolder.MyMusic));
    }
    
    private string GetSpacialFolderPath(System.Environment.SpecialFolder specialFolder)
    {
        // 特定のフォルダのパスを取得
        string specialFolderPath = System.Environment.GetFolderPath(specialFolder);
        return specialFolderPath;
    }
}

UnityEditor上でスクリプトを実行すると、以下の通り実行PCの各種マイフォルダへのパスが取得できました。

UniRxのFirstやTakeを使ってボタンの押下をawaitを使って待機する その2(待機をキャンセルする)

本日は UniRx の小ネタ枠です。
UniRxのFirstやTakeを使ってボタンの押下をawaitを使って待機する方法を記事にします。

前回記事

以下の前回記事の続きです。
bluebirdofoz.hatenablog.com

ToUniTask

ToUniTaskを利用すると様々な型をUniTask型に変換できます。
Firstの返り値のIObservableをUniTaskに変換して待機することでキャンセルトークンを使ったキャンセルを行うことができます。

サンプルスクリプト

前回のサンプルスクリプトを改修してボタンオブジェクトが無効化された場合は待機をキャンセルするように修正しました。
・ButtonFirstAwaitCancelTest.cs

using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class ButtonFirstAwaitCancelTest : MonoBehaviour
{
    /// <summary>
    /// 待機ボタン
    /// </summary>
    private Button Button => _button ? _button : (_button = GetComponent<Button>());
    private Button _button = default;

    /// <summary>
    /// ボタン用キャンセルトークン
    /// </summary>
    private CancellationTokenSource cancellationTokenSource = default;

    /// <summary>
    /// ボタン有効時の処理
    /// </summary>
    void OnEnable()
    {
        // ボタン待機を開始する
        WaitButton().Forget();
    }
    
    /// <summary>
    /// ボタン無効時の処理
    /// </summary>
    void OnDisable()
    {
        // オブジェクトが無効化された場合はボタンの待機をキャンセルする
        cancellationTokenSource?.Cancel();
    }
    
    private async UniTask WaitButton()
    {
        cancellationTokenSource?.Cancel();
        cancellationTokenSource = new CancellationTokenSource();
        
        Debug.Log("ボタンが押されるまで待機します");
        
        // ボタンが押下されたらFirstがOnCompletedを発行して待機を完了する
        var result = await Button.OnClickAsObservable().First()
            .ToUniTask(cancellationToken: cancellationTokenSource.Token) // UniTaskに変換してキャンセルトークンを設定する
            .SuppressCancellationThrow(); // キャンセル時の例外をIsCanceledプロパティで取得できるようにする

        if (result.IsCanceled)
        {
            Debug.Log("ボタンの待機がキャンセルされました");
        }
        else
        {
            Debug.Log("ボタンが押されました");
        }
    }
}

サンプルシーンのButtonオブジェクトにスクリプトを設定して準備完了です。

シーンを再生するとFirstによるボタン押下の待機が開始します。

ボタンを押下するとawaitが解除されてボタン押下ログが表示されます。

ボタンが無効化されるとawaitがキャンセルトークンで中断されてキャンセルログが表示されます。