Цель: внедрить механику прыжков от стены, чтобы игрок мог достигать более высоких мест в платформере с 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); } }