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
win = visual.Window(size=(800, 600))

# 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 its size 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 method close (function that belongs to an object, again, you’ll learn about them later) that tells window win 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).

press_escape_text = visual.TextStim(win, "Press escape to exit")

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 <--

gameover = False
while not gameover:
    --> draw press_escape_text <--
    --> flip the window  <--
    check keyboard for escape button press
    
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 <--

gameover = False
while not gameover:
    --> draw white_square and flip the window here <--
    check keyboard for escape button press

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 library
open the window
create white_square here

gameover = False
while not gameover:
    --> move the square to a random position <--
    draw white_square and flip the window here
    check keyboard for escape button press

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

gameover = event.getKeys(keyList=['escape'])

But I could have written it as

keys_pressed = event.getKeys(keyList=['escape'])
if keys_pressed is not None:
    game_over = True

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 library
open the window
create white_square

gameover = False
while not gameover:
    draw white_square and flip the window here
    
    # ----- New code -----
    keys_pressed = event.getKeys(keyList=['escape', 'space'])
    loop over keys_pressed:
        if participant pressed space key:
            move the square to a random location
        elif 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.


  1. You can “Save as…” the previous exercise to avoid copy-pasting things by hand.↩︎

  2. so specific that I can’t think of one, to be honest.↩︎