Added new Rocket-Processing

This commit is contained in:
2026-08-14 16:54:09 +03:00
parent 3c490bdae8
commit b5eadea728
36 changed files with 17653 additions and 1984 deletions
@@ -15,4 +15,5 @@ MonoBehaviour:
itemId: rocket-back-standart-ready
displayName: Rocket-Back-Standart-Ready
category: 2
isAssemblyReady: 1
worldPrefab: {fileID: 5195529150077527847, guid: ea8e439364ce6aa4dab3586bae0edfed, type: 3}
@@ -15,4 +15,5 @@ MonoBehaviour:
itemId: rocket-base-avia-ready
displayName: Rocket-Base-Avia-Ready
category: 2
isAssemblyReady: 1
worldPrefab: {fileID: 1256934312238460503, guid: 9712e2459410a6d43b55ea9d3847e29a, type: 3}
@@ -15,4 +15,5 @@ MonoBehaviour:
itemId: rocket-base-standart-ready
displayName: Rocket-Base-Standart-Ready
category: 2
isAssemblyReady: 1
worldPrefab: {fileID: 875723161486981202, guid: 6e681ca5358fff648ba85ae9e494fa77, type: 3}
@@ -15,4 +15,5 @@ MonoBehaviour:
itemId: rocket-cone-standart-ready
displayName: Rocket-Cone-Standart-Ready
category: 2
isAssemblyReady: 1
worldPrefab: {fileID: 7151289907260619783, guid: a31204f69b185d54ab9e12b0a3a1e98b, type: 3}
@@ -66,11 +66,16 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
NetworkVariableWritePermission.Server);
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private readonly HashSet<Collider> desiredIgnoredColliders = new();
private readonly List<Collider> collidersToRestore = new();
private readonly RaycastHit[] groundHits = new RaycastHit[16];
private float nextCollisionRefreshTime;
private Vector3 localGroundContactPoint;
private Vector3 smoothedGroundNormal = Vector3.up;
private Vector3 dragVelocity;
private float dragWorldHeight;
private bool hasDragReferenceHeight;
private bool initialUseGravity;
private CollisionDetectionMode initialCollisionDetectionMode;
public float Weight => weight;
public int ActiveDraggerCount
@@ -125,6 +130,12 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
{
ResolvePhysicsReferences();
ApplyWeight();
if (physicsBody != null)
{
initialUseGravity = physicsBody.useGravity;
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
}
}
private void OnValidate()
@@ -157,7 +168,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
return;
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
IgnoreCollisionsWithPlayers();
RefreshDraggerCollisions();
}
private void FixedUpdate()
@@ -168,10 +179,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
RemoveInvalidDraggers();
if (!HasEnoughPullStrength ||
!TryGetCombinedDragTarget(out Vector3 target, out Vector3 forward))
{
dragVelocity = Vector3.zero;
return;
}
float totalStrength = ActiveDraggerCount * pullStrengthPerPlayer;
float loadRatio = Mathf.Clamp01(weight / totalStrength);
@@ -181,7 +189,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
switch (movementMode)
{
case DraggableMovementMode.KeepWorldHeight:
target.y = physicsBody.position.y;
target.y = dragWorldHeight;
FollowDragTarget(target, forward, movementSpeed, Vector3.up);
break;
@@ -360,6 +368,21 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
return true;
}
public bool TryReleaseExternalControlToPlayer(ulong clientId)
{
if (!IsServer || !IsSpawned || !IsExternallyControlled ||
TryGetDraggedObject(clientId, this, out _) ||
!TryGetPlayerObject(clientId, out _))
{
return false;
}
isExternallyControlled.Value = false;
AddDragger(clientId);
ApplyDraggingState();
return IsDragger(clientId);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestReleaseRpc(RpcParams rpcParams = default)
{
@@ -379,34 +402,52 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
{
if (physicsBody != null)
{
bool shouldBeKinematic = HasAnyDragger || IsExternallyControlled || !IsServer;
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
if (physicsBody.isKinematic && !shouldBeKinematic)
if (shouldBeKinematic)
{
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
physicsBody.isKinematic = true;
physicsBody.useGravity = false;
}
else
{
physicsBody.isKinematic = false;
physicsBody.useGravity = initialUseGravity;
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
}
if (!physicsBody.isKinematic)
if (IsExternallyControlled)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
physicsBody.isKinematic = shouldBeKinematic;
}
if (HasAnyDragger || IsExternallyControlled)
if (HasAnyDragger)
{
dragVelocity = Vector3.zero;
if (!hasDragReferenceHeight && physicsBody != null)
{
dragWorldHeight = physicsBody.position.y;
hasDragReferenceHeight = true;
}
CacheGroundContactPoint();
smoothedGroundNormal = physicsBody != null ? physicsBody.transform.up : Vector3.up;
IgnoreCollisionsWithPlayers();
RefreshDraggerCollisions();
}
else
{
dragVelocity = Vector3.zero;
hasDragReferenceHeight = false;
RestorePlayerCollisions();
}
}
private CollisionDetectionMode ResolveDynamicCollisionDetectionMode() =>
initialCollisionDetectionMode == CollisionDetectionMode.Discrete
? CollisionDetectionMode.ContinuousDynamic
: initialCollisionDetectionMode;
private void ResolvePhysicsReferences()
{
if (physicsBody == null)
@@ -430,16 +471,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
float movementSpeed,
Vector3 surfaceNormal)
{
Vector3 nextPosition = Vector3.SmoothDamp(
physicsBody.position,
DrivePhysicsBody(
target,
ref dragVelocity,
positionSmoothTime,
CalculateDragRotation(forward, surfaceNormal),
movementSpeed,
Time.fixedDeltaTime);
physicsBody.MovePosition(nextPosition);
physicsBody.MoveRotation(CalculateDragRotation(forward, surfaceNormal));
movementSpeed);
}
private void MoveAlongGround(
@@ -449,18 +485,10 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
{
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;
Vector3 planarStep = Vector3.ClampMagnitude(
planarTarget - currentPosition,
movementSpeed * Time.fixedDeltaTime);
Vector3 probePosition = currentPosition + planarStep;
if (!TryGetGroundSurface(probePosition, out Vector3 groundPoint, out Vector3 groundNormal))
return;
@@ -469,14 +497,55 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
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);
DrivePhysicsBody(
new Vector3(dragTarget.x, targetHeight, dragTarget.z),
targetRotation,
movementSpeed,
maximumGroundVerticalSpeed);
}
physicsBody.MovePosition(new Vector3(nextPlanarPosition.x, nextHeight, nextPlanarPosition.z));
private void DrivePhysicsBody(
Vector3 targetPosition,
Quaternion targetRotation,
float maximumPlanarSpeed,
float maximumVerticalSpeed)
{
Vector3 positionDelta = targetPosition - physicsBody.position;
float responseTime = Mathf.Max(positionSmoothTime, 0.01f);
Vector3 desiredVelocity = positionDelta / responseTime;
Vector3 desiredPlanarVelocity = Vector3.ClampMagnitude(
new Vector3(desiredVelocity.x, 0f, desiredVelocity.z),
maximumPlanarSpeed);
desiredVelocity = new Vector3(
desiredPlanarVelocity.x,
Mathf.Clamp(desiredVelocity.y, -maximumVerticalSpeed, maximumVerticalSpeed),
desiredPlanarVelocity.z);
physicsBody.MoveRotation(targetRotation);
float maximumAcceleration = Mathf.Max(
maximumPlanarSpeed,
maximumVerticalSpeed) / responseTime;
Vector3 velocityChange = Vector3.ClampMagnitude(
desiredVelocity - physicsBody.linearVelocity,
maximumAcceleration * Time.fixedDeltaTime);
physicsBody.AddForce(velocityChange, ForceMode.VelocityChange);
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
return;
if (angle > 180f)
angle -= 360f;
Vector3 desiredAngularVelocity = axis.normalized *
(angle * Mathf.Deg2Rad * rotationFollowSpeed);
desiredAngularVelocity = Vector3.ClampMagnitude(
desiredAngularVelocity,
rotationFollowSpeed);
Vector3 angularVelocityChange = Vector3.ClampMagnitude(
desiredAngularVelocity - physicsBody.angularVelocity,
rotationFollowSpeed * 4f * Time.fixedDeltaTime);
physicsBody.AddTorque(angularVelocityChange, ForceMode.VelocityChange);
}
private bool TryGetGroundSurface(
@@ -538,8 +607,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
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);
return desiredRotation;
}
private void CacheGroundContactPoint()
@@ -596,19 +664,50 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
return false;
}
private void IgnoreCollisionsWithPlayers()
private void RefreshDraggerCollisions()
{
foreach (CharacterController playerCollider in FindObjectsByType<CharacterController>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None))
{
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null && playerCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, true);
}
desiredIgnoredColliders.Clear();
AddDraggerColliders(firstDraggerClientId.Value);
AddDraggerColliders(secondDraggerClientId.Value);
ignoredPlayerColliders.Add(playerCollider);
collidersToRestore.Clear();
foreach (Collider ignoredCollider in ignoredPlayerColliders)
{
if (ignoredCollider == null || !desiredIgnoredColliders.Contains(ignoredCollider))
collidersToRestore.Add(ignoredCollider);
}
foreach (Collider colliderToRestore in collidersToRestore)
{
SetCollisionIgnored(colliderToRestore, false);
ignoredPlayerColliders.Remove(colliderToRestore);
}
foreach (Collider desiredCollider in desiredIgnoredColliders)
{
if (desiredCollider != null && ignoredPlayerColliders.Add(desiredCollider))
SetCollisionIgnored(desiredCollider, true);
}
}
private void AddDraggerColliders(ulong clientId)
{
if (clientId == NoDragger || !TryGetPlayerObject(clientId, out NetworkObject playerObject))
return;
foreach (Collider playerCollider in playerObject.GetComponentsInChildren<Collider>(true))
desiredIgnoredColliders.Add(playerCollider);
}
private void SetCollisionIgnored(Collider playerCollider, bool ignored)
{
if (playerCollider == null)
return;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, ignored);
}
}
@@ -619,13 +718,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorksta
if (playerCollider == null)
continue;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, false);
}
SetCollisionIgnored(playerCollider, false);
}
ignoredPlayerColliders.Clear();
desiredIgnoredColliders.Clear();
collidersToRestore.Clear();
}
}
@@ -18,6 +18,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
[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.5f)] private float maximumHoldDistance = 4.5f;
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
[Header("Throw")]
@@ -35,10 +36,11 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
NetworkVariableWritePermission.Server);
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private readonly HashSet<Collider> desiredIgnoredColliders = new();
private readonly List<Collider> collidersToRestore = new();
private float nextCollisionRefreshTime;
private Vector3 followVelocity;
private Vector3 previousHolderPosition;
private bool hasPreviousHolderPosition;
private bool initialUseGravity;
private CollisionDetectionMode initialCollisionDetectionMode;
protected abstract Vector3 HoldingOffset { get; }
protected abstract string DropPrompt { get; }
@@ -68,6 +70,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
if (objectColliders == null || objectColliders.Length == 0)
objectColliders = GetComponentsInChildren<Collider>(true);
if (physicsBody != null)
{
initialUseGravity = physicsBody.useGravity;
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
}
}
public override void OnNetworkSpawn()
@@ -75,7 +83,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
SpawnedHeldObjects.Add(this);
holderClientId.OnValueChanged += HandleHolderChanged;
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
ApplyHoldingState(IsHeld || IsExternallyControlled);
ApplyHoldingState();
}
public override void OnNetworkDespawn()
@@ -83,12 +91,24 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
holderClientId.OnValueChanged -= HandleHolderChanged;
isExternallyControlled.OnValueChanged -= HandleExternalControlChanged;
SpawnedHeldObjects.Remove(this);
ApplyHoldingState(false);
RestorePlayerCollisions();
}
private void LateUpdate()
private void Update()
{
if (!IsSpawned || !IsHeld)
if (!IsSpawned || !IsHeld || IsExternallyControlled ||
Time.time < nextCollisionRefreshTime)
{
return;
}
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
RefreshHolderCollisions();
}
private void FixedUpdate()
{
if (!IsServer || !IsSpawned || !IsHeld || IsExternallyControlled || physicsBody == null)
return;
if (!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
@@ -99,15 +119,10 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
return;
}
FollowHolderTranslation(holder.transform.position);
Transform anchor = ResolveHoldingAnchor(holder);
FollowHoldingAnchor(anchor);
if (!DriveTowardAnchor(anchor))
return;
if (Time.time >= nextCollisionRefreshTime)
{
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
IgnoreCollisionsWithPlayers();
}
}
public void RequestThrow()
@@ -126,7 +141,7 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
if (IsHeld)
holderClientId.Value = NoHolder;
ApplyHoldingState(true);
ApplyHoldingState();
return true;
}
@@ -136,7 +151,23 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
return false;
isExternallyControlled.Value = false;
ApplyHoldingState(IsHeld);
ApplyHoldingState();
return true;
}
public bool TryReleaseExternalControlToPlayer(ulong clientId)
{
if (!IsServer || !IsSpawned || !IsExternallyControlled ||
TryGetHeldObject(clientId, out _) ||
NetworkHandItemInteractable.TryGetHeldItem(clientId, out _) ||
!TryGetPlayerObject(clientId, out _))
{
return false;
}
isExternallyControlled.Value = false;
holderClientId.Value = clientId;
ApplyHoldingState();
return true;
}
@@ -208,13 +239,13 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
return;
holderClientId.Value = clientId;
ApplyHoldingState(true);
ApplyHoldingState();
}
private void StopHolding()
{
holderClientId.Value = NoHolder;
ApplyHoldingState(IsExternallyControlled);
ApplyHoldingState();
}
private Transform ResolveHoldingAnchor(NetworkObject holder)
@@ -224,94 +255,134 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
return cameraController != null ? cameraController.AimTransform : holder.transform;
}
private void FollowHoldingAnchor(Transform anchor)
private bool DriveTowardAnchor(Transform anchor)
{
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
Vector3 targetDelta = targetPosition - transform.position;
Vector3 targetDelta = targetPosition - physicsBody.position;
if (targetDelta.sqrMagnitude > maximumFollowDistance * maximumFollowDistance)
if (targetDelta.sqrMagnitude > maximumHoldDistance * maximumHoldDistance)
{
transform.position = targetPosition -
targetDelta.normalized * maximumFollowDistance;
followVelocity = Vector3.zero;
StopHolding();
return false;
}
transform.position = Vector3.SmoothDamp(
transform.position,
targetPosition,
ref followVelocity,
positionSmoothTime,
maximumFollowSpeed,
Time.deltaTime);
float distance = targetDelta.magnitude;
float speedDistance = Mathf.Max(maximumFollowDistance, 0.01f);
float desiredSpeed = maximumFollowSpeed * Mathf.Clamp01(distance / speedDistance);
Vector3 desiredVelocity = distance > 0.0001f
? targetDelta / distance * desiredSpeed
: Vector3.zero;
float maximumAcceleration = maximumFollowSpeed / Mathf.Max(positionSmoothTime, 0.01f);
Vector3 velocityChange = Vector3.ClampMagnitude(
desiredVelocity - physicsBody.linearVelocity,
maximumAcceleration * Time.fixedDeltaTime);
physicsBody.AddForce(velocityChange, ForceMode.VelocityChange);
float rotationBlend = 1f - Mathf.Exp(-rotationFollowSpeed * Time.deltaTime);
transform.rotation = Quaternion.Slerp(
transform.rotation,
anchor.rotation,
rotationBlend);
DriveRotation(anchor.rotation);
return true;
}
private void FollowHolderTranslation(Vector3 holderPosition)
private void DriveRotation(Quaternion targetRotation)
{
if (!hasPreviousHolderPosition)
{
previousHolderPosition = holderPosition;
hasPreviousHolderPosition = true;
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
return;
}
Vector3 holderDelta = holderPosition - previousHolderPosition;
previousHolderPosition = holderPosition;
if (angle > 180f)
angle -= 360f;
transform.position += holderDelta;
Vector3 desiredAngularVelocity = axis.normalized *
(angle * Mathf.Deg2Rad * rotationFollowSpeed);
desiredAngularVelocity = Vector3.ClampMagnitude(
desiredAngularVelocity,
rotationFollowSpeed);
Vector3 angularVelocityChange = Vector3.ClampMagnitude(
desiredAngularVelocity - physicsBody.angularVelocity,
rotationFollowSpeed * 4f * Time.fixedDeltaTime);
physicsBody.AddTorque(angularVelocityChange, ForceMode.VelocityChange);
}
private void HandleHolderChanged(ulong _, ulong currentHolder) =>
ApplyHoldingState(currentHolder != NoHolder || IsExternallyControlled);
private void HandleHolderChanged(ulong _, ulong __) => ApplyHoldingState();
private void HandleExternalControlChanged(bool _, bool isControlled) =>
ApplyHoldingState(IsHeld || isControlled);
private void HandleExternalControlChanged(bool _, bool __) => ApplyHoldingState();
private void ApplyHoldingState(bool isHeld)
private void ApplyHoldingState()
{
followVelocity = Vector3.zero;
hasPreviousHolderPosition = false;
if (physicsBody != null)
{
bool shouldBeKinematic = isHeld || !IsServer;
bool isPhysicallyHeld = IsHeld && !IsExternallyControlled;
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
if (physicsBody.isKinematic && !shouldBeKinematic)
if (shouldBeKinematic)
{
physicsBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
physicsBody.isKinematic = true;
physicsBody.useGravity = false;
}
else
{
physicsBody.isKinematic = false;
physicsBody.useGravity = isPhysicallyHeld ? false : initialUseGravity;
physicsBody.collisionDetectionMode = ResolveDynamicCollisionDetectionMode();
}
if (!physicsBody.isKinematic)
if (!isPhysicallyHeld)
{
physicsBody.linearVelocity = Vector3.zero;
physicsBody.angularVelocity = Vector3.zero;
}
physicsBody.isKinematic = shouldBeKinematic;
}
if (isHeld)
IgnoreCollisionsWithPlayers();
if (IsHeld && !IsExternallyControlled)
RefreshHolderCollisions();
else
RestorePlayerCollisions();
}
private void IgnoreCollisionsWithPlayers()
{
foreach (CharacterController playerCollider in FindObjectsByType<CharacterController>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None))
{
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null && playerCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, true);
}
private CollisionDetectionMode ResolveDynamicCollisionDetectionMode() =>
initialCollisionDetectionMode == CollisionDetectionMode.Discrete
? CollisionDetectionMode.ContinuousDynamic
: initialCollisionDetectionMode;
ignoredPlayerColliders.Add(playerCollider);
private void RefreshHolderCollisions()
{
desiredIgnoredColliders.Clear();
if (TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
{
foreach (Collider holderCollider in holder.GetComponentsInChildren<Collider>(true))
desiredIgnoredColliders.Add(holderCollider);
}
collidersToRestore.Clear();
foreach (Collider ignoredCollider in ignoredPlayerColliders)
{
if (ignoredCollider == null || !desiredIgnoredColliders.Contains(ignoredCollider))
collidersToRestore.Add(ignoredCollider);
}
foreach (Collider colliderToRestore in collidersToRestore)
{
SetCollisionIgnored(colliderToRestore, false);
ignoredPlayerColliders.Remove(colliderToRestore);
}
foreach (Collider desiredCollider in desiredIgnoredColliders)
{
if (desiredCollider != null && ignoredPlayerColliders.Add(desiredCollider))
SetCollisionIgnored(desiredCollider, true);
}
}
private void SetCollisionIgnored(Collider playerCollider, bool ignored)
{
if (playerCollider == null)
return;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, ignored);
}
}
@@ -322,13 +393,11 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
if (playerCollider == null)
continue;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, false);
}
SetCollisionIgnored(playerCollider, false);
}
ignoredPlayerColliders.Clear();
desiredIgnoredColliders.Clear();
collidersToRestore.Clear();
}
}
@@ -1,5 +1,31 @@
using UnityEngine;
public interface IWorkstationItemControl
{
bool TryAcquireExternalControl();
bool TryReleaseExternalControl();
bool TryReleaseExternalControlToPlayer(ulong clientId);
}
public static class WorkstationItemControlUtility
{
public static bool TryGetControl(
NetworkItem item,
out IWorkstationItemControl itemControl)
{
if (item != null)
{
foreach (MonoBehaviour component in item.GetComponents<MonoBehaviour>())
{
if (component is IWorkstationItemControl candidate)
{
itemControl = candidate;
return true;
}
}
}
itemControl = null;
return false;
}
}
@@ -16,11 +16,13 @@ public sealed class ItemDefinition : ScriptableObject
[SerializeField] private string itemId;
[SerializeField] private string displayName;
[SerializeField] private ItemCategory category;
[SerializeField] private bool isAssemblyReady;
[SerializeField] private NetworkObject worldPrefab;
public string ItemId => itemId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public ItemCategory Category => category;
public bool IsAssemblyReady => isAssemblyReady;
public NetworkObject WorldPrefab => worldPrefab;
private void OnValidate()
@@ -10,6 +10,7 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
private Camera playerCamera;
private IInteractable focusedInteractable;
private IPlayerInteractionSession activeSession;
private readonly RaycastHit[] interactionHits = new RaycastHit[32];
private bool activeSessionReleaseRequested;
private bool isInitialized;
@@ -54,10 +55,57 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
private IInteractable FindFocusedInteractable()
{
Ray ray = new(playerCamera.transform.position, playerCamera.transform.forward);
if (!Physics.Raycast(ray, out RaycastHit hit, interactionDistance, interactionMask, QueryTriggerInteraction.Collide))
int hitCount = Physics.RaycastNonAlloc(
ray,
interactionHits,
interactionDistance,
interactionMask,
QueryTriggerInteraction.Collide);
if (hitCount <= 0)
return null;
foreach (MonoBehaviour component in hit.collider.GetComponentsInParent<MonoBehaviour>())
IInteractable closestAvailable = null;
float closestAvailableDistance = float.PositiveInfinity;
IInteractable closestUnavailable = null;
float closestUnavailableDistance = float.PositiveInfinity;
float closestBlockingDistance = float.PositiveInfinity;
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
{
RaycastHit hit = interactionHits[hitIndex];
IInteractable interactable = FindInteractable(hit.collider);
if (interactable == null)
{
closestBlockingDistance = Mathf.Min(closestBlockingDistance, hit.distance);
continue;
}
if (interactable.IsInteractionAvailable)
{
if (hit.distance < closestAvailableDistance)
{
closestAvailable = interactable;
closestAvailableDistance = hit.distance;
}
}
else if (hit.distance < closestUnavailableDistance)
{
closestUnavailable = interactable;
closestUnavailableDistance = hit.distance;
}
}
if (closestAvailable != null && closestAvailableDistance <= closestBlockingDistance)
return closestAvailable;
return closestUnavailable != null && closestUnavailableDistance <= closestBlockingDistance
? closestUnavailable
: null;
}
private static IInteractable FindInteractable(Collider collider)
{
foreach (MonoBehaviour component in collider.GetComponentsInParent<MonoBehaviour>())
{
if (component is IInteractable interactable)
return interactable;
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e48d638d2d22411db1b67c8276c6f6c9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,609 @@
using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public enum RocketAssemblyWorkstationState : byte
{
Loading,
Assembling,
OutputReady
}
public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationController
{
public const int MaximumSlotCount = 9;
private const ulong EmptySlot = ulong.MaxValue;
[Header("Recipes")]
[SerializeField] private List<RocketAssemblyRecipe> recipes = new();
[Header("Slots (bottom to top)")]
[Tooltip("Element 0 is the lowest physical slot. Only the first 9 entries are used.")]
[SerializeField] private List<RocketAssemblyInputSlot> slotsBottomToTop = new();
[Header("Output")]
[SerializeField] private Transform outputPoint;
[SerializeField, Min(0.1f)] private float outputReleaseDistance = 1.5f;
[Header("Interaction")]
[SerializeField, Min(0.1f)] private float maximumTakeDistance = 3.5f;
[SerializeField, Min(0.05f)] private float reinsertCooldownAfterTake = 0.5f;
private readonly NetworkVariable<RocketAssemblyWorkstationState> state = new(
RocketAssemblyWorkstationState.Loading,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> selectedRecipeIndex = new(
-1,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> assemblyStartedAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> assemblyEndsAt = new(
0d,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<bool> assemblyWillBeDefective = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkList<ulong> slotItemNetworkObjectIds = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkItem[] slottedItems = new NetworkItem[MaximumSlotCount];
private readonly IWorkstationItemControl[] slottedItemControls =
new IWorkstationItemControl[MaximumSlotCount];
private readonly Dictionary<ulong, double> reinsertBlockedUntil = new();
private NetworkItem currentOutput;
public override bool IsOperating => State == RocketAssemblyWorkstationState.Assembling;
public RocketAssemblyWorkstationState State => state.Value;
public IReadOnlyList<RocketAssemblyRecipe> Recipes => recipes;
public IReadOnlyList<NetworkItem> SlottedItems => slottedItems;
public int SlotCount => Mathf.Min(slotsBottomToTop.Count, MaximumSlotCount);
public int SelectedRecipeIndex => selectedRecipeIndex.Value;
public RocketAssemblyRecipe SelectedRecipe => TryGetRecipe(
selectedRecipeIndex.Value,
out RocketAssemblyRecipe recipe)
? recipe
: null;
public bool AssemblyWillBeDefective => assemblyWillBeDefective.Value;
public bool LastCompletedAssemblyWasDefective =>
State == RocketAssemblyWorkstationState.OutputReady &&
assemblyWillBeDefective.Value;
public NetworkItem CurrentOutput => currentOutput;
public int OccupiedSlotCount
{
get
{
int count = 0;
int synchronizedSlotCount = Mathf.Min(slotItemNetworkObjectIds.Count, SlotCount);
for (int index = 0; index < synchronizedSlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot)
count++;
}
return count;
}
}
public bool CanStartAssembly =>
State == RocketAssemblyWorkstationState.Loading &&
SelectedRecipe != null &&
OccupiedSlotCount >= SelectedRecipe.RequiredPartCount;
public float AssemblyProgress
{
get
{
if (State == RocketAssemblyWorkstationState.OutputReady)
return 1f;
if (!IsOperating)
return 0f;
double duration = assemblyEndsAt.Value - assemblyStartedAt.Value;
return duration <= 0d
? 1f
: Mathf.Clamp01((float)((GetNetworkTime() - assemblyStartedAt.Value) / duration));
}
}
public float RemainingSeconds => !IsOperating
? 0f
: Mathf.Max(0f, (float)(assemblyEndsAt.Value - GetNetworkTime()));
public event Action Changed;
private void Awake() => ConfigureSlots();
private void OnValidate()
{
if (slotsBottomToTop.Count > MaximumSlotCount)
slotsBottomToTop.RemoveRange(
MaximumSlotCount,
slotsBottomToTop.Count - MaximumSlotCount);
ConfigureSlots();
}
public override void OnNetworkSpawn()
{
state.OnValueChanged += HandleStateChanged;
selectedRecipeIndex.OnValueChanged += HandleSelectedRecipeChanged;
assemblyStartedAt.OnValueChanged += HandleAssemblyTimeChanged;
assemblyEndsAt.OnValueChanged += HandleAssemblyTimeChanged;
assemblyWillBeDefective.OnValueChanged += HandleDefectiveStateChanged;
slotItemNetworkObjectIds.OnListChanged += HandleSlotListChanged;
if (IsServer)
InitializeOnServer();
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
state.OnValueChanged -= HandleStateChanged;
selectedRecipeIndex.OnValueChanged -= HandleSelectedRecipeChanged;
assemblyStartedAt.OnValueChanged -= HandleAssemblyTimeChanged;
assemblyEndsAt.OnValueChanged -= HandleAssemblyTimeChanged;
assemblyWillBeDefective.OnValueChanged -= HandleDefectiveStateChanged;
slotItemNetworkObjectIds.OnListChanged -= HandleSlotListChanged;
}
private void Update()
{
if (!IsServer || !IsSpawned)
return;
RemoveMissingSlottedItems();
if (State == RocketAssemblyWorkstationState.Assembling &&
GetNetworkTime() >= assemblyEndsAt.Value)
{
CompleteAssemblyOnServer();
}
else if (State == RocketAssemblyWorkstationState.OutputReady &&
HasOutputLeftWorkstation())
{
ResetToLoadingOnServer();
}
}
private void LateUpdate()
{
if (!IsServer || !IsSpawned ||
State == RocketAssemblyWorkstationState.OutputReady)
{
return;
}
for (int index = 0; index < SlotCount; index++)
{
if (slottedItems[index] != null)
SnapToSlot(index, slottedItems[index].transform);
}
}
public bool TryInsertItem(int slotIndex, NetworkItem item)
{
if (!IsServer || !IsSpawned ||
State != RocketAssemblyWorkstationState.Loading ||
!IsValidSlotIndex(slotIndex) ||
slottedItems[slotIndex] != null ||
!IsReadyRocketPart(item) ||
IsTemporarilyBlockedFromReinsert(item) ||
IsAlreadySlotted(item) ||
!WorkstationItemControlUtility.TryGetControl(
item,
out IWorkstationItemControl itemControl) ||
!itemControl.TryAcquireExternalControl())
{
return false;
}
slottedItems[slotIndex] = item;
slottedItemControls[slotIndex] = itemControl;
slotItemNetworkObjectIds[slotIndex] = item.NetworkObjectId;
SnapToSlot(slotIndex, item.transform);
Changed?.Invoke();
return true;
}
public bool CanTakeSlotItem(int slotIndex) =>
IsSpawned &&
State == RocketAssemblyWorkstationState.Loading &&
IsValidSlotIndex(slotIndex) &&
slotIndex < slotItemNetworkObjectIds.Count &&
slotItemNetworkObjectIds[slotIndex] != EmptySlot;
public string GetTakePrompt(int slotIndex)
{
if (!CanTakeSlotItem(slotIndex))
return "Take part";
NetworkItem item = IsServer ? slottedItems[slotIndex] : ResolveSynchronizedItem(slotIndex);
return item != null ? $"Take {item.DisplayName}" : "Take part";
}
public void RequestTakeSlotItem(int slotIndex)
{
if (!CanTakeSlotItem(slotIndex))
return;
RequestTakeSlotItemServerRpc(slotIndex);
}
public void RequestSelectRecipe(int recipeIndex)
{
if (!IsSpawned)
return;
if (IsServer)
SelectRecipeOnServer(recipeIndex);
else
RequestSelectRecipeServerRpc(recipeIndex);
}
public void RequestStartAssembly()
{
if (!IsSpawned)
return;
if (IsServer)
TryStartAssemblyOnServer();
else
RequestStartAssemblyServerRpc();
}
public void SetAssemblyRequested(bool isActivated)
{
if (isActivated && IsServer && IsSpawned)
TryStartAssemblyOnServer();
}
[ContextMenu("Debug/Start Assembly (Server Play Mode)")]
private void StartAssemblyFromContextMenu()
{
if (Application.isPlaying && IsServer && IsSpawned)
TryStartAssemblyOnServer();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestTakeSlotItemServerRpc(
int slotIndex,
RpcParams rpcParams = default)
{
if (!CanTakeSlotItem(slotIndex) ||
!TryGetPlayerObject(rpcParams.Receive.SenderClientId, out NetworkObject playerObject))
{
return;
}
float maximumDistanceSquared = maximumTakeDistance * maximumTakeDistance;
if ((playerObject.transform.position -
slotsBottomToTop[slotIndex].InteractionPosition).sqrMagnitude > maximumDistanceSquared)
{
return;
}
IWorkstationItemControl itemControl = slottedItemControls[slotIndex];
if (itemControl == null ||
!itemControl.TryReleaseExternalControlToPlayer(rpcParams.Receive.SenderClientId))
{
return;
}
NetworkItem item = slottedItems[slotIndex];
if (item != null)
{
reinsertBlockedUntil[item.NetworkObjectId] =
GetNetworkTime() + reinsertCooldownAfterTake;
}
ClearSlot(slotIndex);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestSelectRecipeServerRpc(int recipeIndex) =>
SelectRecipeOnServer(recipeIndex);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStartAssemblyServerRpc() => TryStartAssemblyOnServer();
private void InitializeOnServer()
{
slotItemNetworkObjectIds.Clear();
for (int index = 0; index < SlotCount; index++)
slotItemNetworkObjectIds.Add(EmptySlot);
Array.Clear(slottedItems, 0, slottedItems.Length);
Array.Clear(slottedItemControls, 0, slottedItemControls.Length);
reinsertBlockedUntil.Clear();
currentOutput = null;
selectedRecipeIndex.Value = FindFirstValidRecipeIndex();
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
}
private void SelectRecipeOnServer(int recipeIndex)
{
if (!IsServer || !IsSpawned ||
State != RocketAssemblyWorkstationState.Loading ||
!TryGetRecipe(recipeIndex, out _))
{
return;
}
selectedRecipeIndex.Value = recipeIndex;
Changed?.Invoke();
}
private bool TryStartAssemblyOnServer()
{
RocketAssemblyRecipe recipe = SelectedRecipe;
if (!IsServer || !IsSpawned || !CanStartAssembly || recipe == null)
return false;
assemblyWillBeDefective.Value = !recipe.Matches(slottedItems);
double now = GetNetworkTime();
assemblyStartedAt.Value = now;
assemblyEndsAt.Value = now + recipe.AssemblyDuration;
state.Value = RocketAssemblyWorkstationState.Assembling;
Changed?.Invoke();
return true;
}
private void CompleteAssemblyOnServer()
{
RocketAssemblyRecipe recipe = SelectedRecipe;
if (recipe == null)
{
CancelAssemblyOnServer();
return;
}
bool defective = assemblyWillBeDefective.Value;
ItemDefinition outputDefinition = defective
? recipe.DefectiveOutputItem
: recipe.OutputItem;
if (outputDefinition == null || outputDefinition.WorldPrefab == null)
{
CancelAssemblyOnServer();
return;
}
for (int index = 0; index < SlotCount; index++)
{
NetworkItem item = slottedItems[index];
ClearSlotRuntime(index);
if (item != null && item.NetworkObject != null && item.IsSpawned)
item.NetworkObject.Despawn(true);
slotItemNetworkObjectIds[index] = EmptySlot;
}
Transform spawnPoint = outputPoint != null ? outputPoint : transform;
NetworkObject outputObject = Instantiate(
outputDefinition.WorldPrefab,
spawnPoint.position,
spawnPoint.rotation);
outputObject.Spawn();
currentOutput = outputObject.GetComponent<NetworkItem>();
if (currentOutput == null ||
!WorkstationItemControlUtility.TryGetControl(currentOutput, out _))
{
Debug.LogError(
$"{name} cannot produce '{outputDefinition.DisplayName}': the output prefab " +
"must contain NetworkItem and a supported Carry or Drag interaction component.",
this);
outputObject.Despawn(true);
currentOutput = null;
ResetToLoadingOnServer();
return;
}
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
state.Value = RocketAssemblyWorkstationState.OutputReady;
Changed?.Invoke();
}
private void CancelAssemblyOnServer()
{
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
Changed?.Invoke();
}
private void ResetToLoadingOnServer()
{
currentOutput = null;
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
assemblyWillBeDefective.Value = false;
state.Value = RocketAssemblyWorkstationState.Loading;
Changed?.Invoke();
}
private void RemoveMissingSlottedItems()
{
if (State == RocketAssemblyWorkstationState.Assembling)
{
for (int index = 0; index < SlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot &&
(slottedItems[index] == null || !slottedItems[index].IsSpawned))
{
CancelAssemblyOnServer();
return;
}
}
}
for (int index = 0; index < SlotCount; index++)
{
if (slotItemNetworkObjectIds[index] != EmptySlot &&
(slottedItems[index] == null || !slottedItems[index].IsSpawned))
{
ClearSlot(index);
}
}
}
private bool HasOutputLeftWorkstation()
{
if (currentOutput == null || !currentOutput.IsSpawned)
return true;
Transform spawnPoint = outputPoint != null ? outputPoint : transform;
return (currentOutput.transform.position - spawnPoint.position).sqrMagnitude >
outputReleaseDistance * outputReleaseDistance;
}
private void ClearSlot(int slotIndex)
{
ClearSlotRuntime(slotIndex);
slotItemNetworkObjectIds[slotIndex] = EmptySlot;
Changed?.Invoke();
}
private void ClearSlotRuntime(int slotIndex)
{
slottedItems[slotIndex] = null;
slottedItemControls[slotIndex] = null;
}
private void SnapToSlot(int slotIndex, Transform itemTransform)
{
Transform snapPoint = slotsBottomToTop[slotIndex].SnapPoint;
itemTransform.SetPositionAndRotation(snapPoint.position, snapPoint.rotation);
}
private bool IsAlreadySlotted(NetworkItem item)
{
for (int index = 0; index < SlotCount; index++)
{
if (slottedItems[index] == item)
return true;
}
return false;
}
private bool IsTemporarilyBlockedFromReinsert(NetworkItem item)
{
if (item == null ||
!reinsertBlockedUntil.TryGetValue(item.NetworkObjectId, out double blockedUntil))
{
return false;
}
if (GetNetworkTime() < blockedUntil)
return true;
reinsertBlockedUntil.Remove(item.NetworkObjectId);
return false;
}
private NetworkItem ResolveSynchronizedItem(int slotIndex)
{
if (!IsValidSlotIndex(slotIndex) ||
slotIndex >= slotItemNetworkObjectIds.Count ||
slotItemNetworkObjectIds[slotIndex] == EmptySlot ||
NetworkManager == null)
{
return null;
}
return NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(
slotItemNetworkObjectIds[slotIndex],
out NetworkObject networkObject)
? networkObject.GetComponent<NetworkItem>()
: null;
}
private bool TryGetPlayerObject(ulong clientId, out NetworkObject playerObject)
{
playerObject = NetworkManager != null
? NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId)
: null;
return playerObject != null;
}
private void ConfigureSlots()
{
int count = Mathf.Min(slotsBottomToTop.Count, MaximumSlotCount);
for (int index = 0; index < count; index++)
{
if (slotsBottomToTop[index] != null)
slotsBottomToTop[index].Configure(this, index);
}
}
private int FindFirstValidRecipeIndex()
{
for (int index = 0; index < recipes.Count; index++)
{
if (IsRecipeValid(recipes[index]))
return index;
}
return -1;
}
private bool TryGetRecipe(int index, out RocketAssemblyRecipe recipe)
{
if (index >= 0 && index < recipes.Count && IsRecipeValid(recipes[index]))
{
recipe = recipes[index];
return true;
}
recipe = null;
return false;
}
private bool IsValidSlotIndex(int slotIndex) =>
slotIndex >= 0 && slotIndex < SlotCount && slotsBottomToTop[slotIndex] != null;
private static bool IsReadyRocketPart(NetworkItem item) =>
item != null && item.IsSpawned && item.Definition != null &&
item.Category == ItemCategory.RocketPart && item.Definition.IsAssemblyReady;
private static bool IsRecipeValid(RocketAssemblyRecipe recipe) =>
recipe != null && recipe.IsConfigured;
private double GetNetworkTime() => NetworkManager != null
? NetworkManager.ServerTime.Time
: Time.timeAsDouble;
private void HandleStateChanged(
RocketAssemblyWorkstationState _,
RocketAssemblyWorkstationState __) => Changed?.Invoke();
private void HandleSelectedRecipeChanged(int _, int __) => Changed?.Invoke();
private void HandleAssemblyTimeChanged(double _, double __) => Changed?.Invoke();
private void HandleDefectiveStateChanged(bool _, bool __) => Changed?.Invoke();
private void HandleSlotListChanged(NetworkListEvent<ulong> _) => Changed?.Invoke();
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43801e0a52c2499bbcf2c7e39871ce80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Collider))]
public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
{
[SerializeField] private Transform snapPoint;
private NetworkRocketAssemblyWorkstation workstation;
private int slotIndex = -1;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
public Vector3 InteractionPosition => SnapPoint.position;
public string InteractionPrompt => workstation != null
? workstation.GetTakePrompt(slotIndex)
: "Take part";
public bool IsInteractionAvailable =>
workstation != null && workstation.CanTakeSlotItem(slotIndex);
private void Reset()
{
snapPoint = transform;
GetComponent<Collider>().isTrigger = true;
}
private void OnValidate() => GetComponent<Collider>().isTrigger = true;
public void Configure(NetworkRocketAssemblyWorkstation owner, int index)
{
workstation = owner;
slotIndex = index;
}
public void RequestInteraction()
{
if (IsInteractionAvailable)
workstation.RequestTakeSlotItem(slotIndex);
}
private void OnTriggerEnter(Collider other) => TryInsert(other);
private void OnTriggerStay(Collider other) => TryInsert(other);
private void TryInsert(Collider other)
{
if (workstation == null || !workstation.IsServer || !workstation.IsSpawned)
return;
NetworkItem item = other.GetComponentInParent<NetworkItem>();
if (item != null)
workstation.TryInsertItem(slotIndex, item);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54737cf1ab274fdb851b89457fb3412f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(
fileName = "RocketAssemblyRecipe",
menuName = "FORT-BO/Workstations/Rocket Assembly Recipe")]
public sealed class RocketAssemblyRecipe : ScriptableObject
{
public const int MaximumPartCount = 9;
[Header("Recipe")]
[SerializeField] private string recipeId;
[SerializeField] private string displayName;
[Tooltip("Exact assembly order. Element 0 is the lowest rocket part.")]
[SerializeField] private List<ItemDefinition> requiredPartsBottomToTop = new();
[SerializeField] private ItemDefinition outputItem;
[SerializeField] private ItemDefinition defectiveOutputItem;
[SerializeField, Min(0.05f)] private float assemblyDuration = 5f;
[Header("Blueprint")]
[SerializeField] private string blueprintTitle;
[SerializeField] private Sprite blueprintImage;
public string RecipeId => recipeId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public IReadOnlyList<ItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public int RequiredPartCount => requiredPartsBottomToTop.Count;
public ItemDefinition OutputItem => outputItem;
public ItemDefinition DefectiveOutputItem => defectiveOutputItem;
public float AssemblyDuration => assemblyDuration;
public string BlueprintTitle => string.IsNullOrWhiteSpace(blueprintTitle)
? DisplayName
: blueprintTitle;
public Sprite BlueprintImage => blueprintImage;
public bool IsConfigured =>
requiredPartsBottomToTop.Count is > 0 and <= MaximumPartCount &&
requiredPartsBottomToTop.TrueForAll(IsReadyRocketPart) &&
IsSpawnable(outputItem) &&
IsSpawnable(defectiveOutputItem);
public bool Matches(IReadOnlyList<NetworkItem> slottedItems)
{
if (!IsConfigured || slottedItems == null ||
slottedItems.Count < requiredPartsBottomToTop.Count)
{
return false;
}
for (int index = 0; index < requiredPartsBottomToTop.Count; index++)
{
NetworkItem item = slottedItems[index];
if (item == null || !MatchesDefinition(item, requiredPartsBottomToTop[index]))
return false;
}
for (int index = requiredPartsBottomToTop.Count; index < slottedItems.Count; index++)
{
if (slottedItems[index] != null)
return false;
}
return true;
}
private void OnValidate()
{
recipeId = recipeId?.Trim().ToLowerInvariant();
displayName = displayName?.Trim();
blueprintTitle = blueprintTitle?.Trim();
if (requiredPartsBottomToTop.Count > MaximumPartCount)
requiredPartsBottomToTop.RemoveRange(
MaximumPartCount,
requiredPartsBottomToTop.Count - MaximumPartCount);
}
private static bool MatchesDefinition(NetworkItem item, ItemDefinition definition) =>
item.Definition == definition ||
(!string.IsNullOrEmpty(definition.ItemId) && item.ItemId == definition.ItemId);
private static bool IsReadyRocketPart(ItemDefinition definition) =>
definition != null &&
definition.Category == ItemCategory.RocketPart &&
definition.IsAssemblyReady;
private static bool IsSpawnable(ItemDefinition definition) =>
definition != null && definition.WorldPrefab != null;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 04f3334793c14ee791f78059d0c2854c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,148 @@
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class RocketAssemblyWorkstationView : MonoBehaviour
{
[SerializeField] private NetworkRocketAssemblyWorkstation workstation;
[Header("State Events")]
[SerializeField] private UnityEvent onLoading;
[SerializeField] private UnityEvent onAssemblyStarted;
[SerializeField] private UnityEvent onValidOutputReady;
[SerializeField] private UnityEvent onDefectiveOutputReady;
[Header("Blueprint and Slot Events")]
[SerializeField] private UnityEvent<string> onRecipeNameChanged;
[SerializeField] private UnityEvent<string> onBlueprintTitleChanged;
[SerializeField] private UnityEvent<Sprite> onBlueprintImageChanged;
[SerializeField] private UnityEvent<int> onRequiredPartCountChanged;
[SerializeField] private UnityEvent<int> onOccupiedSlotCountChanged;
[SerializeField] private UnityEvent<bool> onCanStartChanged;
[Header("Continuous Progress")]
[SerializeField] private UnityEvent<float> onAssemblyProgress;
private RocketAssemblyWorkstationState previousState;
private bool previousDefectiveResult;
private bool hasAppliedState;
public NetworkRocketAssemblyWorkstation Workstation => workstation;
private void Reset() =>
workstation = GetComponentInParent<NetworkRocketAssemblyWorkstation>();
private void Awake()
{
if (workstation == null)
workstation = GetComponentInParent<NetworkRocketAssemblyWorkstation>();
}
private void OnEnable()
{
if (workstation != null)
{
workstation.Changed += Apply;
Apply();
}
}
private void Start() => Apply();
private void OnDisable()
{
if (workstation != null)
workstation.Changed -= Apply;
hasAppliedState = false;
}
private void Update()
{
if (workstation != null && workstation.IsOperating)
onAssemblyProgress?.Invoke(workstation.AssemblyProgress);
}
public void Apply()
{
if (workstation == null || !workstation.IsSpawned)
return;
ApplyRecipeAndSlots();
RocketAssemblyWorkstationState currentState = workstation.State;
bool defectiveResult = workstation.LastCompletedAssemblyWasDefective;
if (hasAppliedState && currentState == previousState &&
defectiveResult == previousDefectiveResult)
{
return;
}
previousState = currentState;
previousDefectiveResult = defectiveResult;
hasAppliedState = true;
switch (currentState)
{
case RocketAssemblyWorkstationState.Assembling:
onAssemblyStarted?.Invoke();
onAssemblyProgress?.Invoke(workstation.AssemblyProgress);
break;
case RocketAssemblyWorkstationState.OutputReady:
onAssemblyProgress?.Invoke(1f);
if (defectiveResult)
onDefectiveOutputReady?.Invoke();
else
onValidOutputReady?.Invoke();
break;
default:
onAssemblyProgress?.Invoke(0f);
onLoading?.Invoke();
break;
}
}
[ContextMenu("Preview/Loading")]
private void PreviewLoading()
{
onAssemblyProgress?.Invoke(0f);
onLoading?.Invoke();
}
[ContextMenu("Preview/Assembly Started")]
private void PreviewAssemblyStarted()
{
onAssemblyProgress?.Invoke(0f);
onAssemblyStarted?.Invoke();
}
[ContextMenu("Preview/Assembly 50 Percent")]
private void PreviewAssemblyHalfway() => onAssemblyProgress?.Invoke(0.5f);
[ContextMenu("Preview/Valid Output")]
private void PreviewValidOutput()
{
onAssemblyProgress?.Invoke(1f);
onValidOutputReady?.Invoke();
}
[ContextMenu("Preview/Defective Output")]
private void PreviewDefectiveOutput()
{
onAssemblyProgress?.Invoke(1f);
onDefectiveOutputReady?.Invoke();
}
private void ApplyRecipeAndSlots()
{
RocketAssemblyRecipe recipe = workstation.SelectedRecipe;
onRecipeNameChanged?.Invoke(recipe != null ? recipe.DisplayName : string.Empty);
onBlueprintTitleChanged?.Invoke(recipe != null ? recipe.BlueprintTitle : string.Empty);
onBlueprintImageChanged?.Invoke(recipe != null ? recipe.BlueprintImage : null);
onRequiredPartCountChanged?.Invoke(recipe != null ? recipe.RequiredPartCount : 0);
onOccupiedSlotCountChanged?.Invoke(workstation.OccupiedSlotCount);
onCanStartChanged?.Invoke(workstation.CanStartAssembly);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42a6ab0b475f46aebda93f3c671b670a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -139,7 +139,9 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
{
if (!CanAcceptItem || item == null || !item.IsSpawned ||
!TryFindRecipe(item, out int recipeIndex, out ItemProcessingRecipe recipe) ||
!TryGetItemControl(item, out IWorkstationItemControl itemControl))
!WorkstationItemControlUtility.TryGetControl(
item,
out IWorkstationItemControl itemControl))
{
return false;
}
@@ -200,7 +202,8 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
outputObject.Spawn();
currentItem = outputObject.GetComponent<NetworkItem>();
if (currentItem == null || !TryGetItemControl(currentItem, out _))
if (currentItem == null ||
!WorkstationItemControlUtility.TryGetControl(currentItem, out _))
{
Debug.LogError(
$"{name} cannot produce '{recipe.OutputItem.DisplayName}': " +
@@ -291,20 +294,4 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
recipe != null && recipe.InputItem != null &&
recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null;
private static bool TryGetItemControl(
NetworkItem item,
out IWorkstationItemControl itemControl)
{
foreach (MonoBehaviour component in item.GetComponents<MonoBehaviour>())
{
if (component is IWorkstationItemControl candidate)
{
itemControl = candidate;
return true;
}
}
itemControl = null;
return false;
}
}
+187
View File
@@ -257,8 +257,195 @@ MonoBehaviour:
gameplaySceneName: Factory
maxClientCount: 3
useSecureRelay: 0
--- !u!1 &1680150540
GameObject:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1680150542}
- component: {fileID: 1680150541}
m_Layer: 0
m_Name: '!ftraceLightmaps'
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1680150541
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1680150540}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b7fa80e7116296f4eb4f49ec1544ee22, type: 3}
m_Name:
m_EditorClassIdentifier:
renderSettingsBounces: 5
renderSettingsGISamples: 16
renderSettingsGIBackFaceWeight: 0
renderSettingsTileSize: 512
renderSettingsPriority: 2
renderSettingsTexelsPerUnit: 20
renderSettingsForceRefresh: 1
renderSettingsForceRebuildGeometry: 1
renderSettingsPerformRendering: 1
renderSettingsUserRenderMode: 0
renderSettingsDistanceShadowmask: 1
renderSettingsSettingsMode: 0
renderSettingsFixSeams: 1
renderSettingsDenoise: 1
renderSettingsDenoise2x: 0
renderSettingsEncode: 1
renderSettingsEncodeMode: 0
renderSettingsOverwriteWarning: 0
renderSettingsAutoAtlas: 1
renderSettingsUnwrapUVs: 1
renderSettingsForceDisableUnwrapUVs: 0
renderSettingsMaxAutoResolution: 4096
renderSettingsMinAutoResolution: 16
renderSettingsUnloadScenes: 1
renderSettingsAdjustSamples: 1
renderSettingsCheckOverlaps: 0
renderSettingsSkipOutOfBoundsUVs: 1
renderSettingsHackEmissiveBoost: 1
renderSettingsHackIndirectBoost: 1
renderSettingsTempPath:
renderSettingsOutPath: BakeryLightmaps
renderSettingsUseScenePath: 0
renderSettingsHackAOIntensity: 0
renderSettingsHackAOSamples: 16
renderSettingsHackAOSofter: 0
renderSettingsHackAORadius: 1
renderSettingsShowAOSettings: 0
renderSettingsShowTasks: 1
renderSettingsShowTasks2: 0
renderSettingsShowPaths: 1
renderSettingsShowNet: 1
renderSettingsShowSettingsAsset: 1
renderSettingsShowAPV: 0
renderSettingsUseAPVSkyOcclusion: 0
renderSettingsOcclusionProbes: 0
renderSettingsTexelsPerMap: 0
renderSettingsTexelsColor: 1
renderSettingsTexelsMask: 1
renderSettingsTexelsDir: 1
renderSettingsShowDirWarning: 1
renderSettingsRenderDirMode: 0
renderSettingsShowCheckerSettings: 0
renderSettingsSamplesWarning: 1
renderSettingsSuppressPopups: 0
renderSettingsSectorWarning: 0
renderSettingsPrefabWarning: 1
renderSettingsSplitByScene: 0
renderSettingsSplitByTag: 0
renderSettingsUVPaddingMax: 0
renderSettingsUVPaddingPreserveIfExisted: 0
renderSettingsPostPacking: 1
renderSettingsHoleFilling: 0
renderSettingsBruteForce: 1
renderSettingsAlphaDithering: 0
renderSettingsBeepOnFinish: 0
renderSettingsExportTerrainAsHeightmap: 1
renderSettingsRTXMode: 0
renderSettingsLightProbeMode: 1
renderSettingsClientMode: 0
renderSettingsServerAddress: 127.0.0.1
renderSettingsUnwrapper: 0
renderSettingsDenoiserType: 100
renderSettingsExportTerrainTrees: 0
renderSettingsShowPerf: 1
renderSettingsSampleDiv: 1
renderSettingsAtlasPacker: 0
renderSettingsBatchPoints: 1
renderSettingsCompressVolumes: 0
renderSettingsBatchAreaLightSampleLimit: 0
renderSettingsSector: {fileID: 0}
renderSettingsAsset: {fileID: 0}
renderSettingsRTPVExport: 1
renderSettingsRTPVSceneView: 0
renderSettingsRTPVHDR: 0
renderSettingsRTPVWidth: 640
renderSettingsRTPVHeight: 360
renderSettingsRTPVExposure: 1
renderSettingsRemoveDuplicateLightmaps: 0
lastBakeTime: 0
enlightenWarningShown: 0
enlightenWarningShown2: 0
uniqueLights: []
implicitGroups: []
implicitGroupedObjects: []
bounds: []
hasEmissive:
uvBuffOffsets:
uvBuffLengths:
uvSrcBuff: []
uvDestBuff: []
lmrIndicesOffsets:
lmrIndicesLengths:
lmrIndicesBuff:
lmGroupLODResFlags:
lmGroupMinLOD:
lmGroupLODMatrix:
serverGetFileList: []
lightmapHasColor:
lightmapHasMask:
lightmapHasDir:
lightmapHasRNM:
lights: []
modifiedAssetPathList: []
modifiedAssets: []
bakedRenderers: []
nonBakedRenderers: []
bakedLights: []
bakedRenderersTerrain: []
maps: []
masks: []
dirMaps: []
rnmMaps0: []
rnmMaps1: []
rnmMaps2: []
mapsMode:
bakedIDs:
bakedScaleOffset: []
bakedVertexOffset:
bakedVertexColorMesh: []
bakedLightChannels:
bakedIDsTerrain:
bakedScaleOffsetTerrain: []
assetList: []
uvOverlapAssetList:
idremap:
usesRealtimeGI: 0
emptyDirectionTex: {fileID: 0}
anyVolumes: 0
compressedVolumes: 0
sectors: []
prevBakedProbes: []
prevBakedProbePos: []
--- !u!4 &1680150542
Transform:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1680150540}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 535045610}
- {fileID: 1680150542}
File diff suppressed because one or more lines are too long