Permalink
Browse files

Add sprint ability that is replicated using character movement compon…

…ent and that can be replayed.
  • Loading branch information...
error454 committed Mar 23, 2015
1 parent 06f858a commit deb9509d41b22cec810e5a9e6749fe3543298292
@@ -0,0 +1,236 @@

#include "SideScroller.h"
#include "MyCharacterMovementComponent.h"

UMyCharacterMovementComponent::UMyCharacterMovementComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
/* Initialize sprinting flags */
bPressedSprint = false;
bIsSprinting = false;
bWasSprinting = false;
SprintEndTime = 0.f;
SprintRechargeTime = 0.f;

SprintAccel = 5000.f;
SprintSpeed = 2000.f;
}

bool UMyCharacterMovementComponent::CanSprint() const
{
if (CharacterOwner && IsMovingOnGround() && !IsCrouching() && (CharacterOwner->GetWorld()->GetTimeSeconds() > SprintRechargeTime))
{
return true;
}
return false;
}

float UMyCharacterMovementComponent::GetMaxSpeed() const
{
return bIsSprinting ? SprintSpeed : Super::GetMaxSpeed();
}

float UMyCharacterMovementComponent::GetMaxAcceleration() const
{
return (bIsSprinting && Velocity.SizeSquared() > FMath::Square<float>(MaxWalkSpeed)) ? SprintAccel : Super::GetMaxAcceleration();
}

void UMyCharacterMovementComponent::PerformAbilities(float DeltaTime)
{
if (CharacterOwner)
{
float WorldTime = CharacterOwner->GetWorld()->GetTimeSeconds();

/* Keep in mind that this is called both by the server and by the client.
* In both cases, we're checking the state of the bPressedSprintflag , the only
* difference is that in the server case, bPressedSprint was set via compressed flags.
*/
if (bPressedSprint && !bIsSprinting && CanSprint())
{
bIsSprinting = true;
}
else if (bIsSprinting && WorldTime > SprintEndTime)
{
bIsSprinting = false;
}

/* Only calculate new timers if the client isn't replaying moves. In the case of the
* client replaying moves, the timers below are set via the stored values inside
* PrepMoveFor(). Try setting a breakpoint inside PrepMoveFor and set your simulation
* latency and packet loss high to see this in action:
* Net PktLag=500
* Net PktLoss=15
*/
if (!CharacterOwner->bClientUpdating)
{
if (!bWasSprinting && bIsSprinting)
{
SprintEndTime = WorldTime + SprintDurationSeconds;
}
else if (bWasSprinting && !bIsSprinting)
{
SprintRechargeTime = WorldTime + SprintRechargeSeconds;
}
}
}
}

void UMyCharacterMovementComponent::PerformMovement(float DeltaSeconds)
{
bWasSprinting = bIsSprinting;
Super::PerformMovement(DeltaSeconds);
}

void UMyCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags)
{
Super::UpdateFromCompressedFlags(Flags);
bPressedSprint = ((Flags & FSavedMove_Character::FLAG_Custom_0) != 0);

/* This is a perfectly good place to trigger events based on flags. However you'll notice that I don't.
* The MoveAutonomous() function (which only runs on the server) calls:
* UpdateFromCompressedFlags(CompressedFlags);
* CharacterOwner->CheckJumpInput(DeltaTime);
*
* You'll notice that in SideScrollerCharacter, CheckJumpInput() directly calls PerformAbilities() to
* trigger the abilities based on this flag that was just set. The reason it is done this way is because
* clients take a different route. In their TickComponent() function, they call:
* CharacterOwner->CheckJumpInput(DeltaTime);
*
* So CheckJumpInput() ends up being a nice piece of common ground used to trigger abilities.
*/
}

class FNetworkPredictionData_Client* UMyCharacterMovementComponent::GetPredictionData_Client() const
{
/* This is straight-up from the UT code. The default values for update distance seem to work fine.
* Some comments from me added with ZB prefix.*/

// Should only be called on client in network games
check(PawnOwner != NULL);
check(PawnOwner->Role < ROLE_Authority);

/* ZB: I figured I'd leave this original comment in. I haven't searched for NM_Client and don't know what it is. */

// once the NM_Client bug is fixed during map transition, should re-enable this
//check(GetNetMode() == NM_Client);

if (!ClientPredictionData)
{
UMyCharacterMovementComponent* MutableThis = const_cast<UMyCharacterMovementComponent*>(this);

/* ZB: This is the critical line here. Without these we won't be using our custom network prediction data
* client and as a result we won't be using our custom FSavedMove */

MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_Custom();
MutableThis->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f; // 2X character capsule radius
MutableThis->ClientPredictionData->NoSmoothNetUpdateDist = 140.f;
}

return ClientPredictionData;
}

void FSavedMove_Custom::Clear()
{
Super::Clear();
bPressedSprint = false;
SavedSprintEndTime = 0.f;
SavedSprintRechargeTime = 0.f;
}

uint8 FSavedMove_Custom::GetCompressedFlags() const
{
/*
* Jumping and crouching are originally from the CharacterMovementComponent implementation.
* We are preserving them here. Notice that we're OR'ing together all of the flags.
*/
uint8 Result = 0;

if (bPressedJump)
{
Result |= FLAG_JumpPressed;
}

if (bWantsToCrouch)
{
Result |= FLAG_WantsToCrouch;
}

/*
* These are new abilities that we're stuffing into Result. There are 4 pre-defined custom flags:
* FLAG_JumpPressed = 0x01, Jump pressed
* FLAG_WantsToCrouch = 0x02, Wants to crouch
* FLAG_Reserved_1 = 0x04, Reserved for future use
* FLAG_Reserved_2 = 0x08, Reserved for future use
* FLAG_Custom_0 = 0x10,
* FLAG_Custom_1 = 0x20,
* FLAG_Custom_2 = 0x40,
* FLAG_Custom_3 = 0x80,
*
* You might notice that this is a single byte (0x00 - 0x80). It might be easier to see this as the
* 8 bits that this byte represents.
* _ _ _ _ _ _ _ _
* 7 6 5 4 3 2 1 0
* | | | | | | | |- FLAG_JumpPressed
* | | | | | | |--- FLAG_WantsToCrouch
* | | | | | |----- FLAG_Reserved_1
* | | | | |------- FLAG_Reserved_2
* | | | |---------- FLAG_Custom_0
* | | |------------ FLAG_Custom_1
* | |-------------- FLAG_Custom_2
* |---------------- FLAG_Custom_3
*
* In UT, they combine the reserved flags with Custom_0 to get 3 bits of space. Combined that gives them
* 8 unique states they can check (2^3) vs 3. The caveat is a bit of shifting and OR'ing on this end and
* shifting and AND'ing on the UpdateFromCompressedFlags end to pull out the bits of interest.
*/

/* We only need one flag to hold our sprint state */
if (bPressedSprint)
{
Result |= FLAG_Custom_0;
}

return Result;
}

bool FSavedMove_Custom::CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const
{
if (bPressedSprint != ((FSavedMove_Custom*)&NewMove)->bPressedSprint)
{
return false;
}
return Super::CanCombineWith(NewMove, Character, MaxDelta);
}

void FSavedMove_Custom::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData)
{
Super::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData);

/* Saved Move <--- Character Movement Data */
UMyCharacterMovementComponent* CharMov = Cast<UMyCharacterMovementComponent>(Character->GetCharacterMovement());
if (CharMov)
{
bPressedSprint = CharMov->bPressedSprint;
SavedSprintEndTime = CharMov->SprintEndTime;
SavedSprintRechargeTime = CharMov->SprintRechargeTime;
}
}

void FSavedMove_Custom::PrepMoveFor(class ACharacter* Character)
{
Super::PrepMoveFor(Character);

/* Saved Move ---> Character Movement Data */
UMyCharacterMovementComponent* CharMov = Cast<UMyCharacterMovementComponent>(Character->GetCharacterMovement());
if (CharMov)
{
CharMov->bPressedSprint = bPressedSprint;
CharMov->SprintEndTime = SavedSprintEndTime;
CharMov->SprintRechargeTime = SavedSprintRechargeTime;
}
}

FSavedMovePtr FNetworkPredictionData_Client_Custom::AllocateNewMove()
{
return FSavedMovePtr(new FSavedMove_Custom());
}
@@ -0,0 +1,133 @@

#pragma once

#include "GameFramework/CharacterMovementComponent.h"
#include "MyCharacterMovementComponent.generated.h"

/**
*
*/
UCLASS()
class SIDESCROLLER_API UMyCharacterMovementComponent : public UCharacterMovementComponent
{
GENERATED_UCLASS_BODY()
public:

/*
* Tunable Sprint Variables
*/

/* The max speed of sprint */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Sprint")
float SprintSpeed;

/* The acceleration of the sprint */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Sprint")
float SprintAccel;

/* How long the sprint lasts */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Sprint")
float SprintDurationSeconds;

/* How long it takes for the sprint ability to recharge */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Sprint")
float SprintRechargeSeconds;

/*
* Untunable Sprint Variables
*/

/* World time when sprint should end */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sprint")
float SprintEndTime;

/* World time when sprint will be recharged */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sprint")
float SprintRechargeTime;

/* True if sprint button has been pressed */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sprint")
bool bPressedSprint;

/* True if sprinting */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sprint")
bool bIsSprinting;

/* True if we were sprint in the last frame */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Sprint")
bool bWasSprinting;

/* Returns true if we can sprint */
virtual bool CanSprint() const;

/* Support for sprint speed. */
virtual float GetMaxSpeed() const override;

/* Support for sprint acceleration. */
virtual float GetMaxAcceleration() const override;

/* An entry point for the character class to call us to update our added abilities */
virtual void PerformAbilities(float DeltaTime);

/* If you look at the order of events for CharacterMovementComponent, you'll see that this function is the first thing
* called for both client and server when performing movement. We use it as a good place to capture the previous
* sprinting state. */
virtual void PerformMovement(float DeltaSeconds) override;

/* Read the state flags provided by CompressedFlags and trigger the ability on the server */
virtual void UpdateFromCompressedFlags(uint8 Flags) override;

/* A necessary override to make sure that our custom FNetworkPredictionData defined below is used */
virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override;
};

/*
* This custom implementation of FSavedMove_Character is used for 2 things:
* 1. To replicate special ability flags using the compressed flags
* 2. To shuffle saved move data around
*/
class FSavedMove_Custom : public FSavedMove_Character
{
public:
typedef FSavedMove_Character Super;

/** These flags are used to replicate special abilities via the compressed flags. You can only replicate booleans.*/
bool bPressedSprint;

/*
* These properties aren't replicated, they're used to replay moves. You want to be sure to throw any timers in
* here so that once a move is replayed you'll have the correct end time for it.
*/
float SavedSprintEndTime;
float SavedSprintRechargeTime;

/* Sets the default values for the saved move */
virtual void Clear() override;

/* Packs state data into a set of compressed flags. This is undone above in UpdateFromCompressedFlags */
virtual uint8 GetCompressedFlags() const override;

/* Checks if an old move can be combined with a new move for replication purposes (are they different or the same) */
virtual bool CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const override;

/* Populates the FSavedMove fields from the corresponding character movement controller variables. This is used when
* making a new SavedMove and the data will be used when playing back saved moves in the event that a correction needs to happen.*/
virtual void SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character & ClientData) override;

/* This is used when the server plays back our saved move(s). This basically does the exact opposite of what
* SetMoveFor does. Here we set the character movement controller variables from the data in FSavedMove. */
virtual void PrepMoveFor(class ACharacter* Character) override;
};

/*
* This subclass of FNetworkPredictionData_Client_Character is used to create new copies of
* our custom FSavedMove_Character class defined above.
*/
class FNetworkPredictionData_Client_Custom : public FNetworkPredictionData_Client_Character
{
public:
typedef FNetworkPredictionData_Client_Character Super;

/* Allocates a new copy of our custom saved move */
virtual FSavedMovePtr AllocateNewMove() override;
};
Oops, something went wrong.

0 comments on commit deb9509

Please sign in to comment.