Jamie Greunbaum 0b6a8440fc - Camera follower will now track the upper-midsections of VRChat players.
- Camera tracking now has a follow speed setting.
- Camera anchor FOV scaler is now a separate object.
- Camera overlays now overlay properly once again.
- Implemented a much better check for whether a camera was detached.
- Improved code that detects when a game is over, and why it ended.
- Tweaked ending camera animation timing.
2026-03-21 01:02:39 -04:00

212 lines
5.6 KiB
C#

using CameraSystem;
using MMMaellon.LightSync;
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class CameraAnchor : UdonSharpBehaviour
{
[SerializeField] private CameraSystem_Console CameraSystemManager;
[Space]
[SerializeField] private float FOV = 60.0f;
[SerializeField] private float NearClippingPlane = 0.3f;
[SerializeField] private float FarClippingPlane = 1000.0f;
[Space]
[SerializeField] private Transform CameraRoot;
[Tooltip("Changing the Z scale of this object will change the FOV of the attached camera.")]
[SerializeField] private Transform CameraFOVScaler;
[UdonSynced] private int _AttachedCameraIndex = -1;
private Camera _AttachedCamera = null;
private string[] _FollowedPlayerNames = new string[0];
private string[] _FollowedPlayerNames_Cache = new string[0];
private VRCPlayerApi[] _FollowedPlayers = new VRCPlayerApi[0];
private float _CameraFollowSpeed = 1.0f;
void Update()
{
if (_AttachedCamera)
{
if (_AttachedCamera.transform.parent != CameraRoot)
{
Debug.Log("[CameraAnchor] Camera has been detached from " + gameObject.name + ".");
_AttachedCamera = null;
_AttachedCameraIndex = -1;
RequestSerialization();
}
else
{
_AttachedCamera.fieldOfView = FOV * CameraFOVScaler.transform.localScale.z;
}
}
if (_FollowedPlayers.Length > 0)
{
Vector3 CentroidSum = Vector3.zero;
for (int i = 0; i < _FollowedPlayers.Length; i++)
{
Vector3 LeftEyePosition = _FollowedPlayers[i].GetBonePosition(HumanBodyBones.LeftEye);
Vector3 RightEyePosition = _FollowedPlayers[i].GetBonePosition(HumanBodyBones.RightEye);
Vector3 LeftFootPosition = _FollowedPlayers[i].GetBonePosition(HumanBodyBones.LeftFoot);
Vector3 RightFootPosition = _FollowedPlayers[i].GetBonePosition(HumanBodyBones.RightFoot);
CentroidSum += new Vector3(
(LeftEyePosition.x + RightEyePosition.x + LeftFootPosition.x + RightFootPosition.x) / 4.0f,
(LeftEyePosition.y + RightEyePosition.y + LeftFootPosition.y + RightFootPosition.y) / 4.0f,
(LeftEyePosition.z + RightEyePosition.z + LeftFootPosition.z + RightFootPosition.z) / 4.0f);
}
Vector3 CentroidAverage = CentroidSum / _FollowedPlayers.Length;
Vector3 LookDirection = (CentroidAverage - transform.position).normalized;
CameraRoot.transform.rotation = Quaternion.LookRotation(
Vector3.Lerp(
CameraRoot.transform.forward,
Vector3.RotateTowards(CameraRoot.transform.forward, LookDirection, 10.0f, 0.0f),
_CameraFollowSpeed / 10.0f)
);
}
else
{
CameraRoot.transform.localRotation = Quaternion.identity;
}
}
public override void OnDeserialization(DeserializationResult Result)
{
_AttachCamera_Synced();
_FollowPlayers_Synced();
base.OnDeserialization(Result);
}
public override void OnPlayerLeft(VRCPlayerApi LeavingPlayer)
{
//foreach (VRCPlayerApi FollowedPlayer in _FollowedPlayers)
//{
// if (FollowedPlayer.displayName == LeavingPlayer.displayName)
// {
// }
//}
base.OnPlayerLeft(LeavingPlayer);
}
public void AttachCamera(Camera CameraComponent)
{
if (CameraSystemManager == null)
{
Debug.LogError("[CameraAnchor] No CameraSystemManager set for " + gameObject.name + "; can't find a camera without it");
return;
}
for (int i = 0; i < CameraSystemManager.camerasObjects.Length; i++)
{
if (CameraSystemManager.camerasObjects[i] == CameraComponent)
{
Debug.Log("[CameraAnchor] Attaching camera " + i + " to anchor " + gameObject.name);
_AttachedCameraIndex = i;
_AttachCamera_Synced();
RequestSerialization();
break;
}
}
}
public void FollowPlayers(string[] Players, float FollowSpeed = 1.0f)
{
_FollowedPlayerNames = Players;
_CameraFollowSpeed = FollowSpeed;
_FollowPlayers_Synced();
RequestSerialization();
}
public void StopFollowingPlayers()
{
_FollowedPlayerNames = new string[0];
_FollowPlayers_Synced();
RequestSerialization();
}
private void _AttachCamera_Synced()
{
if (_AttachedCameraIndex >= 0)
{
Camera NewCamera = CameraSystemManager.camerasObjects[_AttachedCameraIndex];
if (NewCamera != _AttachedCamera)
{
_AttachedCamera = NewCamera;
_AttachedCamera.gameObject.SetActive(true);
_AttachedCamera.transform.parent = CameraRoot;
_AttachedCamera.fieldOfView = FOV;
_AttachedCamera.nearClipPlane = NearClippingPlane;
_AttachedCamera.farClipPlane = FarClippingPlane;
_AttachedCamera.GetComponent<LightSync>().TeleportToLocalSpace(Vector3.zero, Quaternion.identity, true);
}
}
}
private void _FollowPlayers_Synced()
{
if (_IsPlayerListDifferent())
{
_FollowedPlayers = new VRCPlayerApi[_FollowedPlayerNames.Length];
VRCPlayerApi[] AllPlayers = new VRCPlayerApi[VRCPlayerApi.GetPlayerCount()];
VRCPlayerApi.GetPlayers(AllPlayers);
int i = 0;
foreach (string PlayerName in _FollowedPlayerNames)
{
foreach (VRCPlayerApi Player in AllPlayers)
{
if (Player.displayName == PlayerName)
{
_FollowedPlayers[i] = Player;
i++;
break;
}
}
}
Debug.Log("[CameraAnchor] Anchor " + gameObject.name + " is now following " + _FollowedPlayerNames);
_FollowedPlayerNames_Cache = _FollowedPlayerNames;
}
}
private bool _IsPlayerListDifferent()
{
if (_FollowedPlayerNames.Length != _FollowedPlayerNames_Cache.Length)
{
return true;
}
for (int i = 0; i < _FollowedPlayerNames.Length; i++)
{
if (_FollowedPlayerNames[i] != _FollowedPlayerNames_Cache[i])
{
return true;
}
}
return false;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Gizmos.DrawIcon(transform.position, "CameraAnchor", true);
}
#endif
}