UE4自定义CharacterMovementComponent--实现冲刺、闪避功能

今天实现一个UE4的自定义CharacterMovementComponent,并且实现shift加速跑ctrl进行闪避一段距离的移动功能。

创建工程

创建Blueprint工程,选择ThirdPerson模板

新建c++类–自定义Character类和MovementComponent类

新建AMyCharacter类,继承ACharacter
新建UMyCharacterrMovementComponent类,继承UCharacterMovementComponent

搞起来!!!

1.为MyCharacter设置自定义移动组件类MyCharacterrMovementComponent的成员变量

private:
	UMyCharacterMovementComponent* MyCharacterMovementComponent;

2.MyCharacter类需要修改构造函数,以便后续在编辑器中能够在修改继承父类中被继承

AMyCharacter(const FObjectInitializer& ObjectInitializer);

设置使用我们自定义的移动组件,而不使用默认的移动组件

AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UMyCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

3.再添加一个成员函数,用于获取自定义的移动组件,以后就不用再每次都Cast来转换默认类型到自定义类型

UFUNCTION(BlueprintCallable, Category = "Movement")
		FORCEINLINE class UMyCharacterMovementComponent* GetMyMovementComponent() const { return MyCharacterMovementComponent; }

4.为AMyCharacter添加PostInitializeComponents成员函数,用于生成组件后初始化组件相关内容

virtual void PostInitializeComponents() override;
void AMyCharacter::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	MyCharacterMovementComponent = Cast<UMyCharacterMovementComponent>(Super::GetMovementComponent());
}

5.关闭原来的UE4窗口,在VS中启动项目后并双击打开ThirdPersonCharacter蓝图类
在蓝图编辑界面右上角可以看到,ThirdPersonCharacter的父类为ACharacter
在这里插入图片描述
我们需要修改它的父类,在File->Reparent Blueprint中搜索并选择我们自定义的MyCharacter类,保存后就关闭即可,此时父类已经变成了我们自定义的MyCharacter

6.回到VS中的UMyCharacterMovementComponent
UE4中玩家的所有移动操作需要被保存在一个队列queue中(FSavedMove_Character),因此我们需要实现自定义的移动队列,我们需要重写几个成员函数

class FSavedMove_My : public FSavedMove_Character
{
	public:
			typedef FSavedMove_Character Super;
			virtual bool CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* Character, float MaxDelta) const override;
			virtual void Clear() override;
			virtual uint8 GetCompressedFlags() const override;
			virtual void SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character& ClientData) override;
			virtual void PrepMoveFor(class ACharacter* Character) override;
			//Walk Speed Update
			uint8 bSavedRequestMaxWalkSpeedChange : 1;
			//Dodge
			FVector SavedMoveDirection;
			uint8 bSavedWantsToDodge : 1;
};
  • CanCombineWith 用于判断两个move是否能够被合并,用于减轻带宽使用(比如玩家很长时间内没有移动,这样的move可以合并)
  • Clear 用于清除move队列 GetCompressedFlags用于获取跳跃、蹲以及自定义的移动状态信息
  • SetMoveFor 用于获取玩家输入并且将move放入队列,修改相关移动状态标记
  • PrepMoveFor 用于玩家人物真实开始移动前的准备工作,比如设置移动方向等
  • bSavedRequestMaxWalkSpeedChange 用于标记是否更新走路速度
  • SavedMoveDirection 用于保存闪避(Dodge)的方向 bSavedWantsToDodge 用于标记是否进行闪避操作
 bool UMyCharacterMovementComponent::FSavedMove_My::CanCombineWith(const FSavedMovePtr& NewMove, 
 																	ACharacter* Character, 
 																	float MaxDelta) const
{
	//Set which moves can be combined together. This will depend on the bit flags that are used.	
	if (bSavedRequestMaxWalkSpeedChange != ((FSavedMove_My*)&NewMove)->bSavedRequestMaxWalkSpeedChange)
	{
		return false;
	}
	if (bSavedWantsToDodge != ((FSavedMove_My*)&NewMove)->bSavedWantsToDodge)
	{
		return false;
	}
	if (SavedMoveDirection != ((FSavedMove_My*)&NewMove)->SavedMoveDirection)
	{
		return false;
	}

	return Super::CanCombineWith(NewMove, Character, MaxDelta);
}

//保存移动到savedMove
void UMyCharacterMovementComponent::FSavedMove_My::SetMoveFor(ACharacter* Character, float InDeltaTime, FVector const& NewAccel, class FNetworkPredictionData_Client_Character& ClientData)
{
	Super::SetMoveFor(Character, InDeltaTime, NewAccel, ClientData);

	UMyCharacterMovementComponent* CharacterMovement = Cast<UMyCharacterMovementComponent>(Character->GetCharacterMovement());
	if (CharacterMovement)
	{
		bSavedRequestMaxWalkSpeedChange = CharacterMovement->bRequestMaxWalkSpeedChange;
		bSavedWantsToDodge = CharacterMovement->bWantsToDodge;
		SavedMoveDirection = CharacterMovement->MoveDirection;
	}
}

//客户端更新位置前根据savedMove中的数据进行准备工作
void UMyCharacterMovementComponent::FSavedMove_My::PrepMoveFor(class ACharacter* Character)
{
	Super::PrepMoveFor(Character);

	UMyCharacterMovementComponent* CharacterMovement = Cast<UMyCharacterMovementComponent>(Character->GetCharacterMovement());
	if (CharacterMovement)
	{
		CharacterMovement->MoveDirection = SavedMoveDirection;
	}
}
void UMyCharacterMovementComponent::FSavedMove_My::Clear()
{
	Super::Clear();

	bSavedRequestMaxWalkSpeedChange = false;
	bSavedWantsToDodge = false;
	SavedMoveDirection = FVector::ZeroVector;
}

uint8 UMyCharacterMovementComponent::FSavedMove_My::GetCompressedFlags() const
{
	uint8 Result = Super::GetCompressedFlags();

	if (bSavedRequestMaxWalkSpeedChange)
	{
		Result |= FLAG_Custom_0;
	}

	if (bSavedWantsToDodge)
	{
		Result |= FLAG_Custom_1;
	}

	return Result;
}

UMyCharacterMovementComponent::FNetworkPredictionData_Client_My::FNetworkPredictionData_Client_My(const UCharacterMovementComponent& ClientMovement)
	: Super(ClientMovement)
{

}

7.UMyCharacterMovementComponent类中也需要包含移动相关的标记变量:
为了改变人物行走速度,移动组件中需要设置最大移动速度变量,以及是否更新该速度的标记变量
同理,还有闪避方向变量和闪避强度变量,以及是否发起闪避的标记变量

	float MyNewMaxWalkSpeed;
	//Set Max Walk Speed
	uint8 bRequestMaxWalkSpeedChange : 1;
	//闪避方向
	FVector MoveDirection;
	//闪避强度,可以在UE4编辑器中进行修改
	UPROPERTY(EditAnywhere, Category = "Dodge")
		float DodgeStrength;
	//是否发起了闪避
	uint8 bWantsToDodge : 1;

变量DodgeStrength 在移动组件构造函数中初始化

UMyCharacterMovementComponent::UMyCharacterMovementComponent(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	DodgeStrength = 10000.f;
}

我们需要重写UpdateFromCompressedFlags函数,用于更新标记时根据压缩的标记位判断是否触发相关移动操作,这里使用了自定义的移动状态标记位FLAG_Custom_0FLAG_Custom_1,分别用于判断我们自定义的两个移动行为(冲刺、闪避),是否改变行走速度和是否发起了闪避操作。

void UMyCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags)//Client only
{
	Super::UpdateFromCompressedFlags(Flags);

	bRequestMaxWalkSpeedChange = (Flags & FSavedMove_Character::FLAG_Custom_0) != 0;
	bWantsToDodge = (Flags & FSavedMove_Character::FLAG_Custom_1) != 0;
}

8.我们在UMyCharacterMovementComponent中提供两个蓝图接口函数,分别对应两个移动行为的发生调用函数,后续用于蓝图中shift冲刺ctrl闪避的逻辑功能的实现

//Set Max Walk Speed (Called from the owning client)
UFUNCTION(BlueprintCallable, Category = "Max Walk Speed")
	void SetMaxWalkSpeed(float NewMaxWalkSpeed);
//Trigger the dodge ability (Called from the owning client)
UFUNCTION(BlueprintCallable, Category = "Dodge")
	void Dodge();

以及用于服务器的实现函数接口

//server  Implementation
	UFUNCTION(Unreliable, Server, WithValidation)
		void Server_SetMaxWalkSpeed(const float NewMaxWalkSpeed);
	UFUNCTION(Unreliable, Server, WithValidation)
		void Server_MoveDirection(const FVector& MoveDir);

宏里面的server代表是服务器调用函数,需要提供服务器具体执行函数,也就是xxx_Implemention的形式

void UMyCharacterMovementComponent::Server_SetMaxWalkSpeed_Implementation(const float NewMaxWalkSpeed)
{
	//对应的服务器上的  新最大行走速度的设置
	MyNewMaxWalkSpeed = NewMaxWalkSpeed;
}
void UMyCharacterMovementComponent::Server_MoveDirection_Implementation(const FVector& MoveDir)
{
	MoveDirection = MoveDir;
}

宏里面的withValidation代表需要提工服务器的验证函数,xxx_Validate的形式

bool UMyCharacterMovementComponent::Server_SetMaxWalkSpeed_Validate(const float NewMaxWalkSpeed)
{
	if (NewMaxWalkSpeed < 0.f || NewMaxWalkSpeed > 2000.f)
		return false;
	else
		return true;
}
bool UMyCharacterMovementComponent::Server_MoveDirection_Validate(const FVector& MoveDir)
{
	return true;
}

客户端的实现,原则是准备新的状态数据,更新标记位,等待tick时更新

void UMyCharacterMovementComponent::SetMaxWalkSpeed(float NewMaxWalkSpeed)
{
	if (PawnOwner->IsLocallyControlled())
	{
	//本地控制的pawn,直接设置最新数据。并且调用对应的服务器RPC函数
		MyNewMaxWalkSpeed = NewMaxWalkSpeed;
		Server_SetMaxWalkSpeed(NewMaxWalkSpeed);
	}
	//想要改变速度的标记。服务器就是直接设置想要改变,后续跟客户端一样,在tick处更新状态数据
	bRequestMaxWalkSpeedChange = true;
}
void UMyCharacterMovementComponent::Dodge()
{

	if (PawnOwner->IsLocallyControlled())
	{
	//GetLastMovementInputVector获取玩家输入向量,作为闪避方向
		MoveDirection = PawnOwner->GetLastMovementInputVector();
		Server_MoveDirection(MoveDirection);
	}
	
	bWantsToDodge = true;
}

9.然后就是重写OnMovementUpdated函数,处理移动更新的逻辑

void UMyCharacterMovementComponent::OnMovementUpdated(float DeltaTime, 
													  const FVector& OldLocation, 
													  const FVector& OldVelocity)
{
	Super::OnMovementUpdated(DeltaTime, OldLocation, OldVelocity);
	if (!CharacterOwner)
	{
		return;
	}

	//Set Max Walk Speed
	if (bRequestMaxWalkSpeedChange)
	{
		UE_LOG(LogTemp, Warning, TEXT("Max Walk Speed Changed"));
		bRequestMaxWalkSpeedChange = false;
		MaxWalkSpeed = MyNewMaxWalkSpeed;
	}

	//Dodge
	if (bWantsToDodge)
	{
		UE_LOG(LogTemp, Warning, TEXT("Dodge"));
		bWantsToDodge = false;

		//闪避只能人物在地面时才能触发,不然在空中会存在两种行为mode,产生冲突
		if (IsMovingOnGround())
		{
			MoveDirection.Normalize();
			FVector DodgeVelocity = MoveDirection * DodgeStrength;
			//Set Z component to zero so we don't go up
			DodgeVelocity.Z = 0.0f;
			//设置水平移动闪避,并触发移动
			Launch(DodgeVelocity);
		}
	}
}

10.还需要一个用于玩家移动预测的自定义类–继承于FNetworkPredictionData_Client_Character

class FNetworkPredictionData_Client_My : public FNetworkPredictionData_Client_Character
	{
	public:
		typedef FNetworkPredictionData_Client_Character Super;
		FNetworkPredictionData_Client_My(const UCharacterMovementComponent& ClientMovement);
		virtual FSavedMovePtr AllocateNewMove() override;//为savedMove分配空间
	};

重写AllocateNewMove,用于为自定义的预测类分配内存

FSavedMovePtr UMyCharacterMovementComponent::FNetworkPredictionData_Client_My::AllocateNewMove()
{
	return FSavedMovePtr(new FSavedMove_My());
}

并且重写GetPredictionData_Client函数,用于返回客户端的预测数据的自定义

FNetworkPredictionData_Client* UMyCharacterMovementComponent::GetPredictionData_Client() const
{
	check(PawnOwner != NULL);
	check(PawnOwner->GetLocalRole() < ROLE_Authority);//client only

	if (!ClientPredictionData)
	{
		UMyCharacterMovementComponent* MutableThis = const_cast<UMyCharacterMovementComponent*>(this);
		MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_My(*this);
		//平滑过渡最大距离,不然客户端中的人物会被直接拉到服务器中人物的位置
		MutableThis->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f;
		MutableThis->ClientPredictionData->NoSmoothNetUpdateDist = 140.f;
	}
	return ClientPredictionData;
}

11.如果只这样实现,会存在一个问题,人物在Dodge后会进入falling mode下坠,因此需要重写HandlePendingLaunch函数

bool UMyCharacterMovementComponent::HandlePendingLaunch()
{
	if (!PendingLaunchVelocity.IsZero() && HasValidData())
	{
		Velocity = PendingLaunchVelocity;
		//Remmed out so our dodge move won't play the falling animation.
		//SetMovementMode(MOVE_Falling);
		PendingLaunchVelocity = FVector::ZeroVector;
		bForceNextFloorCheck = true;
		return true;
	}

	return false;
}

12.启动项目打开ThirdpersonCharacter蓝图,并设置如下在这里插入图片描述
这里的left shift按下和松开分别调用了我们自定义移动组件中的SetMaxWalkSpeed函数,
left ctrl则调用了我们定义的Dodge函数。当然首先需要使用定义的GetMyMovementComponent获取我们的移动组件对象,然后对应引脚连接起来。

13.最后,现在可以点击play让我们的小人冲刺起来了!

总结

  • 自定义移动组件需要实现自己的移动组件类,并且在自定义的character类中修改默认的移动组件类;
  • 实现某项移动功能,需要使用 变量 + 标记 的形式,定义在组件类中;
  • 实现移动状态改变的函数接口(包括客户端、服务器两种实现情况),可以根据需要进行验证;
  • 实现自定义的savedMove类,用于保存move,重写SetMoveFor、PrepMoveFor保存和还原move中的变量和标记;
  • 实现自定义的NetworkPredictionData类,用于客户端预测移动,可以重写GetPredictionData_Client函数进行自定义预测相关参数的设置;
  • 重写移动组件类的OnMovementUpdated函数,根据标记位更新状态变量(如改变MaxWalkSpeed),或者执行指定move(如Launch(DodgeVelocity))。
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值