A quick overview

The task structure can be used to help code experiments, it is completely separate from the basic mgl libarary that is used to display to the screen (in that you do not have to use the task code to use the basic mgl functions).

The structure for these experiments involves three main variables:

myscreen: Holds information about the screen parameters like resolution, etc.
task: Holds info about the block/trial/segment structure of the experiment
stimulus: Holds structures associated with the stimulus.

To create and run an experiment, your program will do the following:

  1. Initialize the screen.
  2. Set up the task structure. The task structure holds information about the parameters you want to randomize over and the timing of your experiment.
  3. Initialize the stimulus. Here you will create all the necessary bitmaps or display structures that you will need to display your stimulus.
  4. Create callback functions. These functions will run at various times in the experiment like at the beginning of a trial or when the subject responds with a keypress or before each display refresh. They are the main way that you program how your stimulus will display and what to do when you get subject responses etc.
  5. Create a display loop. This is the part that actually runs your experiment. Essentially all you have to do is call updateTask which handles all the hard work of running your task.

The basic idea of how to set up your experiment with these structures requires defining some terms. Going from the largest organization down to the smallest:

  • Task: Task refers to the overall experiment. The task is the top level structure. It contains all the parameters that you are testing as well as the information about how the trials are to be run. A task might be the parameters for a set of trials in which you show different visual stimuli. Or a set of trials that run a psychophysical staircase. Note that in some cases you might have more than one task running at the same time. For example, if you are running a retinotopy scan, you may want to have the retinotopic stimuli as one task and a staircased fixation task as the second task.
  • Phases: Tasks may sometimes have more than one phase. For example you may want to show an adaptation stimulus for 30 seconds at the beginning of your experiment in one phase, and then go on to the next phase of the experiment in which you will have randomized trials.
  • Blocks: A block is a set of trials in which each combination of parameters is presented in one trial. The code takes care of properly randomizing your trials so that in each block of trials each stimulus type is presented once. (You can also choose not to randomize).
  • Trials: A single trial of an experiment.
  • Segments: Segments divide up the time in a trial. For example you may have one segment with a fixation cross, another segment where the stimulus is presented and a final segment where the subject responds. What each segment does, how many you have and how long they last are all up to you and define how a trial works.

A simple example experiment can be found in mgl/task:

testExperiment

testExperiment

The code for textExperiment is a good starting place for creating a new experiment since it contains all the essential elements for using these functions.

Let's start by briefly going through each one of the steps above in reference to the function testExperiment. Note that when you actually want to program your own task, you can either start by editing testExperiment.m or use the function taskTemplate.m (be sure to copy these to a new name). taskTemplate.m is an even more stripped down version of testExperiment.m that contains only the necessary essentials to start using the code (and everywhere there is a comment that begins with fix: you will need to make changes to customize for your experiment). There are also some more templates that can be used as starting places:

  • taskTemplateStaircase: This is a task that implements a simple staircase task.
  • taskTemplateContrast10bit: Shows you how to use the 10-bit capacity for fine contrast steps
  • taskTemplateDualMain: This is an example of the main task in a dual task pair, to show how to run dual tasks.
  • taskTemplateDualSubsidiary: This is an example of the subsidiary task in a dual task pair, to show how to run dual tasks.

Initialize the screen

This can be done very simply just by calling

	% initalize the screen
	myscreen = initScreen;

This call will handle opening up of the screen with appropriate parameters and setting the gamma table.

If you want to add specific parameters for your computer in mgl 2.0 just use mglEditScreenParams.

Setup the task structure

In the testExperiment, the task structure is a cell array that actually contains two separate tasks that will be run in the course of the experiment.

This sets the first task to be the fixation staircase task. If you don't want to use the fixation task then you can omit this part:

	% set the first task to be the fixation staircase task
	[task{1} myscreen] = fixStairInitTask(myscreen);

This is the first “phase” of our task. Not all tasks need to have different phases, but in this case we want the experiment to start with dots moving incoherently for 10 seconds and then we want trials to run in the next phase.

	% set our task to have two phases. 
	% one starts out with dots moving for incohrently for 10 seconds
	task{2}{1}.waitForBacktick = 1;
	task{2}{1}.seglen = 10;
	task{2}{1}.numBlocks = 1;
	task{2}{1}.parameter.dir = 0;
	task{2}{1}.parameter.coherence = 0;

Each one of the fields in the task set the behavior of that phase of the task.

  • waitForBacktick=1: The task phase will only start running after we receive a keyboard backtick (`).
  • seglen = 10: The segment will run for 10 seconds.
  • numBlocks = 1: There will be one block of trials before we run on to the next phase of the task.
  • paramater.dir = 0: We set the parameter dir to have a value of 0.
  • parameter.coherence = 0: We set the parameter coherence to have a value of 0.

The next phase of the task will be the one that actually runs the trials.

	% the second phase has 2 second bursts of directions, followed by	 
	% a top-up period of the same direction
	task{2}{2}.segmin = [2 6];
	task{2}{2}.segmax = [2 10];
	task{2}{2}.parameter.dir = 0:60:360;
	task{2}{2}.parameter.coherence = 1;
	task{2}{2}.random = 1;

In this task, we have a block of trials in which we will show trials with different motion directions. You set what parameters you want to use in the “parameter” part of your task. Note that you can use any name for parameters that you like. Here we call them dir for direction and coherence for motion coherence. Note that we have only one value of motion coherence so all trials will be run with a motion coherence of 1.

	task{2}{2}.parameter.dir = 0:60:360;
	task{2}{2}.parameter.coherence = 1;

We also have to decide the order in which parameters will be presented in a block of trials. The default is to run them sequentially (in this case directions 0 then 60 then 120 etc). To randomize the order, we set:

	task{2}{2}.random = 1;

Our trial will have two segments, a 2 second segment in which the stimulus is presented and a 6-10 second long intertrial interval:

	task{2}{2}.segmin = [2 6];
	task{2}{2}.segmax = [2 10];

The task code will automatically keep track of the variables in the parameter field, so that you can later access them to find out which direction of motion was shown on what trial. You will be able to do this by using the function getTaskParameters.

Initialize the stimulus.

The stimulus is kept in a global variable so that if the variable is very large, we don't incur overhead with passing it around all the time. If you want to have the stimulus variable saved at the end of the experiment, you can call the function initStimulus as below. Note that you do not need to call initStimulus if you do not want to save the stimulus structure.

	% init the stimulus
	global stimulus; 
	myscreen = initStimulus('stimulus',myscreen);
	stimulus = initDots(stimulus,myscreen);

The function initDots is specific for creating the dots stimulus for this test experiment, you will substitute your own function for creating your stimulus.

Create callback functions

Callbacks are the way that you control what happens on different portions of the trial and what gets drawn to the screen. A callback is simply a function that gets called at a specific time. You write the function and you let updateTask handle when that function needs to be called.

There are two required callbacks:

The first required callback that is used in this program is the one that gets called every time a segment starts.

	function [task myscreen] = startSegmentCallback(task, myscreen)
 
	global stimulus;
	if (task.thistrial.thisseg == 1)
		stimulus.dots.coherence = task.thistrial.coherence;
	else
		stimulus.dots.coherence = 0;
	end
	stimulus.dots.dir = task.thistrial.dir;

What it does is it looks in the “thistrial” structure for what segment we are on, if we are in segment one (the stimulus segment), it sets it to whatever the parameter coherence is set to (defined in the task.parameter.coherence field). Otherwise, we are in segment two (the intertrial interval), so we set coherence to 0 (random movement). It also sets the direction of motion of the dots.

The second (and most important) callback is the one used to draw the stimulus to the screen:

	function [task myscreen] = screenUpdateCallback(task, myscreen)
 
	global stimulus 
	mglClearScreen;
	stimulus = updateDots(stimulus,myscreen);

You can put your stimulus drawing routines in here. In this program, we simply clear the screen and draw the dots. This function gets called every display refresh.


Once these functions are defined in your file, you tell the programs to use these callbacks by using initTask to register the callbacks.

	% initialize our task with only the two required callbacks
	for phaseNum = 1:length(task{1})
		[task{1}{phaseNum} myscreen] = initTask(task{1}{phaseNum},myscreen,@startSegmentCallback,@screenUpdateCallback);
	end

NOTE: It is necessary to register the callbacks in a specific order. The correct order for registering callbacks is: startSegmentCallback, screenUpdateCallback, getResponseCallback, startTrialCallback, endTrialCallback, startBlockCallback

It doesn't matter exactly how you name the callbacks, what matters is what order you call them in. If there is a callback that you are not defining, you can enter it as [] in the initTask call, or leave it out:

for example,

	[task myscreen] = initTask(task,myscreen,@startSegment, @screenUpdate, @getResponse, [],[], @startBlock);

or

	[task myscreen] = initTask(task,myscreen, @startSegment, @screenUpdate, @getResponse);

More details can be found in the callbacks section.

Create a display loop

Now that everything is setup to run your experiment all you need is a display loop that calls updateTask to run each one of the tasks that are being displayed. Then to flip the front and back buffer of the display to show your stimulus, you call tickScreen. This is the main loop in which your program is run. It also checks for whether the user hit the <ESC> key, and ends the program when it has been hit.

	phaseNum = 1;
	while (phaseNum <= length(task{2})) && ~myscreen.userHitEsc
		% update the dots
		[task{2} myscreen phaseNum] = updateTask(task{2},myscreen,phaseNum);
		% update the fixation task
		[task{1} myscreen] = updateTask(task{1},myscreen,1);
		% flip screen
		myscreen = tickScreen(myscreen,task);
	end

At the very end you end the task which will save out information about your experiment.

	myscreen = endTask(myscreen,task);

Integration with an eye tracker

The task structure also provides easy integration with an eye tracker. The basic functionality is handled by a set of callback functions that handle interacting with the eye tracker. Currently, support for the SR Research (http://www.sr-research.com/) EyeLink trackers is fully supported. The current eye position is also available for constructing simple gaze contingent displays. See here.