Skip to content

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 PlayerGesture enum: every gesture value the Unturned engine recognizes and sends.
  • How to detect when a player performs a gesture using RocketMod events.
  • The UnturnedStance enum 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 valueAnimation descriptionTrigger method
PointPlayer points forward with index fingerEmote menu or /point command
SalutePlayer performs a military-style saluteEmote menu or /salute command
WavePlayer waves hand side to sideEmote menu or /wave command
DancePlayer performs a short dance animationEmote menu or /dance command
LaughPlayer laughs with upper-body animationEmote menu or /laugh command
FacepalmPlayer facepalms with hand to foreheadEmote menu or /facepalm command
SurrenderPlayer raises both hands in surrender poseEmote menu or /surrender command
FlexPlayer flexes arm musclesEmote menu or /flex command
ThumbsUpPlayer shows thumbs up gestureEmote 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:

StanceDescriptionMovement speed modifier
CLIMBPlayer is climbing a ladder or ropeFixed climb speed
CROUCHPlayer is crouching50% of walk speed
DRIVEPlayer is driving a vehicleVehicle speed
DUCKPlayer is sliding or divingReduced
PRONEPlayer is lying flat on the ground25% of walk speed
RUNPlayer is running at normal speed100% movement speed
SPRINTPlayer is sprinting130% movement speed
STANDPlayer is standing still or walking100% movement speed
SWIMPlayer is swimming70% of walk speed
SITPlayer is sitting on a chair or vehicle seatStationary

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

APITypePurpose
OnPlayerGestureEventFires when a player performs a gesture animation
PlayerGestureEnumGesture values (Point, Salute, Wave, Dance, Laugh, Facepalm, Surrender, Flex, ThumbsUp)
UnturnedPlayer.StancePropertyCurrent player stance (read-only)
UnturnedStanceEnumStance values (CLIMB, CROUCH, DRIVE, DUCK, PRONE, RUN, SPRINT, STAND, SWIM, SIT)
animator.checkGesture(byte)MethodTriggers a gesture animation programmatically

Cross-references

Document history

VersionDateAuthorNotes
1.02025-06-1857 StudiosInitial 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);
    }
}