kumak1’s blog

kumak1のイラストや技術ログ

UnityとPlayMakerを触り始めたのでカスタムアクション作った

ゲームを継続して作っている。 じっくりと腰を据えて遊ぶ大作・・!というのも憧れはするけども、そういうのは時間も資金も潤沢な専業の会社に任せるほかない。

  • 数分脳汁を垂らしながらプレイするもの
  • 何かのながらでやるもの
  • ちょっとした時間つぶしでやるもの

こういったものが少人数開発だと狙うべき所かなぁという感触をもっている。 (自分自身のスキル的にも限界ががが) となると、いかの条件がでてくる。

  • PC・スマホ双方ビルドできる
  • 2D・3D両方共 同じ感覚で作れる
  • エコシステムが活発なもの

こうなると、Unity だなぁという感じになった。 (神こロしでは JavaScript のゲームライブラリを魔改造したけども、3Dやエコシステムに難ありだった)

Unity だけでも作りやすくはある。 んだけども、せっかくなのでロジックをもっとビジュアライズして、全体の処理をイメージしやすくしたり、 バグを発見・潰しやすくするために PlayMaker というプラグインを導入することにした。

こんな感じに矢印を繋げれば処理を作る事ができる。

f:id:kumak1:20160914221458p:plain

構成要素は大体以下の3つ。

  • ステート
    • 今どんな状態か(歩く・走る etc..)
  • イベント
    • どんな時にアクションを動作させるか(十字キーを押した・壁にぶつかった etc..)
  • アクション
    • どんな行動を起こすか(ステートの移動 ・HPを減らす etc)

イベント・アクションは初期状態で相当な数が用意されていて、これだけで大体のことができてしまう。 けれど、それらは単機能でかつ組み合わせるにも煩雑な過程を経る必要がある(一旦オブジェクト内変数に値を待避させる)など、逆に面倒なことがちょこちょことある。

けれど上手くつくられているもので、これらは自由に追加することが可能だ。 用意されている処理を Edit Script などからエディタで開き、参考にしながらオレオレ イベント・アクションを追加してサイキョーの開発環境にしていける。 今回試しに作ったのは以下2つ。

  • BoolOperatorTest
    • はじめから用意されている BoolTest の拡張。
    • BoolTest は以下の値をセットでき、変数の状態に応じてイベント発火できる。
      • 変数(bool)
      • true 時に動作するイベント
      • false 時に動作するイベント
    • この拡張では、以下の値をセットでき、より柔軟に変数の状態に応じてイベント発火できる。
      • 変数1(bool)
      • 変数2(bool)
      • 論理演算子
      • true 時に動作するイベント
      • false 時に動作するイベント
  • FsmMultipleStateTest
    • はじめから用意されている FsmStateTest の拡張
    • FsmStateTest 以下の値をセットでき、ステートの状態に応じてイベント発火できる。
      • 対象オブジェクト(fsm)
      • 予測するステート名(string)
      • true 時に動作するイベント
      • false 時に動作するイベント
    • この拡張では、以下の値をセットでき、より柔軟に変数の状態に応じてイベント発火できる。
      • 対象オブジェクト(fsm)
      • 予測するステート名(string)をカンマ区切りで複数
      • true 時に動作するイベント
      • false 時に動作するイベント

既存のものをちょいちょいと組み合わせるだけで、より自分が使いやすいものをシュッと作れて、スッと利用できるので大変よい感じである。 いくらGUIで大体作れるからといっても、効率良くしたり、少ない記述で見通し良くしようとしたらば、結局は設計とプログラムは必要なのだなとしみじみしたのであった

BoolOperatorTest.cs

using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory(ActionCategory.Logic)]
    [Tooltip("Performs boolean operations on 2 Bool Variables.")]
    public class BoolOperatorTest : FsmStateAction
    {
        public enum Operation
        {
            AND,
            NAND,
            OR,
            XOR
        }

        [RequiredField]
        [Tooltip("The first Bool variable.")]
        public FsmBool bool1;

        [RequiredField]
        [Tooltip("The second Bool variable.")]
        public FsmBool bool2;

        [Tooltip("Boolean Operation.")]
        public Operation operation;

        [Tooltip("Event to send if the Bool variable is True.")]
        public FsmEvent isTrue;

        [Tooltip("Event to send if the Bool variable is False.")]
        public FsmEvent isFalse;

        [UIHint(UIHint.Variable)]
        [Tooltip("Store the result in a Bool Variable.")]
        public FsmBool storeResult;

        [Tooltip("Repeat every frame while the state is active.")]
        public bool everyFrame;

        public override void Reset()
        {
            bool1 = false;
            bool2 = false;
            operation = Operation.AND;
            storeResult = null;
            everyFrame = false;
            isTrue = null;
            isFalse = null;
        }

        public override void OnEnter()
        {
            DoBoolOperator();

            if (!everyFrame)
            {
                Finish();
            }
        }

        public override void OnUpdate()
        {
            DoBoolOperator();
        }

        void DoBoolOperator()
        {
            var v1 = bool1.Value;
            var v2 = bool2.Value;

            switch (operation)
            {
                case Operation.AND:
                    storeResult.Value = v1 && v2;
                    break;

                case Operation.NAND:
                    storeResult.Value = !(v1 && v2);
                    break;

                case Operation.OR:
                    storeResult.Value = v1 || v2;
                    break;

                case Operation.XOR:
                    storeResult.Value = v1 ^ v2;
                    break;
            }

            Fsm.Event(storeResult.Value ? isTrue : isFalse);
        }
    }
}

FsmMultipleStateTest

using UnityEngine;

namespace HutongGames.PlayMaker.Actions {
    [ActionCategory(ActionCategory.Logic)]
    [ActionTarget(typeof(PlayMakerFSM), "gameObject,fsmName")]
    [Tooltip("Tests if an FSM is in the specified State.")]
    public class FsmMultipleStateTest : FsmStateAction {
        [RequiredField]
        [Tooltip("The GameObject that owns the FSM.")]
        public FsmGameObject gameObject;

        [UIHint(UIHint.FsmName)]
        [Tooltip("Optional name of Fsm on Game Object. Useful if there is more than one FSM on the GameObject.")]
        public FsmString fsmName;

        [RequiredField]
        [Tooltip("Check to see if the FSM is in this state.")]
        public FsmString stateName;

        [Tooltip("Event to send if the FSM is in the specified state.")]
        public FsmEvent trueEvent;

        [Tooltip("Event to send if the FSM is NOT in the specified state.")]
        public FsmEvent falseEvent;

        [UIHint(UIHint.Variable)]
        [Tooltip("Store the result of this test in a bool variable. Useful if other actions depend on this test.")]
        public FsmBool storeResult;

        [Tooltip("Repeat every frame. Useful if you're waiting for a particular state.")]
        public bool everyFrame;

        // store game object last frame so we know when it's changed
        // and have to cache a new fsm
        private GameObject previousGo;

        // cach the fsm component since that's an expensive operation
        private PlayMakerFSM fsm;

        public override void Reset() {
            gameObject = null;
            fsmName = null;
            stateName = null;
            trueEvent = null;
            falseEvent = null;
            storeResult = null;
            everyFrame = false;
        }

        public override void OnEnter() {
            DoFsmStateTest();

            if (!everyFrame)
            {
                Finish();
            }
        }

        public override void OnUpdate() {
            DoFsmStateTest();
        }

        void DoFsmStateTest() {
            var go = gameObject.Value;

            if (go == null)
            {
                return;
            }

            if (go != previousGo)
            {
                fsm = ActionHelpers.GetGameObjectFsm(go, fsmName.Value);
                previousGo = go;
            }

            if (fsm == null)
            {
                return;
            }

            storeResult.Value = stateName.Value.Split(',').Any(s => s == fsm.ActiveStateName);

            Fsm.Event(storeResult.Value ? trueEvent : falseEvent);
        }


    }
}