動機
デッキ編集画面はGUIだけで動かせるのでクリーンアーキテクチャを実践できると思った。
クリーンアーキテクチャを調査したかった。
具体的な目的
- 以前、そういう画面を作ったときに、マウス操作で考えていたものを突然ゲームパッドやキーボードにも対応できるようにしなきゃいけなくったときに、すごく辛かったので、クリーンアーキテクチャを使えば、拡張や差し替えが楽になると思った
- Unityの.asmdefを調査したかった
結果
ゲームパッド対応前に力尽きました
プロジェクト置き場
ゲームパッド、キーボード対応をしようとして諦めた残骸も残っています。
使用ライブラリ
R3
VContainer
クラス図
関数は省略
概要の前に
正しいのか分からないので、参考にするかどうかは自己責任でお願いします…
サボったこと
クリーンアーキテクチャでは、レイヤーを跨いだデータの渡しかたについて、本来はDTOやViewModelに入れ替えなければならないのですが、サボりました。
ありとあらゆるレイヤーがクラス図内でいうCommonEntityに依存しています。
概要
CommonEntity
カード
namespace CommonEntity
{
public class CardEntity
{
public string CardKey { get; }
public CardEntity(string cardKey)
{
CardKey = cardKey;
}
}
}
Domain
Entity
デッキ
using CommonEntity;
using ObservableCollections;
using R3;
namespace Domain.Entity
{
public class DeckEntity
{
private readonly ObservableList<CardEntity> _cardEntities = new();
public Observable<CardEntity> ObserveAdd() => _cardEntities
.ObserveAdd().Select(value => value.Value);
public Observable<CardEntity> ObserveRemove() => _cardEntities
.ObserveRemove().Select(value => value.Value);
public void Add(CardEntity cardEntity)
{
_cardEntities.Add(cardEntity);
}
public void Remove(CardEntity cardEntity)
{
_cardEntities.Remove(cardEntity);
}
private readonly ReactiveProperty<CardEntity> _currentFocusedCard = new();
public CardEntity CurrentFocusedCard
{
get => _currentFocusedCard.Value;
set => _currentFocusedCard.Value = value;
}
public Observable<CardEntity> ObserveCurrentFocusedCard() => _currentFocusedCard;
}
}
カードプール
using System.Collections.Generic;
using CommonEntity;
using R3;
namespace Domain.Entity
{
public class CardPoolEntity
{
private List<CardEntity> _cardList;
public IReadOnlyList<CardEntity> CardList => _cardList;
public void Reset(IEnumerable<CardEntity> cardEntities)
{
_cardList = new List<CardEntity>(cardEntities);
_onReset.OnNext(Unit.Default);
}
private readonly Subject<Unit> _onReset = new();
public Observable<Unit> ObserveReset() => _onReset;
}
}
UseCase
デッキにカードを追加
using CommonEntity;
using Domain.Entity;
using R3;
namespace Domain.UseCase
{
public class AddUseCase
{
private readonly DeckEntity _deckEntity;
public AddUseCase(DeckEntity deckEntity)
{
_deckEntity = deckEntity;
}
public void Add(CardEntity cardEntity) => _deckEntity.Add(cardEntity);
public Observable<CardEntity> ObserveAdd() => _deckEntity.ObserveAdd();
}
}
デッキからカードを削除
using CommonEntity;
using Domain.Entity;
using R3;
namespace Domain.UseCase
{
public class RemoveUseCase
{
private readonly DeckEntity _deckEntity;
public RemoveUseCase(DeckEntity deckEntity)
{
_deckEntity = deckEntity;
}
public void Remove(CardEntity cardEntity) => _deckEntity.Remove(cardEntity);
public Observable<CardEntity> ObserveRemove() =>
_deckEntity.ObserveRemove();
}
}
カードプールの更新
using System.Collections.Generic;
using CommonEntity;
using Domain.Entity;
using R3;
namespace Domain.UseCase
{
public class ResetCardPool
{
private readonly CardPoolEntity _cardPoolEntity;
private readonly ICardPoolGateway _cardPoolGateway;
public ResetCardPool(CardPoolEntity cardPoolEntity, ICardPoolGateway cardPoolGateway)
{
_cardPoolEntity = cardPoolEntity;
_cardPoolGateway = cardPoolGateway;
}
public void Reset() => _cardPoolEntity.Reset(_cardPoolGateway.GetCardPool());
public Observable<IReadOnlyList<CardEntity>> ObserveReset() =>
_cardPoolEntity.ObserveReset().Select(_ => _cardPoolEntity.CardList);
}
}
Adapter
Presenter
カード追加処理の、プレゼンターとビューのインターフェース
using Domain.UseCase;
using R3;
using VContainer.Unity;
namespace Adapter.Presenter
{
public class AddPresenter : IInitializable
{
private readonly IAddView _addView;
private readonly AddUseCase _addUseCase;
public AddPresenter(IAddView addView, AddUseCase addUseCase)
{
_addView = addView;
_addUseCase = addUseCase;
}
public void Initialize()
{
_addUseCase.ObserveAdd().Subscribe(value => _addView.OnAdd(value));
}
}
}
using CommonEntity;
namespace Adapter.Presenter
{
public interface IAddView
{
public void OnAdd(CardEntity cardEntity);
}
}
カード削除処理の、プレゼンターとビューのインターフェース
using Domain.UseCase;
using R3;
using VContainer.Unity;
namespace Adapter.Presenter
{
public class RemovePresenter : IInitializable
{
private readonly RemoveUseCase _removeUseCase;
private readonly IRemoveView _removeView;
public RemovePresenter(RemoveUseCase removeUseCase, IRemoveView removeView)
{
_removeUseCase = removeUseCase;
_removeView = removeView;
}
public void Initialize()
{
_removeUseCase.ObserveRemove().Subscribe(value => _removeView.OnRemove(value));
}
}
}
using CommonEntity;
namespace Adapter.Presenter
{
public interface IRemoveView
{
public void OnRemove(CardEntity cardEntity);
}
}
カードプールの、プレゼンターとビューのインターフェース
using Domain.UseCase;
using R3;
using VContainer.Unity;
namespace Adapter.Presenter
{
public class CardPoolPresenter : IInitializable
{
private readonly ResetCardPool _resetCardPool;
private readonly ICardPoolView _cardPoolView;
public CardPoolPresenter(ResetCardPool resetCardPool, ICardPoolView cardPoolView)
{
_resetCardPool = resetCardPool;
_cardPoolView = cardPoolView;
}
public void Initialize()
{
_resetCardPool.ObserveReset().Subscribe(value => _cardPoolView.OnReset(value));
}
}
}
using System.Collections.Generic;
using CommonEntity;
namespace Adapter.Presenter
{
public interface ICardPoolView
{
public void OnReset(IReadOnlyList<CardEntity> cardEntities);
}
}
Controller
using CommonEntity;
using Domain.UseCase;
namespace Adapter.Controller
{
public class AddController
{
private readonly AddUseCase _addUseCase;
public AddController(AddUseCase addUseCase)
{
_addUseCase = addUseCase;
}
public void Add(CardEntity cardEntity) => _addUseCase.Add(cardEntity);
}
}
using CommonEntity;
using Domain.UseCase;
namespace Adapter.Controller
{
public class RemoveController
{
private readonly RemoveUseCase _removeUseCase;
public RemoveController(RemoveUseCase removeUseCase)
{
_removeUseCase = removeUseCase;
}
public void Remove(CardEntity cardEntity)
{
_removeUseCase.Remove(cardEntity);
}
}
}
Gateway
データベースとの橋渡し
using System.Collections.Generic;
using CommonEntity;
using Domain.UseCase;
namespace Adapter.Gateway
{
public class CardPoolGateway : ICardPoolGateway
{
private readonly ICardDatabase _cardDatabase;
public CardPoolGateway(ICardDatabase cardDatabase)
{
_cardDatabase = cardDatabase;
}
public IReadOnlyList<CardEntity> GetCardPool()
{
return _cardDatabase.GetCardList();
}
}
}
using System.Collections.Generic;
using CommonEntity;
namespace Adapter.Gateway
{
public interface ICardDatabase
{
public IReadOnlyList<CardEntity> GetCardList();
}
}
Detail
View
デッキ
using System.Collections.Generic;
using System.Linq;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;
namespace Detail.View
{
public class DeckView : MonoBehaviour, IAddView, IRemoveView
{
[Inject] private readonly DeckCardViewFactory _deckCardViewFactory;
private readonly List<DeckCardView> _deckCardViews = new();
public IReadOnlyList<DeckCardView> DeckCardViews => _deckCardViews;
public void OnAdd(CardEntity cardEntity)
{
var instance = _deckCardViewFactory.Create(cardEntity);
_deckCardViews.Add(instance);
}
public void OnRemove(CardEntity cardEntity)
{
var instance = _deckCardViews.First(value => value.CardEntity == cardEntity);
_deckCardViews.Remove(instance);
Destroy(instance.gameObject);
}
}
}
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;
namespace Detail.View
{
public class DeckCardViewFactory : MonoBehaviour
{
[SerializeField] private DeckCardView deckCardViewPrefab;
[SerializeField] private Transform parent;
[Inject] private readonly RemoveController _removePresenter;
public DeckCardView Create(CardEntity cardEntity)
{
var instance = Instantiate(deckCardViewPrefab, parent);
instance.Construct(cardEntity, _removePresenter);
return instance;
}
}
}
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Detail.View
{
public class DeckCardView : MonoBehaviour
{
[SerializeField] private Button button;
[SerializeField] private TMP_Text text;
public CardEntity CardEntity { get; private set; }
private RemoveController _removeController;
public void Construct(CardEntity cardEntity, RemoveController removeController)
{
CardEntity = cardEntity;
_removeController = removeController;
Init();
}
private void Init()
{
text.text = CardEntity.CardKey;
button.OnClickAsObservable().Subscribe(_ => _removeController.Remove(CardEntity));
}
}
}
カードプール
using System.Collections.Generic;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;
namespace Detail.View
{
public class CardPoolView : MonoBehaviour, ICardPoolView
{
[Inject] private readonly CardPoolCardViewFactory _cardViewFactory;
private readonly List<CardPoolCardView> _instances = new();
public void OnReset(IReadOnlyList<CardEntity> cardEntities)
{
foreach (var cardPoolCardView in _instances)
{
Destroy(cardPoolCardView.gameObject);
}
_instances.Clear();
foreach (var cardEntity in cardEntities)
{
var instance = _cardViewFactory.Create(cardEntity);
_instances.Add(instance);
}
}
}
}
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using UnityEngine;
using VContainer;
namespace Detail.View
{
public class CardPoolCardViewFactory : MonoBehaviour
{
[SerializeField] private CardPoolCardView cardPrefab;
[SerializeField] private Transform parent;
[Inject] private readonly AddController _addController;
public CardPoolCardView Create(CardEntity cardEntity)
{
var instance = Instantiate(cardPrefab, parent);
instance.Construct(cardEntity, _addController);
return instance;
}
}
}
using Adapter.Controller;
using Adapter.Presenter;
using CommonEntity;
using R3;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Detail.View
{
public class CardPoolCardView : MonoBehaviour
{
[SerializeField] private Button button;
[SerializeField] private TMP_Text text;
private CardEntity _cardEntity;
private AddController _addController;
public void Construct(CardEntity cardEntity, AddController addController)
{
_cardEntity = cardEntity;
_addController = addController;
Init();
}
private void Init()
{
button.OnClickAsObservable().Subscribe(_ => _addController.Add(new CardEntity(_cardEntity.CardKey)));
text.text = _cardEntity.CardKey;
}
}
}
Database
using System.Collections.Generic;
using System.Linq;
using Adapter.Gateway;
using CommonEntity;
using UnityEngine;
namespace Detail.DB
{
[CreateAssetMenu(menuName = "Create CardPoolDatabase", fileName = "CardPoolDatabase", order = 0)]
public class CardPoolDatabase : ScriptableObject, ICardDatabase
{
[SerializeField] private List<CardData> cardData;
public IReadOnlyList<CardEntity> GetCardList()
{
return cardData.Select(value => new CardEntity(value.cardKey)).ToList();
}
}
}
using System;
namespace Detail.DB
{
[Serializable]
public class CardData
{
public string cardKey;
}
}
意識したこと、思ったこととか
Unityや外部ライブラリに依存する層は最下層(Detail層)だけにした。
このとき、それより上の層の.asmdef操作が比較的楽だった。
Detail層の.asmdef操作が面倒だったが、この層はガッツリゲームエンジンに依存させているので、.asmdefを作らなくても良いかもと思った。
Entity、UseCase、Controllerの働きの違いを明確に定義することができなかった…
依存逆転の法則が守れているなら、レイヤーは少なくても良いんじゃないかなと思った。
依存逆転の法則を守るときに、Observableにするかinterfaceにするか迷った。
全部interfaceにすると、どこかで循環参照が生まれるはず。
全部Observableにすると、たしか処理順とかが変になっちゃった気がする…
.asmdefの調査は楽しかった。今度からどんどんやりたい。
デッキ編集画面の答えが分からない…
シンプルに「動的にボタンを生成して、ボタンを押すとそのボタンが削除される」の実装の答えが分からない…