using UnityEngine; /// /// 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 /// [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(); _audio = GetComponent(); _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; }