A project using this tutorial series is tracked on GitHub. The revision after this part is 137f023.

Hej!

In this part we will look at basic collision handling related to movement and make sure that our character doesn't go through walls anymore.

It is actually fairly easy to implement as Unreal does all the heavy lifting of doing low-level collision checking, however we need to make sure that "colliding" means to prevent walking through walls, and also try to slide along them instead. Luckily there is a function built into UMovementComponent called SlideALongSurface that helps with that.

The steps we need to take are:

  1. Set character collision profile to Pawn.

  2. Change MoveUpdatedComponent to SafeMoveUpdatedComponent.

  3. Check for any valid hits after moving.

  4. Call HandleImpact and SlideAlongSurface accordingly.

Collision Profile

First things first we need to set the collision profile of our character to Pawn (or something else that might be appropriate for your project). By default it is set to OverlapAllDynamic which won't trigger any blocking hits. In your equivalent of BP_CactusPlayerPawn, on the capsule component set the collision preset:

Movement Code

SafeMoveUpdatedComponent

First we are going to change MoveUpdatedComponent to SafeMoveUpdatedComponent. This is strictly not necessary for our code to work, however the difference is that the safe version automatically resolves initial penetrations, if we somehow start or end-up inside a wall. To facilitate this the safe version requires an additional FHitResult parameter (optional on the non-safe variant).

Replace our call to MoveUpdatedComponent(...) with:

cpp
12
FHitResult Hit;
SafeMoveUpdatedComponent(Velocity, Rotation, true, Hit);

Notice that we also set the bSweep parameter to true. This is what enables collision checking internally.

SlideAlongSurface

If you hit play now and try moving around you will notice that we won't go through walls anymore. However, unless we are moving directly away from a wall we instead stick to it!

This is because we have yet to handle what happens when we collide. SafeMoveUpdatedComponent automatically stops the movement but we need to slide along the surface as well. Luckily it's very simple; we only need to add the following snippet directly after the movement call:

cpp
12345
if (Hit.IsValidBlockingHit())
{
   HandleImpact(Hit, DeltaTime, Velocity);
   SlideAlongSurface(Velocity, 1.0f - Hit.Time, Hit.Normal, Hit, true);
}

HandleImpact is a helper to catch and propagate an impact throughout the movement component, however the base implementation is empty so in our case it is not strictly needed, but it's still good practice to include.

SlideAlongSurface is the main part and it simply computes and moves the component along a projected vector based on the normal (angle) and other parameters we give it. Internally it calls SafeMoveUpdatedComponent and HandleImpact like we've done.

Results

We should now correctly slide along any walls:

Interestingly, since SlideAlongSurface is not restricted to the horizontal plane we actually slide up slopes as well:

However we don't slide down as no collision is happening and we aren't applying gravity (which is what we will take a look at in the next part!).

Our full TickComponent function now looks like this:

cpp
123456789101112131415161718192021222324252627282930313233343536373839
void USimpleMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
                                             FActorComponentTickFunction* ThisTickFunction)
{
  Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

  if (ShouldSkipUpdate(DeltaTime))
  {
    return;
  }

  if (!PawnOwner || !UpdatedComponent)
  {
    return;
  }
  
  // Reset velocity
  Velocity.X = 0;
  Velocity.Y = 0;

  // Calculate force
  const FVector& Input = ConsumeInputVector().GetClampedToMaxSize2D(1.0f);
  const FVector DesiredInputForce = Input * MoveSpeed;
  Velocity += DesiredInputForce;
  const FVector MovementDelta = Velocity * DeltaTime;

  // Move
  const FRotator& Rotation = UpdatedComponent->GetComponentRotation();

  FHitResult Hit;
  SafeMoveUpdatedComponent(MovementDelta, Rotation, true, Hit);
  
  if (Hit.IsValidBlockingHit())
  {
    HandleImpact(Hit, DeltaTime, MovementDelta);
    SlideAlongSurface(MovementDelta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
  }
  
  UpdateComponentVelocity();
}