Matt Hoffman Profile picture
31 Dec 19, 101 tweets, 45 min read
Code doodlin'.

Sprinting, Crouching, Double Jump, Sliding. Hard to differentiate without sound, but anims are slightly modified for each state.

Double Jump allows changing direction on second jump. Slides end in a crouch, or can launch you if jumping while sliding.

#UE4
Programming is often about building stuff you've never built before (and giving time estimates!).

Let's look at how one might go about building a gameplay feature for a first person game - wall running in #UE4.
The first thing I do is come up with a general idea of what it needs to do. This might involve researching existing games, or looking at what you currently have and decide what isn't good about it.

It doesn't need to be the complete list of features, just a direction to go.
To wall run in a game, you want to run at a wall, jump at it, travel along it for a period of time and be able to jump off of it.

There's a lot more detail to it (animation, leaning, sounds, limiting jump count, etc.) but we now have a direction to go in.
So let's look at the first one. How do we detect a desirable wall surface? Research + thinking says that you don't want to detect a viable surface while on the ground, otherwise every slight bump into a wall will end in an state change, plus 'a step' vs. 'wall' isn't clear.
So we care about walls when we're not touching the ground. How do we detect a wall? We could cast a sphere to our left and to our right every frame and see if they hit a surface while we we aren't touching the ground, instead I've gone with doing a simple overlap check.
The overlap is cheaper than two sweep checks, and we're really only interested in detecting if we should -start- wall running. We can change the logic (and will need to) once we've detected a viable surface to start on.

#UE4 Image
We can see here that it only does the check while we're in the air, and that I've now gotten 'stuck' against the wall - the StartWallRunning() from our previous code simply sets our Movement Component to a CustomMode that does nothing.

#UE4 Image
The other thing I programmed for now is that the Jump function checks if our CustomMode is set to wall running and if so, sets us back to falling so we can get out of our custom mode.

Because we can now enter and exit this mode, we've now created an iteration loop.
This implementation isn't great - it's not even good, because as soon as you start falling off the wall you want to stick to it again, but we can eventually force our self off with air control.

#UE4
This already gives us a problem that we can think about solving, but it's less interesting than the actual wall movement to me right now, so I'll ignore the issue.

Now we need to think about how to run along a wall now that we've detected a viable wall surface.

#UE4
When building walking movement (UCharacterMovementComponent::PhysWalking) we can see that it handles cases where the surface changes (slopes) or has abrupt bumps (steps) in both positive and negative directions. This helps you 'glide' over uneven terrain.

#UE4
We probably want similar for our wall running - we could prototype it for flat surfaces, but experience says a designer will immediately put a tiny lip in by accident between two surfaces and run into issues, so we know we have to handle that in our case too.

#UE4
But you know what, this is getting ahead of ourselves. We've decided that we've overlapped a 'wall' that could be a viable wall, but we don't actually know which direction our wall is in!

Now we can break out the more expensive raycasts.

#UE4
I've followed a similar 'latent' approach to wallrunning as crouch - the ACharacter tells the movement component it wants to do that, and then on the next available tick the character makes the transition at a safe point.

#UE4 Image
I got this far, and was about to write a 'check left, check right' code, but then realized you might want forward and back too and this was getting to be a lot of code, so I left my desk for a minute.

Upon return, I realized we can just sweep in the direction of velocity.

#UE4 Image
I also wanted hit info, so the overlap test wouldn't work.

The velocity should be the direction we're moving in, which should reliably give us the surface the player is intending to go for.

#UE4
Technically if they were perfectly parallel to the wall, they'd 'miss it' even though our character thought we could enter wall running. It's pretty unlikely to happen (I think).

We don't want the owning character to get stuck in an invalid state where it thinks we've

#UE4
entered wall running, but the movement component hasn't. This isn't an issue, because the Character reads the wall running state from the CMC. Minimizing state duplication (ie: them tracking separately) reduces potential for hard to track down edge case bugs like this.

#UE4
(This is iterating with the new Live Coding feature which lets me test changes in ~15-30s while the game is running)

We can see here the green normal on hit. We test against a few walls as well to ensure it works as expected.

#UE4 ImageImage
I create a new struct to hold our wall information. We could just use the FHitResult info, but we may want to expand this with more variables in the future - start time, previous hit, etc. Easier to make a custom struct now instead of type changing later.

#UE4 Image
Now we can start thinking about our custom movement. We touched earlier on the idea that we want to 'glide' over the surface, but let's start with the assumption of flat walls.

PhysCustom (PhysWallRun) isn't going to move your character at all, that's on you now.

#UE4 Image
Let's assume for a second that we want to ignore any Z movement so we stop falling. As a simple test, we'll use cross product between up and out to find 'forward' for the wall, and move you based on your velocity in that direction.

#UE4 Image
As with most things, it's okay if they don't work right the first time. A decade of doing this, and I mix up the argument order for cross product. Easy enough fix!

It exposes some more things to think about though!

#UE4
Obviously we didn't stop running when the wall ran out. And we can't speed up or slow down, but... we can move across walls!

A variety of test cases is important, sometimes I go in the direction I want, and sometimes it's actually the opposite!

#UE4
If we look back at the code, we can see that we didn't take the user's velocity into account at all, so how is it going to know which way is actually the right way?

For now, I'll dot product it with users velocity, and invert 'forward' if it's wrong.

#UE4 Image
I know this might be problematic in the future (if you can slow down and run the other way during the same wall run action), but good enough for now.

Now the 'drifts into the void' problem. Time for our friend from before, the wall check!

#UE4 Image
Again, progress and more problems. There's a certain stickyness on some corners (where you wrap around the corner), and sometimes when jumping off you try to wall run on the floor.

This is because we never cleared the bWantsToWallRun flag after moving to the Falling state.

#UE4
Clearing this flag only solves the 'wall run on floor' problem, because the conditions for starting a new wall run are "in air" and "touching wall", so the first problem still exists. The solution is probably either a minimum time between end/start or similar, avoiding now

#UE4
Because we're not really manipulating the velocity, it does weird things on exit such as me 'jumping' around the corner.

To fix this, we should probably go back to the earlier problem about "can't speed up or slow down" too.

#UE4
Skimming the PhysWalking function, it looks like there's two key things it does;

CalcVelocity(...), and MoveAlongFloor(...)

CalcVelocity handles acceleration, max speeds, braking forces, etc.

#UE4
Different movement modes (walking, flying, swimming) have different max speeds. If the velocity was just clamped to that max each frame, transitioning between them would cause 'hitches' where you slam to a stop if the new speed is lower than current.

#UE4
Unreal handles this by applying an additional braking force (ontop of friction) scaled by the amount you're over the current max. This softly brings you down to the current max without slamming you to a halt. Looks like we can override GetMaxAcceleration and GetMaxSpeed too

#UE4
This way we can use the built in GetVelocity() function, but override the speed/acceleration functions to use our correct numbers if we're in the right state!

I want to verify where player input is taken into account first though to allow speed up/down.

#UE4
It looks like acceleration is set each tick (before PhysCustom is called) by first consuming the input vector, and then scaling it, so CalcVelocity should take user input into account by extension of the user 'applying' acceleration.

PerformMovement calls PhysCustom (iirc) ImageImage
Alright, back to progress now that we've spent a while researching.

In the WallRun function I've added a call to CalcVelocity (copy/pasted from walking movement), and it's turned out to be... really surprisingly helpful!

#UE4 Image
Now that we've added CalcVelocity(...), we no longer pop off in strange ways at the end, plus it's a lot easier to dismount because our velocity is now (nearly) zero so we don't get shoved back into the wall to start another wallrun immediately. There's friction too.

#UE4
So what next? I think the next part would be to handle limiting how long you can wall run before you automatically detach, and jumping vs. falling out of a wall run. If we make jump force us away from the wall we can start to chain together multiple wall runs :D

#UE4
Now, where does the responsibility lie for determining that you've exceeded your wall run duration? Is it in the Movement Component? The Character? Or the Controller?

I don't actually have the right answer to this!

Let's think about our options one by one.

#UE4
We could put it in the movement component. There is precedent for a movement state automatically transitioning to another (walking -> falling, falling -> walking, both -> swimming, etc.), but... I don't think this is the right choice.

#UE4
For the most part, the CMC is about moving (according to an externally applied input vector) and resolving collisions (in my opinion), and I don't think "how long can I wall run" falls under it's umbrella.

The next choice is the Character (Pawn)

#UE4
The character takes intentions (ie: jump, crouch) from a AI/Player controller. In a multiplayer context, both the CMC and the Pawn exist on all machines (but not necessarily the controller). It would be nice to have an API that was simply:
StartWR -> InDir -> StopWR

#UE4
I don't plan to make this work in multiplayer, nor write an AI that uses it, but still worth considering the structure with their context.

An AI/PlayerController is supposed to represent a physical controller, and it seems odd to tie wall run duration to that.

#UE4
So, mostly by process of elimination, I've decided "how long can you run on a wall before falling off" lies in the Character actor (who also owns the StartWallRun/StopWallRun functions, handy for tracking time!)

We'll see if this bites me later :)

#UE4
Taking a moment to re-organize my variables, putting the max speed and friction into my Movement Component and adding the Max Wall Run duration.

This is the first time I've had to restart the editor (adding PROPERTIES) since I started building this feature.

+ Food break.

#UE4
I've left my "Max Sprint Speed" variable in the Character, and not moved to the CMC. Sprinting is achieved by taking walking movement + overriding max speed, and isn't new behavior. I think that will replicate in MP too correctly, without a multicast call for StartSprint()

#UE4
I made a tweet earlier about consolidating state - even though we just agreed that it's the characters responsibility to track time, we can't actually track "wall run start time" in the character, because StartWallRun() could fail.

#UE4 Image
Okay, back to our character now. In the Tick function, we add a simple time check.

This should disconnect us from the wall and let us fall. Except we trigger the StartWR check on the next frame. So, how do we decide how often we can wall run?

#UE4 Image
A simple way is just a minimum refire time, ie: only allow starting a WR every 1s. This might cause your designers headaches though and limit the geometry they can create. Issues:
1) Really tall wall, more than 1s fall time
2) Jump away from wall & return.

#UE4
1) can be worked around by design team (no really tall walls)
2) If we increase the time, we limit them from going onto other surfaces, which may be at 30, 45, 90 and players would expect those to be different surfaces.

So I did some more research.

#UE4
It looks like a combination of two things might be a good solution.

Require either a minimum refire time since last wallrun ended, or, require a significantly different wall normal.

This allows us to increase the minimum time without affecting intended movements.

#UE4
(Yes technically you can keep chaining back onto the same wall again this way, but if you set the time high enough then you can make them drop height with each successive jump)

#UE4
Since we're avoiding duplicating state, we can track when we've successfully left a wall run by overriding
void

UCharacterMovementComponent::OnMovementModeChanged(...)

This helpfully tells you what the previous state was, and the current state. Also track prev. wall

#UE4 Image
Small conundrum; The CMC function that actually starts the wall run knows about the surface normal. We need that info + min refire time to decide if we actually start a WR.

But earlier we decided the max-wallrun-duration is handled by the Character.

#UE4
For a lack of a better idea, we're going to add an event to the CMC.

There's precedent (Landed event), and it might be useful to know when (and with what details). And if someone sets bWantsToWallRun to false as a result of that event, we'll handle it.

#UE4
The flow isn't great, but I'm more interested in shipping than perfect. Though apparently precedent was wrong, it's actually the Character with the landed event, and the CMC specifically calls that function.

I'll just copy what is there rather than try to be better.

#UE4
Now before starting the wall run we call AJumpkitCharacter::OnWallRunStart and then check again if we still intend to wall run before actually changing state.

Didn't return bool b/c Blueprint Multicast Delegate for event might be nice, no returns for those.

#UE4 Image
Plus, at face value, that's a confusing API.

bool OnWallRunStart()

The function name doesn't imply what the return value would do at all, so better to just handle the case where someone calls Stop as a result of Start IMO.

#UE4
I think this does what we want. When we try to start wall running, if not enough time has passed and the wall normals are too similar, then we cancel the wall run before we start.

#UE4 Image
Looks like we have success! Now when you stop moving, time runs out and you fall off the wall it refuses to start another one because it's too similar to the last wall, and we finally no longer re-stick to the surface!

#UE4 Image
I need to adjust the timing and thresholds, but this seems to do what we want. It prevents endless retries on the current wall without limiting how fast you can switch walls. Landing also needs to reset the 'time since last wall run' to allow jump & drop onto same wall.

#UE4
A small polish thing we can do (borrowed from Titanfall) is give them a small boost away from the wall to visually indicate 'disconnection' once wall run time runs out. Helps communicate w/o sound since the user didn't initiate the action.

#UE4 Image
Now you can either leap off walls or naturally fall off of them (and get scooted away).

If this was a JIRA I'd probably check it in this point as "Prototyped" and let the designers start to play with it, tweaking timing and values.

#UE4
It's not the end of the story though - this will go poorly as soon as it meets the real world, ie: anything other than a perfectly flat surface, one that curves, etc. The sticky corner thing is still an issue too.

So there's still work to be done on it (at a later time)

#UE4
Picking this one back up with... more research! The next goal is to make us 'glide' along walls, stepping over small bumps, and following curves.

So let's look at how walking does this, since we're implementing walking... but sideways!

#UE4
This is mostly covered by the MoveAlongFloor function which covers it a reasonably high level.

ComputeGroundMovementDelta projects your velocity onto the floor to try to find a vector that doesn't push you into the ground but instead travel along it.

#UE4 Image
The next step is to just naively move you. There's three possible results of this move;
A) We started inside an object (bStartPenetrating) and can't move.
B) We didn't start inside an object, moved, and did hit something.
C) We moved and didn't hit anything

#UE4
If we started inside of something, we try to push it off (HandleImpact). Then we try to slide along the surface we're stuck in (more on this later). If we still can't move, notify someone, because we're dead in the water.

The second case is a little more nuanced.

#UE4
If it moved at all, and the slope upwards, we re-compute the slope and try the move again. It carefully keeps track of how much of the move you've gone - say you asked it to move 50u, and it made it forward 10u before hitting something. It'll try to move 40u up the slope.

#UE4
Now, line 4723 it checks to see if we hit something again. It only appears to handle one slope check before assuming it's a step - this is probably fine because movement is usually a pretty small step, so unlikely to hit multiple planes of a ramp at once - and if you do

#UE4
Then we just take the slightly more expensive option, which is to treat it as a step instead of a slope, so mostly an optimization imo.

If stepping up fails, it assumes it's a wall and slides you along the surface. If you can't step up (object says no), you slide.

#UE4
StepUp and SlideAlongSurface are the most useful. SlideAlongSurface is a great function that takes a delta and a normal to slide along, and handles a bunch of vector reprojection, two-wall impact adjustment, etc. Good news: We can use the function as is, out of the box!

#UE4
StepUp handles attempting to step up onto a surface, so it goes up, forward, then down. Unfortunately it makes some assumptions about what "Up" is (Z+) so we will have to write our own version of this function, so that's our next task... after building a test environment.

#UE4 ImageImageImageImage
Whipped up a quick test level using the new in-editor Geometry Tools (textures not included).

This showcases how it falls apart - can't stick to the outside of cylinders, stuck on the inside of cylinders, small ledges cause you to fly off or fully stop you.

#UE4
This gives us pretty good test cases to make sure our new "glide over the surface" function works. I think all of these situations are OK in Z-up situations so I'm hoping that mostly following the existing pattern will Just Work™️.

#UE4
Here's a copy/pasted implementation of MoveAlongFloor. I took out the slope check on the second try for now.

Our behavior will be slightly different than a floor in the end - normally if you don't hit anything you transition to falling, but we need an extra surface 'snap'

#UE4 Image
I think this is all the new ComputeGroundMovementDelta function needs - the old one had special handling for Z but we actually want all three axii (Z included) so that when you hit a wall, you preserve your up/down momentum to keep it from slamming you to a halt.

#UE4 Image
If you've ever wondered why out of the box most character controllers don't support rotating (pitch, yaw) your character, is because this kind of math gets a lot harder, since now you're dealing with a lot of vector distances and directions, instead of just manipulating Z.

#UE4 Image
So how does one go about 'translating' a function like this anyways?

I start re-writing it, leaving compile errors where I don't understand it, or there's not an obvious conversion. As I write more of it, I understand the intention more which makes decisions easier.

#UE4 Image
Also getting up, taking breaks, having a cat nap in the middle are all great ways. Pieces tend to fall into place better when I'm not trying to think about them.

#UE4
It looks like #UE4 provides a "Scoped Movement Update" for optimization, where you can move the component multiple times by calling the standard MoveUpdatedComponent, but the propagated results (updating child transforms, etc.) aren't broadcast until it leave scope. Image
It looks like it provides a good way to roll back a failed move as well with less hand management.

#UE4
(This has turned into a rabbit hole of additional functions that need updating, send caffeine)

#UE4
As expected, once it compiled again it worked flawlessly.

Yes it's definitely supposed to throw you across the world when it feels like it, why do you ask

#UE4
So, how do we debug something like this? Patiently.

It's time to go through and make sure the reality matches the expected behavior. This means constructing simple test cases (ie: wall /w (1,0,0) normal), stepping through code, and validating the results.

#UE4
The first thing I noticed is that my ComputeWallMovementDelta function (which flattens your velocity in the direction of wall) isn't giving the right results. I can tell this b/c I made a simple test:

WallNormal(1,0,0)
Velocity(-5, 38, 90)

Expected: (0, 38, 90)

#UE4
What I got wasn't anything close to that. I knew theoretically that flattening against a simple x-only normal should produce a zero in the X. This lead me to try other fn's and find FVector::VectorPlaneProject is what I want and not FVector::ProjectOnToNormal.

#UE4
If I had tried that with a complex test case (ie: WallNormal(0.5, 0.5, 0)) I couldn't intuitively create an expected outcome and I would have had to learn how to do math, I don't want that.

Next, I discover that I fail to set the WallNormal in my "Find Wall" fn.

#UE4
Easy enough fix, simple oversight (too much copy/paste, not enough think).

Now, stepping through it, I wall run perfectly for one frame, but on the next I get stuck. Now I can step through functions I know worked the first time and debate why they're different second.

#UE4
On the first frame your velocity still pushes you into the wall. By the second frame, your velocity has had the wall normal taken out of it (so 0 in our X axis as earlier). Because our walls are nice and flat, we don't do the 'step up' logic (yay optimization)

#UE4
Which means when it goes to find the wall again it tries to use your velocity (instead of the 'step down' result). We just zero'd out the X in our velocity, therefor it doesn't look in the direction of the wall, and stops finding the wall, and we stop running.

#UE4
This means that we probably need to use the 'current' wall normal instead of velocity, but that means I need to verify that there's always a current wall before using that to find the wall normal. Hmmn.

#UE4
I was worried that it wouldn't always be valid (thus the "hmm"). So I put in an ensure(...) before using it.

Hey guess what, it's not always valid. So now I can back track why after several frames there's no longer a valid wall.

Programming = making problems to solve.

#UE4 Image
At least with this you can put breakpoints on all the spots where you say "wall is invalid", disable them, and use one breakpoint to get yourself into a 'known good' state, re-enable the other BPs, and then let it run.

Faster than stepping through all BPs each time.

#UE4
Turns out, there's an even faster way to debug... look at what was happening in the world when the ensure hit.

Turns out, code works as expected. I ran out of wall the previous frame. So there shouldn't have been a current wall.

#UE4 Image
So now I need to re-think the intent, because the code works as expected.

I need to discover when we've run out of wall, and stop trying to wall run, instead of continuing the run next frame and having no wall.

This stuff is the hardest part of programming imo -

#UE4
The code works as intended, the design is wrong. Now you enter the debate about why; Are the assumptions wrong? Or am I trying to use the wrong flow of code? Do I add code, or do I need to change what I have? This gets harder as more possible states are introduced.

#UE4
After taking a break, I remembered that the PhysWalking had a nice big if chunk I had skipped.

Clearly there is the intention here that you could change states as a result of MoveAlongFloor. I couldn't figure out how at the time, but precedent gives me a design direction.

#UE4 Image
After re-checking the MoveAlongFloor function and finding nothing that would change the movement mode, I put a BP in that else block. It doesn't seem to get hit in the expected case(walking off edge), but further down code does.

Good reason to validate your assumptions!

#UE4 Image
We can technically run across walls again! Testing on the more complex map it appears that some of the issues have been fixed, ie: Running on the inside/outside of cylinders counts as a single wall run instead of a bunch of segments.

I've created sticking issues again.

#UE4
Looks like I lost the call to CalcVelocity(...) while working, so having added that back you can now run at speed, and don't launch off the top so easily (drag slows your Z component down).

Now that going around a cylinder works, you can see the view doesn't turn with.

#UE4

• • •

Missing some Tweet in this thread? You can try to force a refresh
 

Keep Current with Matt Hoffman

Matt Hoffman Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

PDF

Twitter may remove this content at anytime! Save it as PDF for later use!

Try unrolling a thread yourself!

how to unroll video
  1. Follow @ThreadReaderApp to mention us!

  2. From a Twitter thread mention us with a keyword "unroll"
@threadreaderapp unroll

Practice here first or read more on our help page!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3/month or $30/year) and get exclusive features!

Become Premium

Too expensive? Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal Become our Patreon

Thank you for your support!

Follow Us on Twitter!