Need something simpler? See the (basic version).
Part of these game systems:
intermediate Health & Combat PRO

Weapon System Pro

Advanced ScriptableObject weapon system with combo chains, weapon switching, damage types, critical hits, and upgrade modifier slots.

Unity 2022.3+ · 7.2 KB · WeaponSystemPro.cs

How to Use

1

Create WeaponDataPro ScriptableObjects: Assets > Create > Weapons > Weapon Data Pro

2

Define damage type, base stats, crit chance/multiplier

3

Add ComboSteps array for melee combo chains (damage multiplier + timing)

4

Attach WeaponSystemPro to your player

5

Add weapons to the inventory list

6

Press 1-9 or scroll wheel to switch weapons

7

Left-click to attack — melee combos chain automatically within the window

8

Create WeaponModifier ScriptableObjects for upgrade slots

9

Call AddModifier() to equip upgrades (flat damage, crit boost, speed)

10

Hook OnCriticalHit for screen effects, OnComboStep for animations

Source Code

WeaponSystemPro.cs
C#
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;

public enum DamageType { Physical, Fire, Ice, Lightning, Poison }

[CreateAssetMenu(fileName = "NewWeaponPro", menuName = "Weapons/Weapon Data Pro")]
public class WeaponDataPro : ScriptableObject
{
    [Header("Identity")]
    public string weaponName = "Weapon";
    public Sprite icon;

    [Header("Base Stats")]
    public float baseDamage = 10f;
    public float attackSpeed = 1f;
    public DamageType damageType = DamageType.Physical;
    public bool isMelee = true;

    [Header("Melee")]
    public float meleeRange = 2f;
    public float meleeAngle = 90f;

    [Header("Ranged")]
    public GameObject projectilePrefab;
    public float projectileSpeed = 30f;
    public int maxAmmo = 30;

    [Header("Critical")]
    public float critChance = 0.1f;
    public float critMultiplier = 2f;

    [Header("Combo")]
    public ComboStep[] comboSteps;
    public float comboWindow = 0.6f;
}

[System.Serializable]
public class ComboStep
{
    public string animationTrigger = "Attack";
    public float damageMultiplier = 1f;
    public float rangeOverride = -1f;
    public float stepDuration = 0.4f;
}

[CreateAssetMenu(fileName = "NewModifier", menuName = "Weapons/Weapon Modifier")]
public class WeaponModifier : ScriptableObject
{
    public string modifierName = "Modifier";
    public Sprite icon;

    [Header("Flat Bonuses")]
    public float flatDamage;
    public float flatCritChance;

    [Header("Percentage Bonuses")]
    [Range(0f, 2f)] public float damageMultiplier = 1f;
    [Range(0f, 2f)] public float attackSpeedMultiplier = 1f;
    [Range(0f, 1f)] public float critChanceBonus;
}

public class WeaponSystemPro : MonoBehaviour
{
    [Header("Weapons")]
    [SerializeField] private List<WeaponDataPro> weaponInventory = new List<WeaponDataPro>();
    [SerializeField] private int startingWeaponIndex;

    [Header("Combat")]
    [SerializeField] private Transform attackPoint;
    [SerializeField] private LayerMask targetLayers;
    [SerializeField] private KeyCode attackKey = KeyCode.Mouse0;

    [Header("Modifiers")]
    [SerializeField] private List<WeaponModifier> modifiers = new List<WeaponModifier>();
    [SerializeField] private int maxModifierSlots = 3;

    [Header("Events")]
    public UnityEvent<float, DamageType, bool> OnAttack;
    public UnityEvent<int> OnComboStep;
    public UnityEvent OnCriticalHit;
    public UnityEvent<WeaponDataPro> OnWeaponSwitch;
    public UnityEvent<int, int> OnAmmoChanged;

    private WeaponDataPro currentWeapon;
    private int currentWeaponIndex = -1;
    private int currentAmmo;
    private float attackTimer;
    private int currentComboStep;
    private float comboTimer;
    private bool comboActive;

    private void Start()
    {
        if (weaponInventory.Count > 0)
            EquipWeapon(Mathf.Clamp(startingWeaponIndex, 0, weaponInventory.Count - 1));
    }

    private void Update()
    {
        attackTimer -= Time.deltaTime;

        // Combo window decay
        if (comboActive)
        {
            comboTimer -= Time.deltaTime;
            if (comboTimer <= 0f)
                ResetCombo();
        }

        // Attack input
        if (Input.GetKeyDown(attackKey) && attackTimer <= 0f && currentWeapon != null)
            PerformAttack();

        // Weapon switching via number keys
        for (int i = 0; i < Mathf.Min(weaponInventory.Count, 9); i++)
        {
            if (Input.GetKeyDown((KeyCode)(KeyCode.Alpha1 + i)))
                EquipWeapon(i);
        }

        // Scroll wheel switching
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if (Mathf.Abs(scroll) > 0.01f && weaponInventory.Count > 1)
        {
            int dir = scroll > 0 ? 1 : -1;
            int next = (currentWeaponIndex + dir + weaponInventory.Count) % weaponInventory.Count;
            EquipWeapon(next);
        }

        // Reload
        if (Input.GetKeyDown(KeyCode.R))
            Reload();
    }

    /// <summary>Equip weapon at inventory index.</summary>
    public void EquipWeapon(int index)
    {
        if (index < 0 || index >= weaponInventory.Count) return;
        if (index == currentWeaponIndex) return;

        currentWeaponIndex = index;
        currentWeapon = weaponInventory[index];
        currentAmmo = currentWeapon.isMelee ? 0 : currentWeapon.maxAmmo;
        ResetCombo();
        attackTimer = 0f;

        OnWeaponSwitch?.Invoke(currentWeapon);
        if (!currentWeapon.isMelee)
            OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);
    }

    private void PerformAttack()
    {
        if (currentWeapon == null) return;

        // Ranged ammo check
        if (!currentWeapon.isMelee && currentAmmo <= 0) return;

        // Calculate effective stats
        float damage = CalculateDamage();
        float speed = CalculateAttackSpeed();
        float critChance = CalculateCritChance();

        // Combo step
        ComboStep step = GetCurrentComboStep();
        if (step != null)
            damage *= step.damageMultiplier;

        // Critical hit
        bool isCrit = Random.value <= critChance;
        if (isCrit)
        {
            damage *= currentWeapon.critMultiplier;
            OnCriticalHit?.Invoke();
        }

        // Set cooldown
        float stepDuration = step != null ? step.stepDuration : (1f / speed);
        attackTimer = stepDuration;

        if (currentWeapon.isMelee)
            PerformMeleeAttack(damage, step);
        else
            PerformRangedAttack(damage);

        OnAttack?.Invoke(damage, currentWeapon.damageType, isCrit);

        // Advance combo
        if (step != null)
        {
            OnComboStep?.Invoke(currentComboStep);
            currentComboStep++;
            if (currentComboStep >= currentWeapon.comboSteps.Length)
                ResetCombo();
            else
            {
                comboActive = true;
                comboTimer = currentWeapon.comboWindow;
            }
        }
    }

    private void PerformMeleeAttack(float damage, ComboStep step)
    {
        Transform origin = attackPoint != null ? attackPoint : transform;
        float range = (step != null && step.rangeOverride > 0)
            ? step.rangeOverride : currentWeapon.meleeRange;

        Collider[] hits = Physics.OverlapSphere(origin.position, range, targetLayers);
        foreach (var hit in hits)
        {
            if (hit.transform == transform || hit.transform.IsChildOf(transform)) continue;

            Vector3 dir = hit.transform.position - origin.position;
            float angle = Vector3.Angle(origin.forward, dir);

            if (angle <= currentWeapon.meleeAngle * 0.5f)
            {
                var health = hit.GetComponent<HealthSystem>();
                if (health != null)
                    health.TakeDamage(damage);
            }
        }
    }

    private void PerformRangedAttack(float damage)
    {
        if (currentWeapon.projectilePrefab == null || attackPoint == null)
            return;

        currentAmmo--;
        OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);

        GameObject proj = Instantiate(currentWeapon.projectilePrefab,
            attackPoint.position, attackPoint.rotation);

        var rb = proj.GetComponent<Rigidbody>();
        if (rb != null)
            rb.linearVelocity = attackPoint.forward * currentWeapon.projectileSpeed;
    }

    /// <summary>Reload current weapon to max ammo.</summary>
    public void Reload()
    {
        if (currentWeapon == null || currentWeapon.isMelee) return;
        currentAmmo = currentWeapon.maxAmmo;
        OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);
    }

    /// <summary>Add a modifier to the weapon. Returns false if slots full.</summary>
    public bool AddModifier(WeaponModifier mod)
    {
        if (mod == null || modifiers.Count >= maxModifierSlots) return false;
        modifiers.Add(mod);
        return true;
    }

    /// <summary>Remove a modifier by reference.</summary>
    public bool RemoveModifier(WeaponModifier mod)
    {
        return modifiers.Remove(mod);
    }

    /// <summary>Get all currently equipped modifiers.</summary>
    public IReadOnlyList<WeaponModifier> GetModifiers() => modifiers.AsReadOnly();

    private float CalculateDamage()
    {
        float dmg = currentWeapon.baseDamage;
        float flatBonus = 0f;
        float multi = 1f;

        for (int i = 0; i < modifiers.Count; i++)
        {
            flatBonus += modifiers[i].flatDamage;
            multi *= modifiers[i].damageMultiplier;
        }

        return (dmg + flatBonus) * multi;
    }

    private float CalculateAttackSpeed()
    {
        float speed = currentWeapon.attackSpeed;
        for (int i = 0; i < modifiers.Count; i++)
            speed *= modifiers[i].attackSpeedMultiplier;
        return speed;
    }

    private float CalculateCritChance()
    {
        float crit = currentWeapon.critChance;
        for (int i = 0; i < modifiers.Count; i++)
        {
            crit += modifiers[i].flatCritChance;
            crit += modifiers[i].critChanceBonus;
        }
        return Mathf.Clamp01(crit);
    }

    private ComboStep GetCurrentComboStep()
    {
        if (currentWeapon.comboSteps == null || currentWeapon.comboSteps.Length == 0)
            return null;
        if (currentComboStep >= currentWeapon.comboSteps.Length)
            return null;
        return currentWeapon.comboSteps[currentComboStep];
    }

    private void ResetCombo()
    {
        currentComboStep = 0;
        comboActive = false;
        comboTimer = 0f;
    }

    /// <summary>Currently equipped weapon data.</summary>
    public WeaponDataPro CurrentWeapon => currentWeapon;

    /// <summary>Current combo step index (0-based).</summary>
    public int CurrentComboStep => currentComboStep;

    /// <summary>Current ammo count.</summary>
    public int CurrentAmmo => currentAmmo;

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        if (currentWeapon == null || !currentWeapon.isMelee) return;
        Transform origin = attackPoint != null ? attackPoint : transform;
        Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.4f);
        Gizmos.DrawWireSphere(origin.position, currentWeapon.meleeRange);
    }
#endif
}