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

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

制作物

プレイ動画

f:id:ayousanz:20210503024527g:plain

公開サイト

unityroom.com

宣伝ツイート

目的と目的

目的

目標

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

開発環境

  • Unity 2020.3.5f1
  • Rider
  • WinPC

使用したアセット

Unity拡張系

素材系

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

企画(1-1.5日)

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

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

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

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

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

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

(注)

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

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

仕様(1-2日)

画面遷移

f:id:ayousanz:20210503192030p:plain

スタート画面

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

f:id:ayousanz:20210503020102p:plain

ステージ選択

f:id:ayousanz:20210503020244p:plain

チュートリアル画面

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

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

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

一枚でもいいかも

f:id:ayousanz:20210503020312p:plain

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

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

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

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

配置ユニットの動き確認

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

Result画面

  • 成功だったかどうか f:id:ayousanz:20210503020339p:plain

実装(4-5日)

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

クラス図

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

f:id:ayousanz:20210503185723p:plain

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の発行とボタンでの遷移を使い分けています.

f:id:ayousanz:20210503020645p:plain

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

light11.hatenadiary.com

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

f:id:ayousanz:20210503185748p:plain

設計で参考にサイト

qiita.com

qiita.com

xrdnk.hateblo.jp

note.com

qiita.com