Started make greybox for main location

This commit is contained in:
2026-08-05 17:49:42 +03:00
parent a259a64e80
commit 90934eca22
648 changed files with 314989 additions and 578 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 27e68eff694375147a9ceadc73023418
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public sealed class DeveloperPlayerController : MonoBehaviour
{
[SerializeField, Min(0.1f)] private float walkSpeed = 5f;
[SerializeField, Min(0.1f)] private float flySpeed = 12f;
[SerializeField, Min(0.01f)] private float mouseSensitivity = 0.08f;
[SerializeField, Min(0.1f)] private float gravity = 20f;
[SerializeField, Range(1f, 89f)] private float verticalLookLimit = 85f;
private CharacterController characterController;
private Camera playerCamera;
private float verticalVelocity;
private float pitch;
private bool isFlying;
private void Awake()
{
if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening)
{
gameObject.SetActive(false);
return;
}
characterController = GetComponent<CharacterController>();
playerCamera = GetComponentInChildren<Camera>(true);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
foreach (Camera camera in FindObjectsByType<Camera>(FindObjectsSortMode.None))
if (camera != playerCamera) camera.enabled = false;
}
private void Update()
{
if (Keyboard.current == null)
return;
if (Keyboard.current.fKey.wasPressedThisFrame)
isFlying = !isFlying;
UpdateLook();
UpdateMovement();
}
private void UpdateLook()
{
if (Mouse.current == null)
return;
Vector2 mouseDelta = Mouse.current.delta.ReadValue() * mouseSensitivity;
transform.Rotate(0f, mouseDelta.x, 0f, Space.Self);
pitch = Mathf.Clamp(pitch - mouseDelta.y, -verticalLookLimit, verticalLookLimit);
playerCamera.transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
}
private void UpdateMovement()
{
Vector2 input = new(
(Keyboard.current.dKey.isPressed ? 1f : 0f) - (Keyboard.current.aKey.isPressed ? 1f : 0f),
(Keyboard.current.wKey.isPressed ? 1f : 0f) - (Keyboard.current.sKey.isPressed ? 1f : 0f));
if (input.sqrMagnitude > 1f)
input.Normalize();
float speed = Keyboard.current.leftShiftKey.isPressed ? flySpeed : walkSpeed;
Vector3 direction = transform.right * input.x + transform.forward * input.y;
if (isFlying)
{
direction += Vector3.up * ((Keyboard.current.spaceKey.isPressed ? 1f : 0f) -
(Keyboard.current.leftCtrlKey.isPressed ? 1f : 0f));
characterController.Move(direction.normalized * speed * Time.deltaTime);
return;
}
if (characterController.isGrounded && verticalVelocity < 0f)
verticalVelocity = -2f;
verticalVelocity -= gravity * Time.deltaTime;
characterController.Move((direction * speed + Vector3.up * verticalVelocity) * Time.deltaTime);
}
private void OnDisable()
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 997d1a55c8f384542a3d46e7bcfa940f
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 346c898a2ae43ee4da589c1fdbad0a3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;
public sealed class NetworkButtonInteractable : NetworkInteractable
{
[SerializeField] private UnityEvent<bool> buttonStateChanged;
private readonly NetworkVariable<bool> isPressed = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public bool IsPressed => isPressed.Value;
public override void OnNetworkSpawn()
{
isPressed.OnValueChanged += HandleStateChanged;
ApplyState(isPressed.Value);
}
public override void OnNetworkDespawn()
{
isPressed.OnValueChanged -= HandleStateChanged;
}
protected override bool CanInteractOnServer(ulong senderClientId) => true;
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
{
isPressed.Value = !isPressed.Value;
ApplyState(isPressed.Value);
}
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
private void ApplyState(bool currentValue) => buttonStateChanged?.Invoke(currentValue);
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43cd66292a303c3478352a33bc74db87
@@ -0,0 +1,9 @@
using UnityEngine;
public sealed class NetworkCarryableInteractable : NetworkHeldInteractable
{
[SerializeField] private Vector3 carryOffset = new(0f, 1.15f, 0.75f);
protected override Vector3 HoldingOffset => carryOffset;
protected override string DropPrompt => "Put down";
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fad79bb3d620ffb46b527a9e361c28ae
@@ -0,0 +1,109 @@
using DG.Tweening;
using Unity.Netcode;
using UnityEngine;
[RequireComponent(typeof(NetworkObject))]
public sealed class NetworkDoorController : NetworkBehaviour
{
[Header("Visual")]
[SerializeField] private Transform doorVisual;
[SerializeField] private Vector3 openLocalPositionOffset = new(0f, 2.5f, 0f);
[SerializeField] private Vector3 openLocalRotationOffset;
[SerializeField, Min(0.01f)] private float animationDuration = 0.65f;
[SerializeField] private Ease animationEase = Ease.OutCubic;
private readonly NetworkVariable<bool> isOpen = new(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private Vector3 closedLocalPosition;
private Vector3 closedLocalEulerAngles;
private Tween doorTween;
public bool IsOpen => isOpen.Value;
private void Awake()
{
if (doorVisual == null)
doorVisual = transform;
closedLocalPosition = doorVisual.localPosition;
closedLocalEulerAngles = doorVisual.localEulerAngles;
}
public override void OnNetworkSpawn()
{
isOpen.OnValueChanged += HandleOpenStateChanged;
ApplyVisualState(isOpen.Value, true);
}
public override void OnNetworkDespawn()
{
isOpen.OnValueChanged -= HandleOpenStateChanged;
}
private void OnDisable()
{
doorTween?.Kill();
}
public void SetOpen(bool shouldOpen)
{
if (!IsServer)
return;
isOpen.Value = shouldOpen;
ApplyVisualState(shouldOpen, false);
}
public void Toggle()
{
SetOpen(!isOpen.Value);
}
[ContextMenu("Preview/Open")]
private void PreviewOpen() => PreviewVisual(true);
[ContextMenu("Preview/Close")]
private void PreviewClose() => PreviewVisual(false);
[ContextMenu("Preview/Toggle")]
private void PreviewToggle() =>
PreviewVisual(doorVisual != null && doorVisual.localPosition == closedLocalPosition);
private void PreviewVisual(bool shouldOpen)
{
ApplyVisualState(shouldOpen, !Application.isPlaying);
}
private void HandleOpenStateChanged(bool _, bool currentValue) =>
ApplyVisualState(currentValue, false);
private void ApplyVisualState(bool shouldOpen, bool immediately)
{
if (doorVisual == null)
return;
doorTween?.Kill();
Vector3 targetPosition = closedLocalPosition +
(shouldOpen ? openLocalPositionOffset : Vector3.zero);
Vector3 targetRotation = closedLocalEulerAngles +
(shouldOpen ? openLocalRotationOffset : Vector3.zero);
if (immediately)
{
doorVisual.localPosition = targetPosition;
doorVisual.localEulerAngles = targetRotation;
return;
}
doorTween = DOTween.Sequence()
.Join(doorVisual.DOLocalMove(targetPosition, animationDuration))
.Join(doorVisual.DOLocalRotate(targetRotation, animationDuration))
.SetEase(animationEase)
.SetLink(gameObject);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 402a356b266639c448f71cfce624ec90
@@ -0,0 +1,9 @@
using UnityEngine;
public sealed class NetworkDraggableInteractable : NetworkHeldInteractable
{
[SerializeField] private Vector3 dragOffset = new(0f, 0.35f, 1.8f);
protected override Vector3 HoldingOffset => dragOffset;
protected override string DropPrompt => "Release";
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5e737931b3bc22468ea0fe857062359
@@ -0,0 +1,158 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public abstract class NetworkHeldInteractable : NetworkInteractable
{
protected const ulong NoHolder = ulong.MaxValue;
[Header("Physics")]
[SerializeField] private Rigidbody physicsBody;
[SerializeField] private Collider[] objectColliders;
[SerializeField, Min(0.05f)] private float collisionRefreshInterval = 0.25f;
private readonly NetworkVariable<ulong> holderClientId = new(
NoHolder,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private float nextCollisionRefreshTime;
protected abstract Vector3 HoldingOffset { get; }
protected abstract string DropPrompt { get; }
private bool IsHeld => holderClientId.Value != NoHolder;
private bool IsHeldByLocalPlayer =>
NetworkManager != null && holderClientId.Value == NetworkManager.LocalClientId;
public override string InteractionPrompt => !IsHeld
? InteractionLabel
: IsHeldByLocalPlayer
? DropPrompt
: "In use";
public override bool IsInteractionAvailable => base.IsInteractionAvailable &&
(!IsHeld || IsHeldByLocalPlayer);
private void Awake()
{
if (physicsBody == null)
physicsBody = GetComponent<Rigidbody>();
if (objectColliders == null || objectColliders.Length == 0)
objectColliders = GetComponentsInChildren<Collider>(true);
}
public override void OnNetworkSpawn()
{
holderClientId.OnValueChanged += HandleHolderChanged;
ApplyHoldingState(IsHeld);
}
public override void OnNetworkDespawn()
{
holderClientId.OnValueChanged -= HandleHolderChanged;
ApplyHoldingState(false);
}
private void LateUpdate()
{
if (!IsSpawned || !IsHeld)
return;
if (!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
{
if (IsServer)
StopHolding();
return;
}
transform.SetPositionAndRotation(
holder.transform.TransformPoint(HoldingOffset),
holder.transform.rotation);
if (Time.time >= nextCollisionRefreshTime)
{
nextCollisionRefreshTime = Time.time + collisionRefreshInterval;
IgnoreCollisionsWithPlayers();
}
}
protected override bool CanInteractOnServer(ulong senderClientId) =>
!IsHeld || holderClientId.Value == senderClientId;
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
{
if (holderClientId.Value == senderClientId)
StopHolding();
else if (!IsHeld)
StartHolding(senderClientId);
}
private void StartHolding(ulong clientId)
{
holderClientId.Value = clientId;
ApplyHoldingState(true);
}
private void StopHolding()
{
holderClientId.Value = NoHolder;
ApplyHoldingState(false);
}
private void HandleHolderChanged(ulong _, ulong currentHolder) =>
ApplyHoldingState(currentHolder != NoHolder);
private void ApplyHoldingState(bool isHeld)
{
if (physicsBody != null)
{
physicsBody.isKinematic = isHeld;
if (!isHeld)
physicsBody.linearVelocity = Vector3.zero;
}
if (isHeld)
IgnoreCollisionsWithPlayers();
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)
continue;
Physics.IgnoreCollision(objectCollider, playerCollider, true);
}
ignoredPlayerColliders.Add(playerCollider);
}
}
private void RestorePlayerCollisions()
{
foreach (Collider playerCollider in ignoredPlayerColliders)
{
if (playerCollider == null)
continue;
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider != null)
Physics.IgnoreCollision(objectCollider, playerCollider, false);
}
}
ignoredPlayerColliders.Clear();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 12231d0b12920fd468d1117aa9a3c59c
@@ -0,0 +1,61 @@
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;
public interface IInteractable
{
string InteractionPrompt { get; }
bool IsInteractionAvailable { get; }
void RequestInteraction();
}
[AddComponentMenu("")]
[RequireComponent(typeof(NetworkObject), typeof(NetworkTransform))]
public class NetworkInteractable : NetworkBehaviour, IInteractable
{
[Header("Interaction")]
[SerializeField] private string interactionLabel = "Interact";
[SerializeField, Min(0.1f)] private float maximumInteractionDistance = 3f;
protected string InteractionLabel => interactionLabel;
public virtual string InteractionPrompt => interactionLabel;
public virtual bool IsInteractionAvailable => IsSpawned;
public void RequestInteraction()
{
if (!IsSpawned || !IsInteractionAvailable)
return;
RequestInteractionServerRpc();
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestInteractionServerRpc(RpcParams rpcParams = default)
{
ulong senderClientId = rpcParams.Receive.SenderClientId;
if (!TryGetPlayerObject(senderClientId, out NetworkObject playerObject))
return;
float maximumDistanceSquared = maximumInteractionDistance * maximumInteractionDistance;
if ((playerObject.transform.position - transform.position).sqrMagnitude > maximumDistanceSquared)
return;
if (!CanInteractOnServer(senderClientId))
return;
InteractOnServer(senderClientId, playerObject);
}
protected virtual bool CanInteractOnServer(ulong senderClientId) => false;
protected virtual void InteractOnServer(ulong senderClientId, NetworkObject playerObject) { }
protected bool TryGetPlayerObject(ulong clientId, out NetworkObject playerObject)
{
playerObject = NetworkManager != null
? NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId)
: null;
return playerObject != null;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2aecb06ed06d9e145acd3163b0bdda1c
@@ -1,54 +1,218 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using Unity.Services.Relay;
using Unity.Services.Relay.Models;
using UnityEngine;
using UnityEngine.SceneManagement;
[RequireComponent(typeof(NetworkManager), typeof(UnityTransport))]
public sealed class NetworkSessionController : MonoBehaviour
{
private const string PlayerNamePreferenceKey = "player_name";
private const string RelayCodeDataKey = "relay_code";
public static NetworkSessionController Instance { get; private set; }
[SerializeField] private NetworkObject playerPrefab;
[SerializeField] private string menuSceneName = "MainMenu";
[SerializeField] private string gameplaySceneName = "Factory";
public string Status { get; private set; } = "Ready.";
[SerializeField, Min(1)] private int maxClientCount = 3;
[SerializeField] private bool useSecureRelay = false;
public string Status { get; private set; } = "Relay is not initialized.";
public string JoinCode { get; private set; } = string.Empty;
public string LocalPlayerName { get; private set; }
public bool IsBusy { get; private set; }
public bool IsOnline => NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening;
public bool IsHost => networkManager != null && networkManager.IsHost;
public IReadOnlyList<Lobby> PublicLobbies => publicLobbies;
private readonly List<Lobby> publicLobbies = new();
private NetworkManager networkManager;
private UnityTransport transport;
private bool loadFactoryAfterServerStart;
private void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this; DontDestroyOnLoad(gameObject);
Instance = this;
DontDestroyOnLoad(gameObject);
LocalPlayerName = PlayerPrefs.GetString(PlayerNamePreferenceKey, "Worker");
networkManager = GetComponent<NetworkManager>();
transport = GetComponent<UnityTransport>();
networkManager.NetworkConfig.NetworkTransport = transport;
networkManager.NetworkConfig.PlayerPrefab = playerPrefab.gameObject;
networkManager.NetworkConfig.EnableSceneManagement = true;
networkManager.OnServerStarted += LoadGameplayScene;
networkManager.OnServerStarted += HandleServerStarted;
networkManager.OnClientConnectedCallback += HandleClientConnected;
}
private void Start() => SceneManager.LoadScene(menuSceneName);
private void OnDestroy()
{
if (networkManager != null) networkManager.OnServerStarted -= LoadGameplayScene;
if (networkManager != null) networkManager.OnServerStarted -= HandleServerStarted;
if (networkManager != null) networkManager.OnClientConnectedCallback -= HandleClientConnected;
if (Instance == this) Instance = null;
}
public bool StartHost(ushort port)
public void SetLocalPlayerName(string playerName)
{
transport.SetConnectionData("0.0.0.0", port, "0.0.0.0");
bool started = networkManager.StartHost();
Status = started ? $"Host started on {port}." : "Host failed to start.";
return started;
string sanitizedName = string.IsNullOrWhiteSpace(playerName) ? "Worker" : playerName.Trim();
LocalPlayerName = sanitizedName.Length > 24 ? sanitizedName[..24] : sanitizedName;
PlayerPrefs.SetString(PlayerNamePreferenceKey, LocalPlayerName);
PlayerPrefs.Save();
}
public bool StartClient(string address, ushort port)
public async Task StartHostAsync()
{
transport.SetConnectionData(address.Trim(), port);
bool started = networkManager.StartClient();
Status = started ? $"Connecting to {address}:{port}..." : "Client failed to start.";
return started;
if (IsBusy || IsOnline) return;
IsBusy = true;
try
{
await InitializeServicesAsync();
Status = "Creating Relay allocation...";
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxClientCount);
transport.SetHostRelayData(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port,
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, useSecureRelay);
JoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
await PublishLobbyAsync();
loadFactoryAfterServerStart = true;
if (!networkManager.StartHost()) throw new InvalidOperationException("Netcode host could not start.");
Status = "Host started.";
}
catch (Exception exception)
{
Status = $"Relay host error: {exception.Message}";
Debug.LogException(exception);
}
finally { IsBusy = false; }
}
public async Task StartClientAsync(string joinCode)
{
if (IsBusy || IsOnline || string.IsNullOrWhiteSpace(joinCode)) return;
IsBusy = true;
try
{
await InitializeServicesAsync();
Status = "Joining Relay allocation...";
JoinAllocation allocation = await RelayService.Instance.JoinAllocationAsync(joinCode.Trim().ToUpperInvariant());
transport.SetClientRelayData(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port,
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, allocation.HostConnectionData, useSecureRelay);
if (!networkManager.StartClient()) throw new InvalidOperationException("Netcode client could not start.");
Status = "Connecting to host...";
}
catch (Exception exception)
{
Status = $"Relay join error: {exception.Message}";
Debug.LogException(exception);
}
finally { IsBusy = false; }
}
public async Task RefreshPublicLobbiesAsync()
{
if (IsBusy) return;
IsBusy = true;
try
{
await InitializeServicesAsync();
QueryResponse response = await LobbyService.Instance.QueryLobbiesAsync(new QueryLobbiesOptions { Count = 20 });
publicLobbies.Clear();
publicLobbies.AddRange(response.Results);
Status = $"{publicLobbies.Count} open lobby/lobbies found.";
}
catch (Exception exception)
{
Status = $"Lobby query error: {exception.Message}";
Debug.LogException(exception);
}
finally { IsBusy = false; }
}
public async Task JoinPublicLobbyAsync(string lobbyId)
{
if (IsBusy || IsOnline || string.IsNullOrWhiteSpace(lobbyId)) return;
string relayCode = null;
IsBusy = true;
try
{
await InitializeServicesAsync();
Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
if (lobby.Data.TryGetValue(RelayCodeDataKey, out DataObject relayCodeData))
relayCode = relayCodeData.Value;
else
throw new InvalidOperationException("The lobby has no Relay join code.");
}
catch (Exception exception)
{
Status = $"Lobby join error: {exception.Message}";
Debug.LogException(exception);
}
finally { IsBusy = false; }
if (!string.IsNullOrEmpty(relayCode))
await StartClientAsync(relayCode);
}
private async Task PublishLobbyAsync()
{
try
{
var options = new CreateLobbyOptions
{
IsPrivate = false,
Data = new Dictionary<string, DataObject>
{
{ RelayCodeDataKey, new DataObject(DataObject.VisibilityOptions.Public, JoinCode) }
}
};
await LobbyService.Instance.CreateLobbyAsync($"{LocalPlayerName}'s Factory", maxClientCount + 1, options);
}
catch (Exception exception)
{
Debug.LogWarning($"Lobby publishing failed. Join code still works. {exception.Message}");
}
}
private static async Task InitializeServicesAsync()
{
if (UnityServices.State == ServicesInitializationState.Uninitialized)
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
private void HandleServerStarted()
{
if (!loadFactoryAfterServerStart) return;
loadFactoryAfterServerStart = false;
LoadGameplayScene();
}
private void HandleClientConnected(ulong clientId)
{
if (!networkManager.IsHost && clientId == networkManager.LocalClientId)
{
Status = "Connected. Loading Factory...";
if (SceneManager.GetActiveScene().name != gameplaySceneName)
SceneManager.LoadScene(gameplaySceneName);
}
}
private void LoadGameplayScene()
{
Status = "Loading factory...";
if (!IsHost) return;
Status = "Loading Factory...";
networkManager.SceneManager.LoadScene(gameplaySceneName, LoadSceneMode.Single);
}
}
}
@@ -0,0 +1,63 @@
using Unity.Netcode;
using UnityEngine;
public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSystem
{
[SerializeField, Range(1f, 89f)] private float verticalLookLimit = 80f;
private Camera playerCamera;
private AudioListener audioListener;
private MeshRenderer playerRenderer;
private float pitch;
private bool isInitialized;
public void Initialize(PlayerFacade facade)
{
playerCamera = GetComponentInChildren<Camera>(true);
audioListener = playerCamera != null ? playerCamera.GetComponent<AudioListener>() : null;
playerRenderer = GetComponentInChildren<MeshRenderer>();
isInitialized = playerCamera != null;
if (!isInitialized)
return;
bool isLocalPlayer = IsOwner;
playerCamera.gameObject.SetActive(isLocalPlayer);
if (!isLocalPlayer)
return;
if (playerRenderer != null)
playerRenderer.enabled = false;
foreach (Camera camera in FindObjectsByType<Camera>(FindObjectsSortMode.None))
{
if (camera != playerCamera)
camera.enabled = false;
}
if (audioListener != null)
audioListener.enabled = true;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
public void ApplyPitch(float pitchDelta)
{
if (!isInitialized || !IsOwner)
return;
pitch = Mathf.Clamp(pitch - pitchDelta, -verticalLookLimit, verticalLookLimit);
playerCamera.transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
}
public override void OnNetworkDespawn()
{
if (IsOwner)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40a194304b53aea488f1573da427fbd4
@@ -0,0 +1,59 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public sealed class PlayerController : NetworkBehaviour, IPlayerSystem
{
[Header("Movement")]
[SerializeField, Min(0.1f)] private float walkSpeed = 4.5f;
[SerializeField, Min(0.1f)] private float sprintSpeed = 7f;
[Header("Vertical movement")]
[SerializeField, Min(0.1f)] private float jumpHeight = 1.4f;
[SerializeField, Min(0.1f)] private float gravity = 20f;
private CharacterController characterController;
private Vector3 verticalVelocity;
private bool isInitialized;
public void Initialize(PlayerFacade facade)
{
characterController = GetComponent<CharacterController>();
isInitialized = characterController != null;
}
public void SubmitMovement(Vector2 moveInput, bool wantsToSprint, bool wantsToJump)
{
if (!isInitialized || !IsOwner)
return;
SimulateMovement(moveInput, wantsToSprint, wantsToJump);
}
public void SubmitYaw(float yawDelta)
{
if (!isInitialized || !IsOwner)
return;
transform.Rotate(0f, yawDelta, 0f, Space.Self);
}
private void SimulateMovement(Vector2 moveInput, bool wantsToSprint, bool wantsToJump)
{
if (moveInput.sqrMagnitude > 1f)
moveInput.Normalize();
if (characterController.isGrounded && verticalVelocity.y < 0f)
verticalVelocity.y = -2f;
if (wantsToJump && characterController.isGrounded)
verticalVelocity.y = Mathf.Sqrt(jumpHeight * 2f * gravity);
verticalVelocity.y -= gravity * Time.deltaTime;
float speed = wantsToSprint ? sprintSpeed : walkSpeed;
Vector3 horizontalDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
characterController.Move((horizontalDirection * speed + verticalVelocity) * Time.deltaTime);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2df317ead1e2c824f8544bef0a049887
@@ -0,0 +1,74 @@
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
public sealed class PlayerIdentity : NetworkBehaviour, IPlayerSystem
{
private readonly NetworkVariable<FixedString64Bytes> displayName = new(
new FixedString64Bytes("Worker"),
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner);
private TextMesh nameLabel;
private bool isInitialized;
public string DisplayName => displayName.Value.ToString();
public void Initialize(PlayerFacade facade)
{
isInitialized = true;
}
public override void OnNetworkSpawn()
{
displayName.OnValueChanged += HandleNameChanged;
CreateNameLabel();
HandleNameChanged(default, displayName.Value);
if (IsOwner)
{
string localName = NetworkSessionController.Instance != null
? NetworkSessionController.Instance.LocalPlayerName
: "Worker";
displayName.Value = new FixedString64Bytes(localName);
}
}
public override void OnNetworkDespawn()
{
displayName.OnValueChanged -= HandleNameChanged;
}
private void LateUpdate()
{
if (!isInitialized || nameLabel == null || Camera.main == null)
return;
Vector3 cameraPosition = Camera.main.transform.position;
nameLabel.transform.rotation = Quaternion.LookRotation(nameLabel.transform.position - cameraPosition);
}
private void CreateNameLabel()
{
if (nameLabel != null)
return;
var labelObject = new GameObject("PlayerNameLabel");
labelObject.transform.SetParent(transform, false);
labelObject.transform.localPosition = new Vector3(0f, 2.25f, 0f);
labelObject.transform.localScale = Vector3.one * 0.12f;
nameLabel = labelObject.AddComponent<TextMesh>();
nameLabel.anchor = TextAnchor.MiddleCenter;
nameLabel.alignment = TextAlignment.Center;
nameLabel.fontSize = 48;
nameLabel.characterSize = 0.1f;
nameLabel.color = Color.white;
}
private void HandleNameChanged(FixedString64Bytes previousValue, FixedString64Bytes newValue)
{
if (nameLabel != null)
nameLabel.text = newValue.ToString();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 77d253756ae661e4a9ba738e9a4d8d86
@@ -0,0 +1,42 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(PlayerController), typeof(FirstPersonCameraController))]
public sealed class PlayerInputController : NetworkBehaviour, IPlayerSystem
{
[SerializeField, Min(0.001f)] private float mouseSensitivity = 0.08f;
private PlayerController playerController;
private FirstPersonCameraController cameraController;
private bool isInitialized;
public void Initialize(PlayerFacade facade)
{
playerController = GetComponent<PlayerController>();
cameraController = GetComponent<FirstPersonCameraController>();
isInitialized = playerController != null && cameraController != null;
}
private void Update()
{
if (!isInitialized || !IsOwner || Keyboard.current == null)
return;
Vector2 moveInput = new(
(Keyboard.current.dKey.isPressed ? 1f : 0f) - (Keyboard.current.aKey.isPressed ? 1f : 0f),
(Keyboard.current.wKey.isPressed ? 1f : 0f) - (Keyboard.current.sKey.isPressed ? 1f : 0f));
playerController.SubmitMovement(
moveInput,
Keyboard.current.leftShiftKey.isPressed,
Keyboard.current.spaceKey.wasPressedThisFrame);
if (Mouse.current == null)
return;
Vector2 mouseDelta = Mouse.current.delta.ReadValue() * mouseSensitivity;
playerController.SubmitYaw(mouseDelta.x);
cameraController.ApplyPitch(mouseDelta.y);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3b45058d0c34be841ad35cce7370cbb7
@@ -0,0 +1,70 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSystem
{
[SerializeField, Min(0.1f)] private float interactionDistance = 3f;
[SerializeField] private LayerMask interactionMask = ~0;
private Camera playerCamera;
private IInteractable focusedInteractable;
private bool isInitialized;
public void Initialize(PlayerFacade facade)
{
playerCamera = GetComponentInChildren<Camera>(true);
isInitialized = playerCamera != null;
}
private void Update()
{
if (!isInitialized || !IsOwner || Keyboard.current == null)
return;
focusedInteractable = FindFocusedInteractable();
if (Keyboard.current.eKey.wasPressedThisFrame &&
focusedInteractable != null &&
focusedInteractable.IsInteractionAvailable)
{
focusedInteractable.RequestInteraction();
}
}
private IInteractable FindFocusedInteractable()
{
Ray ray = new(playerCamera.transform.position, playerCamera.transform.forward);
if (!Physics.Raycast(ray, out RaycastHit hit, interactionDistance, interactionMask, QueryTriggerInteraction.Collide))
return null;
foreach (MonoBehaviour component in hit.collider.GetComponentsInParent<MonoBehaviour>())
{
if (component is IInteractable interactable)
return interactable;
}
return null;
}
private void OnGUI()
{
if (!IsOwner || focusedInteractable == null)
return;
GUIStyle style = new(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 16,
normal = { textColor = Color.white }
};
string prompt = focusedInteractable.IsInteractionAvailable
? "[E] " + focusedInteractable.InteractionPrompt
: "In use";
Rect area = new(0f, Screen.height * 0.62f, Screen.width, 28f);
GUI.Label(area, prompt, style);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0cd5aa652e2f0b145ac4579b39076584
@@ -0,0 +1,43 @@
using Unity.Netcode;
using UnityEngine;
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;
private readonly NetworkVariable<Vector3> assignedSpawnPosition = new(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public void Initialize(PlayerFacade facade) { }
public override void OnNetworkSpawn()
{
assignedSpawnPosition.OnValueChanged += HandleSpawnPositionChanged;
if (IsServer)
{
Vector2 randomOffset = Random.insideUnitCircle * spawnRadius;
assignedSpawnPosition.Value = spawnCenter + new Vector3(randomOffset.x, 0f, randomOffset.y);
}
ApplySpawnPosition(assignedSpawnPosition.Value);
}
public override void OnNetworkDespawn()
{
assignedSpawnPosition.OnValueChanged -= HandleSpawnPositionChanged;
}
private void HandleSpawnPositionChanged(Vector3 _, Vector3 position) =>
ApplySpawnPosition(position);
private void ApplySpawnPosition(Vector3 position)
{
if (IsOwner)
transform.position = position;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb5685deb3f0e014583330890079ebcd
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 573f6124361b715449d31115ecff1db8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
using System;
using Unity.Netcode;
using UnityEngine;
public enum RoundPhase : byte
{
WaitingForPlayers,
DayInProgress,
WaitingForPlayersToReturn
}
[RequireComponent(typeof(NetworkObject))]
public sealed class RoundGameLoopController : NetworkBehaviour
{
[Header("Day")]
[SerializeField, Min(1f)] private float dayDurationSeconds = 300f;
[SerializeField, Min(0.05f)] private float timerSyncInterval = 0.1f;
private readonly NetworkVariable<RoundPhase> phase = new(
RoundPhase.WaitingForPlayers,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> dayNumber = new(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<float> remainingDaySeconds = new(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private StartRoomZone startRoom;
private float dayEndTime;
private float nextTimerSyncTime;
public RoundPhase Phase => phase.Value;
public int DayNumber => dayNumber.Value;
public float RemainingDaySeconds => remainingDaySeconds.Value;
public event Action<RoundPhase> PhaseChanged;
public event Action<int> DayStarted;
public event Action<float> DayTimerChanged;
public override void OnNetworkSpawn()
{
phase.OnValueChanged += HandlePhaseChanged;
dayNumber.OnValueChanged += HandleDayNumberChanged;
remainingDaySeconds.OnValueChanged += HandleTimerChanged;
startRoom = FindFirstObjectByType<StartRoomZone>();
if (IsServer)
{
phase.Value = RoundPhase.WaitingForPlayers;
remainingDaySeconds.Value = 0f;
}
}
public override void OnNetworkDespawn()
{
phase.OnValueChanged -= HandlePhaseChanged;
dayNumber.OnValueChanged -= HandleDayNumberChanged;
remainingDaySeconds.OnValueChanged -= HandleTimerChanged;
}
private void Update()
{
if (!IsServer)
return;
if (startRoom == null)
startRoom = FindFirstObjectByType<StartRoomZone>();
if (startRoom == null || Time.time < nextTimerSyncTime)
return;
nextTimerSyncTime = Time.time + timerSyncInterval;
switch (phase.Value)
{
case RoundPhase.WaitingForPlayers:
case RoundPhase.WaitingForPlayersToReturn:
if (AreAllPlayersInStartRoom())
StartNextDay();
break;
case RoundPhase.DayInProgress:
float secondsLeft = Mathf.Max(0f, dayEndTime - Time.time);
remainingDaySeconds.Value = secondsLeft;
if (secondsLeft <= 0f)
{
phase.Value = RoundPhase.WaitingForPlayersToReturn;
remainingDaySeconds.Value = 0f;
}
break;
}
}
private bool AreAllPlayersInStartRoom()
{
if (NetworkManager == null || NetworkManager.ConnectedClients.Count == 0)
return false;
foreach (NetworkClient client in NetworkManager.ConnectedClients.Values)
{
if (client.PlayerObject == null || !startRoom.Contains(client.PlayerObject.transform.position))
return false;
}
return true;
}
private void StartNextDay()
{
dayNumber.Value++;
dayEndTime = Time.time + dayDurationSeconds;
remainingDaySeconds.Value = dayDurationSeconds;
phase.Value = RoundPhase.DayInProgress;
}
private void HandlePhaseChanged(RoundPhase _, RoundPhase current) =>
PhaseChanged?.Invoke(current);
private void HandleDayNumberChanged(int _, int current) =>
DayStarted?.Invoke(current);
private void HandleTimerChanged(float _, float current) =>
DayTimerChanged?.Invoke(current);
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 520cf065a136e624c9eba714e60891c2
@@ -0,0 +1,26 @@
using UnityEngine;
[RequireComponent(typeof(Collider))]
public sealed class StartRoomZone : MonoBehaviour
{
[SerializeField] private Collider roomCollider;
private void Awake()
{
if (roomCollider == null)
roomCollider = GetComponent<Collider>();
}
public bool Contains(Vector3 worldPosition) =>
roomCollider != null && roomCollider.bounds.Contains(worldPosition);
private void OnDrawGizmosSelected()
{
Collider targetCollider = roomCollider != null ? roomCollider : GetComponent<Collider>();
if (targetCollider == null)
return;
Gizmos.color = new Color(0.2f, 0.75f, 1f, 0.22f);
Gizmos.DrawCube(targetCollider.bounds.center, targetCollider.bounds.size);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c25f56fa92e0cc1438d12b2ebd5f0b96
@@ -0,0 +1,23 @@
using UnityEngine;
public sealed class FactoryLobbyCodeView : MonoBehaviour
{
private void OnGUI()
{
NetworkSessionController session = NetworkSessionController.Instance;
if (session == null || !session.IsOnline) return;
GUI.Box(new Rect(24, 24, 340, 112), string.Empty);
GUILayout.BeginArea(new Rect(42, 42, 304, 76));
if (session.IsHost)
{
GUILayout.Label("LOBBY CODE");
GUILayout.Label(string.IsNullOrEmpty(session.JoinCode) ? "Generating..." : session.JoinCode);
}
else
{
GUILayout.Label("CONNECTED TO FACTORY");
}
GUILayout.EndArea();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39ec4765e311f5f4c989834e9d9055d2
@@ -1,24 +1,75 @@
using Unity.Services.Lobbies.Models;
using UnityEngine;
public sealed class MainMenuController : MonoBehaviour
{
private string address = "127.0.0.1";
private string portText = "7777";
private string joinCode = string.Empty;
private string playerName = string.Empty;
private Vector2 lobbyScrollPosition;
private void OnGUI()
{
NetworkSessionController session = NetworkSessionController.Instance;
GUI.Box(new Rect(24, 24, 380, 285), string.Empty);
GUILayout.BeginArea(new Rect(44, 44, 340, 245));
GUILayout.Label("FRIENDSLOP // MOUNTAIN FACTORY"); GUILayout.Space(10);
if (session == null) { GUILayout.Label("Bootstrap is unavailable."); GUILayout.EndArea(); return; }
if (session.IsOnline) { GUILayout.Label(session.Status); GUILayout.EndArea(); return; }
GUILayout.Label("Host LAN IP"); address = GUILayout.TextField(address);
GUILayout.Label("Port"); portText = GUILayout.TextField(portText); GUILayout.Space(12);
if (GUILayout.Button("Start Host", GUILayout.Height(36))) StartHost(session);
if (GUILayout.Button("Join Factory", GUILayout.Height(36))) StartClient(session);
GUILayout.FlexibleSpace(); GUILayout.Label(session.Status); GUILayout.EndArea();
GUI.Box(new Rect(24, 24, 480, 620), string.Empty);
GUILayout.BeginArea(new Rect(44, 44, 440, 580));
GUILayout.Label("FRIENDSLOP // MOUNTAIN FACTORY");
GUILayout.Space(10);
if (session == null)
{
GUILayout.Label("Network bootstrap is unavailable.");
GUILayout.EndArea();
return;
}
if (session.IsOnline)
{
GUILayout.Label(session.Status);
GUILayout.EndArea();
return;
}
if (string.IsNullOrEmpty(playerName))
playerName = session.LocalPlayerName;
GUILayout.Label("Player name");
playerName = GUILayout.TextField(playerName, 24);
session.SetLocalPlayerName(playerName);
GUILayout.Space(10);
GUI.enabled = !session.IsBusy;
if (GUILayout.Button("Create Relay Host", GUILayout.Height(34)))
_ = session.StartHostAsync();
GUILayout.Space(12);
GUILayout.Label("Join by code");
joinCode = GUILayout.TextField(joinCode).ToUpperInvariant();
if (GUILayout.Button("Join via Relay", GUILayout.Height(30)))
_ = session.StartClientAsync(joinCode);
GUILayout.Space(14);
GUILayout.BeginHorizontal();
GUILayout.Label("Open factories");
if (GUILayout.Button("Refresh", GUILayout.Width(90)))
_ = session.RefreshPublicLobbiesAsync();
GUILayout.EndHorizontal();
lobbyScrollPosition = GUILayout.BeginScrollView(lobbyScrollPosition, GUILayout.Height(220));
foreach (Lobby lobby in session.PublicLobbies)
{
GUILayout.BeginHorizontal(GUI.skin.box);
GUILayout.Label($"{lobby.Name} {lobby.Players.Count}/{lobby.MaxPlayers}", GUILayout.ExpandWidth(true));
GUI.enabled = !session.IsBusy && lobby.AvailableSlots > 0;
if (GUILayout.Button("Join", GUILayout.Width(70)))
_ = session.JoinPublicLobbyAsync(lobby.Id);
GUI.enabled = !session.IsBusy;
GUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
GUILayout.FlexibleSpace();
GUILayout.Label(session.Status);
GUI.enabled = true;
GUILayout.EndArea();
}
private void StartHost(NetworkSessionController session) { if (TryReadPort(out ushort port)) session.StartHost(port); }
private void StartClient(NetworkSessionController session) { if (TryReadPort(out ushort port) && !string.IsNullOrWhiteSpace(address)) session.StartClient(address, port); }
private bool TryReadPort(out ushort port) => ushort.TryParse(portText, out port) && port > 0;
}
}