1 引言
分享一下如何使用MATLAB中的心理學(xué)工具箱(Psychtoolbox,PTB)編寫一個完整的心理學(xué)實(shí)驗(yàn)。一般而言,任何行為實(shí)驗(yàn)都可以按照本文的框架來編寫。
今天要編寫的例子是一個很簡單的按鍵判斷的實(shí)驗(yàn),只有一個block,block里有五個trial。每個trial的流程圖如下。首先呈現(xiàn)一個500~1200ms的注視點(diǎn),然后隨機(jī)出現(xiàn)左箭頭或右箭頭,被試需要根據(jù)出現(xiàn)的刺激按方向鍵反應(yīng),如果500ms之內(nèi)按鍵則記錄反應(yīng)時、正確率等信息,并且箭頭消失,否則箭頭會呈現(xiàn)500ms的時間后自動消失,最后是300ms的空屏。

2 實(shí)驗(yàn)程序的編寫
2.1 創(chuàng)建文件夾、構(gòu)建程序的基本框架
首先新建一個文件夾,將來的實(shí)驗(yàn)程序文件都放在這個文件夾中,這樣子管理起來比較方便。我的叫“My_Psych_exp”,注意MATLAB讀取文件時有一些命名規(guī)范,一般采用英文命名即可,單詞與單詞之間用首字母大寫或下劃線來區(qū)分。

在MATLAB左邊的面板點(diǎn)擊進(jìn)入剛剛創(chuàng)建的文件夾,新建一個叫“exp_example”的文件夾,在該文件夾中通過快捷鍵Ctrl+N新建一個腳本。
開頭輸入如下三行代碼。clear是清除所有變量,clc是清除命令行窗口的代碼,sca相當(dāng)于Screen('CloseAll'),即關(guān)閉PTB里所有打開的窗口。
clear;
clc;
sca;
接下來輸入如下代碼。
try
HideCursor;
catch
ShowCursor;
sca;
psychrethrow(psychlasterror);
end
編寫實(shí)驗(yàn)程序時推薦采用這種“try……catch……end”的框架,實(shí)驗(yàn)的主要代碼就放在try和catch中間,這樣萬一出現(xiàn)錯誤,就會執(zhí)行catch之后的語句。
我們一般做行為實(shí)驗(yàn)時都是通過按鍵反應(yīng),鼠標(biāo)指針是隱藏起來的,因此可以在一開始使用HideCursor隱藏鼠標(biāo)指針,同時在catch部分寫上ShowCursor,這樣程序出錯時鼠標(biāo)指針就會顯示出來。而sca會關(guān)閉窗口,psychrethrow(psychlasterror)則會在命令行窗口告訴我們出錯的代碼的位置。
2.2 創(chuàng)建數(shù)據(jù)表格
現(xiàn)在,讓我們來看看整個實(shí)驗(yàn)程序最重要的部分:數(shù)據(jù)。
在MATLAB中,我們的數(shù)據(jù)都儲存在變量中,雖然可以直接將這些變量保存為“mat”格式的文件,并在MATLAB中處理這些數(shù)據(jù),但最好還是將實(shí)驗(yàn)數(shù)據(jù)統(tǒng)一整理并導(dǎo)出為外部文件,以便將來進(jìn)行查閱和統(tǒng)計(jì)分析。
在這個例子中,我們想要收集的信息有:被試的基本信息(包括被試編號、性別、年齡和利手)、注視點(diǎn)呈現(xiàn)時間、箭頭刺激的朝向、反應(yīng)按鍵、反應(yīng)時、正確率。并且這個實(shí)驗(yàn)總共有5個trial。因此預(yù)期的數(shù)據(jù)矩陣的格式如下(為了方便理解,添加了表頭)。

在實(shí)驗(yàn)過程中,我們需要時不時地從這么一個矩陣中讀寫信息,例如將被試的反應(yīng)按鍵記錄到矩陣中,然后在計(jì)算正確率的時候再將這一信息讀取出來使用。為此,我們先創(chuàng)建一個名為“data”的cell矩陣,矩陣的大小就按照我們預(yù)期的要求。之所以使用cell,是因?yàn)槲覀兊臄?shù)據(jù)信息同時包含了數(shù)值和字符串。現(xiàn)在,讓我們將下面這段代碼添加至“try……”的后面。
% Create a cell array to save data
data = cell(5,9);
2.3 收集被試的基本信息
在開始實(shí)驗(yàn)前,我們還需要收集被試的基本信息。
使用過E-Prime的同學(xué)應(yīng)該知道,E-Prime運(yùn)行實(shí)驗(yàn)程序時,會出現(xiàn)一些收集被試的基本信息的對話框。在MATLAB中,我們同樣可以采用對話框的方式來收集被試的信息。
% Obtain some information of experiment
prompt = {'Subject Number','Gender[1 = m, 2 = f]','Age','Handeness[1 = left, 2 = right]'};
title = 'Exp infor'; % The title of the dialog box
definput = {'','','',''}; % Default input value(s)
% Using inputdlg() to obtain information and save it to cell array
data(1:5, 1:4) = repmat(inputdlg(prompt,title,[1, 50],definput)', 5, 1);
這里用到了MATLAB自帶的inputdlg函數(shù),其作用是打開一個收集用戶輸入的對話框,并將輸入的信息賦給一個變量(在這里就是賦給我們剛剛創(chuàng)建的cell矩陣?yán)玻?。至?code>inputdlg中的參數(shù),prompt是對需要輸入的信息的提示,title就是對話框標(biāo)題,“[1, 50]”是輸入框的維度,表示對話框的高度為一個字符,長度為50個字符。如果需要增加、減少想要收集的信息,則在prompt這個變量里添加/刪除一下就可以了。
接著,我們將收集到的信息,通過'(英文的單引號,效果是將行變列、將列變行)進(jìn)行轉(zhuǎn)置,并通過repmat函數(shù)復(fù)制5次后,寫入我們剛剛創(chuàng)建的數(shù)據(jù)矩陣中。先讓我們看一下最終的效果。

其實(shí)這些信息只需要一行就夠了,我這么做只是為了美觀一點(diǎn)……
2.4 打開窗口
繼續(xù)在try部分添加下面這些代碼。代碼的作用是打開一個窗口(這個窗口便是我們呈現(xiàn)實(shí)驗(yàn)刺激的地方),同時我們還設(shè)置了一些所需的參數(shù)。
HideCursor;
% Open a black backgound window
[w, wrect] = Screen('OpenWindow', 0, [0, 0, 0]);
% Define the center coordinates
[x_center, y_center] = RectCenter(wrect);
% Measure the vertical refresh rate of the monitor
ifi = Screen('GetFlipInterval', w);
% Text font and text color
Screen('TextFont', w, 'Simhei');
Screen('TextSize', w, 65);
接下來解釋一下各行代碼。
Screen('OpenWindow')的作用是打開一個窗口,我們將這個窗口命名為“w”,“wrect”返回的值是窗口的分辨率,0是顯示器的編號,第一個顯示器的編號是0,后面依次是1、2、3……,當(dāng)存在多個顯示器時,可以通過代碼自動獲取各個顯示器的編號,這里不再贅述。[0, 0, 0] 是窗口的顏色,這里以RGB的方式編寫,三個0就是黑色,三個255就是白色,很好記。
RectCenter(wrect)是獲取顯示器屏幕的中心點(diǎn)的位置,之后我們可以用x_center和y_center這兩個值作為坐標(biāo)來呈現(xiàn)注視點(diǎn)。
Screen('GetFlipInterval')的目的是獲取當(dāng)前顯示器每刷新一幀所需的秒數(shù),即每次“Flip”所需的時間,后面講到timing的時候會介紹這一數(shù)值的作用。
最后兩行是設(shè)置窗口中呈現(xiàn)的文本的字體、字號,“w”就是我們定義的窗口的名字啦,要想在PTB中正常顯示中文,就需要選擇一個支持中文的字體,這里選擇的是黑體“SimHei”,字號是65。
2.5 設(shè)置按鍵
接下來設(shè)置一下我們需要用到的按鍵,也就是鍵盤上的左、右方向鍵,然后我們再設(shè)置一個退出鍵——“Esc”,中途需要退出時便可以使用這個按鍵。
% Define some keyboard keys
KbName('UnifyKeyNames');
left_key = KbName('LeftArrow');
right_key = KbName('RightArrow');
esc_key = KbName('escape');
2.6 準(zhǔn)備實(shí)驗(yàn)材料
接下來,我們要準(zhǔn)備兩個內(nèi)容:其一是所有trial的注視點(diǎn)的呈現(xiàn)時間(在500ms至1200ms之內(nèi)隨機(jī)決定 ),其二是所有trial的箭頭刺激的朝向(朝左或朝右)。
% Prepare materials
for i = 1:5
data{i, 5} = unifrnd(0.5,1.2); % Fixation time
if unidrnd(2) == 1 % Arrow orientation
data{i, 6} = '←';
else
data{i, 6} = '→';
end
end
這里用了一個循環(huán)語句,從而依次生成5個trial的注視點(diǎn)呈現(xiàn)時間和箭頭朝向,并將這些信息寫入我們的數(shù)據(jù)矩陣中。首先通過unifrnd(0.5,1.2)隨機(jī)生成 [0.5,1.2] 之中的值,作為注視點(diǎn)的呈現(xiàn)時間。其次,通過unifrnd(2)生成一個隨機(jī)數(shù),這個隨機(jī)數(shù)為1或2,我們通過邏輯語句對這個數(shù)值進(jìn)行判斷,當(dāng)生成的隨機(jī)數(shù)是1時,這次trial的箭頭刺激朝左,否則朝右。
不過,做正式實(shí)驗(yàn)的時候,正確的做法應(yīng)該是將刺激序列保存為一個外部的表格文件,每次運(yùn)行腳本時,就調(diào)用一次表格文件,同時按需要將其順序打亂從而以隨機(jī)序列的方式呈現(xiàn)刺激。本文之所以將所有代碼都放置在一個腳本中,只是為了方便大家理解。
2.7 呈現(xiàn)指導(dǎo)語
然后便是準(zhǔn)備指導(dǎo)語了,雖然我們可以用DrawText或DrawFormattedText直接在屏幕上“畫”出指導(dǎo)語,但是為了美觀,還是推薦通過讀取事先制作好的圖片來呈現(xiàn)指導(dǎo)語。
我們可以在PowerPoint中編寫指導(dǎo)語界面,保存為圖片并放入文件夾中,最后用MakeTexture函數(shù),將指導(dǎo)語和結(jié)束語圖片賦給exp_instruction和exp_end兩個變量,這樣就準(zhǔn)備完成了。需要的時候,用DrawTexture“畫”出來即可。
現(xiàn)在我們先呈現(xiàn)指導(dǎo)語,然后通過KbStrokeWait等待按鍵,以便被試在閱讀完指導(dǎo)語后按任意鍵繼續(xù)。
% Load pictures
exp_instruction = Screen('MakeTexture', w, imread('pic\exp_instruction.tif'));
exp_end = Screen('MakeTexture', w, imread('pic\exp_end.tif'));
% Display instruction
Screen('DrawTexture', w, exp_instruction, []);
Screen('Flip', w);
KbStrokeWait;
需要注意的是,這里我們將指導(dǎo)語和結(jié)束語的圖片放在了實(shí)驗(yàn)程序文件夾中的一個叫“pic”的子文件夾,之后輸出的實(shí)驗(yàn)數(shù)據(jù)文件我們會放到另一個叫“data”的子文件夾,這樣做是為了方便整理,以免各種實(shí)驗(yàn)材料、數(shù)據(jù)、程序都放在同一個文件夾中而導(dǎo)致過于混亂。
指導(dǎo)語和結(jié)束語的圖片如下,你可以將其另存為tif格式的圖片,然后放置在“pic”文件夾中。


2.8 構(gòu)建trial的框架
接下來便是編寫一個trial之中的內(nèi)容,即呈現(xiàn)注視點(diǎn)呀、收集反應(yīng)信息呀什么的,我們采用“for……end”的格式。即該實(shí)驗(yàn)有五個trial,循環(huán)運(yùn)行五個trial之后就結(jié)束這一部分。trial = 1:5中的trial是計(jì)數(shù)用的變量,寫成i = 1:5或任何其它名稱都可以(當(dāng)然不能是MATLAB中的保留字),為了方便理解,這里寫為trial。
% Loop for the total number of trials
for trial = 1:5
end
2.9 呈現(xiàn)注視點(diǎn)
我們可以采用DrawDots繪制注視點(diǎn),其中的參數(shù)是指,在“w”窗口的中心畫出一個點(diǎn),大小是10,顏色是白色。
% Fixation: 500~1200ms
for i = 1:round(cell2mat(data(trial, 5)) / ifi)
Screen('DrawDots', w, [x_center; y_center], 10, [255,255,255], [], 1);
Screen('Flip', w);
end
至于注視點(diǎn)的時間,需要先從我們的數(shù)據(jù)矩陣中調(diào)用剛剛隨機(jī)生成的值(即cell2mat(data(trial, 5))),這個值是秒數(shù),我們將這個值除以ifi,并通過round取整,便得到該時間段在這臺顯示器上對應(yīng)的幀數(shù)。例如,我的顯示器的ifi是0.0167,一秒對應(yīng)的幀數(shù)就是1/0.0167≈60,也就是說我的顯示器的FPS(frames per second)是60幀/秒,也就是60Hz的刷新率。
所以,我們只需根據(jù)秒數(shù)算出幀數(shù)x,然后反復(fù)繪制x次刺激并通過Screen('Flip')刷新屏幕(繪制的刺激是在緩沖區(qū),需要刷新屏幕使其呈現(xiàn)出來),便可以在指定的時間內(nèi)精確地呈現(xiàn)刺激。
2.10 呈現(xiàn)刺激
注視點(diǎn)消失后便是箭頭刺激的呈現(xiàn),很簡單,和呈現(xiàn)注視點(diǎn)是類似的套路。DrawFormattedText的第二個參數(shù)是我們想要呈現(xiàn)的文本,這里同樣從數(shù)據(jù)矩陣中調(diào)用已經(jīng)隨機(jī)生成好的信息。
% Stimulus: 500ms
DrawFormattedText(w, data{trial, 6}, 'center', 'center', [255,255,255]);
Screen('Flip', w);
2.11 收集反應(yīng)信息
現(xiàn)在我們需要收集被試的反應(yīng)啦。首先試著分析一下如何達(dá)到這個目的。我們希望被試在箭頭刺激呈現(xiàn)的500ms之內(nèi)反應(yīng),所以可以通過一個while循環(huán)來實(shí)現(xiàn),只要刺激呈現(xiàn)的時間距離當(dāng)前時間的差值在500ms以內(nèi),則執(zhí)行一些語句,以便記錄被試的按鍵、反應(yīng)時、正確率等信息,并提前結(jié)束刺激的呈現(xiàn),如果超出500ms,則直接結(jié)束刺激的呈現(xiàn),然后將按鍵、反應(yīng)時和正確率記錄為空值。
現(xiàn)在,我們按照上述的構(gòu)思,一步步寫出代碼。
首先,我們通過GetSecs獲取一個時間戳,賦給t0,作為刺激呈現(xiàn)的起始時間點(diǎn),之后我們可以隨時獲取新的時間戳,并減去t0,得到的便是自刺激呈現(xiàn)之后經(jīng)過的時間。
讓我們來定義一個While循環(huán),使腳本在呈現(xiàn)刺激后的500ms內(nèi),反復(fù)運(yùn)行while循環(huán)內(nèi)的語句。
% Record responses data
t0 = GetSecs;
while GetSecs - t0 < 0.5
end
接著在循環(huán)內(nèi)添加以下代碼,其作用是檢查被試按下的按鍵。
[keyisdown, secs, keycode] = KbCheck;
然后通過if語句判斷,如果按的是Esc鍵,則直接結(jié)束腳本。
if keycode(esc_key)
sca;
return
如果是按了其它的按鍵,則將按鍵的名稱和反應(yīng)時間(GetSecs - t0)記錄到數(shù)據(jù)矩陣中。
elseif keyisdown
data{trial, 7} = KbName(keycode); % resp
data{trial, 8} = GetSecs - t0; % rt
接著便是判斷此次反應(yīng)的正確率了,如果按鍵為左方向鍵且刺激為左箭頭,或者按鍵為右方向鍵且刺激為右箭頭,則該試次的正確率記為1,反之記為0,最后通過break來結(jié)束循環(huán)。
% acc
if keycode(left_key) && data{trial, 6} == '←'
data{trial, 9} = 1; % acc=1
elseif keycode(right_key) && data{trial, 6} == '→'
data{trial, 9} = 1; % acc=1
else
data{trial, 9} = 0; % acc=0
end
break % break the loop
還有一種情況是沒有按鍵反應(yīng),此時我們將反應(yīng)按鍵、反應(yīng)時和正確率都記為NA(當(dāng)然,不同的實(shí)驗(yàn)設(shè)計(jì),記錄的方式是不同的,這里只是舉個例子)。
else
data{trial, 7} = 'NA'; % resp
data{trial, 8} = 'NA'; % rt
data{trial, 9} = 'NA'; % acc
end
end
該部分完整的代碼如下:
% Record responses data
t0 = GetSecs;
while GetSecs - t0 < 0.5
[keyisdown, secs, keycode] = KbCheck;
if keycode(esc_key)
sca;
return
elseif keyisdown
data{trial, 7} = KbName(keycode); % resp
data{trial, 8} = GetSecs - t0; % rt
% acc
if keycode(left_key) && data{trial, 6} == '←'
data{trial, 9} = 1; % acc=1
elseif keycode(right_key) && data{trial, 6} == '→'
data{trial, 9} = 1; % acc=1
else
data{trial, 9} = 0; % acc=0
end
break % break the loop
else
data{trial, 7} = 'NA'; % resp
data{trial, 8} = 'NA'; % rt
data{trial, 9} = 'NA'; % acc
end
end
2.12 呈現(xiàn)空屏
之后便是300ms的空屏,刷新一下屏幕然后等待0.3秒即可。
% Interval: 300ms
Screen('Flip', w);
WaitSecs(0.3);
2.13 呈現(xiàn)結(jié)束語
至此,實(shí)驗(yàn)主體部分的內(nèi)容都完成了,接下來便是呈現(xiàn)結(jié)束語并退出實(shí)驗(yàn)程序。這里我們設(shè)置的是在呈現(xiàn)結(jié)束語的1秒后自動退出。
% Display instruction2
for i = 1:round(1 / ifi)
Screen('DrawTexture', w, exp_end, []);
Screen('Flip', w);
end
sca;
2.14 保存數(shù)據(jù)文件
但退出窗口之后,我們還有一個很重要的任務(wù),也就是將收集到的數(shù)據(jù)保存起來。之所以在呈現(xiàn)結(jié)束語、退出窗口后才保存數(shù)據(jù),是因?yàn)閷?shù)據(jù)寫入表格是需要一定的時間的,如果在呈現(xiàn)結(jié)束語之前寫入數(shù)據(jù),會導(dǎo)致trial和結(jié)束語之間有一個短暫的“卡頓”,就讓人感覺程序運(yùn)行起來不夠“絲滑”了。
代碼如下。
% Put all data to a table object
header = {'SubjectNumber', 'Gender', 'Age', 'Handedness',...
'DotsTime', 'Arrow', 'Resp', 'RT', 'ACC'};
data_table = cell2table(data, 'VariableNames', header);
% Create a csv file to save data
exp_data = strcat('data\', 'exp_example_', char(data{1,1}), '_', date, '.csv');
writetable(data_table, exp_data);
現(xiàn)在說明一下這幾行代碼的含義。
首先我們將cell格式的數(shù)據(jù)矩陣轉(zhuǎn)換為table。之所以轉(zhuǎn)換為table的形式,是因?yàn)槲覀兿M玫降臄?shù)據(jù)文件是帶有表頭的,header這個變量的內(nèi)容就是我們的表頭。然后,我們通過writetable將這個table寫入csv格式的表格文件。csv是一個開放性很好的數(shù)據(jù)文件格式,包括Excel、SPSS、R和Mplus在內(nèi)的各種軟件都支持對csv的讀寫。然后,我們通過strcat函數(shù)將各種字符和字符變量串起來,作為數(shù)據(jù)文件的文件名,在這里,exp_data這個變量包含了需要保存的數(shù)據(jù)文件的目錄(保存在“data”這個子文件夾里)、文件名和文件格式。其中文件名由實(shí)驗(yàn)的名稱(“exp_example”)、被試的編號和實(shí)驗(yàn)日期組成(date函數(shù)的功能是獲取當(dāng)前日期,當(dāng)然我們還可以通過datestr函數(shù)獲取更多樣化的日期和時間格式,作為文件名中的實(shí)驗(yàn)日期)。
2.15 收尾
完事之后,在命令行窗口輸出一些信息,這樣我們就知道程序是完全正常地運(yùn)行結(jié)束了。
disp('Succeed!');
對了,如果你無法運(yùn)行這個腳本并且報錯信息指向Screen('OpenWindow')這行代碼,此時可能的原因有很多,其中一個主要的原因是電腦性能可能不夠了……
解決的方法則是在腳本的開頭添加下面這行代碼。
Screen('Preference', 'SkipSyncTests', 2);
這行代碼還有一個作用,就是跳過每次運(yùn)行腳本時會出現(xiàn)的冗長的初始化界面。當(dāng)然,為了使程序的timing更精確,正式實(shí)驗(yàn)時最好還是去掉這一行代碼。
3 實(shí)驗(yàn)程序的運(yùn)行效果
好啦,現(xiàn)在試著運(yùn)行一下看看效果。點(diǎn)擊運(yùn)行,同時系統(tǒng)提示我們選擇文件的保存位置和文件名,我就命名為“exp_demo”啦。

運(yùn)行結(jié)束后,命令行窗口顯示了“Succeed!”,說明是順利運(yùn)行了!
現(xiàn)在打開數(shù)據(jù)文件,看看數(shù)據(jù)的保存情況。

可以發(fā)現(xiàn),注視點(diǎn)的呈現(xiàn)時間確實(shí)如我們期望的一樣,處于500~1200ms的范圍之內(nèi)(為了確保符合預(yù)期效果可以多跑幾次看看)。剛剛運(yùn)行時,第一、第二個trial我按了正確的按鍵,第三個按了未定義的按鍵(上方向鍵),第四個按了相反的按鍵,第五個trial沒有按鍵反應(yīng),這些信息都在表格中記錄下來了。在這個程序里,對于“按了錯誤的按鍵”與“未按任何按鍵”,記錄的信息是一樣的,如果需要區(qū)分的話,可以繼續(xù)在程序里進(jìn)行修改完善。
最后來看一眼該實(shí)驗(yàn)程序所有的文件,m文件就是我們的實(shí)驗(yàn)程序,“pic”文件夾里放的是指導(dǎo)語的圖片,“data”文件夾里的是剛剛運(yùn)行后收集到的數(shù)據(jù),這些文件都保存在“exp_example”這個文件夾之下。

實(shí)驗(yàn)程序的全部代碼如下。加上注釋和換行也只有136行~
% exp_example_procedure.m
%
% A very simple behavioral experiment demo of PTB-3,
% shows you how to create visual stimulus and record responses data.
%
% Wei Zi-Qian, 2020, August
clear;
clc;
sca;
Screen('Preference', 'SkipSyncTests', 2);
try
% Create a cell array to save data
data = cell(5,9);
% Obtain some information of experiment
prompt = {'Subject Number','Gender[1 = m, 2 = f]','Age','Handeness[1 = left, 2 = right]'};
title = 'Exp infor'; % The title of the dialog box
definput = {'','','',''}; % Default input value(s)
% Using inputdlg() to obtain information and save it to cell array
data(1:5, 1:4) = repmat(inputdlg(prompt,title,[1, 50],definput)', 5, 1);
HideCursor;
% Open a black backgound window
[w, wrect] = Screen('OpenWindow', 0, [0, 0, 0]);
% Define the center coordinates
[x_center, y_center] = RectCenter(wrect);
% Measure the vertical refresh rate of the monitor
ifi = Screen('GetFlipInterval', w);
% Text font and text color
Screen('TextFont', w, 'Simhei');
Screen('TextSize', w, 65);
% Define some keyboard keys
KbName('UnifyKeyNames');
left_key = KbName('LeftArrow');
right_key = KbName('RightArrow');
esc_key = KbName('escape');
% Prepare materials
for i = 1:5
data{i, 5} = unifrnd(0.5,1.2); % Fixation time
if unidrnd(2) == 1 % Arrow orientation
data{i, 6} = '←';
else
data{i, 6} = '→';
end
end
% Load pictures
exp_instruction = Screen('MakeTexture', w, imread('pic\exp_instruction.tif'));
exp_end = Screen('MakeTexture', w, imread('pic\exp_end.tif'));
% Display instruction
Screen('DrawTexture', w, exp_instruction, []);
Screen('Flip', w);
KbStrokeWait;
% Loop for the total number of trials
for trial = 1:5
% Fixation: 500~1200ms
for i = 1:round(cell2mat(data(trial, 5)) / ifi)
Screen('DrawDots', w, [x_center; y_center], 10, [255,255,255], [], 1);
Screen('Flip', w);
end
% Stimulus: 500ms
DrawFormattedText(w, data{trial, 6}, 'center', 'center', [255,255,255]);
Screen('Flip', w);
% Record responses data
t0 = GetSecs;
while GetSecs - t0 < 0.5
[keyisdown, secs, keycode] = KbCheck;
if keycode(esc_key)
sca;
return
elseif keyisdown
data{trial, 7} = KbName(keycode); % resp
data{trial, 8} = GetSecs - t0; % rt
% acc
if keycode(left_key) && data{trial, 6} == '←'
data{trial, 9} = 1; % acc=1
elseif keycode(right_key) && data{trial, 6} == '→'
data{trial, 9} = 1; % acc=1
else
data{trial, 9} = 0; % acc=0
end
break % break the loop
else
data{trial, 7} = 'NA'; % resp
data{trial, 8} = 'NA'; % rt
data{trial, 9} = 'NA'; % acc
end
end
% Interval: 300ms
Screen('Flip', w);
WaitSecs(0.3);
end
% Display instruction2
for i = 1:round(1 / ifi)
Screen('DrawTexture', w, exp_end, []);
Screen('Flip', w);
end
sca;
% Put all data to a table object
header = {'SubjectNumber', 'Gender', 'Age', 'Handedness',...
'DotsTime', 'Arrow', 'Resp', 'RT', 'ACC'};
data_table = cell2table(data, 'VariableNames', header);
% Create a csv file to save data
exp_data = strcat('data\', 'exp_example_', char(data{1,1}), '_', date, '.csv');
writetable(data_table, exp_data);
disp('Succeed!');
catch
ShowCursor;
sca;
psychrethrow(psychlasterror);
end
4 擴(kuò)展
4.1 操作多個圖片的方法
舉個例子,假如我們有20張刺激圖片,名叫1.png、2.png、3.png……20.png,這些圖片放在實(shí)驗(yàn)程序所在文件夾中的名為pic的子文件夾。那么,我們可以通過以下代碼,將這些圖片依次加載至MATLAB,并統(tǒng)一放置在名為img的數(shù)組中。
% Prepare image
img = zeros(20, 1);
for i = 1:20
pic = strcat('pic\',string(i),'.png');
img(i) = Screen('MakeTexture', w, imread(pic));
end
上述這段代碼位于打開窗口之后,循環(huán)呈現(xiàn)指導(dǎo)語之前。更多的圖片也是類似的操作,修改一下參數(shù)即可。
接著,如果我們是想隨機(jī)呈現(xiàn)圖片的話,可以用以下代碼打亂img的順序。
img=img(randperm(length(img))); % random order
某些情況下,我們還可以反復(fù)使用這一行代碼來打亂img的順序。例如實(shí)驗(yàn)設(shè)計(jì)有n個block,每個block的刺激內(nèi)容都是相同的,只是順序不同,此時便可以在每個block的起始位置添加該代碼,便能輕松實(shí)現(xiàn)隨機(jī)呈現(xiàn)的效果。
之后便是在每個trial中呈現(xiàn)圖片刺激了,非常簡單,只需要將trial的數(shù)值作為索引來調(diào)用img中的圖片。注意我這里將圖片調(diào)整為255*255像素的大小,你可以根據(jù)自己的實(shí)驗(yàn)設(shè)計(jì)來修改這一參數(shù)。
% Stimulus: 500ms
Screen('DrawTexture', w, img(trial), [0 0 255 255]);
Screen('Flip', w);
最后,也是最重要的一點(diǎn),在保存每個trial的反應(yīng)信息的時候,千萬別忘了記錄這個trial中呈現(xiàn)的img的類型。
在這個例子中,圖片有兩種類型,如下。
condition1 = [11,12,13,14,15,16,17,18,19,20];
condition2 = [21,22,23,24,25,26,27,28,29,30];
其中的11、12、13等等,是img中的值,代表每個圖片,至于為什么會是這些值,我就不太清楚了,將圖片載入MATLAB后就是這樣的,我只是根據(jù)載入后的結(jié)果寫了這些代碼……
于是每個trial的結(jié)尾便可以通過以下代碼判斷img的類型。
% Save img type
if ismember(img(trial), condition1)
imgType = 'condition1';
elseif ismember(img(trial), condition2)
imgType = 'condition2';
end
這里的imgType只是舉例,你也可以直接將判斷的結(jié)果放入到保存所有數(shù)據(jù)的matrix中。
4.2 略
等待施工……
5 結(jié)語
本文的目的是用盡量簡單的方法在PTB中編寫一個心理學(xué)實(shí)驗(yàn)程序。不過鑒于作者也只是剛剛開始自學(xué)PTB,水平有限,某些部分可能會有更便捷的方法。以及,文中難免會有一些錯漏,請大家多多指正。
最后再說一下,在學(xué)習(xí)PTB的過程中,對于任何不清楚的部分,請善用MATLAB中的幫助系統(tǒng)。對于MATLAB自帶的函數(shù),可以通過help函數(shù)查看其功能(例如help writetable),對于PTB中的函數(shù),例如DrawText,可以通過Screen DrawText?這樣的形式查看其功能。此外,“Psychtoolbox”文件夾中的有一個名為“PsychDemos”的文件夾,其包含著許多展示PTB功能的demo。官網(wǎng)里也有不少學(xué)習(xí)PTB的資料,大家可以自行學(xué)習(xí),例如,“Tutorial”這個鏈接里就有很多介紹PTB功能的范例(其中第一、第二部分是入門PTB必看的)。
?
----------2020.10.23更新----------
修改了一處冗余的代碼:將61行的char(data(trial, 6))改成了data{trial, 6}
----------2020.12.30更新----------
更改了timing的方式:將“以秒為單位呈現(xiàn)刺激”改為了“以幀為單位呈現(xiàn)刺激”
更改了文章中的幾處表述,對“收集反應(yīng)信息”一節(jié)的內(nèi)容做了更詳細(xì)的說明
----------2021.01.12更新----------
修改了一處錯誤
----------2021.01.31更新----------
添加了擴(kuò)展內(nèi)容4.1