LoginSignup
18
22

More than 5 years have passed since last update.

有限ステートマシンで遊ぼう その1

Last updated at Posted at 2016-09-11

今日は有限ステートマシンとやらで遊ぶ。
うーんっと、簡単にやるとifとか switchで条件分岐して行動するけど、
複雑になると管理が仕切れなくなるから、
状態が変わるごとにカセット変えようってこと。
そうすれば行動の追加はカセットを追加して、他のカセットからそのカセットを呼ぶ処理追加してあげるだけで追加がすむ。

ステートごとの行動の変化はファジー理論なども使ってみよう。
ファジー理論で遊んでみる
とりあえずシンプルめに作ってみた。

全体のステータス

お外の時間(0~24) 19~5じ前までを夜と定義
一つの行動に1時間かかるとする

あゆめぐのステータス

場所(家か外(森)か)
体力(とりあえずmax 100)
お腹(とりあえずmax 100)
持ち物(あゆめぐのリュックには5つしか入らない) 荷物は食べ物とその他がある
お家の荷物 現在未使用

あゆめぐのState(あゆめぐの行動カセット)

  • SEARCH:遊ぶ(探索)
  • SPEEP:寝る
  • EAT:食べる
  • STASH:しまう
  • RELAX:お家でごろごろ(夜のみ)

ルール

お腹と体力は行動の種類ごとに減ったりする。

  • Search
    • 外にいなければ移動する
    • カバンがいっぱいであればStashへ
    • 夜であれば家にかえる Relaxへ
    • ファジーの計算により、Sleep Eat Searchが実行される
    • 探索の場合7割の確率でアイテムをみつけ、そのうち3割の確率で食べ物をみつける
  • Sleep
    • 寝入ってなくて、かつ夜であれば家に帰る
    • 体力がいっぱいでなければ寝る
    • 夜であれば家に帰る Relaxへ
    • お腹がすいていればご飯を食べるEatへ
    • それ以外Searchへ
  • Eat
    • 森にいる場合でカバンに食べ物がなければ家に帰ります
    • 満腹度が9割以下であればご飯を食べます
    • 森にいるならSearchへ 家にいるならRelaxへ
  • Stash
    • 家にいなければ家に移動します
    • 荷物があれば片付けます Stashへ
    • ファジーの計算で寝るか Sleepへ ご飯を食べるか Eatへを計算
    • 両方0の場合昼間であればSearchへ 夜であればRelaxへ
  • Relax
    • 家にいなければ家に移動します
    • 荷物があれば片付けます Stashへ
    • ファジーの計算で寝るか Sleepへ ご飯を食べるか Eatへを計算
    • 両方0の場合昼間であればSearchへ 夜であればごろごろします

とりあえず作ってみたのはこれ
絵がないからちょっとわかりにくいけど。

とりあえずStateの基盤クラスをこんな感じにして

public abstract class State {
    //子で実装するメソッド
    public abstract void Enter(AyumeguClass ayumegu);
    public abstract void Execute(AyumeguClass ayumegu);
    public abstract void Exit(AyumeguClass ayumegu);
}

これを継承した各カセットを作ります

Searchのカセット

using UnityEngine;
using System.Collections;

public class SearchState : State {

    public override void Enter(AyumeguClass ayumegu)
    {
        Debug.Log("探索開始");
    }

    public override void Execute(AyumeguClass ayumegu)
    {
        Debug.LogWarning("SearchExecute");
        // 森にいなければ移動する
        if(ayumegu.place == Place.HOME)
        {
            ayumegu.MovePlace(Place.FOREST);
            return;
        }

        // カバンがいっぱいであれば家に帰ります
        if(ayumegu.itemList.Count >= ayumegu.maxItemCount)
        {
            Exit(ayumegu);
            ayumegu.ChangeState(new StashState());

            return;
        }

        // 夜であれば家に帰ります
        if(StateTestMain.Instance.GetDayTime() == DayTime.NIGHT)
        {
            Exit(ayumegu);
            ayumegu.ChangeState(new RelaxState());
            return;
        }

        // 欲求により行動変化 優先度は上から
        // 体力が4割以下になると寝たくなる1.5割で100%
        // お腹が5割以下になると徐々にご飯食べたくなり2割で100%
        // TODO 時間の度合いにより家に帰るようにしたい 
        // リュックの中身が軽いと探索したい0で100% 5で0%
        float sleepVale = FuzzyLogic.FuzzyReverseGrade(ayumegu.GetHpRate(), 0.15f, 0.4f);
        float satietyLevelValue = FuzzyLogic.FuzzyReverseGrade(ayumegu.GetSatietyLevelRate(), 0.2f, 0.5f);
        //float goHomeValue = 
        float searchValue = FuzzyLogic.FuzzyReverseGrade((float)ayumegu.itemList.Count / (float)ayumegu.maxItemCount, 0f, 1f);
        StateTestMain.Instance.ShowActionLog("寝たい:" + sleepVale + " 食べたい:" + satietyLevelValue + " 探索したい:" + searchValue);

        float value = Mathf.Max(sleepVale, satietyLevelValue, searchValue);
        if(value == sleepVale)
        {
            // 寝る
            Exit(ayumegu);
            ayumegu.ChangeState(new SleepState());
        }
        else if(value == satietyLevelValue)
        {
            // ご飯食べる
            Exit(ayumegu);
            ayumegu.ChangeState(new EatState());
        }
        else
        {
            // 探索する 7割の確率でアイテムを見つける うち、3割の確率で食べ物
            ayumegu.AddHp(-Random.Range(5, 16));
            ayumegu.AddSatietyLevel(-10);

            if(Random.Range(0f, 1f) >= 0.3f)
            {
                if(Random.Range(0f, 1f) <= 0.3f)
                {
                    ayumegu.AddItem(ItemKind.FOOD);
                    StateTestMain.Instance.ShowActionLog("食べ物を見つけました アイテム数:" + ayumegu.itemList.Count);
                }
                else
                {
                    ayumegu.AddItem(ItemKind.OTHER);
                    StateTestMain.Instance.ShowActionLog("木の枝を見つけました アイテム数:" + ayumegu.itemList.Count);
                }
            }
            else
            {
                StateTestMain.Instance.ShowActionLog("探索しましたが何も見つかりませんでした");
            }
        }
    }

    public override void Exit(AyumeguClass ayumegu)
    {
        Debug.Log("探索終了");
    }
}

こんな感じで全カセット分作る

あゆめぐのクラスはこんな感じ

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[System.Serializable]
public class AyumeguClass {

    public Place place;             // 場所
    public int hp;                  // 体力
    public int maxHp;               // 最大体力
    public int satietyLevel;        // 満腹度
    public int maxSatietyLevel;     // 最大満腹度
    public List<ItemKind> itemList; // 所持アイテムリスト 5こまでしかリュックに入らない
    public int maxItemCount;        // 最大所持アイテム数

    public State state;

    public AyumeguClass()
    {
        maxHp = 100;
        hp = maxHp;
        maxSatietyLevel = 100;
        satietyLevel = maxSatietyLevel;
        itemList = new List<ItemKind>();
        maxItemCount = 5;

        state = new SearchState();
    }

    // アイテム拾った
    public bool AddItem(ItemKind item)
    {
        if(itemList.Count < 5)
        {
            itemList.Add(item);
            return true;
        }
        else return false;
    }

    // ご飯食べる
    public void EatFood()
    {
        // 外であれば持ち物から減らす
        if(place == Place.FOREST)
        {
            for(int i = 0; i < itemList.Count; i++)
            {
                if(itemList[i] == ItemKind.FOOD)
                {
                    itemList.RemoveAt(i);
                    return;
                }
            }
        }
    }

    // お家に荷物を片付ける
    public void StashItem()
    {
        itemList = new List<ItemKind>();
    }

    // 食べ物がカバンに入っているか
    public bool CheckFood()
    {
        for(int i = 0; i < itemList.Count; i++)
        {
            if(itemList[i] == ItemKind.FOOD)
            {
                return true;
            }
        }
        return false;
    }

    // 場所を移動する
    public void MovePlace(Place place)
    {
        AddHp(-5);
        AddSatietyLevel(-5);
        this.place = place;
    }

    public void AddHp(int value)
    {
        hp += value;
        if(hp > maxHp) hp = maxHp;
        if(hp <= 0)
        {
            hp = 0;
            Debug.LogError("死亡");
        }
    }

    public void AddSatietyLevel(int value)
    {
        satietyLevel += value;
        if(satietyLevel > maxSatietyLevel) satietyLevel = maxSatietyLevel;
        if(satietyLevel <= 0)
        {
            satietyLevel = 0;
            Debug.LogError("空腹により死亡");
        }
    }

    // Hpの残り割合を返す
    public float GetHpRate()
    {
        return (float)hp / (float)maxHp;
    }

    // 空腹度残り割合を返す
    public float GetSatietyLevelRate()
    {
        return (float)satietyLevel / (float)maxSatietyLevel;
    }

    // 行動変更
    public void ChangeState(State state)
    {
        // 変更先の行動の後に戻る必要があれば登録する
        //if(isPrev) prevState = this.state;
        this.state = state;
        state.Enter(this);
        state.Execute(this);
    }

    // あゆめぐ行動する
    public void Action()
    {
        state.Execute(this);
    }
}

マネージャークラスはこんな感じ
ボタンを押すごとにayumeguのActionが呼ばれ1時間が経過する

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public enum Place
{
    HOME = 0,   // お家
    FOREST,     // 外
}

public enum ItemKind
{
    FOOD = 0,   // 食べ物
    OTHER,      // それ以外
}

public enum DayTime
{
    DAY = 0,    // 日中
    NIGHT,      // 夜
}

public class StateTestMain : SingletonMonoBehaviour<StateTestMain> {

    public int gameTime;        // ゲームの時間0~24 夜は19~5時
    public AyumeguClass ayumegu;

    void Awake()
    {
        gameTime = 8;
        ayumegu = new AyumeguClass();
    }

    public void NextActionBtnClick()
    {
        ayumegu.Action();   
        gameTime++;
        if(gameTime >= 24) gameTime = 0;
    }

    public DayTime GetDayTime()
    {
        if(gameTime <= 5 || 19 <= gameTime)
        {
            return DayTime.NIGHT;
        }
        return DayTime.DAY;
    }
}

とりあえずこんな感じにすればカセットごとに管理できるから追加なども楽です。
事前にちょっとカセット同士がどういう遷移をするかを整理しないといけないけれど。
今回はシンプルだけれど、本来はこのカセットの量も増えるし、外部からメッセージが飛んでくることもあるからそれを考慮しないといけない。また、前の行動にもどるなどもあるかもしれない。
例:時間を管理しているクラスから落雷のメッセージが来たら、各カセットで行動ごとのアクションを挟まないと・・・とか。

あとは行動査定の時にベイズの定理を使って行動確率を経験に基づいて算出、それを元にファジー理論で優先度付けとかやるともっと賢い子に育つ。今回の場合、学習するところがないけど、
例えば、家の在庫をみて食べ物が少なくなってきた。とする
どこで探索すると食べ物を拾える確率が高いかを経験に基づいてベイズの定理で算出
確率が高いところに行けば生き残りやすくなる

今回はいったんここまで。

18
22
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
18
22