psychopy.org | Reference | Downloads | Github

Stimulus following other moving stimulus

Hello, I’m having troubles with moving stimuli.

I want to present a stimulus that follows another moving stimulus. The ‘mover’ is moving up and down on the right side of the screen for some time, the ‘follower’ is then presented in the middle of the screen and accelerates (almost like a targeted rocket) to the ‘mover’ (then fades away).

I have issues with how to make the start position of the follower (0, 0) and the end (rightMover.pos()) . This is because much of it will be randomized and I won’t know the exact positions, as well as the possibility that the ‘follower’ follows a different stimulus instead (randomized).

I did this, and it’s working fine. I need help with making the code more sophisticated, something that allows more robust analysis of position/speed of the ‘follower’. Right now, the acceleration and timing of the whole thing seems arbitrary. I would love some advice on how to make it more clear-cut.

The code in question (comprehension may be aided through viewing screenshot of ‘follower’ settings in first):

#begin routine ------
collisionStart = randint(1, 20)
collisionSpeed = 0.7
fadeOut = 1
sizeIncrease = 1.1

#each frame ------
if t > collisionStart and collisionSpeed < 1000:
    collisionSpeed *= 1.2
    sizeIncrease *= 1.01

if collisionSpeed >= 200:
    fadeOut *= 0.7

To make things look more natural you need to consider some basic physics involved.

This is how you increase the speed:

collisionSpeed *= 1.2

This does not correspond to what we expect in reality. If a car accelerates, it usually doesn’t accelerate in such a way that if it was going at 50mph, next second it’ll be going at 501.2=60, then 601.2=72… Acceleration isn’t really dependent on speed (and if anything, the more something gains speed, the less it tends to accelerate). So if the acceleration should be done in a way that only depends on a constant, it’s more natural for the acceleration itself to be constant (but in real life of course it would usually decrease after a certain speed, et c.).

In order to determine which direction the follower should move in you need to use basic trigonometry for calculating the angle, and then calculating the movement x/y components using sine/cosine. You also want to use Pythagoras’ theorem to calculate the distance between the objects, to determine when the follower is so close to the target that it would “pass over” if it made a jump as large as its current speed says.

Starting to sound like a physics question come to haunt you from high school? :wink: That’s exactly what it is.

I tried implementing what you suggested and it got fairly complicated. Explaining all that’s going on would become very lengthy, but I’ve inserted comments. You’ll probably want to google things.

I used two polygon components, one code component and a Keyboard component (the keyboard was simply used to create a method of exiting)

This is the follower

This is the target

And this is the code

### Begin experiment
# import functions
from math import atan

# define own functions
def get_dist(pos_tup1, pos_tup2):
    # calculates euclidean distance between two 2d points
    # pos_tup1/pos_tup2: two-element tuples with numeric x/y coordinates
    dx = pos_tup1[0]-pos_tup2[0]
    dy = pos_tup1[1]-pos_tup2[1]
    return (dx**(2) + dy**(2))**(1/2)

def get_angle(pos_tup1, pos_tup2):
    # calculates angle(with 0 being "straight right") of line starting
    # from point described by pos_tup1 and ending at pos_tup2
    # pos_tup1/pos_tup2: two-element tuples with numeric x/y coordinates
    dx = pos_tup2[0]-pos_tup1[0]
    dy = pos_tup2[1]-pos_tup1[1]
    # if the target and follower are on the same x-coordinate,
    # calculating tan won't work (b/c of dividing by 0),
    # so we manually specify which angle to return in these cases
    if dx==0:
        if dy==0:
            return 0
        if dy>0:
            return pi/2
        return -pi/2
    angle = atan(dy/dx)
    # if the target is further to the left than the follower,
    # the angle produced by tan must be "rotated half a circle"
    if dx < 0:
        angle += pi
    return angle

# set acceleration constants
# (using capital letters since that's the convention for
# constants)
POS_SPEED_INCREASE = 0.01
SIZE_SPEED_INCREASE = 0.001
### Begin Routine
# initialize values

# pick a random integer between -500 and 500
# as the start x coordinate, and do the same
# thing but with the range [-300, 300] for the y coordinate
follower_start_pos = [randint(-400, 400), 
                      randint(-300, 300)]
follower_pos = follower_start_pos
target_pos = (randint(-400, 400), 
              randint(-300, 300))
follower_opacity = 1
# start the target somewhere between 1 and 4s
follower_start_time = random() * 3 + 1
follower_size = 10
# initial speed with which the size expands
follower_sizeexpansion_speed = 0.01
# initial speed with which the follower moves
follower_pos_speed = 0.3
# variable that indicates if the follower and target have collided
# yet
have_collided = False

# get the angle of the direction that the follower is to move in 
# (it will stay the same throughout the routine, unless the target
# would also be set to move)
move_angle = get_angle(follower_pos, target_pos)
# use cosine/sine to get factors used to get the x/y velocity vector
# components
x_component_factor = cos(move_angle)
y_component_factor = sin(move_angle)
### Each Frame
if t >= follower_start_time and not have_collided:
    current_dist = get_dist(follower_pos, target_pos)
    # check if the current distance is lesser than the current
    # follower's movement speed, and if so, move the follower
    # to the target's position and end all movement
    if current_dist < follower_pos_speed:
        follower_pos = target_pos
        have_collided = True
if t >= follower_start_time and not have_collided:
    # get velocity vector's x/y components
    x_shift =  x_component_factor * follower_pos_speed
    y_shift =  y_component_factor * follower_pos_speed
    # increment follower size
    follower_size += follower_sizeexpansion_speed
    follower_pos[0] += x_shift 
    follower_pos[1] += y_shift
    # accelerate, by increasing both speed of follower's size
    # expansion, and follower's movement speed
    follower_sizeexpansion_speed += SIZE_SPEED_INCREASE
    follower_pos_speed += POS_SPEED_INCREASE

If you’ll do an actual experiment with this you’ll probably want to do more, like ensuring that the follower doesn’t “spawn” too close to the target, or set a maximum size that it can reach. You could also use the thisExp.addData method to store information on things like the follower and target’s distance from each other when the participant responds.

1 Like

Hey @Arboc,

Wow, thank you. Interesting stuff, it really is reminiscent of high school physics. I didn’t directly use the code you provided, but it pushed me in the right direction. I appreciate it very very much.