1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unityで行動分析 ~その1:時隔スケジュール編~

Last updated at Posted at 2021-02-20

1. はじめに

  準備編 で作成した Operandum1 の Script を編集して、時隔スケジュールを作成します。基本的なことは準備編で一通り解説しているので、本記事では Operandum1 の Script の解説のみとなります。また、今回も「オペランダムへの反応」→「得点上昇」と「強化オペランダムへの反応」→「得点上昇」の2つの場合を考慮して解説したいと思います。

2. 時隔スケジュールとは

 時隔スケジュール(Interval schedule)とは、「1つ前の強化子の提示から一定時間経過後の最初の反応に強化子が随伴する(坂上・井上, 2018, pp.172)」強化スケジュールです。一定時間 ( スケジュール値 ) が固定である場合は固定時隔スケジュール ( Fixed Interval schedule; FI ) 、平均するとスケジュール値になる場合は変動時隔スケジュール ( Variable Interval schedule; VI ) と呼びます。
 Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( x sec ) 経過後に反応しなければ強化子を得られない ( あるいは Ramp が点灯しない ) ようにすれば良いです。時隔スケジュールのイメージ図を下に示します。下図の左は「オペランダムへの反応」→「得点上昇」を、下図の右は「強化オペランダムへの反応」→「得点上昇」を示しています。FIであれば x sec が常に一定となり、VIであれば x sec が毎回変動します。

3. FI

3.1. 「オペランダムへの反応」→「得点上昇」

 Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。

Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    float time;
    public float FITime;
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


    // New
    void Update()
    {
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                if (time >= FITime)
                {
                    audioSource.PlayOneShot(PointSE);
                    CountText.text = "Point : " + Point.ToString();
                    Point += 1;
                    time = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
        }
    }
}

 このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

変数の宣言

  • float time; ... float型の変数 time を宣言
  • public float FITime; ... public な float型の変数 FITime を宣言

    → Editor上ではFIのスケジュール値を入れてください
  • public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言

    → Editor上では Operandum1 に反応したときに鳴るSEを入れてください

Update()

  • **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
    • 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
    • **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
      • 効果音( Operandum1SE )が鳴る
      • **「if (time >= FITime)」**の処理 ... timeがFIのスケジュール値以上になったときの処理
        • 効果音( PointSE )が鳴る
        • 得点が1点上昇( Point += 1; )
        • time を0にリセット

          → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる

          → 弁別刺激が点灯している間、FIが走り続ける

3.2. 「強化オペランダムへの反応」→「得点上昇」

 Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。

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

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    float time;
    public float FITime;


    void ResetTime()
    {
        time = 0;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();
    }

    
    void Update()
    {
        // New_1
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);

                // New_2
                if (time >= FITime)
                {
                    Ramp_off.SetActive(false);
                    Ramp_on.SetActive(true);
                    Invoke("ResetTime", Ramp_Script.ReinforceableTime);
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
            Ramp_off.SetActive(true);
            Ramp_on.SetActive(false);
        }
    }
}

 このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

変数の宣言

  • float time; ... float型の変数 time を宣言
  • public float FITime; ... public な float型の変数 FITime を宣言

    → Editor上ではFIのスケジュール値を入れてください

Update()

  • New_1
    • **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
      • 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
      • **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        • 効果音( Operandum1SE )が鳴る
  • New_2
    • **「if (time >= FITime)」**の処理 ... timeがFIのスケジュール値以上になったときの処理
      • Ramp_off を非アクティブ化、Ramp_on をアクティブ化

        → 疑似的に強化可能ランプの点灯を表現
      • Invoke("ResetTime", Ramp_Script.ReinforceableTime)

        → 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット

4. VI

4.1. Pythonで x sec のリストを作成する

 VIでは、FIとは異なり、x sec が一定ではなく変動します。この変動した値をUnity上で作成しても良いのですが、先にPythonで x sec のリストを作成してCsv形式で出力しておきます。その後、作成したCsv形式の x sec のリストをUnityで読み込みます。

4.1.1. x sec のリストを作成する関数

  x sec のリストを作成する関数は下記のとおりです。環境について、Pythonのバージョンは「Python 3.7.1」で、Jupyter Notebookを使用しています。

Python
import numpy as np

def variable(value, value_min, value_max, reinforcement):
    for i in range(100**100):
        random_ = np.random.randint(value_min, value_max, reinforcement)
        if random_.mean()==value:
            variable = random_
            break
    return variable
  • forの中の処理
    • スケジュール値の範囲 ( value_min から value_max まで ) の乱数(一様分布)を reinforcement 分作成して1次元の行列にする
      • 乱数生成については こちら を参照してください
    • ifの中の処理
      • random_ の平均値がスケジュール値と同じになった場合、variable に random_ を格納
      • variable に random_ を格納したらforループを中断

        → スケジュール値の範囲がよほど無茶なものでない限り、100の100乗回のforループは行われない

4.1.2. x sec のリストをCsvファイルに出力

Python
# 「_」には、value, range_min, range_max, reinforcementの値を入れてください
value, range_min, range_max, reinforcement = _, _, _, _
variable = variable(value, range_min, range_max, reinforcement)

# 「/」の前にデータの出力先を入れてください
np.savetxt('/Variable.csv', variable, delimiter=',') 

 作成したCsvファイルは、Assetの中のResourcesというファイルを作成して、その中に入れます。

4.2. 「オペランダムへの反応」→「得点上昇」

 Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。

Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    bool first = true;
    float time;
    int i;
    int CsvCounter = 0;
    private List<string> CsvVariable = new List<string>();
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();

        //New_1
        TextAsset Csv = Resources.Load("Variable") as TextAsset;
        StringReader reader = new StringReader(Csv.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            string[] values = line.Split(',');

            // New_2
            for (i = 0; i < values.Length; i++)
            {
                CsvVariable.Add(values[i]);
            }
        }
    }


    void Update()
    {
        // New_3
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);

                // New_4
                if (first)
                {
                    if (time >= int.Parse(CsvVariable[CsvCounter]))
                    {
                        audioSource.PlayOneShot(PointSE);
                        CountText.text = "Point : " + Point.ToString();
                        Point += 1;
                        CsvCounter += 1;
                        time = 0;
                    }
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            time = 0;
        }
    }
}

 このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。5行目に「using System.IO;」が追加されているので注意してください。解説は「// New」と書かれている箇所のみ行います。

変数の宣言

  • bool first = true; ... bool型の変数 first が true であることを宣言
  • float time; ... float型の変数 time を宣言
  • int i; ... int型の変数 i を宣言
  • int CsvCounter = 0; ... int型の変数 CsvCounter が 0 であることを宣言
  • private List CsvVariable = new List(); ... string型の List として CsvVariable を宣言
  • public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言

    → Editor上では Operandum1 に反応したときに鳴るSEを入れてください

Start()

  • New_1
    • CsvファイルをUnityに読み込ませる
    • こちらの記事 とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
  • New_2
    • **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
      • C# のforループの書き方については こちら を参照してください

Update()

  • New_3
    • **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
      • 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
      • **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        • 効果音( Operandum1SE )が鳴る
  • New_4
    • **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
      • **「if (time >= int.Parse(CsvVariable[CsvCounter]))」**の処理 ... timeがVIのスケジュール値以上になったときの処理
        • 効果音( PointSE )が鳴る
        • 得点が1点上昇( Point += 1; )
        • CsvCounterが1つ増加

          → 取得するList内の要素の順番を1つずらす
        • time を0にリセット

          → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる

          → 弁別刺激が点灯している間、VIが走り続ける

4.3. 「強化オペランダムへの反応」→「得点上昇」

 Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。

Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    bool first = true;
    float time;
    int i;
    int CsvCounter = 0;
    private List<string> CsvVariable = new List<string>();


    void ResetTime()
    {
        time = 0;
        first = true;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();

        //New_1
        TextAsset Csv = Resources.Load("Variable") as TextAsset;
        StringReader reader = new StringReader(Csv.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            string[] values = line.Split(',');

            // New_2
            for (i = 0; i < values.Length; i++)
            {
                CsvVariable.Add(values[i]);
            }
        }
    }


    void Update()
    {
        // New_3
        if (Sd1_on.activeSelf)
        {
            time += Time.deltaTime;
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);

                // New_4
                if (first)
                {
                    if (time >= int.Parse(CsvVariable[CsvCounter]))
                    {
                        Ramp_off.SetActive(false);
                        Ramp_on.SetActive(true);
                        CsvCounter += 1;
                        first = false;
                        Invoke("ResetTime", Ramp_Script.ReinforceableTime);
                    }
                }
            }

            if (Sd1_off.activeSelf)
            {
                time = 0;
                Ramp_off.SetActive(true);
                Ramp_on.SetActive(false);
            }
        }
    }
}

 このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update() → ResetTime()となります。5行目に「using System.IO;」が追加されているので注意してください。また、ResetTime()では「time = 0;」だけではなく「first = true;」も書かれてあるので注意してください。解説は「// New」と書かれている箇所のみ行います。

変数の宣言

  • bool first = true; ... bool型の変数 first が true であることを宣言
  • float time; ... float型の変数 time を宣言
  • int i; ... int型の変数 i を宣言
  • int CsvCounter = 0; ... int型の変数 CsvCounter が 0 であることを宣言
  • private List CsvVariable = new List(); ... string型の List として CsvVariable を宣言

Start()

  • New_1
    • CsvファイルをUnityに読み込ませる
    • こちらの記事 とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
  • New_2
    • **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
      • C# のforループの書き方については こちら を参照してください

Update()

  • New_3
    • **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
      • 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
      • **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        • 効果音( Operandum1SE )が鳴る
  • New_4
    • **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
      • **「if (time >= int.Parse(CsvVariable[CsvCounter]))」**の処理 ... timeがVIのスケジュール値以上になったときの処理
        • Ramp_off を非アクティブ化、Ramp_on をアクティブ化

          → 疑似的に強化可能ランプの点灯を表現
        • CsvCounterが1つ増加

          → 取得するList内の要素の順番を1つずらす
        • first を false にする
          → 取得するList内の要素の順番が2つ以上ずれないようにする
        • Invoke("ResetTime", Ramp_Script.ReinforceableTime)

          → 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセットして、first を true に戻す

5. 最後に

 「時隔スケジュール(interval schedule)」の中の、「固定時隔スケジュール(Fixed Interval schedule)」と「変動時隔スケジュール(Variable Interval schedule)」をUnityで作る方法の解説を行いました。コードや用語等で間違っている点があれば、ご指摘いただけると幸いです。

参考URL

・NumPy, randomで様々な種類の乱数の配列を生成
https://note.nkmk.me/python-numpy-random/

・Unity で CSV ファイルを読み込む方法
https://note.com/macgyverthink/n/n83943f3bad60

・【Unity】C#の基本構文『for』
http://kimama-up.net/unity-for/

引用文献

坂上 貴之・井上 雅彦 (2018). 行動分析学──行動の科学的理解をめざして── 有斐閣

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?