1538 lines
53 KiB
C#
1538 lines
53 KiB
C#
using System;
|
|
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
/// <summary>
|
|
/// Integrated VR fishing rod controller.
|
|
///
|
|
/// Replaces the following helper scripts when you want a simpler VR fishing setup:
|
|
/// - FishingModelLineFollower
|
|
/// - FishingBobberVRController
|
|
/// - FishingBobberWaterDetector
|
|
/// - FishingRodState
|
|
/// - FishingStartTrigger
|
|
///
|
|
/// Keep FishingGameManager, FishingGaugeUI, FishingRewardSystem, FishingHapticManager,
|
|
/// FishingUIEffects, FishingItemType, and RotateUI as separate scripts.
|
|
///
|
|
/// Recommended hierarchy:
|
|
/// Fishing rod
|
|
/// - Rigidbody
|
|
/// - XR Grab Interactable
|
|
/// - FishingRodVRController
|
|
/// - RodLineEndPoint
|
|
/// - BobberLineStartPoint
|
|
/// - Bobber / Hook_2
|
|
/// - FishingLineRoot (optional empty object for generated line mesh)
|
|
/// </summary>
|
|
public class FishingRodVRController : MonoBehaviour
|
|
{
|
|
public enum BobberState
|
|
{
|
|
Idle,
|
|
Casting,
|
|
WaitingInWater,
|
|
Bite,
|
|
MiniGameActive,
|
|
Returning
|
|
}
|
|
|
|
public enum StartMode
|
|
{
|
|
Disabled,
|
|
StartActionInArea,
|
|
OnBite,
|
|
ManualOnly
|
|
}
|
|
|
|
[Header("Core References")]
|
|
[SerializeField] private FishingGameManager fishingGameManager;
|
|
[SerializeField] private FishingHapticManager haptic;
|
|
|
|
[Tooltip("Line start point. Place this at the fishing rod tip / final guide ring.")]
|
|
[SerializeField] private Transform rodLineEndPoint;
|
|
|
|
[Tooltip("Line end point. The visible bobber should usually be a child of this object.")]
|
|
[SerializeField] private Transform bobberLineStartPoint;
|
|
|
|
[Tooltip("Visible bobber or hook model. Usually a child of BobberLineStartPoint. Used for optional visual correction/shake.")]
|
|
[SerializeField] private Transform bobberVisualRoot;
|
|
|
|
[Header("Rod Held State")]
|
|
[Tooltip("Call MarkHeld/MarkReleased from XR Grab Interactable Select Entered/Exited events.")]
|
|
[SerializeField] private bool isHeld;
|
|
[SerializeField] private bool showHeldDebugLog = true;
|
|
|
|
[Header("Input Actions")]
|
|
[Tooltip("Used to cast the bobber forward.")]
|
|
[SerializeField] private InputActionReference castAction;
|
|
|
|
[Tooltip("Used to return the bobber to the rod tip.")]
|
|
[SerializeField] private InputActionReference returnAction;
|
|
|
|
[Tooltip("Used to start fishing while player is in the start area, if Start Mode = StartActionInArea.")]
|
|
[SerializeField] private InputActionReference startAction;
|
|
|
|
[Tooltip("If the action is not already enabled by Input Action Manager, this script enables it temporarily.")]
|
|
[SerializeField] private bool enableInputActionsManually = true;
|
|
|
|
[Header("Start Area / Start Rules")]
|
|
[SerializeField] private StartMode startMode = StartMode.OnBite;
|
|
|
|
[Tooltip("If On, StartActionInArea mode requires the rod to be held.")]
|
|
[SerializeField] private bool requireRodHeldToStart = true;
|
|
|
|
[Tooltip("If On, CastForward requires the rod to be held.")]
|
|
[SerializeField] private bool requireRodHeldToCast = true;
|
|
|
|
[Tooltip("Shown while player is inside the start area and the rules allow starting.")]
|
|
[SerializeField] private GameObject startGuideUI;
|
|
|
|
[Tooltip("If On, an OverlapBox is used instead of a separate FishingStartTrigger component.")]
|
|
[SerializeField] private bool useStartAreaCheck = false;
|
|
|
|
[SerializeField] private Transform startAreaCenter;
|
|
[SerializeField] private Vector3 startAreaHalfExtents = new Vector3(1.5f, 1.2f, 1.5f);
|
|
[SerializeField] private bool startAreaUsePlayerLayerMask = false;
|
|
[SerializeField] private LayerMask startAreaPlayerLayerMask = ~0;
|
|
[SerializeField] private string playerTag = "Player";
|
|
[SerializeField] private bool hideGuideWhenFishingStarts = true;
|
|
[SerializeField] private bool allowRestartAfterClear = false;
|
|
|
|
[Header("Procedural Fishing Line")]
|
|
[Tooltip("Optional transform that owns the generated MeshFilter/MeshRenderer. If empty, this object is used.")]
|
|
[SerializeField] private Transform lineMeshRoot;
|
|
|
|
[SerializeField] private Material lineMaterial;
|
|
[SerializeField] private float lineRadius = 0.0012f;
|
|
[SerializeField] private int lineSegments = 12;
|
|
[SerializeField] private int lineSides = 6;
|
|
[SerializeField] private bool lineUseSag = false;
|
|
[SerializeField] private float lineSagAmount = 0.02f;
|
|
[SerializeField] private float lineDistanceSagMultiplier = 0.01f;
|
|
[SerializeField] private float lineMaxSagAmount = 0.08f;
|
|
[SerializeField] private Vector3 lineSagDirection = Vector3.down;
|
|
[SerializeField] private bool lineAddEndCaps = false;
|
|
[SerializeField] private bool hideLineWhenMissingTargets = true;
|
|
[SerializeField] private bool drawLineDebug = false;
|
|
|
|
[Header("Bobber Idle / Attachment")]
|
|
[Tooltip("If set, ReturnBobber uses this world position. If empty, initial BobberLineStartPoint local position is used.")]
|
|
[SerializeField] private Transform bobberRestPoint;
|
|
|
|
[Tooltip("On Start, put BobberLineStartPoint at its rest position.")]
|
|
[SerializeField] private bool returnBobberToRestOnStart = true;
|
|
|
|
[Tooltip("If On, BobberLineStartPoint is kept at the cast world position after casting, even while the rod moves.")]
|
|
[SerializeField] private bool pinBobberWorldPositionAfterCast = true;
|
|
|
|
[Tooltip("If the visible bobber is not a child of BobberLineStartPoint, this can force it to the line end.")]
|
|
[SerializeField] private bool forceBobberVisualToLineEnd = false;
|
|
|
|
[SerializeField] private Vector3 bobberVisualLocalOffset = Vector3.zero;
|
|
[SerializeField] private Vector3 bobberVisualWorldOffset = Vector3.zero;
|
|
|
|
[Header("Casting")]
|
|
[SerializeField] private Transform castDirectionSource;
|
|
[SerializeField] private float castDistance = 4f;
|
|
[SerializeField] private float castHeight = 1.1f;
|
|
[SerializeField] private float castDuration = 0.65f;
|
|
[SerializeField] private bool flattenCastDirection = true;
|
|
|
|
[Tooltip("If On, raycast down to find water hit point for the cast target.")]
|
|
[SerializeField] private bool useWaterRaycastTarget = true;
|
|
[SerializeField] private LayerMask waterRaycastMask = 0;
|
|
[SerializeField] private float waterRaycastStartUp = 2f;
|
|
[SerializeField] private float waterRaycastDownDistance = 8f;
|
|
[SerializeField] private bool useFixedWaterHeightFallback = false;
|
|
[SerializeField] private float fixedWaterHeight = 0f;
|
|
[SerializeField] private float bobberFloatHeightOffset = 0.02f;
|
|
|
|
[Header("Water Detection")]
|
|
[Tooltip("If On, this script checks water with OverlapSphere around BobberLineStartPoint. No BobberWaterDetector is needed.")]
|
|
[SerializeField] private bool useWaterOverlapCheck = true;
|
|
[SerializeField] private float waterCheckRadius = 0.08f;
|
|
[SerializeField] private bool useWaterLayerMask = true;
|
|
[SerializeField] private LayerMask waterLayerMask = 0;
|
|
[SerializeField] private bool useWaterTag = true;
|
|
[SerializeField] private string waterTag = "Water";
|
|
|
|
[Header("Bite")]
|
|
[SerializeField] private bool useBiteTimer = true;
|
|
[SerializeField] private Vector2 biteDelayRange = new Vector2(1.5f, 4.0f);
|
|
[SerializeField] private bool startMiniGameOnBite = true;
|
|
[SerializeField] private bool stopBiteShakeWhenMiniGameStarts = true;
|
|
[SerializeField] private float biteShakeAmplitude = 0.035f;
|
|
[SerializeField] private float biteShakeFrequency = 18f;
|
|
[SerializeField] private float biteShakeDuration = 1.25f;
|
|
[SerializeField] private bool keepShakingUntilMiniGameStarts = false;
|
|
|
|
[Header("Reel Handle / Line Length Control")]
|
|
[Tooltip("If On, rotating a hand/controller around the reel center changes BobberLineStartPoint distance, so the procedural line becomes shorter/longer.")]
|
|
[SerializeField] private bool useReelControl = true;
|
|
|
|
[Tooltip("Usually the center/pivot of the reel. The controller should move around this point.")]
|
|
[SerializeField] private Transform reelCenterPoint;
|
|
|
|
[Tooltip("Usually the off-hand controller transform, or a reel-handle tracking transform.")]
|
|
[SerializeField] private Transform reelRotationSource;
|
|
|
|
[Tooltip("If On, Reel Hold Action must be pressed to reel. If Off and Use Reel Grab Events is also Off, being near the reel is enough.")]
|
|
[SerializeField] private bool useReelHoldAction = true;
|
|
|
|
[Tooltip("Button/Grip action used while reeling. Example binding: <XRController>{LeftHand}/gripPressed or <XRController>{LeftHand}/triggerPressed.")]
|
|
[SerializeField] private InputActionReference reelHoldAction;
|
|
|
|
[Tooltip("If On, call MarkReelHandleGrabbed/MarkReelHandleReleased from a reel handle XR Grab Interactable or trigger volume.")]
|
|
[SerializeField] private bool useReelGrabEvents = false;
|
|
|
|
[Tooltip("If On, the rod must be held before reeling is allowed.")]
|
|
[SerializeField] private bool requireRodHeldToReel = true;
|
|
|
|
[Tooltip("켜면 입질 이후 또는 미니게임 중일 때만 릴 감기가 됩니다. 기본 낚시가 먼저 되게 하려면 On 추천.")]
|
|
[SerializeField] private bool reelOnlyAfterBiteOrMiniGame = true;
|
|
|
|
[Tooltip("디버그용입니다. Hold Action과 Grab Events가 모두 꺼져 있어도 릴 근처에 있으면 감기게 합니다.")]
|
|
[SerializeField] private bool allowReelByProximityOnlyForDebug = false;
|
|
|
|
[Tooltip("Maximum distance from Reel Center Point to Reel Rotation Source to start reeling.")]
|
|
[SerializeField] private float reelActivationRadius = 0.35f;
|
|
|
|
[Tooltip("Reel axis in Reel Center Point local space. Try Local X first, then Y/Z depending on your reel model.")]
|
|
[SerializeField] private Vector3 reelLocalAxis = Vector3.right;
|
|
|
|
[Tooltip("Invert if turning the handle makes the line longer instead of shorter.")]
|
|
[SerializeField] private bool invertReelDirection = false;
|
|
|
|
[Tooltip("How many meters of line change per full 360 degree reel turn.")]
|
|
[SerializeField] private float lineLengthPerReelTurn = 0.45f;
|
|
|
|
[SerializeField] private float minReelLineLength = 0.35f;
|
|
[SerializeField] private float maxReelLineLength = 8f;
|
|
|
|
[Tooltip("If Off, reverse rotation will not let more line out; it only reels in.")]
|
|
[SerializeField] private bool allowReelOut = true;
|
|
|
|
[Tooltip("Optional visual object to rotate when reeling. This is only visual.")]
|
|
[SerializeField] private Transform reelHandleVisual;
|
|
|
|
[SerializeField] private bool rotateReelHandleVisual = true;
|
|
|
|
[Tooltip("Reel handle visual axis in its own local space.")]
|
|
[SerializeField] private Vector3 reelHandleVisualLocalAxis = Vector3.right;
|
|
|
|
[SerializeField] private float reelHandleVisualDegreesMultiplier = 1f;
|
|
|
|
[Tooltip("When a cast finishes, current line length is initialized from rod tip to bobber distance.")]
|
|
[SerializeField] private bool initializeLineLengthAfterCast = true;
|
|
|
|
[SerializeField] private bool showReelDebugLog = false;
|
|
|
|
[Header("Return")]
|
|
[SerializeField] private float returnDuration = 0.35f;
|
|
|
|
[Header("Debug")]
|
|
[SerializeField] private bool showDebugLog = true;
|
|
[SerializeField] private bool drawStartAreaGizmo = true;
|
|
[SerializeField] private bool drawWaterCheckGizmo = true;
|
|
|
|
private MeshFilter lineMeshFilter;
|
|
private MeshRenderer lineMeshRenderer;
|
|
private Mesh lineMesh;
|
|
private Material generatedLineMaterial;
|
|
|
|
private Coroutine bobberMoveRoutine;
|
|
private Coroutine biteWaitRoutine;
|
|
private Coroutine biteShakeRoutine;
|
|
|
|
private bool castActionWasEnabled;
|
|
private bool returnActionWasEnabled;
|
|
private bool reelHoldActionWasEnabled;
|
|
private bool startActionWasEnabled;
|
|
|
|
private BobberState state = BobberState.Idle;
|
|
private bool playerInsideStartArea;
|
|
private bool bobberInWater;
|
|
private bool bobberWorldPinned;
|
|
private Vector3 pinnedBobberWorldPosition;
|
|
private Vector3 initialBobberLocalPosition;
|
|
private Quaternion initialBobberLocalRotation;
|
|
private bool hasInitialBobberTransform;
|
|
private Vector3 bobberVisualInitialLocalPosition;
|
|
private bool hasBobberVisualInitialLocalPosition;
|
|
|
|
private bool reelHeldByEvent;
|
|
private bool reelInteractionActive;
|
|
private bool reelAngleInitialized;
|
|
private float previousReelAngle;
|
|
private float currentLineLength;
|
|
private bool hasCurrentLineLength;
|
|
|
|
private const float Epsilon = 0.000001f;
|
|
|
|
public bool IsHeld => isHeld;
|
|
public bool PlayerInsideStartArea => playerInsideStartArea;
|
|
public bool IsBobberInWater => bobberInWater;
|
|
public bool IsBiting => state == BobberState.Bite;
|
|
public bool IsReeling => reelInteractionActive;
|
|
public float CurrentLineLength => hasCurrentLineLength ? currentLineLength : GetCurrentRodToBobberDistance();
|
|
public BobberState State => state;
|
|
|
|
private void Reset()
|
|
{
|
|
AutoBindCommonReferences();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
AutoBindCommonReferences();
|
|
CacheInitialBobberTransform();
|
|
CacheBobberVisualPosition();
|
|
EnsureLineMeshComponents(true);
|
|
SetStartGuideVisible(false);
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (returnBobberToRestOnStart)
|
|
ReturnBobberToRestInstant();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
RegisterInputAction(castAction, OnCastInput, ref castActionWasEnabled);
|
|
RegisterInputAction(returnAction, OnReturnInput, ref returnActionWasEnabled);
|
|
RegisterPassiveInputAction(reelHoldAction, ref reelHoldActionWasEnabled);
|
|
RegisterInputAction(startAction, OnStartInput, ref startActionWasEnabled);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
UnregisterInputAction(castAction, OnCastInput, castActionWasEnabled);
|
|
UnregisterInputAction(returnAction, OnReturnInput, returnActionWasEnabled);
|
|
UnregisterPassiveInputAction(reelHoldAction, reelHoldActionWasEnabled);
|
|
UnregisterInputAction(startAction, OnStartInput, startActionWasEnabled);
|
|
StopAllRoutinesSafe();
|
|
SetStartGuideVisible(false);
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
startAreaHalfExtents.x = Mathf.Max(0.01f, startAreaHalfExtents.x);
|
|
startAreaHalfExtents.y = Mathf.Max(0.01f, startAreaHalfExtents.y);
|
|
startAreaHalfExtents.z = Mathf.Max(0.01f, startAreaHalfExtents.z);
|
|
|
|
lineRadius = Mathf.Max(0.00005f, lineRadius);
|
|
lineSegments = Mathf.Clamp(lineSegments, 1, 128);
|
|
lineSides = Mathf.Clamp(lineSides, 3, 24);
|
|
lineSagAmount = Mathf.Max(0f, lineSagAmount);
|
|
lineDistanceSagMultiplier = Mathf.Max(0f, lineDistanceSagMultiplier);
|
|
lineMaxSagAmount = Mathf.Max(0f, lineMaxSagAmount);
|
|
|
|
castDistance = Mathf.Max(0.1f, castDistance);
|
|
castHeight = Mathf.Max(0f, castHeight);
|
|
castDuration = Mathf.Max(0.01f, castDuration);
|
|
waterRaycastStartUp = Mathf.Max(0f, waterRaycastStartUp);
|
|
waterRaycastDownDistance = Mathf.Max(0.1f, waterRaycastDownDistance);
|
|
bobberFloatHeightOffset = Mathf.Max(0f, bobberFloatHeightOffset);
|
|
waterCheckRadius = Mathf.Max(0.001f, waterCheckRadius);
|
|
|
|
biteDelayRange.x = Mathf.Max(0f, biteDelayRange.x);
|
|
biteDelayRange.y = Mathf.Max(biteDelayRange.x, biteDelayRange.y);
|
|
biteShakeAmplitude = Mathf.Max(0f, biteShakeAmplitude);
|
|
biteShakeFrequency = Mathf.Max(0f, biteShakeFrequency);
|
|
biteShakeDuration = Mathf.Max(0f, biteShakeDuration);
|
|
reelActivationRadius = Mathf.Max(0.01f, reelActivationRadius);
|
|
lineLengthPerReelTurn = Mathf.Max(0.0001f, lineLengthPerReelTurn);
|
|
minReelLineLength = Mathf.Max(0.01f, minReelLineLength);
|
|
maxReelLineLength = Mathf.Max(minReelLineLength, maxReelLineLength);
|
|
if (reelLocalAxis.sqrMagnitude <= Epsilon)
|
|
reelLocalAxis = Vector3.right;
|
|
if (reelHandleVisualLocalAxis.sqrMagnitude <= Epsilon)
|
|
reelHandleVisualLocalAxis = Vector3.right;
|
|
returnDuration = Mathf.Max(0.01f, returnDuration);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
UpdateStartAreaState();
|
|
UpdateWaterState();
|
|
UpdateReelControl();
|
|
UpdateBiteVisualPin();
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (bobberWorldPinned && bobberLineStartPoint != null)
|
|
bobberLineStartPoint.position = GetPinnedBobberPositionWithBiteOffset();
|
|
|
|
UpdateBobberVisualAttachment();
|
|
UpdateProceduralLine();
|
|
}
|
|
|
|
[ContextMenu("Auto Bind Common References")]
|
|
public void AutoBindCommonReferences()
|
|
{
|
|
if (fishingGameManager == null)
|
|
fishingGameManager = GetComponentInParent<FishingGameManager>();
|
|
|
|
if (haptic == null)
|
|
haptic = GetComponentInParent<FishingHapticManager>();
|
|
|
|
if (rodLineEndPoint == null)
|
|
rodLineEndPoint = FindChildRecursive(transform, "RodLineEndPoint", "RodTipPoint", "RodTip", "TipPoint");
|
|
|
|
if (bobberLineStartPoint == null)
|
|
bobberLineStartPoint = FindChildRecursive(transform, "BobberLineStartPoint", "BobberPoint", "BobberRoot", "HookPoint");
|
|
|
|
if (bobberVisualRoot == null && bobberLineStartPoint != null && bobberLineStartPoint.childCount > 0)
|
|
bobberVisualRoot = bobberLineStartPoint.GetChild(0);
|
|
|
|
if (lineMeshRoot == null)
|
|
lineMeshRoot = FindChildRecursive(transform, "FishingLine", "FishingLineRoot", "LineRoot", "ProceduralLineRoot");
|
|
|
|
if (castDirectionSource == null)
|
|
castDirectionSource = rodLineEndPoint != null ? rodLineEndPoint : transform;
|
|
|
|
if (reelCenterPoint == null)
|
|
reelCenterPoint = FindChildRecursive(transform, "ReelCenterPoint", "ReelPivot", "ReelHandlePivot", "ReelCenter");
|
|
|
|
if (reelHandleVisual == null)
|
|
reelHandleVisual = FindChildRecursive(transform, "ReelHandleVisual", "ReelHandle", "ReelKnob", "ReelHandleKnob");
|
|
}
|
|
|
|
public void MarkHeld()
|
|
{
|
|
SetHeld(true);
|
|
}
|
|
|
|
public void MarkReleased()
|
|
{
|
|
SetHeld(false);
|
|
}
|
|
|
|
public void MarkReelHandleGrabbed()
|
|
{
|
|
reelHeldByEvent = true;
|
|
reelAngleInitialized = false;
|
|
LogReel("릴 손잡이 잡음");
|
|
}
|
|
|
|
public void MarkReelHandleReleased()
|
|
{
|
|
reelHeldByEvent = false;
|
|
StopReelInteraction();
|
|
LogReel("릴 손잡이 놓음");
|
|
}
|
|
|
|
public void SetHeld(bool value)
|
|
{
|
|
if (isHeld == value)
|
|
return;
|
|
|
|
isHeld = value;
|
|
|
|
if (showHeldDebugLog)
|
|
Debug.Log(isHeld ? "낚싯대 잡음" : "낚싯대 놓음", this);
|
|
}
|
|
|
|
public void CastForward()
|
|
{
|
|
if (requireRodHeldToCast && !isHeld)
|
|
{
|
|
Log("낚싯대를 잡고 있지 않아 캐스팅을 무시했습니다.");
|
|
return;
|
|
}
|
|
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null)
|
|
{
|
|
Debug.LogWarning("[FishingRodVRController] RodLineEndPoint 또는 BobberLineStartPoint가 없습니다.", this);
|
|
return;
|
|
}
|
|
|
|
Vector3 target = CalculateCastTarget();
|
|
CastToWorldPoint(target);
|
|
}
|
|
|
|
public void CastToWorldPoint(Vector3 worldTarget)
|
|
{
|
|
if (bobberLineStartPoint == null)
|
|
return;
|
|
|
|
StopBobberMoveAndBiteRoutines();
|
|
bobberWorldPinned = false;
|
|
state = BobberState.Casting;
|
|
bobberMoveRoutine = StartCoroutine(CastRoutine(worldTarget));
|
|
}
|
|
|
|
public void ReturnBobberToRest()
|
|
{
|
|
if (bobberLineStartPoint == null)
|
|
return;
|
|
|
|
StopBobberMoveAndBiteRoutines();
|
|
bobberWorldPinned = false;
|
|
state = BobberState.Returning;
|
|
bobberMoveRoutine = StartCoroutine(MoveBobberRoutine(GetRestWorldPosition(), returnDuration, BobberState.Idle));
|
|
}
|
|
|
|
public void ReturnBobberToRestInstant()
|
|
{
|
|
if (bobberLineStartPoint == null)
|
|
return;
|
|
|
|
StopBobberMoveAndBiteRoutines();
|
|
bobberWorldPinned = false;
|
|
bobberLineStartPoint.position = GetRestWorldPosition();
|
|
InitializeReelLineLengthFromCurrentDistance();
|
|
ResetBobberVisualPosition();
|
|
state = BobberState.Idle;
|
|
bobberInWater = false;
|
|
}
|
|
|
|
public void ForceSetReelLineLength(float length)
|
|
{
|
|
SetReelLineLength(length);
|
|
}
|
|
|
|
public void ForceMiniGameActiveState()
|
|
{
|
|
if (state == BobberState.Bite || state == BobberState.WaitingInWater || state == BobberState.Idle)
|
|
state = BobberState.MiniGameActive;
|
|
}
|
|
|
|
public bool TryStartFishing()
|
|
{
|
|
if (fishingGameManager == null)
|
|
{
|
|
Debug.LogWarning("[FishingRodVRController] FishingGameManager가 연결되지 않았습니다.", this);
|
|
return false;
|
|
}
|
|
|
|
if (!allowRestartAfterClear && fishingGameManager.IsMemoryPieceCollected)
|
|
{
|
|
Log("이미 기억의 조각을 획득해서 낚시 시작을 막았습니다.");
|
|
return false;
|
|
}
|
|
|
|
if (requireRodHeldToStart && !isHeld)
|
|
{
|
|
Log("낚싯대를 잡고 있지 않아 낚시 시작을 막았습니다.");
|
|
return false;
|
|
}
|
|
|
|
if (startMode == StartMode.StartActionInArea && useStartAreaCheck && !playerInsideStartArea)
|
|
{
|
|
Log("플레이어가 낚시 시작 영역 안에 있지 않습니다.");
|
|
return false;
|
|
}
|
|
|
|
if (fishingGameManager.IsFishingSessionRunning)
|
|
{
|
|
Log("낚시 세션이 이미 진행 중입니다.");
|
|
return false;
|
|
}
|
|
|
|
fishingGameManager.StartFishing();
|
|
state = BobberState.MiniGameActive;
|
|
|
|
if (hideGuideWhenFishingStarts)
|
|
SetStartGuideVisible(false);
|
|
|
|
Log("낚시 미니게임 시작");
|
|
return true;
|
|
}
|
|
|
|
public void ForceBite()
|
|
{
|
|
TriggerBite();
|
|
}
|
|
|
|
public void StopFishingRodController()
|
|
{
|
|
StopAllRoutinesSafe();
|
|
bobberWorldPinned = false;
|
|
state = BobberState.Idle;
|
|
SetStartGuideVisible(false);
|
|
}
|
|
|
|
private void OnCastInput(InputAction.CallbackContext context)
|
|
{
|
|
CastForward();
|
|
}
|
|
|
|
private void OnReturnInput(InputAction.CallbackContext context)
|
|
{
|
|
ReturnBobberToRest();
|
|
}
|
|
|
|
private void OnStartInput(InputAction.CallbackContext context)
|
|
{
|
|
if (startMode == StartMode.StartActionInArea || startMode == StartMode.ManualOnly)
|
|
TryStartFishing();
|
|
}
|
|
|
|
private IEnumerator CastRoutine(Vector3 target)
|
|
{
|
|
Vector3 start = bobberLineStartPoint.position;
|
|
Vector3 control = (start + target) * 0.5f + Vector3.up * castHeight;
|
|
float timer = 0f;
|
|
|
|
while (timer < castDuration)
|
|
{
|
|
timer += Time.deltaTime;
|
|
float t = Mathf.Clamp01(timer / castDuration);
|
|
t = EaseOutCubic(t);
|
|
bobberLineStartPoint.position = QuadraticBezier(start, control, target, t);
|
|
yield return null;
|
|
}
|
|
|
|
bobberLineStartPoint.position = target;
|
|
pinnedBobberWorldPosition = target;
|
|
bobberWorldPinned = pinBobberWorldPositionAfterCast;
|
|
if (initializeLineLengthAfterCast)
|
|
InitializeReelLineLengthFromCurrentDistance();
|
|
bobberMoveRoutine = null;
|
|
state = BobberState.WaitingInWater;
|
|
|
|
Log("찌 캐스팅 완료");
|
|
|
|
if (!useWaterOverlapCheck && useBiteTimer)
|
|
StartWaitingForBite();
|
|
}
|
|
|
|
private IEnumerator MoveBobberRoutine(Vector3 target, float duration, BobberState endState)
|
|
{
|
|
Vector3 start = bobberLineStartPoint.position;
|
|
float timer = 0f;
|
|
|
|
while (timer < duration)
|
|
{
|
|
timer += Time.deltaTime;
|
|
float t = Mathf.Clamp01(timer / duration);
|
|
bobberLineStartPoint.position = Vector3.Lerp(start, target, EaseOutCubic(t));
|
|
yield return null;
|
|
}
|
|
|
|
bobberLineStartPoint.position = target;
|
|
InitializeReelLineLengthFromCurrentDistance();
|
|
ResetBobberVisualPosition();
|
|
state = endState;
|
|
bobberMoveRoutine = null;
|
|
}
|
|
|
|
private void StartWaitingForBite()
|
|
{
|
|
if (!useBiteTimer)
|
|
return;
|
|
|
|
if (biteWaitRoutine != null || state == BobberState.Bite || state == BobberState.MiniGameActive)
|
|
return;
|
|
|
|
state = BobberState.WaitingInWater;
|
|
biteWaitRoutine = StartCoroutine(BiteWaitRoutine());
|
|
Log("입질 대기 시작");
|
|
}
|
|
|
|
private IEnumerator BiteWaitRoutine()
|
|
{
|
|
float delay = UnityEngine.Random.Range(biteDelayRange.x, biteDelayRange.y);
|
|
float timer = 0f;
|
|
|
|
while (timer < delay)
|
|
{
|
|
timer += Time.deltaTime;
|
|
|
|
if (useWaterOverlapCheck && !bobberInWater)
|
|
{
|
|
biteWaitRoutine = null;
|
|
yield break;
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
biteWaitRoutine = null;
|
|
TriggerBite();
|
|
}
|
|
|
|
private void TriggerBite()
|
|
{
|
|
if (state == BobberState.MiniGameActive)
|
|
return;
|
|
|
|
state = BobberState.Bite;
|
|
Log("입질 발생");
|
|
|
|
if (haptic != null)
|
|
haptic.Good();
|
|
|
|
if (biteShakeRoutine != null)
|
|
StopCoroutine(biteShakeRoutine);
|
|
|
|
biteShakeRoutine = StartCoroutine(BiteShakeRoutine());
|
|
|
|
if (startMode == StartMode.OnBite && startMiniGameOnBite)
|
|
{
|
|
if (TryStartFishing() && stopBiteShakeWhenMiniGameStarts && !keepShakingUntilMiniGameStarts)
|
|
StopBiteShakeOnly();
|
|
}
|
|
}
|
|
|
|
private IEnumerator BiteShakeRoutine()
|
|
{
|
|
CacheBobberVisualPosition();
|
|
|
|
float timer = 0f;
|
|
while (keepShakingUntilMiniGameStarts || timer < biteShakeDuration)
|
|
{
|
|
timer += Time.deltaTime;
|
|
yield return null;
|
|
}
|
|
|
|
ResetBobberVisualPosition();
|
|
biteShakeRoutine = null;
|
|
}
|
|
|
|
private void UpdateBiteVisualPin()
|
|
{
|
|
// The actual vertical offset is applied through GetPinnedBobberPositionWithBiteOffset()
|
|
// so the line end and bobber point always remain identical.
|
|
}
|
|
|
|
private Vector3 GetPinnedBobberPositionWithBiteOffset()
|
|
{
|
|
if (state != BobberState.Bite || biteShakeAmplitude <= 0f)
|
|
return pinnedBobberWorldPosition;
|
|
|
|
float y = Mathf.Sin(Time.time * biteShakeFrequency) * biteShakeAmplitude;
|
|
return pinnedBobberWorldPosition + Vector3.up * y;
|
|
}
|
|
|
|
private void UpdateStartAreaState()
|
|
{
|
|
if (startMode != StartMode.StartActionInArea || !useStartAreaCheck)
|
|
{
|
|
playerInsideStartArea = true;
|
|
if (startMode != StartMode.StartActionInArea)
|
|
SetStartGuideVisible(false);
|
|
return;
|
|
}
|
|
|
|
bool inside = IsPlayerInsideStartArea();
|
|
playerInsideStartArea = inside;
|
|
|
|
bool canShow = inside && CanShowStartGuide();
|
|
SetStartGuideVisible(canShow);
|
|
}
|
|
|
|
private bool CanShowStartGuide()
|
|
{
|
|
if (fishingGameManager != null)
|
|
{
|
|
if (fishingGameManager.IsFishingSessionRunning)
|
|
return false;
|
|
|
|
if (!allowRestartAfterClear && fishingGameManager.IsMemoryPieceCollected)
|
|
return false;
|
|
}
|
|
|
|
if (requireRodHeldToStart && !isHeld)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsPlayerInsideStartArea()
|
|
{
|
|
Transform area = startAreaCenter != null ? startAreaCenter : transform;
|
|
LayerMask mask = startAreaUsePlayerLayerMask ? startAreaPlayerLayerMask : ~0;
|
|
Collider[] hits = Physics.OverlapBox(area.position, startAreaHalfExtents, area.rotation, mask, QueryTriggerInteraction.Collide);
|
|
|
|
for (int i = 0; i < hits.Length; i++)
|
|
{
|
|
if (IsPlayerCollider(hits[i]))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsPlayerCollider(Collider other)
|
|
{
|
|
if (other == null)
|
|
return false;
|
|
|
|
if (startAreaUsePlayerLayerMask && startAreaPlayerLayerMask.value != 0)
|
|
{
|
|
Transform currentForLayer = other.transform;
|
|
while (currentForLayer != null)
|
|
{
|
|
if (((1 << currentForLayer.gameObject.layer) & startAreaPlayerLayerMask.value) != 0)
|
|
return true;
|
|
|
|
currentForLayer = currentForLayer.parent;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(playerTag))
|
|
return !startAreaUsePlayerLayerMask;
|
|
|
|
Transform current = other.transform;
|
|
while (current != null)
|
|
{
|
|
if (CompareTagSafe(current.gameObject, playerTag))
|
|
return true;
|
|
|
|
current = current.parent;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void UpdateWaterState()
|
|
{
|
|
if (!useWaterOverlapCheck || bobberLineStartPoint == null)
|
|
return;
|
|
|
|
bool inWaterNow = IsBobberOverlappingWater();
|
|
|
|
if (inWaterNow == bobberInWater)
|
|
return;
|
|
|
|
bobberInWater = inWaterNow;
|
|
|
|
if (bobberInWater)
|
|
{
|
|
Log("찌가 물에 들어감");
|
|
if (state == BobberState.Casting || state == BobberState.WaitingInWater || state == BobberState.Idle)
|
|
StartWaitingForBite();
|
|
}
|
|
else
|
|
{
|
|
Log("찌가 물에서 나감");
|
|
|
|
if (state == BobberState.WaitingInWater || state == BobberState.Bite)
|
|
{
|
|
StopBiteWaitOnly();
|
|
StopBiteShakeOnly();
|
|
state = BobberState.Idle;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsBobberOverlappingWater()
|
|
{
|
|
LayerMask mask = useWaterLayerMask && waterLayerMask.value != 0 ? waterLayerMask : ~0;
|
|
Collider[] hits = Physics.OverlapSphere(bobberLineStartPoint.position, waterCheckRadius, mask, QueryTriggerInteraction.Collide);
|
|
|
|
for (int i = 0; i < hits.Length; i++)
|
|
{
|
|
Collider hit = hits[i];
|
|
if (hit == null)
|
|
continue;
|
|
|
|
bool layerMatch = !useWaterLayerMask || waterLayerMask.value == 0 || ((1 << hit.gameObject.layer) & waterLayerMask.value) != 0;
|
|
bool tagMatch = !useWaterTag || string.IsNullOrWhiteSpace(waterTag) || CompareTagSafe(hit.gameObject, waterTag);
|
|
|
|
if (useWaterLayerMask && useWaterTag && waterLayerMask.value != 0 && !string.IsNullOrWhiteSpace(waterTag))
|
|
{
|
|
if (layerMatch || tagMatch)
|
|
return true;
|
|
}
|
|
else if (layerMatch && tagMatch)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
private void UpdateReelControl()
|
|
{
|
|
if (!useReelControl)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null || reelCenterPoint == null || reelRotationSource == null)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
if (requireRodHeldToReel && !isHeld)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
if (state == BobberState.Casting || state == BobberState.Returning)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
if (reelOnlyAfterBiteOrMiniGame && state != BobberState.Bite && state != BobberState.MiniGameActive)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
if (!ShouldReelInputBeActive())
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
float distanceToReel = Vector3.Distance(reelCenterPoint.position, reelRotationSource.position);
|
|
if (distanceToReel > reelActivationRadius)
|
|
{
|
|
StopReelInteraction();
|
|
return;
|
|
}
|
|
|
|
float currentAngle = GetReelSourceAngle();
|
|
|
|
if (!reelAngleInitialized)
|
|
{
|
|
previousReelAngle = currentAngle;
|
|
reelAngleInitialized = true;
|
|
reelInteractionActive = true;
|
|
InitializeReelLineLengthFromCurrentDistanceIfNeeded();
|
|
LogReel("릴 감기 시작");
|
|
return;
|
|
}
|
|
|
|
float deltaAngle = Mathf.DeltaAngle(previousReelAngle, currentAngle);
|
|
previousReelAngle = currentAngle;
|
|
|
|
if (Mathf.Abs(deltaAngle) < 0.01f)
|
|
return;
|
|
|
|
if (invertReelDirection)
|
|
deltaAngle = -deltaAngle;
|
|
|
|
// Positive deltaAngle is treated as reeling in, so line length decreases.
|
|
float lengthDelta = -deltaAngle / 360f * lineLengthPerReelTurn;
|
|
|
|
if (!allowReelOut && lengthDelta > 0f)
|
|
lengthDelta = 0f;
|
|
|
|
SetReelLineLength(CurrentLineLength + lengthDelta);
|
|
RotateReelHandleVisual(deltaAngle);
|
|
}
|
|
|
|
private bool ShouldReelInputBeActive()
|
|
{
|
|
bool actionActive = useReelHoldAction && reelHoldAction != null && reelHoldAction.action != null && reelHoldAction.action.IsPressed();
|
|
bool eventActive = useReelGrabEvents && reelHeldByEvent;
|
|
|
|
if (!useReelHoldAction && !useReelGrabEvents)
|
|
return allowReelByProximityOnlyForDebug;
|
|
|
|
return actionActive || eventActive;
|
|
}
|
|
|
|
private void StopReelInteraction()
|
|
{
|
|
reelInteractionActive = false;
|
|
reelAngleInitialized = false;
|
|
}
|
|
|
|
private float GetReelSourceAngle()
|
|
{
|
|
Vector3 axis = GetReelWorldAxis();
|
|
Vector3 fromCenter = reelRotationSource.position - reelCenterPoint.position;
|
|
Vector3 projected = Vector3.ProjectOnPlane(fromCenter, axis);
|
|
|
|
if (projected.sqrMagnitude <= Epsilon)
|
|
return previousReelAngle;
|
|
|
|
projected.Normalize();
|
|
|
|
Vector3 reference = Vector3.ProjectOnPlane(reelCenterPoint.up, axis);
|
|
if (reference.sqrMagnitude <= Epsilon)
|
|
reference = Vector3.ProjectOnPlane(reelCenterPoint.forward, axis);
|
|
if (reference.sqrMagnitude <= Epsilon)
|
|
reference = Vector3.ProjectOnPlane(Vector3.up, axis);
|
|
if (reference.sqrMagnitude <= Epsilon)
|
|
reference = Vector3.right;
|
|
|
|
reference.Normalize();
|
|
return Vector3.SignedAngle(reference, projected, axis);
|
|
}
|
|
|
|
private Vector3 GetReelWorldAxis()
|
|
{
|
|
if (reelCenterPoint == null)
|
|
return Vector3.right;
|
|
|
|
Vector3 localAxis = reelLocalAxis.sqrMagnitude > Epsilon ? reelLocalAxis.normalized : Vector3.right;
|
|
Vector3 worldAxis = reelCenterPoint.TransformDirection(localAxis);
|
|
return SafeNormalize(worldAxis, reelCenterPoint.right);
|
|
}
|
|
|
|
private void InitializeReelLineLengthFromCurrentDistanceIfNeeded()
|
|
{
|
|
if (!hasCurrentLineLength)
|
|
InitializeReelLineLengthFromCurrentDistance();
|
|
}
|
|
|
|
private void InitializeReelLineLengthFromCurrentDistance()
|
|
{
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null)
|
|
return;
|
|
|
|
currentLineLength = Mathf.Clamp(GetCurrentRodToBobberDistance(), minReelLineLength, maxReelLineLength);
|
|
hasCurrentLineLength = true;
|
|
}
|
|
|
|
private float GetCurrentRodToBobberDistance()
|
|
{
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null)
|
|
return 0f;
|
|
|
|
Vector3 end = bobberWorldPinned ? pinnedBobberWorldPosition : bobberLineStartPoint.position;
|
|
return Vector3.Distance(rodLineEndPoint.position, end);
|
|
}
|
|
|
|
private void SetReelLineLength(float targetLength)
|
|
{
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null)
|
|
return;
|
|
|
|
currentLineLength = Mathf.Clamp(targetLength, minReelLineLength, maxReelLineLength);
|
|
hasCurrentLineLength = true;
|
|
|
|
Vector3 start = rodLineEndPoint.position;
|
|
Vector3 baseEnd = bobberWorldPinned ? pinnedBobberWorldPosition : bobberLineStartPoint.position;
|
|
Vector3 direction = baseEnd - start;
|
|
|
|
if (direction.sqrMagnitude <= Epsilon)
|
|
{
|
|
Transform source = castDirectionSource != null ? castDirectionSource : transform;
|
|
direction = source.forward;
|
|
}
|
|
|
|
direction = SafeNormalize(direction, transform.forward);
|
|
Vector3 newEnd = start + direction * currentLineLength;
|
|
|
|
if (bobberWorldPinned)
|
|
pinnedBobberWorldPosition = newEnd;
|
|
|
|
bobberLineStartPoint.position = newEnd;
|
|
|
|
if (state == BobberState.Idle && currentLineLength > minReelLineLength + 0.05f)
|
|
state = BobberState.WaitingInWater;
|
|
|
|
LogReel($"릴 줄 길이: {currentLineLength:F2}m");
|
|
}
|
|
|
|
private void RotateReelHandleVisual(float deltaAngle)
|
|
{
|
|
if (!rotateReelHandleVisual || reelHandleVisual == null)
|
|
return;
|
|
|
|
Vector3 axis = reelHandleVisualLocalAxis.sqrMagnitude > Epsilon ? reelHandleVisualLocalAxis.normalized : Vector3.right;
|
|
reelHandleVisual.Rotate(axis, deltaAngle * reelHandleVisualDegreesMultiplier, Space.Self);
|
|
}
|
|
|
|
private Vector3 CalculateCastTarget()
|
|
{
|
|
Transform source = castDirectionSource != null ? castDirectionSource : (rodLineEndPoint != null ? rodLineEndPoint : transform);
|
|
Vector3 direction = source.forward;
|
|
|
|
if (flattenCastDirection)
|
|
{
|
|
direction.y = 0f;
|
|
if (direction.sqrMagnitude <= Epsilon)
|
|
direction = transform.forward;
|
|
}
|
|
|
|
direction = SafeNormalize(direction, transform.forward);
|
|
|
|
Vector3 fallbackTarget = rodLineEndPoint.position + direction * castDistance;
|
|
|
|
if (useFixedWaterHeightFallback)
|
|
fallbackTarget.y = fixedWaterHeight + bobberFloatHeightOffset;
|
|
|
|
if (useWaterRaycastTarget && waterRaycastMask.value != 0)
|
|
{
|
|
Vector3 rayStart = fallbackTarget + Vector3.up * waterRaycastStartUp;
|
|
if (Physics.Raycast(rayStart, Vector3.down, out RaycastHit hit, waterRaycastDownDistance, waterRaycastMask, QueryTriggerInteraction.Collide))
|
|
return hit.point + Vector3.up * bobberFloatHeightOffset;
|
|
}
|
|
|
|
return fallbackTarget;
|
|
}
|
|
|
|
private Vector3 GetRestWorldPosition()
|
|
{
|
|
if (bobberRestPoint != null)
|
|
return bobberRestPoint.position;
|
|
|
|
if (hasInitialBobberTransform && bobberLineStartPoint.parent != null)
|
|
return bobberLineStartPoint.parent.TransformPoint(initialBobberLocalPosition);
|
|
|
|
if (rodLineEndPoint != null)
|
|
return rodLineEndPoint.position + rodLineEndPoint.TransformDirection(new Vector3(0f, -0.2f, -0.15f));
|
|
|
|
return transform.position;
|
|
}
|
|
|
|
private void UpdateBobberVisualAttachment()
|
|
{
|
|
if (!forceBobberVisualToLineEnd || bobberVisualRoot == null || bobberLineStartPoint == null)
|
|
return;
|
|
|
|
bobberVisualRoot.position = bobberLineStartPoint.position
|
|
+ bobberVisualWorldOffset
|
|
+ bobberLineStartPoint.rotation * bobberVisualLocalOffset;
|
|
}
|
|
|
|
private void CacheInitialBobberTransform()
|
|
{
|
|
if (bobberLineStartPoint == null)
|
|
return;
|
|
|
|
initialBobberLocalPosition = bobberLineStartPoint.localPosition;
|
|
initialBobberLocalRotation = bobberLineStartPoint.localRotation;
|
|
hasInitialBobberTransform = true;
|
|
}
|
|
|
|
private void CacheBobberVisualPosition()
|
|
{
|
|
if (bobberVisualRoot == null || hasBobberVisualInitialLocalPosition)
|
|
return;
|
|
|
|
bobberVisualInitialLocalPosition = bobberVisualRoot.localPosition;
|
|
hasBobberVisualInitialLocalPosition = true;
|
|
}
|
|
|
|
private void ResetBobberVisualPosition()
|
|
{
|
|
if (bobberVisualRoot == null || !hasBobberVisualInitialLocalPosition)
|
|
return;
|
|
|
|
bobberVisualRoot.localPosition = bobberVisualInitialLocalPosition;
|
|
}
|
|
|
|
private void UpdateProceduralLine()
|
|
{
|
|
if (rodLineEndPoint == null || bobberLineStartPoint == null)
|
|
{
|
|
SetLineVisible(!hideLineWhenMissingTargets);
|
|
return;
|
|
}
|
|
|
|
EnsureLineMeshComponents(true);
|
|
|
|
if (lineMeshFilter == null || lineMeshRenderer == null || lineMesh == null)
|
|
return;
|
|
|
|
SetLineVisible(true);
|
|
|
|
if (lineMaterial != null)
|
|
lineMeshRenderer.sharedMaterial = lineMaterial;
|
|
else
|
|
EnsureFallbackLineMaterial();
|
|
|
|
Vector3 start = rodLineEndPoint.position;
|
|
Vector3 end = bobberLineStartPoint.position;
|
|
Vector3 control = (start + end) * 0.5f;
|
|
|
|
if (lineUseSag)
|
|
{
|
|
float distance = Vector3.Distance(start, end);
|
|
Vector3 sagDir = lineSagDirection.sqrMagnitude > Epsilon ? lineSagDirection.normalized : Vector3.down;
|
|
float finalSag = Mathf.Clamp(lineSagAmount + distance * lineDistanceSagMultiplier, 0f, lineMaxSagAmount);
|
|
control += sagDir * finalSag;
|
|
}
|
|
|
|
BuildProceduralTubeMesh(start, control, end);
|
|
|
|
if (drawLineDebug)
|
|
Debug.DrawLine(start, end, Color.yellow);
|
|
}
|
|
|
|
private void EnsureLineMeshComponents(bool createIfMissing)
|
|
{
|
|
Transform root = lineMeshRoot != null ? lineMeshRoot : transform;
|
|
if (root == null)
|
|
return;
|
|
|
|
if (lineMeshFilter == null)
|
|
lineMeshFilter = root.GetComponent<MeshFilter>();
|
|
|
|
if (lineMeshRenderer == null)
|
|
lineMeshRenderer = root.GetComponent<MeshRenderer>();
|
|
|
|
if (!createIfMissing)
|
|
return;
|
|
|
|
if (lineMeshFilter == null)
|
|
lineMeshFilter = root.gameObject.AddComponent<MeshFilter>();
|
|
|
|
if (lineMeshRenderer == null)
|
|
lineMeshRenderer = root.gameObject.AddComponent<MeshRenderer>();
|
|
|
|
if (lineMesh == null)
|
|
{
|
|
lineMesh = new Mesh();
|
|
lineMesh.name = "VR Fishing Procedural Line";
|
|
lineMesh.MarkDynamic();
|
|
}
|
|
|
|
if (lineMeshFilter.sharedMesh != lineMesh)
|
|
lineMeshFilter.sharedMesh = lineMesh;
|
|
}
|
|
|
|
private void EnsureFallbackLineMaterial()
|
|
{
|
|
if (lineMeshRenderer == null)
|
|
return;
|
|
|
|
if (generatedLineMaterial == null)
|
|
{
|
|
Shader shader = Shader.Find("Universal Render Pipeline/Unlit");
|
|
if (shader == null)
|
|
shader = Shader.Find("Unlit/Color");
|
|
if (shader == null)
|
|
shader = Shader.Find("Standard");
|
|
|
|
generatedLineMaterial = new Material(shader);
|
|
generatedLineMaterial.name = "Generated_FishingLine_Material";
|
|
generatedLineMaterial.hideFlags = HideFlags.DontSave;
|
|
|
|
if (generatedLineMaterial.HasProperty("_Color"))
|
|
generatedLineMaterial.SetColor("_Color", new Color(0.02f, 0.02f, 0.02f, 1f));
|
|
if (generatedLineMaterial.HasProperty("_BaseColor"))
|
|
generatedLineMaterial.SetColor("_BaseColor", new Color(0.02f, 0.02f, 0.02f, 1f));
|
|
}
|
|
|
|
lineMeshRenderer.sharedMaterial = generatedLineMaterial;
|
|
}
|
|
|
|
private void SetLineVisible(bool visible)
|
|
{
|
|
if (lineMeshRenderer != null)
|
|
lineMeshRenderer.enabled = visible;
|
|
}
|
|
|
|
private void BuildProceduralTubeMesh(Vector3 worldStart, Vector3 worldControl, Vector3 worldEnd)
|
|
{
|
|
if (lineMesh == null || lineMeshFilter == null)
|
|
return;
|
|
|
|
Transform meshTransform = lineMeshFilter.transform;
|
|
int rings = Mathf.Max(2, lineSegments + 1);
|
|
int sides = Mathf.Max(3, lineSides);
|
|
int capVertexCount = lineAddEndCaps ? 2 : 0;
|
|
int vertexCount = rings * sides + capVertexCount;
|
|
int sideTriangleCount = (rings - 1) * sides * 6;
|
|
int capTriangleCount = lineAddEndCaps ? sides * 6 : 0;
|
|
|
|
Vector3[] vertices = new Vector3[vertexCount];
|
|
Vector3[] normals = new Vector3[vertexCount];
|
|
Vector2[] uvs = new Vector2[vertexCount];
|
|
int[] triangles = new int[sideTriangleCount + capTriangleCount];
|
|
|
|
Vector3 previousNormal = Vector3.zero;
|
|
|
|
for (int r = 0; r < rings; r++)
|
|
{
|
|
float t = (float)r / (rings - 1);
|
|
Vector3 point = QuadraticBezier(worldStart, worldControl, worldEnd, t);
|
|
Vector3 tangent = SafeNormalize(QuadraticBezierTangent(worldStart, worldControl, worldEnd, t), Vector3.forward);
|
|
|
|
Vector3 normal;
|
|
if (r == 0 || previousNormal.sqrMagnitude <= Epsilon)
|
|
{
|
|
Vector3 reference = Mathf.Abs(Vector3.Dot(tangent, Vector3.up)) > 0.94f ? Vector3.right : Vector3.up;
|
|
Vector3 binormalStart = Vector3.Cross(tangent, reference).normalized;
|
|
normal = Vector3.Cross(binormalStart, tangent).normalized;
|
|
}
|
|
else
|
|
{
|
|
normal = Vector3.ProjectOnPlane(previousNormal, tangent);
|
|
if (normal.sqrMagnitude <= Epsilon)
|
|
{
|
|
Vector3 reference = Mathf.Abs(Vector3.Dot(tangent, Vector3.up)) > 0.94f ? Vector3.right : Vector3.up;
|
|
Vector3 binormalFallback = Vector3.Cross(tangent, reference).normalized;
|
|
normal = Vector3.Cross(binormalFallback, tangent).normalized;
|
|
}
|
|
else
|
|
{
|
|
normal.Normalize();
|
|
}
|
|
}
|
|
|
|
Vector3 binormal = Vector3.Cross(tangent, normal).normalized;
|
|
previousNormal = normal;
|
|
|
|
for (int s = 0; s < sides; s++)
|
|
{
|
|
float angle = Mathf.PI * 2f * s / sides;
|
|
Vector3 radial = Mathf.Cos(angle) * normal + Mathf.Sin(angle) * binormal;
|
|
Vector3 worldVertex = point + radial * lineRadius;
|
|
int index = r * sides + s;
|
|
vertices[index] = meshTransform.InverseTransformPoint(worldVertex);
|
|
normals[index] = meshTransform.InverseTransformDirection(radial).normalized;
|
|
uvs[index] = new Vector2((float)s / sides, t);
|
|
}
|
|
}
|
|
|
|
int tri = 0;
|
|
for (int r = 0; r < rings - 1; r++)
|
|
{
|
|
for (int s = 0; s < sides; s++)
|
|
{
|
|
int a = r * sides + s;
|
|
int b = r * sides + (s + 1) % sides;
|
|
int c = (r + 1) * sides + s;
|
|
int d = (r + 1) * sides + (s + 1) % sides;
|
|
|
|
triangles[tri++] = a;
|
|
triangles[tri++] = c;
|
|
triangles[tri++] = b;
|
|
|
|
triangles[tri++] = b;
|
|
triangles[tri++] = c;
|
|
triangles[tri++] = d;
|
|
}
|
|
}
|
|
|
|
if (lineAddEndCaps)
|
|
{
|
|
int startCenter = rings * sides;
|
|
int endCenter = startCenter + 1;
|
|
vertices[startCenter] = meshTransform.InverseTransformPoint(worldStart);
|
|
vertices[endCenter] = meshTransform.InverseTransformPoint(worldEnd);
|
|
normals[startCenter] = meshTransform.InverseTransformDirection(SafeNormalize(worldStart - worldControl, -Vector3.forward));
|
|
normals[endCenter] = meshTransform.InverseTransformDirection(SafeNormalize(worldEnd - worldControl, Vector3.forward));
|
|
uvs[startCenter] = new Vector2(0.5f, 0f);
|
|
uvs[endCenter] = new Vector2(0.5f, 1f);
|
|
|
|
for (int s = 0; s < sides; s++)
|
|
{
|
|
int a = s;
|
|
int b = (s + 1) % sides;
|
|
triangles[tri++] = startCenter;
|
|
triangles[tri++] = b;
|
|
triangles[tri++] = a;
|
|
|
|
int c = (rings - 1) * sides + s;
|
|
int d = (rings - 1) * sides + (s + 1) % sides;
|
|
triangles[tri++] = endCenter;
|
|
triangles[tri++] = c;
|
|
triangles[tri++] = d;
|
|
}
|
|
}
|
|
|
|
lineMesh.Clear();
|
|
lineMesh.vertices = vertices;
|
|
lineMesh.normals = normals;
|
|
lineMesh.uv = uvs;
|
|
lineMesh.triangles = triangles;
|
|
lineMesh.RecalculateBounds();
|
|
}
|
|
|
|
private void RegisterPassiveInputAction(InputActionReference actionReference, ref bool wasEnabled)
|
|
{
|
|
if (actionReference == null || actionReference.action == null)
|
|
return;
|
|
|
|
wasEnabled = actionReference.action.enabled;
|
|
|
|
if (enableInputActionsManually && !wasEnabled)
|
|
actionReference.action.Enable();
|
|
}
|
|
|
|
private void UnregisterPassiveInputAction(InputActionReference actionReference, bool wasEnabled)
|
|
{
|
|
if (actionReference == null || actionReference.action == null)
|
|
return;
|
|
|
|
if (enableInputActionsManually && !wasEnabled)
|
|
actionReference.action.Disable();
|
|
}
|
|
|
|
private void RegisterInputAction(InputActionReference actionReference, Action<InputAction.CallbackContext> callback, ref bool wasEnabled)
|
|
{
|
|
if (actionReference == null || actionReference.action == null)
|
|
return;
|
|
|
|
wasEnabled = actionReference.action.enabled;
|
|
actionReference.action.performed += callback;
|
|
|
|
if (enableInputActionsManually && !wasEnabled)
|
|
actionReference.action.Enable();
|
|
}
|
|
|
|
private void UnregisterInputAction(InputActionReference actionReference, Action<InputAction.CallbackContext> callback, bool wasEnabled)
|
|
{
|
|
if (actionReference == null || actionReference.action == null)
|
|
return;
|
|
|
|
actionReference.action.performed -= callback;
|
|
|
|
if (enableInputActionsManually && !wasEnabled)
|
|
actionReference.action.Disable();
|
|
}
|
|
|
|
private void StopAllRoutinesSafe()
|
|
{
|
|
StopBobberMoveAndBiteRoutines();
|
|
}
|
|
|
|
private void StopBobberMoveAndBiteRoutines()
|
|
{
|
|
if (bobberMoveRoutine != null)
|
|
StopCoroutine(bobberMoveRoutine);
|
|
bobberMoveRoutine = null;
|
|
|
|
StopBiteWaitOnly();
|
|
StopBiteShakeOnly();
|
|
}
|
|
|
|
private void StopBiteWaitOnly()
|
|
{
|
|
if (biteWaitRoutine != null)
|
|
StopCoroutine(biteWaitRoutine);
|
|
biteWaitRoutine = null;
|
|
}
|
|
|
|
private void StopBiteShakeOnly()
|
|
{
|
|
if (biteShakeRoutine != null)
|
|
StopCoroutine(biteShakeRoutine);
|
|
biteShakeRoutine = null;
|
|
ResetBobberVisualPosition();
|
|
}
|
|
|
|
private void SetStartGuideVisible(bool visible)
|
|
{
|
|
if (startGuideUI != null && startGuideUI.activeSelf != visible)
|
|
startGuideUI.SetActive(visible);
|
|
}
|
|
|
|
private Transform FindChildRecursive(Transform root, params string[] names)
|
|
{
|
|
if (root == null || names == null)
|
|
return null;
|
|
|
|
for (int i = 0; i < names.Length; i++)
|
|
{
|
|
if (string.Equals(NormalizeName(root.name), NormalizeName(names[i]), StringComparison.OrdinalIgnoreCase))
|
|
return root;
|
|
}
|
|
|
|
for (int i = 0; i < root.childCount; i++)
|
|
{
|
|
Transform found = FindChildRecursive(root.GetChild(i), names);
|
|
if (found != null)
|
|
return found;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private string NormalizeName(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return string.Empty;
|
|
|
|
return value.Replace(" ", string.Empty)
|
|
.Replace("_", string.Empty)
|
|
.Replace("-", string.Empty)
|
|
.ToLowerInvariant();
|
|
}
|
|
|
|
private bool CompareTagSafe(GameObject obj, string tagName)
|
|
{
|
|
if (obj == null || string.IsNullOrWhiteSpace(tagName))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
return obj.CompareTag(tagName);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private Vector3 QuadraticBezier(Vector3 a, Vector3 b, Vector3 c, float t)
|
|
{
|
|
float u = 1f - t;
|
|
return u * u * a + 2f * u * t * b + t * t * c;
|
|
}
|
|
|
|
private Vector3 QuadraticBezierTangent(Vector3 a, Vector3 b, Vector3 c, float t)
|
|
{
|
|
return 2f * (1f - t) * (b - a) + 2f * t * (c - b);
|
|
}
|
|
|
|
private Vector3 SafeNormalize(Vector3 value, Vector3 fallback)
|
|
{
|
|
if (value.sqrMagnitude <= Epsilon)
|
|
return fallback.sqrMagnitude > Epsilon ? fallback.normalized : Vector3.forward;
|
|
|
|
return value.normalized;
|
|
}
|
|
|
|
private float EaseOutCubic(float t)
|
|
{
|
|
t = Mathf.Clamp01(t);
|
|
t = 1f - t;
|
|
return 1f - t * t * t;
|
|
}
|
|
|
|
private void LogReel(string message)
|
|
{
|
|
if (showReelDebugLog)
|
|
Debug.Log($"[FishingRodVRController/Reel] {message}", this);
|
|
}
|
|
|
|
private void Log(string message)
|
|
{
|
|
if (showDebugLog)
|
|
Debug.Log($"[FishingRodVRController] {message}", this);
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (drawStartAreaGizmo && startMode == StartMode.StartActionInArea && useStartAreaCheck)
|
|
{
|
|
Transform area = startAreaCenter != null ? startAreaCenter : transform;
|
|
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.25f);
|
|
Matrix4x4 oldMatrix = Gizmos.matrix;
|
|
Gizmos.matrix = Matrix4x4.TRS(area.position, area.rotation, Vector3.one);
|
|
Gizmos.DrawCube(Vector3.zero, startAreaHalfExtents * 2f);
|
|
Gizmos.color = new Color(0.2f, 0.8f, 1f, 1f);
|
|
Gizmos.DrawWireCube(Vector3.zero, startAreaHalfExtents * 2f);
|
|
Gizmos.matrix = oldMatrix;
|
|
}
|
|
|
|
if (useReelControl && reelCenterPoint != null)
|
|
{
|
|
Gizmos.color = new Color(1f, 0.65f, 0.15f, 0.7f);
|
|
Gizmos.DrawWireSphere(reelCenterPoint.position, reelActivationRadius);
|
|
Vector3 axis = reelCenterPoint.TransformDirection(reelLocalAxis.sqrMagnitude > Epsilon ? reelLocalAxis.normalized : Vector3.right);
|
|
Gizmos.DrawLine(reelCenterPoint.position - axis * 0.15f, reelCenterPoint.position + axis * 0.15f);
|
|
}
|
|
|
|
if (drawWaterCheckGizmo && bobberLineStartPoint != null)
|
|
{
|
|
Gizmos.color = new Color(0.2f, 0.6f, 1f, 0.6f);
|
|
Gizmos.DrawWireSphere(bobberLineStartPoint.position, waterCheckRadius);
|
|
}
|
|
}
|
|
}
|