Skip to main content

On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Making mechanics of Cult of the Lamb: Attacking Part 3

This is the fifth 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, Part 3, Part 4 and Part 5.

Adding Weapons

The first thing we need is some weapon sprites. For this case I’m going to use this 2 different sprites. I made sure to set the pivot somewhere in the handle of each sprite (the little blue circle in the handle you can see on the sprite to the left)

Unity_cj9KdZoQML.gif

We need to create a weapon game object as a visual of to show the players. Create an empty gameobject and name it Weapon 1. Create 2 nested gameobjects inside to put a Sprites holder and inside this one the actual sprites of the weapon. In our case the sword was only 1 sprite but this is useful if we need to add more sprites or even a TrailRenderer to follow the movement. Drag the roots gameobject (Weapon 1) to the Prefabs folder to create a prefab of this weapon. Repeat the same steps for each weapon you have.

Unity_cj9KdZoQML.gif

Now let’s create a scriptable object that holds our weapon data. I called it WeaponSO. This script will hold each weapon data and will also help calculate the bounds of an attack relative to the player’s position.

using UnityEngine;

namespace Weapons
{
    [CreateAssetMenu(fileName = "Weapon", menuName = "Player/Weapon", order = 0)]
    public class WeaponSO : ScriptableObject
    {
        public GameObject Prefab;
        public float Damage;
        public Sprite WeaponSprite;
        
        [Tooltip("How big is this attack")] 
        public Bounds Bounds;

        public Bounds GetBoundsRelativeToPlayer(Transform player)
        {
            var bounds = Bounds;
            bounds.center = player.position;
            return bounds;
        }
    }
}

In the Projects view right click and create a new Weapon data asset. Right click → Create → Player → Weapon. Name it Weapon 1.

Unity_BjAtWPTkfB.png

After that, modify the properties of this weapon data according to your weapon. Drag the previously made prefab (Weapon 1 above) in the Prefab field and adjust the other values to your liking. Here is my setup:

Unity_nx3WaixuzF.png

Equipping Weapons

Now we need to create a script that will help putting this weapon data into the scene. We can call it WeaponItem. This will help to attach any weapon to the player when touched.

using Player;
using UnityEngine;

namespace Weapons
{
    public class WeaponItem : MonoBehaviour
    {
        [SerializeField] private WeaponSO _weapon;
        [SerializeField] private SpriteRenderer _spriteRenderer;

        private Rigidbody _rigidbody;
        private Collider _collider;

        private void Awake()
        {
            _rigidbody = GetComponent<Rigidbody>();
            _collider = GetComponent<Collider>();
        }

        private void OnEnable()
        {
            _spriteRenderer.sprite = _weapon.WeaponSprite;
            
            _collider.enabled = false;
            
            Invoke(nameof(EnableCollider), 1f);
        }

        private void EnableCollider()
        {
            _collider.enabled = true;
        }

        public void CreateInstance(WeaponSO weapon, float force)
        {
            _weapon = weapon;
            _spriteRenderer.sprite = _weapon.WeaponSprite;

            _rigidbody.AddForce((Vector3.up + Vector3.right) * force, ForceMode.Impulse);
        }

        private void OnTriggerEnter(Collider other)
        {
            if (other.gameObject.CompareTag("Player") && other.TryGetComponent(out PlayerStateMachine stateMachine))
            {
                stateMachine.Weapons.AttachWeapon(_weapon);
                Destroy(gameObject);
            }
        }
    }
}

Drag a weapon sprite into the scene. Attach the WeaponItem script and also add a Collider (a Sphere collider in my case) and a Rigidbody.

Unity_aJNY9toZN7.png

Set the collider to be a trigger and freeze all rotations and positions in the Rigidbody

Unity_YrQhRC9mMr.png

This is the final gameobject’s structure:

Unity_8dMItdhOr5.gif

Drag this WeaponItem gameobject to the prefabs folder to create a prefab of it. We are going to use this prefab to spawn it later.

Finally we need a way of spawning this weapons in the scene. We are going to do a simple Singleton to helps us with this:

using UnityEngine;

namespace Weapons
{
    public class WeaponItemSpawner : MonoBehaviour
    {
        [SerializeField] private WeaponItem _prefab;
        [SerializeField] private float _spawnForce = 10f;

        private static WeaponItemSpawner _instance;

        public static WeaponItemSpawner Instance => _instance;

        private void Awake()
        {
            if (_instance != null && _instance != this)
            {
                Destroy(gameObject);
            }
            else
            {
                _instance = this;
            }
        }

        public void SpawnNew(WeaponSO weapon, Vector3 position)
        {
            var newItem = Instantiate(_prefab, position, Quaternion.identity);
            newItem.CreateInstance(weapon, _spawnForce);
        }
    }
}

Modifying the Attacks

Open the AttackSO script. We need to add the weapon here.

...
using Weapons;

namespace Player.States.Attacks
{    
    public class AttackSO : ScriptableObject
    {
        ...

        [Tooltip("The current weapon we are holding")]
        public WeaponSO Weapon;

        ...

        /// <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 need to check if we have a weapon and if so we need to add the bounds
						// of the weapon into the attack itself
            if (Weapon != null)
            {
                bounds.Encapsulate(Weapon.GetBoundsRelativeToPlayer(origin));
            }
            
            // we call our extension method
            bounds.DrawBounds(1);

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

Using the same logic as the PlayerAnimations class we wrote on the previous tutorial, we now can create a PlayerWeapons class to handle the logic of the weapons in the player’s state machine:

using UnityEngine;
using Weapons;

namespace Player.States.Attacks
{
    public class PlayerWeapons
    {
        public bool HasWeapon => _lastWeapon != null && _lastWeapon.Prefab != null;
        public float WeaponDamage => HasWeapon ? _lastWeapon.Damage : 0;

        private Transform _playerHand;
        private WeaponSO _lastWeapon = null;

        public PlayerWeapons(Transform playerHand)
        {
            _playerHand = playerHand;
        }

        public void AttachWeapon(WeaponSO weapon)
        {
            if (weapon.Prefab == null) return;

            if (_playerHand.childCount > 0)
                RemoveWeapon();

            _lastWeapon = weapon;
            Object.Instantiate(weapon.Prefab, _playerHand);
        }

        private void RemoveWeapon()
        {
            WeaponItemSpawner.Instance.SpawnNew(_lastWeapon, _playerHand.root.position);

            _lastWeapon = null;
            RemoveChildren();
        }

        private void RemoveChildren()
        {
            _playerHand.DestroyAllChildren();
        }
    }
}

Now we just need to connect it to the Player’s state machine script:

namespace Player
{
    public class PlayerStateMachine : StateMachine<PlayerStateMachine>
    {
        ...
				[SerializeField] private Transform _weaponsHand;

        public PlayerWeapons Weapons { get; private set; }
        ...

        protected override void Awake()
        {
            ...
            Weapons = new PlayerWeapons(_weaponsHand);
        }
        ...
    }
}

And in the PlayerAttack state we need to get the actual weapon damage if we are holding a weapon:

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

        public override void Enter(PlayerStateMachine parent)
        {
            ...
            _weaponDamage = _runner.Weapons.WeaponDamage;
        }

        ...

        private void PerformDamage(IReadOnlyCollection<Collider> colliders)
        {
            if (colliders.Count <= 0) return;

            foreach (var col in colliders)
            {
                if (col.TryGetComponent(out IDamageable damageable))
                {
                    damageable.Damage(_attackData.Damage + _weaponDamage);
                }
            }
        }

        ...
    }
}

If everything worked we can press play and see something like this:

Unity_pO9vDu8Muv.gif

Support this post

Did you like this post? Tell us

Leave a comment

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