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.instancetoplayer.first, sets local position toVector3.up * eyes, clears all freecam modes, closes workzone UI if open. - Switching to THIRD: Parents
MainCamera.instancetoplayer.transform. - Calls
UpdateSingleRenderScope()and firesonPerspectiveUpdated.
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.BOTHorECameraMode.VEHICLEwith 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)| Stance | heightLook | heightCamera |
|---|---|---|
| STAND / SPRINT / CLIMB / SWIM | 1.75m | 1.05m |
| CROUCH | 1.20m | 0.95m |
| PRONE | 0.35m | 0.30m |
| SITTING / DRIVING | 1.60m | 0.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
| Stance | Min | Max |
|---|---|---|
| STAND / SPRINT | 0° | 180° |
| CROUCH | 20° | 160° |
| PRONE | 60° | 120° |
| CLIMB | 45° | 100° |
| SWIM | 45° | 135° |
| SITTING / DRIVING (no turret) | 60° | 120° |
| Turret seat | turret.pitchMin | turret.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:
- Legacy:
zoomSensitivity = 1.0f / zoomFactor - ZoomFactor: Same as Legacy —
1.0 / scopeCameraZoomFactoror1.0 / mainCameraZoomFactor - 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:
- Base FOV:
OptionsSettings.DesiredVerticalFieldOfView(default 90). - Zoom factors:
mainCameraZoomFactor(weapon zoom) andscopeCameraZoomFactor(scope camera). - Sprint FOV boost:
OptionsSettings.sprintFovBoostIntensity * 10added during sprint stance. - Scope alpha: For single-render scopes, the main camera FOV interpolates between base and zoomed FOV based on
scopeAlpha(aim-in progress). - Freecam: Uses
freecamVerticalFieldOfView(initialized toDesiredVerticalFieldOfViewon 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.targetTexturerenders a separate camera to aRenderTexture.- 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 == OFFand not using 2D overlay.UnturnedPostProcessapplies 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_Overlayconfig andscopeQuality == 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'snightvisionColor/nightvisionFogIntensity, callsLevelLighting.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:
- Directional (default): rotation around the cross-up axis — provides spatial awareness of damage source.
- RollOnly: isolates to the camera's roll axis (Z-local), zeroing pitch/yaw components.
- 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 * closestDistanceThe 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:
| Mode | Key (Shift+) | Behavior |
|---|---|---|
IsControllingFreecam | F1 | Free-roaming camera independent of player |
isTracking | F2 | Camera follows the player but is freely aimable |
isLocking | F3 | Camera locked to a world position, orbits around it |
isFocusing | F4 | Camera focuses on the player from a lock position |
isSmoothing | F5 | Applies 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
| Feature | Admin Only | Plugin Override |
|---|---|---|
| Freecam (F1-F5) | Yes | sendFreecamAllowed(true) |
| Workzone (F6) | Yes | sendWorkzoneAllowed(true) |
| Spec Stats (F7) | Yes | sendSpecStatsAllowed(true) |
The ReceiveFreecamAllowed RPC also disables freecam if permission is revoked while active.
Aim Transform (player.look.aim)
The Aim → Fire 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(): returnsaim.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
| Method | Input | Returns |
|---|---|---|
getSteamPlayer(string name) | Player or character name (partial match via NameTool.checkNames) | SteamPlayer |
getSteamPlayer(ulong steamID) | 64-bit Steam ID | SteamPlayer |
getSteamPlayer(CSteamID) | CSteamID | SteamPlayer |
findSteamPlayerByChannel(int channel) | Transport channel ID | SteamPlayer |
getPlayer(CSteamID) | Steam ID | Player component |
getPlayer(string name) | Name | Player component |
tryGetSteamPlayer(string input, out SteamPlayer) | Attempts numeric parse then name lookup | bool |
tryGetSteamID(string input, out CSteamID) | Attempts numeric parse then name lookup | bool |
getSteamPlayers() | none | SteamPlayer[] array |
EnumeratePlayers() | none | IEnumerable<Player> |
Spatial Queries
| Method | Purpose |
|---|---|
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:
- Skill cost reduction:
SKILLSETSarray inPlayerSkillsmaps each skillset to 1–3SpecialitySkillPairentries. Skills matching these pairs cost half XP to upgrade (whenSkillset_Reduces_Skill_Costis enabled). - Skill loss protection: Skills matching the skillset's paired entries cannot be reduced on death (when
Skillset_Prevents_Skill_Lossis 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:
| State | Offset | Purpose |
|---|---|---|
| ADS (2D scope) | Vector3.up | Pushes viewmodel off-screen to show scope overlay |
| ADS (single-render) | Vector3.zero | Viewmodel remains visible behind scope overlay |
| Bayonet jab | (0, 0, 0.8) | Viewmodel lurches forward during melee attack |
| Weapon shake | Random ±0.05 | Visual weapon bob for repeated melee |
| Unscoped | Vector3.zero | Normal 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:
- Vertical position: Lerps toward the stance-appropriate eye height.
- 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:
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.Fixed:
mainCamera.transform.localRotation = Quaternion.Euler(orbitPitch, orbitYaw, 0). Camera rotates with the vehicle — default for cars.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, andUseableConsumeablefor all attack/use raycasts. - PlayerLook.pitch/yaw: Used by the animation system for body rotation and weapon alignment.
- PlayerTool.EnumeratePlayers: Used by
DamageToolfor aggressor checks and by effect/alert systems. - Skillset: Referenced by
PlayerSkills.cost()andPlayerSkills.CanDecreaseLevelOfSkill()for cost reduction and death penalty protection.
