Seminar 8 Gettings start with PsychoPy
Before we program our first game using PsychoPy, we need to spend some time figuring out its basics. It is not the most suitable library for writing games, for that you might want to use Python Arcade or PyGame). However, it is (IMHO) the best Python library for developing psychophysical experiments, which is why we will use it in our course.
For this and following projects, we won’t use Jupyter Notebooks but will develop a Python program using IDE environment of your choice (I would recommend Visual Studio Code). You still could and should use Jupyter for playing with and testing small code snippets, though. I’ve added a section on setting up debugging in VS Code in Getting Started, take a look once you are ready to run the code.
From now on, create a separate subfolder for each seminar (e.g. Seminar 08 for today) and create a separate file (or files, later on) for each exercise3 (e.g., exercise01.py, exercise02.py, etc.). This is not the most efficient implementation of a version control and will certainly clutter the folder. But it would allow me to see your solutions on every step, which will make it easier for me to write comments. For submitting the assignment, just zip the folder and submit the zip-file.
8.1 Minimal PsychoPy code
In the subfolder for the current seminar, create file exercise01.py (I would recommend using lead zero in 01, as it will ensure correct file sorting once we get to exercise 10, 11, etc.). Copy-paste the following code:
"""
A minimal PsychoPy code.
"""
# this imports two modules from psychopy
# visual has all the visual stimuli, including the Window class
# that we need to create a program window
# event has function for working with mouse and keyboard
from psychopy import visual, event
# creating a 800 x 600 window
= visual.Window(size=(800, 600))
win
# waiting for any key press
event.waitKeys()
# closing the window
win.close()
Run it to check that PsychoPy work. You should get a gray window with PsychoPy title. Press any key (click on the window, if you switched to another one, so that it registers the key press) and it should close. Not very exciting but does show that everything works as it should.
Put your code into exercise01.py.
Let me explain what we are doing here line-by-line.
from psychopy import visual, event
here we import two (out of many) PsychoPy modules that give us visual stimuli and main program window plus ability to process events, such as keyboard presses or mouse activity.win = visual.Window(size=(800, 600))
we create a new PsychoPy Window object (you will learn about classes and objects soon) and define itssize
as 800 by 600 pixels (you can change that and see how the window also changes its size).event.waitKeys()
function waitKeys() waits for a press of a keyboard key. As we didn’t specify which keys we are interested in, any key will do. Later on, you will learn how to make it wait for specific keys.win.close()
this calls a methodclose
(function that belongs to an object, again, you’ll learn about them later) that tells windowwin
to close itself.
8.2 Adding main loop
Currently, nothing really happens in the code, so let us add the main loop. The loop goes between opening and closing the window:
importing libraries
opening the window
--> our main loop <--
closing the window
For this, you need to create a new variable gameover
and set it too False
(just like we did it in Hunt the Wumpus game) and run the loop for as long as the game is not over. Inside the loop, use function event.getKeys() to check whether escape button was pressed (for this, you need to pass keyList=['escape']
). The function returns a list of keys, if any of them were pressed in the meantime or an empty list, if no keys from the list were pressed. Accordingly, you need to check whether the return value as an empty list. There are two ways to do this. First, you can check whether length of the list is larger than zero (so, it has elements) using function len(). Alternatively, an empty list evaluates to False
when converted to a logical value either explicitly (via bool()
type conversion) or when evaluated inside of the condition in an if
or while
statement:
= []
x if x:
print("List is not empty")
else:
print("List is definitely empty")
## List is definitely empty
If list is not empty, you should change gameover
to True
. Think, how can you do it without an if
statement, computing the logical value directly?
Put your code into exercise02.py.
8.3 Adding text message
Although we are now running a nice loop, we still have only a boring gray window to look at. Let us create a text stimulus, which would say “Press escape to exit” and display it during the loop. For this we will use visual.TextStim class from PsychoPy library.
First, you need to create the press_escape_text
object (instance of the TextStim
class) before the main loop. There are quite a few parameters that you can play with but minimally, you need to pass the program window (our win
variable) and the actual text you want to display ("Press escape to exit"
). For all other settings PsychoPy will use its defaults (default font family, color and size, placed right the screen center).
= visual.TextStim(win, "Press escape to exit") press_escape_text
To show the visuals in PsychoPy, you first draw each element by calling its draw()
method (thus, in our case, press_escape_text.draw()
) and then put the “drawing” on the screen by flipping we window (win.flip()
method). These two calls should go inside the main loop either before (my preference) or after the keyboard check.
importing libraries
opening the window
--> create press_escape_text here <--
= False
gameover while not gameover:
--> draw press_escape_text <--
--> flip the window <--
for escape button press
check keyboard
close the window
Now, you should have a nice, although static, message that tells you how you can exit the game. Check out the manual page for visual.TextStim and try changing it by passing additional parameters to the object constructor call. For example you can change its color
, whether text is bold
and/or italic
, how it is aligned, etc. However, if you want to change where the text is displayed, read on below.
Put your code into exercise03.py.
8.4 Adding a square and placing it not at the center of the window
Now, let us figure out how create and move visuals to an arbitrary location on the screen. In principle, this is very straightforward as every visual stimulus (including TextStim
we used just above) has pos
property that specifies (you guessed it!) its position. However, to make your life easier, PsychoPy first complicates it by having five different units systems.
Before we start exploring the units, let us create a simple white square. The visual class we need is visual.Rect. Just like the TextStim
above, it requires win
variable (so it knows which window it belongs to), width
(defaults to 0.5 of that mysterious units), height
(also defaults to 0.5), pos
(defaults to (0,0)), lineColor
(defaults to white
) and fillColor
(defaults to None
). Thus, to get a “standard” white square with size of (0.5, 0.5)
units at (0, 0)
location you only need pass the win
variable: white_square = visual.Rect(win)
. You draw the square just like you drew the text stimulus, via its draw()
method. Create the code, run it to see a very white square, and read on.
importing libraries
opening the window
--> create white_square here <--
= False
gameover while not gameover:
--> draw white_square and flip the window here <--
for escape button press
check keyboard
close the window
Put your code into exercise04.py.
What did you say, your square was not really a square? Well, I told you, five units systems!
8.5 Five units systems
8.5.1 Height units
With height units everything is specified in the units of window height. The center of the window is at (0,0)
and the window goes vertically from -0.5
to 0.5
. However, its horizontal limits depend on the aspect ratio. For our 800×600 window, it will go from -0.666 to 0.666 (the window is 1.3333 window heights wide). For a 600×800 window from -0.375 to 0.375 (the window is 0.75 window heights wide), for a square window 600×600 from -0.5 to 0.5 (again, in all these cases it goes from -0.5 to 0.5 vertically). This means that the actual on-screen distance for the units is the same for both axes. So that a square of size=(0.5, 0.5)
is actually a square (it spans the same distance vertically and horizontally). Thus, it makes sizing objects easier but placing them on horizontal axis correctly harder (as you need to know the aspect ratio).
Modify your code by specifying the unit system when you create the window: win = visual.Window(..., units="height")
. Play with your code by specifying position of the square when you create it. You just need to pass an extra parameter pos=(<x>, <y>)
. Which was is up, when y is below or above zero?
Put your code into exercise05.py.
Unfortunately, unlike x-axis, the y-axis can go both ways. For PsychoPy y-axis points up (so negative values move the square down and positive up). However, if you would use an Eyelink eye tracker to record where participants looked on the screen, it assumes that y-axis starts at the top of the screen and points down (which could be very confusing, if you forget about this when overlaying gaze data on the image you used and wondering what on Earth the participants were doing).
Now, modify the size of the square (and turn it into a non-square rectangle) by passing width=<some-width-value>
and height=<some-height-value>
.
Put your code into exercise06.py.
8.5.2 Normalized units
These units are default ones and assume that the window goes from -1 to 1 both along x- and x-axis. Again, (0,0) is the center of the screen but the bottom-left corner is (-1, -1) whereas the top-right is (1, 1). This makes placing your objects easier but sizing them harder (you need to know the aspect ratio to ensure that a square is a square).
Modify your code, so that it uses "norm"
units when you create the window and size the Rect
stimulus, so it does look like a square.
Put your code into exercise07.py.
8.5.3 Pixels on screen
In this case, the window center is still at (0,0)
but it goes from -<width-in-pixels>/2
to <width-in-pixels>/2
horizontally (from -400 to 400 in our case) and -<height-in-pixels>/2
to <height-in-pixels>/2
vertically (from -300 to 300). These units could be more intuitive when you are working with a fixed sized window, as the span is the same along the both axes (like for the height units). However, they spell trouble if your window size was changed or you are using a full screen window on some monitor with an unknown resolution.
Modify your code to use "pix"
units and briefly test sizing and placing your square within the window.
Put your code into exercise08.py.
8.5.4 Degrees of visual angle
Unlike the three units above, these require you knowing a physical size of the screen, its resolution, and your viewing distance (how far your eyes are away from the screen). They are the measurement units used in visual psychophysics as they describe stimulus size as it appears on the retina (see Wikipedia for details). Thus, these are the units you want to use when running an actual experiment in the lab but for our purposes we will stick to one of the three units systems above.
8.5.5 Centimeters on screen
Here, you would need know the physical size of your screen and its resolution. These are fairly exotic units for very specific situations4.
8.6 Make your square jump!
So far, we fixed the location of the square when we created it. However, you can move it at any time by assigning a new (<x>, <y>)
coordinates to its pos
property. E.g., white_square.pos = (-0.1, 0.2)
. Let us experiment by moving the square to a random location on every iteration of the loop (this could cause a lot of flashing, so if you have a photosensitive epilepsy that can be triggered by flashing lights, you probably should do it just once before the loop). Use the units of your choice (I would recommend "norm"
) and generate a new position using random.uniform(a, b) function, that generates a random value within a..b range. You need to generate two values (one for x, one for y) and your range is the same for "norm"
units (from -1 to 1) but is different (and depends on the aspect ratio) for "height"
units.
importing libraries, now also the random libraryopen the window
create white_square here
= False
gameover while not gameover:
--> move the square to a random position <--
and flip the window here
draw white_square for escape button press
check keyboard
close the window
Put your code into exercise09.py.
8.7 Make the square jump on your command!
This was very flashy, so let us make the square jump only when you press Space button. For this, we need to expand the code that processes keyboard input. So far, we restricted it to just “escape” button and checked whether any (hence, “escape”) button was pressed. In my case, the code looks like that
= event.getKeys(keyList=['escape']) gameover
But I could have written it as
= event.getKeys(keyList=['escape'])
keys_pressed if keys_pressed is not None:
= True game_over
Let us use the second version, where we explicitly store the output of event.getKeys()
function in keys_pressed
. First, you need to add "space"
to the keyList
parameter. Second, because event.getKeys()
function returns a list of keys that were pressed, we need to loop over that list (which in most cases will contain no or just one element) and use conditional statements inside that loop to make the square jump or to exit the program.
Use for for
loop to loop over the elements of the list. Note that no iterations will occur if the list is empty, you can test this in a Jupyter cell:
for _ in []:
print("No one will ever see me...")
When you loop over keys_pressed
, your current loop variable value will be the string with the name of the button pressed (so, either "escape"
or "space"
). Now, you need to use conditional statements, so that the square jumps if the key was "space"
and game is over if the key was "escape"
. You code should look roughly like that
importing libraries, now also the random libraryopen the window
create white_square
= False
gameover while not gameover:
and flip the window here
draw white_square
# ----- New code -----
= event.getKeys(keyList=['escape', 'space'])
keys_pressed
loop over keys_pressed:if participant pressed space key:
move the square to a random locationelif participant pressed escape key:
set gameover to False
# ----- End of new code -----
close the window
Put your code into exercise10.py.
8.8 I like to move it, move it!
Let us exert more control over our rectangle by moving it around using arrow buttons ("up"
, "down"
, "left"
, and "right"
in PsychoPy). Add these keys to the keyList
parameter of the event.getKeys()
call and add more conditional statements when processing the pressed key. In PsychoPy you can change position by adding to it via +=
or -=
operations (other operations are also supported, see manual). Thus, you can move your square to the right by 0.1 units (whatever they are) by writing white_square.pos += (0.1, 0)
. Please note that you cannot write white_square.pos = white_square.pos + (0.1, 0)
!
Expand your code, so you move the rectangle around by 0.05 units in the direction of the key pressed.
Put your code into exercise11.py.
When we continue, we will expand on this to build a Memory game. In the meantime, experiment with stimuli (you can have a circle or a line rather than a square). Try showing more than one stimulus (e.g., add back the “press escape to exit” message), etc.