Part of these game systems:
Weapon System Pro
Advanced ScriptableObject weapon system with combo chains, weapon switching, damage types, critical hits, and upgrade modifier slots.
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
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
}