Keep Rotation while attacking in FreeLookState

I suggest we keep the rotation while attacking in the FreeLookState - this gives the player more control as to what he is trying to attack. Since we are adding a forward force to each attack, it’s very easy to “miss” the enemy if you cant rotate mid-attack. Think of God of War chains attacks. Those have a large radius and require the player to target its direction. This is not an issue in the target lock state as we will always move towards the enemy. Here is a video of how this looks and how I implemented it

I just copied the rotation and movement functions from FreeLookState and I’m using them in the AttackingState as well. We also check to see if we are targeting or if our movement is zero, we ignore the rotation function.

This is my PlayerAttackingState.cs

(this also incorporated my other suggestion for here UPDATED - Target State - Press & Release same Button)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerAttackingState : PlayerBaseState
{
    private float _previousFrameTime;
    private Attack _attack;
    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);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);
        FaceTarget();
        
        float normalizedTime = GetNormalizedAtime();
        Vector3 movement = CalculateMovement();

        if (normalizedTime >= _previousFrameTime && normalizedTime < 1f)
        {
            if (normalizedTime >= _attack.ForceTime)
            {
                TryApplyForce();
            }
            if (stateMachine.InputReader.IsAttacking)
            {
                TryComboAttack(normalizedTime);
                //We check to see if we have a target or are in the targeting state - if we are , we ignore the rotation
                if (stateMachine.Targeter.CurrentTarget != null && stateMachine.InputReader.IsTargeting){return;}
                //We check to see if we are standing still and our input is zero - if we are , we ignore the rotation
                if (movement == Vector3.zero) {return;}
                //we trigger the rotation based on player input
                FaceDirection(movement, Time.deltaTime);
            }

        }
        else
        {
            //we check to see if we have a target and we are pressing the targeting button 
            if (stateMachine.Targeter.CurrentTarget != null && stateMachine.InputReader.IsTargeting)
            {
                stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
            }
            else 
            {
                //we check to see if we pressed the targeting button while we are attacking
                if (stateMachine.InputReader.IsTargeting)
                {
                    //if we pressed the targeting button while we were attacking, we select a target and enter targeting state
                    stateMachine.Targeter.SelectTarget();
                    stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
                }
                else
                {
                    //if we release the targeting button while we still have a CurrentTarget we remove it before going back to free look
                    stateMachine.Targeter.Cancel();
                    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));
    }
    
    private void TryApplyForce()
    {
        if (_alreadyAppliedForce) {return;}
        stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward*_attack.Force);
        _alreadyAppliedForce = true;
    }


    private float GetNormalizedAtime()
    {
        AnimatorStateInfo currentAnimatorStateInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);
        AnimatorStateInfo nextAnimatorStateInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

        if (stateMachine.Animator.IsInTransition(0) && nextAnimatorStateInfo.IsTag("Attack"))
        {
            return nextAnimatorStateInfo.normalizedTime;
        }

        if (!stateMachine.Animator.IsInTransition(0) && currentAnimatorStateInfo.IsTag("Attack"))
        {
            return currentAnimatorStateInfo.normalizedTime;
        }
        return 0f;
    }
    private Vector3 CalculateMovement()
    {
        Vector3 forward = stateMachine.MainCameraTransform.forward;
        Vector3 right = stateMachine.MainCameraTransform.right;
        forward.y = 0f;
        right.y = 0f;
        forward.Normalize();
        right.Normalize();
 
        return forward*stateMachine.InputReader.MovementValue.y + right*stateMachine.InputReader.MovementValue.x;
    }
    private void FaceDirection(Vector3 movement,float deltaTime)
    {
        stateMachine.transform.rotation = Quaternion.Lerp(stateMachine.transform.rotation,
            Quaternion.LookRotation(movement),
            deltaTime*stateMachine.RotationDamping);
    }



}
5 Likes

Very detailed and informative post. Thank you for sharing

1 Like

Nice! Yeah it makes so much sense to be able to rotate the camera in the AttackState.

I really want to rework Attacking to not be a hold down button. Make the player time the attacks properly for a combo. I got nearly there but it just wasn’t cooperating.

Just incorporated this into my build. Here’s the script I had a lot of help writing. Hope it’s what you’re looking for.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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.InputReader.AttackDown += HandleAttackDown; //Enables Button Push
        
        stateMachine.Weapon.SetAttack(attack.Damage, attack.Knockback);
        stateMachine.Animator.CrossFadeInFixedTime(attack.AnimationName, attack.TransitionDuration);
        Debug.Log(attack.AnimationName);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);
        FaceTarget();

        float normalizedTime = GetNormalizedTime(stateMachine.Animator, "Attack");
        Vector3 movement = CalculateMovement();
            
        if(normalizedTime < 1f)
        {
            if (normalizedTime >= attack.ForceTime)
            {
                TryApplyForce();
                
                if(stateMachine.Targeter.CurrentTarget != null) {return;} //checks if we are targeting something.
                if(movement == Vector3.zero) {return;}
                FaceDirection(movement, Time.deltaTime);
            }
            
            /*if(stateMachine.InputReader.IsAttacking)
            {
                TryComboAttack(normalizedTime);
            } Enables Button Hold*/
        }
        else
        {
            if(stateMachine.Targeter.CurrentTarget != null)
            {
                stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
            }
            else
            {
                stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            }
        }
        
        previousFrameTime = normalizedTime;
    }
    public override void Exit()
    {
        stateMachine.InputReader.AttackDown -= HandleAttackDown; //Enables Button Push
    }
    private void TryComboAttack(float normalizedTime)
    {
        if(attack.ComboStateIndex == -1) { return; }

        if(normalizedTime < attack.ComboAttackTime) { return; }

        stateMachine.SwitchState
        (
            new PlayerAttackingState
            (
                stateMachine,
                attack.ComboStateIndex
            )
        );
    }
    private void TryApplyForce()
    {
        if(alreadyAppliedForce) { return; }
        stateMachine.ForceReceiver.AddForce(stateMachine.transform.forward * attack.Force);
        alreadyAppliedForce = true;
    }

    void HandleAttackDown()
    {
        // stateMachine.InputReader.AttackDown -= HandleAttackDown; //Makes it harder to time combos.
        // with this enabled, you CANNOT button mash and expect to combo.
        
        float normalizedTime = GetNormalizedTime(stateMachine.Animator, "Attack");
        TryComboAttack(normalizedTime);
    }//Enables Button Push

    private Vector3 CalculateMovement() //allows rotation during attacks
    {
        Vector3 forward = stateMachine.MainCameraTransform.forward;
        Vector3 right = stateMachine.MainCameraTransform.right;
        forward.y = 0f;
        right.y = 0f;
        forward.Normalize();
        right.Normalize();
 
        return forward*stateMachine.InputReader.MovementValue.y + right*stateMachine.InputReader.MovementValue.x;
    }

    private void FaceDirection(Vector3 movement,float deltaTime) //also allows rotation during attacks
    {
        stateMachine.transform.rotation = Quaternion.Lerp(stateMachine.transform.rotation,
            Quaternion.LookRotation(movement),
            deltaTime*stateMachine.RotationDamping);
    }

}

Hey there, I’m keen to implement this too, and have been following your posts with this one.

In your code, where is the GetNormalizedTime method? I tried copying and pasting your code and it came up with an error for this method.

Cheers

I double check the code I posted and it Seems I made a separate function called “GetNormalizedAtime” (with an A-for attack" If you scroll down the code you can see it there.

If you want to use GetNormalizedTime I think you need to pass down the Animation arguments

float normalizedTime = GetNormalizedTime(stateMachine.Animator, "Attack");

Privacy & Terms