186 lines
7.1 KiB
C#
186 lines
7.1 KiB
C#
using UnityEngine;
|
|
|
|
public class AxisRotateStrategy : IGhostMovementStrategy
|
|
{
|
|
public bool OverridesSurfaceTracking => true;
|
|
|
|
public enum LocalAxis { Up, Forward, Right }
|
|
|
|
private readonly LocalAxis _localAxis;
|
|
private readonly Vector3 _lockedPosition;
|
|
|
|
private bool _isRotating;
|
|
private float _accumulatedDegrees;
|
|
private Vector2 _lastMousePos;
|
|
|
|
private Quaternion _unsnappedRot;
|
|
private Vector3 _unsnappedPos;
|
|
private bool _wasSnapped;
|
|
|
|
public float Sensitivity = 0.4f;
|
|
public float SnapAngle = 15f;
|
|
public bool EnableSnapping = false;
|
|
|
|
/// <param name="localAxis">Local axis of the ghost to rotate around.</param>
|
|
/// <param name="lockedPosition">Ghost world position frozen for the entire interaction.</param>
|
|
public AxisRotateStrategy(LocalAxis localAxis, Vector3 lockedPosition)
|
|
{
|
|
_localAxis = localAxis;
|
|
_lockedPosition = lockedPosition;
|
|
}
|
|
|
|
public void BeginRotate()
|
|
{
|
|
_lastMousePos = Input.mousePosition;
|
|
_accumulatedDegrees = 0f;
|
|
_isRotating = true;
|
|
_wasSnapped = false;
|
|
}
|
|
|
|
public void EndRotate() => _isRotating = false;
|
|
|
|
public void UpdateMovement(ref GhostMovementContext ctx)
|
|
{
|
|
// 1. Revert previous frame's snap so we rotate from the true axial position
|
|
if (_wasSnapped)
|
|
{
|
|
ctx.GhostTransform.position = _unsnappedPos;
|
|
ctx.GhostTransform.rotation = _unsnappedRot;
|
|
_wasSnapped = false;
|
|
}
|
|
|
|
// Read the axis from the ghost's current orientation every frame
|
|
// so it stays local regardless of how the ghost has been rotated
|
|
Vector3 worldAxis = GetWorldAxis(ctx.GhostTransform);
|
|
|
|
if (_isRotating)
|
|
{
|
|
Vector2 mouseDelta = (Vector2)Input.mousePosition - _lastMousePos;
|
|
_lastMousePos = Input.mousePosition;
|
|
|
|
if (mouseDelta.sqrMagnitude > 0.001f)
|
|
{
|
|
// Project the world axis into screen space to get its 2D direction
|
|
Vector3 axisScreenStart = Camera.main.WorldToScreenPoint(_lockedPosition);
|
|
Vector3 axisScreenEnd = Camera.main.WorldToScreenPoint(_lockedPosition + worldAxis);
|
|
Vector2 axisScreen = (axisScreenEnd - axisScreenStart).normalized;
|
|
|
|
// The rotation direction is the perpendicular to the screen-space axis
|
|
// Dot the mouse delta against that perpendicular to get signed rotation
|
|
Vector2 perpendicular = new Vector2(-axisScreen.y, axisScreen.x);
|
|
float signedDelta = Vector2.Dot(mouseDelta, perpendicular);
|
|
|
|
if (ctx.SnapRotation || EnableSnapping)
|
|
{
|
|
_accumulatedDegrees += signedDelta * Sensitivity;
|
|
|
|
float sign = Mathf.Sign(_accumulatedDegrees);
|
|
while (Mathf.Abs(_accumulatedDegrees) >= SnapAngle)
|
|
{
|
|
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, SnapAngle * sign);
|
|
_accumulatedDegrees -= SnapAngle * sign;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float pendingDegrees = signedDelta * Sensitivity;
|
|
if (pendingDegrees != 0f)
|
|
{
|
|
ctx.GhostTransform.RotateAround(_lockedPosition, worldAxis, pendingDegrees);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the pure axial rotation state
|
|
_unsnappedPos = ctx.GhostTransform.position;
|
|
_unsnappedRot = ctx.GhostTransform.rotation;
|
|
|
|
// Attempt to snap to nearby sockets visually
|
|
if (TrySocketSnap(ref ctx))
|
|
{
|
|
_wasSnapped = true;
|
|
}
|
|
|
|
ctx.RotationAxis = worldAxis;
|
|
}
|
|
|
|
private bool TrySocketSnap(ref GhostMovementContext ctx)
|
|
{
|
|
SocketPoint[] mySockets = ctx.GhostTransform.GetComponentsInChildren<SocketPoint>();
|
|
if (mySockets.Length == 0) return false;
|
|
|
|
SocketPoint bestMySocket = null;
|
|
SocketPoint bestTargetSocket = null;
|
|
float bestDist = 0.5f;
|
|
|
|
foreach (var mySocket in mySockets)
|
|
{
|
|
Collider[] hits = Physics.OverlapSphere(mySocket.transform.position, bestDist, Physics.AllLayers, QueryTriggerInteraction.Collide);
|
|
foreach (var hit in hits)
|
|
{
|
|
if (hit.transform.root == ctx.GhostTransform.root) continue;
|
|
|
|
SocketPoint[] targetSockets = hit.transform.root.GetComponentsInChildren<SocketPoint>();
|
|
foreach (var target in targetSockets)
|
|
{
|
|
if (target.IsOccupied()) continue;
|
|
if (!target.CanAccept(mySocket)) continue;
|
|
|
|
float dist = Vector3.Distance(mySocket.transform.position, target.transform.position);
|
|
if (dist < bestDist)
|
|
{
|
|
bestDist = dist;
|
|
bestMySocket = mySocket;
|
|
bestTargetSocket = target;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestMySocket != null && bestTargetSocket != null)
|
|
{
|
|
// Align the rotations exactly like FreeMoveStrategy
|
|
Vector3 myNormal = bestMySocket.GetNormal().normalized;
|
|
Vector3 myTargetNormal = -bestTargetSocket.GetNormal().normalized;
|
|
|
|
Quaternion primaryRot = Quaternion.FromToRotation(myNormal, myTargetNormal);
|
|
ctx.GhostTransform.rotation = primaryRot * ctx.GhostTransform.rotation;
|
|
|
|
Vector3 mySecondary = Mathf.Abs(Vector3.Dot(bestMySocket.transform.up, myTargetNormal)) < 0.9f
|
|
? bestMySocket.transform.up
|
|
: bestMySocket.transform.forward;
|
|
|
|
Vector3 targetSecondary = Mathf.Abs(Vector3.Dot(bestTargetSocket.transform.up, -myTargetNormal)) < 0.9f
|
|
? bestTargetSocket.transform.up
|
|
: bestTargetSocket.transform.forward;
|
|
|
|
Vector3 projectedMySecondary = Vector3.ProjectOnPlane(mySecondary, myTargetNormal).normalized;
|
|
Vector3 projectedTargetSecondary = Vector3.ProjectOnPlane(targetSecondary, myTargetNormal).normalized;
|
|
|
|
if (projectedMySecondary != Vector3.zero && projectedTargetSecondary != Vector3.zero)
|
|
{
|
|
float angle = Vector3.SignedAngle(projectedMySecondary, projectedTargetSecondary, myTargetNormal);
|
|
float correction = angle - Mathf.Round(angle / 45f) * 45f;
|
|
ctx.GhostTransform.rotation = Quaternion.AngleAxis(correction, myTargetNormal) * ctx.GhostTransform.rotation;
|
|
}
|
|
|
|
// Translate to connect the sockets perfectly
|
|
Vector3 shift = bestTargetSocket.transform.position - bestMySocket.transform.position;
|
|
ctx.GhostTransform.position += shift;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Re-evaluated every frame from the ghost's current transform
|
|
private Vector3 GetWorldAxis(Transform t) => _localAxis switch
|
|
{
|
|
LocalAxis.Up => t.up,
|
|
LocalAxis.Forward => t.forward,
|
|
LocalAxis.Right => t.right,
|
|
_ => t.up,
|
|
};
|
|
} |