0

I'm trying to develop a simple psychology experiment using the Bevy engine, which runs on both WASM and desktop. I measure the response times from when ellipses are displayed until a user provides input.

The experiment measures response times, but I've noticed that sometimes the results are logarithmic and incorrect (In other words, this issue doesn't always occur, making it challenging to reproduce consistently. Both the WASM and desktop versions exhibit this behavior.)

Here is the online demo: https://altunenes.github.io/weber_fechner/ you can get your simple CSV output for the experiment (it has 5 trials).

here is how I handle time in my experiment:

#[derive(Resource)]
struct TrialState {
    start_time: Instant,
}

impl Default for TrialState {
    fn default() -> Self {
        TrialState {
            start_time: Instant::now(),
        }
    }
}

fn refresh_ellipses(
    // ... some code ...
) {
    if experiment_state.ellipses_drawn && (keys.just_pressed(KeyCode::Key1) || keys.just_pressed(KeyCode::Key0) || keys.just_pressed(KeyCode::Space)) {
        trial_state.start_time = Instant::now();
    }
    // ... code ...
}

fn update_user_responses(
    // ... other parameters ...
) {
    if keys.just_pressed(KeyCode::Key1) || keys.just_pressed(KeyCode::Key0) || keys.just_pressed(KeyCode::Space) {
        let elapsed = trial_state.start_time.elapsed().as_secs_f32();
        // ... rest of the function ...
    }
}

I've double-checked the logic for capturing the start time and calculating the elapsed time, but couldn't find any obvious issues. I've also ensured that the Instant::now() function is being called at the correct moments in the code.

Would you be able to provide insights into why the response times might be calculated incorrectly?

other code for how I take information for the writing as csv:

#[derive(Default, Resource)]
struct ExperimentState {
    final_result: Vec<(usize, usize, String, f32)>,
    // ... other ...
}

fn update_user_responses(
    keys: Res<Input<KeyCode>>,
    mut experiment_state: ResMut<ExperimentState>,
    trial_state: Res<TrialState>,
    mut app_state: ResMut<AppState>,
) {
    // ... code ...

    if keys.just_pressed(KeyCode::Key1) || keys.just_pressed(KeyCode::Key0) || keys.just_pressed(KeyCode::Space) {
        let elapsed = trial_state.start_time.elapsed().as_secs_f32();
        experiment_state.final_result.push((num_left, num_right, result, elapsed));
        // ... rest of the function ...
    }

    if experiment_state.num_trials == TOTAL_TRIAL {
        print_final_results(&experiment_state.final_result);
    }
}

fn print_final_results(final_results: &Vec<(usize, usize, String, f32)>) {
    // ... previous code ...

    let mean_correct_rt: f32 = final_results
    .iter()
    .filter(|(_, _, is_correct, _)| is_correct == "Correct") 
    .map(|(_, _, _, response_time)| response_time)
    .sum::<f32>() / final_results.len() as f32;

I tried the best of my can to simplify my problem still I feel it's so long even in its current form, I really appreciate your time. :(

here is the full code: https://github.com/altunenes/weber_fechner/blob/master/src/main.rs

OS: Windows 11 (latest version)

cafce25
  • 15,907
  • 4
  • 25
  • 31
AltunE
  • 375
  • 3
  • 10
  • 2
    What do you mean by the times are 'logarithmic'? Scientific representation like `5.123e2`? Can you add some examples of wrong/correct output? – cafce25 Aug 16 '23 at 13:43
  • @cafce25 Yes, for example (correct) Response_Time 3.034 0.8664 0.7101 0.6768 0.6574 a wrong output: 0.0004 0.0001 0 0.0001 0.0001 – AltunE Aug 16 '23 at 13:49
  • 2
    Can you explain in words how the experiment is supposed to work? (your demo doesn't work on my machine) I would assume reaction-time or something but you call it a "psychology experiment" so I'm unsure. I ask because I'm skeptical why key input is checked when you start the timer with the same key-input checks to stop the timer. Doesn't that mean it could just be an issue with the order that your bevy systems are executed? – kmdreko Aug 16 '23 at 17:14
  • @kmdreko Thank you for your insights! The experiment is based on a common paradigm in psychology that involves multiple trials where participants view ellipses and respond based on their count on the left vs. the right. Each trial's response time is measured independently, starting when ellipses appear and stopping upon a key press indicating the participant's decision. The key-input checks for starting and stopping are designed to capture the exact decision-making moment. I don't know why the demo doesn't work on your machine :( sorry and thank you for your time – AltunE Aug 16 '23 at 17:59
  • 2
    I think my point about the key presses was missed. If a key (1, 0, space) is pressed, then *both* of your `if` statements can be true at the same time. Thus if `refresh_ellipses` runs before `update_user_responses`, then `trial_state.start_time` will be set and then `.elapsed()` would be gotten almost instantly. Therefore the behavior (sometimes correct, sometimes sub-millisecond timing) is dependent on Bevy's execution order which it [advertises](https://bevy-cheatbook.github.io/programming/system-order.html) is non-deterministic if there aren't constraints dictating the order. – kmdreko Aug 16 '23 at 23:42
  • @kmdreko yes!! Thank you so much for pointing out the potential issue with the order of Bevy system execution. Based your insight, I've rearranged the systems in the App::new() function to ensure that update_user_responses always runs before refresh_ellipses. After making this change and testing the experiment 10 times, I haven't encountered the problem anymore. Your insight was invaluable, and I truly appreciate your help! :) – AltunE Aug 17 '23 at 07:17

1 Answers1

0

After diving deeper into the issue and considering the insightful suggestions provided by @kmdreko, I've identified and resolved the problem. The root cause was indeed related to the order of Bevy system execution.

In my original code, both refresh_ellipses and update_user_responses systems were checking for key presses. This introduced a potential race condition where, depending on the non-deterministic order of system execution in Bevy, refresh_ellipses could reset the start_time just after update_user_responses had calculated the elapsed time. This led to the occasional incorrect and near-instantaneous response times.

To address this, I rearranged the systems in the App::new() function to ensure that update_user_responses always runs before refresh_ellipses. Here's the modified order:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                fit_canvas_to_parent: true,
                ..default()
            }), 
            ..default()
        }))
        .insert_resource(AppState::Instruction)
        .insert_resource(ExperimentState::default())
        .insert_resource(TrialState::default()) 
        .insert_resource(FixationTimer::default())

        .add_systems(Startup, setup_camera)
        .add_systems(Update, remove_text_system.before(display_instruction_system))
        .add_systems(Update, display_instruction_system)
        .add_systems(Update, start_experiment_system.after(display_instruction_system))
        .add_systems(Update, display_fixation_system)
        .add_systems(Update, transition_from_fixation_system)
        .add_systems(Update, update_background_color_system)
        .add_systems(Update, update_user_responses) 
        .add_systems(Update, refresh_ellipses.after(update_user_responses)) 
        .run();
}
AltunE
  • 375
  • 3
  • 10