今回はアーキテクチャを学ぶ目的でUnity1Weekに参加しました.
制作物
プレイ動画
公開サイト
unityroom.com
宣伝ツイート
目的と目的
目的
目標
開発環境
- Unity 2020.3.5f1
- Rider
- WinPC
使用したアセット
Unity拡張系
素材系
期間が1週間ということもあり,途中で大きな企画,仕様変更が怖かったのでざっくりと企画と仕様を作ってからゲームの作成を行いました
企画(1-1.5日)
(注)
実装時に以下は実装していません
- チュートリアル画面 → 音声と文字でゲームの操作方法及び流れを説明するように変更しました
- ステージ選択 → ステージの選択はシステム側でランダムで選択されるようにしました
仕様(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;
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 ボタンのイベント
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>
</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
上記を参考にCanvasの Canvas Scaler
を以下のように設定しました
設計で参考にサイト
qiita.com
qiita.com
xrdnk.hateblo.jp
note.com
qiita.com