94. UE5 GAS RPG 实现攻击击退效果

在这一篇里,我们增加一些功能,就是技能击中敌人后,能够让敌人产生一些击退效果,如果敌人死亡,能够产生较大幅度的击退效果。

我们将首先将实现敌人死亡被击退的效果,然后再此基础上,实现攻击击退的效果。

要实现敌人死亡时,受到技能的冲击,我们需要可以在技能上设置技能的冲击力,并且修改死亡函数,可以在死亡时给死亡角色模型应用冲击力。

修改死亡函数

首先,我们在战斗接口中修改死亡函数,增加可以配置技能的冲击向量值,这个之前可以获取冲击的朝向和力度。

cpp 复制代码
virtual void Die(const FVector& DeathImpulse) = 0;

然后我们在角色基类里也修改相关覆写

cpp 复制代码
virtual void Die(const FVector& DeathImpulse) override;

并在实际执行的多播函数里添加参数

cpp 复制代码
	UFUNCTION(NetMulticast, Reliable)
	virtual void MulticastHandleDeath(const FVector& DeathImpulse);

然后调用时传值

cpp 复制代码
void ARPGCharacter::Die(const FVector& DeathImpulse)
{
	//将武器从角色身上分离
	Weapon->DetachFromComponent(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
	MulticastHandleDeath(DeathImpulse);
}

在多播里,我们将角色模型设置了物理模拟以后,通过调用AddImpulse给其一个冲击力,由于武器的质量比模型要小,我们只给其百分之一的冲击,第二个参数是接收冲击的骨骼,默认值NAME_None就是应用根骨骼,这对于复杂的骨骼动画系统尤为重要,可以精确控制冲量的作用位置。最后一个值如果设置为 true,则冲量的作用将被视为速度的改变,而不是一个基于质量的冲量。

cpp 复制代码
void ARPGCharacter::MulticastHandleDeath_Implementation(const FVector& DeathImpulse)
{
	//播放死亡音效
	UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation());
	
	//开启武器物理效果
	Weapon->SetSimulatePhysics(true); //开启模拟物理效果
	Weapon->SetEnableGravity(true); //开启重力效果
	Weapon->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); //开启物理碰撞通道
	//Weapon->AddImpulse(DeathImpulse * 0.01f, NAME_None, true);

	//开启角色物理效果
	GetMesh()->SetSimulatePhysics(true); //开启模拟物理效果
	GetMesh()->SetEnableGravity(true); //开启重力效果
	GetMesh()->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); //开启物理碰撞通道
	GetMesh()->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block); //开启角色与静态物体产生碰撞
	GetMesh()->AddImpulse(DeathImpulse, NAME_None, true);

	//关闭角色碰撞体碰撞通道,避免其对武器和角色模拟物理效果产生影响
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	//设置角色溶解
	Dissolve();

	//设置死亡状态
	bDead = true;
	
	//触发死亡委托
	OnDeath.Broadcast(this);
}

GE增加冲击参数

接着我们修改GE上下文,在里面可以保存对应的参数,在RPGAbilityTypes.h文件里,我们在创建的FDamageEffectParams结构体里增加两个用于传递使用的参数,我们可以在生成结构体时,对其设置参数。

cpp 复制代码
	UPROPERTY()
	float DeathImpulseMagnitude = 0.f; //死亡时受到的冲击力

	UPROPERTY()
	FVector DeathImpulse = FVector::ZeroVector; //死亡时受到冲击的朝向

接着我们在上下文里增加一个参数,用于保存冲击力

cpp 复制代码
	UPROPERTY()
	FVector DeathImpulse = FVector::ZeroVector; //死亡时冲击的方向

并增加get和set方法,方便设置冲击力

cpp 复制代码
FVector GetDeathImpulse() const { return DeathImpulse; } //获取到死亡冲击的方向和力度
void SetDeathImpulse(const FVector& InImpulse) { DeathImpulse = InImpulse; } //设置死亡冲击的方向和力度

然后修改序列化,能够让值通过序列化传递到服务器

cpp 复制代码
		if(!DeathImpulse.IsZero())
		{
			RepBits |= 1 << 14;
		}
	}

	//使用了多少长度,就将长度设置为多少
	Ar.SerializeBits(&RepBits, 15);
	
	...
	
	if (RepBits & (1 << 14))
	{
		DeathImpulse.NetSerialize(Ar, Map, bOutSuccess);
	}

接下来,为了方便设置,我们还需要在函数库里增加设置和获取的函数

cpp 复制代码
	//获取当前GE死亡冲击的方向和力度
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static FVector GetDeathImpulse(const FGameplayEffectContextHandle& EffectContextHandle);
	
	//设置GE是否触发暴击
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetDeathImpulse(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& bInDeathImpulse);
	
cpp 复制代码
FVector URPGAbilitySystemBlueprintLibrary::GetDeathImpulse(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetDeathImpulse();
	}
	return FVector::ZeroVector;
}
void URPGAbilitySystemBlueprintLibrary::SetDeathImpulse(FGameplayEffectContextHandle& EffectContextHandle, const FVector& bInDeathImpulse)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetDeathImpulse(bInDeathImpulse);
}

并在应用配置项时,增加应用冲击力配置

接下来,我们在伤害技能上增加一个可配置参数,用来配置当前技能的冲击力

cpp 复制代码
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float DeathImpulseMagnitude = 60.f; //死亡时,受到的冲击的数值

并在生成配置项函数里,去设置配置结构体的冲击力

实现冲击的应用

火球术发射时,我们通过技能创建对应的发射物,并将配置结构体存储到了发射物身上,在碰撞触发时,我们将为结构体设置冲击的朝向

这样,我们应用时,就可以准确的获取到对应的冲击力和朝向。

我们在调用蓝图函数库URPGAbilitySystemBlueprintLibrary::ApplyDamageEffect(DamageEffectParams);的函数将配置结构体内的值保存的GE,并应用到目标。应用的操作是在服务器端实现的,我们通过蓝图库函数存到了GE的上下文里,并通过序列化上传到了服务器端,在服务器端的AttributeSet函数处理时,就可以获取到对应的函数。

接下来,我们在AS里处理,在调用死亡函数时,将通过函数库获取到GE上下文里的冲击的值传入。

这里有个bug解决了一下,就是如果当前攻击造成了角色死亡,有可能还会触发应用负面效果,我感觉这是没必要的,所以,如果角色生命值小于0,我将不再进行负面效果相关处理。

功能实现完成,我们要进行多次测试:

  1. 将负面效果应用概率降为0,防止负面效果应用伤害导致致命一击时负面效果造成的,测试击退效果。
  2. 将负面效果设置为100,保证每次攻击都能够应用效果,将伤害拉高,保证致命一击由火球术造成,查看是否在死亡时是否能够还会应用负面效果。

增加攻击击退效果参数

接下来,我们实现攻击击退效果,增加参数和死亡冲击时的参数雷同,这里我不做讲解

生成配置参数增加两个参数

cpp 复制代码
	UPROPERTY()
	FVector KnockbackForce = FVector::ZeroVector; //攻击时击退的方向

	UPROPERTY()
	float KnockbackChance = 0.f; //攻击时击退概率

伤害技能基类增加两个参数,用于设置概率和力度

cpp 复制代码
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float KnockbackForceMagnitude = 1000.f; //技能击中敌人后,敌人受到的击退的力度

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float KnockbackChance = 0.f; //技能命中敌人触发击退的概率

在生成配置时,设置参数

自定义GE上下文增加设置函数

cpp 复制代码
FVector GetKnockbackForce() const { return KnockbackForce; } //获取到攻击击退的方向和力度
void SetKnockbackForce(const FVector& InKnockbackForce) { KnockbackForce = InKnockbackForce; } //设置攻击击退的方向和力度

...

	UPROPERTY()
	FVector KnockbackForce = FVector::ZeroVector; //攻击时击退的方向

序列化时,对击退力度进行序列化

cpp 复制代码
...
		if(!KnockbackForce.IsZero())
		{
			RepBits |= 1 << 15;
		}
	}

	//使用了多少长度,就将长度设置为多少
	Ar.SerializeBits(&RepBits, 16);
	...
	if (RepBits & (1 << 15))
	{
		KnockbackForce.NetSerialize(Ar, Map, bOutSuccess);
	}

蓝图函数库增加对应函数,方便调用

cpp 复制代码
	//获取当前GE攻击击退的方向和力度
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static FVector GetKnockbackForce(const FGameplayEffectContextHandle& EffectContextHandle);
		
	//设置GE攻击击退的方向和力度
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetKnockbackForce(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& InKnockbackForce);

cpp实现一下

cpp 复制代码
FVector URPGAbilitySystemBlueprintLibrary::GetKnockbackForce(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetKnockbackForce();
	}
	return FVector::ZeroVector;
}

void URPGAbilitySystemBlueprintLibrary::SetKnockbackForce(FGameplayEffectContextHandle& EffectContextHandle, const FVector& InKnockbackForce)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetKnockbackForce(InKnockbackForce);
}

函数库给目标应用GE时,将击退的值存入GE实例,用于复制到服务器端

实现应用击退逻辑

接下来,就是实现击退的逻辑,在发射火球术时,我们可以通过火球术技能生成配置项传递给发射的火球,在火球碰撞时,实现修改GE实例

这里,我们获取到火球飞行的方向,并将角度向上斜45度,让其可以有一个击飞的效果

在AS里处理时,我们就可以获取到击退的值,通过这个值就可以实现击退效果。

在受到伤害时,如果目标角色没有死亡,我们就获取到击退值,如果值不接近于0,我们将值应用给角色

LaunchCharacter将为角色设置一个待处理的发射速度 (LaunchVelocity),并在角色的 CharacterMovementComponent 下一次更新时应用这个速度。角色会被设置为"falling"(下落)状态,并触发 OnLaunched 事件。这通常用于角色跳跃、被抛出或其他瞬时位移的情况。

LaunchVelocity: 这是一个 FVector 类型的参数,表示施加给角色的速度。这是一个三维向量,决定了角色沿 X、Y、Z 方向的速度。

bXYOverride: 这是一个布尔值参数。如果设置为 true,将替换角色当前的 X 和 Y 方向速度,而不是在现有速度的基础上添加。这意味着角色在 X 和 Y 方向上的速度会被直接设置为 LaunchVelocity 中的对应值。

bZOverride: 这是一个布尔值参数。如果设置为 true,将替换角色当前的 Z 方向速度(通常与垂直跳跃有关),而不是在现有速度的基础上添加。角色在 Z 方向的速度会被设置为 LaunchVelocity 中的 Z 值。

接下来我们测试效果,可以根据需求调整效果。

实现近战攻击的击退逻辑

我们在火球术等投掷类的技能是将配置项传递给投掷物生成的,而近战攻击类型的技能不需要多此一举,而是直接在技能里应用的伤害。

下图为近战攻击实现的蓝图逻辑,可以看到,我们会遍历所有可以攻击的角色,然后通过CauseDamage函数应用的伤害。

如果在c++里实现,我们只需要修改CauseDamage函数,实现投掷技能的那一套即可。

以下是修改过的CauseDamage函数,通过MakeDamageEffectParamsFromClassDefaults配置项,然后再生成击退使用的角度和力度,实现对应的效果。

cpp 复制代码
void URPGDamageGameplayAbility::CauseDamage(AActor* TargetActor)
{
	//生成配置
	FDamageEffectParams Params = MakeDamageEffectParamsFromClassDefaults(TargetActor);

	//设置死亡冲击和击退
	if(IsValid(TargetActor))
	{
		//获取到攻击对象和目标的朝向,并转换成角度
		FRotator Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
		Rotation.Pitch = 45.f; //设置击退角度垂直45度
		const FVector ToTarget = Rotation.Vector();
		Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
		//判断攻击是否触发击退
		if(FMath::RandRange(1, 100) < Params.KnockbackChance)
		{
			Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
		}
	}
		
	//通过配置项应用给目标ASC
	URPGAbilitySystemBlueprintLibrary::ApplyDamageEffect(Params);
}

如果要在蓝图里实现,我们可以将MakeDamageEffectParamsFromClassDefaults设置为BlueprintPure(没有调用的函数)

然后将配置项里的参数设置为BluePrintReadWrite可以在蓝图内获取和修改

接着,我们就可以在蓝图里,创建一个配置项,然后再修改配置项结构体内的一些内容,重新生成配置项,应用

解决角色击退移动的问题

我们发现,在角色被击退漂浮在空中时,她的腿时移动的,而不是那种悬浮浮空的效果。所以,这个问题,我们需要去动画蓝图里去修改。

在击退时,角色会被应用浮空效果,直到坠落到地面,所以,我们在角色更新动画时,去获取角色是否处于浮空状态即可

接着修改状态机,增加浮空状态

Alias是状态别名,可以指定某些状态通过条件可以切换到浮空状态

切换到浮空的条件就是通过判断浮空变量是否设置为true

退出的条件就是变量变为了false,默认切换到idle状态

切换移动的条件,我们也可以添加对浮空状态的判断

相关推荐
SaxoZhao12 分钟前
Vue 中阻止点击事件穿透
前端·javascript·vue.js
1234Wu15 分钟前
高德地图2.0 绘制、编辑多边形覆盖物(电子围栏)
前端·vue
用你的胜利博我一笑吧16 分钟前
vue3+ts+supermap iclient3d for cesium功能集合
前端·javascript·vue.js·3d·cesium·supermap
抚月code22 分钟前
SpringBoot基础
java·spring boot·后端
超级小的大杯柠檬水25 分钟前
Spring Boot 3项目使用Swagger3教程
java·spring boot·后端
Lovely Ruby30 分钟前
Vite + Electron 时,Electron 渲染空白,静态资源加载错误等问题解决
前端·javascript·electron
艾伦~耶格尔31 分钟前
Java 正则表达式详解
java·开发语言·学习·正则表达式
xcLeigh42 分钟前
HTML5好看的水果蔬菜在线商城网站源码系列模板2
java·前端·html5
老田低代码42 分钟前
Dart自从引入null check后写Flutter App总有一种难受的感觉
前端·flutter
啊QQQQQ44 分钟前
C++11(3)
java·开发语言·c++