Files
2026-06-07 16:58:24 +01:00

482 lines
17 KiB
C#

using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
// Sends Events:
// - ClientSimOnPlayerTeleportedEvent
// - ClientSimOnPlayerRespawnEvent
// - ClientSimOnPlayerMovedEvent
// Listens to Events:
// - ClientSimMenuStateChangedEvent
// - ClientSimMenuRespawnClickedEvent
// - ClientSimMouseReleasedEvent
// - ClientSimPlayerDeathStatusChangedEvent
// - ClientSimOnTrackingScaleUpdateEvent
// Listens to Input Events:
// - Jump
// - Run
[AddComponentMenu("")]
[DefaultExecutionOrder(-3000)] // Update before player raycasting
public class ClientSimPlayerController : ClientSimBehaviour, IDisposable
{
private const float CROUCH_SPEED_MULTIPLIER = 0.35f;
private const float PRONE_SPEED_MULTIPLIER = 0.15f;
private const float STICK_TO_GROUND_FORCE = 2f;
private const float RATE_OF_AIR_ACCELERATION = 5f;
private IClientSimPlayerStationManager _stationManager;
private IClientSimPlayerLocomotionData _playerLocomotionData;
private IClientSimPlayerApiProvider _playerApi;
private IClientSimSceneManager _sceneManager;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private IClientSimTrackingProvider _trackingProvider;
private CharacterController _characterController;
private Transform _cameraProxyObject;
private bool _isDead;
private bool _isWalking = true; // Player defaults to walking
private bool _jump;
private Vector2 _prevInput;
// Check if the directionality changed to apply "stutter stepping" for legacy locomotion.
private bool _directionChanged;
private bool _velSet;
// TODO fix handling of SetVelocity as retaining the velocity causes strange bugs due to ignoring collisions
// that normally would stop the player.
private Vector3 _playerRetainedVelocity;
private bool _menuIsOpen;
private bool _mouseReleased;
protected override void Awake()
{
base.Awake();
_characterController = GetComponent<CharacterController>();
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
IClientSimPlayerApiProvider playerApiProvider,
IClientSimPlayerLocomotionData locomotionData,
IClientSimSceneManager sceneManager,
IClientSimProxyObjectProvider proxyProvider,
IClientSimTrackingProvider trackingProvider,
IClientSimPlayerStationManager stationManager)
{
_eventDispatcher = eventDispatcher;
_input = input;
_playerApi = playerApiProvider;
_playerLocomotionData = locomotionData;
_sceneManager = sceneManager;
_trackingProvider = trackingProvider;
_stationManager = stationManager;
_cameraProxyObject = proxyProvider.CameraProxy().transform;
Subscribe();
}
private void Start()
{
NotifyPlayerMoved();
}
private void OnDestroy()
{
Dispose();
}
private void Subscribe()
{
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeJump(JumpInput);
_input?.SubscribeRun(RunInput);
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Subscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
_eventDispatcher.Subscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher.Subscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
_eventDispatcher.Subscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
}
public void Dispose()
{
_input?.UnsubscribeJump(JumpInput);
_input?.UnsubscribeRun(RunInput);
_eventDispatcher.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Unsubscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
_eventDispatcher.Unsubscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher.Unsubscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
_eventDispatcher.Unsubscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
}
#region Input Events
private void JumpInput(bool value, HandType hand)
{
// Only handle on down, and not on release.
if (!value)
{
return;
}
if (!_jump && _characterController.isGrounded && _playerLocomotionData.GetJump() > 0)
{
_jump = true;
}
}
private void RunInput(bool value)
{
_isWalking = !value;
}
#endregion
#region ClientSim Events
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_menuIsOpen = stateChangedEvent.isMenuOpen;
}
private void MenuRespawnEvent(ClientSimMenuRespawnClickedEvent stateChangedEvent)
{
Respawn();
}
private void MouseReleasedEvent(ClientSimMouseReleasedEvent mouseReleasedEvent)
{
_mouseReleased = mouseReleasedEvent.isReleased;
}
private void CombatStatusEvent(ClientSimPlayerDeathStatusChangedEvent combatStatusEvent)
{
_isDead = combatStatusEvent.isDead;
}
private void OnTrackingScaleUpdated(ClientSimOnTrackingScaleUpdateEvent scaleUpdatedEvent)
{
NotifyPlayerMoved();
}
#endregion
public Vector3 GetPosition()
{
return transform.position;
}
public Quaternion GetRotation()
{
return transform.rotation;
}
public Vector3 GetVelocity()
{
return _characterController.velocity;
}
public void SetVelocity(Vector3 velocity)
{
_playerRetainedVelocity = velocity;
_velSet = true;
_jump = false;
}
public bool IsGrounded()
{
return _characterController.isGrounded;
}
public void Respawn()
{
Transform spawnPoint = _sceneManager.GetSpawnPoint(false);
Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
Teleport(position, spawnPoint.rotation, false);
_eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player = _playerApi.Player });
}
public void Respawn(int index)
{
Transform spawnPoint = _sceneManager.GetSpawnPoint(index);
if (spawnPoint == null)
{
this.LogError($"Spawn {index} not found. Spawning at spawn 0");
spawnPoint = _sceneManager.GetSpawnPoint(0);
}
Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
Teleport(position, spawnPoint.rotation, false);
_eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player = _playerApi.Player });
}
public void Teleport(Transform point, bool fromPlaySpace)
{
Teleport(point.position, Quaternion.Euler(0, point.rotation.eulerAngles.y, 0), fromPlaySpace);
}
public void Teleport(Vector3 position, Quaternion floorRotation, bool fromPlaySpace)
{
floorRotation = Quaternion.Euler(0, floorRotation.eulerAngles.y, 0);
if (fromPlaySpace)
{
VRCPlayerApi.TrackingData playspaceData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Origin);
Vector3 playspacePos = playspaceData.position - transform.position;
Quaternion playspaceRot = playspaceData.rotation * Quaternion.Inverse(transform.rotation);
floorRotation = floorRotation * Quaternion.Inverse(playspaceRot);
position += floorRotation * -playspacePos;
}
this.Log($"Moving player to {position.ToString("F3")} " +
$"and rotation {floorRotation.eulerAngles.ToString("F3")} " +
$"(fromPlaySpace={fromPlaySpace})");
transform.rotation = floorRotation;
transform.position = position;
NotifyPlayerMoved();
_eventDispatcher.SendEvent(new ClientSimOnPlayerTeleportedEvent { player = _playerApi.Player });
Physics.SyncTransforms();
}
#region Stations
public void EnterStation(IClientSimStation station)
{
if (!station.IsMobile())
{
_characterController.enabled = false;
Transform spawn = station.EnterLocation();
Teleport(spawn, false);
}
// VRChatBug: Note that in the else case, the player is teleported to a location that is twice the distance
// to the station, but since this appears to be a bug, it will not be implemented.
}
public void ExitStation(IClientSimStation station, bool skipTeleport)
{
_characterController.enabled = true;
if (!skipTeleport)
{
Transform spawn = station.ExitLocation();
Teleport(spawn, false);
}
_jump = false;
}
public void SitPosition(Transform seat)
{
transform.SetPositionAndRotation(seat.position, seat.rotation);
NotifyPlayerMoved();
}
#endregion
private void Update()
{
// Handle below respawn height.
if (transform.position.y < _sceneManager.GetRespawnHeight())
{
Respawn();
}
GetInput();
NotifyPlayerMoved();
}
private void FixedUpdate()
{
Physics.SyncTransforms();
Vector2 speed = GetSpeed();
Vector2 input = _prevInput;
if (!_stationManager.CanPlayerMove(input.magnitude))
{
return;
}
if (_menuIsOpen || _isDead)
{
input = Vector2.zero;
_jump = false;
}
// Immobile does not affect Jump
if (_playerLocomotionData.GetImmobilized())
{
input = Vector2.zero;
}
// Always move along the camera forward as it is the direction that it being aimed at
Vector3 desiredMove = input.y * speed.x * transform.forward + input.x * speed.y * transform.right;
desiredMove.y = 0;
float gravityContribution = _playerLocomotionData.GetGravityStrength() * Time.fixedDeltaTime * Physics.gravity.y;
if (!_velSet)
{
if (_characterController.isGrounded)
{
_playerRetainedVelocity = Vector3.zero;
_playerRetainedVelocity.y = -STICK_TO_GROUND_FORCE;
if (_jump)
{
if (!_playerLocomotionData.GetUseLegacyLocomotion())
{
_playerRetainedVelocity = desiredMove;
}
_playerRetainedVelocity.y = _playerLocomotionData.GetJump();
desiredMove = Vector3.zero;
_jump = false;
}
}
else
{
// Slowly add velocity from movement inputs
if (!_playerLocomotionData.GetUseLegacyLocomotion())
{
Vector3 localVelocity = transform.InverseTransformVector(_characterController.velocity);
localVelocity.x = Mathf.Clamp(localVelocity.x, -speed.y, speed.y);
localVelocity.z = Mathf.Clamp(localVelocity.z, -speed.x, speed.x);
Vector3 maxAc = new Vector3(speed.y - localVelocity.x, 0, speed.x - localVelocity.z);
Vector3 minAc = new Vector3(-speed.y - localVelocity.x, 0, -speed.x - localVelocity.z);
Vector3 inputAcceleration = Time.fixedDeltaTime * RATE_OF_AIR_ACCELERATION * new Vector3(input.x * speed.y, 0, input.y * speed.x);
inputAcceleration.x = Mathf.Clamp(inputAcceleration.x, minAc.x, maxAc.x);
inputAcceleration.z = Mathf.Clamp(inputAcceleration.z, minAc.z, maxAc.z);
inputAcceleration = transform.TransformVector(inputAcceleration);
_playerRetainedVelocity += inputAcceleration;
desiredMove = Vector3.zero;
}
// Legacy stutter stepping
else if (_directionChanged)
{
_playerRetainedVelocity = Vector3.zero;
}
_playerRetainedVelocity.y += gravityContribution;
}
}
else // Dumb behavior that hopefully needs to be removed
{
_characterController.Move(new Vector3(desiredMove.x * 0.05f, desiredMove.y * 0.05f + gravityContribution, desiredMove.z * 0.05f) * Time.fixedDeltaTime);
desiredMove = Vector3.zero;
}
desiredMove += _playerRetainedVelocity;
_characterController.Move(desiredMove * Time.fixedDeltaTime);
_velSet = false;
NotifyPlayerMoved();
}
#region Input
private void GetInput()
{
GetMovementInput();
if (_menuIsOpen && Mathf.Max(_prevInput.x, _prevInput.y) > 0)
{
_input.SendToggleMenuEvent(true, HandType.RIGHT);
}
// Only allow these input actions while the menu is closed
if (!_menuIsOpen)
{
RotateView();
}
}
private void GetMovementInput()
{
Vector2 input = _input.GetMovementAxes();
if (input.sqrMagnitude > 1)
{
input.Normalize();
}
_directionChanged = (input.sqrMagnitude < 1e-3 ^ _prevInput.sqrMagnitude < 1e-3);
_prevInput = input;
}
// TODO Move rotation of the player controller to be done in the tracking provider and have the player controller
// copy the head rotation. This would allow more generic handling of mouse released, VR snap turning, and locked in station.
private void RotateView()
{
// Allow player controller to look left and right when not in a locked station and for desktop users
// when the mouse is not released..
if (!_mouseReleased && !_stationManager.IsLockedInStation())
{
float yRot = _input.GetLookHorizontal();
transform.rotation *= Quaternion.Euler(0f, yRot, 0f);
}
}
// Used in tests to help look a specific direction
public void LookTowardsPoint(Vector3 point)
{
if (_stationManager.IsLockedInStation())
{
return;
}
point.y = transform.position.y;
transform.LookAt(point);
}
#endregion
// Notify all systems that are dependent on the player's position that the player has moved to a new location.
private void NotifyPlayerMoved()
{
_eventDispatcher.SendEvent(new ClientSimOnPlayerMovedEvent { player = _playerApi.Player });
if (_cameraProxyObject == null)
{
return;
}
var cameraTrackingData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
_cameraProxyObject.SetPositionAndRotation(cameraTrackingData.position, cameraTrackingData.rotation);
_cameraProxyObject.localScale = _trackingProvider.GetTrackingScale() * Vector3.one;
}
private Vector2 GetSpeed()
{
// TODO check current bindings to see if non keyboard and only use runspeed.
Vector2 speed = new Vector2(
_isWalking? _playerLocomotionData.GetWalkSpeed() : _playerLocomotionData.GetRunSpeed(),
_playerLocomotionData.GetStrafeSpeed());
switch (_trackingProvider.GetPlayerStance())
{
case ClientSimPlayerStanceEnum.CROUCHING:
speed *= CROUCH_SPEED_MULTIPLIER;
break;
case ClientSimPlayerStanceEnum.PRONE:
speed *= PRONE_SPEED_MULTIPLIER;
break;
}
return speed;
}
}
}