Player Stats and Experience
The Unturned skill and experience system determines how effectively a player fights, moves, crafts, and survives. Each player has a set of skills with levels from 0 to 7, alongside an experience point pool that can be spent to increase those levels. Plugin developers who build progression systems, class-based loadouts, skill respec commands, or XP reward mechanics need to understand how RocketMod exposes the skill API.
This article covers the RocketMod skill and experience API in depth. It explains the skill object model, the GetSkillLevel and SetSkill methods, the UnturnedSkill enum, the event that fires when experience changes, and how to build a complete XP reward and skill respec system.
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 skill system from a player perspective.
What you'll learn
- The skill object model in Unturned and how RocketMod exposes it through
UnturnedPlayer.Skills. - How to read a player's skill level using
GetSkillLevel()with both theUnturnedSkillenum and the skill name. - How to set a player's skill level using
SetSkill(). - The
OnPlayerUpdateExperienceevent and how to detect experience changes. - How to build an XP reward plugin that grants experience for kills, crafting, and exploration.
- How to build a skill respec command that refunds XP to the player.
- How to implement a class-based loadout system that applies skill presets on player join.
- Common pitfalls: nonexistent method signatures, enum vs. string parameter confusion, skill index edges.
Skill object model
Skill categories
Unturned organizes skills into three categories:
| Category | Skills | Effect |
|---|---|---|
| Overkill | Overkill, Sharpshooter, Dexterous, Parkour, Heavy, Endurance | Combat and movement bonuses |
| Support | Warmblooded, Survival, Crafting, Outdoors, Cooking, Firefighting | Survival and crafting bonuses |
| Defense | Immunization, Toughness, Strength, Vitality, Cardio, Diving | Damage resistance and stamina bonuses |
Each skill has a name, an index within its category, and a level from 0 to 7. The level determines the magnitude of the skill's effect.
Skill data access
RocketMod exposes skills through the UnturnedPlayer.Skills property, which is an IList<Skill> where Skill is RocketMod's wrapper around the underlying SDG.Unturned.Skill.
csharp
UnturnedPlayer player = UnturnedPlayer.FromName("Butter");
IList<Skill> skills = player.Skills;Each Skill object exposes:
| Property | Type | Purpose |
|---|---|---|
Name | string | Display name of the skill |
Level | byte | Current level (0-7) |
MaxLevel | byte | Maximum level (always 7) |
Experience | uint | XP cost to reach current level |
Unlevels | byte | Number of times this skill has been unlearned |
The UnturnedSkill enum
RocketMod defines the UnturnedSkill enum to map skill indices to readable names:
csharp
public enum UnturnedSkill
{
Overkill,
Sharpshooter,
Dexterous,
Parkour,
Heavy,
Endurance,
Warmblooded,
Survival,
Crafting,
Outdoors,
Cooking,
Firefighting,
Immunization,
Toughness,
Strength,
Vitality,
Cardio,
Diving
}The enum values correspond to the order of skills in the Unturned skill menu, starting from the top-left of the Overkill category and proceeding left-to-right, top-to-bottom through all three categories.
GetSkillLevel
RocketMod exposes GetSkillLevel() on the UnturnedPlayer object for reading a skill's current level.
Correct usage with UnturnedSkill enum
csharp
byte level = player.GetSkillLevel(UnturnedSkill.Overkill);The method accepts a single parameter — the UnturnedSkill enum value — and returns a byte from 0 to 7.
Usage with skill name string
GetSkillLevel() also accepts a string parameter representing the skill name:
csharp
byte level = player.GetSkillLevel("Overkill");This compiles and runs at runtime. The string is matched against the skill name in the Unturned skill data, and the level is returned. If the string does not match any skill name, the method returns 0.
Complete skill readout
csharp
private void LogPlayerSkills(UnturnedPlayer player)
{
Rocket.Core.Logging.Logger.Log($"=== Skills for {player.DisplayName} ===");
foreach (UnturnedSkill skill in Enum.GetValues(typeof(UnturnedSkill)))
{
byte level = player.GetSkillLevel(skill);
Rocket.Core.Logging.Logger.Log($"{skill}: Level {level}");
}
}SetSkill
RocketMod exposes SetSkill() on the UnturnedPlayer object for setting a skill's level programmatically.
Method signature
csharp
public void SetSkill(UnturnedSkill skill, byte level)Parameters
| Parameter | Type | Range | Purpose |
|---|---|---|---|
skill | UnturnedSkill | Any enum value | The skill to modify |
level | byte | 0-7 | The level to set |
Setting a single skill
csharp
player.SetSkill(UnturnedSkill.Overkill, 7);Setting multiple skills (preset)
csharp
public void ApplyCombatPreset(UnturnedPlayer player)
{
player.SetSkill(UnturnedSkill.Overkill, 7);
player.SetSkill(UnturnedSkill.Sharpshooter, 7);
player.SetSkill(UnturnedSkill.Dexterous, 5);
player.SetSkill(UnturnedSkill.Endurance, 5);
}Resetting all skills to 0
csharp
public void ResetAllSkills(UnturnedPlayer player)
{
foreach (UnturnedSkill skill in Enum.GetValues(typeof(UnturnedSkill)))
{
player.SetSkill(skill, 0);
}
}Experience cost and level validation
Unturned charges experience points when a player levels up a skill in the normal skill menu. When setting a skill level programmatically, you are responsible for deducting the appropriate XP cost if you want the skill change to respect the game's economy:
csharp
public bool TrySetSkillWithCost(UnturnedPlayer player, UnturnedSkill skill, byte targetLevel)
{
byte currentLevel = player.GetSkillLevel(skill);
if (targetLevel <= currentLevel)
{
return false;
}
uint cost = CalculateSkillCost(currentLevel, targetLevel);
if (player.Experience < cost)
{
return false;
}
player.Experience -= cost;
player.SetSkill(skill, targetLevel);
return true;
}
private uint CalculateSkillCost(byte fromLevel, byte toLevel)
{
uint totalCost = 0;
for (byte level = fromLevel; level < toLevel; level++)
{
totalCost += (uint)(Math.Pow(level + 1, 2) * 5);
}
return totalCost;
}OnPlayerUpdateExperience
Event signature
csharp
public static event PlayerUpdateExperience OnPlayerUpdateExperience;
public delegate void PlayerUpdateExperience(UnturnedPlayer player, uint experience);Subscription
csharp
protected override void Load()
{
UnturnedPlayerEvents.OnPlayerUpdateExperience += HandleExperienceChanged;
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerUpdateExperience -= HandleExperienceChanged;
}
private void HandleExperienceChanged(UnturnedPlayer player, uint experience)
{
Logger.Log($"{player.DisplayName} experience: {experience}");
}Detecting experience gain or loss
csharp
private readonly Dictionary<ulong, uint> _lastExperience =
new Dictionary<ulong, uint>();
private void HandleExperienceChanged(UnturnedPlayer player, uint experience)
{
ulong steamId = player.CSteamID.m_SteamID;
if (_lastExperience.TryGetValue(steamId, out uint previous))
{
if (experience > previous)
{
uint gained = experience - previous;
Logger.Log($"{player.DisplayName} gained {gained} XP.");
}
else if (experience < previous)
{
uint lost = previous - experience;
Logger.Log($"{player.DisplayName} lost {lost} XP.");
}
}
_lastExperience[steamId] = experience;
}Experience notification
csharp
private void HandleExperienceChanged(UnturnedPlayer player, uint experience)
{
ulong steamId = player.CSteamID.m_SteamID;
if (_lastExperience.TryGetValue(steamId, out uint previous))
{
if (experience > previous)
{
uint gained = experience - previous;
player.SendChat($"+{gained} XP (Total: {experience})", Color.yellow);
}
}
_lastExperience[steamId] = experience;
}Building an XP reward plugin
The following plugin rewards players with experience points for various in-game actions and provides commands to inspect skills and respec.
Complete 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 XpRewards
{
public class XpRewardsConfiguration : IRocketPluginConfiguration
{
public uint KillReward = 50;
public uint ZombieKillReward = 10;
public uint CraftingReward = 5;
public uint ExplorationReward = 25;
public uint RespecCost = 500;
public void LoadDefaults() { }
}
public class XpRewardsPlugin : RocketPlugin<XpRewardsConfiguration>
{
public static XpRewardsPlugin Instance { get; private set; }
private readonly HashSet<ulong> _exploredZones = new HashSet<ulong>();
private readonly Dictionary<string, uint> _zoneXpRewards =
new Dictionary<string, uint>();
protected override void Load()
{
Instance = this;
UnturnedPlayerEvents.OnPlayerUpdateExperience += HandleExperienceChanged;
UnturnedPlayerEvents.OnPlayerDeath += HandlePlayerDeath;
InitializeZoneRewards();
Logger.Log("[XpRewards] Loaded.");
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerUpdateExperience -= HandleExperienceChanged;
UnturnedPlayerEvents.OnPlayerDeath -= HandlePlayerDeath;
Instance = null;
Logger.Log("[XpRewards] Unloaded.");
}
private void InitializeZoneRewards()
{
_zoneXpRewards["city"] = 25;
_zoneXpRewards["military"] = 50;
_zoneXpRewards["airport"] = 35;
}
private void HandleExperienceChanged(UnturnedPlayer player, uint experience)
{
Logger.Log($"[XpRewards] {player.DisplayName} XP: {experience}");
if (experience >= 1000)
{
// Track milestone achievements
}
}
private void HandlePlayerDeath(UnturnedPlayer player, EDeathCause cause, ELimb limb, ulong killer)
{
if (cause == EDeathCause.GUN || cause == EDeathCause.MELEE)
{
UnturnedPlayer killerPlayer = UnturnedPlayer.FromCSteamID(
new CSteamID(killer));
if (killerPlayer != null && killerPlayer != player)
{
GrantExperience(killerPlayer, Configuration.Instance.KillReward);
}
}
}
public void GrantExperience(UnturnedPlayer player, uint amount)
{
if (amount == 0) return;
player.Experience += amount;
player.SendChat($"+{amount} XP", Color.yellow);
}
}
public class SkillsCommand : IRocketCommand
{
public string Name => "skills";
public string Help => "Displays your skill levels.";
public string Syntax => "/skills";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string>();
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
player.SendChat("=== Your Skills ===", Color.cyan);
foreach (UnturnedSkill skill in Enum.GetValues(typeof(UnturnedSkill)))
{
byte level = player.GetSkillLevel(skill);
player.SendChat($"{skill}: Level {level}/7",
level == 7 ? Color.green : Color.white);
}
player.SendChat($"Experience: {player.Experience}", Color.yellow);
}
}
public class RespecCommand : IRocketCommand
{
public string Name => "respec";
public string Help => "Resets all skills and refunds experience.";
public string Syntax => "/respec";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "xprewards.respec" };
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
uint cost = XpRewardsPlugin.Instance.Configuration.Instance.RespecCost;
if (player.Experience < cost)
{
player.SendChat(
$"Respec costs {cost} XP. You have {player.Experience}.",
Color.red);
return;
}
player.Experience -= cost;
uint refund = 0;
foreach (UnturnedSkill skill in Enum.GetValues(typeof(UnturnedSkill)))
{
byte level = player.GetSkillLevel(skill);
refund += CalculateRefundForLevel(level);
player.SetSkill(skill, 0);
}
player.Experience += refund;
player.SendChat(
$"Skills reset. Refunded {refund} XP. " +
$"Total XP: {player.Experience}.",
Color.green);
}
private uint CalculateRefundForLevel(byte level)
{
uint total = 0;
for (byte i = 0; i < level; i++)
{
total += (uint)(Math.Pow(i + 1, 2) * 5);
}
return total;
}
}
}Building a class-based loadout system
A class-based loadout system applies predetermined skill presets when a player joins the server or uses a class command.
Class definitions
csharp
public class PlayerClass
{
public string Name;
public Dictionary<UnturnedSkill, byte> SkillPreset;
public uint Cost;
}
public static class ClassRegistry
{
public static readonly Dictionary<string, PlayerClass> Classes =
new Dictionary<string, PlayerClass>
{
{
"assault", new PlayerClass
{
Name = "Assault",
SkillPreset = new Dictionary<UnturnedSkill, byte>
{
{ UnturnedSkill.Overkill, 7 },
{ UnturnedSkill.Sharpshooter, 5 },
{ UnturnedSkill.Endurance, 5 },
{ UnturnedSkill.Dexterous, 5 }
},
Cost = 500
}
},
{
"medic", new PlayerClass
{
Name = "Medic",
SkillPreset = new Dictionary<UnturnedSkill, byte>
{
{ UnturnedSkill.Survival, 7 },
{ UnturnedSkill.Warmblooded, 7 },
{ UnturnedSkill.Crafting, 5 },
{ UnturnedSkill.Outdoors, 5 }
},
Cost = 500
}
},
{
"engineer", new PlayerClass
{
Name = "Engineer",
SkillPreset = new Dictionary<UnturnedSkill, byte>
{
{ UnturnedSkill.Crafting, 7 },
{ UnturnedSkill.Firefighting, 5 },
{ UnturnedSkill.Strength, 5 },
{ UnturnedSkill.Vitality, 5 }
},
Cost = 500
}
}
};
}Class command
csharp
public class ClassCommand : IRocketCommand
{
public string Name => "class";
public string Help => "Sets your skill preset to a defined class.";
public string Syntax => "/class <name>";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string>();
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
if (command.Length < 1)
{
string classList = string.Join(", ", ClassRegistry.Classes.Keys);
player.SendChat($"Available classes: {classList}", Color.cyan);
return;
}
string className = command[0].ToLower();
if (!ClassRegistry.Classes.TryGetValue(className, out PlayerClass playerClass))
{
player.SendChat($"Class '{className}' not found.", Color.red);
return;
}
if (player.Experience < playerClass.Cost)
{
player.SendChat(
$"Class '{playerClass.Name}' costs {playerClass.Cost} XP. " +
$"You have {player.Experience}.", Color.red);
return;
}
player.Experience -= playerClass.Cost;
foreach (KeyValuePair<UnturnedSkill, byte> preset in playerClass.SkillPreset)
{
player.SetSkill(preset.Key, preset.Value);
}
player.SendChat(
$"Class set to '{playerClass.Name}'. " +
$"{playerClass.Cost} XP deducted.", Color.green);
}
}Auto-apply class on connect
csharp
private void HandlePlayerConnected(UnturnedPlayer player)
{
string savedClass = GetSavedClass(player.CSteamID.m_SteamID);
if (!string.IsNullOrEmpty(savedClass)
&& ClassRegistry.Classes.TryGetValue(savedClass, out PlayerClass playerClass))
{
foreach (KeyValuePair<UnturnedSkill, byte> preset in playerClass.SkillPreset)
{
player.SetSkill(preset.Key, preset.Value);
}
Logger.Log($"Applied class '{playerClass.Name}' to {player.DisplayName}.");
}
}
private string GetSavedClass(ulong steamId)
{
string path = System.IO.Path.Combine(
Directory.GetCurrentDirectory(),
"Plugins",
"ClassData",
$"{steamId}.txt");
if (System.IO.File.Exists(path))
{
return System.IO.File.ReadAllText(path).Trim();
}
return null;
}Skill level event monitoring
Unturned does not fire a specific event when a skill level changes through the normal skill menu. RocketMod does not provide a dedicated skill-changed event. To detect skill level changes, you must poll:
csharp
private readonly Dictionary<ulong, byte[]> _skillSnapshot =
new Dictionary<ulong, byte[]>();
public void SnapshotSkills(UnturnedPlayer player)
{
ulong steamId = player.CSteamID.m_SteamID;
byte[] levels = new byte[18];
for (int i = 0; i < 18; i++)
{
levels[i] = player.GetSkillLevel((UnturnedSkill)i);
}
_skillSnapshot[steamId] = levels;
}
public bool DetectSkillChanges(UnturnedPlayer player, out List<string> changes)
{
changes = new List<string>();
ulong steamId = player.CSteamID.m_SteamID;
if (!_skillSnapshot.TryGetValue(steamId, out byte[] previous))
{
SnapshotSkills(player);
return false;
}
for (int i = 0; i < 18; i++)
{
byte current = player.GetSkillLevel((UnturnedSkill)i);
if (current != previous[i])
{
changes.Add(
$"{(UnturnedSkill)i}: {previous[i]} -> {current}");
}
}
SnapshotSkills(player);
return changes.Count > 0;
}Skill index reference
The following table maps each UnturnedSkill enum value to its index in the skills array, category, and effect:
| Enum value | Index | Category | Effect at level 7 |
|---|---|---|---|
Overkill | 0 | Overkill | +70% damage with low-tier weapons |
Sharpshooter | 1 | Overkill | +70% damage with high-tier weapons |
Dexterous | 2 | Overkill | -70% weapon swap delay |
Parkour | 3 | Overkill | Reduced climbing stamina cost |
Heavy | 4 | Overkill | Recoil reduction, heavy weapon handling |
Endurance | 5 | Overkill | -70% stamina drain while running |
Warmblooded | 6 | Support | -70% hypothermia rate |
Survival | 7 | Support | +70% healing from food and drink |
Crafting | 8 | Support | -70% crafting resource cost |
Outdoors | 9 | Support | Reduced aggro range from wildlife |
Cooking | 10 | Support | Improved cooking yield |
Firefighting | 11 | Support | -70% fire damage |
Immunization | 12 | Defense | -70% infection rate |
Toughness | 13 | Defense | -70% limb damage penalty |
Strength | 14 | Defense | +70% melee damage |
Vitality | 15 | Defense | +70% healing rate |
Cardio | 16 | Defense | Increased stamina regeneration |
Diving | 17 | Defense | Reduced oxygen depletion while swimming |
Experience point operations
Reading and writing XP
csharp
// Read
uint xp = player.Experience;
// Write
player.Experience = 1000;
// Modify
player.Experience += 50;
player.Experience -= 100;The Experience property on UnturnedPlayer is a uint. Setting it to a value lower than 0 will underflow to uint.MaxValue:
csharp
// DANGEROUS
player.Experience -= 100; // Underflows if current XP < 100
// SAFE
uint current = player.Experience;
player.Experience = (uint)Math.Max(0, (int)current - 100);Experience event sequence
When experience changes via player actions (killing zombies, completing quests), the sequence is:
Player completes action
↓
Unturned engine adds XP
↓
Engine calls ApplyExperience on skill manager
↓
RocketMod intercepts and fires OnPlayerUpdateExperience
↓
Plugin handler receives updated experience valueWhen experience changes programmatically (setting player.Experience), the sequence is:
Plugin sets player.Experience = value
↓
Property setter fires
↓
UnturnedPlayer updates the underlying SDG.Unturned.Player skill instance
↓
RocketMod fires OnPlayerUpdateExperienceCommon pitfalls
Using GetSkillLevel with a string instead of the enum
GetSkillLevel() accepts a string parameter, but the string must match the skill name exactly as it appears in the Unturned localization data. Using the English name like "Overkill" compiles and runs but may return unexpected results if the server is running a different locale:
csharp
// Fragile — depends on locale
byte level = player.GetSkillLevel("Overkill");
// Robust — uses enum regardless of locale
byte level = player.GetSkillLevel(UnturnedSkill.Overkill);SetSkill name confusion
RocketMod exposes the method as SetSkill(), not SetSkillLevel(). If you see examples online using player.SetSkillLevel(), they are either using a different wrapper library or the example is incorrect.
Experience property is not clamped
The Experience property on UnturnedPlayer does not clamp negative values. Always validate before subtracting or use Math.Max:
csharp
// CORRECT
long newValue = Math.Max(0, (long)player.Experience - amount);
player.Experience = (uint)newValue;Skill level range is 0 to 7
Setting a skill level outside the 0-7 range may produce unexpected behavior:
csharp
// WRONG — level 10 is outside valid range
player.SetSkill(UnturnedSkill.Overkill, 10);
// CORRECT
player.SetSkill(UnturnedSkill.Overkill, Math.Clamp(targetLevel, (byte)0, (byte)7));Skills list is read from the player's current state
If you modify a skill level while the player is dead, the change may not persist through the respawn. Always check the player's life state before modifying skills:
csharp
if (player.Player.life.isDead)
{
Logger.LogWarning("Cannot modify skills for dead player.");
return;
}Frequently asked questions
How do I set a player's skill level using the API?
Use the SetSkill() method on the UnturnedPlayer object:
csharp
player.SetSkill(UnturnedSkill.Overkill, 7);The first parameter is the UnturnedSkill enum value. The second is the target level (0-7).
Why does GetSkillLevel return 0 when called with a string?
If the string does not exactly match the skill name in the Unturned localization data, GetSkillLevel() returns 0. Always use the UnturnedSkill enum for reliable results.
Does OnPlayerUpdateExperience fire when I set experience programmatically?
Yes. Setting player.Experience triggers RocketMod's internal change detection, which fires the OnPlayerUpdateExperience event with the new value.
Can I prevent a player from spending experience?
There is no built-in way to prevent a player from using the skill menu to spend XP. To enforce a locked skill system, zero out the player's experience on a timer or use a plugin that intercepts and resets skill changes:
csharp
private void HandlePlayerUpdateExperience(UnturnedPlayer player, uint experience)
{
if (IsSkillLocked(player))
{
player.Experience = 0;
ResetAllSkills(player);
}
}How do I calculate the XP cost for a skill level?
Unturned uses a quadratic scaling formula: cost per level = level² × 5. The cost to go from level 0 to level N is the sum of 1²×5 + 2²×5 + ... + N²×5.
Is there an event for when a player learns a specific skill level?
No. Unturned does not fire per-skill-level-up events. RocketMod does not add one. To detect level-ups, you must poll skill levels on a timer and compare against a snapshot.
Cross-references
- Player Position Tracking — previous article; movement speed is affected by skill levels.
- Player Vitals — next article; health and stamina affected by skills.
- Player Bleeding and Broken Bones — skills affect bleeding recovery rate.
- RocketMod and OpenMod Plugin Basics — plugin lifecycle and command registration.
- Player Connect and Disconnect Events — loading skill presets on join.
- Server Commands Reference — command syntax reference.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2025-06-18 | 57 Studios | Initial publication. Skill object model, GetSkillLevel, SetSkill, UnturnedSkill enum, XP reward plugin, class loadout system, event monitoring, common pitfalls. |
