The enemy looks good and is doing what it should, but the knockback on the player is a little weird. It gets pushed back and then after it stops the animation gets played. Also if the Player is locked in on the target then it doesnt play at all. I feel like I missed something here.
Ok I fixed part of it by just choosing a different animation. Now the animation plays as my player gets knocked back, but only a few times and then it stops working.
Neither of these things seem quite right…
Paste in your AttackAction.cs and ForceReceiver.cs scripts and we’ll take a look.
public class ForceReceiver : MonoBehaviour
{
[SerializeField] private NavMeshAgent agent;
[SerializeField] private CharacterController controller;
[SerializeField] private float drag = 0.3f;
private Vector3 dampingVelocity;
private Vector3 impact;
private float verticalVelocity;
public Vector3 Movement => impact + Vector3.up * verticalVelocity;
private void Update()
{
if (verticalVelocity < 0f && controller.isGrounded)
{
verticalVelocity = Physics.gravity.y * Time.deltaTime;
}
else
{
verticalVelocity += Physics.gravity.y * Time.deltaTime;
}
impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);
if (agent != null)
{
if (impact.sqrMagnitude < .2f*.2f)
{
impact = Vector3.zero;
agent.enabled = true;
}
}
}
public void AddForce(Vector3 force)
{
impact += force;
if(agent != null)
{
agent.enabled = false;
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class Attack
{
[field: SerializeField] public string AnimationName { get; private set; }
[field: SerializeField] public int Damage { get; private set; }
[field: SerializeField] public float TransitionDuration { get; private set; }
[field: SerializeField] public int ComboStateIndex { get; private set; } = -1;
[field: SerializeField] public float ComboAttackTime { get; private set; }
[field: SerializeField] public float ForceTime { get; private set; }
[field: SerializeField] public float Force { get; private set; }
[field: SerializeField] public float Knockback { get; private set; }
}
public class PlayerAttackingState : PlayerBaseState
{
private Attack attack;
private float previousFrameTime;
private bool alreadyAppliedForce;
public PlayerAttackingState(PlayerStateMachine stateMachine, int attackIndex ) : base(stateMachine)
{
attack = stateMachine.Attacks[attackIndex];
}
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName,attack.TransitionDuration);
stateMachine.Weapon.SetAttack(attack.Damage, attack.Knockback);
}
public override void Tick(float deltaTime)
{
Move(deltaTime);
FaceTarget();
float normalizedTime = GetNormalizedTime(stateMachine.Animator);
if(normalizedTime >= previousFrameTime && normalizedTime <1f)
{
if(normalizedTime >= attack.ForceTime)
{
TryApplyForce();
}
if(stateMachine.InputReader.IsAttacking)
{
TryComboAttack(normalizedTime);
}
}
else
{
if(stateMachine.Targeter.currentTarget != null)
{
stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
}
else
{
stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
}
}
previousFrameTime = normalizedTime;
}
public override void Exit()
{
}
private void TryComboAttack(float normalizedTime)
{
if(attack.ComboStateIndex == -1) { return; }
if(normalizedTime < attack.ComboAttackTime) { return; }
stateMachine.SwitchState
(
new PlayerAttackingState(stateMachine, attack.ComboStateIndex)
);
}
void TryApplyForce()
{
if (alreadyAppliedForce) { return; }
stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward * attack.Force);
alreadyAppliedForce = true;
}
}
Is your GetNormalizedTime() in the base state? I’ll need to see this method as well, sorry.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class PlayerBaseState : State
{
protected PlayerStateMachine stateMachine;
public PlayerBaseState(PlayerStateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
protected PlayerBaseState()
{
}
protected void Move( float deltaTime)
{
Move(Vector3.zero, deltaTime);
}
protected void Move(Vector3 motion, float deltaTime)
{
stateMachine.Controller.Move((motion + stateMachine.ForceReceiver.Movement) * deltaTime);
}
protected void FaceTarget()
{
if(stateMachine.Targeter.currentTarget == null) { return; }
Vector3 lookPos = stateMachine.Targeter.currentTarget.transform.position - stateMachine.transform.position;
lookPos.y = 0f;
stateMachine.transform.rotation = Quaternion.LookRotation(lookPos);
}
protected void ReturnToLocomotion()
{
if(stateMachine.Targeter.currentTarget != null)
{
stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
}
else
{
stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class EnemyBaseState : State
{
protected EnemyStateMachine stateMachine;
public EnemyBaseState(EnemyStateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
protected void Move(float deltaTime)
{
Move(Vector3.zero, deltaTime);
}
protected void Move(Vector3 motion, float deltaTime)
{
stateMachine.Controller.Move((motion + stateMachine.ForceReceiver.Movement) * deltaTime);
}
protected void FacePlayer()
{
if (stateMachine.Player == null) { return; }
Vector3 lookPos = stateMachine.Player.transform.position - stateMachine.transform.position;
lookPos.y = 0f;
stateMachine.transform.rotation = Quaternion.LookRotation(lookPos);
}
protected bool IsInChaseRange()
{
float playerDistanceSquare = (stateMachine.Player.transform.position - stateMachine.transform.position).sqrMagnitude;
return playerDistanceSquare <= stateMachine.PlayerChasingRange * stateMachine.PlayerChasingRange;
}
}
its so weird because it works five or six times and then it goes back just knocking my player back with no animation. The enemy also gets sort of bad at switching to the attacking state after a while and starts running in place into my player from time to time.
I’m still not seeing the GetNormalizedTime method, which is where I suspect the issue may be…
It might be easier to get a direct look. Zip up your project and upload it to https://gdev.tv/projectupload. Be sure to remove the Library folder to conserve space.
Here is GetNormalizedTime() in the State script. Is that in the wrong place?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class State
{
public abstract void Enter();
public abstract void Tick(float deltaTime);
public abstract void Exit();
protected float GetNormalizedTime(Animator animator)
{
AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);
if (animator.IsInTransition(0) && nextInfo.IsTag("Attack"))
{
return nextInfo.normalizedTime;
}
else if (!animator.IsInTransition(0) && currentInfo.IsTag("Attack"))
{
return currentInfo.normalizedTime;
}
else
{
return 0f;
}
}
}
protected float GetNormalizedTime(Animator animator)
{
AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);
if (animator.IsInTransition(0) && nextInfo.IsTag("Attack"))
{
return nextInfo.normalizedTime;
}
else if (!animator.IsInTransition(0) && currentInfo.IsTag("Attack"))
{
return currentInfo.normalizedTime;
}
else
{
return 0f;
}
}
Actually, State is a good place for this, as it’s used in both Enemy and Player states…
I’m still not seeing anything that would cause the behavior you’re describing… I would go ahead and upload the project as described in my previous reply. I’ll be able to see the problem in action and hopefully locate the cause more efficiently.