/// <summary> /// Handles the case where the current Audio AiTarget contains a current Audio Threat. /// </summary> /// <returns>A new state or NONE if nothing needed to be handled</returns> private AiStateType HandleAudioThreat() { AiStateType state = AiStateType.None; AiThreatManager manager = zombieStateMachine.ThreatManager; AiTarget audioThreat = zombieStateMachine.ThreatManager.CurrentAudioThreat; if (manager.IsTargeting(AiTargetType.Visual_Food)) { state = AiStateType.Alerted; } else if (manager.IsTargeting(AiTargetType.Audio)) { // Get unique ID of the collider of our target int currentID = zombieStateMachine.ThreatManager.CurrentTarget.GetColliderID(); // If this is the same light if (currentID == zombieStateMachine.ThreatManager.CurrentAudioThreat.GetColliderID()) { RepathToThreatIfNecessary(audioThreat); state = AiStateType.Pursuit; } else { state = AiStateType.Alerted; } } manager.TrackTarget(audioThreat); return(state); }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either Idle or a new state based upon the threats that were processed</returns> public override AiStateType OnUpdate() { UpdateTimer(); AiTarget? potentialThreat = this.zombieStateMachine.ThreatManager.DeterminePotentialThreat(); AiStateType state = this.zombieStateMachine.ThreatManager.DetermineNextPotentialThreatState(potentialThreat); if (state == AiStateType.None) { if (HasReachedMaxTime()) { zombieStateMachine.WaypointManager.TrackWayPoint(); state = AiStateType.Alerted; } else { state = GetDefaultStateType(); } } else { this.zombieStateMachine.ThreatManager.TrackTarget((AiTarget)potentialThreat); } return(state); }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either the current state or a new state.</returns> public override AiStateType OnUpdate() { // setting it here so changes in inspector take immediate affect this.zombieStateMachine.Speed = this.speed; AiTarget? potentialThreat = this.zombieStateMachine.ThreatManager.DeterminePotentialThreat(); AiStateType state = this.zombieStateMachine.ThreatManager.DetermineNextPotentialThreatState(potentialThreat); if (state == AiStateType.None) { state = GetDefaultStateType(); } else { bool isFoodThreat = this.zombieStateMachine.ThreatManager.DoesFoodThreatExist(); if (isFoodThreat && !this.zombieStateMachine.CanHungerBeSatisfied((AiTarget)potentialThreat)) { state = GetDefaultStateType(); } else { this.zombieStateMachine.ThreatManager.TrackTarget((AiTarget)potentialThreat); } } if (state == GetDefaultStateType()) { state = HandleDefaultState(); } return(state); }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either the current state or a new state.</returns> public override AiStateType OnUpdate() { AiStateType state = GetDefaultStateType(); AdjustSpeed(); if (zombieStateMachine.ThreatManager.DoesPlayerThreatExist()) { zombieStateMachine.ThreatManager.TrackTarget(zombieStateMachine.ThreatManager.CurrentVisualThreat); if (zombieStateMachine.IsInMeleeRange) { FaceTargetGradually(this.slerpSpeed); RandomlySetNextAttackAnimation(); } else { state = AiStateType.Pursuit; } } else { // PLayer has stepped outside out FOV or hidden so face in his/her direction and then // drop back to Alerted mode to give the AI a chance to re-aquire target FaceTarget(); state = AiStateType.Alerted; } return(state); }
/// <summary> /// Handles the case where the current visual threat is a Visual_light. /// </summary> /// <returns>A new state or NONE if nothing needed to be handled</returns> private AiStateType HandleVisualLightThreats() { AiStateType state = AiStateType.None; AiThreatManager manager = zombieStateMachine.ThreatManager; AiTarget visualThreat = zombieStateMachine.ThreatManager.CurrentVisualThreat; // and we currently have a lower priority target then drop into alerted // mode and try to find source of light if (manager.IsTargeting(AiTargetType.Audio) || manager.IsTargeting(AiTargetType.Visual_Food)) { state = AiStateType.Alerted; } else if (manager.IsTargeting(AiTargetType.Visual_Light)) { // Get unique ID of the collider of our target int currentID = zombieStateMachine.ThreatManager.CurrentTarget.GetColliderID(); // If this is the same light if (currentID == visualThreat.GetColliderID()) { RepathToThreatIfNecessary(visualThreat); state = AiStateType.Pursuit; } else { state = AiStateType.Alerted; } } manager.TrackTarget(visualThreat); return(state); }
/// <summary> /// Determines the next potential state according to the provided potentil threat. Certain conditions /// may still need to be applied after the fact before the entity will assume that new state. /// I got lucky creating this early on. It turned out that for the future videos, they nearly all returned /// the same states shown here. Using it seems to simplify the clarity/readability of the code for me, but /// it did come at a cost (often I had issues, such as a typo, and I couldn't rely on Gary's code to help me resolve them). /// </summary> /// <param name="potentialThreat">A potential threat or null.</param> /// <returns>The next potential state according to the provided threat or NONE</returns> public AiStateType DetermineNextPotentialThreatState(AiTarget?potentialThreat) { AiStateType state = AiStateType.None; if (potentialThreat != null) { switch (((AiTarget)potentialThreat).Type) { case AiTargetType.Visual_Player: state = AiStateType.Pursuit; break; case AiTargetType.Visual_Light: state = AiStateType.Alerted; break; case AiTargetType.Audio: state = AiStateType.Alerted; break; case AiTargetType.Visual_Food: state = AiStateType.Pursuit; break; default: state = AiStateType.None; break; } } return(state); }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either the current state or a new state.</returns> public override AiStateType OnUpdate() { UpdateTimers(); if (HasReachedMaxTime()) { // reset the waypoint but stay in the alerted state with a fresh timer zombieStateMachine.WaypointManager.TrackWayPoint(); ResetMaxDurationTimer(); } AiTarget? potentialThreat = zombieStateMachine.ThreatManager.DeterminePotentialThreat(); AiStateType state = zombieStateMachine.ThreatManager.DetermineNextPotentialThreatState(potentialThreat); if (state == AiStateType.None) { state = GetDefaultStateType(); } else { AiTarget newThreat = (AiTarget)potentialThreat; if (newThreat.Type == AiTargetType.Visual_Player) { zombieStateMachine.ThreatManager.TrackTarget(newThreat); } else { if (newThreat.Type == AiTargetType.Audio || newThreat.Type == AiTargetType.Visual_Light) { zombieStateMachine.ThreatManager.TrackTarget(newThreat); ResetMaxDurationTimer(); } if (newThreat.Type == AiTargetType.Visual_Food) { if (zombieStateMachine.ThreatManager.DoesTargetExist()) { state = GetDefaultStateType(); // food is less of a priority, so reset back to alerted state (audio or light) } else { zombieStateMachine.ThreatManager.TrackTarget(newThreat); } } } } if (state == GetDefaultStateType()) { state = HandleDefaultState(); } return(state); }
/// <summary> /// Retrieves the AiState from the cache according to the provided type. /// </summary> /// <param name="type">The type of state to fetch.</param> /// <returns>The AiState if it was found or null.</returns> private AiState GetStateFromCache(AiStateType type) { AiState state; if (this.stateCache.TryGetValue(type, out state)) // wow, not a huge fan of trygetvalue with out parameter) { return(state); } Debug.LogWarningFormat( "State of type {0} not found in cache! Did you forget to drag it's AiState script to the AI Entity in the inspector?", type ); return(null); }
/// <summary> /// Handles threats that tend to produce the Alerted state, such as an Audio or Visual_light threat. /// </summary> /// <returns>A new state or NONE if nothing needed to be handled</returns> private AiStateType HandleAlertedStateThreats() { AiStateType state = AiStateType.None; if (zombieStateMachine.ThreatManager.DoesLightThreatExist()) { state = HandleVisualLightThreats(); } else if (zombieStateMachine.ThreatManager.DoesAudioThreatExist()) { state = HandleAudioThreat(); } return(state); }
/// <summary> /// Handles the patrol by continuing to pursue waypoints, etc. /// </summary> /// <returns>Either the patrol state or any new state if certain conditions were met.</returns> private AiStateType HandleDefaultState() { AiStateType state = GetDefaultStateType(); if (zombieStateMachine.NavAgent.pathPending) { // let the navmeshagent path complete before checking for state change, etc. zombieStateMachine.Speed = 0; return(state); } zombieStateMachine.Speed = this.speed; // Calculate angle we need to turn to be facing our target angleNeededForTurning = this.zombieStateMachine.ThreatManager.DetermineAngleNeededToTurnTowardsTarget(); // If its too big then drop out of Patrol and into Alerted if (Mathf.Abs(angleNeededForTurning) > this.turnOnSpotThreshold) { state = AiStateType.Alerted; } else { // If root rotation is not being used then we are responsible for keeping zombie rotated // and facing in the right direction. if (!this.zombieStateMachine.RootMotionProperties.ShouldUseRootRotation) { // Generate a new Quaternion representing the rotation we should have Quaternion newRotation = Quaternion.LookRotation(this.zombieStateMachine.NavAgent.desiredVelocity); // Smoothly rotate to that new rotation over time this.zombieStateMachine.transform.rotation = Quaternion.Slerp( this.zombieStateMachine.AiEntityBodyTransform.rotation, newRotation, Time.deltaTime * this.slerpSpeed ); } // If for any reason the nav agent has lost its path then send it to next waypoint if (zombieStateMachine.HasLostNavMeshPath()) { zombieStateMachine.WaypointManager.SetNextWayPoint(); zombieStateMachine.WaypointManager.TrackWayPoint(); } } return(state); }
/// <summary> /// Changes the current state to the new state, but only if it exists in the cache. /// </summary> /// <param name="newState">The new state to use.</param> /// <returns>true if the new state exists in the cache and the state was changed.</returns> private bool ChangeState(AiStateType newStateType) { bool didStateChange = false; AiState newState = GetStateFromCache(newStateType); if (newState != null) { if (currentState != null) { currentState.OnExitState(); // give the old state a chance to cleanup } newState.OnEnterState(); // give the new state a chance to initialize currentState = newState; this.currentStateType = newStateType; didStateChange = true; } return(didStateChange); }
/// <summary> /// Build the state cache based upon the AiStates that have been added to this GameObject this script is also a part of. /// There may not be any state scripts that have been added. /// </summary> private void BuildStateCache() { AiState[] states = GetComponents <AiState>(); if (states == null || states.Length == 0) { Debug.LogWarning("No Child AiState Script Components were added to the parent AI Entity GameObject!"); return; } foreach (AiState state in states) { AiStateType key = state.GetDefaultStateType(); if (state != null && !stateCache.ContainsKey(key)) { state.SetStateMachine(this); stateCache[key] = state; } } ChangeState(this.currentStateType); }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either Idle or a new state based upon the threats that were processed</returns> public override AiStateType OnUpdate() { IncrementTimer(); AiStateType state = GetDefaultStateType(); // if we got to this state, but its not hungry, go back to alert state if (!zombieStateMachine.IsHungery()) { zombieStateMachine.WaypointManager.TrackWayPoint(); state = AiStateType.Alerted; } else { AiThreatManager manager = zombieStateMachine.ThreatManager; if (manager.DoesPlayerThreatExist() || manager.DoesLightThreatExist()) { manager.TrackTarget(zombieStateMachine.ThreatManager.CurrentVisualThreat); state = AiStateType.Alerted; } else if (manager.DoesAudioThreatExist()) { manager.TrackTarget(zombieStateMachine.ThreatManager.CurrentAudioThreat); state = AiStateType.Alerted; } else if (manager.IsTargeting(AiTargetType.Visual_Food)) { if (IsZombieCurrentlyEating()) { ReplenishSatisfaction(); } } FaceTargetGradually(this.slerpSpeed); } return(state); }
/// <summary> /// Handles state change if it occurs. /// </summary> private void HandlePotentialStateChange() { if (this.currentState == null) { return; } // allow the current state to execute for a single frame // (i.e. notify it that monobehavior life-cycle method update() was called (one frame)) AiStateType potentialNewStateType = this.currentState.OnUpdate(); if (potentialNewStateType != this.currentStateType) { if (!ChangeState(potentialNewStateType)) { // if here, it most likely means that state's script wasn't added to the AI Entity if (!ChangeState(AiStateType.Idle)) { // if here, it most likely means the Idle state script wasn't added to the AI Entity, so just set the type this.currentStateType = AiStateType.Idle; // TODO: maybe make an AiState that has a NONE type so this isn't standalone } } } }
/// <summary> /// Called by the state machine each frame. /// </summary> /// <returns>Either the current state or a new state.</returns> public override AiStateType OnUpdate() { UpdateTimers(); if (HasReachedMaxTime()) { return(AiStateType.Patrol); } // TODO: change method so it doesn't have so many returns (returning at top is ok) - maybe use if/else // IF we are chasing the player and have entered the melee trigger then attack if (zombieStateMachine.ThreatManager.IsTargeting(AiTargetType.Visual_Player) && zombieStateMachine.IsInMeleeRange) { return(AiStateType.Attack); } // Otherwise this is navigation to areas of interest so use the standard target threshold if (zombieStateMachine.IsTargetReached) { switch (zombieStateMachine.ThreatManager.CurrentTarget.Type) { // If we have reached the source // example, flashlight was shown behind, and player ran away, zombie arrived there, so it goes into alerted case AiTargetType.Audio: case AiTargetType.Visual_Light: zombieStateMachine.ThreatManager.StopTrackingTarget(); return(AiStateType.Alerted); // Become alert and scan for targets case AiTargetType.Visual_Food: return(AiStateType.Feeding); } } // If for any reason the nav agent has lost its path then call then drop into alerted state // so it will try to re-aquire the target or eventually giveup and resume patrolling if (zombieStateMachine.HasLostNavMeshPath()) { return(AiStateType.Alerted); } if (zombieStateMachine.NavAgent.pathPending) { zombieStateMachine.Speed = 0; } else { zombieStateMachine.Speed = this.speed; // zombie is very close, so make sure it is still facing target; or it reached it and we need to change state if (HandleZombieIsCloseOrAtTarget()) { return(AiStateType.Alerted); } } // player is probably still current target and so we need to repath continually to ensure we are still pursing it if (HandlePlayerIsVisualThreat()) { return(AiStateType.Pursuit); } // If our target is the last sighting of a player then remain in pursuit as nothing else can override if (zombieStateMachine.ThreatManager.IsTargeting(AiTargetType.Visual_Player)) { return(AiStateType.Pursuit); } // If here, current player is not the threat, but some other visual threat exists (e.g. light, sound) AiStateType type = HandleAlertedStateThreats(); if (type != AiStateType.None) { return(type); } return(GetDefaultStateType()); }
/// <summary> /// Handles the alerted state by continuing to pursue waypoints, etc. /// </summary> /// <returns>Either the alerted state or any new state if certain conditions were met.</returns> private AiStateType HandleDefaultState() { AiStateType state = GetDefaultStateType(); if ( !zombieStateMachine.IsTargetReached && (zombieStateMachine.ThreatManager.DoesAudioThreatExist() || zombieStateMachine.ThreatManager.DoesLightThreatExist()) ) { // we got close to the target due to light or sound, so now we need to know if we should pursue it or seek to find it float angle = CalculationUtil.FindSignedAngle( zombieStateMachine.AiEntityBodyTransform.forward, zombieStateMachine.ThreatManager.CurrentTarget.Position - zombieStateMachine.AiEntityBodyTransform.position ); if (zombieStateMachine.ThreatManager.DoesAudioThreatExist() && Mathf.Abs(angle) < this.threatAngleThreshold) { // it's a sound and we are capable of heading to it, so pursue it state = AiStateType.Pursuit; } else if (HasReachedMaxDirectionChangeTime()) { // it's not a sound and we are not capable of turning towards it, so determine which way we should turn if (Random.value < zombieStateMachine.Intelligence) { SeekTowards(angle); // smartly turn } else { SeekRandomly(); // randomly turn because we are a stupid zombie :) } ResetDirectionChangeTimer(); // TODO: this does sometimes happen often and it makes the zombie appear as if it is stuck in the alerted state } } else if ( zombieStateMachine.ThreatManager.IsTargeting(AiTargetType.Waypoint) && !zombieStateMachine.NavAgent.pathPending ) { // we were targeting a waypoint and we arrived at it, so determine if we can head to next one or turn towards it float angle = zombieStateMachine.ThreatManager.DetermineAngleNeededToTurnTowardsTarget(); if (Mathf.Abs(angle) < this.turnOnSpotThreshold) { state = AiStateType.Patrol; } else if (HasReachedMaxDirectionChangeTime()) { SeekTowards(angle); ResetDirectionChangeTimer(); } } else if (HasReachedMaxDirectionChangeTime()) { // we didn't find what we were looking for and our clock ran out, so turn randomly and repeat the entire alert process // TODO: this does sometimes happen often and it makes the zombie appear as if it is stuck in the alerted state SeekRandomly(); ResetDirectionChangeTimer(); } return(state); }