Skip to content

Player Bleeding and Broken Bones

Unturned's injury system adds two persistent status effects on top of the core health system: bleeding and broken bones. These states modify gameplay by draining health over time (bleeding) and restricting movement and action (broken bones). For roleplay servers, medical RP plugins, hardcore survival configurations, and realistic damage systems, tracking and managing these states is essential.

This article covers the RocketMod bleeding and broken bones API in depth. It explains how to detect bleeding state changes through OnPlayerUpdateBleeding, how to read and modify the broken bones state, how to build a medical treatment system that cures both conditions, and how to implement a complete injury-over-time system for RP servers.

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 patterns.
  • Completion of Player Vitals or equivalent knowledge of the vitals system.
  • Understanding of the Unturned injury system from a player perspective.

What you'll learn

  • How bleeding works in Unturned: health drain rate, causes, and natural recovery.
  • How broken bones work in Unturned: movement restrictions, causes, and recovery time.
  • How to subscribe to OnPlayerUpdateBleeding and what the ool parameter represents.
  • How to read and set the bleeding state programmatically.
  • How to read and set the broken bones state programmatically.
  • How to build a medical treatment plugin with bandages, splints, and medkits.
  • How to implement an injury-over-time system for RP immersion.
  • How to build a triage command for admin medical intervention.
  • Common pitfalls: event frequency assumptions, state change detection, and treatment stacking.

Bleeding system

How bleeding works

When a player takes damage from certain sources (gunshots, melee attacks, explosions, fall damage), the engine may apply a bleeding state. While bleeding:

  • The player loses 1 health approximately every 3 seconds.
  • The bleeding state persists until cured or until the player dies.
  • The player cannot recover health through natural regeneration while bleeding.
  • The bleeding state is cleared on death and respawn.

Causes of bleeding

Damage sourceBleeding chanceBleed duration
Gunshot (head)100%Until cured
Gunshot (body)70%Until cured
Gunshot (limb)50%Until cured
Melee weapon40%Until cured
Explosion60%Until cured
Fall damage20%Until cured
Zombie attack30%Until cured
Animal attack25%Until cured
Starvation/dehydration0%Never causes bleeding
Radiation0%Never causes bleeding

OnPlayerUpdateBleeding

Event signature

csharp public static event PlayerUpdateBleeding OnPlayerUpdateBleeding; public delegate void PlayerUpdateBleeding(UnturnedPlayer player, bool isBleeding);

The handler receives the player whose bleeding state changed and a ool indicating whether the player is currently bleeding.

Subscription pattern

`csharp using Rocket.Unturned.Events; using Rocket.Unturned.Player;

protected override void Load() { UnturnedPlayerEvents.OnPlayerUpdateBleeding += HandleBleedingChanged; }

protected override void Unload() { UnturnedPlayerEvents.OnPlayerUpdateBleeding -= HandleBleedingChanged; }

private void HandleBleedingChanged(UnturnedPlayer player, bool isBleeding) { Logger.Log($"{player.DisplayName} bleeding state: {isBleeding}"); } `

When the event fires

The OnPlayerUpdateBleeding event fires whenever the player's bleeding state transitions — from not bleeding to bleeding, or from bleeding to not bleeding. The event is also fired on every health update tick while the player maintains the same bleeding state, providing continuous status information. This means if a player is bleeding and takes health damage from the bleeding effect, the event fires again with the same rue value, confirming that the bleeding state persists.

Setting the broken bones state

`csharp // Apply broken bones player.Broken = true;

// Cure broken bones player.Broken = false; `

Bone break event

RocketMod does not provide a dedicated broken-bones event equivalent to OnPlayerUpdateBleeding. To detect when a player's bones break or heal, poll the Broken property:

`csharp private readonly Dictionary<ulong, bool> _lastBrokenState = new Dictionary<ulong, bool>();

public void CheckBoneState(UnturnedPlayer player) { ulong steamId = player.CSteamID.m_SteamID; bool current = player.Broken;

if (_lastBrokenState.TryGetValue(steamId, out bool previous))
{
    if (current && !previous)
    {
        player.SendChat("You have broken bones! Movement is restricted.", Color.red);
    }
    else if (!current && previous)
    {
        player.SendChat("Your bones have healed.", Color.green);
    }
}

_lastBrokenState[steamId] = current;

} `

Polling loop for bone state

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

    foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
    {
        CheckBoneState(player);
    }
}

} `

Building a medical treatment plugin

The following plugin implements a complete medical treatment system that handles bleeding and broken bones through configurable items.

Medical treatment configuration

csharp
public class MedicalConfig : IRocketPluginConfiguration
{
    public int BandageBleedStopChance = 100;
    public int SplintBoneHealChance = 100;
    public int MedkitHealAmount = 50;
    public bool MedkitCuresBleeding = true;
    public bool MedkitHealsBones = true;
    public int BleedDamagePerTick = 1;
    public float BleedTickInterval = 3f;
    public bool AnnounceBleedTicks = true;

    public void LoadDefaults() { }
}

Medical plugin

csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace MedicalSystem
{
    public class MedicalPlugin : RocketPlugin<MedicalConfig>
    {
        public static MedicalPlugin Instance { get; private set; }

        private readonly HashSet<ulong> _bleedingPlayers =
            new HashSet<ulong>();
        private readonly Dictionary<ulong, DateTime> _lastBleedTick =
            new Dictionary<ulong, DateTime>();

        protected override void Load()
        {
            Instance = this;

            UnturnedPlayerEvents.OnPlayerUpdateBleeding += HandleBleedingChanged;
            UnturnedPlayerEvents.OnPlayerUpdateHealth += HandleHealthChanged;
            StartCoroutine(BleedTickCoroutine());

            Logger.Log("[MedicalPlugin] Loaded.");
        }

        protected override void Unload()
        {
            UnturnedPlayerEvents.OnPlayerUpdateBleeding -= HandleBleedingChanged;
            UnturnedPlayerEvents.OnPlayerUpdateHealth -= HandleHealthChanged;
            StopAllCoroutines();

            Instance = null;
            Logger.Log("[MedicalPlugin] Unloaded.");
        }

        private void HandleBleedingChanged(UnturnedPlayer player, bool isBleeding)
        {
            ulong steamId = player.CSteamID.m_SteamID;

            if (isBleeding)
            {
                _bleedingPlayers.Add(steamId);
                player.SendChat("You are bleeding! Apply a bandage.", Color.red);
            }
            else
            {
                _bleedingPlayers.Remove(steamId);
                player.SendChat("Bleeding stopped.", Color.green);
            }
        }

        private void HandleHealthChanged(UnturnedPlayer player, byte health)
        {
            // Track health changes in tandem with bleeding
            ulong steamId = player.CSteamID.m_SteamID;

            if (_bleedingPlayers.Contains(steamId) && health <= 10)
            {
                player.SendChat(
                    "CRITICAL: Your health is dangerously low from blood loss!",
                    Color.red);
            }
        }

        private IEnumerator BleedTickCoroutine()
        {
            while (true)
            {
                yield return new WaitForSeconds(
                    Configuration.Instance.BleedTickInterval);

                foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
                {
                    ulong steamId = player.CSteamID.m_SteamID;

                    if (_bleedingPlayers.Contains(steamId)
                        && !player.Player.life.isDead)
                    {
                        if (Configuration.Instance.AnnounceBleedTicks)
                        {
                            player.SendChat(
                                $"Bleeding: {Configuration.Instance.BleedDamagePerTick} HP lost.",
                                Color.red);
                        }
                    }
                }
            }
        }

        public void CureBleeding(UnturnedPlayer player)
        {
            player.Bleeding = false;
            _bleedingPlayers.Remove(player.CSteamID.m_SteamID);
            player.SendChat("Bleeding has been treated.", Color.green);
        }

        public void HealBones(UnturnedPlayer player)
        {
            player.Broken = false;
            player.SendChat("Your bones have been set and healed.", Color.green);
        }
    }
}

Treat command

csharp
public class TreatCommand : IRocketCommand
{
    public string Name => "treat";
    public string Help => "Treats bleeding or broken bones on a player.";
    public string Syntax => "/treat <player> <bleed|bones|both>";
    public List<string> Aliases => new List<string>();
    public List<string> Permissions => new List<string> { "medical.treat" };
    public AllowedCaller AllowedCaller => AllowedCaller.Console;

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

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

        string treatment = command[1].ToLower();
        MedicalPlugin plugin = MedicalPlugin.Instance;

        switch (treatment)
        {
            case "bleed":
                plugin.CureBleeding(target);
                UnturnedChat.Say(caller, string.Format("Cured bleeding on {0}.", target.DisplayName));
                break;

            case "bones":
                plugin.HealBones(target);
                UnturnedChat.Say(caller, string.Format("Healed bones on {0}.", target.DisplayName));
                break;

            case "both":
                plugin.CureBleeding(target);
                plugin.HealBones(target);
                UnturnedChat.Say(caller, string.Format("Cured bleeding and healed bones on {0}.", target.DisplayName));
                break;

            default:
                UnturnedChat.Say(caller, "Unknown treatment. Use: bleed, bones, or both.");
                break;
        }
    }
}

Injury-over-time system for RP immersion

For roleplay servers, tracking injury duration and applying progressive penalties creates a more immersive medical experience. The following system tracks how long a player has been injured and applies escalating effects.

Injury tracker

csharp
public class InjuryTracker
{
    private readonly Dictionary<ulong, InjuryRecord> _injuries =
        new Dictionary<ulong, InjuryRecord>();

    public class InjuryRecord
    {
        public DateTime BleedingStarted;
        public DateTime BonesBrokenStarted;
        public int TotalBleedTicks;
    }

    public void RecordBleedingStart(UnturnedPlayer player)
    {
        ulong steamId = player.CSteamID.m_SteamID;

        if (!_injuries.ContainsKey(steamId))
        {
            _injuries[steamId] = new InjuryRecord();
        }

        _injuries[steamId].BleedingStarted = DateTime.UtcNow;
    }

    public void RecordBleedingEnd(UnturnedPlayer player)
    {
        ulong steamId = player.CSteamID.m_SteamID;

        if (_injuries.TryGetValue(steamId, out InjuryRecord record))
        {
            record.BleedingStarted = DateTime.MinValue;
        }
    }

    public double GetBleedingDuration(UnturnedPlayer player)
    {
        ulong steamId = player.CSteamID.m_SteamID;

        if (_injuries.TryGetValue(steamId, out InjuryRecord record)
            && record.BleedingStarted != DateTime.MinValue)
        {
            return (DateTime.UtcNow - record.BleedingStarted).TotalSeconds;
        }

        return 0;
    }

    public string GetInjuryReport(UnturnedPlayer player)
    {
        ulong steamId = player.CSteamID.m_SteamID;

        if (!_injuries.TryGetValue(steamId, out InjuryRecord record))
        {
            return "No injuries on record.";
        }

        double bleedingDuration = record.BleedingStarted != DateTime.MinValue ? (DateTime.UtcNow - record.BleedingStarted).TotalSeconds : 0;
        double boneDuration = record.BonesBrokenStarted != DateTime.MinValue ? (DateTime.UtcNow - record.BonesBrokenStarted).TotalSeconds : 0;

        return string.Format("Bleeding duration: {0:F0}s | Bone break duration: {1:F0}s | Total bleed ticks: {2}", bleedingDuration, boneDuration, record.TotalBleedTicks);
    }
}

Progressive bleeding effects

csharp
public class ProgressiveInjurySystem
{
    private readonly InjuryTracker _tracker = new InjuryTracker();

    public void OnBleedingTick(UnturnedPlayer player)
    {
        ulong steamId = player.CSteamID.m_SteamID;
        double duration = _tracker.GetBleedingDuration(player);

        // Escalating effects based on bleeding duration
        if (duration > 120) // 2+ minutes
        {
            player.SendChat("You are growing weak from blood loss. Your vision is darkening.", Color.red);
            player.Effect(50); // Visual distortion effect
        }
        else if (duration > 60) // 1+ minute
        {
            player.SendChat("Blood loss is taking its toll. You feel lightheaded.", Color.yellow);
        }
        else if (duration > 30)
        {
            player.SendChat("You are still bleeding. Find medical supplies.", Color.yellow);
        }
    }
}

Admin triage command

csharp
public class TriageCommand : IRocketCommand
{
    public string Name => "triage";
    public string Help => "Shows bleeding and bone status for all players.";
    public string Syntax => "/triage";
    public List<string> Aliases => new List<string>();
    public List<string> Permissions => new List<string> { "medical.triage" };
    public AllowedCaller AllowedCaller => AllowedCaller.Console;

    public void Execute(IRocketPlayer caller, string[] command)
    {
        UnturnedChat.Say(caller, "=== TRIAGE REPORT ===");

        foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
        {
            string bleedStatus = player.Bleeding ? "BLEEDING" : "stable";
            string boneStatus = player.Broken ? "BROKEN BONES" : "intact";
            string healthStatus = string.Format("{0}/100", player.Health);

            UnturnedChat.Say(caller, string.Format(
                "{0}: HP {1} | {2} | {3}",
                player.DisplayName,
                healthStatus,
                bleedStatus,
                boneStatus));
        }

        UnturnedChat.Say(caller, "=== END REPORT ===");
    }
}

Common pitfalls

Assuming OnPlayerUpdateBleeding fires only on state change

The OnPlayerUpdateBleeding event fires whenever the player's bleeding state transitions (isBleeding changes from true to false or false to true). It also fires on every health update tick while the player maintains the same bleeding state, providing continuous status information. If a player remains bleeding for 30 seconds, the event fires multiple times with isBleeding = true across those 30 seconds. Handle this by tracking the previous state:

csharp
private void HandleBleedingChanged(UnturnedPlayer player, bool isBleeding)
{
    ulong steamId = player.CSteamID.m_SteamID;

    if (isBleeding && !_currentlyBleeding.Contains(steamId))
    {
        // Transition to bleeding — run once
    }
    else if (!isBleeding && _currentlyBleeding.Contains(steamId))
    {
        // Transition to not bleeding — run once
    }
    // Same-state firings are ignored by the state tracker
}

Not resetting state on death and respawn

The bleeding and broken bones states are cleared by the engine on death, but your plugin's internal tracking dictionaries are not automatically cleared. Always clear tracking state when a player dies:

csharp
private void HandlePlayerDeath(UnturnedPlayer player, EDeathCause cause, ELimb limb, ulong killer)
{
    ulong steamId = player.CSteamID.m_SteamID;
    _currentlyBleeding.Remove(steamId);
    _lastBrokenState.Remove(steamId);
}

Setting Broken while the player is dead

Modifying the Broken property on a dead player can cause the state to persist incorrectly through the respawn cycle. Check life state before setting:

csharp
if (!player.Player.life.isDead)
{
    player.Broken = false;
}

Confusing Bleeding and isBleeding

The UnturnedPlayer.Bleeding property is a settable bool. The event's isBleeding parameter reflects the state at the time the event fires. Modifying player.Bleeding inside the OnPlayerUpdateBleeding handler can cause re-entrant event firing:

csharp
// DANGEROUS — setting Bleeding inside the bleeding event handler
private void HandleBleedingChanged(UnturnedPlayer player, bool isBleeding)
{
    if (isBleeding)
    {
        player.Bleeding = false; // Triggers another OnPlayerUpdateBleeding
    }
}

// SAFER — use a flag or defer the change
private bool _isProcessingBleeding;

private void HandleBleedingChanged(UnturnedPlayer player, bool isBleeding)
{
    if (_isProcessingBleeding) return;
    _isProcessingBleeding = true;

    if (isBleeding)
    {
        // Schedule cure on next frame
        StartCoroutine(DelayedCure(player));
    }

    _isProcessingBleeding = false;
}

Polling broken bones too frequently

The Broken property read is cheap, but polling it every frame for every online player is unnecessary. A 1-2 second polling interval is sufficient to detect bone state changes because bones cannot heal faster than 60 seconds of game time.

Frequently asked questions

How do I detect when a player starts bleeding?

Subscribe to OnPlayerUpdateBleeding and track state transitions with a HashSet<ulong> of currently bleeding players. The event fires with isBleeding = true when bleeding starts.

Why does OnPlayerUpdateBleeding fire repeatedly for the same bleeding state?

The event fires on every health update tick while the player maintains the same bleeding state. This provides continuous status information for systems that need periodic updates on active bleeding.

Can I cure bleeding immediately when it starts?

Yes. Set player.Bleeding = false inside the OnPlayerUpdateBleeding handler. However, be aware that setting the property inside the handler can cause re-entrant event firing. Use a one-shot flag to prevent infinite loops.

How do I detect broken bones healing naturally?

The engine heals broken bones after approximately 60 seconds of in-game time. Poll the player.Broken property on a 1-2 second timer and compare against the previous state to detect the transition.

Can bleeding kill a player?

Yes. Bleeding drains 1 HP every 3 seconds. A player at full health will die from blood loss alone in approximately 5 minutes (100 HP / (1 HP per 3 seconds) = 300 seconds). Combined with other damage sources, bleeding accelerates death significantly.

Is there a visual indicator for broken bones?

Unturned displays a screen-crack overlay effect when a player has broken bones. This effect is applied by the engine and cannot be controlled through the RocketMod API.

Do bleeding and broken bones persist across server restarts?

No. Both states are held in memory only. On server restart, all players respawn with no active status effects. To persist injury state across restarts, save the bleeding/broken state in a player data file during OnPlayerDisconnected and restore it during OnPlayerConnected.

Full event reference

EventClassSignatureFires
OnPlayerUpdateBleedingUnturnedPlayerEventsvoid(UnturnedPlayer, bool)Bleeding state transitions and on health update ticks
OnPlayerUpdateHealthUnturnedPlayerEventsvoid(UnturnedPlayer, byte)Any health change
OnPlayerDeathUnturnedPlayerEventsvoid(UnturnedPlayer, EDeathCause, ELimb, ulong)Player dies

Cross-references

Document history

VersionDateAuthorNotes
1.02025-06-1857 StudiosInitial publication. Bleeding system, OnPlayerUpdateBleeding, broken bones API, medical treatment plugin, injury-over-time system, triage command, common pitfalls.