Creating Training Exercises

phrack edited this page Oct 15, 2016 · 24 revisions

Training exercises are what make ShootOFF fun. While we always enjoy seeing new exercises you create, as of 3.8 we no longer accept new default training exercises from developers that are not ShootOFF core developers. This is the case both because additional training exercises increase our support burden when included as defaults and because ShootOFF already includes a large number of default exercises (i.e. we don't want to increase the number of options even further for new users unless they intentionally seek out additional plugins).

Training exercises are written in Java and can be quite powerful. At the end of the day, an exercise can do anything you are competent to program. ShootOFF provides an API for training exercises. These APIs are implemented in the following classes:


If your exercise is intended to be used regardless of how the user is practicing with ShootOFF, it should extend com.shootoff.plugins.TrainingExerciseBase -- these are known as standard exercises. If the exercise is intended to only work on the projector arena it should extend com.shootoff.plugins.ProjectorTrainingExerciseBase -- these are known as projector-only exercises.

Regardless of the type of exercise you are creating, it must implement com.shootoff.plugins.TrainingExercise otherwise it cannot be used by ShootOFF

The current implementation of the com.shootoff.plugins.TrainingExercise interface is available here:


Please carefully read the javadoc comments for each method your exercise must implement.

Packaging and Using Modular Training Exercises

An exercise should be contained in a single jar file. ShootOFF will only recognize the jar as a plugin if it contains a configuration file named shootoff.xml at the root of the jar file. Here is an example shootoff.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<shootoffExercise exerciseClass="com.shootoff.plugins.ShotScore" />

At this time, there is only one interesting parameter to configure: the exerciseClass. The exerciseClass is essentially your exercises main class. This is the class that implements com.shootoff.plugins.TrainingExercise and contains the most important event-processing methods for any exercise.

To register your training exercise, copy the jar file into ${shootoff.home}/exercises (create the folder if it doesn't already exist). ShootOFF will detect the new jar file, will automatically register the exercise, and will add it to the training menu if it is a valid ShootOFF exercise. You can unregister an exercise by deleting the jar file, in which case ShootOFF will de-activate it if it is enabled and will remove it from the training menu.

If ShootOFF is run standalone ${shootoff.home} is the current working directory ShootOFF was run from (usually the same directory ShootOFF.jar is in). If ShootOFF is run from web start (i.e. the browser) ${shootoff.home} is ${HOME}/.shootoff. For example, if the username is neil and the user is running Windows, ${shootoff.home} when run from the browser is C:\user\neil\.shootoff.

You can view example training exercise jar files in src/test/exercises in the main ShootOFF repository. Aside from Unknown.jar, which is not a valid ShootOFF exercise, the rest are copies of default exercises implemented as modular exercises.

If you want other people to be able to install and use your packaged exercise from the Exercise Manager, add a plugin tag for your exercise to the plugin metadata file:

Standard Exercise API

The standard exercise API is implemented in com.shootoff.plugins.TrainingExerciseBase. com.shootoff.plugins.ShootForScore is a simple example of a standard exercise that you should consider altering to create your first exercise.

public void getDelayedStartInterval(final DelayedStartListener listener)

Ask the user for a delayed start interval which can be used to pick a random delay for exercise rounds.

Example usage:

public class YourExercise extends TrainingExerciseBase implements TrainingExercise, **DelayedStartListener** {

    public void updatedDelayedStartInterval(int min, int max) {
        delayMin = min;
        delayMax = max; 


    final int randomDelay = new Random().nextInt((delayMax - delayMin) + 1) + delayMin;

public void getParInterval(final ParListener listener)

Similar to getDelayedStartInterval, but for retrieving a par interval.

public void addShotTimerColumn(String name, int width)

Add a column to the shot timer table on the main ShootOFF window. ShootOFF keeps track of any columns you add and will automatically remove them when your exercise is destroyed.

Example usage:

super.addShotTimerColumn("Round", 80);

public void setShotTimerColumnText(final String name, final String value)

Set the text of an exercise-specific column (added with addShotTimerColumn) in the shot timer table on the main ShootOFF window.

Example usage:

super.setShotTimerColumnText("Round", "1");

public void setShotTimerRowColor(final Color c)

Set the background color for an row added to the shot timer table on the main ShootOFF window after this method is called. This is useful for visually separating data for different rounds.

Example usage:

if (coloredRows) {
} else {
    // Set default background color

public void showTextOnFeed(String message)

Show message in the top left corner of all webcam feeds. Any message is automatically removed when your exercise is destroyed.

Example usage:

String message = String.format("red score: %d%ngreen score: %d", redScore, greenScore);

public void clearShots()

Clear all shots that have been detected on all active webcam feeds.

public void reset()

Perform the same operation that occurs when the user hits the reset button (this clears all shots, resets shot detection filters, and resets whatever exercise is active.

public void pauseShotDetection(final boolean isPaused)

Either enable or disable shot detection for all webcam feeds. It is useful to disable shot detection (e.g. this.pauseShotDetection(true) when you are intentionally making the user wait and do not want to process their shots.

public Button addShootOFFButton(final String text, final EventHandler<ActionEvent> eventHandler)

Adds a button the right of the reset button on the main ShootOFF window. ShootOFF keeps track of the buttons you add and will automatically remove them when your exercise is destroyed.

this.pauseResumeButton = addShootOFFButton(PAUSE, (event) -> {
    Button pauseResumeButton = (Button) event.getSource();
    if (PAUSE.equals(pauseResumeButton.getText())) {
        repeatExercise = false;
    } else {
        repeatExercise = true;
        executorService.schedule(new SetupWait(), RESUME_DELAY, TimeUnit.SECONDS);

public void removeShootOFFButton(final Button exerciseButton)

Remove a button you added with addShootOFFButton. You only need to call this if you are intentionally removing a button before the destroy event. ShootOFF will automatically remove any button you added when your exercise is destroyed.

public static void playSound(String soundFilePath)
public static void playSound(File soundFile)
public static void playSound(InputStream is)
public static void playSounds(final List<File> soundFiles)

Play a .wav file or multiple .wav files one after another.

Projector-only Exercise API

The projector-only exercise API is implemented in com.shootoff.plugins.ProjectorTrainingExerciseBase. com.shootoff.plugins.ShootDontShoot is a simple example of a projector-only exercise that you should consider altering to create your first exercise.

public Optional<Target> addTarget(File target, final double x, final double y)

Add a target to the projector arena at a specific coordinate. ShootOFF will automatically remove any targets you added when your exercise is destroyed.

Example usage:

int x = rng.nextInt(((int) super.getArenaWidth() - 100) + 1) + 0;
int y = rng.nextInt(((int) super.getArenaHeight() - 100) + 1) + 0;

Optional<Target> newTarget = super.addTarget(new File("targets/shoot_dont_shoot/"), x, y);
if (newTarget.isPresent()) // do something with newTarget.get(). ... 
                           (or nothing, the target is displayed and usable anyway)

You can add an @ to the front of the target file name to signal to the API that the target is actually a resource in a modular exercise:

Optional<Target> newTarget = super.addTarget(new File("@clays/"), x, y);

public void showTextOnFeed(String message)
public void showTextOnFeed(String message, boolean showOnArena)
public void showTextOnFeed(String message, int x, int y, Color backgroundColor, Color textColor, Font font)

Show a message on all webcam feeds and the arena, all webcam feeds and not the arena, or all webcam feeds and the arena with a custom location, color, and font configuration for the message on the arena. Once location, colors, and fonts are set for an arena message they will continue to be used by showTextOnFeed versions that do not set them for the duration of the exercise's session.

Example usage:

super.showTextOnFeed("left score: 0\nright score: 0", 50, 100, Color.NAVY, Color.YELLOW, 
    new Font(Font.getDefault().getFamily(), 20));

public void removeTarget(Target target)

Remove a target you added with addTarget. This is not required unless you need to remove a target before the destroy event occurs for your exercise.

Example usage:


public void setArenaBackground(LocatedImage background)
public void setArenaBackground(String defaultResourcePath)

Set the background of the projector arena to an image that is either a file on the filesystem or a JAR resource. GIF or PNG is preferred because JPG can be flaky with JavaFX.

Example usage:

// Use backgrounds from the training exercise's jar
super.setArenaBackground(new LocatedImage(new File("images/some_image.png").toURI().toString()));

String resourceFilename = "background/shotgun_range.gif";
InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceFilename);
LocatedImage img = new LocatedImage(is, resourceFilename);

// Use a default background

public double getArenaWidth()
public double getArenaHeight()

Get the projector arena's current dimensions. This is useful if you need to place a target or image on the arena in a specific and visible location.

public List<Target> setCourse(File courseFile)

Remove all targets on the and replace them with targets loaded from courseFile.

public boolean isPerspectiveInitialized()
public boolean setTargetDistance(Target target, int currentRealWidth, int currentRealHeight,
			int currentRealDistance, int desiredDistance)

Detetermine whether or not perspective projection was successfully initialized and resize a target to appear as if it is desiredDistance millimeters away. Setting the target distance assumes you know what its current width and height are on the projection and its current "apparent" distance all in millimeters. You will likely not know the target's current dimensions and distances unless the target file contains default width, height, and distance data or you are give the target default dimenions. You can give a target defaults at run time by setting currentRealWidth and currentRealHeight to your desired defaults and setting currentRealDistance and desiredDistance to the value you want as the target's default distance.

Example usage:

if (!isPerspectiveInitialized()) {
    // Show the user an error that this exercise can't work
    // because perspective isn't initialized and link them to

// ...

if (target.tagExists(Target.TAG_CURRENT_PERCEIVED_WIDTH)
		&& target.tagExists(Target.TAG_CURRENT_PERCEIVED_HEIGHT)
		&& target.tagExists(Target.TAG_CURRENT_PERCEIVED_DISTANCE)) {

	int width = Integer.parseInt(target.getTag(Target.TAG_CURRENT_PERCEIVED_WIDTH));
	int height = Integer.parseInt(target.getTag(Target.TAG_CURRENT_PERCEIVED_HEIGHT));
	int distance = Integer.parseInt(target.getTag(Target.TAG_CURRENT_PERCEIVED_DISTANCE));

        // Put the target a meter farther out than it is now
        setTargetDistance(width, height, distance, distance + 1000 /* mm */);