Intro

Abstract

This project involves implementing ballistic trajectory prediction for a projectile attempting to intercept a moving target. The goal is to compute the firing direction and timing needed for the projectile, given the target’s motion and the effects of gravity. The 2nd part of the project involves implementing shot selection heuristics to decide when to throw a dodgeball at enemy targets.

description

TODO

  • 1.) Implement ballistic trajectory prediction for projectiles
  • 2.) Implement shot selection logic (decide when it is appropriate throw a dodgeball)

Ballistic Trajectory

Shot Selection

Once you have PredictThrow, now you’re deciding: “Even if I can hit them… should I throw right now?” which can be answered by checking for the following:

  • Is opponent currently accelerating?
  • Will opponent be forced to change direction? (NavMesh)
  • Is the throw path blocked? (Physics.Raycast)

The MinionThrowTester calls your ShotSelection logic. That state machine is a bare bones AI Agent that just stands in one spot and requests a ball for throwing whenever he doesn’t have one (allowed in special throw testing mode)

Resources

Useful Unity Features

For this assignment, you will probably want to use several built-in Unity features.
Refer to the Unity documentation for full details.

  • Mathf
    General math utilities such as constants, interpolation, clamping, square roots, etc.

  • Vector3 / Vector2
    Structures used to represent vectors in 3D and 2D space. Provide many built-in operations such as:

    • magnitude
    • normalization
    • dot products
    • distance calculations
  • NavMesh.Raycast() (Only used in SelectThrow())
    Useful for checking the extrapolated path of the opponent along the navigation mesh.

  • Physics.Raycast() (Only used in SelectThrow())
    Useful for detecting collisions along the projectile path before it reaches the target.

  • Debug.DrawLine() (Only used in SelectThrow())
    Useful for visualizing raycasts in the Scene view to verify they are working correctly.

Ballistic Trajectory Prediction

You can use any method for prediction including Millington’s static target method coupled with iterative refinement, Law of Cosines (LoC) with 10% Holdback, LoC with iterative refinement, or directly solve with a more advanced method (you will likely need to research quartic solving methods and implement).

Static target method

Iterative Method

Iterative Refinement for Projectile Prediction

  • Concept: Predict where a moving target will be when your projectile arrives.
    • Analogy- Like adjusting your throw in mid-air, but mathematically, before actually throwing.
  • Step 1 – Initial Guess:

    • Assume target is at its current position.
    • Compute initial flight time t₁ and projectile launch vector.
  • Step 2 – Predict Target Future Position:

    • Target moves during t₁ → calculate predicted position at t₁.
  • Step 3 – Recompute Projectile Path:

    • Aim at predicted position.
    • Calculate new flight time t₂ and updated launch vector.
  • Step 4 – Iterate:

    • Each iteration updates intercept time and aim point.
    • Converges toward actual intercept where projectile meets moving target.

Setup

  • Open project files

~/Unity/Projects/GameAIPrisonDodgeball_Spring2026/Assets/Scripts/GameAIStudentWork

  • Open ~/Unity/Projects/GameAIPrisonDodgeball_Spring2026/ in Unity

  • Open Open Assets/Scenes/ShootingRange.scene for testing

Implementation

  • Starter code

Testing

Shooting Range Scene

Scene: Assets/Scenes/ShootingRange.scene

Used to test your ThrowBall() implementation in a controlled environment.

Key notes:

  • Assets/Scripts/ShootingRange/ShootingRange.cs can be modified locally
  • The autograder uses the original version
  • Changes may produce different results than the autograder

Controls:

  • Keyboard presets simulate target motion
  • Mode 4 ≈ closest to the Prison Dodgeball scenario
  • Spacebar → switch between shooting algorithms
  • R → reset statistics

You can add new aim methods in Awake() as long as they match the PredictThrow() method signature.


Unit / Integration Tests

Editor Mode tests
Assets/Scripts/GameAIStudentWork/EditorModeTests

  • Example test calling PredictThrow()
  • Used for isolated function testing

Play Mode tests
Assets/Scripts/GameAIStudentWork/PlayModeTests

Tests include:

  • Shooting Range performance (trajectory solver)
  • AdvancedMinionTestThrowScenario (shot selection + trajectory)

Use the Unity Test Runner and select either:

  • EditorMode

  • PlayMode

  • Locate TestResults.xml

ls ~/.config/unity3d/DefaultCompany/GameAI_PrisonDodgeball/

Rubric

  • PredictThrow() tests — 20%
  • Shooting Range performance — 40%
  • Shot selection performance — 40%

Provided tests use the same scoring algorithm as the autograder.

Tips

PredictThrow Return Value

  • Do not always return true from PredictThrow().
  • The autograder runs isolated tests on this function, and your shot selection logic depends on accurate returns.
  • You must determine if the projectile actually intercepts the target within maxAllowedErrorDist at interceptT.

How to Validate the Solution

  • Compute projectile position at interceptT using the kinematic equation:
  • Compute target position at interceptT:
  • Check the distance:
  • If this condition fails:

Code

return false;

Output Parameters When Returning false

  • If PredictThrow() returns false, the out parameters are considered undefined.
  • However, assigning safe defaults is recommended:

Code

projectileDir = Vector3.zero;
projectileSpeed = 0f;
interceptT = -1f;
altT = -1f;
  • The autograder will not evaluate these values when the function returns false.

Law of Cosines Lecture Error

  • The relative vector a is reversed in the lecture video.
  • The slides/notes contain the correct version.

Correct form:

  • Using the incorrect version may still work but tends to produce the slower high-arc solution.

Law of Cosines Special Case

  • There is a special case when the denominator becomes zero.
  • Handling this case allows the solver to work in additional edge conditions and improves reliability slightly.

Completed

TODO

  • asdf

ShotSelection Starter

> public class ShotSelection
> {
    > public const string StudentName = "George P. Burdell <- Not your name, change it!";
    > public enum SelectThrowReturn
    > {
        > DoThrow,
        > NoThrowTargettingFailed,
        > NoThrowOpponentCurrentlyAccelerating,
        > NoThrowOpponentWillAccelerate,
        > NoThrowOpponentOccluded
    > }
    > public static SelectThrowReturn SelectThrow(
            > // the minion doing the throwing, can also be used to query generic params true of all minions
            > MinionScript thisMinion,
            > // info about the target
            > PrisonDodgeballManager.OpponentInfo opponent,
            > // What is the navmask that defines where on the navmesh the opponent can traverse
            > int opponentNavmask,
            > // typically this is a value a tiny bit smaller than the radius of minion added with radius of the dodgeball
            > float maxAllowedThrowErrDist,
            > // Time since last frame
            > float deltaT,
            > // Output param: The solved projectileDir for ballistic trajectory that intercepts target                
            > out Vector3 projectileDir,
            > // Output param: The speed the projectile is launched at in projectileDir such that
            > // there is a collision with target. projectileSpeed must be <= maxProjectileSpeed
            > out float projectileSpeed,
            > // Output param: The time at which the projectile and target collide
            > out float interceptT,
            > // Output param: where the shot is expected to hit
            > out Vector3 interceptPos
        > )
    > {
        > var Mgr = PrisonDodgeballManager.Instance;
        > var opponentVel = opponent.Vel; // Or perhaps use thisMinion.MaxPathSpeed (max speed a minion can go)
                                        > // times dir if you think minion is nearly there.
                                        > // Using something other than the opponent's current Vel requires extra logic
        > interceptPos = opponent.Pos;
        > // see if throw is even possible, before deciding whether to actually do it
        > if (!ThrowMethods.PredictThrow(thisMinion.HeldBallPosition, thisMinion.ThrowSpeed, Physics.gravity, opponent.Pos,
            > opponentVel, opponent.Forward, maxAllowedThrowErrDist,
            > out projectileDir, out projectileSpeed, out interceptT, out float altT))
        > {
            > return SelectThrowReturn.NoThrowTargettingFailed;
        > }
        > interceptPos = opponent.Pos + opponent.Vel * interceptT;
        > // OK, the throw is possible based on assumptions. But there are other reasons why we might skip throwing right now.
        > // TODO Screen opponent. Consider if there are obvious signs that the agent is accelerating (breaking constant acceleration assumption)
        > // possibilities:
        > // * agent not currently sufficiently stopped or sufficiently up to full speed
        > // * agent appears to be turning significantly from previous direction
        > // On failure, return NoThrowOpponentCurrentlyAccelerating
        > // TODO Next consider the impact of the environment on future agent movement. 
        > // Use NavMesh.Raycast() to determine if opponent would run into barrier before throw would get there
        > // We know the opponent won't actually run into a barrier, so if you get a raycast hit that means the opponent
        > // is going to be changing direction or stopping. So it is probably good to not throw in these circumstances.
        > // NavMesh.Raycast() call and appropriate logic goes here (also see: opponentNavmask)
        > // On failure, return NoThrowOpponentWillAccelerate
        > // TODO next consider the possibility that if the ball is thrown, it will hit something before it gets to the agent.
        > // This isn't helpful for a normal game of prison dodgeball (nothing to get in the way) but it is important
        > // for the AdvancedMinionTestThrowScenario.
        > // You should use Physics.Raycast()
        > // TIP: For best result in AdvancedMinionTestThrowScenario map, cast two parallel rays ball width apart
        > // Use the mask below for ignoring geometry we don't care about.
        > // carverMask exclusion only needed for AdvancedMinionTestThrowScenario
        > int carverMask = ~(1 << Mgr.NavMeshCarverLayerIndex);
        > // We don't care about minion hits from raycast. Self hits should already be avoided but will filter all minions.
        > // And the whole point of the throw is to hit the opponent minion, so we don't want a raycast hit stopping us.
        > int minionMask = ~(1 << Mgr.MinionTeamBLayerIndex) & ~(1 << Mgr.MinionTeamALayerIndex);
        > // Ignore dodgeballs. They'll most likely be out of the way before they collide
        > int ballMask = ~(1 << Mgr.BallTeamALayerIndex) & ~(1 << Mgr.BallTeamBLayerIndex);
        > int mask = Physics.AllLayers & carverMask & ballMask & minionMask;
        > // On failure due to Physics.Raycast() hit, return NoThrowOpponentOccluded
        > // We got this far, so the throw is probably a good idea!
        > return SelectThrowReturn.DoThrow;
    > }
> }

}

Tips

  • If you are “close enough” to your target or at “point-blank range” then forget any complex calculations just fire away (start with a hardcoded value but can calculate it dynamically)

Related