Code covered by the BSD License  

Highlights from
TargetTracker

image thumbnail

TargetTracker

by

 

23 Jul 2012 (Updated )

This is a one-dimensional tracking game. Use a joystick to make the cursor follow the target.

PlayTargetTracker()
% NOTES
%--------------------------------------------------------------------------
% 1) Game requires file 'jst.mexw32' or 'jst.mexw64' from Marc Bodson's website:
%    http://www.ece.utah.edu/~bodson/fun/index.html 
%    The jst file MUST BE IN THE SAME DIRECTORY as this PlayTargetTracker() function
% 2) This function also requires MATLAB's Control System Toolbox
% 3) Feel free to email questions or comments to: jjpotter@gatech.edu
%--------------------------------------------------------------------------

% % Default game settings
% %--------------------------------------------------------------------------
%     boo.pursuitView   = true;       % Choose display type: if boo.pursuitView is false, game uses compensatory view (only the error between cursor and target is shown)
%     boo.saveGameData  = true;       % If true, saves workspace as .mat file after game
%     num.inputGain     = 5;          % Gain between joystick and cursor motion (higher = more aggressive)
%     str.dataFileName  = 'Data';     % Name of file if boo.saveGameData is true
%     num.fps           = 25;         % [frames per second] framerate ... faster computers may be able to go higher than this
%     num.gameDuration  = 30;         % [sec]
%     num.startLt       = 0.2;        % Starting size of target
%     num.growLt        = 2.0;        % Amount the target grows by the time the game ends (can be negative, but not larger in magnitude than num.startLt)
%     num.unitLc        = 0.4;        % Size of cursor
%     num.screenWfactor = 0.7;        % Adjust width of screen (RELATIVE TO vec.targetFreqs!!!)
%     num.lineW         = 4;                          % Line widths in game display
%     vec.targetFreqs   = [0.1, 0.5, 1, 2, 4, 8];     % [rad/sec] sine wave frequencies of target motion
%     vec.targetAmps    = 1./(vec.targetFreqs);       % Amplitudes of each sine wave in target motion
%     
% % Define cursor dynamics: transfer function between joystick input and cursor position output (default transfer function is a simple integrator
%     vec.tfNumerator   = [1];                        % Numerator polynomial of cursor dynamics transfer function
%     vec.tfDenominator = [1 0];                      % Denominator polynomial of cursor dynamics transfer function
% %--------------------------------------------------------------------------


%==========================================================================
function [] = PlayTargetTracker()
%==========================================================================
% Declare global variables (actually structs) to share between functions
    global str num vec mat boo obj hist;
            
% Choose game settings
%--------------------------------------------------------------------------
    boo.pursuitView   = true;       % Choose display type: if boo.pursuitView is false, game uses compensatory view (only the error between cursor and target is shown)
    boo.saveGameData  = true;       % If true, saves workspace as .mat file after game
    num.inputGain     = 5;          % Gain between joystick and cursor motion (higher = more aggressive)
    str.dataFileName  = 'Data';     % Name of file if boo.saveGameData is true
    num.fps           = 25;         % [frames per second] framerate ... faster computers may be able to go higher than this
    num.gameDuration  = 30;         % [sec]
    num.startLt       = 0.2;        % Starting size of target
    num.growLt        = 2.0;        % Amount the target grows by the time the game ends (can be negative, but not larger in magnitude than num.startLt)
    num.unitLc        = 0.4;        % Size of cursor
    num.screenWfactor = 0.7;        % Adjust width of screen (RELATIVE TO vec.targetFreqs!!!)
    num.lineW         = 4;                          % Line widths in game display
    vec.targetFreqs   = [0.1, 0.5, 1, 2, 4, 8];     % [rad/sec] sine wave frequencies of target motion
    vec.targetAmps    = 1./(vec.targetFreqs);       % Amplitudes of each sine wave in target motion
    
% Define cursor dynamics: transfer function between joystick input and cursor position output (default transfer function is a simple integrator
    vec.tfNumerator   = [1];                        % Numerator polynomial of cursor dynamics transfer function
    vec.tfDenominator = [1 0];                      % Denominator polynomial of cursor dynamics transfer function
%--------------------------------------------------------------------------

% Calculate derived settings
    num.targetMaxVelocity = sum((vec.targetFreqs).*(vec.targetAmps));
    num.dt       = 1/num.fps;
    num.frames   = num.gameDuration*num.fps + 1;
    hist.time    = linspace(0, num.gameDuration, num.frames); 
    num.screenW  = num.screenWfactor*sum(vec.targetAmps);
    num.screenH  = num.screenW/4;
    num.halfH    = num.screenH/2;
    num.halfW    = num.screenW/2;
    num.quarterW = num.halfW/2;
    num.unitLt   = num.startLt; 
    
% Initialized miscellaneous variables
    num.timeOnTarget = 0;
    num.currentFrame = 1;     

% Create discrete state space object for cursor dynamics: this requires the control systems toolbox
    [Ac, Bc, Cc, Dc]   = tf2ss(vec.tfNumerator, vec.tfDenominator);
    obj.cursorSystem   = ss(Ac, Bc, Cc, Dc);
    obj.cursorDiscrete = c2d(obj.cursorSystem, num.dt);
    num.n              = length(obj.cursorDiscrete.b);

% Create target trajectory
    [vec.phi, mat.sin, mat.cos] = MakeReferenceInput(vec.targetAmps, vec.targetFreqs, hist.time);
    hist.target = sum(mat.sin, 1);        
    
% Initialize arrays for data storage
    hist.cursor = zeros(1, num.frames);
    hist.state  = zeros(4, num.frames);
    hist.input  = zeros(1, num.frames);
    
% Initialize target and cursor position variables
    num.targetPos    = hist.target(1);
    num.cursorPos    = num.targetPos;
    vec.state        = zeros(num.n,1);
    vec.state(num.n) = num.targetPos;

% Initialize game figure and wait until user is ready    
    UpdateGameFigure();    
    waitfor(msgbox('Click ''OK'' when ready'));
    
% Run loops of tracking game ... use timer object to enforce framerate 
    obj.timer = timer('ExecutionMode', 'FixedRate', ...
                      'Period', num.dt, ...
                      'TimerFcn', @RunOneFrame, ...
                      'TasksToExecute', num.frames, ...
                      'StartDelay', 0);  

% Run timer
    start(obj.timer);
    wait(obj.timer);
    
% Notify user that game has finished    
    waitfor(msgbox(['Game time expired!', char(10), 'Your time on-target: ', num2str(num.timeOnTarget), ' seconds']));
    close all;
        
% Save results if requested by user
    if boo.saveGameData
        clear obj;
        pause(0.1);
        save([str.dataFileName, '_', datestr(now, 'ddmmmyyyy_HHMMSS'), '.mat']);
        disp(['Data file saved as "', str.dataFileName, '_', datestr(now, 'ddmmmyyyy_HHMMSS'), '.mat"'])
    end
end


%==========================================================================
function [] = RunOneFrame(objectIn, eventIn)
%==========================================================================
% Declare global variables
    global str num vec mat boo obj hist;

% Get input from joystick
    num.input = 0;
    joyInput  = jst;       
    num.input = num.inputGain*joyInput(2);

% Compute next timestep
    ComputeNextStep();

% Determine if cursor is on-target
    if abs(num.cursorPos - num.targetPos) < num.unitLt/2
        set(obj.target1, 'FaceColor', [1, 0, 0]);   % If cursor is on-target, make target pure red
        set(obj.target3, 'FaceColor', [1, 0, 0]);  
        num.timeOnTarget = num.timeOnTarget + num.dt;
    elseif abs(num.cursorPos - num.targetPos) < num.halfW
        offColors = 0.5 + 0.3*(abs(num.cursorPos - num.targetPos)/num.halfW);   % Change target colors based on how far away cursor is from target
        set(obj.target1, 'FaceColor', [1, offColors, offColors]);
        set(obj.target3, 'FaceColor', [1, offColors, offColors]);
    else
        set(obj.target1, 'FaceColor', [1, 0.8, 0.8]);
        set(obj.target3, 'FaceColor', [1, 0.8, 0.8]);
    end
    
% Increment frame number and refresh game figure
    if num.currentFrame == 1
    else
        UpdateGameFigure();
    end
    
    num.currentFrame = num.currentFrame + 1;
end


%==========================================================================
function ComputeNextStep()                        
%==========================================================================
% Declare global variables
    global str num vec mat boo obj hist;
    
% Store state from previous timestep
    vec.statePrev = vec.state;
    num.targetPos = hist.target(num.currentFrame);
    
% Calculate next time step and find cursor position
    vec.state = obj.cursorDiscrete.a*vec.statePrev + obj.cursorDiscrete.b*num.input;
    num.cursorPos = vec.state(num.n);
    
% Store data
    hist.cursor(num.currentFrame)  = num.cursorPos;
    hist.input(num.currentFrame)   = num.input;  
    hist.state(:,num.currentFrame) = vec.state;
end


%==========================================================================
function UpdateGameFigure()
%==========================================================================
% Declare global variables
    global str num vec mat boo obj hist;
   
    if num.currentFrame == 1    % Initialize figure if on first frame
        figure;
        hold on;
        set(gcf, ...
            'Units', 'normalized', ...
            'Position', [0, 0.1, 1, 0.8], ...
            'KeyPressFcn','1;', ...
            'MenuBar', 'none', ...
            'Color', [0.8, 0.8, 0.8]);  
        axis equal;
                
    % Set window boundaries based on whether pursuit or compensatory display was chosen
        if boo.pursuitView
            axis([mat.sin(1,1)-num.halfW, mat.sin(1,1)+num.halfW, -num.halfH, num.halfH]);
            set(gca, 'Box', 'on', 'YTick', [], 'XTickLabel',{''}, 'YTickLabel',{''});
        else
            axis([num.cursorPos-num.halfW, num.cursorPos+num.halfW, -num.halfH, num.halfH]);
            set(gca, 'Box', 'on', 'XTick', [], 'YTick', [], 'XTickLabel',{''}, 'YTickLabel',{''});
        end
            
    % Initialize graphical objects
        obj.target1 = rectangle('Position', [(num.targetPos-num.unitLt/2), (-num.unitLt/2), (num.unitLt), (num.unitLt)], ...
                                'Curvature', [1, 1], ...
                                'EdgeColor', [0, 0, 0], ...
                                'FaceColor', [1, 0, 0]);

        obj.target2 = rectangle('Position', [(num.targetPos-num.unitLt/3), (-num.unitLt/3), (num.unitLt/1.5), (num.unitLt/1.5)], ...
                                'Curvature', [1, 1], ...
                                'EdgeColor', 'none', ...
                                'FaceColor', [1, 1, 1]);

        obj.target3 = rectangle('Position', [(num.targetPos-num.unitLt/6), (-num.unitLt/6), (num.unitLt/3), (num.unitLt/3)], ...
                                'Curvature', [1, 1], ...
                                'EdgeColor', 'none', ...
                                'FaceColor', [1, 0.5, 0.5]); 

        obj.cursor = rectangle('Position', [(num.cursorPos-num.unitLc), (-num.unitLc), (2*num.unitLc), (2*num.unitLc)], ...
                               'Curvature', [1, 1], ...
                               'LineWidth', num.lineW);

        obj.vcross1 = line([num.cursorPos, num.cursorPos], [-1.2*num.unitLc, -0.5*num.unitLc]);
            set(obj.vcross1, 'LineStyle', '-', 'LineWidth', num.lineW, 'Color', 'k');

        obj.vcross2 = line([num.cursorPos, num.cursorPos], [0.5*num.unitLc, 1.2*num.unitLc]);
            set(obj.vcross2, 'LineStyle', '-', 'LineWidth', num.lineW, 'Color', 'k');

        obj.hcross1 = line([num.cursorPos-1.2*num.unitLc, num.cursorPos-0.5*num.unitLc], [0, 0]);
            set(obj.hcross1, 'LineStyle', '-', 'LineWidth', num.lineW, 'Color', 'k');

        obj.hcross2 = line([num.cursorPos+0.5*num.unitLc, num.cursorPos+1.2*num.unitLc], [0, 0]);
            set(obj.hcross2, 'LineStyle', '-', 'LineWidth', num.lineW, 'Color', 'k');

        obj.middleDot = rectangle('Position', [(num.cursorPos - 0.02), (-0.02), (0.04), (0.04)], ...
                                  'Curvature', [1, 1], ...
                                  'EdgeColor', 'k', ...
                                  'FaceColor', 'k');               

        obj.text1 = annotation('textbox', [0.1 0.05 0.8 0.05], 'String', ['Time On-Target: '], 'FontSize', 16, 'EdgeColor', [0.8, 0.8, 0.8]);        
    
    else
    % If later than the first frame, update graphical objects and reset display bounds
    % Reset axis by first defining nominal left and right edges of display
        if boo.pursuitView
            leftSide  = mat.sin(1,num.currentFrame) - num.halfW;
            rightSide = mat.sin(1,num.currentFrame) + num.halfW;
        else
            leftSide  = num.cursorPos - num.halfW;
            rightSide = num.cursorPos + num.halfW;
        end    
        
    % Zoom out if needed (cursor is more than half a screen away from target)
        num.lineWzoom = num.lineW;      % Start with default line widths
        for k = 1:10    
            if (num.cursorPos+1.2*num.unitLc > rightSide) || (num.cursorPos-1.2*num.unitLc < leftSide)
                leftSide  = leftSide - num.halfW;
                rightSide = rightSide + num.halfW;
                num.lineWzoom = num.lineW/(k+1);        % Adjust line width to make zoom-out more realistic
            end
        end

    % Adjust axis
        axis([leftSide, rightSide, -num.halfH, num.halfH]); 
        
    % Grow the target
        num.unitLt = num.startLt + (num.currentFrame/num.frames)*num.growLt;
        
    % Redraw figure objects
        set(obj.target1, 'Position', [(num.targetPos-num.unitLt/2), (-num.unitLt/2), (num.unitLt), (num.unitLt)]);
        set(obj.target2, 'Position', [(num.targetPos-num.unitLt/3), (-num.unitLt/3), (num.unitLt/1.5), (num.unitLt/1.5)]);            
        set(obj.target3, 'Position', [(num.targetPos-num.unitLt/6), (-num.unitLt/6), (num.unitLt/3), (num.unitLt/3)]);              
        set(obj.cursor, 'Position', [(num.cursorPos-num.unitLc), (-num.unitLc), (2*num.unitLc), (2*num.unitLc)], 'LineWidth', num.lineWzoom);          
        set(obj.vcross1, 'XData', [num.cursorPos, num.cursorPos], 'YData', [-1.2*num.unitLc, -0.5*num.unitLc], 'LineWidth', num.lineWzoom);       
        set(obj.vcross2, 'XData', [num.cursorPos, num.cursorPos], 'YData', [0.5*num.unitLc, 1.2*num.unitLc], 'LineWidth', num.lineWzoom);       
        set(obj.hcross1, 'XData', [num.cursorPos-1.2*num.unitLc, num.cursorPos-0.5*num.unitLc], 'YData', [0, 0], 'LineWidth', num.lineWzoom);
        set(obj.hcross2, 'XData', [num.cursorPos+0.5*num.unitLc, num.cursorPos+1.2*num.unitLc], 'YData', [0, 0], 'LineWidth', num.lineWzoom);
        set(obj.middleDot, 'Position', [(num.cursorPos - 0.02), (-0.02), (0.04), (0.04)], 'LineWidth', num.lineWzoom/2);
        if mod(num.currentFrame, 10) == 0
            set(obj.text1, 'String', ['Time On-Target: ', num2str(num.timeOnTarget)]);      % Update "time on-target" score
        end
    end
end


%==========================================================================
function [phiVec, sineMat, cosMat] = MakeReferenceInput(ampVec, freqVec, timeVec)
%==========================================================================
    numSines = length(freqVec);
    numTimes = length(timeVec);
    
% Pick random relative phase for each sine wave
    phiVec = 2*pi*rand(numSines,1);
    
% Initialize sine and cosine matrices
    sineMat = zeros(numSines, numTimes);
    cosMat = zeros(numSines, numTimes);
    
% Fill-in sines and cosines with amplitude from ampVec, frequency from freqVec, 
% and phase angle from phiVec
    for i = 1:numSines
        sineMat(i,:) = ampVec(i)*sin(freqVec(i)*timeVec + phiVec(i));
        cosMat(i,:) = ampVec(i)*cos(freqVec(i)*timeVec + phiVec(i));
    end
end

Contact us