今回はアーキテクチャを学ぶ目的でUnity1Weekに参加しました.
制作物
プレイ動画
公開サイト
宣伝ツイート
「チー子のお散歩」
— ようさん (@ayousanz) 2021年5月3日
可愛い動物たちに当たらないように散歩しよう!
・可愛いボイスでゲームの説明をしてくれるよ
・可愛いUIで簡単操作
・一回のプレイは3分!
▼Web版https://t.co/V5eWNMhfeo
▼Windows版https://t.co/K4GDlSeGxp#unity1week #チー子のお散歩 pic.twitter.com/IVMjlYglfj
目的と目的
目的
- アーキテクチャを学ぶ
目標
- 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の発行とボタンでの遷移を使い分けています.
画面サイズ・アスペクト比の対応
上記を参考にCanvasの Canvas Scaler
を以下のように設定しました