using RPG.Control;
using UnityEngine;
namespace RPG.States.Enemies
{
public class EnemyPatrolState : EnemyBaseState
{
private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";
public EnemyPatrolState(EnemyStateMachine stateMachine) : base(stateMachine) {}
private float movementSpeed = 0.5f;
private float acceptanceRadius = 2f;
private float dwellTime = 2f;
private Vector3 targetPatrolPoint;
public override void Enter()
{
if (stateMachine.PatrolPath == null)
{
stateMachine.SwitchState(new EnemyIdleState(stateMachine));
return;
}
int index;
// Check blackboard for key, set index if key is set
if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
{
index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
}
else
{
// The first time we enter a Patrol state, the index will not be set in the blackboard
// So we get it from the PatrolPath's GetNearestIndex
// We will also be resetting the index if the enemy goes into 'EnemyChasingState.cs'
index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
}
// Set our goal
targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);
if (patrolPoint) // If the current index has a PatrolPoint attached, then we can adjust the settings from the default
{
movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
acceptanceRadius = patrolPoint.AcceptanceRadius;
dwellTime = patrolPoint.DwellTime;
}
else // if not, calculate the movementSpeed as a percentage of the stateMachine's movement speed
{
movementSpeed *= stateMachine.MovementSpeed;
}
// Squaring the acceptanceRadius (to save calculation time when we use 'SqrMagnitude')
acceptanceRadius *= acceptanceRadius;
// next waypoint index setup
stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
// Since the waypoint won't move, set the destination here on the Agent
stateMachine.Agent.SetDestination(targetPatrolPoint);
// Set the animation
stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
}
public override void Tick(float deltaTime)
{
if (IsInChaseRange() && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line
{
// if you want the vision system to work, uncomment the if statement below:
// if (CanSeePlayer()) {
// Clearing key to ensure that at the end of the battle, the enemy finds the nearest waypoint
stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
stateMachine.SwitchState(new EnemyChasingState(stateMachine));
return;
// }
}
if (IsInAcceptanceRange())
{
// Once we're close enough to the waypoint, we head to a Dwell state
stateMachine.SwitchState(new EnemyDwellState(stateMachine, dwellTime));
return;
}
// Chasing Code
Vector3 lastPosition = stateMachine.transform.position;
MoveToWayPoint(deltaTime);
Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
float deltaMagnitude = deltaMovement.magnitude;
float grossSpeed = deltaMagnitude / deltaTime;
stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
if (deltaMagnitude > 0)
{
FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
}
else
{
FaceTarget(targetPatrolPoint, deltaTime);
}
}
private bool IsInAcceptanceRange()
{
return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
}
public override void Exit()
{
stateMachine.Agent.ResetPath();
stateMachine.Agent.velocity = Vector3.zero;
}
void MoveToWayPoint(float deltaTime)
{
Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;
Move(direction * movementSpeed, deltaTime);
stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
stateMachine.Agent.nextPosition = stateMachine.transform.position;
}
}
}
and before I forget, one last question regarding the respawning. What does deactivating and reactivating the character controller have to do with helping the chances of properly respawning?
the same errors are still showing up⦠(again though, I want to stress that they donāt hurt the game in any way shape or form, apart from the fact that the enemy attacks are still at a rapid pace and need breaks between them)
While the CharacterController is active, it rejects changes to the transform outright, just like the NavMeshAgent does. In fact, thatās one of the reasons that the NavMeshAgent and the CharacterController donāt get along well and we have to manually call CharacterController.Move based on the Agentās desiredVelocity.
OK now I see why that bug was there⦠at first I thought I planted something somewhere and forgot to take it out, and when I deleted all sorts of movement when respawning and got my player to be where he was, I was even more shockedā¦
basically the rejection was because of the controller behind the scenes, got it
Another question on the fly, would deleting the mesh colliders on my weapons or shields hurt my attacks in any way? I deleted them last night because they were messing with Malbersā animals (collider collisions), as in the colliders had the animals move in weird ways. They donāt seem to be doing so badly as we speak, but I still have to ask
any other ideas for this? Thatās my script right now btw:
using RPG.Control;
using UnityEngine;
namespace RPG.States.Enemies
{
public class EnemyPatrolState : EnemyBaseState
{
private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";
public EnemyPatrolState(EnemyStateMachine stateMachine) : base(stateMachine) {}
private float movementSpeed = 0.5f;
private float acceptanceRadius = 2f;
private float dwellTime = 2f;
private Vector3 targetPatrolPoint;
public override void Enter()
{
if (stateMachine.PatrolPath == null)
{
stateMachine.SwitchState(new EnemyIdleState(stateMachine));
return;
}
int index;
// Check blackboard for key, set index if key is set
if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
{
index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
}
else
{
// The first time we enter a Patrol state, the index will not be set in the blackboard
// So we get it from the PatrolPath's GetNearestIndex
// We will also be resetting the index if the enemy goes into 'EnemyChasingState.cs'
index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
}
// Set our goal
targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);
if (patrolPoint) // If the current index has a PatrolPoint attached, then we can adjust the settings from the default
{
movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
acceptanceRadius = patrolPoint.AcceptanceRadius;
dwellTime = patrolPoint.DwellTime;
}
else // if not, calculate the movementSpeed as a percentage of the stateMachine's movement speed
{
movementSpeed *= stateMachine.MovementSpeed;
}
// Squaring the acceptanceRadius (to save calculation time when we use 'SqrMagnitude')
acceptanceRadius *= acceptanceRadius;
// next waypoint index setup
stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
// Enabling the NavMeshAgent
stateMachine.enabled = true;
// Since the waypoint won't move, set the destination here on the Agent
stateMachine.Agent.SetDestination(targetPatrolPoint);
// Set the animation
stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
}
public override void Tick(float deltaTime)
{
if (IsInChaseRange() && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line
{
// if you want the vision system to work, uncomment the if statement below:
// if (CanSeePlayer()) {
// Clearing key to ensure that at the end of the battle, the enemy finds the nearest waypoint
stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
stateMachine.SwitchState(new EnemyChasingState(stateMachine));
return;
// }
}
if (IsInAcceptanceRange())
{
// Once we're close enough to the waypoint, we head to a Dwell state
stateMachine.SwitchState(new EnemyDwellState(stateMachine, dwellTime));
return;
}
// Chasing Code
Vector3 lastPosition = stateMachine.transform.position;
MoveToWayPoint(deltaTime);
Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
float deltaMagnitude = deltaMovement.magnitude;
float grossSpeed = deltaMagnitude / deltaTime;
stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
if (deltaMagnitude > 0)
{
FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
}
else
{
FaceTarget(targetPatrolPoint, deltaTime);
}
}
private bool IsInAcceptanceRange()
{
return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
}
public override void Exit()
{
if (stateMachine.Agent.enabled) {
stateMachine.Agent.ResetPath();
stateMachine.Agent.velocity = Vector3.zero;
}
}
void MoveToWayPoint(float deltaTime)
{
Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;
Move(direction * movementSpeed, deltaTime);
stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
stateMachine.Agent.nextPosition = stateMachine.transform.position;
}
}
}
speaking of this bug, I think Iāll be the black sheep here and eliminate the āAttackā value instead. I want my attacks to be reliant on the weapon rather than on the animation - because of a few reasons:
My animations are more limited than my weapons
Upgrading weapons will make a whole lot more sense this way
I wonāt delete it, but I will keep it as zero wherever I donāt want to apply it
So for this setup, the weapon damage is included in the GetStat(Stat.Damage) because the GetAdditiveModifiers returns the weapon damage.
Then that damage is multiplied by the attackās modifier⦠so that some attacks can do more or less damage, a modifier of 1 is the same as the attack, .5f is 50% of the damage, 2f is double damage, etc.
You shouldnāt be hitting twice, only one TakeDamage should be called.
0 * 1 =ā¦
So that leaves the question of why youāre hitting twice⦠Do you have multiple TryHit animation events on the animation? Is there still a Hit animation event in your animation? Does your TryHit have multiple damage calls?
nope, weāre hitting twice because thereās a āWeaponDamageā and an āAttackForceā both exerting force⦠I like the weapon damage because of the easier name and that itās directly accessible from when we are creating the weapon, but attack force is there as well, and I couldnāt be more baffled down the line
Anyway, Iām still trying to figure out the sudden character generator bug before we get back into adding new stuff
did I miss out on something with the Impact statesā¦? When both me and my enemy try to return to idle state after we received a hit, we both literally freeze pretty much forever⦠As if the āHandleForceCompleted()ā function never unsubscribed to begin with⦠(and yes, I made sure it was in both āEnterā and āExitā states of both the Impact States)
In āEnemyImpactState.csā (SPOILER), this script:
using UnityEngine;
namespace RPG.States.Enemies {
public class EnemyImpactState : EnemyBaseState
{
private readonly int ImpactHash = Animator.StringToHash("Impact");
public EnemyImpactState(EnemyStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(ImpactHash, stateMachine.CrossFadeDuration);
stateMachine.ForceReceiver.OnForceCompleted += HandleOnForceCompleted;
}
public override void Tick(float deltaTime)
{
Move(deltaTime);
}
public override void Exit()
{
stateMachine.ForceReceiver.OnForceCompleted -= HandleOnForceCompleted;
}
private void HandleOnForceCompleted()
{
stateMachine.SwitchState(new EnemyIdleState(stateMachine));
}
}
}
and my āPlayerImpactState.csā (ANOTHER SPOILER):
using UnityEngine;
namespace RPG.States.Player {
public class PlayerImpactState : PlayerBaseState
{
private readonly int ImpactHash = Animator.StringToHash("Impact");
public PlayerImpactState(PlayerStateMachine stateMachine) : base(stateMachine) {}
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(ImpactHash, stateMachine.CrossFadeDuration);
stateMachine.ForceReceiver.OnForceCompleted += HandleOnForceCompleted;
}
public override void Tick(float deltaTime)
{
Move(deltaTime);
}
public override void Exit()
{
stateMachine.ForceReceiver.OnForceCompleted -= HandleOnForceCompleted;
}
private void HandleOnForceCompleted()
{
SetLocomotionState();
}
}
}
Placing debugs around seems to work, but⦠they both still freeze. I went through the tutorial twice so far, I still canāt find where I went wrong (apart from placing āHandleForceAppliedā in the StateMachine script (because the āStart()ā function needs that), instead of the āImpactStateā scriptsā¦
and I also have to mention that if we donāt give the āOnForceAppliedā event in āForceReceiver.csā a type of Vector3, we will get errors, so this is how itās written (since the force type using it in āAddForceā is of type Vector3):
public event System.Action <Vector3> OnForceApplied;
without doing that, āInvokeā wonāt take in the force Input (the stuff bixarrio and you teach me⦠)