Цель: внедрить механику прыжков от стены, чтобы игрок мог достигать более высоких мест в платформере с Unity.

До сих пор в этом проекте я рассказывал, как создать физический базовый контроллер персонажа с возможностью двойного прыжка. Я добавил динамические платформы, коллекционные предметы и лифт, управляемый событиями.

Сегодня я вернусь к скрипту контроллера персонажа (Player) и добавлю функцию прыжка от стены. Это позволит игроку обнаруживать поверхность стены и прыгать с одной стены на другую.

Ниже приведены статьи, которые я написал, объясняющие, как я создал контроллер персонажа и функцию двойного прыжка. Все делается с помощью новой системы ввода.





Настройка сцены

У меня есть платформа и две стены для моей сцены, создающие путь вверх. Единственный способ добраться до вершины — это прыжок между двумя стенами.

На данный момент у меня есть две стены, помеченные как «Стена», чтобы контроллер игрока мог определить, на какую именно стену выполнить прыжок.

Скрипт ввода игры

Опять же, все это делается с помощью Новой системы ввода Unity. У меня есть отдельный скрипт, который обрабатывает ввод в игру, который прикреплен к игроку.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameInput : MonoBehaviour
{
    private ActionMaps _input;
    public event EventHandler OnInteract;
    private void Awake()
    {
        _input = new ActionMaps();
    }

    private void OnEnable()
    {
        _input.Player.Enable();
        _input.Player.Interact.performed += Interact_performed;
    }

    private void Interact_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj)
    {
        OnInteract?.Invoke(this,EventArgs.Empty);
    }

    public Vector2 GetMovementVectorNormalized() 
    {
        Vector2 _direction = _input.Player.Move.ReadValue<Vector2>();
        _direction = _direction.normalized;
        return _direction;
    }

    public bool IsJumping() 
    {
        if (_input.Player.Jump.IsPressed())
            return true;
        else return false;
    }
}

Выполнение прыжков от стены

Несколько переменных, методов и операторов if необходимы, чтобы заставить стену работать с текущим контроллером персонажа.

Скрипт контроллера игрока

Я добавил новые полевые переменные:

  • Float wallJumpForce X & Y: определяет силу прыжка через стену в направлениях X и Y.
  • Bool _canWallJump: C проверяет, может ли игрок прыгать от стены
  • Vector3 WallJumpNormal: здесь будет храниться Vector3 нормали поверхности в том направлении, в котором мы хотим прыгнуть.

Метод OnControllerColliderHit

Затем я использовал метод удара коллайдера контроллера персонажа для обнаружения объектов, с которыми сталкивается игрок. Мы можем получить преобразование объекта, используя переменную Hit, а затем сравнив тег объекта, чтобы найти стены, через которые можно перепрыгнуть. При столкновении игрока со стеной испускается синий луч, представляющий норма поверхности, которая будет использоваться в качестве вектора направления для прыжок от стены.

  • Утверждение If: прыгать от стены нужно только тогда, когда игрок находится в воздухе, не касаясь земли, и тег == «Стена».
private void OnControllerColliderHit(ControllerColliderHit hit)
    {

        if (_controller.isGrounded == false && hit.transform.CompareTag("Wall"))
        {
            Debug.DrawRay(hit.point, hit.normal, Color.blue);
            if (hit.normal.x == 1f || hit.normal.x == -1f) 
            {
                _wallJumpNormal = hit.normal * _wallJumpForceX;
                _canWallJump = true;
            }
        }    
    }

Следующий оператор if проверяет значение x поверхности нормали. Мы хотим, чтобы нормаль x была равной1f или -1f. Это означает, что поверхность перпендикулярна игроку (плоская). Это предохраняет игрока от случайного прыжка в угол и запуска в космос (показано ниже).

Затем мы берем переменную поля _wallJumpNormal и присваиваем ей значение hit.normal * wallJumpForceX (NormalVector (1.0/-1.0,0,0) * 2.50f) и _canWallJump = true.

if (hit.normal.x == 1f || hit.normal.x == -1f) 
            {
                _wallJumpNormal = hit.normal * _wallJumpForceX;
                _canWallJump = true;
            }
        }

Метод движения

После этого пришло время скорректировать метод движения.

Всякий раз, когда игрок касается наземной стены, прыжок отключается.

if (_groundPlayer == true)
        {
            _canWallJump = false;
            _controller.Move(Vector3.zero);
            _yVelocity = -_gravity;
        }

Когда игрок находится в воздухе, у нас есть два варианта: прыжок от стены или двойной прыжок.

Прыжки от стены: мы проверяем, разрешаем ли мы игроку прыгать от стены, только если выполняются условия. Если мы можем прыгать через стену, отключите функцию двойного прыжка, установите вектор xVelocity на вектор _wallJumpNormal и добавьте yVelocity.

if ((_canWallJump && _gameInput.IsJumping() && Time.time > _jumpDelay))
            {
                _doubleJump = true;
                _canWallJump = false;
                _xVelocity = _wallJumpNormal;

                if (_yVelocity < 0)
                {
                    _yVelocity = 0;
                    _yVelocity += _wallJumpForceY;
                }
                else
                    _yVelocity += _wallJumpForceY;
            }

Затем эти значения передаются в последнюю строку кода метода движения, который обновляет скорость и направление игрока.

Vector3 _movement = new Vector3(_xVelocity.x, _yMaxVelocity, 0);
            _controller.Move(_movement * Time.deltaTime);

В заключение, именно так я реализовал функцию прыжков от стены в этом проекте. Это работает путем получения нормальной поверхности стены к игроку и использования этого вектора, чтобы подтолкнуть игрока к следующей стене.

Полный скрипт игрока

using UnityEngine;
using UnityEngine.SceneManagement;

public class Player : MonoBehaviour
{
    private GameInput _gameInput;
    private CharacterController _controller;
    
    [SerializeField] private float _speed = 2.0f;
    [SerializeField] private float _jumpStrength = 15.0f;
    [SerializeField]private float _gravity = 1.0f;
    [SerializeField] private bool _groundPlayer;
    
    public int _coinCollected { get; private set; } = 0;
    private int _lives = 3;
    
    private float _yVelocity;
    private  Vector3 _direction;
    private Vector3 _xVelocity;
    
    private bool _doubleJump;
    private float _jumpDelay;
    
    [SerializeField] private float _wallJumpForceX;
    [SerializeField] private float _wallJumpForceY;
    [SerializeField] private bool _canWallJump;
    [SerializeField] private Vector3 _wallJumpNormal;
    private void Awake()
    {
        _gameInput = GetComponent<GameInput>();
        _controller = GetComponent<CharacterController>();
        if (_gameInput == null) Debug.LogError("Missing Game Input");
        if (_controller == null) Debug.LogError("Missing Character Controller");
    }

    private void Start()
    {
        UIManager._instance.UpdateLivesText(_lives);
        UIManager._instance.UpdateCoinText(_coinCollected);
    }

    private void Update()
    {
        Movement();
        ResetSpawn();
        Death();
    }

    private void OnControllerColliderHit(ControllerColliderHit hit)
    {


        if (_controller.isGrounded == false && hit.transform.CompareTag("Wall"))
        {
            Debug.DrawRay(hit.point, hit.normal, Color.blue);
            if (hit.normal.x == 1f || hit.normal.x == -1f) 
            {
                _wallJumpNormal = hit.normal * _wallJumpForceX;
                _canWallJump = true;
            }
        }    
    }


    private void Movement()
    {
        _groundPlayer = _controller.isGrounded;


        if (_groundPlayer == true)
        {
            _canWallJump = false;
            _controller.Move(Vector3.zero);
            _yVelocity = -_gravity;
        }

        if (_groundPlayer == true && _gameInput.IsJumping() && !_canWallJump)
        {
            _yVelocity += _jumpStrength;
            _doubleJump = false;
            _jumpDelay = Time.time + 0.3f;

        }
        else if (_groundPlayer == false)
        {
            if ((_canWallJump && _gameInput.IsJumping() && Time.time > _jumpDelay))
            {
                _doubleJump = true;
                _canWallJump = false;
                _xVelocity = _wallJumpNormal;

                if (_yVelocity < 0)
                {
                    _yVelocity = 0;
                    _yVelocity += _wallJumpForceY;
                }
                else
                    _yVelocity += _wallJumpForceY;
            }

            if ((!_doubleJump && _gameInput.IsJumping() && Time.time > _jumpDelay))
            {
                _doubleJump = true;
              
                if (_yVelocity < 0)
                {
                    _yVelocity = 0;
                    _yVelocity += 6f;
                }
                else
                    _yVelocity += 6f;

            }

            _yVelocity -= _gravity * Time.deltaTime;

        }

        var _yMaxVelocity = Mathf.Clamp(_yVelocity, -20, 100f);
        
        if (_groundPlayer == true)
        {
            _direction = _gameInput.GetMovementVectorNormalized();
            _xVelocity = _direction * _speed;
        }

   
            Vector3 _movement = new Vector3(_xVelocity.x, _yMaxVelocity, 0);
            _controller.Move(_movement * Time.deltaTime);
    }

    private void ResetSpawn() 
    {
        Vector3 _currentPosition = transform.position;
        Vector3 _spawnLocation = new Vector3(0, 1, 0);
        bool uiZeroLivesText = _lives > 0;

        if (_currentPosition.y < -5) 
        {
            transform.position = _spawnLocation;
            _lives--;
            if(uiZeroLivesText)
            UIManager._instance.UpdateLivesText(_lives);
        }
    }


    private void Death() 
    {
        if(_lives < 0) 
        {
            SceneManager.LoadScene(0);
        }
    }

    public void AddCoins() 
    {
        _coinCollected++;
        UIManager._instance.UpdateCoinText(_coinCollected);
    }

}