Build a Finite State Machine (FSM) to control a team of minions in Prison Dodgeball.
Your goal is to create agents that can consistently beat opponent AI using movement, throwing, evasion, and teamwork.
Use provided FSM framework (don’t rewrite the core engine)
Add states in Start() via AddState()
Key APIs
Movement: GoTo(), Stop()
Aiming: FaceTowardsForThrow()
Actions: ThrowBall(), Evade()
Info: opponent + ball queries from manager
ThrowMethods.cs
ShotSelection.cs
Finite State Machine
MinionStateMachine.cs Starter Code
// 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;using MinionFSMData = GameAIStudent.BaseMinionStateMachine.MinionFSMData;namespace GameAIStudent{ [DefaultExecutionOrder(100)] [RequireComponent(typeof(MinionScript))] public class MinionStateMachine : BaseMinionStateMachine { public const string StudentName = "George P. Burdell ← Not your name. Change it!"; protected override string StudentNameText => StudentName; public const string GlobalTransitionStateName = "GlobalTransition"; public const string CollectBallStateName = "CollectBall"; public const string GoToThrowSpotStateName = "GoToThrowBall"; public const string ThrowBallStateName = "ThrowBall"; public const string DefensiveDemoStateName = "DefensiveDemo"; public const string GoToPrisonStateName = "GoToPrison"; public const string LeavePrisonStateName = "LeavePrison"; public const string GoHomeStateName = "GoHome"; public const string RescueStateName = "Rescue"; public const string RestStateName = "Rest"; // For throws... public static float MaxAllowedThrowPositionError = (0.25f + 0.5f) * 0.99f; // Example of extending TeamShare with custom data ("blackboard" style). // Add your own fields/methods here. In states, access it via GetTeamShare<ExampleTeamShare>(). class ExampleTeamShare : TeamShare { // Example data: a label and a timestamp of when the share was created. public string TeamLabel { get; private set; } public float CreatedAtTime { get; private set; } public ExampleTeamShare(PrisonDodgeballManager.Team team, int teamSize, int numBalls) : base(team, teamSize, numBalls) { TeamLabel = $"Team {team} Share"; CreatedAtTime = Time.timeSinceLevelLoad; } } // Override this method to provide your own TeamShare subtype with extra data. protected override TeamShare CreateTeamShare(PrisonDodgeballManager.Team team, int teamSize, int numBalls) { return new ExampleTeamShare(team, teamSize, numBalls); } // Example of extending the base state types for shared helpers. abstract class MinionStateCommon : MinionState { protected ExampleTeamShare Shared => GetTeamShare<ExampleTeamShare>(); } abstract class MinionStateCommon<S0> : MinionState<S0> { protected ExampleTeamShare Shared => GetTeamShare<ExampleTeamShare>(); } abstract class MinionStateCommon<S0, S1> : MinionState<S0, S1> { protected ExampleTeamShare Shared => GetTeamShare<ExampleTeamShare>(); } // Go get a ball! class CollectBallState : MinionStateCommon { public override string Name => CollectBallStateName; bool hasDestBall = false; PrisonDodgeballManager.DodgeballInfo destBall; public override void Enter() { base.Enter(); if (FindClosestAvailableDodgeball(out destBall)) { hasDestBall = true; Minion.GoTo(destBall.Pos); } } public override StateTransitionBase<MinionFSMData> Update() { // could pick up a ball accidentally before getting to desired ball if (Minion.HasBall) return ParentFSM.CreateStateTransition(GoToThrowSpotStateName); var dbInfo = TeamData.DBInfo; if (dbInfo == null) return null; if (hasDestBall) { destBall = dbInfo[destBall.Index]; if (destBall.IsHeld || destBall.State != PrisonDodgeballManager.DodgeballState.Neutral || !destBall.Reachable) hasDestBall = false; } if (!hasDestBall && FindClosestAvailableDodgeball(out destBall)) hasDestBall = true; if (hasDestBall) { // The ball might be moving, so keep updating. Minion.GoTo(destBall.NavMeshPos); } else { // No ball, so focus on defense return ParentFSM.CreateStateTransition(DefensiveDemoStateName); } return null; } } // This state gets the minion close to the enemy for a throw (or rescue) class GoToThrowSpotState : MinionStateCommon { public override string Name => GoToThrowSpotStateName; public override void Enter() { base.Enter(); Minion.GoTo(Mgr.TeamAdvance(Team).position); } public override StateTransitionBase<MinionFSMData> Update() { // just in case something bad happened if (!Minion.HasBall) return ParentFSM.CreateStateTransition(CollectBallStateName); if (Minion.ReachedTarget()) { if (FindRescuableTeammate(out var m)) return ParentFSM.CreateStateTransition<MinionScript>(RescueStateName, m, true); return ParentFSM.CreateStateTransition(ThrowBallStateName); } return null; } } // Rescue a buddy class RescueState : MinionStateCommon<MinionScript> { public override string Name => RescueStateName; MinionScript buddy; public override void Enter(MinionScript m) { base.Enter(m); buddy = m; Minion.FaceTowards(buddy.transform.position); } public override StateTransitionBase<MinionFSMData> Update() { // just in case something bad happened if (!Minion.HasBall) return ParentFSM.CreateStateTransition(CollectBallStateName); if (buddy == null || !buddy.CanBeRescued) if (!FindRescuableTeammate(out buddy)) buddy = null; // Nothing to do without buddy in prison... if (buddy == null) return ParentFSM.CreateStateTransition(ThrowBallStateName); var canThrow = ThrowMethods.PredictThrow( Minion.HeldBallPosition, Minion.ThrowSpeed, Physics.gravity, buddy.transform.position, buddy.Velocity, buddy.transform.forward, MaxAllowedThrowPositionError, out var dir, out var speed, out var t, out var _); var intercept = Minion.HeldBallPosition + dir * speed * t; Minion.FaceTowardsForThrow(intercept); if (canThrow) if (Minion.ThrowBall(dir, speed / Minion.ThrowSpeed)) return ParentFSM.CreateStateTransition(CollectBallStateName); return null; } } // Throw the ball at the enemy class ThrowBallState : MinionStateCommon { public override string Name => ThrowBallStateName; int opponentIndex = -1; PrisonDodgeballManager.OpponentInfo opponentInfo; bool hasOpponent = false; public override void Enter() { base.Enter(); if (Mgr.FindClosestNonPrisonerOpponentIndex(Minion.transform.position, Team, out opponentIndex)) if (hasOpponent = Mgr.GetOpponentInfo(Team, opponentIndex, out opponentInfo)) Minion.FaceTowards(opponentInfo.Pos); } public override StateTransitionBase<MinionFSMData> Update() { // just in case something bad happened if (!Minion.HasBall) return ParentFSM.CreateStateTransition(CollectBallStateName); // Check if opponent still valid if (!(hasOpponent = Mgr.GetOpponentInfo(Team, opponentIndex, out opponentInfo)) || opponentInfo.IsPrisoner || opponentInfo.IsFreedPrisoner) { if (Mgr.FindClosestNonPrisonerOpponentIndex(Minion.transform.position, Team, out opponentIndex)) hasOpponent = Mgr.GetOpponentInfo(Team, opponentIndex, out opponentInfo); } // Nothing to do without opponent... if (!hasOpponent) return ParentFSM.CreateStateTransition(DefensiveDemoStateName); var selection = ShotSelection.SelectThrow( Minion, opponentInfo, NavMesh.AllAreas, MaxAllowedThrowPositionError, Time.deltaTime, out var dir, out var speed, out var t, out var pos); if (selection == ShotSelection.SelectThrowReturn.DoThrow) if (Minion.ThrowBall(dir, Mathf.Min(1f, speed / Minion.ThrowSpeed))) return ParentFSM.CreateStateTransition(CollectBallStateName); Minion.FaceTowardsForThrow(pos); return null; } } // Handles global/wildcard transitions class GlobalTransitionState : MinionStateCommon { public override string Name => GlobalTransitionStateName; bool wasPrisoner = false; public override StateTransitionBase<MinionFSMData> Update() { if (Mgr.IsGameOver && !ParentFSM.CurrentState.Name.Equals(RestStateName)) return ParentFSM.CreateStateTransition(RestStateName); if (Minion.IsPrisoner && !wasPrisoner) { wasPrisoner = true; return ParentFSM.CreateStateTransition(GoToPrisonStateName); } else if (!Minion.IsPrisoner && wasPrisoner) wasPrisoner = false; return null; } } protected override void ConfigureFSM(FiniteStateMachine<MinionFSMData> fsm) { // Handles global/wildcard transitions fsm.SetGlobalTransitionState(new GlobalTransitionState()); fsm.AddState(new CollectBallState(), true); fsm.AddState(new GoToThrowSpotState()); fsm.AddState(new ThrowBallState()); fsm.AddState(new DefensiveDemoState()); fsm.AddState(new GoToPrisonState()); fsm.AddState(new LeavePrisonState()); fsm.AddState(new GoHomeState()); fsm.AddState(new RescueState()); fsm.AddState(new RestState()); } }}