Shimmy State/ new ClimbingBlendTree

I finished this Unity 3rd Person Combat & Traversal course recently and wanted to continue working on the climbing movement some more. I’ve been focusing on a new PlayerShimmyState which allows my character to shimmy along the ledge by the X axis with mixed results, mostly very buggy. My original PlayerShimmyState.cs was merely an iteration of the PlayerPullupState.cs albeit along the X axis with a Braced Hang Shimmy animation from Mixamo, and this new state mostly works bar three main bugs:

  1. Even with an appropriate Offset Vector3 and a shouldFade boolean connected to the PlayerHangingState.cs (similar to the one connected to FreeLookState via PullupState) it looks awful, with my character snapping into the new position.
  2. I haven’t worked out a solution for the PlayerShimmyState to detect when the character slips off the Ledge along the X axis, as in, when the LedgeDetector collider separates from the Ledge collider, and so once in the ShimmyState my character can shimmy forever.
  3. Rather amusing but just because of animation constraints, my character can only move left even if I press down right.

I decided a cool solution would be to actually combine the PlayerHangingState, PlayerPullupState and this new PlayerShimmyState into a full-blown PlayerClimbingState with it’s own blend tree (2D Freeform Directional), parameters and animations for each axis, drawing mostly from the flexibility of the PlayerTargetingState.cs, and despite it currently being incomplete I really quite like this solution because it solves problems 1 and 3, and also looks a lot more fluid. However a new problem has come up: when I hit A or D to ‘shimmy’, the character comes off the ledge collider completely. It quite literally looks as if my character, when shimmying, is just back on the ground (by its CharacterController) but shimmying in mid-air, so it’s effectively not much different to a Target or FreeLookState in terms of movement. It occurs to me that the state machine in this configuration can no longer detect if I’m intending to slide along a ledge collider and relies on offsetting a Transform like in Pullup.

So I’m currently looking for a solution to upgrade my ClimbingState and making sure it all works. I’ve been looking into YT guides on ledge climbing and also Raycasting, but it seems I’m somewhat out of my depth here. I don’t want to rely on Offsets and booleans because except in PlayerPullup, it just doesn’t look very good and feels pretty rigid. I imagine if I find a solution it will require a lot of refactoring in several scripts, but I’m willing to do the extra work if I get a neat ClimbingBlendTree working.

Also the vast majority of my code right now is identical to the course material but if anyone wants me to post snippets, I can do that.

Thanks,
Maezumo.

Unfortunately, a lot of this is a combination of tweaks in the Unity Editor and in code.

While I can make no guarantees in this case, I can take a look at your code. Rather than snippets, however, I do a lot better if I can examine the whole state.

Sure thing. Again my PlayerShimmyState is very similar to PlayerPullupState, just along the x-axis instead.

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

public class PlayerShimmyState : PlayerBaseState 
{
    private Vector3 ledgeForward;
    private Vector3 closestPoint;
    private readonly int ShimmyHash = Animator.StringToHash("Shimmy");
    private readonly Vector3 Offset = new Vector3(1f, 0f, 0f);
    private const float CrossFadeDuration = 0.1f;
    private const float AnimatorDampTime = 0.1f; //?
    public PlayerShimmyState(PlayerStateMachine stateMachine, Vector3 ledgeForward, Vector3 closestPoint) : base(stateMachine)
    {
        this.ledgeForward = ledgeForward;
        this.closestPoint = closestPoint + Offset;
    }

    public override void Enter()
    {
        stateMachine.Animator.CrossFadeInFixedTime(ShimmyHash, CrossFadeDuration);
    }
    public override void Tick(float deltaTime) //can currently only move left
    {
        if(GetNormalizedTime(stateMachine.Animator, "Climbing") < 1f) { return; }
        stateMachine.CharacterController.enabled = false;
        stateMachine.transform.Translate(Offset, Space.Self);
        stateMachine.CharacterController.enabled = true;
        stateMachine.SwitchState(new PlayerHangingState(stateMachine, ledgeForward, closestPoint, false)); 
    }   //character snaps into place, looks pretty bad.
    public override void Exit()
    {
        stateMachine.CharacterController.Move(Vector3.zero);
        stateMachine.ForceReceiver.Reset();
    }


}

I’ll also post what I currently have for my PlayerClimbingState but it’s a work-in-progress and at the moment there’s not much to see. It’s mostly just a new movement blend tree with the Braced Hang Shimmy animations and I drew mostly from PlayerTargetingState. There’s some greyed out code in CalculateMovement() where I was experimenting with trying to add a Boxcast but I didn’t really know what I was doing even after checking out the guides. I was thinking maybe I have to do some extra work in LedgeDetector.cs?

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

public class PlayerClimbingState : PlayerBaseState
{
    
    
    private Vector3 ledgeForward;
    private Vector3 closestPoint;
    private readonly int ClimbingBlendTreeHash = Animator.StringToHash("ClimbingBlendTree");
    private readonly int ClimbingForward = Animator.StringToHash("ClimbingForward");
    private readonly int ClimbingRightHash = Animator.StringToHash("ClimbingRight");

    private const float AnimatorDampTime = 0.1f;
    private const float CrossFadeDuration = 0.1f;
    public PlayerClimbingState(PlayerStateMachine stateMachine, Vector3 ledgeForward, Vector3 closestPoint) : base(stateMachine)
    {
        this.ledgeForward = ledgeForward;
        this.closestPoint = closestPoint;
    }

    public override void Enter()
    {
        stateMachine.Animator.CrossFadeInFixedTime(ClimbingBlendTreeHash, CrossFadeDuration);
    }
    public override void Tick(float deltaTime)
    {   
        
        Vector3 movement = CalculateMovement(deltaTime);
        Move(movement * stateMachine.ClimbingMovementSpeed, deltaTime);
        UpdateAnimator(deltaTime);

        
        
    }
    public override void Exit()
    {
        
    }

    private Vector3 CalculateMovement(float deltaTime) //different from PlayerFreeLookState CalculateMovement()
    {
        //bool onLedge = Physics.BoxCast(stateMachine.LedgeDetector.transform.position, stateMachine.LedgeDetector.transform.localPosition, Vector3.zero, out ledgeHit,  );
        //if(!onLedge)  { return; }
        Vector3 movement = new Vector3();
        

        movement += (stateMachine.transform.right * stateMachine.InputReader.MovementValue.x);
        movement += (stateMachine.transform.forward * stateMachine.InputReader.MovementValue.y);
        
        return movement;
    }

    private void UpdateAnimator(float deltaTime)
    {
        if(stateMachine.InputReader.MovementValue.y == 0)
        {
            stateMachine.Animator.SetFloat(ClimbingForward, 0, 0.1f, deltaTime);
        }
        else
        {
            float value = stateMachine.InputReader.MovementValue.y > 0 ? 1f : -1f;
            stateMachine.Animator.SetFloat(ClimbingForward, value, 0.1f, deltaTime);
        }

        if(stateMachine.InputReader.MovementValue.x == 0)
        {
            stateMachine.Animator.SetFloat(ClimbingRightHash, 0, 0.1f, deltaTime);
        }
        else
        {
            float value = stateMachine.InputReader.MovementValue.x > 0 ? 1f : -1f;
            stateMachine.Animator.SetFloat(ClimbingRightHash, value, 0.1f, deltaTime);
        }
        
    }


}

Anyway if I can find some solution to re-attach my character to the ledge collider during a shimmy so he’s sliding along it and not just back on the ground doing a shimmy animation I’d be pretty happy with that and I can work from there. I could move PlayerHangingState into this permanently and treat it as a form of ClimbingIdle. Then I guess I’d need to work out how to handle the PlayerPullupState with maybe some boolean condition, I don’t know.

This is actually Vector3.right. Since it’s a readonly, it’ can’t be changed (not that you are anyways)

You are telling the stateMachine to move in one direction only.

So it looks like we’re entering PlayerShimmyingState from PlayerHangingState()… perhaps adding a float direction to the Constructor and passing in stateMachine.InputReader.Movement.x, then multiplying Offset * direction.

In terms of the smoothness of the movement… you’ve basically set this up to jump the entire unit at once. This would require some reworking to move by a speed * time.deltaTime and leaving the state when the goal has been reached.

For the PlayerClimbingState(), I see two issues… 1) You do not have any way out of this state… so you’ll be climbing forever… 2) You never set the stateMachine.transform.forward = to the ledgeForward (in fact, you never use either ledgeForward or closestPoint. This means as you move left or right, you’re not guaranteed to stay parallel with the ledge.

I was having trouble understanding this and after a few attempts at implementing this and getting nowhere, I decided to give up on the standalone PlayerShimmyState and instead integrate it into PlayerClimbingState and focus on that. I don’t really enjoy using Space.Self and offsets but I’ll keep it for PlayerPullupState and maybe return to learning Space.Self more in the future.

Made a little progress in the PlayerClimbingState today. I fixed the first issue of exiting the state, just by adding into the Tick() method the if/else if statements from PlayerHangingState for now, allowing me to switch to either PlayerPullupState or PlayerFallingState depending on what value goes into InputReader.MovementValue.y.

Also added Quaternion.LookRotation(ledgeforward, Vector3.up) in Enter(), same as in PlayerHangingState (which will eventually become the Idle part of PlayerClimbingState). As I experiment with the Transforms my Enter() method is a joyous mess and currently looks like this (sorry for the load of greyed text - it’s how I learn!):

public override void Enter()
    {
        stateMachine.Animator.CrossFadeInFixedTime(ClimbingBlendTreeHash, CrossFadeDuration);
        stateMachine.transform.rotation = Quaternion.LookRotation(ledgeForward, Vector3.up);
        stateMachine.transform.position = closestPoint; // - (stateMachine.LedgeDetector.transform.position - stateMachine.transform.position);
        //stateMachine.LedgeDetector.transform.position = closestPoint;
        //causes LedgeDetector transform to sit at the bottom of the stateMachine transform.
    }

This of course has the amusing effect of the very bottom of my stateMachine.transform to sit on top of closestPoint. I’m having a lot of trouble understanding closestPoint, I’ll be honest.
1

The line in Enter() which mentions closestPoint is one I grabbed from PlayerHangingState, greyed out bit included. If I add the whole line - which I was banking on to work - it unfortunately drops my character back onto the ground. Nevertheless, I feel like I’m getting a little closer to a solution. I just need help understanding how to write a line that accounts for closestPoint, so that my character can slide along the ledge using the LedgeDetector transform (the tiny Box Collider that he’s holding).

At least in this setup, he can actually slide off the ledge and gravity kicks in. So in Tick(), along with adding the line;

stateMachine.transform.forward = ledgeForward;

and also adding SwitchState logic to exit the state, the only other thing I tried was adding an OR operator into the else if like so:

else if (stateMachine.InputReader.MovementValue.y < 0f || stateMachine.CharacterController.velocity.y <= 0) 
        {//if you press S or whatever is backwards
            stateMachine.CharacterController.Move(Vector3.zero);
            stateMachine.ForceReceiver.Reset();
            stateMachine.SwitchState(new PlayerFallingState(stateMachine));
        }

But this doesn’t work because as the stateMachine.transform (or should I say, ledgeDetector.transform) is still not set up properly on closestPoint, the moment I enter a shimmy I am apparently falling. So except for InputReader.MovementValue.y floats I’m unable to implement any clear logic for falling when he slides off the side of the ledge.

Anyway I know this was a long post. I’m very committed to getting this to work and I’m starting to enjoy it quite a lot!

The closest point issue needs a bit more than assigning Closest point… Here’s the assignment from PlayerHangingState (remember that Nathan faced a similar issue).

stateMachine.transform.position = closestPoint - (stateMachine.LedgeDetector.transform.position - stateMachine.transform.position);

Yep, when applied to Enter() that causes my character to drop back to the ground, although it does not limit his movement along the X:

2

However, when applied to Tick(), he snaps onto the ledge in the correct position, but it does limit his movement. When I add “+ movement * Time.deltaTime” to the end of the line he moves a little bit but then returns to his original position. I’m aware that this is an incorrect way to implement the movement. Thoughts?

Vectors and movement are not my strong suit, some extra study required it seems.

I don’t have the course code in front of me atm, but I seem to remember something about resetting the ForceReciever’s y force every frame in the PlayerHangingState and PlayerClimbingState… We would need to do a similar thing here.

Scratching my head for days on this one and then I literally just added “stateMachine.ForceReceiver.Reset();” into the Tick() method of the PlayerClimbingState and hey presto it actually works - my character now shimmies along the ledge. Very happy with this because it had me stuck for ages.

Of course, more work needed to be done. I still need to work out how to do things like rotate the character around a ledge corner (probably just more usage of ledgeForward) and add a proper PlayerFallingState switch prompt when I shimmy off the side of the ledge (or I could just disable it somehow with a Lerp? Or even just OnTriggerExit!) but either way I’m grateful for the progress made and your assistance Brian. I’ll be off continuing to make my advanced climbing even more advanced…then maybe I’ll throw myself into IK.

Great work! I tried not to give it away completely, as I could see you were already thinking through the problem.
You’re on the right track to get some Assassin’s Creed like moves going.

Starting to use this growing post as some kind of devlog. My ClimbingState is coming along quite well. I added functionality to fall off the side of a ledge, by creating a new HandleLedgeLeave event tracing back to the OnTriggerExit in LedgeDetector, which the ClimbingState subscribes to. This works like a charm and triggers the PlayerFallingState with pretty much no issues at all.

However I’ve disabled that for now because I really wanted to add the ability to move around ledge corners (I’ll reactivate the FallingState once I figure out some logic to have both scenarios work in harmony). This required some further study of vector math, namely cross/dot products.

Tarodev has a great YT video on using Vector3.Cross() and utilising it by rotating a cube. My character transform is essentially doing the same thing, it’s like rotating a cube around a ledge collider. Kind of like this:

Unfortunately I’m still unable to get this to work on my state machine. The closest I’ve had is getting my character to slide, via the ledgeDetector.transform, around the corner using Quaternion.LookRotation(), but it’s wildly inconsistent and he does not rotate to face into the wall as desired. Also this worked only for the right hand side of the ledge - the left hand side I go backwards away from the wall, looks like a zigzag - so clearly my choice of vectors are wrong. But I feel like I’ve thrown every combination of vector parameters into the code that I can think of.

private void HandleLedgeLeave() 
    {   
        var anchor = stateMachine.LedgeDetector.transform.position + (Vector3.forward + movement) * 0.5f; 
        Vector3 axis = Vector3.Normalize(Vector3.Cross(ledgeForward, movement)); 
        
        for (int i = 0; i < (90 / rollSpeed); i++)
        {
            stateMachine.transform.RotateAround(anchor, axis, rollSpeed); //like rolling a cube
        }
        //stateMachine.SwitchState(new PlayerFallingState(stateMachine));
    }

This code lacks the Quaternion.LookRotation() I used to get him around the wall originally, and also I have not once managed to get transform.RotateAround() to work. No idea what I’m doing wrong at the moment asides from simply using the wrong vector parameters. Despite this, I feel this PlayerClimbingState has come a long way. More study required.

I wonder if the Quaternion.LookRotation() in Enter() might be blocking my character from rotating at all?

1 Like

It shouldn’t stop you from rotating in the Tick() method… it just sets the initial rotation.

For cleanup purposes, I’m going to change this from the Ask forum to the Talk forum (it won’t affect how this thread works, but it will keep the topic open without dealing with bumping or not having a “solution”.

I’ve finally achieved my original goal of advancing the climbing state to something I’m quite happy with. I’ll post the most recent touches below. To summarise though it’s effectively a new state which combines shimmying, pull-up and falling into one whole blend tree similar to say the TargetingState. There is no raycasting or root motion IK involved and it is by no means perfect and could be improved upon still, and unfortunately the hands came off the ledges again, but I can at least shimmy around ledges given that they are 90 degrees. This became an exercise to see how far I could go personally without adding raycasting or IK and I think it turned out reasonably well.

private void HandleLedgeLeave(Collider other) 
    {   
        if(stateMachine.InputReader.MovementValue.x > 0f) {movement = Vector3.right * -1;}
        else if (stateMachine.InputReader.MovementValue.x < 0f) {movement = Vector3.left * -1;}

        Debug.Log(other);
        Debug.Log(other.gameObject.transform.position);
        float rollSpeed = 3f;
        var anchor = stateMachine.LedgeDetector.transform.position + (stateMachine.LedgeDetector.transform.forward) * Mathf.Epsilon; 
        var axis = Vector3.Cross(Vector3.forward, movement); //Cross-product is simply Vector3.up...
        
        _isMoving = true;
        for (int i = 0; i < (90 / rollSpeed); i++)
        {
            stateMachine.transform.RotateAround(anchor, axis, rollSpeed); //like rolling a cube
        }
        _isMoving = false;
        //stateMachine.SwitchState(new PlayerFallingState(stateMachine));
    }

(The Debug.Logs aren’t important… I just wanted a way to get reference to the ledge that the LedgeDetector collides with. More complicated than I thought because it concerned an event Action, meaning I had to pass the Collider ‘other’ as a parameter in OnLedgeLeave.)

Some notes about the code which is rather untidy. I largely drew from Tarodev’s YT tutorial on rotating a cube (code is freely available online) and spent a while getting another cube to rotate on it’s own pivot point in the opposite direction of the x-input, so that within 3 rotations it returns to where it started. Really just an exercise to teach myself vectors some more. The hard part was translating that over to this state when it isn’t simply written as Input.GetKey etc. This is what the if/else if lines do. Before that it was days of rotating right no matter what.

One issue with this is that it’s somewhat designed to rotate around a given distance (i.e. cube * 0.5) and it’s pretty obvious that it doesn’t stay aligned with the ledge given a few turns. Apart from raycasting, which I’m moving onto later, I don’t really have a decent solution however I noticed that adding Mathf.Epsilon helped a bit. The isMoving bool isn’t entirely necessary although if you ran this Climbingstate you’d notice that maybe without it you might come off the wall. Needs a bit of work.
Also due to ledgeForward still being used on initial hanging, you will end up at the front of the wall regardless of whether you grabbed onto the front, sides or back of the wall. ledgeForward has caused me a lot of stress and I’m eager to change it to grab the data of collider bounds somehow.

The RotateAround method works even though it contains a float rollSpeed which is entirely irrelevant. This was meant to be part of a co-routine until I learned that co-routines only work in Monobehaviours, and PlayerClimbingState obviously cannot inherit from Monobehaviour as well without some encapsulation or something which, at the moment, I can’t bothered implementing lol

This is probably my last post on this little project because I’d like to finally move onto other things (raycast, IK, procedural animation), but anyway this ClimbingState can now for the most part shimmy along walls and around them at 90 degrees, with pullup and falling accounted for, and I’m pretty satisfied with that. :+1:

~ Maezumo

2 Likes

Congrats, I too get excited when I do something big!

1 Like

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms