340 lines
13 KiB
C#
340 lines
13 KiB
C#
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;
|
|
} |