Unity 攀爬系统

编程入门 行业动态 更新时间:2024-10-15 16:23:18

Unity 攀爬<a href=https://www.elefans.com/category/jswz/34/1770742.html style=系统"/>

Unity 攀爬系统

说明

角色的位移主要是靠爬墙动画的根运动来实现

本代码主要是对墙体进行检测,将角色固定在墙体表面,以及对攀爬状态进行管理

思路

我们先来想一下整个实现的过程:

一开始人物是正常行走,然后发现前面有一堵墙,然后开始往上爬,爬呀爬,爬到顶上的时候,最后手掌撑着墙爬到墙顶。

这样,我们就将攀爬分为大体四个步骤:

①进入爬墙状态 ②爬墙 ③爬到墙顶④退出爬墙状态

一、进入爬墙状态

已经检测到墙壁,并且在玩家控制按键向前走的时候,并且要持续一段时间,才可以上墙。

进入爬墙状态时,为了根运动动画的正确播放,要勾选IsKinematic,把角色刚体设为运动学刚体(即不受外力影响)。

二、爬墙

角色在墙壁上移动时,必须时刻贴着墙壁,并且面朝墙壁,但又应该有一段距离。

我们需要时刻约束角色的x轴、z轴的位置(不约束物体所处的高度),以使其贴于墙面。

由 墙体某点 + 墙体某点的法向 * 水平偏移量(需丢弃y值,令其不变),可以计算得到角色的目标位置。

然后让角色的朝向与墙面的法向保持一致。

三、爬到墙顶

播放登顶动画,这个动画需要应用根运动,也就是让动画改变角色的位置。

为了应对动画资源的根运动不准确,在动画播放到最后时,更改角色的位置,令角色置于墙体顶部。

四、退出爬墙状态

要将角色的运动学刚体设为普通刚体。

墙体的检测

首先,先从角色的体心进行一次球形检测,范围为角色的两倍半径,获取范围内的全部碰撞体。

然后在这些碰撞体中,找一个距离角色体心最近的点。

将这个点与角色体心连起来,丢弃y轴的信息,就是水平面上射线检测的方向。

从角色的底部和顶部分别依照这个方向发射射线,如果都检测到了,才能确定检测到了墙壁。
 

动画状态的管理

动画状态机连线

仅 Climb Top -> start 有退出时间

所需参数

需要一个 int 类型 的参数 Climb 、 一个 float 类型 的参数 ClimbSpeed

各参数其值代表的含义:

Climb

Climb = 0 在墙面上保持静止

Climb = 1 向上爬 或 向下爬

Climb = 2 攀顶

ClimbSpeed

用于控制播放上爬动画的速度和方向

向上爬时,ClimbSpeed设为1

向下爬时,ClimbSpeed设为-1

静止时,ClimbSpeed设为0

关键代码

判断攀顶动画播放至末尾

if(GetComponent<PlayerControl>().anim.GetCurrentAnimatorStateInfo(0).IsName("Climb Top") &&
GetComponent<PlayerControl>().anim.GetCurrentAnimatorStateInfo(0).normalizedTime > 0.95)

将角色置于墙体顶部

//把角色放到墙顶
transform.position = new Vector3(transform.position.x,currentCollider.bounds.center.y + currentCollider.bounds.size.y * 0.5f + 0.03f,transform.position.z);//往前走1个单位
transform.position = transform.position + transform.forward;

代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClimbingSystem : MonoBehaviour
{public ClimbStatus climbStatus;public enum ClimbStatus{notAtClimbing,isOnTheWall,isOnTheTopOfTheWall,}void Awake(){if (wall == null)wall = new GameObject("Wall").transform;}float v;float h;float p;void FixedUpdate(){Debug.DrawRay(transform.position + new Vector3(-0.5f, 0, 0), -Vector3.up, Color.yellow);v = Input.GetAxis("Vertical");h = Input.GetAxis("Horizontal");p = Input.GetAxis("QE");//不在墙上时的移动if (climbStatus == ClimbStatus.notAtClimbing){MoveOnGround();}//每60帧检测一次墙壁DetectWallOnDepends();//在墙上爬if (climbStatus == ClimbStatus.isOnTheWall)Climb();if (climbStatus == ClimbStatus.isOnTheTopOfTheWall)TheLastClimbOnTheTop();//退出爬墙状态if (Input.GetKeyDown(KeyCode.LeftShift)){ExitClimbWall();}}bool IsGrounded(){return Physics.Raycast(transform.position, -Vector3.up, 0.05f);}void MoveOnGround(){//transform.Translate(Vector3.forward * v * Time.deltaTime * 3);//transform.Translate(Vector3.right * h * Time.deltaTime * 3);//transform.Rotate(Vector3.up * p * Time.deltaTime * 45);}//配合PlayerControl脚本使用时,不执行private Vector3 targetPosition; //目标位置private Quaternion targetRotation;  //目标姿态private float step; //每秒移动的距离void Climb(){//对角色的位移和旋转进行插值//上下移动transform.position = Vector3.MoveTowards(transform.position, targetPosition, step * Time.deltaTime);//旋转transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, 0.2f);//左右移动transform.Translate(Vector3.right * h * Time.deltaTime * 3);//动画根运动if (v > 0){GetComponent<PlayerControl>().anim.SetFloat("ClimbSpeed", 1);}else if (v < 0){GetComponent<PlayerControl>().anim.SetFloat("ClimbSpeed", -1);}else if (v == 0){GetComponent<PlayerControl>().anim.SetFloat("ClimbSpeed", 0);}}//注:只能通过修改targetPosition来改变角色的位置public float charactorHeight = 1.58f;public float charactorRasius = 0.25f;public LayerMask climbableMask = 1 << 26;                                   //把1左移26位,打开第26层,在以下的射线检测中,都只检测第26层。可以在面板手动进行设置。                          public float maxDetectDistance = 1.5f;                                      //最大检测距离,包含角色的半径public float horizontalOffsetFromWall = 0.32f;                              //水平偏移量public Collider currentCollider;                                            //当前碰撞体private Transform wall;                                                     //墙壁姿态,仅用于检测是否到达边缘public bool isTopDetected;public bool isBottomDetected;public bool isRightDetected;public bool isLeftDetected;private Vector3 playerBodyCentre;private Vector3 closestPoint;public Collider[] detectedColliders;bool DetectWallOnDepends(){if (climbStatus == ClimbStatus.notAtClimbing && Time.frameCount % 60 != 0 && v > 0) //必须在按键向前的情况下才能上墙{if (DetectWallRightNow(out bottom, out top)){EnterClimbWall();return true;}}if (climbStatus == ClimbStatus.isOnTheWall || climbStatus == ClimbStatus.isOnTheTopOfTheWall){if (IsGrounded() && Time.frameCount % 60 == 0 || IsGrounded() && v < 0) //着地并且按下键,立即退出爬墙{Debug.Log("着地");ExitClimbWall();}if (DetectWallRightNow(out bottom, out top)){UpdateClimbWall();return true;}else{//如果顶部没有墙壁if (top.collider == null){climbStatus = ClimbStatus.isOnTheTopOfTheWall;OnTheLastClimbOnTheTop();}//如果底部没有碰撞体if (bottom.collider == null){ExitClimbWall();}return false;}}return false;}void EnterClimbWall(){climbStatus = ClimbStatus.isOnTheWall;GetComponent<Rigidbody>().isKinematic = true;step = Vector3.Distance(transform.position, targetPosition) / 0.05f;SetCharacterPosition(bottom, true);currentCollider = bottom.collider;        //设定当前攀附的碰撞体GetComponent<PlayerControl>().playerStatus = PlayerControl.PlayerStatus.Climb;GetComponent<PlayerControl>().anim.SetInteger("Climb", 1);Debug.Log("进入爬墙状态");}void UpdateClimbWall(){SetCharacterPosition(bottom, true);currentCollider = bottom.collider;}void TheLastClimbOnTheTop(){UpdateClimbWall();if(GetComponent<PlayerControl>().anim.GetCurrentAnimatorStateInfo(0).IsName("Climb Top") &&GetComponent<PlayerControl>().anim.GetCurrentAnimatorStateInfo(0).normalizedTime > 0.95){//把角色放到墙顶transform.position = new Vector3(transform.position.x,currentCollider.bounds.center.y + currentCollider.bounds.size.y * 0.5f + 0.03f,transform.position.z);//往前走1个单位transform.position = transform.position + transform.forward;}if (GetComponent<PlayerControl>().anim.GetCurrentAnimatorStateInfo(0).IsName("start")){ExitClimbWall();}}void OnTheLastClimbOnTheTop(){//播放完这个动画就退出爬墙状态GetComponent<PlayerControl>().anim.SetInteger("Climb", 2);}void ExitClimbWall(){climbStatus = ClimbStatus.notAtClimbing;GetComponent<Rigidbody>().isKinematic = false;GetComponent<PlayerControl>().playerStatus = PlayerControl.PlayerStatus.Basic;GetComponent<PlayerControl>().anim.SetInteger("Climb", 0);Debug.Log("退出爬墙状态");}RaycastHit bottom, top;bool DetectWallRightNow(out RaycastHit hitFromBottom, out RaycastHit hitFromTop)//从底部和顶部发射射线{//顶部检测点是 角色原点+角色的高Vector3 topDetectPoint = transform.position + Vector3.up * (charactorHeight);Vector3 bottomDetectPoint = transform.position + new Vector3(0, 0.2f, 0);Vector3 detectDirection = transform.forward;Debug.DrawRay(topDetectPoint, transform.forward, Color.blue);Debug.DrawRay(bottomDetectPoint, transform.forward, Color.red);Debug.DrawLine(topDetectPoint, bottomDetectPoint, Color.magenta);if (climbStatus == ClimbStatus.notAtClimbing)//没有爬墙才进行判断{//获取球体范围内的碰撞体detectedColliders = Physics.OverlapSphere(playerBodyCentre, charactorRasius * 2, climbableMask, QueryTriggerInteraction.Collide);//不能用全部layer//如果有if (detectedColliders.Length > 0){//体心playerBodyCentre = transform.position + Vector3.up * (charactorHeight - charactorRasius);//在全部碰撞体上获取一个距离体心最近的一个点closestPoint = detectedColliders[0].ClosestPoint(playerBodyCentre);//选择一个最近的攀爬点foreach (Collider coll in detectedColliders){//找到一个高度低于体心的碰撞体if (coll.transform.position.y > playerBodyCentre.y)continue;Vector3 point = coll.ClosestPoint(playerBodyCentre);//如果体心与该碰撞体的最近点的距离 小于 体心与之前的最近点的距离,就更改最近点if (Vector3.Distance(playerBodyCentre, point) < Vector3.Distance(playerBodyCentre, closestPoint))closestPoint = point;}//因为检测要在水平面上,所以不需要最近点的高度信息,用体心的高代替closestPoint.y = playerBodyCentre.y;//检测方向为: 从体心出发,到最近点detectDirection = closestPoint - playerBodyCentre;//获取检测方向与角色朝向在水平面的夹角float angle = Mathf.Abs(Vector3.SignedAngle(detectDirection, transform.forward, Vector3.up));//如果夹角大于100度,将检测方向设定为角色的朝向if (angle > 100)detectDirection = transform.forward;}}isTopDetected = Physics.Raycast(topDetectPoint, detectDirection, out hitFromTop, 2 * maxDetectDistance, climbableMask);    //上面的检测距离应该远一些isBottomDetected = Physics.Raycast(bottomDetectPoint, detectDirection, out hitFromBottom, maxDetectDistance, climbableMask);//绘制射线Debug.DrawRay(bottomDetectPoint, detectDirection * maxDetectDistance, Color.cyan);Debug.DrawRay(topDetectPoint, detectDirection * maxDetectDistance, Color.cyan);//检测是否到达了墙壁边缘if (isBottomDetected && isBottomDetected){//将墙壁的位置设置为底部碰撞点//将墙壁的前向设置为底部的法向wall.position = hitFromBottom.point;wall.forward = hitFromBottom.normal;//从底部检测点的右侧某处、左侧某处,向上述方向发射射线RaycastHit right, left;isRightDetected = Physics.Raycast(bottomDetectPoint + wall.right * 0.5f, detectDirection, out right, Mathf.Infinity, climbableMask);isLeftDetected = Physics.Raycast(bottomDetectPoint - wall.right * 0.5f, detectDirection, out left, Mathf.Infinity, climbableMask);}//两个方向都检测到了,才是真的检测到了return (isTopDetected && isBottomDetected);}//仅仅负责上下移动private void SetCharacterPosition(RaycastHit bottomHit, bool setToTarget = false){//目标位置是 底部碰撞点 + 底部碰撞点的法向 * 一段偏移量。也就是说,要距离墙壁一定距离。targetPosition = bottomHit.point + bottomHit.normal * horizontalOffsetFromWall;targetPosition.y = transform.position.y;//设定高度//targetPosition.y += v * 2f * Time.deltaTime;//应用根运动时,不再人为改变高度//让角色朝向与墙面法向保持一致targetRotation = GetRotationFromDirection(-bottomHit.normal);//角色设定到预定位置if (setToTarget){transform.position = targetPosition;transform.rotation = targetRotation;}}private float yaw;//所给方向与世界坐标系的z轴夹角Quaternion GetRotationFromDirection(Vector3 direction){//根据所给的方向向量求夹角,然后在原姿态上旋转到这个角度yaw = Mathf.Atan2(direction.x, direction.z);return Quaternion.Euler(0, yaw * Mathf.Rad2Deg, 0);}}

效果

更多推荐

Unity 攀爬系统

本文发布于:2024-02-26 11:06:54,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1702264.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:系统   Unity

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!