Creating path-dependent object control using keyboard

I need to code a task, in which an object (image of a spaceship) is flying down the screen on a path, with some gravity force and some left/right drift force, and the participant should use left and right keys to keep the spaceship on the path.
I have taken care of the object-control using keyboard, but to define the path and the forces I have no idea where to start coding.
Any ideas are welcome!
Thank you.

I think the answer depends on how exact you want to model the underlying physics. I have attempted a very simple implementation below, let me know if this suits your needs:

First of all, I assume that the code makes use of a game loop as in the code below, so that we can update the position of the ship on every next frame.

while True:
   ship.draw()
   win.flip()

Step 1: Implement vertical motion
If nothing else is going to affect the vertical motion of the ship, you could use the actual kinematic equation:

y(t) = y_0 + v_0 t + at²

where y_0 is the initial position of the ship, v_0 the initial velocity of the ship, and a the acceleration of the ship. In code I have declared these three variables and also used a core.Clock:

y_0 = 0
v_0 = 0
a   = 3
while True:
    # Update vertical position
    t = timer.getTime()
    y = y_0 + v_0*t + a*t**2

   ship.pos = (0, y)

Step 2: Implement horizontal motion
Change in horizontal direction can be defined as a combination of a drift value and a user controlled value. In the example below, the ship will drift towards the left, and depending on which key is pressed this drift will be increased or decreased. For this update we apply the change in horizontal motion to the current value of the horizontal position of the ship.

drift = -1
while True:    
    # Update horizontal position
    user_steering = 0
    if keyboard[key.D]:
        user_steering = 1.5
    elif keyboard[key.Q]:
        user_steering = -1.5
    elif keyboard[key.ESCAPE]:
        break
        
    dx = drift + user_steering
    x  = ship.pos[0] + dx
    
    # Set new position
    ship.pos = (x, 0)

To make the keyboard input work, I also added the following code to the top of my script so that I can handle the current status of a key:

import pyglet

key = pyglet.window.key              
keyboard = key.KeyStateHandler()   
win.winHandle.push_handlers(keyboard) # win is the psychopy window

More info on modelling kinematics can be found here.

2 Likes

Thank you very much @Christophe_Bossens! Your explanations made the problem much clearer for me. I don’t need the underlying physics to be more complicated than what you have already sugetsed.

For the object control, I’m using the following code, which works find for moving the object to left and right.


from __future__ import absolute_import, division

from psychopy import locale_setup
from psychopy import prefs
prefs.hardware['audioLib'] = 'ptb'
from psychopy import sound, gui, visual, core, data, event, logging, clock, colors
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
                                STOPPED, FINISHED, PRESSED, RELEASED, FOREVER)

import numpy as np  # whole numpy lib is available, prepend 'np.'
from numpy import (sin, cos, tan, log, log10, pi, average,
                   sqrt, std, deg2rad, rad2deg, linspace, asarray)
from numpy.random import random, randint, normal, shuffle, choice as randchoice
import os  # handy system and path functions
import sys  # to get file system encoding

from psychopy.hardware import keyboard



# Ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
os.chdir(_thisDir)

# Store info about the experiment session
psychopyVersion = '2021.2.3'
expName = 'moveSquare'  # from the Builder filename that created this script
expInfo = {'participant': '', 'session': ''}
dlg = gui.DlgFromDict(dictionary=expInfo, sortKeys=False, title=expName)
if dlg.OK == False:
    core.quit()  # user pressed cancel
expInfo['date'] = data.getDateStr()  # add a simple timestamp
expInfo['expName'] = expName
expInfo['psychopyVersion'] = psychopyVersion

# Data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
filename = _thisDir + os.sep + u'data/%s_%s_%s' % (expInfo['participant'], expName, expInfo['date'])

# An ExperimentHandler isn't essential but helps with data saving
thisExp = data.ExperimentHandler(name=expName, version='',
    extraInfo=expInfo, runtimeInfo=None,
    originPath='C:\\Users\\CVBE\\Desktop\\moveSquare.py',
    savePickle=True, saveWideText=True,
    dataFileName=filename)
# save a log file for detail verbose info
logFile = logging.LogFile(filename+'.log', level=logging.EXP)
logging.console.setLevel(logging.WARNING)  # this outputs to the screen, not a file

endExpNow = False  # flag for 'escape' or other condition => quit the exp
frameTolerance = 0.001  # how close to onset before 'same' frame

# Start Code - component code to be run after the window creation

# Setup the Window
win = visual.Window(
    size=[1920, 1080], fullscr=True, screen=0,
    winType='pyglet', allowGUI=False, allowStencil=False,
    monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
    blendMode='avg', useFBO=True,
    units='height')
# store frame rate of monitor if we can measure it
expInfo['frameRate'] = win.getActualFrameRate()
if expInfo['frameRate'] != None:
    frameDur = 1.0 / round(expInfo['frameRate'])
else:
    frameDur = 1.0 / 60.0  # could not measure, so guess

# Setup eyetracking
ioDevice = ioConfig = ioSession = ioServer = eyetracker = None

# create a default keyboard (e.g. to check for escape)
defaultKeyboard = keyboard.Keyboard()

# Initialize components for Routine "trial_moveSq"
trial_moveSqClock = core.Clock()
#set initial position of square
posX = 0
posY = 0
squarePos = [posX, posY] # to be placed in the position field in squareBody component properties
squareBody = visual.ImageStim(
    win=win,
    name='squareBody',
    image='spaceship.png', mask=None,
    ori=0.0, pos=[0,0], size=[0.3, 0.3],
    color=[1,1,1], colorSpace='rgb', opacity=None,
    flipHoriz=False, flipVert=False,
    texRes=128.0, interpolate=True, depth=-1.0)

# Create some handy timers
globalClock = core.Clock()  # to track the time since experiment started
routineTimer = core.CountdownTimer()  # to track time remaining of each (non-slip) routine

# ------Prepare to start Routine "trial_moveSq"-------
continueRoutine = True
# update component parameters for each repeat
squarePos = [posX, posY]
pressedUp = 0
pressedDown = 0
pressedLeft = 0
pressedRight = 0
# keep track of which components have finished
trial_moveSqComponents = [squareBody]
for thisComponent in trial_moveSqComponents:
    thisComponent.tStart = None
    thisComponent.tStop = None
    thisComponent.tStartRefresh = None
    thisComponent.tStopRefresh = None
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED
# reset timers
t = 0
_timeToFirstFrame = win.getFutureFlipTime(clock="now")
trial_moveSqClock.reset(-_timeToFirstFrame)  # t0 is time of first possible flip
frameN = -1

# -------Run Routine "trial_moveSq"-------
while continueRoutine:
    # get current time
    t = trial_moveSqClock.getTime()
    tThisFlip = win.getFutureFlipTime(clock=trial_moveSqClock)
    tThisFlipGlobal = win.getFutureFlipTime(clock=None)
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame
    squarePos = [posX, posY]

    # *squareBody* updates
    if squareBody.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
        # keep track of start time/frame for later
        squareBody.frameNStart = frameN  # exact frame index
        squareBody.tStart = t  # local t and not account for scr refresh
        squareBody.tStartRefresh = tThisFlipGlobal  # on global time
        win.timeOnFlip(squareBody, 'tStartRefresh')  # time at next scr refresh
        squareBody.setAutoDraw(True)
    if squareBody.status == STARTED:  # only update if drawing
        squareBody.setPos((posX, posY), log=False)
    #check which keys have been pressed and move accordingly

    if event.getKeys(["right"]):
        posX = posX + 0.05
    elif event.getKeys(["left"]):
        posX = posX - 0.05
    elif event.getKeys(["up"]):
        posY = posY + 0.05
    elif event.getKeys(["down"]):
        posY = posY - 0.05

    # check for quit (typically the Esc key)
    if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
        core.quit()

    # check if all components have finished
    if not continueRoutine:  # a component has requested a forced-end of Routine
        break
    continueRoutine = False  # will revert to True if at least one component still running
    for thisComponent in trial_moveSqComponents:
        if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
            continueRoutine = True
            break  # at least one component has not yet finished

    # refresh the screen
    if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
        win.flip()

# -------Ending Routine "trial_moveSq"-------
for thisComponent in trial_moveSqComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)
thisExp.addData('squareBody.started', squareBody.tStartRefresh)
thisExp.addData('squareBody.stopped', squareBody.tStopRefresh)
# the Routine "trial_moveSq" was not non-slip safe, so reset the non-slip timer
routineTimer.reset()

# Flip one final time so any remaining win.callOnFlip()
# and win.timeOnFlip() tasks get executed before quitting
win.flip()

# these shouldn't be strictly necessary (should auto-save)
thisExp.saveAsWideText(filename+'.csv', delim='auto')
thisExp.saveAsPickle(filename)
logging.flush()
# make sure everything is closed down
thisExp.abort()  # or data files will save again on exit
win.close()
core.quit()

Now I need to figure out how to combine it with your ideas.
For the acceleration (a), there would be only gravity (a=9,8?).
It’s interesting how you designed the drift to increase or decrease based on the user’s key press.

There should be a path, on which the user needs to keep the spaceship on. For the sake of the experiment, this path needs to be random every time. I’m thinking of having some basic building blocks with the same length, e.g., “straight line” “left curve” “right curve” and then for each path randomly patch a fixed number of these basic building blocks together, e.g. a path would be “straight-left-left-right-straight”. Do you know if a way I can code this?

Thanks a lot for your kind support!
Zahra