Initial commit
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user