Unity・3DCG技術ブログ

TAもどきによるUnity・3DCGに関する記事をアップします。

UniRx+Zenjectで作るシーン遷移基盤 メモその2ZenjectBindingとか

例えばタイトル画面からホーム画面に遷移するとして、基盤を使う側のコードはこんな感じとなります。

コード

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using Zenject;

public class Title : SceneBase
{
    [Inject(Id = "start")] Button startButton;
    [Inject] TransitionController transitionController;

    void Start()
    {
        // タッチでホーム画面へ
        startButton.OnClickAsObservable().Subscribe(_ =>
        {
            transitionController.LoadScene("Home", TransitionController.LoadingMode.LongLoading);
        }).AddTo(this);
    }
}
using UnityEngine;
using UnityEngine.UI;
using Zenject;
using UniRx;

public class Home : SceneBase
{
    [Inject(Id = "quest")] Button questButton;
    [Inject] TransitionController transitionController;

    void Start()
    {
        // Something code here ...
    }
}

[SerializeField]でオブジェクトを直接リンクして参照するかわりに ZenjectのInjectアトリビュートでもってインスタンスを注入します。

ZenjectBinding

UIの参照がInjectされるためにZenjectBindingコンポーネントを使用します。

ZenjectBindingというコンポーネントidentifierとともに貼り付けておきます。 f:id:serialtv:20190817231014p:plain

ProjectContext

Resources以下にあるProjectContext.prefabはZenjectの仕組みによってゲーム起動直後にシーンに読み込まれます。 そして、DontDestroyなのでシーン遷移しても消えないグローバルオブジェクトです。 この性質を利用して、ProjectContext直下にトランジション演出用のコンポーネントTransitionControllerを置きました。

f:id:serialtv:20190817232046p:plain

こんな具合でProjectContext以下に置いたCanvasにあります。 そしてTransitionControllerのインスタンスProjectContextのグローバルなInstallerに下記のような コードでアタッチさせています。

コード

using UnityEngine;
using Zenject;
using UniRx;
using System.Linq;

/// <summary>
/// ProjectContextにアタッチされるグローバルなInstaller.
/// </summary>
public class ApplicationInstaller : MonoInstaller
{
    [SerializeField] TransitionController transitionController;

    public override void InstallBindings()
    {
        Container.Bind<TransitionController>().FromInstance(transitionController).AsSingle();
    }
}

あと色々省略してますがTransitionControllerのコードです。

public class TransitionController : MonoBehaviour
{
    public enum LoadingMode
    {
        // ローディング画面を挟まない
        None,
        // 長めのローディングになりそうな場合
        LongLoading,
    }

    [SerializeField] GameObject view = default;
    [SerializeField] GameObject block = default;
    [SerializeField] Material transitionMaterial = default;
    LoadingMode loadingMode = LoadingMode.None;

    public void LoadScene(string sceneName, LoadingMode mode = LoadingMode.None, object argument = null)
    {
        loadingMode = mode;
        NavigationService.NavigateSingleAsync(sceneName, argument)
        .Subscribe(_ => { }, e => Debug.LogException(e));
    }

    public void LoadSceneAdditive(string sceneName, object argument = null, LoadingMode mode = LoadingMode.None)
    {
        loadingMode = mode;
        NavigationService.NavigateAsync(sceneName, argument)
        .Subscribe(_ => { }, e => Debug.LogException(e));
    }

    void Awake()
    {
        GameObject.DontDestroyOnLoad(this.gameObject);

        NavigationService.AsyncBroker.Subscribe<AsyncBeginLoadSceneEvent>(_ => Observable.FromCoroutine(Show)).AddTo(this);
        NavigationService.AsyncBroker.Subscribe<AsyncAfterLoadSceneEvent>(_ => Observable.FromCoroutine(Hide)).AddTo(this);
        NavigationService.Broker.Receive<BackButtonEvent>().Subscribe(_ => OnBackEvent()).AddTo(this);
        NavigationService.Broker.Receive<TapBlockEvent>().Subscribe(_ => block.SetActive(true)).AddTo(this);
        NavigationService.Broker.Receive<TapUnblockEvent>().Subscribe(_ => block.SetActive(false)).AddTo(this);
    }

 ....~~いろいろ略~~
}

最後に

その3に続く...(予定)

C#リフレクションでComponentのSerializeFieldをゲットする

// SerializeFieldをゲットする。componentはなんらかのComponentを指定する。

var fields = component.GetType()
                .GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
                .SelectMany(x => x.CustomAttributes.Where(t => t.AttributeType == typeof(SerializeField)).Select(_ => x));

UniRx+Zenjectで作るシーン遷移基盤 メモその1

UnityでUniRxとZenjectを使ってのソシャゲを意識したシーン遷移基盤を趣味で作ってます。 f:id:serialtv:20190812194023g:plain

初めに

仕事ではUniRxは使っているのですが、Zenjectは仕事では使ったことがありません。 最近はやりのZenjectの練習もかねて、UniRxとZenjectを組み合わせればいいものができるのではないかという 実験的な試みとして、ゆるゆると作っていきたいと思います。

すでに作ったものの基本的な機能を挙げていくと...

シーン遷移基盤基本機能

  • シーンに引数付きで遷移または加算ロード
  • シーン履歴機能(前の画面に戻れる)
  • Androidのバックキーでも戻れる(EditorではEscキー)
  • 特定の画面にURLジャンプ
  • 遷移時にトランジション演出

と、このあたりをすでに実装してます。

シーンに引数付きで遷移または加算ロード

NavigationServiceNavigateAsyncメソッドを呼ぶと、内部でZenjectSceneLoaderLoadSceneし、 遷移先にSceneBaseコンポーネントがあることを想定してFindし、 SceneBaseコンポーネントのPrepareSceneAsyncなどのインターフェイスを呼びだす流れとなっています。

NavigationServiceクラス抜粋

public static class NavigationService
{
    static public IMessageBroker Broker { get; } = new MessageBroker();
    static public IAsyncMessageBroker AsyncBroker { get; } = new AsyncMessageBroker();
    static Stack<SceneParam> sceneStack = new Stack<SceneParam>();
    static bool isNavigating;
    static ZenjectSceneLoader loader =>
        ProjectContext.Instance.Container.Resolve<ZenjectSceneLoader>();

    /// <summary>
    /// シーンを加算ロードします。
    /// </summary>
    /// <param name="sceneName">読み込むシーン名</param>
    /// <param name="argument">読み込むシーンで必要となる場合はパラメータを与えます</param>
    /// <returns></returns>
    public static IObservable<Unit> NavigateAsync(string sceneName, object argument = null)
    {
        var sceneParam = new SceneParam(sceneName, argument, isStack: true, loadSceneMode: LoadSceneMode.Additive);
        return NavigateAsyncCore(sceneParam);
    }

    ~いろいろ省略してます~

    static IEnumerator CoNavigateAsync(IEnumerable<SceneParam> sceneParams)
    {
        ~略~

        yield return DispatchAsync<AsyncBeginLoadSceneEvent>(new AsyncBeginLoadSceneEvent());
        foreach (var sceneParam in sceneParams)
        {
            yield return LoadSceneAsync(sceneParam, false);
        }
        yield return DispatchAsync<AsyncAfterLoadSceneEvent>(new AsyncAfterLoadSceneEvent());

        foreach (var sceneParam in sceneParams)
        {
            if (sceneParam.SceneBase != null) yield return sceneParam.SceneBase.StartSceneAsync().ToYieldInstruction();
        }
}

SceneBaseクラス抜粋

public abstract class SceneBase : MonoBehaviour
{
    // これがシーン遷移時にセットされる引数を表す
    [InjectOptional(Id = "SceneBaseArgument")] public object Argument { get; protected set; }

    public virtual void ArgParseTo(SceneParam p) { }

    public bool IsLoaded { get; set; }

    public virtual IObservable<SceneBase> PrepareSceneAsync()
    {
        return Observable.Return(this);
    }

    public virtual IObservable<Unit> StartSceneAsync()
    {
        return Observable.Return(Unit.Default);
    }
}

権利表記

Unityちゃんの素材を使用しました。 © Unity Technologies Japan/UCL

最後に

と、今日はこのへんで... その2に続く...

追記

UniRx+Zenjectで作るシーン遷移基盤 メモその2ZenjectBindingとか - 技術ブログ を書きました。

【ShaderGraph】揺らめく炎のエフェクト

はじめに

ShaderGraphで炎が揺らゆくエフェクトを作成してみましたのでその作成方法をご紹介します。 炎の画像をゆらゆらと揺らしつつ、炎の下のほうは揺らめきを抑えています。 f:id:serialtv:20190811222130g:plain

ShaderGraph

全体

f:id:serialtv:20190811222731j:plain

画像のように大きく3つの部分から構成されています。 3つのグループをそれぞれ解説します。

Mainグループ

UVにスクロールするノイズUV成分を足すことで炎の揺らぎを表現します。 f:id:serialtv:20190811222734j:plain

Scrolling UV グループ

Gradient Noiseノードをノイズとして使っています。 が、そのままでは静止したままですので、Timeノードをごにょごにょとつなげていくことで UVをスクロールさせています。 f:id:serialtv:20190811222735j:plain

fractionノードは小数部分を取り出すノードです。

Affected by UV.v グループ

炎らしくするために、下の方ほど揺らがないようにするための成分を作成しています。 UVノードからV成分を取り出すためにSplitノードにつないでます。 f:id:serialtv:20190811222737j:plain

権利表記

炎画像 - WrathGames Studio

SerializeField付きの新規スクリプトを作成できる「ScriptWizard」の紹介

コード

https://bitbucket.org/macnaga/scriptwizard/src/master/Assets/Editor/ScriptWizard.cs

確認バージョン

Window/Script Wizard ..

f:id:serialtv:20181119001013p:plain

Exposed ReferencesにSerializeFieldをAddボタンにて追加します。 フィールドにオブジェクトの参照をリンクします。

Use [SerializeField] チェックを入れておくと、publicフィールドとしてではなく、[SerializeField]として追加します。

f:id:serialtv:20181119002326p:plain

Createスクリプトが生成されます。

f:id:serialtv:20181119002400p:plain

Create時にCreate And AddComponent チェックを入れてAddComponentToにゲームオブジェクトを指定すると、 Exposed Referencesに指定されていたオブジェクトへの参照を維持した状態で そのゲームオブジェクトに新規スクリプトをAddComponentします。

参考

  • DefaultPlayablesのTimelinePlayableWizard.csを参考にしてます。

Unityシーン遷移時にパラメータを渡せるようにしてみる

Unityシーン遷移時に引数(パラメータ)を渡したい時ってあると思います。 インターフェイスを実装したら引数が渡されてくれれば便利かなと思ったので実装してみます。

環境

  • Unity2018.3.0b6
  • Windows10

コード

このようなインターフェイスを定義します。

using UnityEngine;
using UnityEngine.EventSystems;

public interface ISceneWasLoaded : IEventSystemHandler
{
    void OnSceneWasLoaded(object argument);
}

引数を指定できるバージョンの LoadSceneWithArg というメソッドを作成します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;

public static class SceneManagerEx
{
    static public void LoadSceneWithArg(
         string sceneName,
         object argument,
         LoadSceneMode mode)
    {
        UnityAction<Scene, LoadSceneMode> sceneLoaded = default;
        Action removeHandler = () =>
        {
            SceneManager.sceneLoaded -= sceneLoaded;
        };

        sceneLoaded = (loadedScene, sceneMode) =>
        {
            removeHandler();
            foreach (var root in loadedScene.GetRootGameObjects())
            {
                ExecuteEvents.Execute<ISceneWasLoaded>(root, null, (receiver, e) => receiver.OnSceneWasLoaded(argument));
            }
        };

        SceneManager.sceneLoaded += sceneLoaded;
        SceneManager.LoadScene(sceneName, mode);
    }
}

遷移先のシーンでISceneWasLoadedインターフェイスを実装して、引数をとれるようにします。

public class HogeScene : MonoBehaviour, ISceneWasLoaded
{
    public void OnSceneWasLoaded(object argument)
    {
        Debug.Log("OnSceneWasLoaded.");
    }
}
SceneManagerEx.LoadSceneWithArg("HogeScene", new Hoge(), LoadSceneMode.Additive);

遷移後のHogeSceneにて OnSceneWasLoadedが呼ばれます。