Added many other things and processings

This commit is contained in:
2026-08-13 17:55:05 +03:00
parent 36ecac038a
commit 3c490bdae8
625 changed files with 929224 additions and 151067 deletions
@@ -12,9 +12,10 @@ public enum DraggableMovementMode
}
[RequireComponent(typeof(Rigidbody), typeof(NetworkRigidbody))]
public sealed class NetworkDraggableInteractable : NetworkInteractable
public sealed class NetworkDraggableInteractable : NetworkInteractable, IWorkstationItemControl
{
private const ulong NoDragger = ulong.MaxValue;
private static readonly HashSet<NetworkDraggableInteractable> SpawnedDraggables = new();
[Header("Weight")]
[SerializeField, Min(0.1f)] private float weight = 35f;
@@ -59,6 +60,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<bool> isExternallyControlled = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private readonly RaycastHit[] groundHits = new RaycastHit[16];
private float nextCollisionRefreshTime;
@@ -79,6 +85,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
}
public int RequiredDraggerCount => Mathf.Max(1, Mathf.CeilToInt(weight / pullStrengthPerPlayer));
public bool HasEnoughPullStrength => ActiveDraggerCount >= RequiredDraggerCount;
public bool IsExternallyControlled => isExternallyControlled.Value;
private bool HasAnyDragger => ActiveDraggerCount > 0;
private bool IsDraggedByLocalPlayer =>
@@ -88,6 +95,9 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
{
get
{
if (IsExternallyControlled)
return "In machine";
if (IsDraggedByLocalPlayer)
return "Release";
@@ -105,7 +115,11 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
}
public override bool IsInteractionAvailable => base.IsInteractionAvailable &&
(IsDraggedByLocalPlayer || ActiveDraggerCount < maximumDraggers);
!IsExternallyControlled &&
(IsDraggedByLocalPlayer ||
(ActiveDraggerCount < maximumDraggers &&
(NetworkManager == null ||
!TryGetDraggedObject(NetworkManager.LocalClientId, this, out _))));
private void Awake()
{
@@ -121,15 +135,19 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
public override void OnNetworkSpawn()
{
SpawnedDraggables.Add(this);
firstDraggerClientId.OnValueChanged += HandleDraggerChanged;
secondDraggerClientId.OnValueChanged += HandleDraggerChanged;
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
ApplyDraggingState();
}
public override void OnNetworkDespawn()
{
SpawnedDraggables.Remove(this);
firstDraggerClientId.OnValueChanged -= HandleDraggerChanged;
secondDraggerClientId.OnValueChanged -= HandleDraggerChanged;
isExternallyControlled.OnValueChanged -= HandleExternalControlChanged;
RestorePlayerCollisions();
}
@@ -144,7 +162,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
private void FixedUpdate()
{
if (!IsServer || !IsSpawned || physicsBody == null)
if (!IsServer || !IsSpawned || physicsBody == null || IsExternallyControlled)
return;
RemoveInvalidDraggers();
@@ -177,9 +195,17 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
}
}
protected override bool CanInteractOnServer(ulong senderClientId) =>
IsDragger(senderClientId) ||
ActiveDraggerCount < maximumDraggers;
protected override bool CanInteractOnServer(ulong senderClientId)
{
if (IsExternallyControlled)
return false;
if (IsDragger(senderClientId))
return true;
return ActiveDraggerCount < maximumDraggers &&
!TryGetDraggedObject(senderClientId, this, out _);
}
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
{
@@ -191,6 +217,9 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
private void AddDragger(ulong clientId)
{
if (IsExternallyControlled || TryGetDraggedObject(clientId, this, out _))
return;
if (firstDraggerClientId.Value == NoDragger)
firstDraggerClientId.Value = clientId;
else if (maximumDraggers > 1 && secondDraggerClientId.Value == NoDragger)
@@ -265,19 +294,78 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
if (clientId == NoDragger || !TryGetPlayerObject(clientId, out NetworkObject playerObject))
return;
Transform dragAnchor = ResolveDragAnchor(playerObject);
target += dragAnchor.TransformPoint(dragOffset);
forward += dragAnchor.forward;
FirstPersonCameraController cameraController =
playerObject.GetComponent<FirstPersonCameraController>();
Vector3 aimForward = cameraController != null
? cameraController.AimForward
: playerObject.transform.forward;
target += playerObject.transform.position +
playerObject.transform.right * dragOffset.x +
Vector3.up * dragOffset.y +
aimForward * dragOffset.z;
forward += aimForward;
validTargets++;
}
private static Transform ResolveDragAnchor(NetworkObject playerObject)
private static bool TryGetDraggedObject(
ulong clientId,
NetworkDraggableInteractable except,
out NetworkDraggableInteractable draggedObject)
{
FirstPersonCameraController cameraController =
playerObject.GetComponent<FirstPersonCameraController>();
return cameraController != null
? cameraController.AimTransform
: playerObject.transform;
foreach (NetworkDraggableInteractable candidate in SpawnedDraggables)
{
if (candidate != null && candidate != except && candidate.IsDragger(clientId))
{
draggedObject = candidate;
return true;
}
}
draggedObject = null;
return false;
}
public static bool TryGetDraggedObject(
ulong clientId,
out NetworkDraggableInteractable draggedObject) =>
TryGetDraggedObject(clientId, null, out draggedObject);
public void RequestRelease()
{
if (IsSpawned && IsDraggedByLocalPlayer)
RequestReleaseRpc();
}
public bool TryAcquireExternalControl()
{
if (!IsServer || !IsSpawned || IsExternallyControlled)
return false;
isExternallyControlled.Value = true;
firstDraggerClientId.Value = NoDragger;
secondDraggerClientId.Value = NoDragger;
ApplyDraggingState();
return true;
}
public bool TryReleaseExternalControl()
{
if (!IsServer || !IsSpawned || !IsExternallyControlled)
return false;
isExternallyControlled.Value = false;
ApplyDraggingState();
return true;
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestReleaseRpc(RpcParams rpcParams = default)
{
ulong senderClientId = rpcParams.Receive.SenderClientId;
if (!IsExternallyControlled && IsDragger(senderClientId))
RemoveDragger(senderClientId);
}
private bool IsDragger(ulong clientId) =>
@@ -285,11 +373,13 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
private void HandleDraggerChanged(ulong _, ulong __) => ApplyDraggingState();
private void HandleExternalControlChanged(bool _, bool __) => ApplyDraggingState();
private void ApplyDraggingState()
{
if (physicsBody != null)
{
bool shouldBeKinematic = HasAnyDragger || !IsServer;
bool shouldBeKinematic = HasAnyDragger || IsExternallyControlled || !IsServer;
if (physicsBody.isKinematic && !shouldBeKinematic)
physicsBody.isKinematic = false;
@@ -303,7 +393,7 @@ public sealed class NetworkDraggableInteractable : NetworkInteractable
physicsBody.isKinematic = shouldBeKinematic;
}
if (HasAnyDragger)
if (HasAnyDragger || IsExternallyControlled)
{
dragVelocity = Vector3.zero;
CacheGroundContactPoint();
@@ -4,7 +4,7 @@ using Unity.Netcode.Components;
using UnityEngine;
[RequireComponent(typeof(Rigidbody), typeof(NetworkRigidbody))]
public abstract class NetworkHeldInteractable : NetworkInteractable
public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstationItemControl
{
protected const ulong NoHolder = ulong.MaxValue;
private static readonly HashSet<NetworkHeldInteractable> SpawnedHeldObjects = new();
@@ -0,0 +1,5 @@
public interface IWorkstationItemControl
{
bool TryAcquireExternalControl();
bool TryReleaseExternalControl();
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b32c69f6645bac8499f4e8e068e84034
@@ -16,12 +16,22 @@ public sealed class PlayerCarryController : NetworkBehaviour, IPlayerSystem
if (!isInitialized || !IsOwner || Keyboard.current == null)
return;
if (Keyboard.current.gKey.wasPressedThisFrame &&
NetworkHeldInteractable.TryGetHeldObject(
if (!Keyboard.current.gKey.wasPressedThisFrame)
return;
if (NetworkHeldInteractable.TryGetHeldObject(
OwnerClientId,
out NetworkHeldInteractable heldObject))
{
heldObject.RequestThrow();
return;
}
if (NetworkDraggableInteractable.TryGetDraggedObject(
OwnerClientId,
out NetworkDraggableInteractable draggedObject))
{
draggedObject.RequestRelease();
}
}
}
@@ -1,43 +1,116 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
public sealed class PlayerSpawnController : NetworkBehaviour, IPlayerSystem
{
[Header("Random spawn area")]
[SerializeField] private Vector3 spawnCenter = new(0f, 1f, 0f);
[SerializeField, Min(0f)] private float spawnRadius = 8f;
[Header("Start Room Spawn")]
[SerializeField, Min(0f)] private float edgePadding = 0.75f;
[SerializeField, Min(0f)] private float heightAboveFloor = 0.15f;
private readonly NetworkVariable<Vector3> assignedSpawnPosition = new(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<bool> hasAssignedSpawnPosition = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private int assignedSceneHandle = -1;
public void Initialize(PlayerFacade facade) { }
public override void OnNetworkSpawn()
{
assignedSpawnPosition.OnValueChanged += HandleSpawnPositionChanged;
hasAssignedSpawnPosition.OnValueChanged += HandleSpawnAssignmentChanged;
SceneManager.sceneLoaded += HandleSceneLoaded;
if (IsServer)
{
Vector2 randomOffset = Random.insideUnitCircle * spawnRadius;
assignedSpawnPosition.Value = spawnCenter + new Vector3(randomOffset.x, 0f, randomOffset.y);
}
TryAssignStartRoomPosition(SceneManager.GetActiveScene());
ApplySpawnPosition(assignedSpawnPosition.Value);
if (hasAssignedSpawnPosition.Value)
ApplySpawnPosition(assignedSpawnPosition.Value);
}
public override void OnNetworkDespawn()
{
assignedSpawnPosition.OnValueChanged -= HandleSpawnPositionChanged;
hasAssignedSpawnPosition.OnValueChanged -= HandleSpawnAssignmentChanged;
SceneManager.sceneLoaded -= HandleSceneLoaded;
}
private void HandleSpawnPositionChanged(Vector3 _, Vector3 position) =>
ApplySpawnPosition(position);
private void HandleSceneLoaded(Scene scene, LoadSceneMode _)
{
if (IsServer)
TryAssignStartRoomPosition(scene);
if (IsOwner && hasAssignedSpawnPosition.Value)
ApplySpawnPosition(assignedSpawnPosition.Value);
}
private void TryAssignStartRoomPosition(Scene scene)
{
if (!scene.IsValid() || !scene.isLoaded || scene.handle == assignedSceneHandle)
return;
StartRoomZone startRoom = FindStartRoomInScene(scene);
if (startRoom == null ||
!startRoom.TryGetRandomSpawnPosition(
edgePadding,
heightAboveFloor,
out Vector3 position))
{
return;
}
hasAssignedSpawnPosition.Value = false;
assignedSpawnPosition.Value = position;
hasAssignedSpawnPosition.Value = true;
assignedSceneHandle = scene.handle;
}
private static StartRoomZone FindStartRoomInScene(Scene scene)
{
foreach (GameObject rootObject in scene.GetRootGameObjects())
{
StartRoomZone startRoom = rootObject.GetComponentInChildren<StartRoomZone>(true);
if (startRoom != null)
return startRoom;
}
return null;
}
private void HandleSpawnPositionChanged(Vector3 _, Vector3 position)
{
if (hasAssignedSpawnPosition.Value)
ApplySpawnPosition(position);
}
private void HandleSpawnAssignmentChanged(bool _, bool isAssigned)
{
if (isAssigned)
ApplySpawnPosition(assignedSpawnPosition.Value);
}
private void ApplySpawnPosition(Vector3 position)
{
if (IsOwner)
transform.position = position;
if (!IsOwner)
return;
CharacterController characterController = GetComponent<CharacterController>();
bool restoreCharacterController = characterController != null && characterController.enabled;
if (restoreCharacterController)
characterController.enabled = false;
transform.position = position;
Physics.SyncTransforms();
if (restoreCharacterController)
characterController.enabled = true;
}
}
@@ -5,6 +5,8 @@ public sealed class StartRoomZone : MonoBehaviour
{
[SerializeField] private Collider roomCollider;
public Collider RoomCollider => roomCollider;
private void Awake()
{
if (roomCollider == null)
@@ -14,6 +16,56 @@ public sealed class StartRoomZone : MonoBehaviour
public bool Contains(Vector3 worldPosition) =>
roomCollider != null && roomCollider.bounds.Contains(worldPosition);
public bool TryGetRandomSpawnPosition(
float edgePadding,
float heightAboveFloor,
out Vector3 position)
{
if (roomCollider == null)
{
position = default;
return false;
}
edgePadding = Mathf.Max(0f, edgePadding);
heightAboveFloor = Mathf.Max(0f, heightAboveFloor);
if (roomCollider is BoxCollider boxCollider)
{
Vector3 lossyScale = boxCollider.transform.lossyScale;
float localPaddingX = edgePadding / Mathf.Max(Mathf.Abs(lossyScale.x), 0.0001f);
float localPaddingZ = edgePadding / Mathf.Max(Mathf.Abs(lossyScale.z), 0.0001f);
Vector3 halfSize = boxCollider.size * 0.5f;
float availableX = Mathf.Max(0f, halfSize.x - localPaddingX);
float availableZ = Mathf.Max(0f, halfSize.z - localPaddingZ);
Vector3 localPosition = boxCollider.center + new Vector3(
Random.Range(-availableX, availableX),
-halfSize.y,
Random.Range(-availableZ, availableZ));
position = boxCollider.transform.TransformPoint(localPosition) +
Vector3.up * heightAboveFloor;
return true;
}
Bounds bounds = roomCollider.bounds;
float minX = bounds.min.x + edgePadding;
float maxX = bounds.max.x - edgePadding;
float minZ = bounds.min.z + edgePadding;
float maxZ = bounds.max.z - edgePadding;
if (minX > maxX)
minX = maxX = bounds.center.x;
if (minZ > maxZ)
minZ = maxZ = bounds.center.z;
position = new Vector3(
Random.Range(minX, maxX),
bounds.min.y + heightAboveFloor,
Random.Range(minZ, maxZ));
return true;
}
private void OnDrawGizmosSelected()
{
Collider targetCollider = roomCollider != null ? roomCollider : GetComponent<Collider>();
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
@@ -11,8 +12,8 @@ public enum ItemProcessingWorkstationState : byte
public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationController
{
[Header("Recipe")]
[SerializeField] private ItemProcessingRecipe recipe;
[Header("Recipes")]
[SerializeField] private List<ItemProcessingRecipe> recipes = new();
[Header("Slot")]
[SerializeField] private Transform inputPoint;
@@ -32,16 +33,27 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> activeRecipeIndex = new(
-1,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private NetworkItem currentItem;
private IWorkstationItemControl currentItemControl;
public override bool IsOperating => State == ItemProcessingWorkstationState.Processing;
public ItemProcessingWorkstationState State => state.Value;
public ItemProcessingRecipe Recipe => recipe;
public IReadOnlyList<ItemProcessingRecipe> Recipes => recipes;
public int ActiveRecipeIndex => activeRecipeIndex.Value;
public ItemProcessingRecipe ActiveRecipe => TryGetRecipe(
activeRecipeIndex.Value,
out ItemProcessingRecipe recipe)
? recipe
: null;
public NetworkItem CurrentItem => currentItem;
public bool CanAcceptItem => IsServer && IsSpawned &&
State == ItemProcessingWorkstationState.Empty &&
recipe != null && recipe.InputItem != null &&
recipe.OutputItem != null && recipe.OutputItem.WorldPrefab != null;
recipes.Exists(IsRecipeValid);
public float ProcessProgress
{
@@ -72,6 +84,7 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
state.OnValueChanged += HandleStateChanged;
processStartedAt.OnValueChanged += HandleProcessTimeChanged;
processEndsAt.OnValueChanged += HandleProcessTimeChanged;
activeRecipeIndex.OnValueChanged += HandleActiveRecipeChanged;
if (IsServer)
ResetWorkstationOnServer();
@@ -84,6 +97,7 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
state.OnValueChanged -= HandleStateChanged;
processStartedAt.OnValueChanged -= HandleProcessTimeChanged;
processEndsAt.OnValueChanged -= HandleProcessTimeChanged;
activeRecipeIndex.OnValueChanged -= HandleActiveRecipeChanged;
}
private void Update()
@@ -123,15 +137,19 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
public bool TryAcceptItem(NetworkItem item)
{
if (!CanAcceptItem || item == null || !item.IsSpawned || !recipe.Accepts(item))
if (!CanAcceptItem || item == null || !item.IsSpawned ||
!TryFindRecipe(item, out int recipeIndex, out ItemProcessingRecipe recipe) ||
!TryGetItemControl(item, out IWorkstationItemControl itemControl))
{
return false;
}
NetworkCarryableInteractable carryable =
item.GetComponent<NetworkCarryableInteractable>();
if (carryable == null || !carryable.TryAcquireExternalControl())
if (!itemControl.TryAcquireExternalControl())
return false;
currentItem = item;
currentItemControl = itemControl;
activeRecipeIndex.Value = recipeIndex;
SnapToInputPoint(item.transform);
double now = GetNetworkTime();
@@ -156,6 +174,7 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
private void CompleteProcessingOnServer()
{
ItemProcessingRecipe recipe = ActiveRecipe;
if (currentItem == null || recipe == null ||
recipe.OutputItem == null || recipe.OutputItem.WorldPrefab == null)
{
@@ -169,6 +188,7 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
NetworkObject inputObject = currentItem.NetworkObject;
currentItem = null;
currentItemControl = null;
if (inputObject != null && inputObject.IsSpawned)
inputObject.Despawn(true);
@@ -180,13 +200,12 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
outputObject.Spawn();
currentItem = outputObject.GetComponent<NetworkItem>();
if (currentItem == null ||
outputObject.GetComponent<NetworkCarryableInteractable>() == null)
if (currentItem == null || !TryGetItemControl(currentItem, out _))
{
Debug.LogError(
$"{name} cannot produce '{recipe.OutputItem.DisplayName}': " +
"the output prefab must contain NetworkItem and " +
"NetworkCarryableInteractable.",
"the output prefab must contain NetworkItem and a supported " +
"Carry or Drag interaction component.",
this);
outputObject.Despawn(true);
currentItem = null;
@@ -205,13 +224,12 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
if (!IsServer)
return;
if (State == ItemProcessingWorkstationState.Processing && currentItem != null &&
currentItem.TryGetComponent(out NetworkCarryableInteractable carryable))
{
carryable.TryReleaseExternalControl();
}
if (State == ItemProcessingWorkstationState.Processing && currentItemControl != null)
currentItemControl.TryReleaseExternalControl();
currentItem = null;
currentItemControl = null;
activeRecipeIndex.Value = -1;
processStartedAt.Value = 0d;
processEndsAt.Value = 0d;
state.Value = ItemProcessingWorkstationState.Empty;
@@ -233,4 +251,60 @@ public sealed class NetworkItemProcessingWorkstation : NetworkWorkstationControl
ItemProcessingWorkstationState __) => Changed?.Invoke();
private void HandleProcessTimeChanged(double _, double __) => Changed?.Invoke();
private void HandleActiveRecipeChanged(int _, int __) => Changed?.Invoke();
private bool TryFindRecipe(
NetworkItem item,
out int recipeIndex,
out ItemProcessingRecipe recipe)
{
for (int index = 0; index < recipes.Count; index++)
{
ItemProcessingRecipe candidate = recipes[index];
if (IsRecipeValid(candidate) && candidate.Accepts(item))
{
recipeIndex = index;
recipe = candidate;
return true;
}
}
recipeIndex = -1;
recipe = null;
return false;
}
private bool TryGetRecipe(int index, out ItemProcessingRecipe recipe)
{
if (index >= 0 && index < recipes.Count && IsRecipeValid(recipes[index]))
{
recipe = recipes[index];
return true;
}
recipe = null;
return false;
}
private static bool IsRecipeValid(ItemProcessingRecipe recipe) =>
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;
}
}