public class ParkourController : MonoBehaviour
{
[SerializeField]
private LayerMask _obstacleMask;
[SerializeField]
[Range(0f, 5f)]
private float _parkourHeight = 1f;
[SerializeField]
[Range(0, 60f)]
float _translationVelocity = 2f;
[SerializeField]
[Range(0, 60f)]
float _climbVelocity = 2f;
[SerializeField]
[Range(0, 60f)]
float _forwardVelocity = 2f;
private List<ParkourObstacle> _parkourObstacles = new List<ParkourObstacle>();
private DoomController _playerController;
private CapsuleCollider _parkourCollider;
private Rigidbody _rigidbody;
private bool _isClimbing = false;
private float _velocityBeforeClimb;
private void Start()
{
_playerController = GetComponent<DoomController>();
_rigidbody = GetComponent<Rigidbody>();
foreach (CapsuleCollider collider in GetComponents<CapsuleCollider>())
{
if (collider.isTrigger)
{
_parkourCollider = collider;
break;
}
}
}
private void Update()
{
bool isForwardPressed = Input.GetAxis("Vertical") > 0f;
bool isJumpPressed = Input.GetAxis("Jump") > 0f;
if (isCrouchPressed && _parkourCoroutine != null)
{
StopCoroutine(_parkourCoroutine);
_parkourCoroutine = null;
StopClimbing(false);
}
if (!_playerController.IsGrounded && isForwardPressed && !_isClimbing && _parkourObstacles.Count > 0 && isJumpPressed)
{
RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, _parkourCollider.radius, _obstacleMask))
{
var parkourObstacle = _parkourObstacles.Find(obstacle => obstacle.Collider == hit.collider);
ParkourObstacle.Wall wall = default(ParkourObstacle.Wall);
if (parkourObstacle != null && parkourObstacle.TryGetWall(hit.point, ref wall))
{
Vector3 highestPoint = wall.GetHighestPoint();
if (highestPoint.y > transform.position.y && highestPoint.y - transform.position.y <= _parkourHeight)
{
StartClimbing(parkourObstacle, wall, hit.point);
}
}
}
}
}
private void StartClimbing(ParkourObstacle obstacle, ParkourObstacle.Wall wall, Vector3 startPoint)
{
_isClimbing = true;
Plane wallPlane = new Plane(wall.GetPoint(0), wall.GetPoint(1), wall.GetPoint(2));
Vector3 startPostion1 = startPoint + (wallPlane.normal * _playerController.CharacterRadius * -1);
Vector3 startPostion2 = startPoint + (wallPlane.normal * _playerController.CharacterRadius);
Vector3 climbStartPosition = Vector3.Distance(transform.position, startPostion1) < Vector3.Distance(transform.position, startPostion2) ? startPostion1 : startPostion2;
Quaternion climbStartRtotation = Quaternion.LookRotation(startPoint - climbStartPosition, Vector3.up);
_velocityBeforeClimb = new Vector2(_rigidbody.velocity.x, _rigidbody.velocity.z).magnitude;
_rigidbody.velocity = Vector3.zero;
_playerController.enabled = false;
_rigidbody.isKinematic = true;
_parkourCoroutine = StartCoroutine(Climbing(obstacle, wall, climbStartPosition, climbStartRtotation));
}
private void StopClimbing(bool isComplite = true)
{
_isClimbing = false;
_playerController.enabled = true;
_rigidbody.isKinematic = false;
LastParkourTime = Time.realtimeSinceStartup;
_rigidbody.velocity = transform.forward * _velocityBeforeClimb;
}
private IEnumerator Climbing(ParkourObstacle obstacle, ParkourObstacle.Wall wall, Vector3 startPoint, Quaternion startRotation)
{
Vector3 startPlayerPosition = transform.position;
Quaternion startPlayerRotation = transform.rotation;
float elapsed = 0f;
float translationTime = Vector3.Distance(startPlayerPosition, startPoint) / _translationVelocity;
while (elapsed < translationTime)
{
transform.position = Vector3.Lerp(startPlayerPosition, startPoint, elapsed / translationTime);
transform.rotation = Quaternion.Lerp(startPlayerRotation, startRotation, elapsed / translationTime);
elapsed += Time.deltaTime;
yield return null;
}
transform.rotation = startRotation;
startPlayerPosition = transform.position;
Vector3 targetUpPosition = new Vector3(transform.position.x,
wall.GetHighestPoint().y + _playerController.CharacterHeight / 2,
transform.position.z);
Vector3 targetForwardPosition = targetUpPosition + transform.forward * _playerController.CharacterRadius * 2;
elapsed = 0f;
float climbTime = Vector3.Distance(startPlayerPosition, targetUpPosition) / _climbVelocity;
while (elapsed < climbTime)
{
transform.position = Vector3.Lerp(startPlayerPosition, targetUpPosition, elapsed / climbTime);
elapsed += Time.deltaTime;
yield return null;
}
float forwardTime = Vector3.Distance(startPlayerPosition, targetForwardPosition) / _forwardVelocity;
elapsed = 0f;
startPlayerPosition = transform.position;
while (elapsed < forwardTime)
{
transform.position = Vector3.Lerp(startPlayerPosition, targetForwardPosition, elapsed / forwardTime);
elapsed += Time.deltaTime;
yield return null;
}
StopClimbing();
}
private void OnTriggerEnter(Collider other)
{
if (((1 << other.gameObject.layer) & _obstacleMask) != 0)
{
_parkourObstacles.Add(GetParkourObstacle(other.gameObject));
}
}
private void OnTriggerExit(Collider other)
{
if (((1 << other.gameObject.layer) & _obstacleMask) != 0)
{
BoxCollider collider = other.GetComponent<BoxCollider>();
_parkourObstacles.Remove(_parkourObstacles.Find(obstacle => obstacle.Collider == collider));
Debug.Log(_parkourObstacles.Count());
}
}
private ParkourObstacle GetParkourObstacle(GameObject gameObject)
{
BoxCollider collider = gameObject.GetComponent<BoxCollider>();
var points = new Vector3[8];
float xSize = collider.size.x;
float ySize = collider.size.y;
float zSize = collider.size.z;
Vector3 center = collider.center;
//Wall0 - 0, 1, 2, 3
//Wall1 - 0, 2, 4, 6
//Wall2 - 4, 5, 6, 7
//Wall3 - 1, 3, 5, 7
//Floor - 0, 1, 4, 5
points[0] = gameObject.transform.TransformPoint(new Vector3(center.x + xSize / 2, center.y + ySize / 2, center.z + zSize / 2));
points[1] = gameObject.transform.TransformPoint(new Vector3(center.x - xSize / 2, center.y + ySize / 2, center.z + zSize / 2));
points[2] = gameObject.transform.TransformPoint(new Vector3(center.x + xSize / 2, center.y - ySize / 2, center.z + zSize / 2));
points[3] = gameObject.transform.TransformPoint(new Vector3(center.x - xSize / 2, center.y - ySize / 2, center.z + zSize / 2));
points[4] = gameObject.transform.TransformPoint(new Vector3(center.x + xSize / 2, center.y + ySize / 2, center.z - zSize / 2));
points[5] = gameObject.transform.TransformPoint(new Vector3(center.x - xSize / 2, center.y + ySize / 2, center.z - zSize / 2));
points[6] = gameObject.transform.TransformPoint(new Vector3(center.x + xSize / 2, center.y - ySize / 2, center.z - zSize / 2));
points[7] = gameObject.transform.TransformPoint(new Vector3(center.x - xSize / 2, center.y - ySize / 2, center.z - zSize / 2));
return new ParkourObstacle(new ParkourObstacle.Wall(points[0], points[1], points[4], points[5]),
new ParkourObstacle.Wall[]
{
new ParkourObstacle.Wall(points[0], points[1], points[2], points[3]),
new ParkourObstacle.Wall(points[0], points[2], points[4], points[5]),
new ParkourObstacle.Wall(points[4], points[5], points[6], points[7]),
new ParkourObstacle.Wall(points[1], points[3], points[5], points[7]),
},
collider);
}
}
public class ParkourObstacle
{
private Wall _floor;
private Wall[] _walls;
private BoxCollider _collider;
public BoxCollider Collider
{
get => _collider;
}
public ParkourObstacle(Wall floor, Wall[] walls, BoxCollider collider)
{
_floor = floor;
_walls = walls;
_collider = collider;
}
public bool TryGetWall(Vector3 point, ref Wall wall)
{
for (int i = 0; i < _walls.Length; i++)
{
Plane wallPlane = new Plane(_walls[i].GetPoint(0), _walls[i].GetPoint(1), _walls[i].GetPoint(2));
if (wallPlane.ClosestPointOnPlane(point) == point)
{
wall = _walls[i];
return true;
}
}
return false;
}
public struct Wall
{
private Vector3 _point0;
private Vector3 _point1;
private Vector3 _point2;
private Vector3 _point3;
private const int _pointsCount = 4;
public Wall(Vector3 point0, Vector3 point1, Vector3 point2, Vector3 point3)
{
_point0 = point0;
_point1 = point1;
_point2 = point2;
_point3 = point3;
}
public Vector3 GetHighestPoint()
{
Vector3 maxPoint = Vector3.negativeInfinity;
for (int i = 0; i < _pointsCount; i++)
{
var currentPoint = GetPoint(i);
if (currentPoint.y > maxPoint.y)
{
maxPoint = currentPoint;
}
}
return maxPoint;
}
public Vector3 GetPoint(int index)
{
switch (index)
{
case 0: return _point0;
case 1: return _point1;
case 2: return _point2;
case 3: return _point3;
default: throw new System.Exception("Wall::GetPoint index out of range");
}
}
}
}
Просто напомню, что у нас есть подсайт /unity (:
Вы правы. Я не обратил на него внимания. Но думаю, что данный материал будет интересен не только в рамках разработки на Unity. Логика будет работать и для других движков. Unity идеально подходит для быстрого создания POC-а.
На счёт подсайта по движкам. Когда я последний раз проверял подсайт UE, там небыли ни единой статьи об UE5, все публиковались в какие-то другие посайты. Возможно, подсайт Unity так же живёт на какой-то своей волне,)
Лучшим примером паркура в играх от первого лица это бесспорно Mirror’s Edge.Не соглашусь, Mirror’s Edge скорее был первой годной игрой про паркур, а вот на сегодняшний день первое место - это Dying Light.
Те, кто проходил там испытания ловкости на время не дадут соврать.
Если рассматривать его для выполнения игровых задач - да.
А по ощущениям все же Mirror’s Edge лучше.
Спасибо за материал! Но не забывайте ставить теги, пожалуйста
Сейчас попробую найти подходящий пример использования и добавлю.