Added UI to Furnance, fixed carry and drag obj and added fish

This commit is contained in:
2026-08-12 12:04:54 +03:00
parent 73069d8242
commit 487e0d72ff
73 changed files with 13209 additions and 15923 deletions
@@ -2,6 +2,14 @@ using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;
using UnityEngine.Serialization;
public enum DraggableMovementMode
{
Free3D = 0,
KeepWorldHeight = 1,
FollowGround = 2
}
[RequireComponent(typeof(Rigidbody), typeof(NetworkRigidbody))]
public sealed class NetworkDraggableInteractable : NetworkInteractable
@@ -18,7 +26,23 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
[SerializeField, Min(0.1f)] private float maximumDragSpeed = 3f;
[SerializeField, Range(0.05f, 1f)] private float minimumSpeedMultiplier = 0.35f;
[SerializeField, Min(0.5f)] private float maximumDraggerDistance = 4.5f;
[SerializeField] private bool keepCurrentHeight;
[SerializeField, Min(0.01f)] private float positionSmoothTime = 0.12f;
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 8f;
[SerializeField] private Vector3 rotationOffset;
[FormerlySerializedAs("keepCurrentHeight")]
[SerializeField] private DraggableMovementMode movementMode;
[Header("Ground Following")]
[SerializeField] private LayerMask groundLayers = ~0;
[SerializeField] private Transform groundContactPoint;
[SerializeField, Min(0.01f)] private float groundProbeRadius = 0.2f;
[SerializeField, Min(0.1f)] private float groundProbeHeight = 1.25f;
[SerializeField, Min(0.1f)] private float maximumGroundDrop = 2f;
[SerializeField, Range(0f, 89f)] private float maximumGroundAngle = 55f;
[SerializeField, Min(0f)] private float groundClearance = 0.02f;
[SerializeField, Min(0.1f)] private float maximumGroundVerticalSpeed = 6f;
[SerializeField] private bool alignToGround = true;
[SerializeField, Min(0.1f)] private float groundAlignmentSpeed = 10f;
[Header("Physics")]
[SerializeField] private Rigidbody physicsBody;
@@ -36,7 +60,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
NetworkVariableWritePermission.Server);
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private readonly RaycastHit[] groundHits = new RaycastHit[16];
private float nextCollisionRefreshTime;
private Vector3 localGroundContactPoint;
private Vector3 smoothedGroundNormal = Vector3.up;
private Vector3 dragVelocity;
public float Weight => weight;
public int ActiveDraggerCount
@@ -120,18 +148,33 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
return;
RemoveInvalidDraggers();
if (!HasEnoughPullStrength || !TryGetCombinedDragTarget(out Vector3 target))
if (!HasEnoughPullStrength ||
!TryGetCombinedDragTarget(out Vector3 target, out Vector3 forward))
{
dragVelocity = Vector3.zero;
return;
if (keepCurrentHeight)
target.y = physicsBody.position.y;
}
float totalStrength = ActiveDraggerCount * pullStrengthPerPlayer;
float loadRatio = Mathf.Clamp01(weight / totalStrength);
float speedMultiplier = Mathf.Lerp(1f, minimumSpeedMultiplier, loadRatio);
float movementStep = maximumDragSpeed * speedMultiplier * Time.fixedDeltaTime;
Vector3 nextPosition = Vector3.MoveTowards(physicsBody.position, target, movementStep);
physicsBody.MovePosition(nextPosition);
float movementSpeed = maximumDragSpeed * speedMultiplier;
switch (movementMode)
{
case DraggableMovementMode.KeepWorldHeight:
target.y = physicsBody.position.y;
FollowDragTarget(target, forward, movementSpeed, Vector3.up);
break;
case DraggableMovementMode.FollowGround:
MoveAlongGround(target, forward, movementSpeed);
break;
default:
FollowDragTarget(target, forward, movementSpeed, Vector3.up);
break;
}
}
protected override bool CanInteractOnServer(ulong senderClientId) =>
@@ -188,27 +231,42 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
return (playerObject.transform.position - physicsBody.position).sqrMagnitude <= maximumDistanceSquared;
}
private bool TryGetCombinedDragTarget(out Vector3 target)
private bool TryGetCombinedDragTarget(out Vector3 target, out Vector3 forward)
{
target = Vector3.zero;
forward = Vector3.zero;
int validTargets = 0;
AddDragTarget(firstDraggerClientId.Value, ref target, ref validTargets);
AddDragTarget(secondDraggerClientId.Value, ref target, ref validTargets);
AddDragTarget(firstDraggerClientId.Value, ref target, ref forward, ref validTargets);
AddDragTarget(secondDraggerClientId.Value, ref target, ref forward, ref validTargets);
if (validTargets == 0)
return false;
target /= validTargets;
forward = Vector3.ProjectOnPlane(forward, Vector3.up);
if (forward.sqrMagnitude < 0.0001f)
forward = Vector3.ProjectOnPlane(target - physicsBody.position, Vector3.up);
if (forward.sqrMagnitude < 0.0001f)
forward = Vector3.ProjectOnPlane(physicsBody.transform.forward, Vector3.up);
forward.Normalize();
return true;
}
private void AddDragTarget(ulong clientId, ref Vector3 target, ref int validTargets)
private void AddDragTarget(
ulong clientId,
ref Vector3 target,
ref Vector3 forward,
ref int validTargets)
{
if (clientId == NoDragger || !TryGetPlayerObject(clientId, out NetworkObject playerObject))
return;
target += playerObject.transform.TransformPoint(dragOffset);
forward += playerObject.transform.forward;
validTargets++;
}
@@ -221,15 +279,32 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
{
if (physicsBody != null)
{
physicsBody.isKinematic = HasAnyDragger || !IsServer;
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
bool shouldBeKinematic = HasAnyDragger || !IsServer;
if (physicsBody.isKinematic && !shouldBeKinematic)
physicsBody.isKinematic = false;
if (!physicsBody.isKinematic)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
physicsBody.isKinematic = shouldBeKinematic;
}
if (HasAnyDragger)
{
dragVelocity = Vector3.zero;
CacheGroundContactPoint();
smoothedGroundNormal = physicsBody != null ? physicsBody.transform.up : Vector3.up;
IgnoreCollisionsWithPlayers();
}
else
{
dragVelocity = Vector3.zero;
RestorePlayerCollisions();
}
}
private void ResolvePhysicsReferences()
@@ -239,6 +314,8 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
if (objectColliders == null || objectColliders.Length == 0)
objectColliders = GetComponentsInChildren<Collider>(true);
CacheGroundContactPoint();
}
private void ApplyWeight()
@@ -247,6 +324,178 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
physicsBody.mass = Mathf.Max(0.1f, weight);
}
private void FollowDragTarget(
Vector3 target,
Vector3 forward,
float movementSpeed,
Vector3 surfaceNormal)
{
Vector3 nextPosition = Vector3.SmoothDamp(
physicsBody.position,
target,
ref dragVelocity,
positionSmoothTime,
movementSpeed,
Time.fixedDeltaTime);
physicsBody.MovePosition(nextPosition);
physicsBody.MoveRotation(CalculateDragRotation(forward, surfaceNormal));
}
private void MoveAlongGround(
Vector3 dragTarget,
Vector3 forward,
float movementSpeed)
{
Vector3 currentPosition = physicsBody.position;
Vector3 planarTarget = new(dragTarget.x, currentPosition.y, dragTarget.z);
Vector3 planarVelocity = new(dragVelocity.x, 0f, dragVelocity.z);
Vector3 nextPlanarPosition = Vector3.SmoothDamp(
currentPosition,
planarTarget,
ref planarVelocity,
positionSmoothTime,
movementSpeed,
Time.fixedDeltaTime);
nextPlanarPosition.y = currentPosition.y;
dragVelocity = new Vector3(planarVelocity.x, dragVelocity.y, planarVelocity.z);
Vector3 probePosition = nextPlanarPosition;
if (!TryGetGroundSurface(probePosition, out Vector3 groundPoint, out Vector3 groundNormal))
return;
Quaternion targetRotation = CalculateDragRotation(forward, groundNormal);
Vector3 scaledContactPoint = Vector3.Scale(localGroundContactPoint, physicsBody.transform.lossyScale);
float contactOffsetY = (targetRotation * scaledContactPoint).y;
float targetHeight = groundPoint.y - contactOffsetY + groundClearance;
float nextHeight = Mathf.MoveTowards(
currentPosition.y,
targetHeight,
maximumGroundVerticalSpeed * Time.fixedDeltaTime);
physicsBody.MovePosition(new Vector3(nextPlanarPosition.x, nextHeight, nextPlanarPosition.z));
physicsBody.MoveRotation(targetRotation);
}
private bool TryGetGroundSurface(
Vector3 probePosition,
out Vector3 groundPoint,
out Vector3 groundNormal)
{
Vector3 origin = probePosition + Vector3.up * groundProbeHeight;
float castDistance = groundProbeHeight + maximumGroundDrop;
int hitCount = Physics.SphereCastNonAlloc(
origin,
groundProbeRadius,
Vector3.down,
groundHits,
castDistance,
groundLayers,
QueryTriggerInteraction.Ignore);
float nearestDistance = float.PositiveInfinity;
groundPoint = default;
groundNormal = Vector3.up;
for (int index = 0; index < hitCount; index++)
{
RaycastHit hit = groundHits[index];
if (hit.collider == null || IsOwnCollider(hit.collider) ||
hit.collider.GetComponentInParent<CharacterController>() != null)
{
continue;
}
if (Vector3.Angle(Vector3.up, hit.normal) > maximumGroundAngle ||
hit.distance >= nearestDistance)
{
continue;
}
nearestDistance = hit.distance;
groundPoint = hit.point;
groundNormal = hit.normal;
}
return !float.IsPositiveInfinity(nearestDistance);
}
private Quaternion CalculateDragRotation(Vector3 forward, Vector3 groundNormal)
{
Vector3 targetUp = alignToGround ? groundNormal : Vector3.up;
float normalBlend = 1f - Mathf.Exp(-groundAlignmentSpeed * Time.fixedDeltaTime);
smoothedGroundNormal = Vector3.Slerp(
smoothedGroundNormal,
targetUp,
normalBlend).normalized;
Vector3 surfaceForward = Vector3.ProjectOnPlane(forward, smoothedGroundNormal);
if (surfaceForward.sqrMagnitude < 0.0001f)
surfaceForward = Vector3.ProjectOnPlane(physicsBody.transform.forward, smoothedGroundNormal);
Quaternion desiredRotation = Quaternion.LookRotation(
surfaceForward.normalized,
smoothedGroundNormal) * Quaternion.Euler(rotationOffset);
float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.fixedDeltaTime);
return Quaternion.Slerp(physicsBody.rotation, desiredRotation, rotationBlend);
}
private void CacheGroundContactPoint()
{
if (physicsBody == null)
return;
if (groundContactPoint != null)
{
localGroundContactPoint = physicsBody.transform.InverseTransformPoint(groundContactPoint.position);
return;
}
bool hasBounds = false;
Bounds combinedBounds = default;
if (objectColliders != null)
{
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider == null || objectCollider.isTrigger || !objectCollider.enabled)
continue;
if (!hasBounds)
{
combinedBounds = objectCollider.bounds;
hasBounds = true;
}
else
{
combinedBounds.Encapsulate(objectCollider.bounds);
}
}
}
Vector3 contactWorldPosition = hasBounds
? new Vector3(combinedBounds.center.x, combinedBounds.min.y, combinedBounds.center.z)
: physicsBody.position;
localGroundContactPoint = physicsBody.transform.InverseTransformPoint(contactWorldPosition);
}
private bool IsOwnCollider(Collider candidate)
{
if (objectColliders == null)
return false;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider == candidate)
return true;
}
return false;
}
private void IgnoreCollisionsWithPlayers()
{
foreach (CharacterController playerCollider in FindObjectsByType<CharacterController>(
@@ -14,6 +14,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable
[SerializeField] private Collider[] objectColliders;
[SerializeField, Min(0.05f)] private float collisionRefreshInterval = 0.25f;
[Header("Holding Follow")]
[SerializeField, Min(0.01f)] private float positionSmoothTime = 0.12f;
[SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f;
[SerializeField, Min(0.1f)] private float maximumFollowDistance = 1.25f;
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
[Header("Throw")]
[SerializeField, Min(0f)] private float throwForce = 6f;
[SerializeField, Min(0f)] private float throwUpwardForce = 0.5f;
@@ -25,6 +31,9 @@ public abstract class NetworkHeldInteractable : NetworkInteractable
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private float nextCollisionRefreshTime;
private Vector3 followVelocity;
private Vector3 previousHolderPosition;
private bool hasPreviousHolderPosition;
protected abstract Vector3 HoldingOffset { get; }
protected abstract string DropPrompt { get; }
@@ -79,10 +88,9 @@ public abstract class NetworkHeldInteractable : NetworkInteractable
return;
}
FollowHolderTranslation(holder.transform.position);
Transform anchor = ResolveHoldingAnchor(holder);
transform.SetPositionAndRotation(
anchor.TransformPoint(HoldingOffset),
anchor.rotation);
FollowHoldingAnchor(anchor);
if (Time.time >= nextCollisionRefreshTime)
{
@@ -175,16 +183,70 @@ public abstract class NetworkHeldInteractable : NetworkInteractable
return cameraController != null ? cameraController.AimTransform : holder.transform;
}
private void FollowHoldingAnchor(Transform anchor)
{
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
Vector3 targetDelta = targetPosition - transform.position;
if (targetDelta.sqrMagnitude > maximumFollowDistance * maximumFollowDistance)
{
transform.position = targetPosition -
targetDelta.normalized * maximumFollowDistance;
followVelocity = Vector3.zero;
}
transform.position = Vector3.SmoothDamp(
transform.position,
targetPosition,
ref followVelocity,
positionSmoothTime,
maximumFollowSpeed,
Time.deltaTime);
float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.deltaTime);
transform.rotation = Quaternion.Slerp(
transform.rotation,
anchor.rotation,
rotationBlend);
}
private void FollowHolderTranslation(Vector3 holderPosition)
{
if (!hasPreviousHolderPosition)
{
previousHolderPosition = holderPosition;
hasPreviousHolderPosition = true;
return;
}
Vector3 holderDelta = holderPosition - previousHolderPosition;
previousHolderPosition = holderPosition;
transform.position += holderDelta;
}
private void HandleHolderChanged(ulong _, ulong currentHolder) =>
ApplyHoldingState(currentHolder != NoHolder);
private void ApplyHoldingState(bool isHeld)
{
followVelocity = Vector3.zero;
hasPreviousHolderPosition = false;
if (physicsBody != null)
{
physicsBody.isKinematic = isHeld || !IsServer;
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
bool shouldBeKinematic = isHeld || !IsServer;
if (physicsBody.isKinematic && !shouldBeKinematic)
physicsBody.isKinematic = false;
if (!physicsBody.isKinematic)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
physicsBody.isKinematic = shouldBeKinematic;
}
if (isHeld)
@@ -16,6 +16,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
private float pitch;
private bool isInitialized;
public Camera PlayerCamera => playerCamera;
public Transform AimTransform => playerCamera != null ? playerCamera.transform : transform;
public Vector3 AimForward => AimTransform.forward;
@@ -38,6 +39,8 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
if (!isLocalPlayer)
return;
LocalPlayerCameraRegistry.Register(playerCamera);
if (playerRenderer != null)
playerRenderer.enabled = false;
@@ -98,6 +101,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
if (IsOwner)
{
LocalPlayerCameraRegistry.Unregister(playerCamera);
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
@@ -0,0 +1,34 @@
using System;
using UnityEngine;
public static class LocalPlayerCameraRegistry
{
public static Camera Current { get; private set; }
public static event Action<Camera> Changed;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetState()
{
Current = null;
Changed = null;
}
public static void Register(Camera camera)
{
if (camera == null || Current == camera)
return;
Current = camera;
Changed?.Invoke(Current);
}
public static void Unregister(Camera camera)
{
if (Current != camera)
return;
Current = null;
Changed?.Invoke(null);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e3d71c247514be98407f39adb750371
@@ -0,0 +1,52 @@
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Canvas))]
public sealed class WorldSpaceCanvasCameraBinder : MonoBehaviour
{
[SerializeField] private Canvas targetCanvas;
public Canvas TargetCanvas => targetCanvas;
private void Reset()
{
targetCanvas = GetComponent<Canvas>();
RefreshBinding();
}
private void Awake() => ResolveCanvas();
private void OnEnable()
{
LocalPlayerCameraRegistry.Changed += HandleLocalCameraChanged;
RefreshBinding();
}
private void OnDisable() =>
LocalPlayerCameraRegistry.Changed -= HandleLocalCameraChanged;
public void RefreshBinding()
{
ResolveCanvas();
if (targetCanvas == null || targetCanvas.renderMode != RenderMode.WorldSpace)
return;
targetCanvas.worldCamera = LocalPlayerCameraRegistry.Current;
}
private void ResolveCanvas()
{
if (targetCanvas == null)
targetCanvas = GetComponent<Canvas>();
}
private void HandleLocalCameraChanged(Camera localCamera)
{
if (targetCanvas != null && targetCanvas.renderMode == RenderMode.WorldSpace)
targetCanvas.worldCamera = localCamera;
}
[ContextMenu("Refresh Local Player Camera")]
private void RefreshFromContextMenu() => RefreshBinding();
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 271d68bfa3ee4606a3ca57fa19150a49
@@ -20,6 +20,8 @@ public sealed class FurnacePanelUIController : MonoBehaviour, IGlobalUISectionCo
if (furnace != null)
furnace.Changed += Refresh;
if (view != null)
view.StartRequested += StartSmelting;
GlobalUIController.Instance?.Register(this);
@@ -30,6 +32,8 @@ public sealed class FurnacePanelUIController : MonoBehaviour, IGlobalUISectionCo
{
if (furnace != null)
furnace.Changed -= Refresh;
if (view != null)
view.StartRequested -= StartSmelting;
GlobalUIController.Instance?.Unregister(this);
}
@@ -1,3 +1,4 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
@@ -12,6 +13,20 @@ public sealed class FurnacePanelUIView : MonoBehaviour
[SerializeField] private Image progressImage;
[SerializeField] private Button startButton;
public event Action StartRequested;
private void OnEnable()
{
if (startButton != null)
startButton.onClick.AddListener(HandleStartClicked);
}
private void OnDisable()
{
if (startButton != null)
startButton.onClick.RemoveListener(HandleStartClicked);
}
public void Render(
float storedMetal,
float metalCapacity,
@@ -48,4 +63,6 @@ public sealed class FurnacePanelUIView : MonoBehaviour
if (startButton != null)
startButton.interactable = canStart;
}
private void HandleStartClicked() => StartRequested?.Invoke();
}