keypress to move to next entry in sequence (resolved)

I’m adjusting someone else’s code to modify one piece of a trial structure. Right now there is a sequence that runs each trial, and the times for each component are fixed, using Wait. I’d like to modify it so that during the ‘test’ function, if the user presses a specific key (probably spacebar), it will move on to the next function in the sequence (‘iti’). If the spacebar is not pressed, I want it to still just wait 8 seconds and then move on to the next function in the sequence.

I tried adding an interval, with no luck. This code also has some associated world base code, and I’m only attaching the sequence part, and the information about what is in the ‘test’ function, but I can add more if that is helpful. Since I didn’t build this originally, I’m not sure of all the details of the code, but any suggestions would be appreciated!

# Assemble the trial.
            trial_seq = Sequence(Func(self.setStage, 'map', trial),
                                 Wait(2.0),
                                 Func(self.setStage, 'fixation'),
                                 Wait(4.0),
                                 Func(self.setStage, 'test', trial),
                                 Wait(8.0),
                                 Func(self.setStage, 'iti'),
                                 Wait(trial['iti']), 
                                 name="Trial")
            
            self.runSequence.append(trial_seq)
            
        self.runSequence.append(Func(self.setTracing, trace, False))
        self.runSequence.append(Func(self.userExit))
if name == 'test':
            self.activateActorOIControl(True)
            self.setBackgroundColor(self.skyColor)
            self.activateDisplayRegion('default', True)
            self.activateCamera(trial['camera'])
            if trial['camera'] == 'TPV':
                self.cameraNP['TPV'].setPos(self.actorOINP, (0,-5,5))
            self.getObject('Goal')[0].hide()
            self.getObject('Start')[0].hide()
            self.markLogFile('TEST')
            self.traceUpdateFunc = self.testTraceMessageFunc
            

Hmm… I’m not sure that the following will actually work, but it might be worth trying:

Store a reference to each of your "Wait"s, and keep track of which is the current “Wait”. Then, when the relevant key is pressed, call the current “Wait”'s “finish” method. Perhaps something like this:

# First, a variable in which to store the "current Wait"
self.currentWait = None

# ~~~~~~~~

# Next, alter "setStage"--or create a wrapper for it--that takes an interval as a fourth parameter
def setStage(self, map, trial, waitObj):
    self.currentWait = waitObj
    # Followed by the rest of the method

# ~~~~~~~~

#  Then, when you create your sequence:
#  Note that we create the sequence empty, then add its elements later; this
# allows us to make use of the references to each element.
trial_seq = Sequence()
wait1 = Wait(2.0)
func1 = Func(self.setStage, 'map', trial, wait1) # Note that we include wait1 as the final parameter
trial_seq.append(func1)
trial_seq.append(wait1)
# Similarly for each Func and Wait ...

# ~~~~~~~~

# Finally, in your key-press code:
if self.currentWait is not None:
    self.currentWait.finish()

Thanks for the suggestion. I tried it, but the code to finish currentWait doesn’t work. currentWait appears to just be the variable with the time, but doesn’t actually correspond to the Wait function. I can stop the sequence altogether, but can’t seem to just stop that piece of the sequence and move on.

# Finally, in your key-press code:
if self.currentWait is not None:
    self.currentWait.finish()

If the object that you get in “currentWait” has the relevant time, could you perhaps alter that such that it times out on the next update?

This seems to be complicated by a number of factors.

  • A Sequence takes everything passed in and copies them into its own internal sequence structure. This means modifying instances of the parts from the outside has no effect; you’d have to reinstantiate a new Sequence.
  • Likewise the IntervalManager seems to copy the Sequence when it is started, so modifications don’t take effect while it is running, only the next time it is started again.
  • I even tried wrapping a single Wait inside a Sequence and calling finish on that…no dice. I think it is difficult or impossible modify a running Sequence or its parts in-place.

A solution I found was to put the last portion in it’s own Sequence iti_seq and if the spacebar is pressed call a function skiptest which pauses the original Sequence, grabs the last two items, and then starts that new Sequence. If the key is not pressed, you can see trial_seq still continues on to iti_seq inline. It works in my minimalist test code, but it still may fail if the original code expects self.runSequence to finish without interruption.
This is a pretty hacky solution, but its intended to work with the code you have rather than redo everything from scratch. Intervals/Sequences just aren’t designed to be very interactive.

    # order is important - iti_seq must be defined before skiptest and trial_seq
    iti_seq = Sequence(IgnoreInterval(base, "space"),
                       Func(self.setStage, 'iti'),
                       Wait(trial['iti']))
    
    # skiptest must be defined before trial_seq for the AcceptInterval keybind to work
    def skiptest():
        self.runSequence.pause()
        iti_seq.append(self.runSequence[-2])
        iti_seq.append(self.runSequence[-1])
        iti_seq.start()
    
    # Assemble the trial.
    trial_seq = Sequence(Func(self.setStage, 'map', trial),
                         Wait(2.0),
                         Func(self.setStage, 'fixation'),
                         Wait(4.0),
                         Func(self.setStage, 'test', trial),
                         AcceptInterval(base, "space", skiptest),
                         Wait(8.0),
                         iti_seq,
                         name="Trial")
    # end of modifications

*edit: this was bugged as originally posted. I think I’ve fixed it.

Wow, I didn’t expect all of that copying within Sequence and IntervalManager. 0_0

However, your solution has given me another idea:

Instead of a single Sequence, create a list of sequences, and either use a task to start each or have each start the next on completion; to my mind the former seems easier, although it may incur a frame between sequence-runs in which no sequence is running.

Something like this:

# In your initialisation code:
    self.sequenceList = []
    self.currentSequence = None
    taskMgr.add(self.updateSequences, "sequence update"

def addStage(self, funcObject, waitTime):
    seq = Sequence(funcObject, Wait(waitTime), Func(self.clearSequence))
    self.sequenceList.append(seq)

def clearSequence(self):
    self.currentSequence = None

def keyPressed(self):
    if self.currentSequence is not None:
        self.currentSequence.finish()

def updateSequences(self, task):
    if self.currentSequence is None:
        self.currentSequence = self.sequenceList.pop()
        self.currentSequence.start()
    return Task.done

# Assemble the trial.
    self.addStage(Func(self.setStage, "map", trial), 2.0)
    self.addStage(Func(self.setStage, "fixation"), 4.0)
    # Etc...

Sure, the original code looks rather unorganized and could probably use a rewrite. In my own project I’ve been using the FSM class for this kind of state management. Intervals/Sequences aren’t very interactive and are probably best suited for cutscenes, special effects, or replays.

BTW I just edited the code I posted above because I noticed the trial_seq was appended to another sequence, thus breaking what I had written assuming trial_seq was run directly.

Thanks for the new suggestions. The list of sequences seems promising. I am still getting errors (I think it’s interfering with some of the other sequences), but will continue to work on it. It seemed like such a simple modification, but it looks like there are some deeper issues here.

I’m sure I made too “clever” changes and too many assumptions.
Here’s a much simpler change that just skips to time 14.0 in the sequence.

    # Assemble the trial.
    trial_seq = Sequence(Func(self.setStage, 'map', trial),
                         Wait(2.0),
                         Func(self.setStage, 'fixation'),
                         Wait(4.0),
                         Func(self.setStage, 'test', trial),
                         AcceptInterval(base, "space", lambda: self.runSequence.setT(14.0)),
                         Wait(8.0),
                         IgnoreInterval(base, "space"),
                         Func(self.setStage, 'iti'),
                         Wait(trial['iti']),
                         name="Trial")

Adjust that 14.0 number as necessary. I don’t know what the run time of self.runSequence is at the time it is started, but you can find out by printing self.runSequence.getDuration().

Thanks for the code suggestions. I tried the lambda, with self.runSequence.setT(14.0), which works fine for one trial, but then takes me back to the 14th second each time I press it, so it basically keeps repeating the trials over and over again.

I tried doing something like this:

AcceptInterval(base, "space", lambda: self.runSequence.setT(((trial['trialNum']-1.0)*16.0)+14.0)),

The idea was to take me to the 14th second of each trial (since the total of each trial is 16 seconds, I fixed iti timing to be 2.0 sec). But that takes me through all of the trials with one press. I think it’s registering the keypress a bunch of times.

Trying “space-up” didn’t help with it, and I can’t get acceptOnce to work with the interval

Thanks for all the help! We have resolved the issue. A bit hacky, but we defined a global variable trialTime based on the trial number to pass into self.runSequence.setT(trialTime) so that it takes us to the defined timepoint for each trial.