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 にはなかったコードです。
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 にはなかったコードです。
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 )が鳴る
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- New_2
- **「if (time >= FITime)」**の処理 ... timeがFIのスケジュール値以上になったときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現 - Invoke("ResetTime", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
- **「if (time >= FITime)」**の処理 ... timeがFIのスケジュール値以上になったときの処理
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を使用しています。
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ループは行われない
- スケジュール値の範囲 ( value_min から value_max まで ) の乱数(一様分布)を reinforcement 分作成して1次元の行列にする
4.1.2. x sec のリストをCsvファイルに出力
# 「_」には、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 にはなかったコードです。
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ループの書き方については こちら を参照してください
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
Update()
- New_3
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- New_4
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
- **「if (time >= int.Parse(CsvVariable[CsvCounter]))」**の処理 ... timeがVIのスケジュール値以上になったときの処理
- 効果音( PointSE )が鳴る
- 得点が1点上昇( Point += 1; )
- CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす - time を0にリセット
→ 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、VIが走り続ける
- **「if (time >= int.Parse(CsvVariable[CsvCounter]))」**の処理 ... timeがVIのスケジュール値以上になったときの処理
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
4.3. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
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ループの書き方については こちら を参照してください
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
Update()
- New_3
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 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 に戻す
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
- **「if (time >= int.Parse(CsvVariable[CsvCounter]))」**の処理 ... timeがVIのスケジュール値以上になったときの処理
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
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). 行動分析学──行動の科学的理解をめざして── 有斐閣