using UdonSharp; using UnityEngine; using VRC.SDKBase; public class WaterSystem : UdonSharpBehaviour { [Header("--- Настройки синевы ---")] // Смещение синего экрана относительно центра триггера (уровень глаз) [SerializeField] private float offset = -0.4f; // Прозрачность/плотность тумана под водой (от 0 до 1) [SerializeField] [Range(0f, 1f)] private float waterDensity = 1.0f; [Header("--- Настройки плавания ---")] // Скорость, с которой персонаж взлетает/всплывает вверх [SerializeField] private float swimSpeed = 2.5f; // Расстояние от глаз до плеч аватара (чтобы не вылетать из воды слишком высоко) [SerializeField] private float shouldersOffset = 0.22f; // Внутренние технические переменные private VRCPlayerApi localPlayer; // Ссылка на самого игрока private bool inWater = false; // Находится ли игрок в воде прямо сейчас private float mobileVerticalInput = 0f; // Сюда мы будем записывать движение мобильного джойстика void Start() { // Находим локального игрока (того, кто запустил игру на своем устройстве) localPlayer = Networking.LocalPlayer; } public override void OnPlayerTriggerEnter(VRCPlayerApi player) { // Проверяем, что в триггер вошел именно наш (локальный) игрок if (player != null && player.isLocal) { inWater = true; // Настройка физики: включаем "невесомость" и замедляем бег/ходьбу player.SetGravityStrength(0.01f); player.SetWalkSpeed(2f); // Скорость шага в воде player.SetRunSpeed(2.5f); // Скорость бега в воде } } public override void OnPlayerTriggerExit(VRCPlayerApi player) { // Проверяем, что триггер покинул именно наш (локальный) игрок if (player != null && player.isLocal) { inWater = false; // Полностью отключаем подводный туман экрана RenderSettings.fog = false; RenderSettings.fogDensity = 0f; // Возвращаем стандартные настройки физики для суши player.SetGravityStrength(1.0f); player.SetWalkSpeed(4f); player.SetRunSpeed(4f); } } public override void OnInputMoveVertical(float value, VRCInputMethod inputMethod) { // VRChat сам передает сюда значение джойстика: // 1.0 — джойстик зажат полностью вперед, -1.0 — полностью назад mobileVerticalInput = value; } void Update() { // Если игрок на суше или ссылка на него пропала — ничего не делаем if (!inWater || localPlayer == null) return; // Находим текущую высоту головы (глаз) игрока по Y float headY = localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position.y; // Вычисляем верхнюю границу воды с учетом нашего смещения offset float triggerTopY = transform.position.y + offset; // Вычисляем высоту плеч аватара float shouldersY = headY - shouldersOffset; // --- ЛОГИКА СИНЕВЫ ЭКРАНА (ВИЗУАЛ) --- if (headY < triggerTopY) { // Если глаза под водой: включаем туман RenderSettings.fog = true; RenderSettings.fogDensity = waterDensity; } else { // Если глаза над водой: выключаем туман RenderSettings.fog = false; RenderSettings.fogDensity = 0f; } // --- ЛОГИКА ПЛАВАНИЯ (ФИЗИКА) --- // Получаем направление взгляда игрока (куда повернута голова) Vector3 lookDir = localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).rotation * Vector3.forward; // Условие для мобилок и ПК: зажат джойстик/кнопка "Вперед" bool isMovingForward = mobileVerticalInput > 0.1f; if (isMovingForward) { // ПЛЫВЕМ ВВЕРХ: если смотрим вверх (lookDir.y > 0.2) и плечи еще под водой if (lookDir.y > 0.2f && shouldersY < triggerTopY) { Vector3 vel = localPlayer.GetVelocity(); vel.y = swimSpeed; // Задаем скорость движения вверх localPlayer.SetVelocity(vel); } // ПЛЫВЕМ ВНИЗ: если смотрим вниз (lookDir.y < -0.2) else if (lookDir.y < -0.2f) { Vector3 vel = localPlayer.GetVelocity(); vel.y = -swimSpeed; // Задаем скорость движения вниз (отрицательная) localPlayer.SetVelocity(vel); } } // Защита от "взлета": если плечи поднялись выше воды, обнуляем вертикальную скорость if (shouldersY >= triggerTopY) { Vector3 vel = localPlayer.GetVelocity(); if (vel.y > 0) { vel.y = 0; localPlayer.SetVelocity(vel); } } } // Закрывается метод Update() } // Закрывается класс WaterSystem