Files
SkopjeMods/ThirdPerson/ThirdPersonRig.cs
T

712 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace Skopje.ThirdPerson
{
public class ThirdPersonDriver : MonoBehaviour
{
void Update()
{
ThirdPersonRig.TickBody();
}
void LateUpdate()
{
ThirdPersonRig.PrepareView();
}
}
static class ThirdPersonRig
{
const float MoveThreshold = 0.2f;
const float ExternalTurn = 35f;
static readonly List<Renderer> BodyRenderers = new List<Renderer>();
static readonly List<Renderer> ArmRenderers = new List<Renderer>();
static readonly List<Renderer> ShadowOnly = new List<Renderer>();
static readonly List<Collider> InteractColliders = new List<Collider>();
static readonly List<int> InteractLayers = new List<int>();
static readonly RaycastHit[] Hits = new RaycastHit[24];
static SkopjePlayer _player;
static bool _holding;
static bool _hasFacing;
static Quaternion _facing;
static Quaternion _incomingFacing;
static bool _hasIncoming;
static float _boom = 2.7f;
static float _ads;
static bool _capturedIk;
static bool _aimWasEnabled;
static bool _lookWasEnabled;
static float _nextScan;
static int _errors;
static bool _logged;
static int _viewId;
static bool _hasView;
static Vector3 _viewPos;
static Quaternion _viewRot;
static float _viewFov;
static bool _pushed;
static Vector3 _savedPos;
static Quaternion _savedRot;
static float _savedFov;
static bool _maskSaved;
static int _savedMask;
static int _bodyLayers;
static GameObject _thirdRoot;
static Vector3 _thirdScale;
static bool _scaledThird;
static bool _control;
static IntPtr _controllerPtr;
static Camera _viewCamera;
static Vector3 _movePlanar;
static float _turnSharpness;
static int _interactMask = -1;
static bool _layersShifted;
static float _nextColliderScan;
public static void OnClientStarted()
{
}
public static void OnClientStopped()
{
Release();
_player = null;
}
public static void OnPlayerStarted(SkopjePlayer player)
{
if (player != null && player.IsOwner)
_player = player;
}
public static void OnPlayerStopped(SkopjePlayer player)
{
if (_player == player)
{
Release();
_player = null;
}
}
public static void Shutdown()
{
Release();
_player = null;
}
public static void TickBody()
{
RefreshControl();
if (!_control)
{
if (_holding)
Release();
return;
}
try
{
CacheInteractMask();
if (Time.unscaledTime >= _nextColliderScan)
{
_nextColliderScan = Time.unscaledTime + 0.5f;
CacheInteractColliders();
}
Present(_player, null);
}
catch (Exception ex)
{
Fail(ex);
}
}
// UpdateRotation runs inside an IL2CPP call. Touching IsOwner from here re-enters the runtime and crashes the process.
public static void RememberBody(SkopjeCharacterController controller, Quaternion currentRotation)
{
if (!_control || controller is null || controller.Pointer != _controllerPtr)
{
_hasIncoming = false;
return;
}
_incomingFacing = FlatYaw(currentRotation);
_hasIncoming = true;
}
public static void HoldBodyFacing(SkopjeCharacterController controller, ref Quaternion currentRotation, float deltaTime)
{
if (!_hasIncoming || !_control || controller is null || controller.Pointer != _controllerPtr)
return;
Quaternion held = _hasFacing ? _facing : _incomingFacing;
if (!_hasFacing || Quaternion.Angle(held, _incomingFacing) > ExternalTurn)
held = _incomingFacing;
if (ThirdPersonPlugin.FaceMovement.Value && _movePlanar.sqrMagnitude > MoveThreshold * MoveThreshold)
{
float sharp = _turnSharpness > 0.01f ? _turnSharpness : 12f;
float blend = 1f - Mathf.Exp(-sharp * Mathf.Max(0f, deltaTime));
held = Quaternion.Slerp(held, Quaternion.LookRotation(_movePlanar.normalized, Vector3.up), blend);
}
_facing = FlatYaw(held);
_hasFacing = true;
currentRotation = _facing;
}
static void RefreshControl()
{
_control = false;
_controllerPtr = IntPtr.Zero;
if (!CanControl(_player))
return;
SkopjeCharacterController body = _player.SkopjeCharacterController;
SkopjeFPSCamera camera = _player.SkopjeFPSCamera;
if (body is null || camera is null || body.Pointer == IntPtr.Zero || camera.Pointer == IntPtr.Zero)
return;
_controllerPtr = body.Pointer;
_movePlanar = body.moveInputVector;
_movePlanar.y = 0f;
_turnSharpness = body.OrientationSharpness;
_control = true;
}
public static void PrepareView()
{
if (_pushed && _viewCamera != null)
{
_viewCamera.transform.SetPositionAndRotation(_savedPos, _savedRot);
_viewCamera.fieldOfView = _savedFov;
_pushed = false;
}
_hasView = false;
_viewId = 0;
if (!_control || _player is null)
return;
SkopjeFPSCamera fps = _player.SkopjeFPSCamera;
if (fps is null)
return;
Camera cam = fps.mainCamera != null ? fps.mainCamera : Camera.main;
if (cam == null)
return;
try
{
// Offset from the live eye camera. Offset interiors move that camera, not the root position.
Quaternion look = ReadLook(fps, cam);
Vector3 eye = cam.transform.position;
float adsTarget = fps.adsMode ? 1f : 0f;
_ads = Mathf.MoveTowards(_ads, adsTarget, Time.deltaTime / 0.12f);
float distance = Mathf.Lerp(Clamp(ThirdPersonPlugin.Distance.Value, 0.8f, 8f), Clamp(ThirdPersonPlugin.AdsDistance.Value, 0.6f, 6f), _ads);
float shoulder = Mathf.Lerp(Clamp(ThirdPersonPlugin.Shoulder.Value, 0f, 2.5f), Clamp(ThirdPersonPlugin.AdsShoulder.Value, 0f, 2f), _ads);
float height = Clamp(ThirdPersonPlugin.Height.Value, -1f, 2f);
if (_boom <= 0.01f)
_boom = distance;
_boom = Mathf.Lerp(_boom, distance, 1f - Mathf.Exp(-16f * Time.deltaTime));
Vector3 side = eye + look * new Vector3(shoulder, height, 0f);
Vector3 back = look * Vector3.back;
float allowed = PullIn(_player, side, back, _boom);
Vector3 camPos = side + back * allowed;
Vector3 aim = AimPoint(_player, eye, look * Vector3.forward);
Vector3 toAim = aim - camPos;
Vector3 up = look * Vector3.up;
_viewPos = camPos;
_viewFov = cam.fieldOfView + Clamp(ThirdPersonPlugin.FovBonus.Value, 0f, 40f) * (1f - _ads);
if (toAim.sqrMagnitude < 0.0001f || up.sqrMagnitude < 0.0001f || Mathf.Abs(Vector3.Dot(toAim.normalized, up.normalized)) > 0.995f)
_viewRot = look;
else
_viewRot = Quaternion.LookRotation(toAim, up);
_viewId = cam.GetInstanceID();
_viewCamera = cam;
_hasView = true;
Present(_player, cam);
if (!_logged)
{
_logged = true;
ThirdPersonPlugin.LogSource.LogInfo("Right-shoulder view is active. World streaming still uses the eye camera.");
}
}
catch (Exception ex)
{
Fail(ex);
}
}
// Render-only. The gameplay camera stays on the eyes so chunk streaming does not follow the shoulder.
public static void PushView(Camera camera)
{
if (!_hasView || camera == null || camera.GetInstanceID() != _viewId || _pushed)
return;
_savedPos = camera.transform.position;
_savedRot = camera.transform.rotation;
_savedFov = camera.fieldOfView;
camera.transform.SetPositionAndRotation(_viewPos, _viewRot);
camera.fieldOfView = _viewFov;
if (_maskSaved)
camera.cullingMask |= _bodyLayers;
RevealCached();
_pushed = true;
}
public static void PopView(Camera camera)
{
if (!_pushed || camera == null || camera.GetInstanceID() != _viewId)
return;
camera.transform.SetPositionAndRotation(_savedPos, _savedRot);
camera.fieldOfView = _savedFov;
_pushed = false;
}
static Quaternion ReadLook(SkopjeFPSCamera fps, Camera cam)
{
if (fps.VCamera != null && fps.VCamera.transform != cam.transform)
return fps.VCamera.transform.rotation;
if (fps.SocketTransform != null && fps.SocketTransform != cam.transform)
return fps.SocketTransform.rotation;
return cam.transform.rotation;
}
static Vector3 Pivot(SkopjePlayer player)
{
float height = Clamp(ThirdPersonPlugin.PivotHeight.Value, 0.4f, 2.4f);
var body = player.SkopjeCharacterController;
if (body != null)
{
if (body.standingHeight > 0.5f)
height = Mathf.Min(height, body.standingHeight * 0.78f);
float crouch = body.CrouchedHeightMult > 0.05f ? body.CrouchedHeightMult : 0.55f;
height *= Mathf.Lerp(1f, crouch, Mathf.Clamp01(body.currentCrouch01));
}
return player.transform.position + Vector3.up * height;
}
static Vector3 AimPoint(SkopjePlayer player, Vector3 origin, Vector3 direction)
{
float reach = Clamp(ThirdPersonPlugin.AimDistance.Value, 5f, 200f);
if (direction.sqrMagnitude < 0.0001f)
direction = Vector3.forward;
direction.Normalize();
int count = Physics.RaycastNonAlloc(origin, direction, Hits, reach, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
float best = float.MaxValue;
Vector3 point = origin + direction * reach;
for (int i = 0; i < count; i++)
{
if (Hits[i].collider == null || IsOwn(player, Hits[i].collider))
continue;
if (Hits[i].distance < best)
{
best = Hits[i].distance;
point = Hits[i].point;
}
}
return point;
}
static float PullIn(SkopjePlayer player, Vector3 origin, Vector3 direction, float distance)
{
const float radius = 0.18f;
int count = Physics.SphereCastNonAlloc(origin, radius, direction, Hits, distance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
float best = distance;
for (int i = 0; i < count; i++)
{
if (Hits[i].collider == null || IsOwn(player, Hits[i].collider))
continue;
float room = Mathf.Max(0.35f, Hits[i].distance);
if (room < best)
best = room;
}
return best;
}
static void Present(SkopjePlayer player, Camera cam)
{
GameObject third = player.ThirdPerson;
if (third != null)
{
if (!third.activeSelf)
third.SetActive(true);
if (!_scaledThird && third.transform.localScale.sqrMagnitude < 0.0001f)
{
_thirdScale = third.transform.localScale;
third.transform.localScale = Vector3.one;
_scaledThird = true;
_thirdRoot = third;
}
}
if (player.ThirdPersonBackpackParent != null && !player.ThirdPersonBackpackParent.activeSelf)
player.ThirdPersonBackpackParent.SetActive(true);
var anim = player.TPAnimationController;
if (anim != null)
{
if (!_capturedIk)
{
_aimWasEnabled = anim.AimAtMotor != null && anim.AimAtMotor.enabled;
_lookWasEnabled = anim.LookAtMotor != null && anim.LookAtMotor.enabled;
_capturedIk = true;
}
if (anim.AimAtMotor != null)
anim.AimAtMotor.enabled = false;
if (anim.LookAtMotor != null)
anim.LookAtMotor.enabled = false;
}
if (Time.unscaledTime >= _nextScan)
{
_nextScan = Time.unscaledTime + 0.25f;
CacheVisuals(player);
}
RevealCached();
if (cam != null)
IncludeBodyLayers(cam);
_holding = true;
}
static void CacheVisuals(SkopjePlayer player)
{
RestoreArms();
BodyRenderers.Clear();
ArmRenderers.Clear();
_bodyLayers = 0;
Transform third = player.ThirdPerson != null ? player.ThirdPerson.transform : null;
if (player.ThirdPersonRenderer != null)
AddBody(player.ThirdPersonRenderer);
if (third != null)
{
Renderer[] found = third.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < found.Length; i++)
AddBody(found[i]);
}
if (player.SkopjeCharacterController != null && player.SkopjeCharacterController.MeshRoot != null)
{
Renderer[] found = player.SkopjeCharacterController.MeshRoot.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < found.Length; i++)
{
Renderer renderer = found[i];
if (renderer == null)
continue;
if (third != null && (renderer.transform == third || renderer.transform.IsChildOf(third)))
AddBody(renderer);
else if (player.FirstPerson == null || !renderer.transform.IsChildOf(player.FirstPerson.transform))
AddBody(renderer);
}
}
CollectArms(player.FirstPerson, third);
}
static void AddBody(Renderer renderer)
{
if (renderer == null || BodyRenderers.Contains(renderer))
return;
BodyRenderers.Add(renderer);
_bodyLayers |= 1 << renderer.gameObject.layer;
if (renderer.shadowCastingMode == ShadowCastingMode.ShadowsOnly && !ShadowOnly.Contains(renderer))
{
ShadowOnly.Add(renderer);
renderer.shadowCastingMode = ShadowCastingMode.On;
}
}
static void CollectArms(GameObject root, Transform thirdPerson)
{
if (root == null)
return;
if (thirdPerson != null && (root.transform == thirdPerson || root.transform.IsChildOf(thirdPerson)))
return;
Renderer[] found = root.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < found.Length; i++)
{
Renderer renderer = found[i];
if (renderer == null || BodyRenderers.Contains(renderer) || ArmRenderers.Contains(renderer))
continue;
if (thirdPerson != null && renderer.transform.IsChildOf(thirdPerson))
continue;
ArmRenderers.Add(renderer);
}
}
static void RevealCached()
{
if (_thirdRoot != null && !_thirdRoot.activeSelf)
_thirdRoot.SetActive(true);
for (int i = 0; i < BodyRenderers.Count; i++)
{
Renderer renderer = BodyRenderers[i];
if (renderer == null)
continue;
renderer.forceRenderingOff = false;
renderer.enabled = true;
if (renderer is SkinnedMeshRenderer skin)
skin.updateWhenOffscreen = true;
}
for (int i = 0; i < ArmRenderers.Count; i++)
{
if (ArmRenderers[i] != null)
ArmRenderers[i].forceRenderingOff = true;
}
}
static void IncludeBodyLayers(Camera cam)
{
if (_bodyLayers == 0)
return;
if (!_maskSaved)
{
_savedMask = cam.cullingMask;
_maskSaved = true;
}
cam.cullingMask |= _bodyLayers;
}
public static void HideBodiesFromInteract()
{
RestoreInteractLayers();
if (_interactMask == 0 || InteractColliders.Count == 0)
return;
int free = 2;
if ((_interactMask & (1 << free)) != 0)
{
free = -1;
for (int layer = 8; layer < 32; layer++)
{
if ((_interactMask & (1 << layer)) == 0)
{
free = layer;
break;
}
}
}
if (free < 0)
return;
for (int i = 0; i < InteractColliders.Count; i++)
{
Collider collider = InteractColliders[i];
if (collider == null)
{
InteractLayers.Add(0);
continue;
}
int layer = collider.gameObject.layer;
InteractLayers.Add(layer);
if ((_interactMask & (1 << layer)) != 0)
collider.gameObject.layer = free;
}
_layersShifted = true;
}
public static void RestoreInteractLayers()
{
if (!_layersShifted)
return;
for (int i = InteractColliders.Count - 1; i >= 0; i--)
{
Collider collider = InteractColliders[i];
if (collider != null && i < InteractLayers.Count)
collider.gameObject.layer = InteractLayers[i];
}
InteractLayers.Clear();
_layersShifted = false;
}
static void CacheInteractMask()
{
InteractController interact = _player.InteractController;
if (interact == null)
{
_interactMask = 0;
return;
}
_interactMask = interact.BlockingLayers.value | interact.InteractLayers.value;
}
static void CacheInteractColliders()
{
if (_layersShifted)
return;
InteractColliders.Clear();
SkopjePlayer[] players = UnityEngine.Object.FindObjectsOfType<SkopjePlayer>();
for (int i = 0; i < players.Length; i++)
AddPlayerColliders(players[i]);
}
static void AddPlayerColliders(SkopjePlayer player)
{
if (player == null || player.ThirdPerson == null)
return;
Collider owner = player.OwnerCollider;
Collider[] found = player.ThirdPerson.GetComponentsInChildren<Collider>(true);
for (int i = 0; i < found.Length; i++)
{
Collider collider = found[i];
if (collider == null || collider.isTrigger || collider == owner || InteractColliders.Contains(collider))
continue;
InteractColliders.Add(collider);
}
}
static void RestoreArms()
{
for (int i = 0; i < ArmRenderers.Count; i++)
{
if (ArmRenderers[i] != null)
ArmRenderers[i].forceRenderingOff = false;
}
}
static void Release()
{
if (_pushed && _viewCamera != null)
{
_viewCamera.transform.SetPositionAndRotation(_savedPos, _savedRot);
_viewCamera.fieldOfView = _savedFov;
}
if (_maskSaved && _viewCamera != null)
_viewCamera.cullingMask = _savedMask;
for (int i = 0; i < ShadowOnly.Count; i++)
{
if (ShadowOnly[i] != null)
ShadowOnly[i].shadowCastingMode = ShadowCastingMode.ShadowsOnly;
}
if (_scaledThird && _thirdRoot != null)
_thirdRoot.transform.localScale = _thirdScale;
RestoreInteractLayers();
InteractColliders.Clear();
RestoreArms();
BodyRenderers.Clear();
ArmRenderers.Clear();
ShadowOnly.Clear();
if (_player != null)
{
var anim = _player.TPAnimationController;
if (_capturedIk && anim != null)
{
if (anim.AimAtMotor != null)
anim.AimAtMotor.enabled = _aimWasEnabled;
if (anim.LookAtMotor != null)
anim.LookAtMotor.enabled = _lookWasEnabled;
}
try
{
_player.RefreshActiveModels();
}
catch (Exception ex)
{
Fail(ex);
}
}
_capturedIk = false;
_holding = false;
_hasFacing = false;
_hasIncoming = false;
_ads = 0f;
_logged = false;
_control = false;
_controllerPtr = IntPtr.Zero;
_hasView = false;
_pushed = false;
_maskSaved = false;
_scaledThird = false;
_thirdRoot = null;
_viewCamera = null;
_bodyLayers = 0;
}
static bool CanControl(SkopjePlayer player)
{
if (!ThirdPersonPlugin.Enabled.Value || player == null || !player.IsOwner)
return false;
if (player.IsDead || player.IsFallen || player.IsSpectating || player.IsPaused || player.InInventory)
return false;
if (player.InsideVehicle != null)
return false;
var cine = player._cinematicController;
if (cine != null && cine.PlayingCinematic != null && cine.PlayingCinematic.PlayingCinematic != null)
return false;
return player.SkopjeFPSCamera != null;
}
static bool IsOwn(SkopjePlayer player, Collider collider)
{
if (player == null || collider == null)
return false;
Transform transform = collider.transform;
Transform root = player.transform;
return transform == root || transform.IsChildOf(root);
}
static Quaternion FlatYaw(Quaternion rotation)
{
Vector3 forward = Vector3.ProjectOnPlane(rotation * Vector3.forward, Vector3.up);
if (forward.sqrMagnitude < 0.0001f)
forward = Vector3.ProjectOnPlane(rotation * Vector3.up, Vector3.up);
if (forward.sqrMagnitude < 0.0001f)
forward = Vector3.forward;
return Quaternion.LookRotation(forward.normalized, Vector3.up);
}
static float Clamp(float value, float min, float max)
{
return Mathf.Clamp(value, min, max);
}
static void Fail(Exception ex)
{
if (_errors++ < 5 && ThirdPersonPlugin.LogSource != null)
ThirdPersonPlugin.LogSource.LogWarning("Third person failed: " + ex.Message);
}
}
}