% 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