Weapon System
ScriptableObject-driven weapon system with melee and ranged attacks, cooldowns, ammo, and damage calculation.
How to Use
Create weapon ScriptableObjects: Assets > Create > Weapons > Weapon Data
Configure melee (range, angle) or ranged (projectile, ammo)
Attach WeaponSystem to your player
Assign attack point Transform (muzzle or hand)
Set initial weapon or call EquipWeapon() at runtime
Left-click to attack (configurable)
Hook OnAmmoChanged to UI, OnAttack to animation/SFX
Features
- ScriptableObject weapon definitions created via Assets menu
- Supports both melee (range + angle cone) and ranged (projectile) attack types
- Attack rate cooldown and ammo management with reload
- Melee uses OverlapSphere with angle check for arc-based hits
- UnityEvents for equip, attack, ammo change, and out-of-ammo
- Editor gizmo visualization for melee attack range
When to Use This
Ideal for RPGs, hack-and-slash games, and FPS titles that need swappable weapons with different behaviors. The ScriptableObject approach lets designers create and tweak weapons without touching code. Pair with Health System for damage dealing and Projectile System for ranged attacks.
Common Mistakes
Don't forget to assign the attackPoint Transform — it defaults to the object's own transform, which may not match your muzzle or hand position. For ranged weapons, the projectilePrefab must have a Projectile component attached. The meleeAngle is the full arc (90 means 45 degrees on each side of forward), and meleeHitLayers must include enemy layers.
Source Code
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// ScriptableObject weapon definition. Create weapons via Assets menu.
/// </summary>
[CreateAssetMenu(menuName = "Weapons/Weapon Data")]
public class WeaponData : ScriptableObject
{
[Header("Info")]
public string weaponName = "Weapon";
public Sprite icon;
[Header("Type")]
public bool isMelee = true;
[Header("Damage")]
public float damage = 10f;
public float attackRate = 1f; // attacks per second
[Header("Melee")]
public float meleeRange = 2f;
public float meleeAngle = 90f;
public LayerMask meleeHitLayers = ~0;
[Header("Ranged")]
public GameObject projectilePrefab;
public float projectileSpeed = 20f;
public int maxAmmo = 30;
public int ammoPerShot = 1;
}
/// <summary>
/// Weapon controller. Handles attacking, cooldowns, and ammo.
/// Attach to the player or weapon holder.
/// </summary>
public class WeaponSystem : MonoBehaviour
{
[Header("Current Weapon")]
[SerializeField] private WeaponData currentWeapon;
[Header("References")]
[SerializeField] private Transform attackPoint;
[Header("Events")]
public UnityEvent<WeaponData> OnWeaponEquipped;
public UnityEvent OnAttack;
public UnityEvent<int, int> OnAmmoChanged; // current, max
public UnityEvent OnOutOfAmmo;
private float cooldownTimer;
private int currentAmmo;
/// <summary>Currently equipped weapon data.</summary>
public WeaponData CurrentWeapon => currentWeapon;
/// <summary>Current ammo count (ranged only).</summary>
public int CurrentAmmo => currentAmmo;
private void Start()
{
if (currentWeapon != null)
EquipWeapon(currentWeapon);
}
private void Update()
{
if (cooldownTimer > 0f)
cooldownTimer -= Time.deltaTime;
if (Input.GetMouseButton(0) && cooldownTimer <= 0f)
{
Attack();
}
}
/// <summary>
/// Equip a weapon by setting its data.
/// </summary>
public void EquipWeapon(WeaponData weapon)
{
currentWeapon = weapon;
if (!weapon.isMelee)
{
currentAmmo = weapon.maxAmmo;
OnAmmoChanged?.Invoke(currentAmmo, weapon.maxAmmo);
}
OnWeaponEquipped?.Invoke(weapon);
}
/// <summary>
/// Perform an attack with the current weapon.
/// </summary>
public void Attack()
{
if (currentWeapon == null || cooldownTimer > 0f) return;
if (currentWeapon.isMelee)
MeleeAttack();
else
RangedAttack();
cooldownTimer = 1f / currentWeapon.attackRate;
OnAttack?.Invoke();
}
private void MeleeAttack()
{
Transform origin = attackPoint != null ? attackPoint : transform;
Collider[] hits = Physics.OverlapSphere(origin.position, currentWeapon.meleeRange, currentWeapon.meleeHitLayers);
foreach (var hit in hits)
{
if (hit.gameObject == gameObject) continue;
// Angle check
Vector3 dirToTarget = (hit.transform.position - origin.position).normalized;
float angle = Vector3.Angle(origin.forward, dirToTarget);
if (angle <= currentWeapon.meleeAngle * 0.5f)
{
HealthSystem health = hit.GetComponent<HealthSystem>();
if (health != null)
health.TakeDamage(currentWeapon.damage);
}
}
}
private void RangedAttack()
{
if (currentAmmo < currentWeapon.ammoPerShot)
{
OnOutOfAmmo?.Invoke();
return;
}
currentAmmo -= currentWeapon.ammoPerShot;
OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);
if (currentWeapon.projectilePrefab != null)
{
Transform origin = attackPoint != null ? attackPoint : transform;
GameObject proj = Instantiate(
currentWeapon.projectilePrefab,
origin.position,
origin.rotation
);
Projectile projectile = proj.GetComponent<Projectile>();
if (projectile != null)
{
// Projectile handles its own speed from its own settings
}
}
}
/// <summary>Add ammo to the current weapon.</summary>
public void AddAmmo(int amount)
{
if (currentWeapon == null || currentWeapon.isMelee) return;
currentAmmo = Mathf.Min(currentAmmo + amount, currentWeapon.maxAmmo);
OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);
}
/// <summary>Reload to max ammo.</summary>
public void Reload()
{
if (currentWeapon == null || currentWeapon.isMelee) return;
currentAmmo = currentWeapon.maxAmmo;
OnAmmoChanged?.Invoke(currentAmmo, currentWeapon.maxAmmo);
}
#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
}