うどんてっくメモ

技術的なメモをまったりと

Rxで条件付きの能力を設計する

バトルゲームを作成していて頻繁に出てくるのが「Aの時Bを発動する」と言った条件付きの能力です 例えばパズドラのゼウスというキャラクターは「HPが満タンの時、味方全体の攻撃力が3倍になる」という能力を持っています

f:id:myudon:20180329003932j:plain

またスプラトゥーンのギアの中にも、「対戦開始から30秒までの時、速度が上昇する」「復活した時、相手の位置が遠くから見える」等といった条件付きの能力を含んだものが存在します

f:id:myudon:20180329004718p:plain

勿論これらの他にもこれらのような条件をトリガーとして発動する能力は多く、バトルをデザインする上でかなりメジャーな材料と言えるでしょう

ですが、それらを安直にゲームシステム内にどんどん実装していくと、ゲーム開発が進むにつれ見るに堪えない構造になりがちです。これは時間を条件としたり、キャラクターのパラメーターを条件としたり、取り巻く環境の状況を条件としたりと様々な箇所にトリガーが考えられるからです
また、これらの能力はゲームを運用して、バトル環境をサイクルしていくにつれても増えていきます、Aという条件でB、Aという条件でC、AとDという条件でC等、組み合わせ的にも増えていくことが考えられるため、変更や振る舞いの追加に強い設計が求められます

そこで今回はUniRxを使って、この課題に対してゲームのシステムロジックに依存せず稼働し、かつ変更などに柔軟に対応できる設計を考えていきます

設計と実装例

まずはベースとなる形を考えます、条件付きの能力をレベルデザインの点から見た時、条件と効果は別々で考えていき、そこから組み合わせ的に一つの物を作りだすということが多くなります
つまり、デザインする上で条件と効果がそれぞれ単独で動く作りであると、個々にテストを行い、調整を行うことが出来ます

条件と効果

条件側は、単一の担当している条件を監視して達成したかどうかだけを通知し、効果側は単一の担当している効果の発動や有効無効の切り替えを行うようにします
それぞれ条件と効果の基盤をスーパークラス化し、それを元に実装していきます

using System;
using UnityEngine;
using UniRx;

public abstract class ConditionBase : IDisposable
{
    protected Component _dependencyComponent;

    public ConditionBase(Component component)
    {
        _dependencyComponent = component;
    }

    //条件達成通知
    public Subject<bool> OnAchieve = new Subject<bool>();

    public void Dispose()
    {
        OnAchieve.Dispose();
    }

    public static ConditionBase GetCondition(ConditionType type, Component dependency)
    {
        // 具体的な条件の取得
    }

    public enum ConditionType
    {

    }
}
using System;
using UnityEngine;

public abstract class EffectBase
{
    protected Component _dependencyComponent;

    public EffectBase(Component component)
    {
        _dependencyComponent = component;
    }
    
    //効果の有効化/無効化
    public abstract void SwitchEffect(bool invoke);

    public static EffectBase GetEffect(EffectType type, Component dependency)
    {
        //具体的な効果の取得
    }

    public enum EffectType
    {

    }
}

Binder

条件と効果を元に、この2つをBindする大本の能力の基盤クラスを設計します、能力自身はあくまでも条件と効果の中身に干渉せず、ただBindするだけです
ここで汎用的な能力設計を行うために、RxでどのようにBindをするかを考えていきます
能力が持つ条件については、例えば上記で上げた例では単一条件で単一効果を担保していましたが、例えば複合条件、また複数条件の内1つでも満たしていたら発動など、条件をAND的に、OR的に考えることも考慮したほうが汎用性が高いと言えます
そこで、複数の条件からくる通知を束ねて考えて、その束からAND,OR的に導出をすることで様々な条件発動に対する回答を試みます、ここで役に立つのがRxです
条件達成をイベントストリーム的に捉え、それらを並列に監査します、イメージは下図です

f:id:myudon:20180408111705p:plain

これをUniRxを用いて実装していきます、肝になるのがCombineLatestです
CombineLatestは複数のストリームを統一して監視するものです、対象とする複数ストリームよりいずれかのストリームから値が発行された時に全てのストリームが保持している値を流します
これを応用し、条件の達成通知を統合して購読します
f:id:myudon:20180401224826p:plain
Binderに条件と効果の識別子、そしてその処理に必要となる依存性を外部から流し込みます、そしてBinderを立ち上げて条件と効果の結びつきをCombineLatestによって実現します

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UniRx;

public class Binder : IDisposable
{

    private List<ConditionBase> _conditions;

    private List<EffectBase> _effects;

    private IDisposable _andDisposable;
    private IDisposable _orDisposable;

    // 能力の干渉対象
    private GameObject _dependencyObject;

 public Binder(Component dependency, List<ConditionBase.ConditionType> conditionTypes, List<EffectBase.EffectType> effectTypes)
    {
        _dependencyObject = dependency.gameObject;

        _conditions = conditionTypes.Select(type => ConditionBase.GetCondition(type, dependency)).ToList();
        _effects = effectTypes.Select(type => EffectBase.GetEffect(type, dependency)).ToList();
    }
  
    //初期化でそれぞれの通知をCombineLatestで束ねて処理
    public void Initialize()
    {
        _andDisposable =
            _conditions.Select(x => (IObservable<bool>)x.OnAchieve)
            .CombineLatest()
            .Subscribe(_ => _effects.All(x => true))//andで条件を束ねる
            .AddTo(_dependencyObject);

       _orDisposable =
            _conditions.Select(x => (IObservable<bool>)x.OnAchieve)
            .CombineLatest()
            .Subscribe(_ => _effects.Any(x => true))//orで条件を束ねる
            .AddTo(_dependencyObject);
    }

    public void Dispose()
    {
        _conditions.ForEach(condition => condition.Dispose());

        _andDisposable.Dispose();
        _orDisposable.Dispose();
    }
}

今回はもっとも単純な形としてEnumから具体的なサブクラスを呼び出し、共通処理からポリモーフィズム的な条件と効果の呼び出しを行っています
条件についても 基本型としてBindを設計しましたが、条件の性質によっては束ね方が変わってくるかと思います

メリット

まず1番のメリットは先述にもあったように、条件と効果が疎に連結され、組み合わせを柔軟に組むことができることです
能力の追加や仕様変更に対しても強く、また条件についてはストリームで監視を行う以上条件の処理の重さに合わせて監視の粒度を調整することも可能です
何より効果毎の調整といったデバッグ機能などのテストコードの実装が容易であり、細かい変更にコストを割くことが無くなるでしょう

デメリット

一つはRxそのものがゲームロジックにクリティカルに介入するという事そのものです
自分はRxが確かに好きなのですが、好きであるからこそしっかりと使い所は見極めるべきだと考えています
特定のゲームロジックに対し監視を仕掛ける以上、Rxは処理コストを要します、ゲーム内容によってはここの処理コストがボトルネックとなってしまう可能性もあり、簡単に導入を決定できるものではないと言えます
もう一つはイベント処理の通知化にとらわれてしまう事です
依存性を持つコンポーネントに対して監視し、条件が処理を取り行う為、条件側からイベントを何かしらの形で購読できる形が必要になってきます
その為に多数のSubjectの発生やReactive化が必要となるという懸念があります、無論それによる処理負荷の肥大の可能性も考えられるでしょう


今回、設計の一例としてRxを用いた汎用化を紹介しましたが、あくまでも形の1つであり、内部の既存構造によって最善形は変わってくるかと思います
ですが、瞬間的に完結しないゲームロジックの汎用的設計の1つの解決案として、Rxを頭の片隅に置いておくことは良いことですし、道具として知ってだけでもロジックの捉え方が広くなるのでおすすめです