Skip to content

Player Vitals

The Unturned survival system tracks five core player vitals: health, food, water, virus, and stamina. These values determine whether a player lives, dies, or thrives in the game world. Every survival mechanic — hunger, thirst, radiation poisoning, healing items, environmental damage, and natural regeneration — operates through this vitals system. Plugin developers who build medical systems, survival enhancements, environmental hazard zones, or custom damage mechanics need to understand how RocketMod exposes and manages player vitals.

This article covers the RocketMod player vitals API in depth. It explains each vital's role, the events that fire when vitals change, how to read and modify vitals from plugin code, and how to build a complete medical system with custom healing items and environmental damage zones.

57 Studios maintains a suite of server-side plugins for the Horizon Life RP community. The patterns documented here are drawn from production plugin authoring experience, not from documentation alone.

Prerequisites

  • A working Unturned dedicated server with RocketMod installed. See RocketMod and OpenMod Plugin Basics for installation guidance.
  • Visual Studio 2022 with C# support.
  • .NET Framework 4.7.2 SDK.
  • Familiarity with RocketMod event subscription and command registration.
  • Understanding of the Unturned survival system from a player perspective.

What you'll learn

  • The five player vital values: health, food, water, virus, and stamina, and what each represents.
  • How to read and write each vital value using the UnturnedPlayer API.
  • The OnPlayerUpdateHealth event — when it fires and what data it provides.
  • How to build a custom damage system that applies different damage types.
  • How to build environmental hazard zones that drain specific vitals.
  • How to implement a healing item system with cooldowns and stacking rules.
  • How to monitor vital changes and trigger effects at thresholds.
  • Common pitfalls: event frequency assumptions, missing thresholds, and negative value edge cases.

Vitals overview

Vital properties

RocketMod exposes each vital as a byte property on the UnturnedPlayer object:

PropertyTypeRangeDefault maxDescription
Healthbyte0-100100Player's current health. Reaches 0 = death.
Foodbyte0-100100Player's current food level. Decreases over time.
Waterbyte0-100100Player's current water level. Decreases over time.
Virusbyte0-100100Player's radiation/infection level. Increases near radiation sources.
Staminabyte0-100100Player's current stamina. Decreases with sprinting, jumping.

Vital mechanics

Each vital follows different game rules:

Health decreases from damage (weapons, fall damage, environmental hazards, zombies, starvation, thirst, virus). It increases from healing items, natural regeneration (when food and water are full), and medical attention. When health reaches 0, the player dies.

Food decreases at a constant rate over real time. A full food bar takes approximately 20 minutes of gameplay to deplete to 0. When food reaches 0, the player starts taking health damage.

Water decreases at a slightly faster rate than food. A full water bar takes approximately 15 minutes to deplete. When water reaches 0, the player starts taking health damage.

Virus increases when the player is near radioactive zones, irradiated items, or certain environmental hazards. When virus reaches 100, the player starts taking health damage until they die or take an antidote.

Stamina decreases when the player sprints, jumps, or performs strenuous actions. It regenerates naturally when the player stands still or walks. The regeneration rate is affected by the Cardio skill.

Vital interaction diagram

Reading and writing vitals

Reading a vital

csharp
UnturnedPlayer player = UnturnedPlayer.FromName("Butter");

byte health = player.Health;
byte food = player.Food;
byte water = player.Water;
byte virus = player.Virus;
byte stamina = player.Stamina;

Writing a vital

csharp
player.Health = 100;    // Full health
player.Food = 100;      // Full food
player.Water = 100;     // Full water
player.Virus = 0;       // Zero radiation
player.Stamina = 100;   // Full stamina

Clamping vital values

All vitals are byte values and cannot go below 0 or above 255 in raw C# terms, but the game engine clamps each vital to its natural range (0-100 for all five). Setting a value outside 0-100 is safe — the engine clamps it — but produces no additional effect:

csharp
// Safe but unnecessary
player.Health = 200;  // Engine clamps to 100
player.Health = 0;    // Engine allows 0 (player dies)

// Recommended — explicit clamping for readability
player.Health = (byte)Math.Clamp(newValue, (byte)0, (byte)100);

Checking player life state

Before modifying vitals, check whether the player is alive:

csharp
public bool IsPlayerAlive(UnturnedPlayer player)
{
    return player.Player != null 
        && !player.Player.life.isDead;
}

Healing a player

csharp
public void HealPlayer(UnturnedPlayer player, byte healthAmount)
{
    if (!IsPlayerAlive(player))
    {
        Logger.LogWarning($"Cannot heal dead player {player.DisplayName}.");
        return;
    }

    player.Health = (byte)Math.Min(100, player.Health + healthAmount);
}

Damaging a player

csharp
public void DamagePlayer(UnturnedPlayer player, byte damageAmount)
{
    if (!IsPlayerAlive(player))
    {
        return;
    }

    int newHealth = player.Health - damageAmount;

    if (newHealth <= 0)
    {
        player.Health = 0;
        player.Player.life.askDamage(damageAmount, 
            Vector3.zero, EDeathCause.PUNCH, ELimb.SKULL,
            CSteamID.Nil);
    }
    else
    {
        player.Health = (byte)newHealth;
    }
}

OnPlayerUpdateHealth

Event signature

csharp
public static event PlayerUpdateHealth OnPlayerUpdateHealth;
public delegate void PlayerUpdateHealth(UnturnedPlayer player, byte health);

The handler receives the player whose health changed and the player's new health value.

Subscription pattern

csharp
using Rocket.Unturned.Events;

protected override void Load()
{
    UnturnedPlayerEvents.OnPlayerUpdateHealth += HandleHealthChanged;
}

protected override void Unload()
{
    UnturnedPlayerEvents.OnPlayerUpdateHealth -= HandleHealthChanged;
}

private void HandleHealthChanged(UnturnedPlayer player, byte health)
{
    Logger.Log($"{player.DisplayName} health changed to {health}.");
}

When the event fires

The OnPlayerUpdateHealth event fires every time the player's health changes by any amount. Each healing tick, each damage tick, each environmental damage instance — every change triggers the event. This allows plugins to react to health changes with precision, whether the change is a single point of starvation damage or a full 50-point gunshot wound.

Event timing relative to damage

The event fires after the health value has been updated. The health parameter represents the new health value, not the change amount. To calculate how much health changed, compare against a stored previous value:

csharp
private readonly Dictionary<ulong, byte> _lastHealth =
    new Dictionary<ulong, byte>();

private void HandleHealthChanged(UnturnedPlayer player, byte health)
{
    ulong steamId = player.CSteamID.m_SteamID;

    if (_lastHealth.TryGetValue(steamId, out byte previous))
    {
        int delta = health - previous;

        if (delta < 0)
        {
            // Player lost health (took damage)
            HandleDamageTaken(player, (byte)Math.Abs(delta), health);
        }
        else if (delta > 0)
        {
            // Player gained health (healed)
            HandleHealingReceived(player, (byte)delta, health);
        }
    }

    _lastHealth[steamId] = health;
}

private void HandleDamageTaken(UnturnedPlayer player, byte damageAmount, byte newHealth)
{
    if (newHealth <= 20 && newHealth > 0)
    {
        player.SendChat("You are critically wounded!", Color.red);
    }

    if (newHealth == 0)
    {
        player.SendChat("You have died."); // May not reach client if death screen takes over
    }
}

private void HandleHealingReceived(UnturnedPlayer player, byte healAmount, byte newHealth)
{
    if (newHealth == 100)
    {
        player.SendChat("You are fully healed.", Color.green);
    }
}

Health threshold alerts

csharp
private void HandleHealthChanged(UnturnedPlayer player, byte health)
{
    if (health <= 25 && health > 0)
    {
        player.SendChat(
            $"WARNING: Your health is critically low ({health}/100). " +
            "Seek medical attention immediately.", Color.red);
    }
    else if (health <= 50 && health > 25)
    {
        player.SendChat(
            $"Your health is moderate ({health}/100). " +
            "Consider healing.", Color.yellow);
    }
    else if (health == 100)
    {
        player.SendChat("You are fully healed.", Color.green);
    }
}

Building a custom damage system

The following system applies damage from custom sources — environmental hazards, zone effects, and admin actions — using the RocketMod vitals API.

Damage source enum

csharp
public enum CustomDamageSource
{
    Environmental,
    ZoneHazard,
    AdminAction,
    Starvation,
    Dehydration,
    Radiation
}

Damage application with death check

csharp
public class DamageSystem
{
    public void ApplyDamage(
        UnturnedPlayer player,
        byte amount,
        CustomDamageSource source)
    {
        if (player.Player.life.isDead)
            return;

        int newHealth = player.Health - amount;

        if (newHealth <= 0)
        {
            player.Health = 0;
            player.Player.life.askDamage(
                amount,
                Vector3.zero,
                EDeathCause.PUNCH,
                ELimb.SKULL,
                CSteamID.Nil);

            Rocket.Core.Logging.Logger.Log(
                $"[DamageSystem] {player.DisplayName} killed by {source}.");
        }
        else
        {
            player.Health = (byte)newHealth;
        }
    }
}

Damage cooldown system

csharp
private readonly Dictionary<ulong, Dictionary<CustomDamageSource, DateTime>> _damageCooldowns =
    new Dictionary<ulong, Dictionary<CustomDamageSource, DateTime>>();

private readonly Dictionary<CustomDamageSource, int> _cooldownConfig =
    new Dictionary<CustomDamageSource, int>
    {
        { CustomDamageSource.Environmental, 3 },
        { CustomDamageSource.ZoneHazard, 2 },
        { CustomDamageSource.Radiation, 1 }
    };

public bool CanApplyDamage(UnturnedPlayer player, CustomDamageSource source)
{
    ulong steamId = player.CSteamID.m_SteamID;

    if (!_damageCooldowns.ContainsKey(steamId))
    {
        _damageCooldowns[steamId] = new Dictionary<CustomDamageSource, DateTime>();
        return true;
    }

    if (_damageCooldowns[steamId].TryGetValue(source, out DateTime lastDamage))
    {
        int cooldown = _cooldownConfig[source];
        return (DateTime.UtcNow - lastDamage).TotalSeconds >= cooldown;
    }

    return true;
}

public void TrackDamageCooldown(UnturnedPlayer player, CustomDamageSource source)
{
    ulong steamId = player.CSteamID.m_SteamID;
    _damageCooldowns[steamId][source] = DateTime.UtcNow;
}

Environmental hazard zones

Hazard zones drain specific vitals while the player is inside them. The following plugin implements a radiation zone system.

Zone definition

csharp
public class HazardZone
{
    public string Name;
    public Vector3 Center;
    public float Radius;
    public VitalType AffectedVital;
    public byte DamagePerSecond;
    public bool IsLethal;
}

public enum VitalType
{
    Health,
    Food,
    Water,
    Virus,
    Stamina
}

Zone processing

csharp
public class HazardZoneProcessor
{
    private readonly List<HazardZone> _zones;
    private readonly Dictionary<ulong, DateTime> _lastTick =
        new Dictionary<ulong, DateTime>();

    public HazardZoneProcessor(List<HazardZone> zones)
    {
        _zones = zones;
    }

    public void ProcessPlayer(UnturnedPlayer player)
    {
        if (player.Player.life.isDead)
            return;

        ulong steamId = player.CSteamID.m_SteamID;

        if (_lastTick.TryGetValue(steamId, out DateTime lastTick))
        {
            if ((DateTime.UtcNow - lastTick).TotalSeconds < 1.0)
                return;
        }

        _lastTick[steamId] = DateTime.UtcNow;

        foreach (HazardZone zone in _zones)
        {
            float distance = Vector3.Distance(
                new Vector3(player.Position.x, 0, player.Position.z),
                new Vector3(zone.Center.x, 0, zone.Center.z));

            if (distance > zone.Radius)
                continue;

            ApplyZoneEffect(player, zone);
        }
    }

    private void ApplyZoneEffect(UnturnedPlayer player, HazardZone zone)
    {
        switch (zone.AffectedVital)
        {
            case VitalType.Health:
                int newHealth = player.Health - zone.DamagePerSecond;
                if (newHealth <= 0)
                {
                    if (zone.IsLethal)
                    {
                        player.Health = 0;
                        player.Player.life.askDamage(
                            zone.DamagePerSecond,
                            Vector3.zero,
                            EDeathCause.PUNCH,
                            ELimb.SKULL,
                            CSteamID.Nil);
                    }
                    else
                    {
                        player.Health = 1;
                    }
                }
                else
                {
                    player.Health = (byte)newHealth;
                }
                break;

            case VitalType.Virus:
                player.Virus = (byte)Math.Min(100,
                    player.Virus + zone.DamagePerSecond);
                break;

            case VitalType.Food:
                player.Food = (byte)Math.Max(0,
                    player.Food - zone.DamagePerSecond);
                break;

            case VitalType.Water:
                player.Water = (byte)Math.Max(0,
                    player.Water - zone.DamagePerSecond);
                break;

            case VitalType.Stamina:
                player.Stamina = (byte)Math.Max(0,
                    player.Stamina - zone.DamagePerSecond);
                break;
        }
    }
}

Timer-driven zone processing

csharp
private HazardZoneProcessor _zoneProcessor;
private IEnumerator _zoneCoroutine;

protected override void Load()
{
    List<HazardZone> zones = new List<HazardZone>
    {
        new HazardZone
        {
            Name = "Radiation Spill",
            Center = new Vector3(256f, 0f, 256f),
            Radius = 50f,
            AffectedVital = VitalType.Virus,
            DamagePerSecond = 5,
            IsLethal = false
        },
        new HazardZone
        {
            Name = "Toxic Pit",
            Center = new Vector3(128f, 0f, 400f),
            Radius = 30f,
            AffectedVital = VitalType.Health,
            DamagePerSecond = 10,
            IsLethal = true
        }
    };

    _zoneProcessor = new HazardZoneProcessor(zones);
    _zoneCoroutine = StartZoneProcessing();
}

private IEnumerator StartZoneProcessing()
{
    while (true)
    {
        yield return new WaitForSeconds(1f);

        foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
        {
            _zoneProcessor.ProcessPlayer(player);
        }
    }
}

Building a medical system

The following code implements a medical system with configurable healing items, cooldowns, and effects.

Medical item configuration

csharp
public class MedicalItem
{
    public ushort ItemId;
    public string Name;
    public byte HealthRestore;
    public byte FoodRestore;
    public byte WaterRestore;
    public byte VirusReduce;
    public byte StaminaRestore;
    public bool CuresBleeding;
    public bool HealsBones;
    public int CooldownSeconds;
}

Medical item registry

csharp
public static class MedicalItemRegistry
{
    public static readonly Dictionary<ushort, MedicalItem> Items =
        new Dictionary<ushort, MedicalItem>
    {
        { 1, new MedicalItem // Placeholder IDs — replace with actual item IDs
            {
                ItemId = 1,
                Name = "Bandage",
                HealthRestore = 20,
                FoodRestore = 0,
                WaterRestore = 0,
                VirusReduce = 0,
                StaminaRestore = 0,
                CuresBleeding = true,
                HealsBones = false,
                CooldownSeconds = 10
            }
        },
        { 2, new MedicalItem
            {
                ItemId = 2,
                Name = "Medkit",
                HealthRestore = 100,
                FoodRestore = 0,
                WaterRestore = 0,
                VirusReduce = 0,
                StaminaRestore = 50,
                CuresBleeding = true,
                HealsBones = true,
                CooldownSeconds = 30
            }
        },
        { 3, new MedicalItem
            {
                ItemId = 3,
                Name = "Antidote",
                HealthRestore = 0,
                FoodRestore = 0,
                WaterRestore = 0,
                VirusReduce = 100,
                StaminaRestore = 0,
                CuresBleeding = false,
                HealsBones = false,
                CooldownSeconds = 15
            }
        },
        { 4, new MedicalItem
            {
                ItemId = 4,
                Name = "Canned Food",
                HealthRestore = 0,
                FoodRestore = 35,
                WaterRestore = 10,
                VirusReduce = 0,
                StaminaRestore = 0,
                CuresBleeding = false,
                HealsBones = false,
                CooldownSeconds = 5
            }
        },
        { 5, new MedicalItem
            {
                ItemId = 5,
                Name = "Canteen",
                HealthRestore = 0,
                FoodRestore = 0,
                WaterRestore = 40,
                VirusReduce = 0,
                StaminaRestore = 10,
                CuresBleeding = false,
                HealsBones = false,
                CooldownSeconds = 3
            }
        }
    };
}

Item-use detection

RocketMod intercepts item consumption through OnPlayerConsumeItem or by detecting changes in the player's inventory active item:

csharp
public void HandleItemUse(UnturnedPlayer player, ushort itemId)
{
    if (!MedicalItemRegistry.Items.TryGetValue(itemId, out MedicalItem item))
        return;

    if (!ApplyMedicalItem(player, item))
    {
        // Cooldown active or item could not be applied
    }
}

Medical application with cooldown

csharp
public class MedicalSystem
{
    private readonly Dictionary<ulong, Dictionary<ushort, DateTime>> _cooldowns =
        new Dictionary<ulong, Dictionary<ushort, DateTime>>();

    public bool ApplyMedicalItem(UnturnedPlayer player, MedicalItem item)
    {
        ulong steamId = player.CSteamID.m_SteamID;

        if (_cooldowns.ContainsKey(steamId)
            && _cooldowns[steamId].TryGetValue(item.ItemId, out DateTime lastUse))
        {
            double remaining = item.CooldownSeconds -
                (DateTime.UtcNow - lastUse).TotalSeconds;

            if (remaining > 0)
            {
                player.SendChat(
                    $"{item.Name} on cooldown: {remaining:F0}s remaining.",
                    Color.red);
                return false;
            }
        }

        // Apply vitals
        if (item.HealthRestore > 0)
        {
            player.Health = (byte)Math.Min(100,
                player.Health + item.HealthRestore);
        }

        if (item.FoodRestore > 0)
        {
            player.Food = (byte)Math.Min(100,
                player.Food + item.FoodRestore);
        }

        if (item.WaterRestore > 0)
        {
            player.Water = (byte)Math.Min(100,
                player.Water + item.WaterRestore);
        }

        if (item.VirusReduce > 0)
        {
            player.Virus = (byte)Math.Max(0,
                player.Virus - item.VirusReduce);
        }

        if (item.StaminaRestore > 0)
        {
            player.Stamina = (byte)Math.Min(100,
                player.Stamina + item.StaminaRestore);
        }

        // Track cooldown
        if (!_cooldowns.ContainsKey(steamId))
        {
            _cooldowns[steamId] = new Dictionary<ushort, DateTime>();
        }
        _cooldowns[steamId][item.ItemId] = DateTime.UtcNow;

        player.SendChat(
            $"Applied {item.Name}. " +
            $"+{item.HealthRestore} HP, " +
            $"+{item.FoodRestore} Food, " +
            $"+{item.WaterRestore} Water.",
            Color.green);

        return true;
    }
}

Natural vital drain modification

Unturned applies natural drain to food and water over time. RocketMod does not provide an event for natural vital drain ticks, but you can modify the rate or disable it entirely:

csharp
public class VitalDrainManager
{
    private readonly bool _disableNaturalDrain;

    public VitalDrainManager(bool disableNaturalDrain)
    {
        _disableNaturalDrain = disableNaturalDrain;
    }

    public IEnumerator OverrideDrain()
    {
        while (_disableNaturalDrain)
        {
            yield return new WaitForSeconds(1f);

            foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
            {
                if (player.Player.life.isDead) continue;

                // Replenish food and water to prevent natural drain
                if (player.Food < 100)
                {
                    player.Food = (byte)Math.Min(100, player.Food + 1);
                }

                if (player.Water < 100)
                {
                    player.Water = (byte)Math.Min(100, player.Water + 1);
                }
            }
        }
    }
}

Admin vitals commands

Heal command

csharp
public class HealCommand : IRocketCommand
{
    public string Name => "heal";
    public string Help => "Heals you or a target player to full health.";
    public string Syntax => "/heal [player]";
    public List<string> Aliases => new List<string> { "healme" };
    public List<string> Permissions => new List<string> { "vitals.heal" };
    public AllowedCaller AllowedCaller => AllowedCaller.Player;

    public void Execute(IRocketPlayer caller, string[] command)
    {
        UnturnedPlayer target;

        if (command.Length > 0)
        {
            target = UnturnedPlayer.FromName(command[0]);
            if (target == null)
            {
                UnturnedChat.Say(caller, "Player not found.");
                return;
            }
        }
        else
        {
            target = (UnturnedPlayer)caller;
        }

        target.Health = 100;
        target.Food = 100;
        target.Water = 100;
        target.Virus = 0;
        target.Stamina = 100;

        target.SendChat("You have been fully healed.", Color.green);
        UnturnedChat.Say(caller,
            $"Healed {target.DisplayName} to full vitals.");
    }
}

Damage command

csharp
public class DamageCommand : IRocketCommand
{
    public string Name => "damage";
    public string Help => "Applies damage to a player.";
    public string Syntax => "/damage <player> <amount>";
    public List<string> Aliases => new List<string>();
    public List<string> Permissions => new List<string> { "vitals.damage" };
    public AllowedCaller AllowedCaller => AllowedCaller.Console;

    public void Execute(IRocketPlayer caller, string[] command)
    {
        if (command.Length < 2)
        {
            UnturnedChat.Say(caller, "Usage: /damage <player> <amount>");
            return;
        }

        UnturnedPlayer target = UnturnedPlayer.FromName(command[0]);
        if (target == null)
        {
            UnturnedChat.Say(caller, "Player not found.");
            return;
        }

        if (!byte.TryParse(command[1], out byte amount))
        {
            UnturnedChat.Say(caller, "Invalid damage amount.");
            return;
        }

        int newHealth = target.Health - amount;

        if (newHealth <= 0)
        {
            target.Health = 0;
            target.Player.life.askDamage(
                amount,
                Vector3.zero,
                EDeathCause.PUNCH,
                ELimb.SKULL,
                CSteamID.Nil);
            UnturnedChat.Say(caller,
                $"{target.DisplayName} was killed with {amount} damage.");
        }
        else
        {
            target.Health = (byte)newHealth;
            UnturnedChat.Say(caller,
                $"{target.DisplayName} took {amount} damage. Health: {target.Health}.");
        }
    }
}

Vitals monitoring dashboard

csharp
public class VitalsMonitor
{
    private readonly Dictionary<ulong, VitalSnapshot> _snapshots =
        new Dictionary<ulong, VitalSnapshot>();

    public class VitalSnapshot
    {
        public byte Health;
        public byte Food;
        public byte Water;
        public byte Virus;
        public byte Stamina;
    }

    public VitalSnapshot TakeSnapshot(UnturnedPlayer player)
    {
        var snapshot = new VitalSnapshot
        {
            Health = player.Health,
            Food = player.Food,
            Water = player.Water,
            Virus = player.Virus,
            Stamina = player.Stamina
        };

        _snapshots[player.CSteamID.m_SteamID] = snapshot;
        return snapshot;
    }

    public string GenerateReport(UnturnedPlayer player)
    {
        VitalSnapshot current = TakeSnapshot(player);

        return $"=== Vitals Report: {player.DisplayName} ===\n" +
               $"Health:  {current.Health}/100\n" +
               $"Food:    {current.Food}/100\n" +
               $"Water:   {current.Water}/100\n" +
               $"Virus:   {current.Virus}/100\n" +
               $"Stamina: {current.Stamina}/100\n" +
               $"Status:  {(player.Player.life.isDead ? "DEAD" : "ALIVE")}";
    }
}

Common pitfalls

Assuming OnPlayerUpdateHealth fires on every single HP change

The OnPlayerUpdateHealth event fires every time the player's health changes by any amount. Each healing tick, each damage application, and each natural regeneration tick triggers the event. This makes it suitable for reactive health monitoring but also means the handler must be efficient to avoid lag during rapid damage events like zombie swarms or fall damage.

Not checking player life state

Modifying vitals on a dead player can cause unexpected behavior. The player object may still exist, but the vitals may not be in a consistent state:

csharp
// WRONG — modifies vitals on a dead player
player.Health = 100;

// CORRECT — check before modifying
if (player.Player != null && !player.Player.life.isDead)
{
    player.Health = 100;
}

Setting vitals to values outside 0-100

All vitals are byte properties. Setting a value below 0 wraps to 255 due to C# integer underflow. The game engine clamps internally, but the intermediate state can cause unexpected behavior:

csharp
// WRONG — underflows to 255 before engine clamps
player.Food = (byte)(player.Food - 200);

// CORRECT — clamp before assignment
int newFood = Math.Max(0, player.Food - 200);
player.Food = (byte)Math.Min(100, newFood);

Health event handler not firing for environmental damage

Environmental damage (starvation, dehydration, radiation poisoning) fires OnPlayerUpdateHealth with each damage tick, at the same granularity as weapon damage. The event is not batched or throttled for environmental sources.

Equating stamina regeneration with the stamina event

RocketMod does not have a dedicated OnPlayerUpdateStamina event. Track stamina changes by polling on a timer:

csharp
private IEnumerator MonitorStamina()
{
    while (true)
    {
        yield return new WaitForSeconds(2f);

        foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
        {
            byte currentStamina = player.Stamina;

            if (currentStamina < 30)
            {
                player.SendChat("You are exhausted. Rest to recover stamina.", Color.yellow);
            }
        }
    }
}

Not accounting for skill-modified vitals

The player's skills affect vital behavior. A player with max Vitality skill heals faster naturally. A player with max Immunization takes longer to reach 100 virus. A plugin that assumes fixed vital rates will behave differently across players with different skill levels.

Frequently asked questions

Does OnPlayerUpdateHealth fire for every health change?

Yes. The event fires on every health change, regardless of the amount. A 50-point gunshot and a 1-point starvation tick both trigger the event. The handler receives the new health value after the change has been applied.

How do I detect food and water changes?

RocketMod does not provide dedicated food or water change events. Poll food and water values on a timer and compare against a stored snapshot to detect changes.

Can I prevent health damage from a specific source?

RocketMod does not provide a pre-damage event that can cancel incoming damage. To block damage from a specific source, use a coroutine that refills health immediately after the damage is applied, or use the PlayerDamage event if available in your RocketMod version.

What happens if I set health to 0?

Setting player.Health = 0 does not automatically trigger the death sequence. You must also call player.Player.life.askDamage() to trigger the death animation, ragdoll, and respawn flow. Setting health to 0 alone leaves the player alive but with zero health — they will die on the next damage tick.

How do I make a player invulnerable?

csharp
// In a coroutine or timer
if (player.Health < 100 && isGodMode)
{
    player.Health = 100;
}

This refills health every frame, effectively making the player invulnerable. For a proper god mode implementation, see the Player God Mode and Vanish Mode article.

Why does health sometimes skip values?

Unturned batches damage in certain situations (explosions, environmental damage). The health event fires with the final health value after the batch, not for each individual damage tick within the batch.

Can a player's max health be changed?

The max health is hardcoded at 100 in the Unturned engine. RocketMod does not provide a way to modify maximum vital values. Workarounds involve scaling: treat 100 as a percentage and map plugin-internal health pools on top.

Cross-references

Document history

VersionDateAuthorNotes
1.02025-06-1857 StudiosInitial publication. Vitals API, OnPlayerUpdateHealth, damage systems, hazard zones, medical system, admin commands, monitoring, common pitfalls.