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.
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).
Open ~/Unity/Projects/GameAIPrisonDodgeball_Spring2026/ in Unity
Open Open Assets/Scenes/ShootingRange.scene for testing
Implementation
Starter code
ThrowMethods.cs
// compile_check// Remove the line above if you are submitting to GradeScope for a grade. But leave it if you only want to check// that your code compiles and the autograder can access your public methods.using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;using GameAI;namespace GameAIStudent{ public class ThrowMethods { public const string StudentName = "George P. Burdell <- Not your name, change it!"; // Note: You have to implement the following method with prediction: // Either directly solved (e.g. Law of Cosines or similar) or iterative. // You cannot modify the method signature. However, if you want to do more advanced // prediction (such as analysis of the navmesh) then you can make another method that calls // this one. // Be sure to run the editor mode unit test to confirm that this method runs without // any gamemode-only logic public static bool PredictThrow( // The initial launch position of the projectile Vector3 projectilePos, // The initial ballistic speed of the projectile float maxProjectileSpeed, // The gravity vector affecting the projectile (likely passed as Physics.gravity) Vector3 projectileGravity, // The initial position of the target Vector3 targetInitPos, // The constant velocity of the target (zero acceleration assumed) Vector3 targetConstVel, // The forward facing direction of the target. Possibly of use if the target // velocity is zero Vector3 targetForwardDir, // For algorithms that approximate the solution, this sets a limit for how far // the target and projectile can be from each other at the interceptT time // and still count as a successful prediction float maxAllowedErrorDist, // 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: An alternate time at which the projectile and target collide // Note that this is optional to use and does NOT coincide with the solved projectileDir // and projectileSpeed. It is possibly useful to pass on to an incremental solver. // It only exists to simplify compatibility with the ShootingRange out float altT) { // TODO implement an accurate throw with prediction. This is just a placeholder // FYI, if Minion.transform.position is sent via param targetPos, // be aware that this is the midpoint of Minion's capsuleCollider // (Might not be true of other agents in Unity though. Just keep in mind for future game dev) // Only going 2D for simple demo. this is not useful for proper prediction // Basically, avoiding throwing down at enemies since we aren't predicting accurately here. var targetPos2d = new Vector3(targetInitPos.x, 0f, targetInitPos.z); var launchPos2d = new Vector3(projectilePos.x, 0f, projectilePos.z); var relVec = (targetPos2d - launchPos2d); interceptT = relVec.magnitude / maxProjectileSpeed; altT = -1f; // This is a hard-coded approximate sort of of method to figure out a loft angle // This is NOT the right thing to do for your prediction code! // Refer to assignment reqs and ballistic trajectory lecture! var normAngle = Mathf.Lerp(0f, 20f, interceptT * 0.007f); var v = Vector3.Slerp(relVec.normalized, Vector3.up, normAngle); // Make sure this is normalized! (The direction of your throw) projectileDir = v; // You'll probably want to leave this as is. For some prediction methods you can slow your throw down // You don't need to predict the speed of your throw. Only the direction assuming full speed. // Note that Law of Cosines with holdback WILL require adjusting this. projectileSpeed = maxProjectileSpeed; // TODO return true or false based on whether target can actually be hit // This implementation just thinks, "I guess so?", and returns true. // Implementations that don't exactly solve intercepts will need to test the approximate // solution with maxAllowedErrorDist. If your solution does solve exactly, you will // probably want to add a debug assertion to check your solution against it. return true; } }}
ShotSelection.cs
// compile_check// Remove the line above if you are submitting to GradeScope for a grade. But leave it if you only want to check// that your code compiles and the autograder can access your public methods.using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;using GameAI;namespace GameAIStudent{ 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; } }}
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.
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:
a=pp0−pt0
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
ShotSelection.cs
// compile_check// Remove the line above if you are submitting to GradeScope for a grade. But leave it if you only want to check// that your code compiles and the autograder can access your public methods.using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.AI;using GameAI;namespace GameAIStudent{
> public class ShotSelection
> {
> public const string StudentName = "George P. Burdell <- Not your name, change it!";
> // 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;
> }
> // 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)