Skip to content

Player Tools — Skillsets, Teleport, Camera

Overview

Three systems govern how the player sees the world and how the world sees the player. PlayerLook (2005 lines) manages the camera system — first/third-person perspective, aiming zoom, scope rendering (dual-render and single-render), field of view, input sensitivity scaling, freecam, damage flinch, explosion camera shake, and the third-person collision sweep. PlayerTool (316 lines) provides static utility functions for player lookup, reputation display, and spatial queries. PlayerTeleport handles safe teleportation with physics interpolation.

The services file PlayerTool.cs lives at Unturned/Tools/PlayerTool.cs. PlayerLook is part of the Player component system at Unturned/Player/PlayerLook.cs. There is no standalone PlayerTeleport.cs in the available source; teleport functionality is integrated into PlayerMovement and is accessed through PlayerLife respawn logic. Skillset assignment is stored on SteamPlayer and referenced by the skills system.


PlayerLook Camera System

Perspective Management

PlayerLook._perspective is of type EPlayerPerspective (FIRST or THIRD). The setActivePerspective method:

  • Switching to FIRST: Parents MainCamera.instance to player.first, sets local position to Vector3.up * eyes, clears all freecam modes, closes workzone UI if open.
  • Switching to THIRD: Parents MainCamera.instance to player.transform.
  • Calls UpdateSingleRenderScope() and fires onPerspectiveUpdated.

Perspective switching is triggered by ControlsSettings.perspective key, gated by:

  • Player is alive (player.life.IsAlive)
  • No cursor is showing (!PlayerUI.window.showCursor)
  • Camera mode allows it (ECameraMode.BOTH or ECameraMode.VEHICLE with driving stance)

Eye Height System

Two height tracks are maintained:

heightLook — determines the aim transform position (used for raycasting origin)
heightCamera — determines the camera height (used for third-person camera origin)
StanceheightLookheightCamera
STAND / SPRINT / CLIMB / SWIM1.75m1.05m
CROUCH1.20m0.95m
PRONE0.35m0.30m
SITTING / DRIVING1.60m0.70m

eyes is a smoothed lerp toward heightLook (4× delta). thirdPersonEyeHeight is similarly smoothed and clamped within the character controller bounds minus a near-clip margin.

Pitch and Yaw

  • _pitch: 0 = up, 90 = forward, 180 = down. Range [0, 180] depending on stance.
  • _yaw: degrees, wrapped to [−360, 360], clamped when seated or turret-limited.

Pitch Clamp by Stance

StanceMinMax
STAND / SPRINT180°
CROUCH20°160°
PRONE60°120°
CLIMB45°100°
SWIM45°135°
SITTING / DRIVING (no turret)60°120°
Turret seatturret.pitchMinturret.pitchMax

Yaw Clamp

  • Free (no vehicle): no yaw clamp applied.
  • Driver seat (car): ±160°.
  • Passenger seat (non-turret): ±90°.
  • Turret seat: turret.yawMin / turret.yawMax.

Yaw is wrapped via _yaw %= 360f before clamping.

Input Processing

The Update() method (only local player) processes mouse/keyboard input to update _pitch, _yaw, _orbitPitch, _orbitYaw, _look_x, and _look_y.

Sensitivity Scaling

Three scaling modes defined in ESensitivityScalingMode:

  1. Legacy: zoomSensitivity = 1.0f / zoomFactor
  2. ZoomFactor: Same as Legacy — 1.0 / scopeCameraZoomFactor or 1.0 / mainCameraZoomFactor
  3. ProjectionRatio (focal length scaling):
    halfFov = Deg2Rad * fov * 0.5
    zoomSensitivity = Atan(coefficient * Tan(halfCurrentFov)) / Atan(coefficient * Tan(halfDesiredFov))

Sensitivity scaling is applied only when shouldUseZoomFactorForSensitivity is true (during ADS with a scoped weapon).

Hallucinogen Effects

When under vision-altering effects, yawInputMultiplier and pitchInputMultiplier are randomly set to −1.0 (inverted) with 25% probability, distorting camera controls.

Recoil System

csharp
public void recoil(float x, float y, float h, float v)
{
    _yaw += x;
    _pitch -= y;
    recoil_x += x * h;
    recoil_y += y * v;
}

Called from weapon firing code. x is horizontal recoil (yaw), y is vertical recoil (pitch). h and v are recovery multipliers — the accumulated recoil_x and recoil_y are lerped toward zero each frame (4× delta), and the delta is applied to yaw/pitch to simulate recovery.

Field of View Management

The camera FOV is computed from several layered values:

  1. Base FOV: OptionsSettings.DesiredVerticalFieldOfView (default 90).
  2. Zoom factors: mainCameraZoomFactor (weapon zoom) and scopeCameraZoomFactor (scope camera).
  3. Sprint FOV boost: OptionsSettings.sprintFovBoostIntensity * 10 added during sprint stance.
  4. Scope alpha: For single-render scopes, the main camera FOV interpolates between base and zoomed FOV based on scopeAlpha (aim-in progress).
  5. Freecam: Uses freecamVerticalFieldOfView (initialized to DesiredVerticalFieldOfView on first use).

The main camera FOV is updated in two code paths: 2D scope mode (smoother UpdateMainCameraTargetFieldOfView with 8× lerp) and the standard path.

Scope System

PlayerLook manages two separate vision systems depending on configuration:

Dual-Render Scope

  • scopeCamera.targetTexture renders a separate camera to a RenderTexture.
  • Resolution varies by EGraphicQuality: LOW=256, MEDIUM=512, HIGH=1024, ULTRA=2048.
  • Complete scene re-render from weapon-eye perspective.

Single-Render Scope

  • GraphicsSettings.scopeQuality == OFF and not using 2D overlay.
  • UnturnedPostProcess applies a zoom effect to the existing view with an alpha mask.
  • Resolution: min(Screen.width, Screen.height).
  • Scope camera renders without a target texture; post-process stack handles the overlay.

2D Scope Overlay

  • Use_2D_Scope_Overlay config and scopeQuality == OFF.
  • No render texture — the scope reticule texture is displayed as a fullscreen overlay via PlayerLifeUI.scopeOverlay.
  • Used for performance on low-end hardware.

Enable/Disable Scope

csharp
public void enableScope(float zoom, ItemSightAsset sightAsset)
{
    scopeCameraZoomFactor = zoom;
    _isScopeActive = true;
    scopeVision = sightAsset.vision;
    scopeNightvisionColor = sightAsset.nightvisionColor;
    scopeNightvisionFogIntensity = sightAsset.nightvisionFogIntensity;
    scopeCamera.enabled = (targetTexture != null && vision == NONE);
}

Scope Vision System

When a scope has a vision mode (nightvision, thermal, etc.), the scope scene applies custom lighting:

  • ApplyScopeVisionToLighting(): saves current lighting, applies scope's nightvisionColor/nightvisionFogIntensity, calls LevelLighting.updateLighting().
  • RestoreSavedLightingVision(): restores after scope render.
  • For single-render scopes, the lighting modification is toggled based on isSingleRenderScopeVisionAppliedToLighting.

Damage Flinch

FlinchFromDamage(byte damageAmount, Vector3 worldDirection):

magnitude = Min(damage, 25) * 0.5
magnitude *= 1.0 - (Toughness_mastery * 0.75)
worldRotationAxis = Cross(up, worldDirection).normalized
localRotationAxis = InverseTransformDirection(worldRotationAxis)
flinchLocalRotation *= AngleAxis(magnitude, localRotationAxis)

Three flinch modes:

  1. Directional (default): rotation around the cross-up axis — provides spatial awareness of damage source.
  2. RollOnly: isolates to the camera's roll axis (Z-local), zeroing pitch/yaw components.
  3. Disabled: no flinch.

Flinch decays via Quaternion.Lerp(flinchLocalRotation, identity, 4 * delta).

Explosion Camera Shake

FlinchFromExplosion(Vector3 position, float radius, float magnitudeDegrees):

distance = |cameraPos - explosionPos|
distanceMultiplier = 1 - (distance/radius)²  (exponential falloff)
magnitude *= Toughness_skillMultiplier * distanceMultiplier * shakeIntensity
targetExplosionLocalRotation *= AngleAxis(magnitude, localRotationAxis)

The explosion rotation uses a spring-damper (Rk4SpringQ targetExplosionLocalRotation) for smooth buildup and recovery. smoothedExplosionLocalRotation provides an additional smoothing layer.

Third-Person Camera Sweep

sphereCastCamera(Vector3 origin, Vector3 direction, float length, int layerMask) prevents the third-person camera from clipping through geometry:

hitCount = Physics.SphereCastNonAlloc(ray, radius=0.39f, hits, length, layerMask, IgnoreTrigger)
closestDistance = Min(all hit distances)
return origin + direction * closestDistance

The sweep radius (0.39f) matches PlayerStance.RADIUS to ensure the camera cannot enter spaces the player cannot occupy. Used for vehicle cameras, killcam, and standard third-person.

Freecam System

Five freecam modes for admin/plugin use:

ModeKey (Shift+)Behavior
IsControllingFreecamF1Free-roaming camera independent of player
isTrackingF2Camera follows the player but is freely aimable
isLockingF3Camera locked to a world position, orbits around it
isFocusingF4Camera focuses on the player from a lock position
isSmoothingF5Applies lerp (4×) to camera movement, on by default with tracking

Freecam uses orbitPitch, orbitYaw, and orbitPosition (offset from player/lock position) as its coordinate system. orbitSpeed = 16 controls movement rate.

Freecam camera transform can be copied/restored via clipboard (Ctrl+C, Ctrl+V) in [position]:[pitch],[yaw] format.

Permission System

FeatureAdmin OnlyPlugin Override
Freecam (F1-F5)YessendFreecamAllowed(true)
Workzone (F6)YessendWorkzoneAllowed(true)
Spec Stats (F7)YessendSpecStatsAllowed(true)

The ReceiveFreecamAllowed RPC also disables freecam if permission is revoked while active.

Aim Transform (player.look.aim)

The AimFire transform is the authoritative origin for all weapon raycasts. Its position and rotation are updated in updateAim(float delta):

  • Position: Lerp(localPosition, Vector3.up * heightLook, 4 * delta).
  • Yaw rotation: parent rotation set to Quaternion.Euler(0, yaw, 0); when leaning is obstructed, parent rotation is identity.
  • Pitch rotation: Quaternion.Euler(pitch - 90 + scopeSway.x, scopeSway.y, 0).
  • For turret seats with useAimCamera, aim is overridden to the turret aim transform.

Eyes Position

  • getEyesPosition(): returns aim.position (the weapon-fire raycast origin).
  • GetEyesPositionWithoutLeaning(): transforms through the parent to bypass leaning rotation, useful for third-person prediction.

PlayerTool Utility Class

PlayerTool at Unturned/Tools/PlayerTool.cs is a static utility class with several categories of methods.

Reputation Display

getRepKey(int rep): Maps reputation integer to string key used for UI icon lookup and localization:

  • ≤ -200 → "Villain", -100 to -199 → "Bandit", -33 to -99 → "Gangster", -8 to -32 → "Outlaw", -1 to -7 → "Thug"
  • 0 → "Neutral"
  • 1–7 → "Vigilante", 8–32 → "Constable", 33–99 → "Deputy", 100–199 → "Sheriff", ≥ 200 → "Paragon"

getRepTexture(int rep): Loads reputation icon texture from UI/Player/Icons/Reputation bundle. getRepTitle(int rep): Formats localized reputation title string. getRepColor(int rep): Returns color:

  • 0 → White (Neutral)
  • Negative → White→Yellow→Red blend (two-phase: 0 to -100 white→yellow, -100 to -200 yellow→red)
  • Positive → White→Green blend (0 to 200)

Player Lookup

MethodInputReturns
getSteamPlayer(string name)Player or character name (partial match via NameTool.checkNames)SteamPlayer
getSteamPlayer(ulong steamID)64-bit Steam IDSteamPlayer
getSteamPlayer(CSteamID)CSteamIDSteamPlayer
findSteamPlayerByChannel(int channel)Transport channel IDSteamPlayer
getPlayer(CSteamID)Steam IDPlayer component
getPlayer(string name)NamePlayer component
tryGetSteamPlayer(string input, out SteamPlayer)Attempts numeric parse then name lookupbool
tryGetSteamID(string input, out CSteamID)Attempts numeric parse then name lookupbool
getSteamPlayers()noneSteamPlayer[] array
EnumeratePlayers()noneIEnumerable<Player>

Spatial Queries

MethodPurpose
getPlayersInRadius(Vector3 center, float sqrRadius, List<Player>)Fills list with all players within squared distance
GetNearestPlayerInRadius(Vector3 center, float sqrRadius)Returns nearest player, or null

Both iterate all Provider.clients, checking client.player for null before processing.


Skillset Assignment

Skillset is stored on SteamPlayer as channel.owner.skillset (type EPlayerSkillset). It is assigned during character creation based on the selected character preset and influences:

  1. Skill cost reduction: SKILLSETS array in PlayerSkills maps each skillset to 1–3 SpecialitySkillPair entries. Skills matching these pairs cost half XP to upgrade (when Skillset_Reduces_Skill_Cost is enabled).
  2. Skill loss protection: Skills matching the skillset's paired entries cannot be reduced on death (when Skillset_Prevents_Skill_Loss is enabled).

No source file for PlayerTeleport was found in the available assembly; teleportation is handled through PlayerMovement.teleport() and PlayerLife.respawn() methods, which are in PlayerMovement.cs.


First-Person Viewmodel Management

The viewmodel camera alignment is managed through player.animator.viewmodelCamera and its transforms.

Viewmodel Switching

csharp
// During Update():
player.animator.viewmodelParentTransform.rotation = mainCamera.transform.rotation;

The viewmodel parent transform tracks the main camera rotation exactly, ensuring the weapon model in first-person follows the player's look direction.

Viewmodel Camera Offset

player.animator.viewmodelCameraLocalPositionOffset is modified by weapon recoil/shake and scoping:

StateOffsetPurpose
ADS (2D scope)Vector3.upPushes viewmodel off-screen to show scope overlay
ADS (single-render)Vector3.zeroViewmodel remains visible behind scope overlay
Bayonet jab(0, 0, 0.8)Viewmodel lurches forward during melee attack
Weapon shakeRandom ±0.05Visual weapon bob for repeated melee
UnscopedVector3.zeroNormal position

Aim Transform and Recoil

csharp
// In updateAim():
aim.localPosition = Vector3.Lerp(aim.localPosition, Vector3.up * heightLook, 4 * delta);
aim.localRotation = Quaternion.Euler(pitch - 90 + scopeSway.x, scopeSway.y, 0);

The aim transform has two interpolation dimensions:

  1. Vertical position: Lerps toward the stance-appropriate eye height.
  2. Rotation: Combines pitch offset (with 0=up convention) and scope sway.

Scope Sway

Scope sway is a sinusoidal camera movement while aiming:

csharp
player.animator.scopeSway = Vector3.Lerp(scopeSway,
    new Vector3(Sin(0.75 * swayTime) * sway, Sin(1.0 * swayTime) * sway, 0),
    Time.deltaTime * 4);

Accumulated sway is converted to pitch/yaw when exiting ADS (ConvertScopeSwayToInputRotation), preventing a jarring snap back to center.


Killcam Camera

When the player dies, a killcam rotation animates:

csharp
killcam = transform.rotation.eulerAngles.y; // Capture at death
// In Update():
if (player.life.isDead)
{
    killcam += -16 * Time.deltaTime;
    mainCamera.transform.rotation = Quaternion.Lerp(mainCamera.transform.rotation,
        Quaternion.Euler(32, killcam, 0), 2 * Time.deltaTime);
}

The camera rotates 16 degrees per second clockwise. Sphere-cast collision prevents clipping through terrain:

csharp
Vector3 origin = player.first.position + Vector3.up;
Vector3 direction = -mainCamera.transform.forward;
float length = 4.0f;
mainCamera.transform.position = sphereCastCamera(origin, direction, length, RayMasks.BLOCK_KILLCAM);

Third-Person Camera — Shoulder Offset

The third-person camera offset depends on the server's Allow_Shoulder_Camera config:

csharp
if (Provider.modeConfigData.Gameplay.Allow_Shoulder_Camera)
{
    direction = (forward * -1.5f) + (up * 0.25f) + (right * animator.shoulder * 1f);
}
else
{
    direction = (forward * -1.5f) + (up * 0.5f) + (right * animator.shoulder2 * 0.5f);
}

The shoulder and shoulder2 values are controlled by the player's lean input, creating over-the-shoulder camera offsets when peeking around corners.

Camera Collision Handling

csharp
Vector3 origin = player.first.position + new Vector3(0, thirdPersonEyeHeight, 0);
float length = 2.0f;
mainCamera.transform.position = sphereCastCamera(origin, direction, length, RayMasks.BLOCK_PLAYERCAM);

The BLOCK_PLAYERCAM layer mask includes world geometry but excludes the player's own collider. The sphere sweep radius (0.39f) prevents the camera from fitting through gaps smaller than the player character.


Vehicle Camera Modes

Third-Person Vehicle

Three camera modes controlled by EVehicleThirdPersonCameraMode:

  1. RotationDetached: Camera orbit is independent of vehicle rotation. mainCamera.transform.rotation = Quaternion.Euler(orbitPitch, orbitYaw, 0). Useful for aircraft where the camera should stay level.

  2. Fixed: mainCamera.transform.localRotation = Quaternion.Euler(orbitPitch, orbitYaw, 0). Camera rotates with the vehicle — default for cars.

  3. Speed-dependent distance: For all vehicles, dist = asset.camFollowDistance + speed * 0.1f, providing a dynamic follow distance based on velocity.

First-Person Vehicle

Driver first-person camera uses camDriverOffset + camPassengerOffset for vertical position:

csharp
float verticalOffset = asset.camDriverOffset + asset.camPassengerOffset;
mainCamera.transform.localPosition = Vector3.Lerp(
    localPosition,
    (Vector3.up * (heightLook + verticalOffset)) - (Vector3.left * yaw / 360f),
    4 * Time.deltaTime);

The yaw-dependent horizontal offset simulates look-around while keeping the camera parented to the vehicle.

Vehicle Speed Wind Effect

csharp
if (engine == PLANE && AnimatedForwardVelocity > 16)
    LevelLighting.UpdateForViewer(position, Lerp(0, 1, (speed - 16) / 8), delta);
else if ((engine == HELICOPTER || engine == BLIMP) && AnimatedForwardVelocity > 4)
    LevelLighting.UpdateForViewer(position, Lerp(0, 1, (speed - 8) / 8), delta);

At high speed, the viewport gains a wind effect overlay (fog/dust texture). The Lerp blends from 0 at threshold to 1.0 at (threshold + 8) m/s.


Procedural Camera Effects

Explosion Camera Shake Spring

The Rk4SpringQ type is a Runge-Kutta 4th-order spring-damper for quaternions:

csharp
targetExplosionLocalRotation.currentRotation = Quaternion.identity;
targetExplosionLocalRotation.targetRotation = Quaternion.identity;
// On explosion:
targetExplosionLocalRotation.currentRotation *= Quaternion.AngleAxis(magnitude, axis);

The RK4 spring provides smooth acceleration and deceleration, preventing the jittery feel of simple lerp-based shake. smoothedExplosionLocalRotation adds an additional smoothing layer for the visual output:

csharp
smoothedExplosionLocalRotation = Quaternion.Lerp(
    smoothedExplosionLocalRotation,
    targetExplosionLocalRotation.currentRotation,
    explosionSmoothingSpeed * Time.deltaTime);

Damage Flinch Smear Prevention

Flinch rotation is explicitly capped in the FlinchFromDamage method:

csharp
float magnitude = Mathf.Min(damageAmount, 25) * 0.5f;

This prevents high-damage hits from causing extreme camera rotation that would disorient the player.


Input Pipeline — Axis Processing

Mouse Look

csharp
// In Update() (local player only):
_look_x = ControlsSettings.mouseAimSensitivity * Input.GetAxis("mouse_x") * yawInputMultiplier;
_look_y = ControlsSettings.mouseAimSensitivity * -Input.GetAxis("mouse_y") * pitchInputMultiplier;

The mouseAimSensitivity is the player's configured sensitivity. yawInputMultiplier and pitchInputMultiplier are ±1.0, inverted by hallucination effects.

Keyboard Look

For vehicles with aircraft controls:

csharp
if (InputEx.GetKey(ControlsSettings.rollLeft))
    _look_x = vehicle != null ? -vehicle.asset.airTurnResponsiveness : -1;
if (InputEx.GetKey(ControlsSettings.pitchUp))
    _look_y = vehicle != null ? -vehicle.asset.airTurnResponsiveness : -1;

The airTurnResponsiveness from VehicleAsset provides per-vehicle tuning for flight controls. Flight inversion (ControlsSettings.invertFlight) flips _look_y independently of the standard invert setting.

Orbit Controls (Freecam + 3P Vehicle)

csharp
_orbitYaw += ControlsSettings.mouseAimSensitivity * zoomSensitivity * Input.GetAxis("mouse_x");
_orbitPitch -= ControlsSettings.mouseAimSensitivity * zoomSensitivity * Input.GetAxis("mouse_y");

Freecam orbit uses the same sensitivity chain but applies it to orbitYaw/orbitPitch instead of _yaw/_pitch. In third-person vehicles, orbit yaw is updated but pitch is driven by the main look system.

Safety Guards

csharp
if (float.IsInfinity(yaw) || float.IsNaN(yaw)) _yaw = 0;
if (float.IsInfinity(pitch) || float.IsNaN(pitch)) _pitch = 90;
if (float.IsInfinity(orbitYaw) || float.IsNaN(orbitYaw)) _orbitYaw = 0;
if (float.IsInfinity(orbitPitch) || float.IsNaN(orbitPitch)) _orbitPitch = 0;

These guards prevent NaN propagation from corrupted input or extreme frame-time spikes from breaking the camera orientation permanently.


First-Person Obstruction Detection

When the player is in first-person and not in a vehicle, the system checks for obstructions above the camera:

csharp
float castRadius = 0.25f;
Vector3 castOrigin = player.first.position + new Vector3(0, HEIGHT_LOOK_PRONE - castRadius, 0);
Vector3 castDirection = Vector3.up;
float castLength = PlayerMovement.HEIGHT_STAND - HEIGHT_LOOK_PRONE - castRadius;
RaycastHit upwardHit;
bool surfaceAboveHead = Physics.SphereCast(castOrigin, castRadius, castDirection, out upwardHit,
    castLength, RayMasks.BLOCK_PLAYERCAM_1P, QueryTriggerInteraction.Ignore);
if (surfaceAboveHead)
{
    float surfaceHeight = upwardHit.point.y - player.first.position.y;
    float maxEyesHeight = surfaceHeight - castRadius;
    eyes = Mathf.Min(eyes, maxEyesHeight);
}

This prevents the camera from clipping through low ceilings when transitioning from prone to crouch or stand.


Foliage Focus System

When zoomed in (scope or main camera zoom), the foliage system updates its focus point for dynamic LOD:

csharp
if (isMainCameraZoomFactorActive || isScopeActive || isSingleRenderScopeZoomFactorActive)
{
    FoliageSystem.isFocused = true;
    RaycastHit focus;
    if (Physics.Raycast(MainCamera.instance.transform.position, MainCamera.instance.transform.forward,
        out focus, FoliageSettings.focusDistance, RayMasks.FOLIAGE_FOCUS))
    {
        FoliageSystem.focusPosition = focus.point;
        // Use scope camera FOV when scoped
    }
}

The foliage system uses this focus point to allocate detail budget to the area the player is actually looking at rather than uniformly across the view frustum.


Integration Points

  • PlayerLook.aim: Used as the authoritative raycast origin by UseableGun, UseableMelee, UseableThrowable, and UseableConsumeable for all attack/use raycasts.
  • PlayerLook.pitch/yaw: Used by the animation system for body rotation and weapon alignment.
  • PlayerTool.EnumeratePlayers: Used by DamageTool for aggressor checks and by effect/alert systems.
  • Skillset: Referenced by PlayerSkills.cost() and PlayerSkills.CanDecreaseLevelOfSkill() for cost reduction and death penalty protection.