Weapon System Pro
Advanced ScriptableObject weapon system with combo chains, weapon switching, damage types, critical hits, and upgrade modifier slots.
How to Use
Create WeaponDataPro ScriptableObjects: Assets > Create > Weapons > Weapon Data Pro
Define damage type, base stats, crit chance/multiplier
Add ComboSteps array for melee combo chains (damage multiplier + timing)
Attach WeaponSystemPro to your player
Add weapons to the inventory list
Press 1-9 or scroll wheel to switch weapons
Left-click to attack — melee combos chain automatically within the window
Create WeaponModifier ScriptableObjects for upgrade slots
Call AddModifier() to equip upgrades (flat damage, crit boost, speed)
Hook OnCriticalHit for screen effects, OnComboStep for animations
Features
- ScriptableObject-based weapon definitions with damage types (Physical, Fire, Ice, Lightning, Poison)
- Melee combo chain system with per-step damage multipliers and timing windows
- Ranged weapons with projectile spawning, ammo tracking, and reload
- Critical hit system with configurable chance and damage multiplier
- Weapon switching via number keys or scroll wheel with inventory support
- Modifier slot system for flat and percentage-based weapon upgrades
When to Use This
Designed for FPS, RPG, and action games that need a data-driven weapon system supporting both melee and ranged combat. Use this when you want designers to create and balance weapons via ScriptableObjects without touching code, especially for games with loot or upgrade mechanics.
Common Mistakes
The attackPoint Transform must be assigned for ranged weapons or projectiles won't spawn — it also determines the origin of melee OverlapSphere checks. ComboSteps require careful stepDuration tuning; if it's shorter than the attack animation, the combo window closes before the player can input the next hit. Modifier stacking is multiplicative for damageMultiplier, so two 1.5x modifiers give 2.25x, not 2x.
Source Code
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
}