Hello. I have a question. I’m using AI to write code (I know it causes errors, but I don’t know where else to find what I need). The problem is that when a character is on a slope, regardless of the angle set in maxWalkableSlope and startSlideAngle, he slides only at 40 degrees. And when he tries to climb a slope when that angle is 40 degrees or higher, he bounces and shoots himself backward.
using UnityEngine;
using UnityEngine.InputSystem;
public enum SlopeDirection
{
None,
DownForward,
DownBackward,
DownLeft,
DownRight
}
public enum MovementState
{
Idle, // Postać stoi w miejscu, brak ruchu
Walking, // Postać porusza się powoli, np. trzymając Ctrl
Running, // Postać porusza się normalnie, bez dodatkowych klawiszy
Sprinting, // Postać porusza się szybko, np. trzymając Shift
Jumping // Postać jest w powietrzu po wykonaniu skoku
}
public enum SlopeState
{
Flat, // 0-10° - płasko
SlightDown, // 10-25° - lekki spadek
SteepDown, // 25-40° - stromy spadek
SlideDown, // 40-55° - ślizg w dół
VerySteep, // 55-65° - bardzo stroma ściana
NearVertical, // 65-80° - prawie pionowo
VerticalWall // 80-90° - ściana
}
public enum Condition
{
Standing, // Postać stoi na ziemi, gotowa do ruchu
Airborne, // Postać jest w powietrzu, np. po skoku lub podczas spadania
Climbing // (opcjonalnie) Postać wspina się po ścianie lub drabinie
}
public enum GroundedState
{
Grounded, // Postać jest na ziemi, bezpieczna
SafeFall, // Postać spada z niewielkiej wysokości, bez ryzyka obrażeń
SoftFall, // Postać spada z umiarkowanej wysokości, może odczuć lekki dyskomfort
NormalFall, // Postać spada z dużej wysokości, ryzyko obrażeń
HardFall // Postać spada z bardzo dużej wysokości, ryzyko poważnych obrażeń lub śmierci
}
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(Gravity))]
public class CharacterMovement : MonoBehaviour
{
// ------------------------
// Komponenty
// ------------------------
private PlayerInput _playerInput;
private CharacterController _characterController;
private Animator _animator;
private Gravity _gravity;
// ------------------------
// Input
// ------------------------
private Vector2 _moveInput; // aktualny wektor wejścia z klawiatury (X = strafe, Y = forward)
private bool _sprintInput;
private bool _walkInput;
// ------------------------
// Movement Settings
// ------------------------
[Header("Movement Settings")]
[Tooltip("Podstawowa prędkość poruszania się postaci (w metrach na sekundę)")]
public float baseSpeed = 2f;
[Tooltip("Mnożnik prędkości podczas biegu, im wyższa wartość, tym szybszy bieg.")]
public float sprintMultiplier = 1.5f;
[Tooltip("Mnożnik prędkości podczas chodzenia, im wyższa wartość, tym szybsze chodzenie.")]
public float walkMultiplier = 0.75f;
[Tooltip("Mnożnik przyspieszenia, im wyższa wartość, tym szybsze przyspieszenie postaci")]
public float acceleration = 10f;
[Tooltip("Mnożnik hamowania, im wyższa wartość, tym szybsze zatrzymanie postaci")]
public float deceleration = 15f;
[Tooltip("Czy postać powinna poruszać się z normalizowaną prędkością podczas ruchu diagonalnego. Jeśli jest ustawione na true, prędkość podczas ruchu diagonalnego będzie taka sama jak podczas ruchu w jednym kierunku.")]
public bool useNormalizedDiagonalMovement = true;
[Header("Directional Speed Modifiers")]
[Tooltip("Mnożnik prędkości podczas ruchu w bok (strafe)")]
[Range(0.1f, 1f)] public float strafeSpeedMultiplier = 0.8f;
[Tooltip("Mnożnik prędkości podczas cofania się")]
[Range(0.1f, 1f)] public float backwardSpeedMultiplier = 0.6f;
// ------------------------
// Slope Settings
// ------------------------
[Header("Slope & Sliding")]
[Tooltip("Maksymalny kąt, na którym postać jeszcze normalnie chodzi (0-90°)")]
public float maxWalkableSlope = 40f; // do SlideDown
[Tooltip("Kąt, od którego postać zaczyna się ślizgać (0-90°)")]
public float startSlideAngle = 40f; // od tego kąta zaczyna się ślizg
[Tooltip("Prędkość ślizgu na stromych stokach")]
[Range(3f, 15f)] public float slideSpeed = 8f;
[Tooltip("Maksymalna prędkość na bardzo stromych powierzchniach")]
[Range(8f, 25f)] public float maxSlideSpeed = 15f;
private SlopeState currentSlopeState;
private bool isSliding;
private float currentSlopeAngle;
private Vector3 slideVelocity;
// ------------------------
// Jump Settings
// ------------------------
[Header("Jump Settings")]
[Tooltip("Wysokość skoku, im wyższa wartość, tym wyższy skok.(w metrach)")]
public float jumpHeight = 1.5f;
[Range(0f, 1f)]
[Tooltip("Mnożnik kontroli nad postacią podczas lotu, im niższa wartość, tym mniejsza kontrola.")]
public float airboneControlMultiplier = 0.5f;
[Tooltip("Czas, w którym gracz może jeszcze skoczyć po zejściu z krawędzi (w sekundach)")]
public float coyoteTime = 0.2f;
[Tooltip("Czas, w którym gracz nie może ponownie skoczyć po wykonaniu skoku i opadnięciu na ziemię (w sekundach)")]
public float jumpCooldown = 0.5f;
[Header("Fall Thresholds")]
[Tooltip("Wysokość od jakiej następuje przejście od spadku bezpiecznego do spadku delikatnego (w metrach)")]
public float softGroundedDistance = 1f;
[Tooltip("Wysokość od jakiej następuje przejście od spadku delikatnego do spadku normalnego (w metrach)")]
public float normalGroundedDistance = 3f;
[Tooltip("Wysokość od jakiej następuje przejście od spadku normalnego do spadku twardego (w metrach)")]
public float hardGroundedDistance = 6f;
// ------------------------
// Runtime State
// ------------------------
private MovementState currentMovementState = MovementState.Idle;
private Condition currentCondition = Condition.Standing;
private GroundedState currentGroundedState = GroundedState.Grounded;
private SlopeDirection currentSlopeDirection = SlopeDirection.None;
private float coyoteTimer;
private float jumpCooldownTimer;
private float fallStartHeight;
private bool wasGrounded;
private Vector3 velocity; // aktualny wektor ruchu
private float currentSpeed; // prędkość w płaszczyźnie XZ
private float fallStateTimer = 0f; // Timer, który kontroluje jak długo stan spadku jest aktywny po lądowaniu
[Tooltip("Jak długo stan spadku (HardFall, NormalFall itd.) ma być widoczny w Animatorze")]
public float fallStateDisplayTime = 0.65f; // 0.65s = wystarczająco na większość animacji lądowania
private Vector3 groundNormal = Vector3.up;
// ------------------------
// Public Read-Only Access
// ------------------------
public MovementState MoveState => currentMovementState;
public Condition CurrentCondition => currentCondition;
public GroundedState GroundState => currentGroundedState;
public SlopeDirection SlopeDirection => currentSlopeDirection;
public bool IsGrounded => _characterController.isGrounded;
public bool IsIdle => currentMovementState == MovementState.Idle;
public bool IsWalking => currentMovementState == MovementState.Walking;
public bool IsRunning => currentMovementState == MovementState.Running;
public SlopeState CurrentSlopeState => currentSlopeState;
public bool IsSliding => isSliding;
public float SlopeAngle => currentSlopeAngle;
// ------------------------
// Unity
// ------------------------
private void Awake()
{
_playerInput = GetComponent<PlayerInput>();
_characterController = GetComponent<CharacterController>();
_gravity = GetComponent<Gravity>();
// Szukamy Animatora tylko w dziecku "Body", żeby nie mieszać go z innymi Animatorami (np. na broni)
Transform animatorChild = transform.Find("Body");
if (animatorChild != null)
_animator = animatorChild.GetComponent<Animator>();
if (_animator == null)
Debug.LogWarning("Animator not found on Body child!", this);
_characterController.slopeLimit = 89.5f;
_characterController.skinWidth = 0.1f; // zwiększ trochę
_characterController.stepOffset = 0.3f;
}
private void Update()
{
// 1. Dynamiczne parametry CC
if (isSliding)
{
_characterController.stepOffset = 0f;
_characterController.slopeLimit = 89f;
// _characterController.skinWidth = Mathf.Lerp(_characterController.skinWidth, 0.12f, 10f * Time.deltaTime);
}
else
{
_characterController.stepOffset = 0.3f;
_characterController.slopeLimit = 89.5f;
// _characterController.skinWidth = Mathf.Lerp(_characterController.skinWidth, 0.08f, 10f * Time.deltaTime);
}
// 2. Input i timery
ReadInput();
UpdateTimers();
// 3. Wykrywanie stoku – musi być przed ruchem
DetectSlopeAndSliding();
// 4. Ruch – tu dzieje się magia
HandleMovement();
// 5. Skok (po ruchu – bo velocity.y może być nadpisane)
HandleJump();
// 6. Śledzenie spadania i lądowania
UpdateFallTracking();
// 7. Aktualizacja stanów (Idle/Running/Sprinting/Airborne)
UpdateStates();
// 8. Animator na samym końcu – widzi już wszystko
UpdateAnimator();
// ────────────────────────────────────────────────
// Opcjonalne debugowanie – włącz na czas testów
// ────────────────────────────────────────────────
if (isSliding)
{
Debug.DrawRay(transform.position, groundNormal * 2f, Color.magenta, 0.1f);
Debug.DrawRay(transform.position, velocity.normalized * 2f, Color.yellow, 0.1f);
}
}
// ------------------------
// Input
// ------------------------
private void ReadInput()
{
_moveInput = _playerInput.actions["Move"].ReadValue<Vector2>();
_sprintInput = _playerInput.actions["Sprint"].IsPressed();
_walkInput = _playerInput.actions["Walk"].IsPressed();
}
private void UpdateTimers()
{
// Coyote time – bez zmian
// Coyote timer odlicza się tylko wtedy, gdy postać jest w powietrzu. Gdy postać jest na ziemi, timer jest resetowany do wartości coyoteTime.
if (_characterController.isGrounded)
{
// Reset coyote timer, gdy postać jest na ziemi
coyoteTimer = coyoteTime;
}
else
{
// Odliczanie coyote timer, gdy postać jest w powietrzu
coyoteTimer -= Time.deltaTime;
}
// Zapewnienie, że coyoteTimer nigdy nie spadnie poniżej 0
coyoteTimer = Mathf.Max(coyoteTimer, 0f);
// jumpCooldownTimer odlicza się ZAWSZE (w powietrzu i na ziemi)
if (jumpCooldownTimer > 0f)
{
// Odliczanie jump cooldown timer niezależnie od stanu postaci
jumpCooldownTimer -= Time.deltaTime;
}
// Zapewnienie, że jumpCooldownTimer nigdy nie spadnie poniżej 0
jumpCooldownTimer = Mathf.Max(jumpCooldownTimer, 0f);
}
// ------------------------
// Movement
// ------------------------
[Header("Camera")]
[Tooltip("Kamera, względem której porusza się postać (zazwyczaj Main Camera)")]
public Transform cameraTransform;
private void HandleMovement()
{
DetectSlopeAndSliding(); // musi być na samym początku
Vector3 inputDir = GetCameraRelativeInput();
float targetSpeed = _moveInput.sqrMagnitude > 0.01f ? baseSpeed : 0f;
// Twoje modyfikatory prędkości (backward, strafe, sprint, walk) – zostaw bez zmian
if (_moveInput.sqrMagnitude > 0.01f)
{
float forwardAmount = _moveInput.y;
float sideAmount = _moveInput.x;
if (forwardAmount < -0.1f) targetSpeed *= backwardSpeedMultiplier;
else if (Mathf.Abs(sideAmount) > 0.1f && Mathf.Abs(forwardAmount) < 0.1f) targetSpeed *= strafeSpeedMultiplier;
else if (Mathf.Abs(sideAmount) > 0.1f && forwardAmount > 0.1f) targetSpeed *= 0.9f;
if (_sprintInput) targetSpeed *= sprintMultiplier;
else if (_walkInput) targetSpeed *= walkMultiplier;
}
float control = _characterController.isGrounded ? 1f : airboneControlMultiplier;
float currentAccel = _moveInput.sqrMagnitude > 0.01f ? acceleration : deceleration;
// ====================== ŚLIZG ======================
if (isSliding)
{
_characterController.stepOffset = 0f; // ważne!
// PEŁNY wektor ślizgu + lekka kontrola gracza
velocity = slideVelocity;
if (_moveInput.sqrMagnitude > 0.01f)
{
Vector3 projectedInput = Vector3.ProjectOnPlane(inputDir, groundNormal);
velocity += projectedInput * (targetSpeed * 0.4f); // delikatny wpływ inputu
}
// Siła docisku do powierzchni (eliminuje odbicia przy wchodzeniu)
velocity -= groundNormal * 5f * Time.deltaTime;
// Zapobiegamy „wspinaniu się” w górę stoku
velocity.y = Mathf.Min(velocity.y, 0f);
}
// ====================== NORMALNY RUCH ======================
else
{
_characterController.stepOffset = 0.3f; // przywróć normalną wartość
Vector3 horizontalVel = new Vector3(velocity.x, 0f, velocity.z);
Vector3 targetVelocity = inputDir * targetSpeed;
horizontalVel = Vector3.MoveTowards(horizontalVel, targetVelocity, currentAccel * control * Time.deltaTime);
velocity.x = horizontalVel.x;
velocity.z = horizontalVel.z;
velocity.y = _gravity.UpdateGravity(velocity.y, _characterController.isGrounded);
}
// Ruch
_characterController.Move(velocity * Time.deltaTime);
}
// ------------------------
// Jump
// ------------------------
private void HandleJump()
{
bool jumpPressed = _playerInput.actions["Jump"].triggered;
if (jumpPressed && coyoteTimer > 0f && jumpCooldownTimer <= 0f)
{
// Oblicz prędkość skoku
velocity.y = Mathf.Sqrt(jumpHeight * -2f * _gravity.GravityValue);
currentCondition = Condition.Airborne;
jumpCooldownTimer = jumpCooldown;
coyoteTimer = 0f;
}
}
// ------------------------
// Camera and Rotation
// ------------------------
private void LateUpdate()
{
Vector3 horizontalVelocity = new Vector3(velocity.x, 0f, velocity.z);
if (horizontalVelocity.sqrMagnitude > 0.01f)
{
RotateToCameraDirection();
}
}
private void RotateToCameraDirection()
{
if (cameraTransform == null)
return;
Vector3 camForward = cameraTransform.forward;
camForward.y = 0f;
if (camForward.sqrMagnitude < 0.001f)
return;
Quaternion targetRotation = Quaternion.LookRotation(camForward);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
15f * Time.deltaTime
);
}
// ------------------------
// Fall Tracking
// ------------------------
private void UpdateFallTracking()
{
bool isGroundedNow = _characterController.isGrounded;
// Start spadania
if (!isGroundedNow && wasGrounded)
{
fallStartHeight = transform.position.y;
}
// LĄDOWANIE ──────────────── TU RESETUJEMY COOLDOWN ────────────────
if (isGroundedNow && !wasGrounded)
{
float fallDistance = Mathf.Max(0f, fallStartHeight - transform.position.y);
currentGroundedState = EvaluateFallDistance(fallDistance);
// Kluczowa linia – pełny reset cooldownu przy każdym dotknięciu ziemi
jumpCooldownTimer = jumpCooldown;
// reszta obsługi lądowania
if (currentGroundedState >= GroundedState.SoftFall)
{
fallStateTimer = fallStateDisplayTime;
}
else
{
currentGroundedState = GroundedState.Grounded;
fallStateTimer = 0f;
}
// ──────────────── opcjonalny debug, żeby zobaczyć co się dzieje
// Debug.Log($"[LAND] Reset jump cooldown → {jumpCooldownTimer:F2}s");
}
// Dynamiczna zmiana w powietrzu
if (!isGroundedNow)
{
float currentFallDistance = Mathf.Max(0f, fallStartHeight - transform.position.y);
currentGroundedState = EvaluateFallDistance(currentFallDistance);
}
// Odliczanie timera stanu lądowania
if (fallStateTimer > 0f)
{
fallStateTimer -= Time.deltaTime;
if (fallStateTimer <= 0f && isGroundedNow)
{
currentGroundedState = GroundedState.Grounded;
}
}
else if (isGroundedNow)
{
currentGroundedState = GroundedState.Grounded;
}
wasGrounded = isGroundedNow;
}
private GroundedState EvaluateFallDistance(float distance)
{
if (distance < 0.1f)
return GroundedState.Grounded;
if (distance < softGroundedDistance)
return GroundedState.SafeFall;
if (distance < normalGroundedDistance)
return GroundedState.SoftFall;
if (distance < hardGroundedDistance)
return GroundedState.NormalFall;
return GroundedState.HardFall;
}
// ------------------------
// Update States
// ------------------------
private void UpdateStates()
{
if (!_characterController.isGrounded)
{
currentCondition = Condition.Airborne;
currentMovementState = MovementState.Jumping;
return;
}
currentCondition = Condition.Standing;
// Brak ruchu → Idle (nawet jeśli trzymamy shift/ctrl)
if (_moveInput.sqrMagnitude < 0.1f)
{
currentMovementState = MovementState.Idle;
return;
}
// Priorytety modyfikatorów prędkości (kolejność ważna!)
if (_sprintInput) // najszybszy stan
{
currentMovementState = MovementState.Sprinting;
}
else if (_walkInput) // najwolniejszy stan
{
currentMovementState = MovementState.Walking;
}
else
{
currentMovementState = MovementState.Running; // domyślny bieg
}
}
private void DetectSlopeAndSliding()
{
isSliding = false;
slideVelocity = Vector3.zero;
currentSlopeAngle = 0f;
currentSlopeState = SlopeState.Flat;
float checkDistance = _characterController.height * 0.8f;
Vector3 origin = transform.position;
if (Physics.SphereCast(origin, _characterController.radius * 0.8f, Vector3.down,
out RaycastHit hit, checkDistance, ~0, QueryTriggerInteraction.Ignore))
{
groundNormal = hit.normal;
currentSlopeAngle = Vector3.Angle(hit.normal, Vector3.up);
currentSlopeState = GetSlopeState(currentSlopeAngle);
// kierunek zsuwania w dół stoku
Vector3 slideDir = Vector3.ProjectOnPlane(Vector3.down, hit.normal).normalized;
// tylko jeśli stromy stok
if ((int)currentSlopeState >= (int)SlopeState.SlideDown)
{
isSliding = true;
// Jeśli gracz podaje input, lekko modyfikujemy kierunek
if (_moveInput.sqrMagnitude > 0.01f)
{
Vector3 inputDir = GetCameraRelativeInput();
float inputInfluence = Mathf.InverseLerp(startSlideAngle, 85f, currentSlopeAngle);
// maksymalnie 25% wpływu inputu
slideDir = Vector3.Lerp(slideDir, inputDir, inputInfluence * 0.25f).normalized;
}
// Prędkość ślizgu rośnie wraz z kątem
float angleFactor = Mathf.InverseLerp(startSlideAngle, 90f, currentSlopeAngle);
float targetSlideSpeed = Mathf.Lerp(slideSpeed, maxSlideSpeed, angleFactor);
// Ustawiamy velocity tylko w X/Z
slideVelocity = slideDir * targetSlideSpeed;
}
// Kierunek stoku względem postaci (dla debuga i Animatora)
Vector3 playerForward = transform.forward;
Vector3 playerRight = transform.right;
float forwardDot = Vector3.Dot(playerForward, slideDir);
float rightDot = Vector3.Dot(playerRight, slideDir);
currentSlopeDirection = SlopeDirection.None;
if (Mathf.Abs(forwardDot) > Mathf.Abs(rightDot))
{
if (forwardDot > 0.3f) currentSlopeDirection = SlopeDirection.DownForward;
else if (forwardDot < -0.3f) currentSlopeDirection = SlopeDirection.DownBackward;
}
else
{
if (rightDot > 0.3f) currentSlopeDirection = SlopeDirection.DownRight;
else if (rightDot < -0.3f) currentSlopeDirection = SlopeDirection.DownLeft;
}
Debug.Log($"[SLOPE] Angle: {currentSlopeAngle:F1}° | Sliding: {isSliding} | Speed: {slideVelocity.magnitude:F1}");
}
}
private SlopeState GetSlopeState(float angle)
{
if (angle < 10f) return SlopeState.Flat;
if (angle < 25f) return SlopeState.SlightDown;
if (angle < 40f) return SlopeState.SteepDown;
if (angle < 55f) return SlopeState.SlideDown;
if (angle < 65f) return SlopeState.VerySteep;
if (angle < 80f) return SlopeState.NearVertical;
return SlopeState.VerticalWall;
}
private Vector3 GetCameraRelativeInput()
{
Vector3 inputDir = new Vector3(_moveInput.x, 0f, _moveInput.y);
if (useNormalizedDiagonalMovement && inputDir.sqrMagnitude > 0.01f)
inputDir.Normalize();
if (cameraTransform != null)
{
Vector3 camForward = cameraTransform.forward;
Vector3 camRight = cameraTransform.right;
camForward.y = camRight.y = 0f;
camForward.Normalize();
camRight.Normalize();
inputDir = camRight * inputDir.x + camForward * inputDir.z;
}
return inputDir;
}
private void UpdateAnimator()
{
if (_animator == null)
return;
// Prędkość w płaszczyźnie XZ
Vector3 horizontalVelocity = new Vector3(velocity.x, 0f, velocity.z);
float speed = horizontalVelocity.magnitude;
_animator.SetFloat("Speed", speed);
float displayVertical = _characterController.isGrounded ? 0f : velocity.y;
_animator.SetFloat("VerticalVelocity", displayVertical);
_animator.SetFloat("DirectionX", _moveInput.x);
_animator.SetFloat("DirectionY", _moveInput.y);
// ========================
// NOWE – IsJumping TYLKO podczas prawdziwego skoku!
// ========================
bool isJumping = !_characterController.isGrounded && velocity.y > 0.1f; // > 0.1f żeby uniknąć szumów
_animator.SetBool("IsJumping", isJumping);
// Opcjonalnie: osobny parametr na spadanie (bardzo polecam!)
_animator.SetBool("IsFalling", !_characterController.isGrounded && velocity.y < -0.1f);
// Stany
_animator.SetBool("IsGrounded", _characterController.isGrounded);
_animator.SetInteger("MovementState", (int)currentMovementState);
_animator.SetInteger("FallState", (int)currentGroundedState);
_animator.SetInteger("SlopeState", (int)currentSlopeState);
_animator.SetBool("IsSliding", isSliding);
_animator.SetFloat("SlopeAngle", currentSlopeAngle);
_animator.SetFloat("SlideSpeed", slideVelocity.magnitude);
_animator.SetInteger("SlopeDirection", (int)currentSlopeDirection);
}
}