This commit is contained in:
2026-04-10 18:03:49 +03:00
commit 068966e049
2179 changed files with 564568 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
using UnityEngine;
/// <summary>
/// FPS Player Movement Controller
/// ───────────────────────────────
/// SETUP (přečti před použitím!):
///
/// 1. Na Player GameObject přidej:
/// • CharacterController (Height: 1.8, Center Y: 0.9, Radius: 0.4)
/// • Tento script
/// • AudioSource (Loop: OFF, Play On Awake: OFF)
///
/// 2. Struktura hierarchie:
/// Player (tento script zde)
/// └── Camera Holder (prázdný GO, local pos 0,0,0)
/// └── Main Camera (local pos 0, 0.75, 0)
///
/// 3. V Inspectoru přiřaď:
/// • cameraHolder → "Camera Holder" GameObject
/// • GROUND CHECK: NECH PRÁZDNÝ — script ho vytvoří automaticky!
///
/// 4. Ground Mask:
/// • Vytvoř Layer "Ground" (Edit → Project Settings → Tags & Layers)
/// • Přiřaď ho svým podlahám/terénu
/// • V Inspectoru tohoto scriptu vyber "Ground" v Ground Mask
///
/// 5. Footstepy (volitelné):
/// • Přidej AudioClipy do Walk Sounds pole
/// • Bez clipů script funguje normálně, jen tiše
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
// ───────── MOUSE LOOK ─────────
[Header("Mouse Look")]
[Tooltip("GameObject který se otáčí nahoru/dolů (obsahuje kameru)")]
public Transform cameraHolder;
[Tooltip("Citlivost myši")]
public float mouseSensitivity = 2f;
[Tooltip("Maximální úhel pohledu nahoru/dolů")]
public float verticalClamp = 80f;
[Tooltip("Invertovat vertikální osu myši")]
public bool invertY = false;
// ───────── POHYB ─────────
[Header("Movement")]
public float walkSpeed = 3.5f;
public float sprintSpeed = 7.0f;
public float crouchSpeed = 1.8f;
[Tooltip("Plynulost rozběhu (nižší = plynulejší)")]
public float smoothTime = 0.08f;
// ───────── SKOK & GRAVITACE ─────────
[Header("Jump & Gravity")]
public float jumpHeight = 1.2f;
public float gravity = -20f;
// ───────── PODŘEP ─────────
[Header("Crouch")]
public float standHeight = 1.8f;
public float crouchHeight = 0.9f;
public float crouchTransitionSpeed = 10f;
[Tooltip("Výška cameraHolderu ve stoje")]
public float cameraStandY = 0.75f;
[Tooltip("Výška cameraHolderu v podřepu")]
public float cameraCrouchY = 0.2f;
// ───────── GROUND CHECK ─────────
[Header("Ground Check")]
[Tooltip("Necháš-li prázdné, script vytvoří GroundCheck automaticky")]
public Transform groundCheck;
public float groundCheckRadius = 0.3f;
public LayerMask groundMask;
// ───────── ZVUKY ─────────
[Header("Footstep Sounds")]
public AudioClip[] walkSounds;
public AudioClip[] sprintSounds;
public AudioClip jumpSound;
public AudioClip landSound;
[Range(0f, 1f)]
public float footstepVolume = 0.5f;
[Tooltip("Jak často hrají kroky při chůzi (v sekundách)")]
public float walkStepInterval = 0.45f;
public float sprintStepInterval = 0.28f;
public float crouchStepInterval = 0.65f;
// ───────── PRIVÁTNÍ ─────────
private CharacterController _cc;
private AudioSource _audio;
// Mouse look
private float _xRotation = 0f;
// Pohyb
private Vector3 _velocity;
private Vector2 _currentInput;
private Vector2 _smoothVel;
// Stavy
private bool _isGrounded;
private bool _wasGrounded;
private bool _isCrouching;
private bool _isSprinting;
// Footstep timer
private float _footstepTimer = 0f;
// ══════════════════════════════════════════════
private void Awake()
{
_cc = GetComponent<CharacterController>();
_audio = GetComponent<AudioSource>();
_cc.height = standHeight;
_cc.center = new Vector3(0f, standHeight / 2f, 0f);
// Automaticky vytvoř GroundCheck pokud chybí
if (groundCheck == null)
{
GameObject gc = new GameObject("GroundCheck");
gc.transform.SetParent(transform);
gc.transform.localPosition = new Vector3(0f, -standHeight / 2f + 0.05f, 0f);
groundCheck = gc.transform;
}
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
HandleMouseLook();
CheckGround();
HandleMovement();
HandleJump();
ApplyGravity();
HandleCrouch();
HandleFootsteps();
// ESC = odemkni kurzor
if (Input.GetKeyDown(KeyCode.Escape))
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
if (Input.GetMouseButtonDown(0) && Cursor.lockState != CursorLockMode.Locked)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
// ══════════════════════════════════════════════
// MOUSE LOOK
// ══════════════════════════════════════════════
private void HandleMouseLook()
{
if (Cursor.lockState != CursorLockMode.Locked) return;
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * (invertY ? 1f : -1f);
// Horizontálně otáčíme celým Playerem
transform.Rotate(Vector3.up * mouseX);
// Vertikálně otáčíme jen Camera Holderem
_xRotation = Mathf.Clamp(_xRotation + mouseY, -verticalClamp, verticalClamp);
if (cameraHolder != null)
cameraHolder.localRotation = Quaternion.Euler(_xRotation, 0f, 0f);
}
// ══════════════════════════════════════════════
// GROUND CHECK
// ══════════════════════════════════════════════
private void CheckGround()
{
_wasGrounded = _isGrounded;
_isGrounded = Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask);
if (_isGrounded && !_wasGrounded)
OnLand();
}
// ══════════════════════════════════════════════
// POHYB
// ══════════════════════════════════════════════
private void HandleMovement()
{
_isSprinting = (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
&& !_isCrouching;
float targetSpeed = _isCrouching ? crouchSpeed
: _isSprinting ? sprintSpeed
: walkSpeed;
Vector2 rawInput = new Vector2(
Input.GetAxisRaw("Horizontal"),
Input.GetAxisRaw("Vertical")
).normalized;
_currentInput = Vector2.SmoothDamp(_currentInput, rawInput, ref _smoothVel, smoothTime);
Vector3 move = transform.right * _currentInput.x
+ transform.forward * _currentInput.y;
_cc.Move(move * targetSpeed * Time.deltaTime);
}
// ══════════════════════════════════════════════
// SKOK
// ══════════════════════════════════════════════
private void HandleJump()
{
if (Input.GetButtonDown("Jump") && _isGrounded)
{
_velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
PlayOneShot(jumpSound, footstepVolume);
if (_isCrouching) _isCrouching = false;
}
}
// ══════════════════════════════════════════════
// GRAVITACE
// ══════════════════════════════════════════════
private void ApplyGravity()
{
if (_isGrounded && _velocity.y < 0f)
_velocity.y = -2f;
else
_velocity.y += gravity * Time.deltaTime;
_cc.Move(_velocity * Time.deltaTime);
}
// ══════════════════════════════════════════════
// PODŘEP
// ══════════════════════════════════════════════
private void HandleCrouch()
{
bool wantCrouch = Input.GetKey(KeyCode.LeftControl)
|| Input.GetKey(KeyCode.RightControl)
|| Input.GetKey(KeyCode.C);
if (wantCrouch)
_isCrouching = true;
else if (_isCrouching && !CeilingAbove())
_isCrouching = false;
float targetH = _isCrouching ? crouchHeight : standHeight;
_cc.height = Mathf.MoveTowards(_cc.height, targetH, crouchTransitionSpeed * Time.deltaTime);
_cc.center = new Vector3(0f, _cc.height / 2f, 0f);
groundCheck.localPosition = new Vector3(0f, -_cc.height / 2f + 0.05f, 0f);
if (cameraHolder != null)
{
float targetCamY = _isCrouching ? cameraCrouchY : cameraStandY;
Vector3 lp = cameraHolder.localPosition;
cameraHolder.localPosition = new Vector3(
lp.x,
Mathf.MoveTowards(lp.y, targetCamY, crouchTransitionSpeed * Time.deltaTime),
lp.z
);
}
}
private bool CeilingAbove()
{
Vector3 top = transform.position + Vector3.up * crouchHeight;
return Physics.SphereCast(top, _cc.radius * 0.9f, Vector3.up,
out _, standHeight - crouchHeight + 0.1f, groundMask);
}
// ══════════════════════════════════════════════
// FOOTSTEPY
// ══════════════════════════════════════════════
private void HandleFootsteps()
{
float horizontalSpeed = new Vector2(_cc.velocity.x, _cc.velocity.z).magnitude;
if (!_isGrounded || horizontalSpeed < 0.5f)
{
_footstepTimer = 0f;
return;
}
float interval = _isSprinting ? sprintStepInterval
: _isCrouching ? crouchStepInterval
: walkStepInterval;
_footstepTimer += Time.deltaTime;
if (_footstepTimer >= interval)
{
_footstepTimer = 0f;
PlayRandomFootstep();
}
}
private void PlayRandomFootstep()
{
AudioClip[] pool = (_isSprinting && sprintSounds != null && sprintSounds.Length > 0)
? sprintSounds
: walkSounds;
if (pool != null && pool.Length > 0)
PlayOneShot(pool[Random.Range(0, pool.Length)], footstepVolume);
}
private void OnLand()
{
_footstepTimer = 0f;
PlayOneShot(landSound, footstepVolume * 1.2f);
}
private void PlayOneShot(AudioClip clip, float vol)
{
if (clip != null && _audio != null)
_audio.PlayOneShot(clip, vol);
}
// ══════════════════════════════════════════════
// DEBUG GIZMOS
// ══════════════════════════════════════════════
private void OnDrawGizmosSelected()
{
if (groundCheck == null) return;
Gizmos.color = _isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
// ══════════════════════════════════════════════
// PUBLIC API
// ══════════════════════════════════════════════
public bool IsGrounded => _isGrounded;
public bool IsSprinting => _isSprinting;
public bool IsCrouching => _isCrouching;
public float CurrentSpeed => new Vector2(_cc.velocity.x, _cc.velocity.z).magnitude;
}