Yup. Here’s the entire script, if needed:
using System.Collections.Generic;
using System.Linq;
using RPG.States.Enemies;
using RPG.States.Player;
using UnityEngine;
// This function, unlike other attacking states, is simply here to allow the boat to be capable of
// Ramming other boats
public class BoatAttackingState : BoatBaseState
{
AllyBoatPosition[] allyBoatPositions;
// The list of boat allies that are alive
// (if none are left, this boat has no permission to be in any movement-based state.
// Will be applied in 'BoatChasingState.cs', 'BoatAttackingState.cs' and 'BoatBackingUpState.cs')
List<EnemyStateMachine> aliveBoatAllies = new List<EnemyStateMachine>();
public BoatAttackingState(BoatStateMachine stateMachine) : base(stateMachine)
{
// Cache the ally boat positions
allyBoatPositions = stateMachine.GetComponentsInChildren<AllyBoatPosition>();
}
public override void Enter()
{
Debug.Log($"{stateMachine.gameObject.name} has entered the boat attacking state");
stateMachine.OnVictimBoatTilted += HandleBoatCollision;
}
public override void Tick(float deltaTime)
{
// If the last attacker is not driving a boat,
// (or on one, which is automatically checked for
// after checking for whether the last attacker is driving a boat)
// get back to mounted state (which will automatically
// take you to chasing state)
if (!IsLastAttackerDrivingBoat())
{
stateMachine.SwitchState(new BoatMountedState(stateMachine));
return;
}
// -------------------------------------------------------------------- IF THE BOAT HAS NO ALLIES ON HIM, SWITCH TO THE MOUNTED STATE, SINCE IT CAN NO LONGER BE DRIVEN -------------------------------------------------------------------------
// If the boat has no driver, then it shouldn't move (because nobody is driving it)
aliveBoatAllies = stateMachine.GetComponentsInChildren<EnemyStateMachine>().ToList();
bool allAlliesDead = aliveBoatAllies.All(ally => ally.Health.IsDead());
if (allAlliesDead)
{
Debug.Log($"No Boat Allies are left alive, switching to boat mounted state");
stateMachine.ClearBoatPatrolPath(); // Clear the patrol path first, so it doesn't accidentally go to patrolling state (it's an empty boat)
stateMachine.SwitchState(new BoatMountedState(stateMachine));
return;
}
// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// Assuming the last attacker is on a boat,
// go for the RAMMING attempt
RamLastAttackerBoat(deltaTime);
// -------------------------------------------------------------------- BOAT ALLIES -----------------------------------------------------------------------------------------------------------
// The boat allies need to be in the exact same position as their spots (i.e: Get the boat NPC allies to move along with the boat)
if (allyBoatPositions != null)
{
foreach (AllyBoatPosition allyBoatPosition in allyBoatPositions)
{
if (allyBoatPosition.GetComponentInChildren<EnemyStateMachine>() != null && // rotation is only for boat allies, which are children of the boat
allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().GetLastAttacker() == null)
{
allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().transform.position = allyBoatPosition.transform.position;
allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().transform.rotation = allyBoatPosition.transform.rotation;
}
}
}
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
}
public override void Exit()
{
// Not much to do when the State Machine exits here
stateMachine.OnVictimBoatTilted -= HandleBoatCollision;
}
/// <summary>
/// Is the Last Attacker driving a boat? (Checks for
/// the parent of the Last Attacker, whether it has a 'BoatStateMachine.cs' or not)
/// </summary>
/// <returns></returns>
private bool IsLastAttackerDrivingBoat()
{
var lastAttacker = stateMachine.GetBoatLastAttacker();
if (lastAttacker != null && lastAttacker.GetComponentInParent<BoatStateMachine>() != null)
{
return true;
}
return IsLastAttackerOnBoat(); // If he wasn't driving a boat, at least is he on a boat?
}
/// <summary>
/// Is the Last Attacker on a boat? (Does an overlap sphere check to determine if the
/// Last Attacker has a boat he's on, so we can RAM it, or not)
/// </summary>
/// <returns></returns>
private bool IsLastAttackerOnBoat()
{
// This is untested, as the bug is quite rare. If it happens,
// though, we'll get back to investigate this block
if (stateMachine.GetBoatLastAttacker() == null)
{
Debug.Log($"The boat's Last Attacker is dead, so the last attacker is not on a boat");
return false; // Kills a NullReferenceException if the Boat's Last Attacker is null
}
// Is there a boat anywhere near the last attacker?
float detectionRadius = 5.0f; // Reduce this from 5 to 1, if needed
Vector3 lastAttackerPosition = stateMachine.GetBoatLastAttacker().transform.position;
Collider[] hitColliders = Physics.OverlapSphere(lastAttackerPosition, detectionRadius);
foreach (Collider collider in hitColliders)
{
if (collider.GetComponentInParent<BoatStateMachine>() != null)
{
return true;
}
}
return false;
}
private void RamLastAttackerBoat(float deltaTime)
{
var targetBoat = stateMachine.GetBoatLastAttacker().GetComponentInParent<BoatStateMachine>();
if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true; // If you turn this off, the code below won't work
if (targetBoat != null)
{
var playerOnBoat = targetBoat.GetComponentInChildren<PlayerStateMachine>();
if (playerOnBoat != null)
{
stateMachine.Agent.SetDestination(targetBoat.transform.position); // If you turn this off, the boat won't even try to reach you
Vector3 direction = stateMachine.Agent.desiredVelocity; // The direction the NavMeshAgent prefers to use to reach its goal
direction.y = 0; // Eliminate the y-axis (no flying stuff for boats, PLEASE!)
Move(direction.normalized * stateMachine.MovementSpeed, deltaTime); // If you turn this off, the boat will hug you (don't want that)
stateMachine.Agent.velocity = direction * stateMachine.MovementSpeed; // The velocity of the boat each frame
stateMachine.Agent.nextPosition = stateMachine.transform.position; // The next position for the NavMeshAgent to go towards (if it's not itself, it'll teleport. Don't want that!)
Debug.Log($"{stateMachine.gameObject.name} is moving towards {stateMachine.GetBoatLastAttacker().gameObject.name}, BoatAttackingState");
}
else
{
stateMachine.Agent.ResetPath(); // Don't try to move, because the player is not on the opponent boat (i.e: his allies are fighting you, so you should stop moving too)
Debug.Log($"{stateMachine.GetBoatLastAttacker().gameObject.name} is off the boat, resetting NavMeshAgent path, BoatAttackingState");
}
}
else
{
stateMachine.Agent.ResetPath(); // Don't try to move, because the player is not on the opponent boat (i.e: his allies are fighting you, so you should stop moving too)
Debug.Log($"TargetBoat not found. Resetting NavMeshAgent path, BoatAttackingState");
}
}
private void HandleBoatCollision(Collider thisCollider)
{
Debug.Log($"{thisCollider.gameObject.name} is heading to Boat Backing Up State");
thisCollider.GetComponent<BoatStateMachine>().SwitchState(new BoatBackingUpState(stateMachine));
}
}
We’re 66 days late for that. Been working on it since September 1st, literally. I’m like 93% done with the basic system. I might as well just get the job done now, considering that it (mostly) works quite well, for now at least. I just need a better escape algorithm, because mine is a little messy (I wrote a new one, and for now it seems to be doing the job fine… Probably a little computationally expensive though), fix a few bugs and hopefully it’ll be done
I’ll say this though, the issue was significantly reduced by just reducing the radius and height of the NavMeshAgent itself in the hierarchy. If the AI boat gets too close, and we dismount and don’t mount again quickly, the problem arises again, but if we are quickly mounting and dismounting with a much smaller Agent, then the AI boat will perform my Ramming operation just fine, and this problem will be significantly less visible
BUT… YOU NEED TO TRY AND ERROR A LOT BEFORE YOU GET THE VALUES. TOO BIG, OR TOO SMALL, WILL BOTH RESULT IN THE ERROR SHOWING UP
For example, this is what worked for my test boat (circled in red):
And earlier today, I wrote an interface to get the boat to accurately swap its chase range if the player is the last attacker, depending on whether he’s driving a boat or not. That one took me months to fix… (and I am sure it’ll need some fixing again when the game expands down the line)
(For context, ‘OnVictimBoatTilted’ is an event in ‘BoatStateMachine.cs’. It’s part of 3 events which are called when the boat gets hit to make it tilt. “HandleBoatCollision”, the subscribed event, ensures the AI boat backs off and then RAMs the victim again, so that the player has a fair battle. It’s a little complex to explain here, but it works perfectly fine. Just know the ships can bump you, back off for a few seconds and then bump you again)