Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Making mechanics of Cult of the Lamb: Attacking Part 1

This is the fourth tutorial of the making of the Cult of the Lamb mechanics in Unity. If you want to follow along the series you need to first read Ground Zero, Part 1, Part 2 and Part 3.

First of all, we need to fix things regarding the animations. I made a mistake and we need to move the Animator component from the inner/child Player GameObject to the root Player GameObject. This will break all the animations. The good thing is that it’s a straightforward way of fixing them.

On each animation you will see that the properties are in a yellow color showing that it’s missing. Click it once an then slow click it again. This will expose the string path. Now we need to append the next string in the beginning of the text to all of our properties:

Sprites/Player/

Like this:

Unity_nM9gvvUuYV.gif

After doing this on every property of each animation, it should be working as before.

In your GameControls asset, add a button type for the Attack. I put the G key on my keyboard.

Unity_WVqtijT0DY.png

After that we need to update our PlayerInput class. We are going to take another approach as we did with the Roll event. This is done just to show we can do this both ways. The bool parameter will indicate if the action was performed or not. Remember that the CallbackContext has this three boolean properties: “started”, “performed” or “cancelled”. We only care about the “performed” value.

namespace Player.Input
{
    public class PlayerInput : MonoBehaviour, GameControls.IPlayerActions
    {
        ...
        public event UnityAction<bool> AttackEvent = delegate {  }; 
        ...
        public void OnAttack(InputAction.CallbackContext context)
        {
            AttackEvent?.Invoke(context.performed);
        }
    }
}

After that we also need to change our PlayerStateMachine class to use this events:

namespace Player
{
    public class PlayerStateMachine : StateMachine<PlayerStateMachine>
    {
        ...        
        public bool AttackPressed => _attackPressed;
        private bool _attackPressed;

	public bool IsFacingRight => _isFacingRight;
        private bool _isFacingRight = true;
        ...

        private void OnEnable()
        {
            ...
            _playerInput.AttackEvent += HandleAttack;
        }

        private void OnDisable()
        {
            ...
            _playerInput.AttackEvent -= HandleAttack;
        }

        ...

        private void HandleAttack(bool isPressed)
        {
            _attackPressed = isPressed;
        }
    }
}

Now we are ready to create our new Attack state. We will name it PlayerAttackState. I created a new folder named Attacks just to make everything more organised but this step is not needed:

using BerserkPixel.StateMachine;
using UnityEngine;

namespace Player.States.Attacks
{
    [CreateAssetMenu(menuName = "States/Player/Attack")]
    public class PlayerAttackState : State<PlayerStateMachine>
    {
        [SerializeField] private AttackSO _attackData;

        public override void Enter(PlayerStateMachine parent)
        {
            base.Enter(parent);
            parent.Animations.PlayAttack(_attackData.attackName);
        }

        public override void Tick(float deltaTime)
        {
        }

        public override void AnimationTriggerEvent(AnimationTriggerType triggerType)
        {
            base.AnimationTriggerEvent(triggerType);

            if (triggerType == AnimationTriggerType.FinishAttack)
            {
                // check for new combo
                if (_runner.AttackPressed)
                {
                    // next combo
                }
                else
                {
                    _runner.SetState(typeof(PlayerIdleState));
                }
            }

            if (triggerType != AnimationTriggerType.HitBox) return;

            // check for collisions
            var colliders = _attackData.Hit(_runner.transform, _runner.IsFacingRight);

            PerformDamage(colliders);
        }

        private void PerformDamage(Collider[] colliders)
        {
            if (colliders.Length <= 0) return;
           
            foreach (var col in colliders)
            {
                Debug.Log($"colliding with {col.gameObject.name}");
                // if (col.TryGetComponent(out Health health))
                // {
                //     health.TakeDamage(_attackData.Damage);
                // }
            }
        }

        public override void FixedTick(float fixedDeltaTime)
        {
        }

        public override void ChangeState()
        {
            if (_runner.Movement.sqrMagnitude != 0)
            {
                _runner.SetState(typeof(PlayerMove3DState));
            }
        }
    }
}

You will notice that there are a couple of errors because we need to create and update some scripts for this to work. Let’s start with the easy ones. Open the AnimationTriggerType:

namespace BerserkPixel.StateMachine
{
    // whatever could be called from the animation timeline
    public enum AnimationTriggerType
    {
        HitBox,
        FinishAttack,
    }
}

Now we are going to actually start using this properly.

Also create a new ScriptableObject for the attack data. I called it AttackSO:

using Extensions;
using UnityEngine;

namespace Player.States.Attacks
{
    [CreateAssetMenu(fileName = "Attack", menuName = "Player/Attack", order = 0)]
    public class AttackSO : ScriptableObject
    {
        [Tooltip("The name of the animation to use")]
        public string attackName;

        [Tooltip("How big is this attack")] 
        public Bounds AttackBounds;

        [Tooltip("The offset from the player")]
        public Vector3 BoundsOffset;

        [Tooltip("The damage to perform on the target")]
        public float Damage;

        [Tooltip("Which Layer is this target")]
        public LayerMask TargetMask;

        /// <summary>
        /// Calculates the bounds from the player
        /// </summary>
        /// <param name="player">The "caller" game object</param>
        /// <param name="isFacingRight"></param>
        /// <returns></returns>
        private Bounds GetBoundsRelativeToPlayer(Transform player, bool isFacingRight)
        {
            var bounds = AttackBounds;
            var xValue = isFacingRight ? 1 : -1;
            var offset = BoundsOffset;
            offset.x *= xValue;
            bounds.center = player.position + offset;

            return bounds;
        }

        /// <summary>
        /// Performs a Physics query to check for colliders in a specific point
        /// </summary>
        /// <param name="origin">The point to perform the check</param>
        /// <param name="isFacingRight">Is the attacker facing right or not</param>
        /// <returns></returns>
        public Collider[] Hit(Transform origin, bool isFacingRight)
        {
            var bounds = GetBoundsRelativeToPlayer(origin, isFacingRight);
            // we call our extension method
            bounds.DrawBounds(1);

            return Physics.OverlapBox(bounds.center, bounds.extents / 2f, Quaternion.identity, TargetMask);
        }
    }
}

Now go to the PlayerAnimations and add this mehod as well:

public void PlayAttack(string attackName)
{
    _animator.CrossFade(attackName, _transitionDuration);
}

For debugging purposes I created an extension file for drawing things on the Scene view. I called it DrawExt. Notice that this is a static class with static methods. Extension functions are really useful to add functionality for an object. In our case we are adding functionality to a Bounds object. This is done by adding the “this” keyword when passing the parameter:

using UnityEngine;

namespace Extensions
{
    public static class DrawExt
    {
        public static void DrawBounds(this Bounds b, float delay = 0)
        {
            // bottom
            var p1 = new Vector3(b.min.x, b.min.y, b.min.z);
            var p2 = new Vector3(b.max.x, b.min.y, b.min.z);
            var p3 = new Vector3(b.max.x, b.min.y, b.max.z);
            var p4 = new Vector3(b.min.x, b.min.y, b.max.z);

            Debug.DrawLine(p1, p2, Color.blue, delay);
            Debug.DrawLine(p2, p3, Color.red, delay);
            Debug.DrawLine(p3, p4, Color.yellow, delay);
            Debug.DrawLine(p4, p1, Color.magenta, delay);

            // top
            var p5 = new Vector3(b.min.x, b.max.y, b.min.z);
            var p6 = new Vector3(b.max.x, b.max.y, b.min.z);
            var p7 = new Vector3(b.max.x, b.max.y, b.max.z);
            var p8 = new Vector3(b.min.x, b.max.y, b.max.z);

            Debug.DrawLine(p5, p6, Color.blue, delay);
            Debug.DrawLine(p6, p7, Color.red, delay);
            Debug.DrawLine(p7, p8, Color.yellow, delay);
            Debug.DrawLine(p8, p5, Color.magenta, delay);

            // sides
            Debug.DrawLine(p1, p5, Color.white, delay);
            Debug.DrawLine(p2, p6, Color.gray, delay);
            Debug.DrawLine(p3, p7, Color.green, delay);
            Debug.DrawLine(p4, p8, Color.cyan, delay);
        }
    }
}

Unity_Q8BOYmI4PL.png

As you can see, this will help out to debug our Bounds from the AttackSO and tweak it to our likings.

Right click and create an AttackSO scriptable object asset.

Unity_qfebl0PoP6.png

This are the settings that works for my specific animation:

Unity_9gWffxQj56.png

I created a separate layer for enemies called Enemy. I will assign this layer to any enemy I have with a collider attached.

Now you can create an animation for an attack. For simplicity I will create a punch effect:

Unity_xaZAUxptno.gif

I created 2 trigger marks in the animation. The first one (the one on the left side) is targeting the SetAnimationTriggerEvent and passing the HitBox as a parameter. The other one is the same but passing a FinishAttack parameter.

Unity_RtNtXcKhxI.gif

Now we need to go back to your code editor and decide from which state we want to attack. I think the best ones are from Idle and Move.

So in PlayerIdleState:

namespace Player.States
{
    public class PlayerIdleState : State<PlayerStateMachine>
    {
	...
        public override void ChangeState()
        {
            if (_runner.AttackPressed)
            {
                _runner.SetState(typeof(PlayerAttackState));
                return;
            }
            
            if (_runner.Movement.sqrMagnitude != 0)
            {
                _runner.SetState(typeof(PlayerMove3DState));
            }
        }
    }
}

And also in the PlayerMove3DState:

namespace Player.States
{   
    public class PlayerMove3DState : State<PlayerStateMachine>
    {
	...
        public override void ChangeState()
        {
            if (_runner.AttackPressed)
            {
                _runner.SetState(typeof(PlayerAttackState));
                return;
            }
            
            if (_runner.RollPressed)
            {
                _runner.SetState(typeof(PlayerRollState));
                return;
            }
            
            if (_playerInput == Vector3.zero)
            {
                _runner.SetState(typeof(PlayerIdleState));
            }
        }
    }
}

Make sure your new attack animation is not set to loop. Your Animator should look something like this:

Unity_ujA0MzGhM3.png

Finally we need something to hit. I will create a really simple enemy. Right click on the Hierarchy →2D Object→ Sprite → Capsule. Attack a Capsule Collider and set the Layer to Enemy:

Unity_uNoOs7fX7Y.gif

Now, press play and test it out! if you get your enemy you should see a log in the console “colliding with Enemy

On the next tutorial we will tackle a weapon system and some basic health system.

See you in the next one!

Support this post

Did you like this post? Tell us

Leave a comment

Log in with your itch.io account to leave a comment.