Unity人物移动、跳跃笔记

使用刚体+碰撞体的移动

移动功能除了移动本身,还包括不能穿模进其他物体、下坡、上坡、上楼梯。乍一想人物移动本质上就是在场景里移动一个胶囊体,同时根据方向、速度播放动画。但仔细一想,物理系统提供的功能有碰撞检测、重力、摩擦力,人物需要碰撞检测、重力,但是人物贴着墙移动、跳跃时不能因为摩擦力而变慢,但是人物站在斜坡上又需要斜坡的摩擦力防止人物滑下来。然后如果想靠设置物理材质来改变摩擦力,那如果一个房子既可以让人物贴着,又可以让人物上房顶,而且它的房顶还是斜的怎么办?墙和房顶用不同的碰撞体?可是房子一般都用网格碰撞体,有几个网格就有几个碰撞体,那么还必须找房顶和墙壁是分开的网格的模型。对美术素材又有要求。退一步说,如果不用网格碰撞体,用多个基本碰撞体拟合,理论上可行,只是原本做场景一个房子加个网格碰撞体就完事,现在变成加多个基本碰撞体,手调大小、位置旋转,再给不同部位加不同的物理材质。工作量很恐怖。说到底,物理系统的大部分效果我们想要,也有和墙的摩擦力这种不想要的效果。再退一步,直接改写人物位置可以不受墙的摩擦力,但是如下文所说,这会大大提升穿墙bug的概率。因此人物可能并不适合用刚体+碰撞体。

移动看起来好像有太多种方法可以实现。首先是执行时机,就有Update()、OnAnimatorMove()等生命周期函数;实现方法则有直接改transform.position,使用刚体API,使用animator的Root motion。其实很多方法都有坑。

最开始我用:

public void OnAnimatorMove(){
        transform.position+=animator.deltaPosition;
    }

因为它能避免人物移动速度和动画表现的速度不一致。然后发现人物冲着墙角跑步很容易穿模进墙里。然后查资料查到:

[Unity]如何解决带刚体的物体在墙角会穿墙的问题 - 被窝儿 - 博客园

里面说碰撞是物理系统做的,检测到碰撞时,物体完全不会进入对方,如果直接改transform.position,即使检测到碰撞,也是每一帧轻微穿模然后被碰撞排斥开(所以能看到人物对着墙跑时前后抖动),这样穿模多一点,当物体中心点进入对方时,就穿模进去了。

然后我用

public virtual void OnAnimatorMove(){
        rigidbody.velocity=animator.velocity;
    }

在墙角挤进墙的问题没有了,人物对着墙跑也不前后抖动了。但是人物贴着墙走时因为摩擦力走得慢,上楼梯的时候因为速度被改成动画系统的速度(无竖直速度)又不会被抬起来了。

然后直接把OnAnimatorMove()删了,直接用Root motion,也没有穿模的问题(说明Apply root motion也是靠写入刚体速度来移动,但是没有完全替换竖直速度)。代码可能是这样的:

public virtual void OnAnimatorMove(){
        Vector3 velocity=new Vector3(animator.velocity.x,rigidbody.velocity.y,animator.velocity.z);
        rigidbody.velocity=velocity;
    }

在楼梯上盖了一个斜面,上楼梯的问题才算解决。

贴着墙走路的摩擦对用户体验不好,但是比起穿模这种更严重的问题,只能先牺牲用户体验。

总之,人物不适合刚体+碰撞体方案的原因:

1.只有使用刚体API运动才能完全避免穿模,但是物理系统又带来不想要的和墙的摩擦,但因为我们还想要人物和斜面的摩擦,又不能把物理材质的摩擦系数设为0;

2.不能上楼梯

使用CharacterController移动

一开始不用CharacterController是以为这个组件没有重力。后来知道不停执行characterController.SimpleMove()是有重力的,而且它可以上楼梯。然后试了用animator Root motion水平运动,characterController.SimpleMove(Vector3.zero)处理碰撞和重力,看起来效果良好。再后来发现勾选Apply root motion人物就有重力,能移动,不勾选就不能移动,执行SimpleMove()也没用,Animator还会出现Root position or rotation are controlled by curves。

Animator不勾选Apply root motion时什么情况下会出现Root position or rotation are controlled by curves?

这个提示似乎会导致不能移动人物。

1.Starter Asset ThirdPerson里没有Root position or rotation are controlled by curves,同一个AnimatorController给自己的人物用上就有了。

然后我注意到Starter Asset ThirdPerson人物和动画用的Avatar是同一个。会是这个原因吗?

2.然后我给Starter Asset ThirdPerson的人物用上由Mixamo动画组成的AnimatorController,也没有出现Root position or rotation are controlled by curves。所以是人物的原因吗?

3.然后我把Starter Asset ThirdPerson的人物unpack,替换骨架、模型、Avatar,也没有出现Root position or rotation are controlled by curves。

4.然后我把我场景里的人物在播放中把组件一个一个删掉,把Rig builder删掉的时候Root position or rotation are controlled by curves没有了。

5.给Starter Asset ThirdPerson的人物做Rig Setup,并添加一个约束,也有Root position or rotation are controlled by curves了。

结论:使用AnimationRigging会导致Animator没有勾选Apply root motion 时出现Root position or rotation are controlled by curves,并导致无法移动人物。

Animator和CharacterController协作

上文查明是因为使用了AnimationRigging,出现Root position or rotation are controlled by curves,人物不能移动,AnimationRigging肯定要用,所以只能勾选Apply root motion或使用OnAnimatorMove()。

如果想用characterController.Move()或characterController.SimpleMove(),只能在OnAnimatorMove()里。

void OnAnimatorMove(){
    Vector3 ccInput=animator.deltaPosition+Vector3.up*velocityY*Time.deltaTime;
    characterController.Move(ccInput);
}

地面检测、跳跃、坠落

跳跃的功能一般是为了翻上障碍物。如果场景里需要翻上障碍物,也会有坠落的情况。跳跃、坠落功能可以分为物理部分、动画部分和输入部分。跳跃、坠落的动画本质上是由物理检测判定应该何时播放什么动画。

跳跃和坠落有密切的关系又有区别。它们的共同点是人物都会离地,离地时都要保持水平速度,跳跃可以看成主动给人物一个向上速度使人物进入坠落状态。

跳跃功能要面对的问题

地面检测;

人物离地时要不要播放滞空动画(不播放滞空动画,;

下坡时被判定为离地的问题(地面检测);

跳起位移使用动画还是刚体实现(使用刚体,贴墙时会受到摩擦力跳不起来;使用动画,实际跳跃时同时受动画和重力的影响,做动画时向上位移不好确定;使用characterController.Move()或transform.Translate()一段时间,再通过协程停止调用);

移动中跳跃、下坠保持水平速度(跳跃瞬间把animator.velocity的水平值写入rigidbody.velocity,问题是;

地面检测

如果不做跳跃、不需要人物从高处坠落时做出坠落动作,就并不一定要写地面检测。人物坠落时保持在地面上的动作,可以在空中跑步,如果对效果要求不高就问题不大。

地面检测可能的方案有好几种,这里直接说几种方案和它们的问题:

  1. characterController.isGrounded太容易离地,跑步下楼梯时经常离地;
  2. 从脚下向下射线检测,如果场景里有裂缝,经过的时候就会离地,如果不允许人物空中移动,人物会用永远卡住;
  3. 碰撞检测,和characterController.isGrounded一样容易离地;

总之,如果不允许玩家空中移动,离地的条件应该苛刻一点,一旦被判定离地,人物又没有下落到地,人物就永远卡死,对游戏体验是毁灭性打击。一定要保证重力不使人物下落时判定为触地。

直接用characterController.isGrounded?

问题:在下坡中打印characterController.isGrounded,会发现打印的大部分是false,有少量的true。去看了Starter Asset ThirdPerson,里面也没有用characterController.isGrounded,而是每帧执行Physics.CheckSphere()。

碰撞检测方案

判定离地的代码写在OnCollisionExit(),在地面上的代码写在了OnCollisionStay(),不能写在OnCollisionEnter(),因为人物如果先接触另一块地面,再离开第一块地面,OnCollisionExit()的效果覆盖了OnCollisionEnter(),人物被判断为离地。如果写在OnCollisionStay(),离开一块地面时OnCollisionExit()执行一次,而OnCollisionStay()一直执行,人物还被判断为在地面上。坠落伤害的代码则可以放OnCollisionEnter()。

原本用碰撞检测是为了防止每帧执行的开销,现在已经需要用OnCollisionStay(),也是每帧执行,比范围检测就没有优势了,所以不如用范围检测。

下坡和跳跃的矛盾

下坡是不希望人物被判定为离地的,需要地面检测触发器靠下一点,根据游戏场景内会出现的最陡的坡设置触发器的大小,保证不会判定为离地。这样又可能使跳跃也不会判定为离地。

靠近地形里的树后被判定离地的问题

地面检测触发器碰撞体是去除除地面外的层的。在人物被误判为离地后,我把Exclude里的地面层勾选再取消,然后就tm被判触地了。是人物走到地形里刷的树的树根上再离开后出现的,树组成了地形碰撞体,但是又不触发OnTriggerStay()。只能把树根的碰撞体取消,或者在人物碰撞体的OnCollisionStay()也判定触地,因为地形里的树的碰撞体能触发OnCollisionStay()。这样只能让人物跳跃时被判定离地了。

速度检测方案

现实世界中人类对坠落的反应好像并不是因为脚离地,而是因为下落的加速度达到了一定值。比如在加速下落的电梯里人的脚着地,但是会感觉到在下落。rigidbody没有加速度字段,但是可以通过rigidbody.velocity.y达到一定值来判断。

触发器高度、最大下坡陡度、跳跃高度三者应满足:跑步下最陡的坡时不判定为离地&&跳跃时应有可见的一段时间判定为离地。

这样写的问题是从起跳到最高点的过程不会被判为离地,导致还是可以多段跳。所以此方案不可行。

float fallSpeedLastFrame;
    public float fallSpeedThreshold=-3;
    void CheckFall(){
        if(fallSpeedLastFrame>=fallSpeedThreshold&&myCharacter.rigidbody.velocity.y<fallSpeedThreshold){
            myCharacter.animator.SetBool(CharacterBase.onGroundPara,false);
            myCharacter.rigidbody.velocity=new Vector3//开始下坠时继承人物之前移动的速度
            (myCharacter.animator.velocity.x,myCharacter.rigidbody.velocity.y,myCharacter.animator.velocity.z);
        }
        fallSpeedLastFrame=myCharacter.rigidbody.velocity.y;
    }

范围检测 

StarterAsset里的方法是Physics.CheckSphere(),不过还是Physics.CheckBox()能设置长宽高更灵活。判定深度应以人物跳起时有足够的时间滞空为准。

地面检测的层的问题

如果只检测地面层,玩家走到其他人物头上的时候就会判断离地,下落速度增加,触到地时直接摔死。如果包括人物层,因为检测范围和人物碰撞体重叠,导致空中也检测为触地。地面检测需要区分自己人物碰撞体和踩着的其他人物碰撞体。只能把玩家和其他人物设为不同层。

执行跳跃

跳起的移动有哪些方案?

1.动画根运动;

3.CharacterController的API;

4.transform的API;

如下面问题所说,使用刚体速度起跳,紧贴其他物体时会受到摩擦力,跳不起来(造成跳跃原有的越障功能失效),如果把摩擦改小,人站在物体上又会打滑,所以刚体速度方案不可行。

使用动画跳跃

跳跃动画的要点:

  • 给跳跃动画的Root T.y一个增加值,不勾选Root transform position(Y) Bake into pose;
  • 因为这个位移要克服重力,需要的位移比预览里看起来合适的要更大;
  • 起跳时的曲线斜率应该调整为斜率先大后小。
  • 不勾选Loop time;

  • 进入和离开跳跃的过渡时间为0;

 触发跳跃动画时把animator的速度给刚体,可在移动中跳跃时保持移动,因为触发跳跃的这一帧还没有向上移动,所以不用担心动画和刚体的向上移动同时生效。

public void Jump(){
        //动画跳跃方案
        animator.SetTrigger(jumpPara);
        rigidbody.velocity=animator.velocity;//移动中跳跃,跳跃中保持移动
}

自己写重力+跳跃

维护一个竖直速度变量,在地上时写入一个向下的常值,在空中不断向下增加,跳跃时写入向上的速度。

void Gravity(){
            velocityY+=g*Time.deltaTime;
            if(onGround&&velocityY<0){
                velocityY=-onGroundVelocityY;//把人物按在地上
            }
            if(swim){
                velocityY=0;
            }
        }

人物在地面上时向下速度的作用

这里在地面上时需要有一个向下的速度。这个速度也是人物下坡时竖直下落的速度,人物下坡时是被判定触地的同时依靠这个速度下降,这个速度设小了,人物下坡就不够快,会脱离斜坡,直到被判离地,重力加速度生效。效果就是人物下陡坡时反复腾空并播放下坠动作。

这个速度我设了8.

移动和动画的同步问题

这样人物跳跃滞空的时间和跳跃动画的时间是独立的,有可能跳跃已经落地了跳跃动画还没做完。需要调整跳起速度和跳跃动画长度,使动画长度短于滞空时间,滞空比跳跃多的时间交给滞空动画。

移动中跳跃保持水平速度

现在人物的移动完全是在OnAnimatorMove()里执行characterController.Move()实现,那么移动中跳跃保持水平速度就需要在跳跃开始时得到动画的水平速度并在滞空中保持,在落地后取消这个速度。知道起跳的时机很简单,在触发跳跃动画之前记录下animator的水平速度。落地的时机我通过滞空状态机行为脚本的OnStateExit(),在这里把滞空水平速度设为0.

void OnAnimatorMove(){
            Vector3 ccInput=animator.deltaPosition+Vector3.up*velocityY*Time.deltaTime;
            ccInput+=jumpingXZVelocity*Time.deltaTime;
            characterController.Move(ccInput);
        }
public override void Jump(){
            jumpingXZVelocity=new Vector3(animator.velocity.x,0,animator.velocity.z);
            base.Jump();
            velocityY=jumpSpeed;
        }
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
      if(myCharacter is PlayerCharacter2){
        PlayerCharacter2 playerCharacter2=myCharacter as PlayerCharacter2;
        playerCharacter2.jumpingXZVelocity=Vector3.zero;
      }
    }

记录上一帧是否在地面?

除了这样还可以声明一个bool onGroundLastFrame,记录上一帧是否在地面,获得落地的时机。这样遇到的问题是人物头上有障碍物,跳跃无法离地时就不会清空水平速度。这时需要把记录水平速度的条件改成上一帧触地,这一帧离地。

总之,跳跃中保持水平速度的功能,如果是跳跃就记录速度,必须保证能执行清空速度(在滞空状态的行为脚本退出时),如果根据触地清空速度,那么就只有在离地时记录速度!简称要根据动作都根据动作,要根据物理都根据物理。

动画效果

简单的跳跃只需要滞空动画。如果想做得更逼真,可以添加起跳和落地动画。我这里做滞空和落地动画。落地动画因为本身属于过渡动画,为了避免自动过渡出现不想要的效果,应该把滞空到落地、落地到站立的过渡设置为0。

触发滞空动画的方式

一种是输入后设置动画参数触发器Jump播放,但如果人物从高处坠落就不会播放;另一种是由地面检测判定离地后把动画参数onGround设为false播放。

所以准确做法是地面检测滞空>播放滞空动画。跳跃是播放跳跃动作>滞空>播放滞空动画。

问题记录

跑步中跳跃有一定概率出现在空中跑步,看状态机显示进入跳跃状态后立刻退出进入了跑步状态。跳跃状态的退出条件是onGround==true,也就是说进入跳跃状态后立刻满足了退出的条件。

解决方法:稍微增加了跑步到跳跃状态过渡的时长。

CharacterController遇到的问题

人物从树的某个方向很容易走到树上

把树干的碰撞体设为竖直的也没用。

解决方法:之前树干碰撞体是X轴向的,有一个Z轴旋转,把碰撞体改成Y轴向的,旋转归0,可解决。

开始运行后人物先做出滞空动作,然后摔死,死后还每帧执行摔死

原因:改了地面LayerMask的字段名,字段变成Nothing,地面检测一直检测滞空,竖直速度一直增加,直到达到摔死速度,被OnControllerColliderHit()触发摔死。

人物偶发播放落地动画

有时候好像在固定地点容易发生,有时感觉是开枪时容易发生(之前发现弹壳有碰撞体可能导致人物离地,弹壳碰撞体已删除)。

研究思路:

  1. 直接在人物被判离地的地方把编辑器播放暂停,观察人物的情况;
    if(!onGround){
        UnityEditor.EditorApplication.isPaused=true;
    }

  2. 在OnDrawGizmosSelected用Gizmos.DrawWireCube把地面检测区域画出来;
void OnDrawGizmosSelected(){
        Gizmos.DrawWireCube(transform.position-Vector3.up*checkGroundDepth,2*new Vector3(characterController.radius,checkGroundDepth,characterController.radius));
    }

然后截取到了如下画面:

虽然肉眼看起来这个白方框和地面是相交的,但是我猜想有可能是判定区域全部跑到地面以下导致离地的。因为这个区域和人物胶囊碰撞体刚刚相切。然后就把碰撞区域上移。

摔伤检测:地面检测不在地面但是CharacterController一直执行OnControllerColliderHit()

导致人物一直执行摔伤直到摔死。OnControllerColliderHit()这个函数是CharacterController接触到物体就执行,不是只在碰到的那一帧执行。OnControllerColliderHit()执行摔伤的思路好像就是错的,如果人贴着一个摩天大楼下落,那么人物一直执行OnControllerColliderHit(),下落速度达到摔伤速度时就会摔伤,且下落速度不重置,会快速反复摔伤到死。

在上图的情况,CharacterController.isGrounded是true,此时CharacterController的地面检测结果正确,自己写的范围检测结果错误。那么就让在地面的条件是二者或,让离地的条件苛刻一点。还是解决不了摩天大楼旁边下落的情景(需要只在碰撞瞬间执行的函数)

onGround = Physics.CheckSphere(transform.position, characterController.radius,
GameSceneManager.Single.layersCheckGround)||characterController.isGrounded;

总结

整个移动功能给人的感觉就是可能的方案太多,坑也多,特定方案有特定的坑,不得不修补或换方案。总的感觉就是思路很乱。

  1. 再次总结人物移动和跳跃需要解决的问题:
  2. 穿墙(修改transform.position移动导致的);
  3. 和墙摩擦导致移动跳跃变慢,站在斜坡上又不应该滑下来(使用刚体并使用velocity移动导致的);
  4. 不能上楼梯(使用刚体并使用velocity移动导致的);
  5. 要保证下场景里最陡的坡不被判定为离地(通过地面检测深度和触地Y速度调,触地Y速度适当调大);
  6. 移动中跳跃需要保持水平速度(为了真实。如果不追求真实可以允许人物在空中移动);

以上可以看出使用刚体导致了几个问题,一个词总结就是摩擦力,人物不想要墙的摩擦力,想要斜坡的摩擦力,刚体除非改物理材质否则对摩擦力一视同仁。以及为什么应该用CharacterController。

由此得到的调一些参数的标准:

  1. 地面检测深度:首先保证人物跳起来有足够的时间离地(检测深度必须小于跳跃高度),然后在场景里找一些裂缝,保证通过时不被判离地;
  2. 触地Y速度:确定地面检测深度后,找场景里最陡的坡跑步下坡,保证人物不离地,离地就增加触地Y速度

游泳

上文已经确定用CharacterController控制人物,这里也讨论CharacterController的情况。

需求:

  1. 水淹到人物的胸及以上时,进入游泳状态;
  2. 关闭重力;
  3. 人物在各种情况下入水(从岸边走入、从空中掉入、在水下),最后都稳定在一个深度;

1

实现的方法有:

  1. 在人物胸处做某种检测,水面加触发器,检测到水就进入游泳;
  2. 在水下放隐形碰撞体;

这里先尝试方法1,做哪种检测,为了让人物意外在水下也进入游泳,可以从胸向上做射线检测。

const float inWaterDepth=1;//淹入水中的深度
RaycastHit raycastHit;
protected void CheckSwim(){
        swim=Physics.Raycast(transform.position+Vector3.up*inWaterDepth,Vector3.up,out raycastHit,Mathf.Infinity,1<<MyGameManager.waterLayer,QueryTriggerInteraction.Collide);
        Swim(swim);
    }

2

游泳的效果写在重力后面,覆盖重力的效果。

void Gravity(){
            velocityY+=g*Time.deltaTime;
            if(onGround){
                if(velocityY<0){
                    velocityY=-2;//把人物按在地上
                }
            }
            if(swim){
                velocityY=0;
            }
        }

3

就用1射线检测得到的水面的点,把人物的y坐标设为让入水检测点在水面下面一点。如果刚好让入水检测点在水面,也会判为没入水,人物会下落到水里再被人物放到水面,无限循环。

上浮的函数要在LateUpdate()执行,以防止修改y坐标被动画系统覆盖。

要在人物游泳&&离地时执行,否则靠岸时一直被放到水面上,直到穿入地下。

    protected void FloatToSurface(){
        if(swim&&!onGround){
            transform.position=new Vector3(transform.position.x,
            raycastHit.point.y-inWaterDepth-.1f,transform.position.z);
        }
    }

水面检测和地面检测的关系:在入水时,胸检测到水面,关闭重力,随后地面进一步下降,人物离地。出水时人物先触地,随着地面升高,把人物向上顶,人物的胸出水,结束游泳。有一段范围,人物既游泳又触地,这个范围人物是既不受重力,也不执行漂浮的。这样,再往深处,人物离地,执行漂浮,往浅处,地面把人物顶出水,开启重力。故而人物存在3种状态:

  1. 陆地上,受重力;
  2. 深水区,无重力,执行漂浮;
  3. 浅水区,无重力,无漂浮,以便向前两种状态过渡;

这是检测水面的方案。如果在水底放一个碰撞体,顶着人物,人物离地时,检测脚下是不是水底,是则进入游泳,就简单一些。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值