This commit is contained in:
2026-08-10 19:23:04 +02:00
commit 404d15a56c
134 changed files with 5374 additions and 0 deletions
@@ -0,0 +1,49 @@
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Networking
{
/// <summary>
/// Einstiegspunkt für Host/Join. Bewusst minimal Steam-Lobbies kommen
/// später über Facepunch.Steamworks dazu (siehe docs/ROADMAP.md).
/// </summary>
public sealed class NetworkBootstrap : MonoBehaviour
{
public const int MaxPlayers = 4;
[SerializeField] private string _gameSceneName = "Shop";
public void HostGame()
{
NetworkManager.Singleton.ConnectionApprovalCallback = ApproveConnection;
if (!NetworkManager.Singleton.StartHost())
{
Debug.LogError("[NetworkBootstrap] Host konnte nicht gestartet werden.");
return;
}
NetworkManager.Singleton.SceneManager.LoadScene(
_gameSceneName, UnityEngine.SceneManagement.LoadSceneMode.Single);
}
public void JoinGame()
{
if (!NetworkManager.Singleton.StartClient())
Debug.LogError("[NetworkBootstrap] Client konnte nicht verbinden.");
}
public void Leave() => NetworkManager.Singleton.Shutdown();
private void ApproveConnection(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
bool hasRoom = NetworkManager.Singleton.ConnectedClientsIds.Count < MaxPlayers;
response.Approved = hasRoom;
response.CreatePlayerObject = true;
response.Reason = hasRoom ? null : "Der Laden ist voll (max. 4 Angestellte).";
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3f7b7e91ebc572543a8324413a34c7a9
+71
View File
@@ -0,0 +1,71 @@
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Networking
{
/// <summary>
/// Spielerbewegung mit Client-Prediction: der Owner bewegt sich sofort,
/// NetworkTransform repliziert. Bewegung ist der EINZIGE Bereich, in dem
/// wir dem Client vertrauen siehe CLAUDE.md §3.
/// </summary>
[RequireComponent(typeof(CharacterController))]
public sealed class PlayerAvatar : NetworkBehaviour
{
[SerializeField] private float _walkSpeed = 3.4f;
[SerializeField] private float _mouseSensitivity = 2f;
[SerializeField] private Transform _cameraRoot;
[SerializeField] private float _gravity = -12f;
private CharacterController _controller;
private float _pitch;
private float _verticalVelocity;
private void Awake() => _controller = GetComponent<CharacterController>();
public override void OnNetworkSpawn()
{
if (!IsOwner)
{
if (_cameraRoot != null && _cameraRoot.GetComponentInChildren<Camera>() is { } cam)
cam.gameObject.SetActive(false);
enabled = false;
return;
}
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
if (!IsOwner) return;
Look();
Move();
}
private void Look()
{
float mouseX = Input.GetAxis("Mouse X") * _mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * _mouseSensitivity;
transform.Rotate(Vector3.up, mouseX);
_pitch = Mathf.Clamp(_pitch - mouseY, -85f, 85f);
if (_cameraRoot != null)
_cameraRoot.localRotation = Quaternion.Euler(_pitch, 0f, 0f);
}
private void Move()
{
Vector3 input = new(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
Vector3 direction = transform.TransformDirection(Vector3.ClampMagnitude(input, 1f));
_verticalVelocity = _controller.isGrounded
? -1f
: _verticalVelocity + _gravity * Time.deltaTime;
Vector3 velocity = direction * _walkSpeed + Vector3.up * _verticalVelocity;
_controller.Move(velocity * Time.deltaTime);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f88015bc4effd5b429972c480fabe831
+57
View File
@@ -0,0 +1,57 @@
using Unity.Netcode;
using UnityEngine;
namespace AfterHours.Networking
{
/// <summary>
/// Was der Spieler gerade trägt. Server-authoritativ der Client darf
/// weder Produkt noch Menge selbst setzen.
/// </summary>
public sealed class PlayerHands : NetworkBehaviour
{
public const int MaxCarry = 12;
private readonly NetworkVariable<int> _productId = new(-1);
private readonly NetworkVariable<int> _quantity = new(0);
public int ProductId => _productId.Value;
public int Quantity => _quantity.Value;
public bool IsEmpty => _quantity.Value <= 0;
/// <summary>Serverseitig: nimmt so viel wie möglich auf, gibt die tatsächliche Menge zurück.</summary>
public int TryTake(int productId, int quantity)
{
if (!IsServer) return 0;
if (quantity <= 0) return 0;
if (!IsEmpty && _productId.Value != productId)
return 0; // Hände sind mit etwas anderem belegt
int free = MaxCarry - _quantity.Value;
int taken = Mathf.Min(free, quantity);
if (taken <= 0) return 0;
_productId.Value = productId;
_quantity.Value += taken;
return taken;
}
/// <summary>Serverseitig: gibt bis zu <paramref name="quantity"/> ab.</summary>
public int TryGive(int quantity)
{
if (!IsServer || IsEmpty) return 0;
int given = Mathf.Min(_quantity.Value, quantity);
_quantity.Value -= given;
if (_quantity.Value == 0) _productId.Value = -1;
return given;
}
public void Clear()
{
if (!IsServer) return;
_productId.Value = -1;
_quantity.Value = 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 402647b9bce9a6a4481b75e1004e3bfb