Overview
Default Unreal supports a couple of general movement states like flying, swimming, walking, etc. There are many examples of games and projects that need to add onto or make new movement states. For multiplayer games, it its important that the locally controlled player and the server can simulate these new movement behaviors so reconciliation or "rubber-banding" is kept minimal.
Adding you own custom movement mode in multiplayer games requires a bit of extra work done within the CharacterMovementComponent (CMC). The type and amount of work really depends on the custom movement feature you are implementing.
This post aims to outline the process of implementing your own custom movement mode.
There are many great sources and documentation on the CMC and I highly recommend reading Unreal's official documentation.
*This post does not cover extrapolated/interpolated movement of simulated proxies, instead this only relates to smooth movement prediction between the locally controlled client and server.
Custom Movement State
Adding a new movement state should only be necessary if the other movement states can't be built upon or are still needed fundamentally.
Examples of cases to use a new movement state are:
Wallrunning: Needs advanced handling of movement totally different than regular walking.
Mantling: Needs handling of what object is being mantled and separation with walking state.
Examples of cases to not use a new movement state are:
Sliding: Can be handled during walking state since it still represents us on the "floor".
Grappling: Can be handled during the Falling state as gravity still affecting.
To add a new custom mode you just reference the CustomMovementMode enum. This is just a uint8 so I recommend making your own enum type.
We know if we are in a custom movement state if MovementMode is MOVE_Custom.
/** Custom movement modes for Characters. */ UENUM(BlueprintType) enum ECustomMovementMode { CMOVE_CUSTOM1 , CMOVE_MAX UMETA(Hidden), };


Prediction Data
Whether or not you are using a custom state, you most likely need prediction data to predicatively indicate actions and/or player inputs. With the example of sliding and wallrunning, I used a flag to indicate that the desired movement state is requested by the client.
Prediction data is done by implementing two classes: the FSavedMove_Character and FNetworkPredictionData_Client_Character.
FSavedMove_Character is the base class that holds all the data to be sent from client to server. You will need to add your own data and flags if necessary. The following functions will also need to be implemented.
// Placed in header of movement component class FSavedMove_MyMovement : public FSavedMove_Character { public: typedef FSavedMove_Character Super; ///@brief Resets all saved variables. virtual void Clear() override; ///@brief Store input commands in the compressed flags. virtual uint8 GetCompressedFlags() const override; ///@brief This is used to check whether or not two moves can be combined into one. ///Basically you just check to make sure that the saved variables are the same. virtual bool CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const override; ///@brief Sets up the move before sending it to the server. virtual void SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData) override; ///@brief Sets variables on character movement component before making a predictive correction. virtual void PrepMoveFor(class ACharacter* Character) override; ///@brief Saved input movement direction of player FVector SavedMoveDirection; ///@brief Flag for activating sprint. uint8 bSavedWantsToPowerSlide : 1; uint8 bSavedWantsToWallRun : 1; };
// placed in movementcomponent class public: friend class FSavedMove_ExtendedMyMovement; virtual void UpdateFromCompressedFlags(uint8 Flags) override; protected: UPROPERTY(Transient) uint8 bWantsToPowerSlide : 1; UPROPERTY(Transient) uint8 bWantsToWallRun : 1;
//Set input flags on character from saved inputs void UYourCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags)//Client only { Super::UpdateFromCompressedFlags(Flags); bWantsToPowerSlide = (Flags & FSavedMove_Character::FLAG_Custom_1) != 0; bWantsToWallRun = (Flags & FSavedMove_Character::FLAG_Custom_2) != 0; } void FSavedMove_MyMovement::Clear() { Super::Clear(); SavedMoveDirection = FVector::ZeroVector; bSavedWantsToPowerSlide = 0; bSavedWantsToWallRun = 0; } uint8 FSavedMove_MyMovement::GetCompressedFlags() const { // Result # 1 and 2 taked by jump and crouch respectively uint8 Result = Super::GetCompressedFlags(); if (bSavedWantsToPowerSlide) { //Result |= (1 << 2); Result |= FLAG_Custom_1; } if (bSavedWantsToWallRun) { //Result |= (2 << 2); Result |= FLAG_Custom_2; } return Result; } bool FSavedMove_MyMovement::CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const { if (bSavedWantsToPowerSlide != ((FSavedMove_MyMovement*)&NewMove)->bSavedWantsToPowerSlide) { return false; } if (bSavedWantsToWallRun != ((FSavedMove_MyMovement*)&NewMove)->bSavedWantsToWallRun) { return false; } if (SavedMoveDirection != ((FSavedMove_MyMovement*)&NewMove)->SavedMoveDirection) { return false; } return Super::CanCombineWith(NewMove, Character, MaxDelta); } void FSavedMove_MyMovement::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData) { Super::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData); UYourCharacterMovementComponent* CharMov = Cast<UYourCharacterMovementComponent>(Character->GetCharacterMovement()); if (CharMov) { //This is literally just the exact opposite of UpdateFromCompressed flags. We're taking the input //from the player and storing it in the saved move. bSavedWantsToPowerSlide = CharMov->bWantsToPowerSlide; bSavedWantsToWallRun = CharMov->bWantsToWallRun; // Taking player movement component state and storing it for later SavedMoveDirection = CharMov->MoveDirection; } // Round acceleration, so sent version and locally used version always match Acceleration.X = FMath::RoundToFloat(Acceleration.X); Acceleration.Y = FMath::RoundToFloat(Acceleration.Y); Acceleration.Z = FMath::RoundToFloat(Acceleration.Z); } void FSavedMove_MyMovement::PrepMoveFor(class ACharacter* Character) { Super::PrepMoveFor(Character); UYourCharacterMovementComponent* CharMov = Cast<UYourCharacterMovementComponent>(Character->GetCharacterMovement()); if (CharMov) { //This is just the exact opposite of SetMoveFor. It copies the state from the saved move to the movement //component before a correction is made to a client. CharMov->MoveDirection = SavedMoveDirection; CharMov->bWantsToPowerSlide = bSavedWantsToPowerSlide; CharMov->bWantsToWallRun = bSavedWantsToWallRun; //Don't update flags here. They're automatically setup before corrections using the compressed flag methods. } }
FNetworkPredictionData_Client_Character is the data object that holds all the client-side data with the server. This class contains the data that handles server corrections and rollback. Custom implementation is pretty simple with subclassing to make your new SavedMovement object. You then need to override the GetPredictionData_Client() function in the movement component to use this new class.
// placed in header of movementcomponent class FNetworkPredictionData_Client_MyMovement : public FNetworkPredictionData_Client_Character { public: typedef FNetworkPredictionData_Client_Character Super; FNetworkPredictionData_Client_MyMovement(const UCharacterMovementComponent& ClientMovement); ///@brief Allocates a new copy of our custom saved move virtual FSavedMovePtr AllocateNewMove() override; };
// placed in your movementcomponent class public: virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override;
FNetworkPredictionData_Client_MyMovement::FNetworkPredictionData_Client_MyMovement(const UCharacterMovementComponent& ClientMovement) : Super(ClientMovement) {} FSavedMovePtr FNetworkPredictionData_Client_MyMovement::AllocateNewMove() { return FSavedMovePtr(new FSavedMove_MyMovement()); }
class FNetworkPredictionData_Client* UYourCharacterMovementComponent::GetPredictionData_Client() const { check(PawnOwner != NULL); //check(PawnOwner->GetLocalRole() < ROLE_Authority); if (!ClientPredictionData) { UYourCharacterMovementComponent* MutableThis = const_cast<UYourCharacterMovementComponent*>(this); MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_MyMovement(*this); MutableThis->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f; MutableThis->ClientPredictionData->NoSmoothNetUpdateDist = 140.f; } return ClientPredictionData; }
Prediction Simulation
In the CMC there are a couple simulation functions to implement, one for each movement state, including the custom movement state. These are PhysCustom, PhysWalking, PhysFlying, etc.
If you are using your own custom movement state then you need to update PhysCustom, else implement off of the relevant Phys-movement state.
void UYourCharacterMovementComponent::PhysCustom(float deltaTime, int32 Iterations) { if (GetOwner()->GetLocalRole() == ROLE_SimulatedProxy) return; switch (CustomMovementMode) { case ECustomMovementMode::CMOVE_WallRunning: { // code to determine wheter to stay and if so, do update break; } } Super::PhysCustom(deltaTime, Iterations); }
Conclusion
Adding custom movement to a CMC requires quite a bit of setup but this is the same for all projects and the result is definitely worth it. I have only outlined the general networking setup to allow for custom states and the implementation from here can fit the style of your project.
Thanks for making it this far! Have a great day!
Outline of locally controlled movement prediction in the CharacterMovementComponent in Unreal.