はじめに
これはKMC Advent Calendar 2020 の11日目の記事*1、並びにinonoa Advent Calendar 2020の2日目の記事です*2。また、 一年前のコード書き直した(前編) の続きでもあります。
はい
一年前のコードとはこちらになります。
StageSelector.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StageSelector : MonoBehaviour
{
#region Objects
public GameObject draftselect;
public SpriteRenderer dsRenderer;
public GameObject waku;
public SpriteRenderer wakuRenderer;
public GameObject[] stages;
public SpriteRenderer[] stRenderer;
public SpriteRenderer bg;
public SpriteRenderer bgbg;
public SpriteRenderer anmaku;
public Sprite[] bgs;
public WakuLightMover wakuLight;
SoundGroup soundGroup;
#endregion
#region States
enum State{
Entering, WakuAppearing, Active, Selected
}
State state = State.Entering;
public int selected = 1;
#endregion
#region 依存
IAskedInput input;
#endregion
void Start()
{
stRenderer = new SpriteRenderer[stages.Length];
for(int i=0;i<stRenderer.Length;i++){
stRenderer[i] = stages[i].GetComponent<SpriteRenderer>();
}
dsRenderer = draftselect.GetComponent<SpriteRenderer>();
wakuRenderer = waku.GetComponent<SpriteRenderer>();
soundGroup = GetComponent<SoundGroup>();
input = ServicesLocator.Instance.GetInput();
}
void Update()
{
Vector3 vv = stages[selected-1].transform.position - waku.transform.position;
if(vv.x*vv.x+vv.y*vv.y<5){
waku.transform.position = new Vector3(stages[selected-1].transform.position.x,stages[selected-1].transform.position.y,-2);
}else{
int signX = 1; int signY = 1; if(vv.x<0){signX = -1;} if(vv.y<0){signY = -1;}
vv = 2* new Vector3(signX *(float)System.Math.Sqrt(vv.x * signX), signY *(float)System.Math.Sqrt(vv.y * signY),0);
waku.transform.position += vv;
}
if(bg.color.a<1){
bg.color -= new Color(0,0,0,0.05f);
anmaku.color = new Color(1,1,1,System.Math.Min(bg.color.a,1-bg.color.a));
if(bg.color.a<=0){
bg.sprite = bgbg.sprite;
bg.color = new Color(1,1,1,1);
}
}
switch(state){
case State.Entering:
const float targetAlpha = 0.7f;
for(int i=0;i<stages.Length;i++){
stRenderer[i].color += new Color(0,0,0,targetAlpha * 0.1f);
stages[i].transform.position -= new Vector3((float)System.Math.Sqrt(stages[i].transform.position.x),0,0);
}
dsRenderer.color += new Color(0,0,0,0.1f);
if(stRenderer[0].color.a >= targetAlpha){
state = State.WakuAppearing;
for(int i=0;i<stages.Length;i++){
stRenderer[i].color = new Color(stRenderer[i].color.r,stRenderer[i].color.g,stRenderer[i].color.b,targetAlpha);
stages[i].transform.position = new Vector3(0,stages[i].transform.position.y,stages[i].transform.position.z);
}
}
break;
case State.WakuAppearing:
wakuRenderer.color += new Color(0,0,0,0.1f);
if(wakuRenderer.color.a>=1){
state = State.Active;
}
break;
case State.Active:
if(input.GetButtonDown(ButtonCode.Up)){
if(selected>1){
selected--;
bgbg.sprite = bgs[selected-1];
bg.color = new Color(1,1,1,0.99f);
soundGroup.Play("Move");
}
}
if(input.GetButtonDown(ButtonCode.Down)){
if(selected<3){
selected++;
bgbg.sprite = bgs[selected-1];
bg.color = new Color(1,1,1,0.99f);
soundGroup.Play("Move");
}
}
if(input.GetButtonDown(ButtonCode.Enter))
{
state = State.Selected;
wakuLight.Stop();
SceneTransition.Start2ChangeScene(SceneName(selected), SceneTransition.TransitionType.Normal);
soundGroup.Play("Enter");
}
break;
case State.Selected:
stRenderer[selected-1].color += new Color(0,0,0,0.05f);
break;
}
string SceneName(int stage) => "Draft" + stage;
}
}
読まなくて大丈夫です。
これがこうなりました。
StageSelectManager.cs
using System;
using Config;
using Sirenix.OdinInspector;
using UniRx;
using UnityEngine;
public class StageSelectManager : SerializedMonoBehaviour
{
[SerializeField] IStageSelectView view;
[SerializeField] ConfigManager configManager;
[SerializeField] RankingsSelectManager rankingsSelectManager;
void Start()
{
view.StageSelected.Subscribe(stage =>
{
SceneTransition.Start2ChangeScene(stage.ToString(), SceneTransition.TransitionType.Normal);
});
view.OnGoToConfig.Subscribe(_ => configManager.Enter());
view.OnGoToRankings.Subscribe(_ => rankingsSelectManager.Enter());
configManager.OnExit.Subscribe(_ => view.Enter());
rankingsSelectManager.OnExit.Subscribe(_ => view.Enter());
}
}
public interface IStageSelectView
{
void Enter();
IObservable<EDraft> StageSelected { get; }
IObservable<Unit> OnGoToConfig { get; }
IObservable<Unit> OnGoToRankings { get; }
}
StageSelectView.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Config;
using DG.Tweening;
using Sirenix.OdinInspector;
using UniRx;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
public class StageSelectView : SerializedMonoBehaviour, IStageSelectView
{
#region Objects
[SerializeField] Image chooseADraftImage;
[SerializeField] Image wakuImage;
[SerializeField] WakuLightMover wakuLight;
[SerializeField] Image[] stageImages;
[SerializeField] SoundGroup soundGroup;
[SerializeField] StageSelectBGChanger bgChanger;
[SerializeField] Button goToRankingsButton;
[SerializeField] Button goToConfigButton;
#endregion
#region States
enum State{ Entering, Active, Selected }
State state = State.Entering;
EDraft selected = EDraft.Draft1;
#endregion
[SerializeField] IAskedInput input;
Subject<EDraft> _StageSelected = new Subject<EDraft>();
public IObservable<EDraft> StageSelected => _StageSelected;
Subject<Unit> _OnGoToConfig = new Subject<Unit>();
public IObservable<Unit> OnGoToConfig => _OnGoToConfig;
Subject<Unit> _OnGoToRankings = new Subject<Unit>();
public IObservable<Unit> OnGoToRankings => _OnGoToRankings;
void Start()
{
goToRankingsButton.onClick.AddListener(() =>
{
ExitMain();
_OnGoToRankings.OnNext(Unit.Default);
});
goToConfigButton.onClick.AddListener(() =>
{
ExitMain();
_OnGoToConfig.OnNext(Unit.Default);
});
DOVirtual.DelayedCall(1f, () =>
{
goToRankingsButton.gameObject.SetActive(true);
goToConfigButton.gameObject.SetActive(true);
});
FadeIn();
}
void ExitMain()
{
gameObject.SetActive(false);
goToConfigButton.gameObject.SetActive(false);
goToRankingsButton.gameObject.SetActive(false);
}
public void Enter()
{
gameObject.SetActive(true);
goToConfigButton.gameObject.SetActive(true);
goToRankingsButton.gameObject.SetActive(true);
}
void FadeIn()
{
const float targetAlpha = 0.8f;
const float fadeInDuration = 0.4f;
foreach (Image stageImage in stageImages)
{
Sequence fadeIn = DOTween.Sequence()
.Append(stageImage.DOFade(0, 0))
.Join(stageImage.transform.DOLocalMoveX(100, 0))
.Join(chooseADraftImage.DOFade(0, 0))
.Join(chooseADraftImage.transform.DOLocalMoveX(100, 0))
.Join(wakuImage.DOFade(0, 0))
.Join(wakuLight.GetComponent<Image>().DOFade(0, 0))
.Append(stageImage.DOFade(targetAlpha, fadeInDuration).SetEase(Ease.Linear))
.Join(stageImage.transform.DOLocalMoveX(0, fadeInDuration).SetEase(Ease.OutCubic))
.Join(chooseADraftImage.DOFade(1, fadeInDuration).SetEase(Ease.Linear))
.Join(chooseADraftImage.transform.DOLocalMoveX(0, fadeInDuration).SetEase(Ease.OutCubic))
.Append(wakuImage.DOFade(1, 0.2f).SetEase(Ease.Linear))
.AppendCallback(() => state = State.Active);
}
}
void MoveStage(EDraft dst)
{
selected = dst;
float dstY = stageImages[dst.ToInt()].transform.position.y;
wakuImage.transform.DOMoveY(dstY, 0.3f);
bgChanger.OnChangeStage(selected.ToInt());
soundGroup.Play("Move");
}
void OnDetermine()
{
state = State.Selected;
wakuLight.Stop();
soundGroup.Play("Enter");
stageImages[selected.ToInt()].DOFade(1, 0.3f).SetEase(Ease.Linear);
_StageSelected.OnNext(selected);
}
void Update()
{
if(state != State.Active) return;
if(input.GetButtonDown(ButtonCode.Up) && selected != EDraft.Draft1)
{
MoveStage(selected.Minus1());
}
if(input.GetButtonDown(ButtonCode.Down) && selected != EDraft.Draft3)
{
MoveStage(selected.Plus1());
}
if(input.GetButtonDown(ButtonCode.Enter))
{
OnDetermine();
}
}
}
StageSelectBGChanger.cs
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
public class StageSelectBGChanger : MonoBehaviour
{
[SerializeField] Image bg;
[SerializeField] Image bgbg;
[SerializeField] Image anmaku;
[SerializeField] Sprite[] bgSprites;
[SerializeField] float changeDuration = 0.4f;
public void OnChangeStage(int stage)
{
Debug.Assert(stage == 0 || stage == 1 || stage == 2);
bgbg.sprite = bgSprites[stage];
bg.DOFade(0, changeDuration)
.SetEase(Ease.Linear)
.onComplete += () =>
{
bg.sprite = bgbg.sprite;
bg.DOFade(1, 0);
};
anmaku.DOFade(0.5f, changeDuration / 2)
.onComplete += () =>
{
anmaku.DOFade(0, changeDuration / 2);
};
}
}
クラスが三つに分裂して関数も増えてますがそのあたりは前編を参照のこと………
インターフェースで切る
インターフェースをご存じでしょうかって書こうとしたんですが説明は人にぶん投げることにします。今回の場合はStageSelectManager.csの下部のこれです。
public interface IStageSelectView
{
void Enter();
IObservable<EDraft> StageSelected { get; }
IObservable<Unit> OnGoToConfig { get; }
IObservable<Unit> OnGoToRankings { get; }
}
で、StageSelectManagerのメンバの型に使われています。
public class StageSelectManager : SerializedMonoBehaviour
{
[SerializeField] IStageSelectView view;
こうするとviewにはIStageSelectViewを継承(実装)しているものなら何でも入れられるわけです。例えば場面によって違うUIからステージ選択をしたい*3としても、ここにそれぞれ違うクラス(IStageSelectViewを継承している)を入れればStageSelectManager自体は使いまわせますし、そもそもどういうUIにするか決まってないけどとりあえずManagerだけ作っておこう、ただメンバのviewがnullだと動かない、というときにも、最低限のViewを素早く作って差し込み、あとからしっかりしたViewを作って取り換えるなんて芸当も可能です。というか実際今仮置きになっています。
というかそもそもManagerがViewへの参照を持たなくてすむ方法がある気がしてきましたが考えないことにします。
インターフェースの[SerializeField]………?
まだ説明が不足している部分があります。当たり前のようにStageSelectManagerのメンバに [SerializeField] IStageSelectView view;
とか書いてましたが、 [SerializeField]
は基本的にはMonoBehaviourやそのほかシリアライズできるクラスにしか対応していません(書いてもインスペクタに出ない)。インターフェースも [SerializeField]
を消してしまってもスクリプト上で代入すれば使えはしますが、せっかくだしUnity上でドラッグアンドドロップしたい!そんな方にお勧めなのがOdinです。
assetstore.unity.com
public class StageSelectManager : SerializedMonoBehaviour
{
[SerializeField] IStageSelectView view;
実は先ほどのStageSelectManagerはMonoBehaviourではなくSirenix.OdinInspector名前空間のSerializedMonoBehaviourを継承しています。これを継承することでなぜだかわかりませんがインターフェースでもインスペクタに表示できるみたいです。めでたしめでたし。
ほかに書くことなかったっけ……?
後編に書こうと思ってたこと少なくとも複数あったはずなんですがこれしか思い浮かばねえ…………
とりあえずこれで終わります。思いついたら追記します…………