Player Damage
Handling player damage events is essential for plugins that implement custom combat systems, invulnerability zones, damage tracking, or gameplay modifiers. RocketMod provides the UnturnedEvents.OnPlayerDamaged event for intercepting damage before it is applied to a player. Understanding how damage flows through Unturned's systems and how RocketMod exposes it is necessary for any gameplay-related plugin.
This article covers the damage event system, how to subscribe and respond to damage events, how damage cancellation works, and practical patterns for damage modification and tracking.
57 Studios operates multiple Unturned servers with custom PvP and PvE mechanics. The damage patterns documented here are drawn from production plugin authoring experience where damage modification is used for zone-based invulnerability, weapon balancing, and combat logging.
Prerequisites
- A working RocketMod installation. See RocketMod and OpenMod Plugin Basics.
- Visual Studio with C# development workload.
- Familiarity with RocketMod event subscription patterns.
- Basic understanding of Unturned's damage system (melee, gun, explosion, environmental).
What you'll learn
- How to subscribe to the
UnturnedEvents.OnPlayerDamagedevent. - What damage sources exist and how to identify them.
- How to modify or cancel damage before it is applied.
- How to implement safe zones and invulnerability.
- How to track damage statistics per player.
- Best practices for damage-related plugins.
Damage event overview
RocketMod exposes player damage through the UnturnedEvents static class. The primary event is OnPlayerDamaged, which fires when a player is about to take damage from any source. The event fires before the damage is applied, allowing plugins to modify or cancel it.
Event location
The OnPlayerDamaged event lives on Rocket.Unturned.Events.UnturnedEvents:
csharp
using Rocket.Unturned.Events;Subscribing to damage events
csharp
using Rocket.Unturned.Events;
protected override void Load()
{
UnturnedEvents.OnPlayerDamaged += OnPlayerDamaged;
}
protected override void Unload()
{
UnturnedEvents.OnPlayerDamaged -= OnPlayerDamaged;
}Damage event parameters
The OnPlayerDamaged event passes parameters that describe the damage being dealt, including the player receiving the damage, the amount of damage, the damage source, and contextual information about how the damage occurred.
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, /* additional parameters */)
{
// victim — the player receiving the damage
// damageAmount — the amount of damage (modifiable via ref)
// cancel — whether to cancel the damage (modifiable via ref)
}Setting ref bool cancel to true prevents the damage from being applied:
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
// Cancel all damage for testing
cancel = true;
}::: caution Cancellation behavior varies RocketMod's OnPlayerDamaged event supports cancellation through event arguments. Setting ref bool cancel = true in the handler should prevent the damage from being applied. However, the cancellation behavior depends on the specific damage source and the version of Unturned the server is running.
For some damage sources — particularly environmental damage (falling, drowning, starvation) and legacy damage types — the cancellation may not prevent the damage from being applied. The event fires, the cancel flag is set, but the damage is still processed by Unturned's internal systems.
Always test your damage cancellation logic on the specific Unturned version your server runs. Do not assume that setting cancel = true works uniformly across all damage sources.
For reliable invulnerability, consider combining event cancellation with post-damage health restoration:
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
if (victim.Player.life.health > 0)
{
cancel = true;
// Post-damage health reset as a safety net
// for cases where cancellation does not fully work
}
}:::
Damage source identification
The event provides an EDeathCause enum that identifies the type of damage:
| EDeathCause value | Damage type | Example |
|---|---|---|
Bleeding | Bleed-out damage | Player ignored a bleed effect |
Bones | Fall damage | Player fell from a height |
Brain | Headshot damage | Sniper rifle headshot |
Burning | Fire damage | Standing in fire |
Food | Starvation damage | Player's food meter reached zero |
Gun | Firearm damage | Shot by a gun |
Infection | Infection damage | Player ignored an infection |
Melee | Melee weapon damage | Hit with a melee weapon |
Roadkill | Vehicle impact | Run over by a vehicle |
Sentry | Sentry gun damage | Hit by a buildable sentry |
Shark | Shark attack | Attacked by a shark in water |
Spawn | Kill command | /kill or suicide command |
Suicide | Suicide | Player killed themselves |
Vehicle | Vehicle explosion | Vehicle exploded near the player |
Water | Drowning damage | Player's oxygen meter depleted |
Zombie | Zombie attack | Attacked by a zombie |
Damage amount
The damageAmount parameter is a ushort (0–65535). It is the raw damage value before player armor and resistance calculations. Modifying this value changes the damage the player receives:
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
// Halve all gun damage
if (cause == EDeathCause.Gun)
{
damageAmount = (ushort)(damageAmount / 2);
}
}Safe zone implementation
A common use case for damage events is implementing safe zones — areas where players cannot take or deal damage:
csharp
using System.Collections.Generic;
using UnityEngine;
public class SafeZoneComponent : UnturnedPlayerComponent
{
public bool IsInSafeZone { get; set; }
}
// In the plugin:
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
var component = victim.GetComponent<SafeZoneComponent>();
if (component != null && component.IsInSafeZone)
{
cancel = true;
}
// Also prevent players in safe zones from dealing damage
if (killer != null)
{
var killerPlayer = UnturnedPlayer.FromSteamPlayer(killer);
var killerComponent = killerPlayer.GetComponent<SafeZoneComponent>();
if (killerComponent != null && killerComponent.IsInSafeZone)
{
cancel = true;
}
}
}Zone checking with distance
A more practical approach checks distance from predefined safe zone centers:
csharp
public class SafeZone
{
public Vector3 Center;
public float Radius;
}
private readonly List<SafeZone> _safeZones = new List<SafeZone>
{
new SafeZone { Center = new Vector3(100f, 0f, 200f), Radius = 50f }
};
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
Vector3 victimPos = victim.Player.transform.position;
foreach (var zone in _safeZones)
{
if (Vector3.Distance(victimPos, zone.Center) <= zone.Radius)
{
cancel = true;
return;
}
}
}Damage tracking and statistics
Plugins that track damage statistics for leaderboards or gameplay analysis can aggregate damage data in the event handler:
csharp
using System.Collections.Generic;
public class DamageTrackerComponent : UnturnedPlayerComponent
{
public Dictionary<EDeathCause, int> DamageTaken;
public Dictionary<string, int> DamageDealtByPlayer;
public int TotalDamageTaken;
public int TotalDamageDealt;
protected override void Load()
{
DamageTaken = new Dictionary<EDeathCause, int>();
DamageDealtByPlayer = new Dictionary<string, int>();
TotalDamageTaken = 0;
TotalDamageDealt = 0;
}
}
// In the plugin:
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
var victimComp = victim.GetComponent<DamageTrackerComponent>();
if (victimComp != null)
{
if (!victimComp.DamageTaken.ContainsKey(cause))
victimComp.DamageTaken[cause] = 0;
victimComp.DamageTaken[cause] += damageAmount;
victimComp.TotalDamageTaken += damageAmount;
}
if (killer != null)
{
var killerPlayer = UnturnedPlayer.FromSteamPlayer(killer);
var killerComp = killerPlayer.GetComponent<DamageTrackerComponent>();
if (killerComp != null)
{
killerComp.TotalDamageDealt += damageAmount;
}
}
}Damage modification by weapon type
For gameplay balancing, plugins can modify damage based on the equipped weapon:
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
if (cause != EDeathCause.Gun && cause != EDeathCause.Melee)
return;
if (killer == null)
return;
// Get the killer's active item
Items kItems = killer.player.equipped;
if (kItems == null)
return;
ushort itemId = kItems.id;
// Apply damage modifiers based on weapon ID
switch (itemId)
{
case 101: // Maple Rifle
damageAmount = (ushort)(damageAmount * 0.8); // 20% damage reduction
break;
case 102: // Sport Shot
damageAmount = (ushort)(damageAmount * 1.2); // 20% damage increase
break;
case 103: // Snayperskya
damageAmount = (ushort)(damageAmount * 1.5); // 50% damage increase
break;
}
}Combat logging and invulnerability
A common pattern in RP servers is combat logging prevention — players who have taken recent damage cannot disconnect without consequences:
csharp
public class CombatLogComponent : UnturnedPlayerComponent
{
public bool IsInCombat { get; private set; }
public UnturnedPlayer LastAttacker;
private float _combatEndTime;
public void Tag(UnturnedPlayer attacker)
{
IsInCombat = true;
LastAttacker = attacker;
_combatEndTime = Time.realtimeSinceStartup + 30f; // 30-second combat timer
Player.SendChat("You are now in combat! Do not disconnect.", Color.red);
}
private void Update()
{
if (IsInCombat && Time.realtimeSinceStartup >= _combatEndTime)
{
IsInCombat = false;
LastAttacker = null;
Player.SendChat("You are no longer in combat.", Color.green);
}
}
}
// In the plugin:
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
var victimCombat = victim.GetComponent<CombatLogComponent>();
if (victimCombat != null && victimCombat.IsInCombat)
{
// Extend combat timer on each hit
victimCombat.Tag(killer != null ? UnturnedPlayer.FromSteamPlayer(killer) : null);
}
if (killer != null)
{
var killerPlayer = UnturnedPlayer.FromSteamPlayer(killer);
var killerCombat = killerPlayer.GetComponent<CombatLogComponent>();
if (killerCombat != null)
{
killerCombat.Tag(victim);
}
}
}Damage immunity periods
Some gameplay mechanics require temporary damage immunity — after respawn, teleportation, or entering a protected area:
csharp
public class DamageImmunityComponent : UnturnedPlayerComponent
{
private float _immunityEndTime;
public void GrantImmunity(float seconds)
{
_immunityEndTime = Time.realtimeSinceStartup + seconds;
Player.SendChat($"You are immune to damage for {seconds} seconds.", Color.cyan);
}
public bool IsImmune
{
get { return Time.realtimeSinceStartup < _immunityEndTime; }
}
}
// In the plugin:
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
var immunity = victim.GetComponent<DamageImmunityComponent>();
if (immunity != null && immunity.IsImmune)
{
cancel = true;
}
}Limb-specific damage
The ELimb parameter identifies which body part was hit:
| ELimb value | Body part |
|---|---|
LeftArm | Left arm |
RightArm | Right arm |
LeftLeg | Left leg |
RightLeg | Right leg |
Spine | Torso |
Skull | Head |
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
// Headshots deal 3x damage
if (limb == ELimb.Skull)
{
damageAmount = (ushort)(damageAmount * 3);
}
// Leg shots deal 0.5x damage
if (limb == ELimb.LeftLeg || limb == ELimb.RightLeg)
{
damageAmount = (ushort)(damageAmount * 0.5);
}
}Complete damage plugin example
The following plugin demonstrates damage modification, safe zones, combat logging, and limb-specific damage multipliers integrated into a single plugin.
CombatPlugin.cs:
csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using SDG.Unturned;
using System.Collections.Generic;
using UnityEngine;
namespace CombatPlugin
{
public class CombatPlugin : RocketPlugin<CombatPluginConfiguration>
{
public static CombatPlugin Instance { get; private set; }
private readonly List<Vector3> _safeZoneCenters = new List<Vector3>
{
new Vector3(0f, 0f, 0f), // Example safe zone 1
new Vector3(200f, 0f, 300f) // Example safe zone 2
};
private const float SAFE_ZONE_RADIUS = 30f;
private readonly Dictionary<EDeathCause, float> _damageMultipliers =
new Dictionary<EDeathCause, float>
{
{ EDeathCause.Gun, 1.0f },
{ EDeathCause.Melee, 1.0f },
{ EDeathCause.Zombie, 0.5f },
{ EDeathCause.Burning, 1.5f },
{ EDeathCause.Bones, 1.0f }
};
protected override void Load()
{
Instance = this;
UnturnedEvents.OnPlayerDamaged += OnPlayerDamaged;
Rocket.Core.Logging.Logger.Log("CombatPlugin loaded.");
}
protected override void Unload()
{
UnturnedEvents.OnPlayerDamaged -= OnPlayerDamaged;
Instance = null;
Rocket.Core.Logging.Logger.Log("CombatPlugin unloaded.");
}
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
// 1. Safe zone check
foreach (Vector3 zoneCenter in _safeZoneCenters)
{
float dist = Vector3.Distance(victim.Player.transform.position, zoneCenter);
if (dist <= SAFE_ZONE_RADIUS)
{
cancel = true;
return;
}
}
// 2. Combat logging tag
var combatLog = victim.GetComponent<CombatLogComponent>();
if (combatLog != null)
{
if (killer != null)
{
combatLog.Tag(UnturnedPlayer.FromSteamPlayer(killer));
}
else
{
combatLog.Tag(null);
}
}
// 3. Apply damage multiplier by cause
if (_damageMultipliers.TryGetValue(cause, out float multiplier))
{
damageAmount = (ushort)Mathf.RoundToInt(damageAmount * multiplier);
}
// 4. Limb multiplier
if (limb == ELimb.Skull)
{
damageAmount = (ushort)Mathf.RoundToInt(damageAmount * 2.5f);
}
else if (limb == ELimb.LeftLeg || limb == ELimb.RightLeg)
{
damageAmount = (ushort)Mathf.RoundToInt(damageAmount * 0.6f);
}
// 5. Log significant damage events
if (damageAmount > 50)
{
string killerName = killer != null ? killer.playerID.playerName : "environment";
Rocket.Core.Logging.Logger.Log(
$"HIGH DAMAGE: {victim.DisplayName} took {damageAmount} damage " +
$"from {killerName} ({cause})"
);
}
}
}
}Debugging damage events
Logging all damage
During development, log every damage event to understand the event flow:
csharp
private void OnPlayerDamaged(UnturnedPlayer victim, ref ushort damageAmount, ref bool cancel, EDeathCause cause, ELimb limb, SteamPlayer killer)
{
string killerName = killer != null ? killer.playerID.playerName : "environment";
Rocket.Core.Logging.Logger.Log(
$"DAMAGE: {victim.DisplayName} | Amount: {damageAmount} | " +
$"Cause: {cause} | Limb: {limb} | " +
$"Killer: {killerName} | Cancel: {cancel}"
);
}Common damage event issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Cancellation does not prevent damage | Damage source does not support cancellation | Add post-damage health restoration as fallback |
| Damage modification does not apply | Handler subscribed after damage applied | Verify subscription in Load() |
| NullReferenceException in handler | killer or victim is null | Null-check all parameters before accessing |
| Damage values appear incorrect | Raw damage vs modified damage confusion | Log both raw and modified values |
Frequently asked questions
Can I prevent all damage to a player?
Yes, by setting cancel = true in the damage event handler. However, as noted in the caution above, some damage sources may bypass the cancellation mechanism. For complete invulnerability, combine cancellation with periodic health restoration.
How do I identify the weapon used?
The weapon item ID can be obtained from the killer's equipped item context through SteamPlayer and the Items class. Note that in some damage events, the weapon information may not be directly available.
Does OnPlayerDamaged fire for environmental damage?
Yes. Environmental damage (falling, drowning, burning, starving) fires the OnPlayerDamaged event with the corresponding EDeathCause value.
Can I make a player take more damage from specific sources?
Yes. Modify the damageAmount parameter in the event handler based on the EDeathCause value. Apply a multiplier greater than 1.0 to increase damage.
Is there an event for when a player heals?
RocketMod does not expose a built-in healing event. To track healing, you would need to poll the player's health periodically or subscribe to Unturned's lower-level healing events.
How do I make a player invulnerable for a specific duration?
Use a component with a timer. In the OnPlayerDamaged handler, check whether the component's immunity flag is active and set cancel = true if so. Reset the flag after the duration expires using Update() or a coroutine.
Can I log all damage dealt and received?
Yes. Subscribe to OnPlayerDamaged and log the victim, damage amount, cause, limb, and killer to a file. Use a Dictionary<ulong, DamageLog> structure keyed by player Steam ID to aggregate per-player statistics.
What happens if I set damageAmount to 0?
Setting damageAmount to 0 effectively cancels the damage for most sources, but the event still fires and the player may still receive a hit marker or sound effect. For complete cancellation, set cancel = true instead of reducing damage to zero.
Can I reflect damage back to the attacker?
Yes. In the OnPlayerDamaged handler, when killer is not null, apply damage back to the killer using the same damage amount. Be careful to add a reflection cap or counter to prevent infinite reflection loops.
Does OnPlayerDamaged fire for self-inflicted damage?
Yes. Self-inflicted damage (such as from a explosive placed by the player themselves or fall damage from a self-built structure) fires the event with the player as both the victim and the killer.
How do I test damage events during development?
Use a debug command that deals a fixed amount of damage to yourself. Subscribe a test handler that logs the full event payload: victim, damage amount, cause, limb, and killer. Compare the logged output against your expected behavior.
Can I make certain items deal no damage?
Yes. Check the killer's equipped item ID in the handler and set cancel = true for specific items. This is useful for disabling damage from admin-only weapons or event items during special game modes.
Cross-references
- Player Death and Respawn — death/respawn handling following damage events.
- Player Chat — chat events that may interact with damage systems.
- Player Components — per-player component setup for tracking damage state.
- Permissions System — permission-based damage feature access.
- RocketMod and OpenMod Plugin Basics — plugin lifecycle and structure.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Coverage of damage events, damage modification, safe zones, combat logging, damage tracking, best practices. |
