Player Components
RocketMod provides UnturnedPlayerComponent as a base class for attaching per-player data and behavior to individual players. Each component is a MonoBehaviour that follows a lifecycle — created when a player connects (or when the component is first accessed), and destroyed when the player disconnects. Components are the standard way to track per-player state across multiple events and commands without resorting to static dictionaries.
This article covers the component lifecycle, how to create and use components, patterns for data persistence, and common use cases encountered in production RocketMod plugins.
57 Studios maintains a suite of RocketMod plugins for the Horizon Life RP community. The component patterns documented here are drawn from production plugin authoring experience where per-player state management is critical for gameplay systems.
Prerequisites
- A working RocketMod installation. See RocketMod and OpenMod Plugin Basics.
- Visual Studio with C# development workload.
- Familiarity with
UnturnedPlayerclass and basic RocketMod event subscription. - Understanding of Unity's
MonoBehaviourlifecycle.
What you'll learn
- What
UnturnedPlayerComponentis and when to use it. - How to create and attach a component to a player.
- The component lifecycle: attachment, initialization, and cleanup.
- How to store and retrieve per-player data using components.
- Advanced patterns: data persistence, timed state, component interaction.
- Best practices for component design in production plugins.
What is UnturnedPlayerComponent?
UnturnedPlayerComponent is a MonoBehaviour subclass that RocketMod uses to associate data with individual players. Each player can have one instance of each component type. Components are automatically managed by RocketMod's internally — they are created when needed and cleaned up when the player disconnects.
When to use a component over a dictionary
| Scenario | Recommended approach |
|---|---|
| Tracking a player's cooldown timers | Component with fields for each timer |
| Storing a player's temporary item loadout | Component with a list field |
| Tracking player state across multiple events | Component with state enum |
| Caching data that is expensive to look up | Component with cached properties |
| Simple key-value pair with one field | Dictionary (overkill to use a component) |
| Data that must survive server restart | Database or XML file (component is in-memory) |
Creating a player component
A player component extends UnturnedPlayerComponent:
csharp
using Rocket.Unturned.Player;
namespace MyPlugin.Components
{
public class MyPlayerComponent : UnturnedPlayerComponent
{
protected override void Load()
{
// Called when the component is attached to a player
Rocket.Core.Logging.Logger.Log(
$"MyPlayerComponent loaded for {Player.DisplayName}"
);
}
protected override void Unload()
{
// Called when the component is removed from a player
Rocket.Core.Logging.Logger.Log(
$"MyPlayerComponent unloaded for {Player.DisplayName}"
);
}
}
}The Player property
Every UnturnedPlayerComponent has a Player property of type UnturnedPlayer that references the player the component is attached to. This property is available in Load(), Unload(), and at any point during the component's lifetime.
Adding a component to a player
Attach a component to a player by calling player.GameObject.AddComponent<T>():
csharp
using Rocket.Unturned.Player;
using Rocket.Unturned.Events;
private void OnPlayerConnected(UnturnedPlayer player)
{
// Add the component to the player's GameObject
// This calls Load() on the component after creation
player.GameObject.AddComponent<MyPlayerComponent>();
Rocket.Core.Logging.Logger.Log(
$"Attached MyPlayerComponent to {player.DisplayName}"
);
}The Load() method is called automatically when the component is added to the player's GameObject. The component remains attached until the player disconnects, at which point Unload() is called and the component is destroyed.
Always use AddComponent for UnturnedPlayerComponent
When attaching a UnturnedPlayerComponent to a player, you must use player.GameObject.AddComponent<T>(). This is the correct API for RocketMod. The AddComponent<T>() method on GameObject is how Unity's MonoBehaviour system works — it creates the component instance, registers it with the game object's lifecycle manager, and calls Awake() followed by your custom Load() method.
Do not attempt to use the GetComponent<T>() method to add a component — GetComponent only returns an existing component instance. If the component has not been added yet, GetComponent returns null. The proper sequence is: call AddComponent<T>() first to create and attach it, then use GetComponent<T>() on subsequent accesses to retrieve the existing instance.
If you use new to create the component instance directly (var comp = new MyPlayerComponent()), the Unity engine will not register it, the Player property will be null, and Load() will never fire. Always go through AddComponent<T>().
Retrieving an existing component
Once a component is attached, access it with player.GetComponent<T>():
csharp
var component = player.GetComponent<MyPlayerComponent>();
if (component != null)
{
// Component exists — use it
DoSomethingWith(component);
}
else
{
// Component does not exist — the player may not have gone through
// the connection flow, or the component was not attached
Rocket.Core.Logging.Logger.LogWarning(
$"MyPlayerComponent not found for {player.DisplayName}"
);
}Auto-attaching on connection
The recommended pattern is to attach the component in the OnPlayerConnected event and retrieve it later in commands or event handlers:
csharp
public class MyPlugin : RocketPlugin<MyPluginConfiguration>
{
public static MyPlugin Instance { get; private set; }
protected override void Load()
{
Instance = this;
UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected -= OnPlayerDisconnected;
Instance = null;
}
private void OnPlayerConnected(UnturnedPlayer player)
{
// Attach component to every player on join
player.GameObject.AddComponent<MyPlayerComponent>();
}
private void OnPlayerDisconnected(UnturnedPlayer player)
{
// Component is destroyed automatically.
// Unload() is called before destruction.
// No explicit cleanup needed here.
}
}Component lifecycle
The lifecycle of an UnturnedPlayerComponent follows this sequence:
Load()
Load() is called after the component is created and the Player property has been set. This is where you initialize component state, subscribe to per-player events, and set default field values.
Unload()
Unload() is called when the component is being destroyed, either because the player disconnected or because the plugin is being unloaded. Clean up subscriptions, timers, and temporary state here.
Component fields and state
Components store per-player data as fields:
csharp
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;
namespace MyPlugin.Components
{
public class PlayerStateComponent : UnturnedPlayerComponent
{
// Per-player state fields
public bool IsInCombat;
public float CombatEndTime;
public int KillStreak;
public string LastDeathMessage;
public List<string> RecentInteractions;
public Vector3 LastPosition;
// Private tracking
private float _lastHealthCheck;
protected override void Load()
{
IsInCombat = false;
CombatEndTime = 0f;
KillStreak = 0;
LastDeathMessage = "";
RecentInteractions = new List<string>();
LastPosition = Player.Player.transform.position;
_lastHealthCheck = Time.realtimeSinceStartup;
Rocket.Core.Logging.Logger.Log(
$"PlayerStateComponent initialized for {Player.DisplayName}"
);
}
protected override void Unload()
{
// Save any critical state before unload
if (IsInCombat)
{
Rocket.Core.Logging.Logger.Log(
$"{Player.DisplayName} disconnected while in combat. " +
$"Combat state lost on disconnect."
);
}
}
}
}Data persistence
Component data is in-memory only. When the server restarts or the player reconnects, all component fields reset to their defaults (or to whatever Load() sets). For persistent data, save to a configuration file or database in the Unload() event.
csharp
public class PersistentPlayerComponent : UnturnedPlayerComponent
{
public int TotalKills;
public int TotalDeaths;
public int PlayTimeMinutes;
protected override void Unload()
{
// Save to persistent storage
var data = new XMLFileAsset<PlayerData>(
$"Rocket/Plugins/MyPlugin/playerdata/{Player.CSteamID.m_SteamID}.xml",
load: true
);
data.Instance.TotalKills = TotalKills;
data.Instance.TotalDeaths = TotalDeaths;
data.Instance.PlayTimeMinutes = PlayTimeMinutes;
data.Save();
}
}Advanced component patterns
Timed state components
For gameplay features like combat tagging or PvP cooldowns, components manage timed state with Unity's Update loop:
csharp
using Rocket.Unturned.Player;
using UnityEngine;
public class CombatTagComponent : UnturnedPlayerComponent
{
public bool IsTagged { get; private set; }
private float _tagEndTime;
public void TagForDuration(float seconds)
{
IsTagged = true;
_tagEndTime = Time.realtimeSinceStartup + seconds;
Player.SendChat("You are now in combat mode!", Color.red);
}
private void Update()
{
if (IsTagged && Time.realtimeSinceStartup >= _tagEndTime)
{
IsTagged = false;
Player.SendChat("You are no longer in combat.", Color.green);
}
}
}The Update() method is called every frame by Unity's MonoBehaviour system. Keep the body lightweight to avoid performance impact.
Component with permissions
Components can check player permissions for conditional behavior:
csharp
public class FeatureAccessComponent : UnturnedPlayerComponent
{
public bool HasVanishAccess { get; private set; }
public bool HasFlyAccess { get; private set; }
public int MaxTeleportDistance { get; private set; }
protected override void Load()
{
// Resolve permissions once on connect rather than every check
HasVanishAccess = R.Permissions.HasPermission(Player, "myplugin.vanish");
HasFlyAccess = R.Permissions.HasPermission(Player, "myplugin.fly");
MaxTeleportDistance = R.Permissions.HasPermission(Player, "myplugin.tp.unlimited")
? int.MaxValue
: 1000;
}
}This pattern caches permission lookups in the component fields, avoiding repeated R.Permissions.HasPermission() calls in hot paths.
Component interaction
Components can reference each other when multiple systems need to share per-player state:
csharp
public class InteractionComponent : UnturnedPlayerComponent
{
protected override void Load()
{
// Access another component on the same player
var combatTag = player.GetComponent<CombatTagComponent>();
if (combatTag != null && combatTag.IsTagged)
{
Player.SendChat("You cannot interact while in combat.", Color.red);
}
}
}Because components are separate MonoBehaviour instances on the same GameObject, GetComponent<T>() can retrieve any component attached to the same player.
Complete example: player activity tracking
The following example demonstrates a complete component that tracks player activity statistics with optional persistence.
ActivityComponent.cs:
csharp
using Rocket.Unturned.Player;
using Rocket.Unturned.Chat;
using UnityEngine;
using System;
namespace ActivityTracker.Components
{
public class ActivityComponent : UnturnedPlayerComponent
{
// Activity tracking fields
public DateTime ConnectTime { get; private set; }
public int MessagesSent;
public int CommandsUsed;
public float DistanceTraveled;
public int DeathsThisSession;
// Internal tracking
private Vector3 _lastPosition;
private float _positionUpdateInterval;
protected override void Load()
{
ConnectTime = DateTime.UtcNow;
MessagesSent = 0;
CommandsUsed = 0;
DistanceTraveled = 0f;
DeathsThisSession = 0;
_lastPosition = Player.Player.transform.position;
_positionUpdateInterval = 5f;
Player.SendChat(
"Your activity is being tracked this session.", Color.gray);
}
protected override void Unload()
{
TimeSpan sessionDuration = DateTime.UtcNow - ConnectTime;
Rocket.Core.Logging.Logger.Log(
$"Player {Player.DisplayName} session: " +
$"{sessionDuration.TotalMinutes:F1} min, " +
$"{MessagesSent} messages, {CommandsUsed} commands, " +
$"{DistanceTraveled:F0} units traveled."
);
}
public void OnMessageSent()
{
MessagesSent++;
}
public void OnCommandUsed()
{
CommandsUsed++;
}
public void OnDeath()
{
DeathsThisSession++;
}
private void Update()
{
// Track distance traveled every few frames
_positionUpdateInterval -= Time.deltaTime;
if (_positionUpdateInterval <= 0f)
{
Vector3 currentPosition = Player.Player.transform.position;
DistanceTraveled += Vector3.Distance(_lastPosition, currentPosition);
_lastPosition = currentPosition;
_positionUpdateInterval = 5f;
}
}
public string GetSessionSummary()
{
TimeSpan duration = DateTime.UtcNow - ConnectTime;
return $"Session: {duration.TotalMinutes:F0} min | " +
$"Messages: {MessagesSent} | " +
$"Deaths: {DeathsThisSession} | " +
$"Dist: {DistanceTraveled:F0}u";
}
}
}ActivityPlugin.cs:
csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
namespace ActivityTracker
{
public class ActivityPlugin : RocketPlugin<ActivityPluginConfiguration>
{
public static ActivityPlugin Instance { get; private set; }
protected override void Load()
{
Instance = this;
UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
Rocket.Core.Logging.Logger.Log("ActivityTracker loaded.");
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected -= OnPlayerDisconnected;
Instance = null;
Rocket.Core.Logging.Logger.Log("ActivityTracker unloaded.");
}
private void OnPlayerConnected(UnturnedPlayer player)
{
player.GameObject.AddComponent<ActivityComponent>();
}
private void OnPlayerDisconnected(UnturnedPlayer player)
{
// Component is auto-destroyed; Unload() fires automatically.
// No additional cleanup required.
}
}
}SessionCommand.cs:
csharp
using Rocket.API;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;
using ActivityTracker.Components;
namespace ActivityTracker.Commands
{
public class SessionCommand : IRocketCommand
{
public string Name => "session";
public string Help => "Display your current session statistics.";
public string Syntax => "/session";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "activitytracker.session" };
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
var player = (UnturnedPlayer)caller;
var component = player.GetComponent<ActivityComponent>();
if (component != null)
{
string summary = component.GetSessionSummary();
UnturnedChat.Say(caller, summary, Color.cyan);
}
else
{
UnturnedChat.Say(caller,
"Your activity component could not be found. " +
"Try reconnecting.", Color.red);
}
}
}
}Component performance considerations
Minimize Update usage
Unity's Update() method runs every frame (approximately 60 times per second for most servers). Heavy operations in Update() on every player's component can significantly impact server performance:
- Avoid file I/O in
Update(). - Avoid LINQ queries that allocate memory.
- Use
Time.realtimeSinceStartupfor interval-based checks instead of per-frame logic. - Consider using coroutines for delayed actions instead of
Update().
Component pooling for reconnection
When a player reconnects frequently, the component is destroyed and re-created. If component creation is expensive, consider caching data in the plugin's static scope and reloading it into the component:
csharp
public static class PlayerDataCache
{
private static readonly Dictionary<ulong, PlayerPersistentData> _cache =
new Dictionary<ulong, PlayerPersistentData>();
public static PlayerPersistentData GetOrCreate(ulong steamId)
{
if (!_cache.ContainsKey(steamId))
{
_cache[steamId] = new PlayerPersistentData();
}
return _cache[steamId];
}
}Null-checking Player
The Player property can be null if the component is accessed outside its normal lifecycle (for example, during Awake() or in a corner case during shutdown). Always null-check Player before accessing it:
csharp
private void Update()
{
if (Player == null || Player.Player == null)
return;
// Safe to use Player here
}Frequently asked questions
Can I have multiple components of the same type on one player?
No. Each GameObject can have at most one instance of a given component type. If you call AddComponent<T>() when a component of type T already exists, Unity returns the existing instance.
When exactly is Load() called?
Load() is called by RocketMod's internal component manager shortly after AddComponent<T>() completes and the Player property has been set. It is not called by Unity's MonoBehaviour system directly.
Do components persist through server restarts?
No. Components are in-memory only and are destroyed when the server shuts down. Use XMLFileAsset<T> or a database to persist component data.
Can I use UnturnedPlayerComponent in OpenMod?
No. OpenMod has its own per-player data system using IPlayerDataStore and does not use Unity's MonoBehaviour components. UnturnedPlayerComponent is RocketMod-specific.
How do I share data between two components?
Both components can access each other through player.GetComponent<T>(). Alternatively, use a shared static data class that both components reference.
Why is my component's Load() not being called?
Load() is only called when the component is attached via AddComponent<T>(). If the component was created with new or if GetComponent<T>() was called before AddComponent<T>() was ever invoked, Load() has not fired.
Cross-references
- Permissions System — permission checks within component logic.
- Player Chat — chat events that may interact with component state.
- Player Damage — damage events that may interact with component state.
- Player Death and Respawn — death/respawn handling with component tracking.
- 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 component lifecycle, attachment patterns, data persistence, advanced component designs, best practices. |
