Player Gesture and Stance
Unturned players can express themselves through gestures (intentional animations triggered by hotkeys or menu) and stances (movement postures determined by the player's current action). For roleplay servers, emote systems, animation-based mechanics, and movement-state-dependent plugins, understanding how RocketMod exposes gestures and stances is essential.
This article covers the RocketMod gesture and stance API in depth. It explains the PlayerGesture enum and its values, how to detect gestures through events, how to read and modify stance states, and how to build a complete emote menu 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.
- Understanding of the Unturned emote system from a player perspective.
What you'll learn
- The
PlayerGestureenum: every gesture value the Unturned engine recognizes and sends. - How to detect when a player performs a gesture using RocketMod events.
- The
UnturnedStanceenum and how to read a player's current stance. - How to detect stance changes through polling.
- How to build an emote menu system with chat-triggered animations.
- How to build a stance-aware movement system that enforces roleplay walk speeds.
- Common pitfalls: nonexistent gesture values, stance polling frequency, and client-server animation sync.
Gesture system
What gestures are
Gestures are intentional player animations triggered through the Unturned emote menu (default key: B) or through configurable hotkeys. Each gesture plays a specific animation on the player character and is visible to all nearby clients.
RocketMod intercepts gesture animations through the OnPlayerGesture event.
PlayerGesture enum
RocketMod defines the PlayerGesture enum for gesture detection:
csharp
public enum PlayerGesture
{
Point,
Salute,
Wave,
Dance,
Laugh,
Facepalm,
Surrender,
Flex,
ThumbsUp
}Each value corresponds to a specific animation in the Unturned player animation system:
| Gesture value | Animation description | Trigger method |
|---|---|---|
Point | Player points forward with index finger | Emote menu or /point command |
Salute | Player performs a military-style salute | Emote menu or /salute command |
Wave | Player waves hand side to side | Emote menu or /wave command |
Dance | Player performs a short dance animation | Emote menu or /dance command |
Laugh | Player laughs with upper-body animation | Emote menu or /laugh command |
Facepalm | Player facepalms with hand to forehead | Emote menu or /facepalm command |
Surrender | Player raises both hands in surrender pose | Emote menu or /surrender command |
Flex | Player flexes arm muscles | Emote menu or /flex command |
ThumbsUp | Player shows thumbs up gesture | Emote menu or /thumbsup command |
OnPlayerGesture event
csharp
public static event PlayerGestureEvent OnPlayerGesture;
public delegate void PlayerGestureEvent(UnturnedPlayer player, PlayerGesture gesture);The handler receives the player who performed the gesture and the PlayerGesture enum value representing the gesture.
Subscription pattern
csharp
using Rocket.Unturned.Events;
protected override void Load()
{
UnturnedPlayerEvents.OnPlayerGesture += HandlePlayerGesture;
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerGesture -= HandlePlayerGesture;
}
private void HandlePlayerGesture(UnturnedPlayer player, PlayerGesture gesture)
{
Logger.Log($"{player.DisplayName} performed gesture: {gesture}");
}Gesture logging
csharp
private readonly Dictionary<ulong, DateTime> _lastGesture =
new Dictionary<ulong, DateTime>();
private void HandlePlayerGesture(UnturnedPlayer player, PlayerGesture gesture)
{
ulong steamId = player.CSteamID.m_SteamID;
DateTime now = DateTime.UtcNow;
if (_lastGesture.TryGetValue(steamId, out DateTime lastTime))
{
double elapsed = (now - lastTime).TotalSeconds;
if (elapsed < 0.5)
{
return; // Ignore rapid-fire gesture spam
}
}
_lastGesture[steamId] = now;
Logger.Log($"[Gesture] {player.DisplayName}: {gesture}");
}Gesture-based RP chat announcements
csharp
private void HandlePlayerGesture(UnturnedPlayer player, PlayerGesture gesture)
{
string announcement = string.Empty;
switch (gesture)
{
case PlayerGesture.Salute:
announcement = string.Format("* {0} salutes. *", player.DisplayName);
break;
case PlayerGesture.Wave:
announcement = string.Format("* {0} waves. *", player.DisplayName);
break;
case PlayerGesture.Point:
announcement = string.Format("* {0} points. *", player.DisplayName);
break;
case PlayerGesture.Dance:
announcement = string.Format("* {0} starts dancing. *", player.DisplayName);
break;
case PlayerGesture.Laugh:
announcement = string.Format("* {0} laughs. *", player.DisplayName);
break;
case PlayerGesture.Facepalm:
announcement = string.Format("* {0} facepalms. *", player.DisplayName);
break;
case PlayerGesture.Surrender:
announcement = string.Format("* {0} surrenders with hands up. *", player.DisplayName);
break;
case PlayerGesture.Flex:
announcement = string.Format("* {0} flexes. *", player.DisplayName);
break;
case PlayerGesture.ThumbsUp:
announcement = string.Format("* {0} gives a thumbs up. *", player.DisplayName);
break;
}
if (!string.IsNullOrEmpty(announcement))
{
ChatManager.serverSendChat(announcement, Color.cyan, null, EChatMode.GLOBAL, null);
}
}Stance system
What stances are
Stances represent the player's current movement posture. The Unturned engine tracks the player's stance continuously and changes it based on the player's actions:
| Stance | Description | Movement speed modifier |
|---|---|---|
CLIMB | Player is climbing a ladder or rope | Fixed climb speed |
CROUCH | Player is crouching | 50% of walk speed |
DRIVE | Player is driving a vehicle | Vehicle speed |
DUCK | Player is sliding or diving | Reduced |
PRONE | Player is lying flat on the ground | 25% of walk speed |
RUN | Player is running at normal speed | 100% movement speed |
SPRINT | Player is sprinting | 130% movement speed |
STAND | Player is standing still or walking | 100% movement speed |
SWIM | Player is swimming | 70% of walk speed |
SIT | Player is sitting on a chair or vehicle seat | Stationary |
UnturnedStance enum
RocketMod exposes stances through the UnturnedStance enum, which wraps the underlying EPlayerStance:
csharp
public enum UnturnedStance
{
CLIMB,
CROUCH,
DRIVE,
DUCK,
PRONE,
RUN,
SPRINT,
STAND,
SWIM,
SIT
}Reading the current stance
csharp
UnturnedStance stance = player.Stance;Stance detection example
csharp
private void LogPlayerStance(UnturnedPlayer player)
{
string stanceName = player.Stance.ToString();
Logger.Log($"{player.DisplayName} stance: {stanceName}");
}Stance-based speed control
csharp
private float GetSpeedMultiplier(UnturnedStance stance)
{
switch (stance)
{
case UnturnedStance.STAND: return 1.0f;
case UnturnedStance.RUN: return 1.0f;
case UnturnedStance.SPRINT: return 1.3f;
case UnturnedStance.CROUCH: return 0.5f;
case UnturnedStance.PRONE: return 0.25f;
case UnturnedStance.SWIM: return 0.7f;
case UnturnedStance.CLIMB: return 0.3f;
case UnturnedStance.DRIVE: return 0f; // Handled by vehicle
case UnturnedStance.SIT: return 0f;
case UnturnedStance.DUCK: return 0.4f;
default: return 1.0f;
}
}Stance change detection
RocketMod does not provide a dedicated stance-change event. Detect stance changes by polling:
csharp
private readonly Dictionary<ulong, UnturnedStance> _lastStance =
new Dictionary<ulong, UnturnedStance>();
private IEnumerator MonitorStanceChanges()
{
while (true)
{
yield return new WaitForSeconds(0.5f);
foreach (UnturnedPlayer player in UnturnedPlayer.OnlinePlayers)
{
ulong steamId = player.CSteamID.m_SteamID;
UnturnedStance currentStance = player.Stance;
if (_lastStance.TryGetValue(steamId, out UnturnedStance previous))
{
if (currentStance != previous)
{
OnStanceChanged(player, previous, currentStance);
}
}
_lastStance[steamId] = currentStance;
}
}
}
private void OnStanceChanged(UnturnedPlayer player, UnturnedStance oldStance, UnturnedStance newStance)
{
Logger.Log($"{player.DisplayName} stance: {oldStance} -> {newStance}");
if (newStance == UnturnedStance.SWIM)
{
player.SendChat("You are swimming!", Color.cyan);
}
else if (newStance == UnturnedStance.PRONE)
{
player.SendChat("You are prone.", Color.gray);
}
}Building an emote menu plugin
The following plugin provides chat-based emote commands that trigger gestures and send RP announcements.
Emote commands
csharp
using Rocket.API;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;
public class EmoteCommand : IRocketCommand
{
public string Name => "emote";
public string Help => "Performs a gesture animation. Usage: /emote <name>";
public string Syntax => "/emote <point|salute|wave|dance|laugh|facepalm|surrender|flex|thumbsup>";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "emotes.use" };
public AllowedCaller AllowedCaller => AllowedCaller.Player;
private static readonly Dictionary<string, PlayerGesture> GestureMap =
new Dictionary<string, PlayerGesture>
{
{ "point", PlayerGesture.Point },
{ "salute", PlayerGesture.Salute },
{ "wave", PlayerGesture.Wave },
{ "dance", PlayerGesture.Dance },
{ "laugh", PlayerGesture.Laugh },
{ "facepalm", PlayerGesture.Facepalm },
{ "surrender", PlayerGesture.Surrender },
{ "flex", PlayerGesture.Flex },
{ "thumbsup", PlayerGesture.ThumbsUp }
};
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
if (command.Length < 1)
{
string available = string.Join(", ", GestureMap.Keys);
player.SendChat(string.Format("Available emotes: {0}", available), Color.cyan);
return;
}
string gestureName = command[0].ToLower();
if (!GestureMap.TryGetValue(gestureName, out PlayerGesture gesture))
{
player.SendChat(string.Format("Unknown emote: {0}", gestureName), Color.red);
return;
}
// Trigger the gesture on the player
player.Player.animator.checkGesture((byte)gesture);
// Announce
string announcement = string.Format("* {0} performs {1}. *", player.DisplayName, gesture);
ChatManager.serverSendChat(announcement, Color.cyan, null, EChatMode.GLOBAL, null);
}
}Emote cooldown
csharp
public class EmoteCommand : IRocketCommand
{
private readonly Dictionary<ulong, DateTime> _lastEmote =
new Dictionary<ulong, DateTime>();
private readonly int _cooldownSeconds = 3;
// ... Name, Help, Syntax, Aliases, Permissions, AllowedCaller ...
public void Execute(IRocketPlayer caller, string[] command)
{
UnturnedPlayer player = (UnturnedPlayer)caller;
ulong steamId = player.CSteamID.m_SteamID;
if (_lastEmote.TryGetValue(steamId, out DateTime lastTime))
{
double remaining = _cooldownSeconds - (DateTime.UtcNow - lastTime).TotalSeconds;
if (remaining > 0)
{
player.SendChat(
string.Format("Emote cooldown: {0:F0}s remaining.", remaining),
Color.red);
return;
}
}
// ... execute emote ...
_lastEmote[steamId] = DateTime.UtcNow;
}
}Building a stance-aware walk system
For roleplay servers that enforce realistic movement speeds, a stance-aware walk system monitors player stances and enforces speed limits.
csharp
public class StanceEnforcer
{
private readonly Dictionary<UnturnedStance, float> _speedLimits;
public StanceEnforcer()
{
_speedLimits = new Dictionary<UnturnedStance, float>
{
{ UnturnedStance.STAND, 4.5f },
{ UnturnedStance.RUN, 7.5f },
{ UnturnedStance.SPRINT, 9.0f },
{ UnturnedStance.CROUCH, 2.5f },
{ UnturnedStance.PRONE, 1.0f }
};
}
public void EnforceStanceSpeed(UnturnedPlayer player)
{
if (player.Player.life.isDead) return;
UnturnedStance stance = player.Stance;
if (_speedLimits.TryGetValue(stance, out float limit))
{
float currentSpeed = new Vector3(
player.Player.transform.position.x - player.Player.transform.position.x,
0,
player.Player.transform.position.z - player.Player.transform.position.z).magnitude;
if (currentSpeed > limit * 1.2f)
{
// Slow the player down
player.Player.movement.sendSpeed(limit);
}
}
}
}Gesture and stance event ordering
Gesture cooldown and anti-spam
csharp
public class GestureAntiSpam
{
private readonly Dictionary<ulong, List<DateTime>> _gestureHistory =
new Dictionary<ulong, List<DateTime>>();
private readonly int _maxGesturesPerMinute = 10;
private readonly int _minIntervalMs = 500;
public bool CanPerformGesture(UnturnedPlayer player)
{
ulong steamId = player.CSteamID.m_SteamID;
DateTime now = DateTime.UtcNow;
if (!_gestureHistory.ContainsKey(steamId))
{
_gestureHistory[steamId] = new List<DateTime>();
return true;
}
List<DateTime> history = _gestureHistory[steamId];
// Remove entries older than 1 minute
history.RemoveAll(t => (now - t).TotalMinutes > 1);
// Check rate limit
if (history.Count >= _maxGesturesPerMinute)
{
return false;
}
// Check minimum interval
if (history.Count > 0 && (now - history[history.Count - 1]).TotalMilliseconds < _minIntervalMs)
{
return false;
}
history.Add(now);
return true;
}
}Common pitfalls
Using nonexistent gesture values
The PlayerGesture enum includes Dance and Laugh values, but these are not recognized by the Unturned gesture system in all game versions. Before relying on a specific gesture value in a production plugin, test it against the version of Unturned your server runs. Gesture enum values that the engine does not recognize will be silently ignored when triggered and the gesture event handler will not be invoked.
Incorrect gesture trigger method
The correct method to trigger a gesture programmatically is:
csharp
player.Player.animator.checkGesture((byte)gesture);Using player.Player.animator.checkGesture with the enum cast to byte is the only reliable way to trigger gestures from plugin code. Other animation methods in the Unturned API may not produce visible results.
Stance polling is necessary
RocketMod does not fire a dedicated stance change event. The stance value is updated by the engine on every frame, but the RocketMod event system does not expose a per-change callback. Always poll stance values on a timer and compare against a stored snapshot to detect changes.
Stance is not reliable during vehicle entry/exit
When a player enters or exits a vehicle, the stance value can briefly report an intermediate or incorrect state. Add a short debounce delay before reading the stance after a vehicle interaction:
csharp
private IEnumerator DelayedStanceCheck(UnturnedPlayer player)
{
yield return new WaitForSeconds(0.5f);
UnturnedStance stance = player.Stance;
// Now safe to read
}Gesture animation is client-predicted
When a gesture is triggered, the animation plays immediately on the triggering client, but there is a network delay (typically 100-500ms) before other clients see the animation. Do not use gesture events for time-sensitive game logic that depends on all clients seeing the animation simultaneously.
Frequently asked questions
How do I trigger a gesture from plugin code?
Use player.Player.animator.checkGesture((byte)gesture) where gesture is a PlayerGesture enum value cast to byte. This triggers the gesture animation on the player and fires the OnPlayerGesture event.
Why does my gesture handler not fire for certain gestures?
If the gesture value is not recognized by the Unturned engine version running on the server, the gesture is silently ignored and the handler does not fire. Test each gesture value against your specific Unturned version.
Can I prevent a player from using a specific gesture?
RocketMod does not provide a pre-gesture event that can cancel a gesture. To block gestures, track the OnPlayerGesture event and immediately play a different animation to override, or implement a permission check in a custom emote command system that bypasses the built-in emote menu.
How do I know when a player sits down?
The stance changes to UnturnedStance.SIT when a player sits in a vehicle seat or on a chair. Poll the stance value to detect the change.
Can I force a player into a specific stance?
Stance is controlled by the player's client. There is no reliable server-side API to force a stance change. Attempting to set stance values directly on the underlying PlayerAnimator object is not supported through the RocketMod API.
Why does the stance event not exist?
The Unturned engine does not expose a stance-change callback through its public API. RocketMod cannot add one without engine-level hooks. Polling is the only option.
Full event and API reference
| API | Type | Purpose |
|---|---|---|
OnPlayerGesture | Event | Fires when a player performs a gesture animation |
PlayerGesture | Enum | Gesture values (Point, Salute, Wave, Dance, Laugh, Facepalm, Surrender, Flex, ThumbsUp) |
UnturnedPlayer.Stance | Property | Current player stance (read-only) |
UnturnedStance | Enum | Stance values (CLIMB, CROUCH, DRIVE, DUCK, PRONE, RUN, SPRINT, STAND, SWIM, SIT) |
animator.checkGesture(byte) | Method | Triggers a gesture animation programmatically |
Cross-references
- Player Bleeding and Broken Bones — previous article; broken bones affect movement stances.
- Player Wearables — next article; clothing and equipment visual state.
- Player Position Tracking — movement tracking interacts with stance state.
- RocketMod and OpenMod Plugin Basics — plugin lifecycle and event subscription.
- Server Commands Reference — command syntax reference.
- Debugging Server Exceptions — diagnosing plugin issues.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2025-06-18 | 57 Studios | Initial publication. Gesture enum and events, stance system, emote plugin, stance-aware movement, anti-spam, common pitfalls. |
Gesture spam prevention configuration
`csharp public class GestureSpamConfig {
Gesture spam prevention configuration
csharp
public class GestureSpamConfig
{
public int MaxGesturesPerMinute = 10;
public int MinIntervalMs = 500;
public bool EnableGlobalCooldown = false;
public int GlobalCooldownMs = 1000;
public string SpamWarningMessage = "Please slow down with the gestures.";
}Gesture blocking for specific zones
In some RP scenarios, certain gestures should be blocked in specific zones (no laughing in serious areas, no dancing in military zones):
csharp
public class ZoneGestureBlocker
{
private readonly HashSet<string> _noGestureZones;
private readonly HashSet<PlayerGesture> _blockedGestures;
public ZoneGestureBlocker()
{
_noGestureZones = new HashSet<string>
{
"Hospital", "Courtroom", "MilitaryBase"
};
_blockedGestures = new HashSet<PlayerGesture>
{
PlayerGesture.Dance,
PlayerGesture.Laugh
};
}
public bool IsGestureAllowed(UnturnedPlayer player, PlayerGesture gesture, string currentZone)
{
if (_noGestureZones.Contains(currentZone) && _blockedGestures.Contains(gesture))
{
player.SendChat(string.Format("Gesture '{0}' is not allowed in this zone.", gesture), Color.red);
return false;
}
return true;
}
}Stance override system
For certain server mechanics, forcing a player into a specific stance (or preventing one) can be useful. While RocketMod does not provide a direct stance setter, you can use the underlying engine methods:
csharp
public class StanceController
{
public void ForceStand(UnturnedPlayer player)
{
if (player.Player.stance.stance == EPlayerStance.CROUCH || player.Player.stance.stance == EPlayerStance.PRONE)
{
player.Player.stance.stand();
}
}
public void ForceCrouch(UnturnedPlayer player)
{
if (player.Player.stance.stance == EPlayerStance.STAND || player.Player.stance.stance == EPlayerStance.RUN)
{
player.Player.stance.crouch();
}
}
public bool IsInCombatStance(UnturnedPlayer player)
{
EPlayerStance stance = player.Player.stance.stance;
return stance == EPlayerStance.RUN || stance == EPlayerStance.SPRINT || stance == EPlayerStance.CROUCH;
}
}Stance polling integration with position tracking
Combine stance data with position tracking to create movement-context-aware systems:
csharp
public class MovementContext
{
private readonly Dictionary<ulong, MovementSnapshot> _snapshots = new Dictionary<ulong, MovementSnapshot>();
public class MovementSnapshot
{
public Vector3 Position;
public UnturnedStance Stance;
public float Speed;
public DateTime Timestamp;
}
public MovementSnapshot TakeSnapshot(UnturnedPlayer player)
{
var snap = new MovementSnapshot
{
Position = player.Position,
Stance = player.Stance,
Speed = 0f,
Timestamp = DateTime.UtcNow
};
ulong steamId = player.CSteamID.m_SteamID;
if (_snapshots.TryGetValue(steamId, out MovementSnapshot previous))
{
float distance = Vector3.Distance(snap.Position, previous.Position);
double elapsed = (snap.Timestamp - previous.Timestamp).TotalSeconds;
if (elapsed > 0)
{
snap.Speed = distance / (float)elapsed;
}
}
_snapshots[steamId] = snap;
return snap;
}
public string GetMovementReport(UnturnedPlayer player)
{
MovementSnapshot snap = TakeSnapshot(player);
return string.Format("{0}: {1} at {2:F1} u/s", player.DisplayName, snap.Stance, snap.Speed);
}
}