0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】クリーンアーキテクチャでデッキ編集画面を作りたかった

Posted at

動機

デッキ編集画面はGUIだけで動かせるのでクリーンアーキテクチャを実践できると思った。
クリーンアーキテクチャを調査したかった。

具体的な目的

  1. 以前、そういう画面を作ったときに、マウス操作で考えていたものを突然ゲームパッドやキーボードにも対応できるようにしなきゃいけなくったときに、すごく辛かったので、クリーンアーキテクチャを使えば、拡張や差し替えが楽になると思った
  2. Unityの.asmdefを調査したかった

結果

Movie_001.gif

ゲームパッド対応前に力尽きました

プロジェクト置き場

ゲームパッド、キーボード対応をしようとして諦めた残骸も残っています。

使用ライブラリ

R3

VContainer

クラス図

関数は省略

image.png

概要の前に

正しいのか分からないので、参考にするかどうかは自己責任でお願いします…

サボったこと

クリーンアーキテクチャでは、レイヤーを跨いだデータの渡しかたについて、本来は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の調査は楽しかった。今度からどんどんやりたい。

デッキ編集画面の答えが分からない…
シンプルに「動的にボタンを生成して、ボタンを押すとそのボタンが削除される」の実装の答えが分からない…

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?