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

# 1. はじめに
　[ 準備編 ](https://qiita.com/TeRa_YUKI/items/b9433da24af552d6b3e3)で作成した *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* が毎回変動します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555056/4354839a-fe90-6ce3-c629-6d2a5c8f03d3.png" width=90%>

# 3. FI
## 3.1. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#: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 を宣言<br>
 → Editor上ではFIのスケジュール値を入れてください
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → 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にリセット<br>
             → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、FIが走り続ける

## 3.2. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#: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 を宣言<br>
 → 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 をアクティブ化<br>
         → 疑似的に強化可能ランプの点灯を表現
        * Invoke("ResetTime", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、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: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次元の行列にする
        * 乱数生成については[ **こちら** ](https://note.nkmk.me/python-numpy-random/)を参照してください
    * ifの中の処理
        * random_ の平均値がスケジュール値と同じになった場合、variable に random_ を格納
        * variable に random_ を格納したらforループを中断<br>
 → スケジュール値の範囲がよほど無茶なものでない限り、100の100乗回のforループは行われない

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

```Python: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というファイルを作成して、その中に入れます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555056/0fe03efe-1f59-6140-1500-d061444a66ad.png" width=35%>



## 4.2. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#: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<string> CsvVariable = new List<string>(); ... string型の List として CsvVariable を宣言
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → Editor上では *Operandum1* に反応したときに鳴るSEを入れてください

**Start()**

* New_1
    * CsvファイルをUnityに読み込ませる
    * [ **こちらの記事** ](https://note.com/macgyverthink/n/n83943f3bad60)とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
* New_2
    * **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
        * C# のforループの書き方については[ **こちら** ](http://kimama-up.net/unity-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つ増加<br>
            → 取得するList内の要素の順番を1つずらす
            * time を0にリセット<br>
             → 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、VIが走り続ける

## 4.3. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#: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<string> CsvVariable = new List<string>(); ... string型の List として CsvVariable を宣言

**Start()**

* New_1
    * CsvファイルをUnityに読み込ませる
    * [ **こちらの記事** ](https://note.com/macgyverthink/n/n83943f3bad60)とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
* New_2
    * **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
        * C# のforループの書き方については[ **こちら** ](http://kimama-up.net/unity-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 をアクティブ化<br>
             → 疑似的に強化可能ランプの点灯を表現
            * CsvCounterが1つ増加<br>
            → 取得するList内の要素の順番を1つずらす
            * first を false にする
            → 取得するList内の要素の順番が2つ以上ずれないようにする
            * Invoke("ResetTime", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、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). 行動分析学──行動の科学的理解をめざして── 有斐閣