MessagePipeを使って簡単なアクションゲームを作る【MessagePipe,Unity】

最近注目を集めている MessagePipeを使ってみようと思いいろいろ調べて,自分なりにアクションゲームを作ってみました! (ゲームといってもMessagePipeを使うことが目的なので,ゲームにすらなっていないです)

成果物

Demo

キーボードのAを押すことで,左側のUnity chanに攻撃することができます
また,左のUnity chanからは定期的に(5秒後ごと)攻撃を受けます どちらかのHPが0になったら勝敗モーションに移行後,ゲーム終了します

リポジトリ

リポジトリはpublicにしていますので,詳細はリポジトリをご確認ください

github.com

WebGL

ayutaz.github.io

MessagePipeとは?

公式のGithubページより

MessagePipe is a high-performance in-memory/distributed messaging pipeline for .NET and Unity. It supports all cases of Pub/Sub usage, mediator pattern for CQRS, EventAggregator of Prism(V-VM decoupling), etc.

  • Dependency-injection first
  • Filter pipeline
  • better event
  • sync/async
  • keyed/keyless
  • buffered/bufferless
  • broadcast/response(+many)
  • in-memory/distributed

MessagePipe is faster than standard C# event and 78 times faster than Prism's EventAggregator.

簡単に言うと,型でイベントの発行等を管理するライブラリみたいです(理解が怪しい)

環境

© Unity Technologies Japan/UCL

(注) VContainerとMessagePipeを使うときは,こちらのMessagePipe.VContainerを入れないと動かない?気がします

f:id:ayousanz:20210601161227p:plain

MessagePipeでの値の受け渡し方

関係する部分のみ記載しています.実際に使用方法は, #MessagePipe部分のコード もしくはリポジトリをご確認ください

イベントPublish側

//使用するPublisherとDisposeを定義
[Inject] private IPublisher<PlayerData> PlayerData { get; set; }
private IDisposable _disposable;

//イベントの発行

PlayerData.Publish(new PlayerData(){AttackValue = value,IsLose = isLose});

イベントSubscribe側

[Inject] private ISubscriber<EnemyData> EnemyData { get; set; }
var d = DisposableBag.CreateBuilder();
            EnemyData.Subscribe(e =>
            {
                playerHp -= e.AttackValue;
                isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                playerHpText.text = $"enemy hp:{playerHp}";

                if (playerHp <= 0)
                {
                    _animator.SetBool("IsLose",true);
                    PublishData(0,true);
                }
            }).AddTo(d);

            _disposable = d.Build();

MessagePipe部分のコード

Player.cs

using System;
using MessagePipe;
using UnityEngine;
using UnityEngine.UI;
using VContainer;

namespace Model
{
    public class Player : MonoBehaviour
    {
        [SerializeField] private int playerHp;
        [SerializeField] private int attackValue;
        [SerializeField] private Text playerHpText;
        [Inject] private ISubscriber<EnemyData> EnemyData { get; set; }
        [Inject] private IPublisher<PlayerData> PlayerData { get; set; }
        private IDisposable _disposable;

        private Animator _animator;
        private bool isPlay;

        private void Awake()
        {
            playerHpText.text = $"player hp:{playerHp}";
            _animator = GetComponent<Animator>();
        }

        private void Start()
        {
            var d = DisposableBag.CreateBuilder();
            EnemyData.Subscribe(e =>
            {
                playerHp -= e.AttackValue;
                isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                playerHpText.text = $"enemy hp:{playerHp}";

                if (playerHp <= 0)
                {
                    _animator.SetBool("IsLose",true);
                    PublishData(0,true);
                }
            }).AddTo(d);

            _disposable = d.Build();
        }


        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.A) && !isPlay)
            {
                Debug.Log("プレイヤーが攻撃");
                _animator.SetTrigger("IsAttack");
                PublishData(3,false);
            }
        }

        private void PublishData(int value, bool isLose)
        {
            PlayerData.Publish(new PlayerData(){AttackValue = value,IsLose = isLose});
        }

        private void OnDestroy()
        {
            _disposable?.Dispose();
        }
    }
    
}

Enemy.cs

using System;
using MessagePipe;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using VContainer.Unity;

namespace Model
{
    public class Enemy : MonoBehaviour,IStartable
    {
        [SerializeField] private int enemyHp;
        [SerializeField] private Text enemyHpText;
        private bool _isPlay;

        [Inject] private ISubscriber<PlayerData> PlayerData { get; set; }
        [Inject] private IPublisher<EnemyData> EnemyData { get; set; }

        private Animator _animator;
        private IDisposable _disposable;

        private void Awake()
        {
            enemyHpText.text = $"enemy hp:{enemyHp}";
            _animator = GetComponent<Animator>();
        }

        public void Start()
        {
            var d = DisposableBag.CreateBuilder();
            PlayerData.Subscribe(e =>
            {
                enemyHp -= e.AttackValue;
                _isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                enemyHpText.text = $"enemy hp:{enemyHp}";

                if (enemyHp <= 0)
                {
                    PublishData(0,true);
                    _animator.SetBool("IsLose",true);
                }
            }).AddTo(d);

            _disposable = d.Build();

            Observable.Interval(TimeSpan.FromSeconds(5f))
                .Where(_=>!_isPlay)
                .Subscribe(_ =>
                {
                    Debug.Log("敵が攻撃");
                    _animator.SetTrigger("IsAttack");
                    PublishData(3,false);
                }).AddTo(this);
        }
        
        private void PublishData(int value, bool isLose)
        {
            EnemyData.Publish(new EnemyData(){AttackValue = value,IsLose = isLose});
        }

        private void OnDestroy()
        {
            _disposable?.Dispose();
        }
    }
}

DI(VContainer)

using MessagePipe;
using Model;
using VContainer;
using VContainer.Unity;


public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        var options = builder.RegisterMessagePipe();
        builder.RegisterMessageBroker<PlayerData>(options);
        builder.RegisterMessageBroker<EnemyData>(options);

    }
}

受け渡しているデータクラス

namespace Model
{
    public class Data
    {
        public int AttackValue;
        public bool IsLose;
    }

    public class PlayerData : Data
    {
        
    }

    public class EnemyData : Data
    {
        
    }
}

参考にしたサイト

piffett.hateblo.jp

qiita.com

OSS(SaveGameFree)を使ってUnityでセーブ・ロード機能を実装する【Unity,SaveGameFree】

今回は,ゲーム制作で無料ものでセーブ・ロード機能を実装してほしいと要望があったので実装してみました

使用したライブラリ

github.com

有料版もありましたので,気になる方は確認してみてください

assetstore.unity.com

ライブラリの機能(GithubPageより)

The below features made Save Game Free excellent:

セーブ・ロード機能の実装

以下のスクリプトでは,以下の機能を実装しています

  • カスタムクラスのデータをデーブ
  • パスワード付きデータで保存
  • デーブデータの暗号化
using _Project.Scripts.Interface;
using BayatGames.SaveGameFree;

namespace _Project.Scripts.Model
{
    public class SaveHandler : ISaveData
    {
        public void Save(SaveDataClass saveDataClass)
        {
            SaveGame.Encode = true;
            SaveGame.Save("SaveData",saveDataClass,"pass");
        }

        public SaveDataClass Load()
        {
            return SaveGame.Load<SaveDataClass>("SaveData",true,"pass");
        }
    }
}

備考

セーブデータの保存場所

デフォルトでは,Application.persistentDataPath に保存されるみたいです

各種プラットフォームのパスの詳細

qiita.com

データの暗号化

データの暗号化

暗号化は SaveGame.Encode = true; で有無を変更できるみたいです

暗号化されているのかの確認

まず,データが保存されていくところまで行きます f:id:ayousanz:20210531152440p:plain

メモ帳で開いてみます f:id:ayousanz:20210531152524p:plain

とりあえずソフトとかを使わない限りは見えてないと思います

パスワード付きデータ

セーブ時

パスワードは,SaveGame.Save("SaveData",saveDataClass,"pass"); という感じで引数にいれれば指定できます

ロード時

セーブデータをロードするときは,SaveGame.Load<SaveDataClass>("SaveData",true,"pass"); という感じで自分で設定したパスワードを入れます

パスワードが間違っている場合は以下のようにロードできないみたいです.

パスワードが違うときに表示されるエラー文

CryptographicException: Padding is invalid and cannot be removed.

f:id:ayousanz:20210531153005p:plain

Github ActionsでGameCIを使ったUnityのCI/CD環境構築方法 【Unity,Github Actions(GameCI)】

TechTrainさんのほうでゲームの課題?をやらせていただいたときにGithub Actionsを使ったUnityのCI/CDを取り組みました(特に課題内容とかではないです)

Github Actionsを使ったUnityのCI/CDはいろいろ詰まるところがあったので,今後使う方/未来の自分に向けてまとめています

成果

  • UnityTestからのUnityビルドの実行(1枚目画像) : テストが成功しないとビルドが実行されない
  • テスト結果とBuildファイルをアーティストに保存
  • Unity TestのPlayModeTestの実行と結果の表示(2枚目画像)

リポジトリは公開していますので,詳細はリポジトリをご確認ください また,テスト用にシンプルな実装でも同じことを行っています.こちらのリポジトリもご参考ください.

環境

CI/CDの実行の流れ

  1. テストを実行するマシーンの作成
  2. リポジトリをcheckout
  3. テストの実行
  4. テスト結果をアーティストをしてアップロード
  5. ビルドを実行するマシーンの作成
  6. リポジトリをcheckout
  7. cacheの取得(これがないと初回と同じ時間毎回かかるらしい)
  8. ビルドの実行
  9. ビルドファイルをアーティストにアップロード

Unityのシリアルキーを取得する

詳しい説明はこちらの記事を参考にさせていただきました

注意ポイント 公式のライセンスを取得できるサイトからダウンロードできるファイルの中身は全部そのまま Github secretのvalueに入れましょう!

license.unity3d.com

TestをActions上で実行する

Unityのテストを作成する

テストフレームのライブラリを導入

今回はUnityでUIテストを簡単に作成するために,Unity UI Test Automation Frameworkを導入しました 詳細は,こちらで詳しく説明されています

baba-s.hatenablog.com

unityUITestライブラリから,DependencyInjector.csUITest.cs を導入します

テストの作成

テストファイルと asmdefファイルを作成します

実行されるようにymlファイルを設定する

test:
    name: Run EditMode and PlayMode Test
    runs-on: ubuntu-latest
    steps:
      - name: Check out my unity project.
        uses: actions/checkout@v2
      - name: Run EditMode and PlayMode Test
        uses: game-ci/unity-test-runner@v2
        with:
          projectPath: .
          githubToken: ${{ secrets.GITHUB_TOKEN }}
          unityVersion: 2020.3.9f1
      # テストの実行結果をアーティファクトにアップロードして後から参照可能にする
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: Test results
          path: artifacts

テストの結果を保存/簡単に確認できるようにする

テスト結果の表示

githubToken: ${{ secrets.GITHUB_TOKEN }} を設定すると,以下のようにテストの結果が簡単に見れます. 詳細はこちらに記載されています

テスト結果の保存

- uses: actions/upload-artifact@v2
        if: always()
        with:
          name: Test results
          path: artifacts

を入れることで,テスト結果をアーティストとして保存することができます

BuildをActions上で実行する

ビルドのプラットフォームは,以下のものが設定できるみたいです(公式サイトより)

targetPlatform:
          - StandaloneOSX # Build a macOS standalone (Intel 64-bit).
          - StandaloneWindows # Build a Windows standalone.
          - StandaloneWindows64 # Build a Windows 64-bit standalone.
          - StandaloneLinux64 # Build a Linux 64-bit standalone.
          - iOS # Build an iOS player.
          - Android # Build an Android .apk standalone app.
          - WebGL # WebGL.

ビルドの設定は以下になります.

build:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        projectPath:
          - .
        unityVersion:
          - 2020.3.9f1
        targetPlatform:
         - Android # Build an Android player.
    needs: test
    steps:
    - name: Checkout
      uses: actions/checkout@v2
      with:
        lfs: false
        clean: false
        
    # Cache
    - uses: actions/cache@v2
      with:
        path: Library
        key: Library

    # Build
    - name: Build project
      uses: game-ci/unity-builder@v2.0-alpha-6
      with:
        unityVersion: ${{ matrix.unityVersion }}
        targetPlatform: ${{ matrix.targetPlatform }}

結論

game-ciのライブラリを使うことですごく楽にUnityのCI/CDを構築できることができました~ 他にもいろいろ機能があるみたいなので,使っていきたいです

ymlファイル全体の構成

# This is a basic workflow to help you get started with Actions

name: Test and Build,Release APK

env:
  UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}

# Controls when the action will run. 
on: [push]
  # Triggers the workflow on push or pull request events but only for the main branch

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:  
  test:
    name: Run EditMode and PlayMode Test
    runs-on: ubuntu-latest
    steps:
      - name: Check out my unity project.
        uses: actions/checkout@v2
      - name: Run EditMode and PlayMode Test
        uses: game-ci/unity-test-runner@v2
        with:
          projectPath: .
          githubToken: ${{ secrets.GITHUB_TOKEN }}
          unityVersion: 2020.3.9f1
      # テストの実行結果をアーティファクトにアップロードして後から参照可能にする
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: Test results
          path: artifacts
  build:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        projectPath:
          - .
        unityVersion:
          - 2020.3.9f1
        targetPlatform:
         - Android # Build an Android player.
    needs: test
    steps:
    - name: Checkout
      uses: actions/checkout@v2
      with:
        lfs: false
        clean: false
        
    # Cache
    - uses: actions/cache@v2
      with:
        path: Library
        key: Library

    # Build
    - name: Build project
      uses: game-ci/unity-builder@v2.0-alpha-6
      with:
        unityVersion: ${{ matrix.unityVersion }}
        targetPlatform: ${{ matrix.targetPlatform }}

    # Output
    - uses: actions/upload-artifact@v2
      with:
        name: Build-${{ matrix.targetPlatform }}
        path: build/${{ matrix.targetPlatform }}

Unity・ゲームの開発でのおすすめ/よく使用するアセット・参考サイト(その他あり)【Unity,UnityAssets,素材リンク】

自分用またほかの学習者の方が何を使っていいのかがわからなくなったときにご活用ください

アセットストア経由

(OpenUPMでも入れれるものはあります)

1. DoozyUI: Complete UI Management System

使い方などが紹介されているサイト

Doozy UI 使おうぜ! #unity_lt

DoozyUI使ってみる【Unity】 - トマシープが学ぶ

2. Odin - Inspector and Serializer

使い方などが紹介されているサイト

kan-kikuchi.hatenablog.com

www.midnightunity.net

3. UniRx - Reactive Extensions for Unity

公式より

Provides an efficient allocation free async/await integration for Unity.

assetstore.unity.com

使い方などが紹介されているサイト

qiita.com

こちらはUniTaskも合わせてこちらの本がおすすめです

3. DOTween Pro

無料版もあります(正直無料版で十分だと思います)

assetstore.unity.com

使い方などが紹介されているサイト

qiita.com

amagamina.jp

Easy Save - The Complete Save & Load Tool for Unity

使い方などが紹介されているサイト

ayousanz.hatenadiary.jp

kan-kikuchi.hatenablog.com

Open UPM経由

unity-reference-viewer

📦 unity-reference-viewer - jp.amagamina.reference-viewer | OpenUPM

UniTask

github.com

VContainer

公式より

The extra fast DI (Dependency Injection) library running on Unity Game Engine.

Zenjectよりも速度が速く,機能も欲しいところだけになっています

github.com

使い方などが紹介されているサイト light11.hatenadiary.com

qiita.com

UnityScreenNavigator

UnityのuGUIで画面遷移、画面遷移アニメーション、遷移履歴のスタック、画面のライフサイクルマネジメントを行うためのライブラリです。

(ReadMeより)

github.com

フリー素材(Unity使用OK)

ドット素材

damagedgold.wp.xdomain.jp

itch.io

dotown.maeda-design-room.net

フリー3Dモデル

3dyasan.com ]

www.kenney.nl

お寿司3Dモデル(商用利用OK)

ddd.pink

quaternius.com

パワポとか会議資料風

kage-design.com

アイソメトリックの画像・マップの作成サイト

その他

www.shigureni.com

soco-st.com

https://woobro.design/woobro.design

www.openpeeps.com

mixkit.co

pixelbuddha.net

www.manypixels.co

www.manypixels.co

フォント

photoshopvip.net

dotcolon.net

サウンド(SE・BGM)

splice.com

  • 魔王魂 164
  • H/MIX GALLERY 61
  • DOVA-SYNDROME 58
  • 甘茶の音楽工房 51
  • Wingless Seraph(ユーフルカ) 51
  • ポケットサウンド 34
  • OtoLogic 24
  • 煉獄庭園 22
  • PANICPUMPKIN 22
  • Senses circuit 20
  • PeriTune 20
  • こんとどぅふぇ 15
  • TAM Music Factory 14

(*1) 以下のツイートより

takai(音楽の卵)♪ on Twitter: "ふりーむで音楽素材サイト名で検索してみた。 魔王魂 164 H/MIX GALLERY 61 DOVA-SYNDROME 58 甘茶の音楽工房 51 Wingless Seraph(ユーフルカ) 51 ポケットサウンド 34 OtoLogic 24 煉獄庭園 22 PANICPUMPKIN 22 Senses circuit 20 PeriTune 20 こんとどぅふぇ 15 TAM Music Factory 14 参考にどうぞ" / Twitter

www.springin.org

youfulca.com

UI/UX

商用でも完全無料で利用できる、SVG完備のアイコン素材 -iconsax

coliss.com

参考サイト

www.gameuidatabase.com

interfaceingame.com

www.designnotes.co

Unity uGUI アドバンスド・リファレンス

github.com

参考記事・サイト

無料で使えるツールをまとめているサイト

freestuff.dev

PlantUMLで高画質な画像を出力する【Rider,PlatUML】

昨日Unity1Weekでゲームを作った際にクラスの依存関係のクラス図を作成しました.しかし,画質がわるくほかの人が見るには絶えないものでした..

公式に質問したところ解決方法がわかったので,ほかの方のお役に立てればと思います

変化

画像を新しいウインドウで確認すると高画質にしたほうはそれぞれのクラスの名前が確認できると思います!!(うれしい)

追加する画像情報

skinparam dpi 600

を入れることでdpiを変えることができます

(画像のサイズも変更できるみたいです.詳しくは参考サイトのほうを見てください)

変更前

f:id:ayousanz:20210503185723p:plain

変更後

f:id:ayousanz:20210504192149p:plain

参考情報

forum.plantuml.net

MV(R)Pでゲームを作ってみた【Unity,Unity1Week】

今回はアーキテクチャを学ぶ目的でUnity1Weekに参加しました.

制作物

プレイ動画

公開サイト

unityroom.com

宣伝ツイート

目的と目的

目的

目標

  • MV(R)Pを使ったゲームを作成する

開発環境

  • Unity 2020.3.5f1
  • Rider
  • WinPC

使用したアセット

Unity拡張系

素材系

期間が1週間ということもあり,途中で大きな企画,仕様変更が怖かったのでざっくりと企画と仕様を作ってからゲームの作成を行いました

企画(1-1.5日)

  • メインキャラの動きだけ決まっている.

  • キャラクターの配置をして,当たらないようにする

  • 配置するキャラクターは事前に数と種類が決まっている.

  • すべて使わないとPlayMoveできない

  • 最後まで当たらずに動くとステージクリア

  • ステージが上がるごとにキャラクターの種類が増える

(注)

実装時に以下は実装していません

  • チュートリアル画面 → 音声と文字でゲームの操作方法及び流れを説明するように変更しました
  • ステージ選択 → ステージの選択はシステム側でランダムで選択されるようにしました

仕様(1-2日)

画面遷移

スタート画面

  • ゲーム開始でスタート
  • ゲーム終了でUnityRoomにトップページに飛ぶ

ステージ選択

チュートリアル画面

  • 実際に使用する画面にテキストをいれた画像を表示する

こんな感じのところに各箇所の説明を入れる

3枚くらい入れて,ボタンで移動? 

一枚でもいいかも

キャラクターの動きをプレイヤーが確認

  • 中心にいるキャラクターが動く(ロードまいにランダムで動きを決める)

プレイヤーによるキャラクターの配置

  • 右のキャラクターをクリックすることで,配置するキャラクターを選択することができる.
  • 選択した状態で任意の水色の部分を選択することで配置することができる

配置ユニットの動き確認

  • 配置したユニットが実際に動く
  • スタート画面を押してから行動開始(この間は基本ほかの操作はうけつけない)

Result画面

  • 成功だったかどうか

実装(4-5日)

大まかな機能の実装とクラス図は以下になります. なおシーンは一つのみで,画面の切り替えはDoozyUIを使っています

クラス図

意識してViewとModelの部分を分けて実装しました UniRxとUniTaskのおかけでコードがきれいになっているのではないかと思います. (もっときれいになりそうですが,初めということもあり今回はここまでで..)

Presenterのコード

以下はGamePresenter.csのコードのなります

using System;
using System.Linq;
using AcquireChan.Scripts;
using Cysharp.Threading.Tasks;
using Doozy.Engine;
using Project.Scripts;
using Project.Scripts.Model;
using Project.Scripts.Model.Character;
using Project.Scripts.View;
using Sirenix.OdinInspector;
using UniRx;
using UnityEngine;

public class GamePresenter : MonoBehaviour
{
    [SerializeField,FoldoutGroup("ViewClass")] private SelectUnitView selectUnitView;
    [SerializeField,FoldoutGroup("ViewClass")] private GamePlayView gamePlayView;
    [SerializeField,FoldoutGroup("ViewClass")] private SelectUnitPlacementView selectUnitPlacementView;
    [SerializeField,FoldoutGroup("ViewClass")] private LoadView loadView;
    [SerializeField,FoldoutGroup("ViewClass")] private TitleView titleView;
    [SerializeField,FoldoutGroup("ViewClass")] private ResultView resultView;
    
    [SerializeField,FoldoutGroup("ModelClass")] private CharacterMotion characterMotion;
    [SerializeField,FoldoutGroup("ModelClass")] private CharacterMove characterMove;
    [SerializeField,FoldoutGroup("ModelClass")] private CharacterCollisionDetection characterCollisionDetection;
    [SerializeField,FoldoutGroup("ModelClass")] private SoundModel soundModel;
    [SerializeField,FoldoutGroup("ModelClass")] private StageModel stageModel;
    private UnitModel _unitModel;
    private ICharacterMotion _characterMotion;
    private ICharacterMove _characterMove;
    private IDisposable _disposable;

    private bool _isPlaying;
    private bool _isPlayMode = false; //アニメションモードではない,ゲームモード
    private readonly BoolReactiveProperty _isLoading = new BoolReactiveProperty();
    private const float MAXTime = 20f;
    private readonly IntReactiveProperty _placementUnitCount = new IntReactiveProperty();//設置したユニットの数

    [SerializeField] private LoadStageInfo loadStageInfo;

    private StageInfo _stageInfo;
    // Start is called before the first frame update
    private void Start()
    {
        _unitModel = GetComponent<UnitModel>();
        _characterMotion = characterMotion;
        _characterMove = characterMove;

        selectUnitPlacementView.PlacementEnum
            .SkipLatestValueOnSubscribe()
            .Where(value =>!_isPlaying)
            .Subscribe(value =>
            {
                _unitModel.PlacementUnit(selectUnitView.SelectUnitIndex.Value, value);
                selectUnitView.SelectUnitButtonNoActive();
                selectUnitView.IsImageAlpha(selectUnitView.SelectUnitIndex.Value,false);
                _placementUnitCount.Value++;
            }).AddTo(this);

        _placementUnitCount
            .SkipLatestValueOnSubscribe()
            .Where(value => value == _stageInfo.UnitInfos.Count())
            .Subscribe(_ => gamePlayView.IsUnitPlayInteractable(true)).AddTo(this);
        
        //ユニットの詳細を更新
        selectUnitView.SelectUnitIndex
            .SkipLatestValueOnSubscribe()
            .Where(value => 0 < value)
            .Subscribe(value =>
        {
            gamePlayView.SetHowToPlayText(false);
            gamePlayView.SetUnitDetailText(true);
            gamePlayView.UpdateUnitDetail(_stageInfo.UnitInfos[value]);
        }).AddTo(this);
        
        

        #region ボタンのイベント


        //Unitの位置のリセット
        gamePlayView.OnClickResetUnit()
            .Where(_=> !_isPlaying)
            .Subscribe(_ =>
            {
                _unitModel.ResetUnitPlacement();
                selectUnitView.SetAllFrameZeroAlpha();
                selectUnitView.AllButtonActive(true);
            }).AddTo(this);

        //ゲームモード開始
        gamePlayView.OnClickPlayUnit()
            .Where(_=>!_isPlaying)
            .Subscribe(_ =>
            {
                _isPlaying = true;
                _isPlayMode = true;
                AllButtonActive(false);
                selectUnitView.SetAllFrameZeroAlpha();
                _characterMotion.WalkAnimation();
                _characterMove.Move();
                _unitModel.PlayUnit();
            }).AddTo(this);

        //キャラクターの動き確認
        gamePlayView.OnClickPlayCharacterMove()
            .Where(_ => !_isPlaying)
            .Subscribe(async _ =>
            {
                _isPlayMode = false;
                _unitModel.SetAllUnitCollider(false);
                CharacterPlayMAnimation();
                _characterMove.ResetPosition(_stageInfo.StartXPosition,_stageInfo.StartZPosition);
                
                //ボタンを押せないように
                AllButtonActive(false);
                await UniTask.Delay(TimeSpan.FromSeconds(_characterMove.AnimationTime));
                AllButtonActive(true);
                _unitModel.SetAllUnitCollider(true);
            }).AddTo(this);

        //ゲームのロード
        titleView.OnClickGameStart()
            .Subscribe(_ =>
            {
                LoadStart();
                _stageInfo = loadStageInfo.GetStageData();
                stageModel.CreateStage(_stageInfo.stageObject);

                // ユニットの初期設定
                _unitModel.RegistrationUnits(_stageInfo.UnitInfos);
                selectUnitView.SetUnitIcons(_stageInfo.UnitInfos);
                
                //キャラクターの初期設定
                _characterMove.ResetPosition(_stageInfo.StartXPosition,_stageInfo.StartZPosition);
                _characterMove.SetAnimation(_stageInfo.CharacterDirectionList);
            }).AddTo(this);

        //ゲーム終了
        titleView.OnClickGameEnd()
            .Subscribe(_ =>
            {
                soundModel.GameEndVoice();
            }).AddTo(this);


        //同じゲームを実行
        resultView.OnClickReGameButton()
            .Subscribe(_ =>
            {
                GameEventMessage.SendEvent("ToGamePlay");
                LoadStart();
                ResetData();
            }).AddTo(this);

        //新しいゲームを実行
        resultView.OnClickNewGameButton()
            .Subscribe(_ =>
            {
                ResetData();
                GameEventMessage.SendEvent("ToGamePlay");
                LoadStart();
                _stageInfo = loadStageInfo.GetStageData();
                stageModel.ClearStage();
                stageModel.CreateStage(_stageInfo.stageObject);

                // ユニットの初期設定
                _unitModel.RegistrationUnits(_stageInfo.UnitInfos);
                selectUnitView.SetUnitIcons(_stageInfo.UnitInfos);
                
                //キャラクターの初期設定
                _characterMove.ResetPosition(_stageInfo.StartXPosition,_stageInfo.StartZPosition);
                _characterMove.SetAnimation(_stageInfo.CharacterDirectionList);
                
            }).AddTo(this);
        

        #endregion

        _isLoading
            .SkipLatestValueOnSubscribe()
            .Where(value => !value)
            .Subscribe(async _ =>
        {
            soundModel.StartBgm();
            soundModel.StopVoice();
            soundModel.ConfirmVoiceForCharacterMove();
            GameEventMessage.SendEvent("GameStart");
            await UniTask.Delay(TimeSpan.FromSeconds(1f));
            LoadStop();
            CharacterPlayMAnimation();
            await UniTask.Delay(TimeSpan.FromSeconds(9f));
            _characterMove.ResetPosition(_stageInfo.StartXPosition,_stageInfo.StartZPosition);
            soundModel.StopVoice();
            soundModel.PlaceUnitVoice();
                    
            //すべてのボタンをアクティブ
            AllButtonActive(true);
            gamePlayView.IsUnitPlayInteractable(false);
        }).AddTo(this);


        #region Result処理

        _characterMove.IsMove.Where(value => !value)
            .Subscribe(_ =>
            {
                _characterMotion.IdleAnimation();
                if (_isPlayMode)
                {
                    ToResult("ゲームクリアおめでとう!");
                }
            })
            .AddTo(this);

        characterCollisionDetection.IsHitAnimal.Where(value => value)
            .Subscribe(_ =>
            {
                ToResult("もう一回やってみよう!");
            }).AddTo(this);
        

        #endregion
    }

    private void ToResult(string resultContent)
    {
        soundModel.StopBgm();
        _characterMotion.IdleAnimation();
        _characterMove.Stop();
        _characterMove.ResetPosition(_stageInfo.StartXPosition,_stageInfo.StartZPosition);
                
        _unitModel.StopUnit();
        _unitModel.ResetUnitPlacement();
        resultView.SetResultText(resultContent);
        GameEventMessage.SendEvent("ToResult");

        _isPlaying = false;
        gamePlayView.IsUnitPlayInteractable(true);
        selectUnitView.ResetSelectUnit();
        selectUnitPlacementView.ResetSelectPlacement();
    }

    private void CharacterPlayMAnimation()
    {
        _characterMotion.WalkAnimation();
        _characterMove.Move();
    }

    private void LoadStart()
    {
        _isLoading.Value = true;
        soundModel.LoadVoice();
        _disposable = Observable.Interval(TimeSpan.FromSeconds(0.1f))
            .Subscribe(value =>
            {
                loadView.ChangeLoad(value/MAXTime);
                if (MAXTime <= value)
                {
                    _isLoading.Value = false;
                }
            });
    }

    private void LoadStop()
    {
        loadView.ResetValue();
        _disposable.Dispose();
    }

    private void AllButtonActive(bool active)
    {
        gamePlayView.AllButtonActive(active);
        selectUnitPlacementView.AllButtonActive(active);
        selectUnitView.AllButtonActive(active);
    }

    private void ResetData()
    {
        _isPlaying = false;
        _isPlayMode = false;
        _placementUnitCount.Value = 0;
        characterCollisionDetection.ResetData();
        gamePlayView.IsUnitPlayInteractable(false);
    }
}

Viewクラスのコード

多数あるので,一部だけになります.

ゲームプレイ画面を更新するGamePlayView.csになります

using System;
using Sirenix.OdinInspector;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace Project.Scripts.View
{
    public class GamePlayView : MonoBehaviour
    {
        [SerializeField] private Button resetUnit;
        [SerializeField] private Button playUnit;
        [SerializeField] private Button playCharacterMove;
        [SerializeField] private TextMeshProUGUI howToPlayText;
        
        [SerializeField,BoxGroup("ユニット詳細")] private TextMeshProUGUI unitNameText;
        [SerializeField,BoxGroup("ユニット詳細")] private TextMeshProUGUI unitSpeedText;
        [SerializeField,BoxGroup("ユニット詳細")] private TextMeshProUGUI unitSpecialMoveText;
        [SerializeField,BoxGroup("ユニット詳細")] private Image unitImage;
        [SerializeField, BoxGroup("ユニット詳細")] private GameObject imageFrame;
        
        /// <summary>
        /// ユニットの配置をリセット
        /// </summary>
        /// <returns></returns>
        public IObservable<Unit> OnClickResetUnit()
        {
            return resetUnit.OnClickAsObservable();
        }

        /// <summary>
        /// ユニットの動きを確認
        /// </summary>
        /// <returns></returns>
        public IObservable<Unit> OnClickPlayUnit()
        {
            return playUnit.OnClickAsObservable();
        }

        /// <summary>
        /// キャラクターの移動アニメーション開始
        /// </summary>
        /// <returns></returns>
        public IObservable<Unit> OnClickPlayCharacterMove()
        {
            return playCharacterMove.OnClickAsObservable();
        }

        public void IsUnitPlayInteractable(bool active)
        {
            playUnit.interactable = active;
        }

        /// <summary>
        /// すべてのボタンのInteractableの切り替え
        /// </summary>
        /// <param name="active"></param>
        public void AllButtonActive(bool active)
        {
            playUnit.interactable = active;
            resetUnit.interactable = active;
            playCharacterMove.interactable = active;
        }

        /// <summary>
        /// ユニットの詳細情報の更新
        /// </summary>
        /// <param name="unitInfo"></param>
        public void UpdateUnitDetail(UnitInfo unitInfo)
        {
            unitNameText.text = unitInfo.Name;
            unitSpeedText.text = $"移動速度:{unitInfo.Speed}";
            unitImage.sprite = unitInfo.UnitIcon;
            unitSpecialMoveText.text = unitInfo.SpecialMoveContent;
        }

        /// <summary>
        /// ゲーム操作テキストの表示の切り替え
        /// </summary>
        /// <param name="active"></param>
        public void SetHowToPlayText(bool active)
        {
            howToPlayText.gameObject.SetActive(active);
        }

        /// <summary>
        /// ユニットの詳細の表示の切り替え
        /// </summary>
        /// <param name="active"></param>
        public void SetUnitDetailText(bool active)
        {
            unitImage.gameObject.SetActive(active);
            unitNameText.gameObject.SetActive(active);
            unitSpeedText.gameObject.SetActive(active);
            unitSpecialMoveText.gameObject.SetActive(active);
            imageFrame.SetActive(active);
        }
    }
}

画面の切り替え

以下がDoozyUIのGraphになります. GameEventの発行とボタンでの遷移を使い分けています.

画面サイズ・アスペクト比の対応

light11.hatenadiary.com

上記を参考にCanvasCanvas Scalerを以下のように設定しました

設計で参考にサイト

qiita.com

qiita.com

xrdnk.hateblo.jp

note.com

qiita.com

Oculus Quest2でのOnApplicationFocusとOnApplicationPauseの挙動まとめ【Oculus,Quest2,Unity】

いろいろ詰まったためメモしておきます. Questでのアプリ開発でアプリを終了したときに何かの処理をしたいと思うことが多々あるかと思います. (間違えている可能性もがあるため,コメントまたはtwitterのDMにてご指摘等お待ちしています)

環境

  • Unity 2019.4.5f1
  • Oculus XR Plugin 1.7.0
  • XR Plugin Management 3.2.16
  • Oculus Integration 25.0

挙動まとめ表

関数名 UI表示[1] 時間経過[2] アプリ終了
OnApplicationFocus(bool hasFocus) 変化なし true → false true → false
OnApplicationPause(bool pauseStatus) false → true 変化なし false → true

[1] Oculusボタンを押してメニュー画面を出した状態
[2] HMDを外してHMDが非アクティブになった状態

またそれぞれの関数のEventStatusの初期値は以下の通りです.

  • OnApplicationFocus(bool hasFocus) : true
  • OnApplicationPause(bool pauseStatus) : false

その他の関数の挙動

  • OnDestroy() 呼ばれなかった
  • OnApplicationQuit() 呼び出されなかった

追記 以下のサイトより,Oculus Goの時代から残る問題として上記の二つが呼ばれない問題が残っているそうです. forum.unity.com

関連サイト

developer.oculus.com

stackoverflow.com

qiita.com

greenkour.hateblo.jp

pafu-of-duck.hatenablog.com

developer.android.com

docs.unity3d.com