书接上回,在上一篇博客里,我们实现了角色升级的基础的功能。给敌人增加的经验奖励配置,并且在敌人死亡时,能够将经验通过事件传递给击杀者,玩家定义了被动技能,在被动技能中接收传递的事件,通过SetByCaller的GE应用给自身。并在AttributeSet中打印出获得的经验。
我们还修改了PlayerState,在内部增加了对经验和等级添改查的,并创建对应的委托,在UI的Controller里面,实现了对委托的监听,并实现回调函数,通过经验获取等级和升级进度广播给UI表现出来。
创建玩家接口类
有了之前制作的内容,我们接下来,将实现玩家在AS里获取到经验后,设置的PlayerState上,这里防止耦合度太高,我们选择创建一个新的接口实现此功能。
新添加一个接口类
作为玩家专用的命名为PlayerInterface ,除了它我们还有CombateInterface(战斗接口)EnemyInterface(敌人接口)
在接口类里,我们增加一个增加经验的函数
class RPG_API IPlayerInterface
{GENERATED_BODY()// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:UFUNCTION(BlueprintNativeEvent)void AddToXP(int32 InXP); //增加经验
};
在玩家角色类继承此接口
class RPG_API ARPGHero : public ARPGCharacter, public IPlayerInterface
{GENERATED_BODY()
并覆写此函数
/* IPlayerInterface战斗接口 */virtual void AddToXP_Implementation(int32 InXP) override;/* IPlayerInterface战斗接口 结束 */
在实现这里,获取到PlayerState,并调用PlayerState身上的增加经验的函数
void ARPGHero::AddToXP_Implementation(int32 InXP)
{ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游PlayerStateBase->AddToXP(InXP);
}
最后在AS里,我们通过调用此函数实现经验增长
if(Data.EvaluatedData.Attribute == GetIncomingXPAttribute()){const float LocalIncomingXP = GetIncomingXP();SetIncomingXP(0);// UE_LOG(LogRPG, Log, TEXT("获取传入经验值:%f"), LocalIncomingXP);//将经验应用给自身if(Props.SourceCharacter->Implements<UPlayerInterface>()){IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);}}
在UI上实现对经验变动监听
现在,玩家角色可以获得经验,并通过接口设置给PlayerState,然后广播给UI的控制器了。我们要先将之前创建的升级数据设置给PlayerState
在WBP_Overlay里面,给经验条设置控制器
在经验条的事件中,设置控制器回调中,绑定对经验变动的监听,在变动时,获取经验条的百分比
接下来可以测试了,发现上一篇文章里面,在最后类型转换时,需要提前转换将一个数值转换为浮点数类型 ,浮点数/整型 结果就可以是浮点型,要不然,整型除以整型结果还是整型,获取的结果为0
接着,你就会发现经验条随着怪物死亡,增长了一丝
然后测试杀死两只,给的经验是否一致
重构战斗接口中的获取等级函数
我们想将之前书写的获取等级的函数修改成可以在蓝图定义的,达到结构统一,所以,需要额外修改一些内容
UFUNCTION(BlueprintNativeEvent)int32 GetPlayerLevel(); //获取玩家等级
修改后编译会发现一些报错
按照之前方式,将其名称后续加上_Implementation即可
/* ICombatInterface战斗接口 */virtual int32 GetPlayerLevel_Implementation() override;/* ICombatInterface战斗接口 结束 */
接下来编译,会发现还有一些额外报错,这是因为调用的地方没有修改
我们可以通过双击shift键,打开搜索,找到调用的地方依次修改。
找到需要修改的地方,将代码修改成以下类型。注意:Implements后面的类型是以U开头的接口,它是UE增加的,而调用的时候是I开头是c++内置的接口调用方式。
//从战斗接口获取到角色的等级int32 CharacterLevel = 1;if(ASC->GetAvatarActor()->Implements<UCombatInterface>()){CharacterLevel = ICombatInterface::Execute_GetPlayerLevel(ASC->GetAvatarActor());}
增加角色接口函数
为了实现接下来角色升级的功能,我们扩展角色接口的函数
class RPG_API IPlayerInterface
{GENERATED_BODY()// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:UFUNCTION(BlueprintNativeEvent)int32 FindLevelForXP(int32 InXP) const; //根据经验获取等级UFUNCTION(BlueprintNativeEvent)int32 GetXP() const; //获取当前经验值UFUNCTION(BlueprintNativeEvent)int32 GetAttributePointsReward(int32 Level) const; //获取属性点奖励UFUNCTION(BlueprintNativeEvent)int32 GetSpellPointsReward(int32 Level) const; //获取技能点奖励UFUNCTION(BlueprintNativeEvent)void AddToXP(int32 InXP); //增加经验UFUNCTION(BlueprintNativeEvent)void AddToPlayerLevel(int32 InPlayerLevel); //增加等级UFUNCTION(BlueprintNativeEvent)void AddToAttributePoints(int32 InAttributePoints); //增加属性点UFUNCTION(BlueprintNativeEvent)void AddToSpellPoints(int32 InSpellPoints); //增加技能点UFUNCTION(BlueprintNativeEvent)void LevelUp(); //升级
};
接下来,我们在继承接口的玩家角色类里面覆写函数
/* IPlayerInterface战斗接口 */virtual void AddToXP_Implementation(int32 InXP) override;virtual void LevelUp_Implementation() override;virtual int32 GetXP_Implementation() const override;virtual int32 FindLevelForXP_Implementation(int32 InXP) const override;virtual int32 GetAttributePointsReward_Implementation(int32 Level) const override;virtual int32 GetSpellPointsReward_Implementation(int32 Level) const override;virtual void AddToPlayerLevel_Implementation(int32 InPlayerLevel) override;virtual void AddToAttributePoints_Implementation(int32 InAttributePoints) override;virtual void AddToSpellPoints_Implementation(int32 InSpellPoints) override;/* IPlayerInterface战斗接口 结束 */
除了已经实现的添加经验的函数,我们将其它函数依次实现。
首先是升级函数,这个函数主要用于提供角色升级时,播放一下升级表现效果,因为当前类是在服务器运行的,我们要实现一个在每个客户端都可以运行的效果,就需要增加一个可以在多端运行的函数,这个在稍微讲解。
接下来是获取当前角色经验值
int32 ARPGHero::GetXP_Implementation() const
{const ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏return PlayerStateBase->GetXP();
}
通过经验值获取当前等级
int32 ARPGHero::FindLevelForXP_Implementation(const int32 InXP) const
{const ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏return PlayerStateBase->LevelUpInfo->FindLevelForXP(InXP);
}
获取当前等级奖级的属性点
int32 ARPGHero::GetAttributePointsReward_Implementation(const int32 Level) const
{const ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏return PlayerStateBase->LevelUpInfo->LevelUpInformation[Level].AttributePointAward;
}
获取当前等级奖励的技能点
int32 ARPGHero::GetSpellPointsReward_Implementation(const int32 Level) const
{const ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏return PlayerStateBase->LevelUpInfo->LevelUpInformation[Level].SpellPointAward;
}
设置当前角色升级,主要是修改角色的数值
void ARPGHero::AddToPlayerLevel_Implementation(int32 InPlayerLevel)
{ARPGPlayerState* PlayerStateBase = GetPlayerState<ARPGPlayerState>();check(PlayerStateBase); //检测是否有效,无限会暂停游戏PlayerStateBase->AddToLevel(InPlayerLevel);
}
还有在后续章节中添加的属性修改功能函数,为角色增加属性点和技能点
void ARPGHero::AddToAttributePoints_Implementation(int32 InAttributePoints)
{//TODO:实现增加属性点
}void ARPGHero::AddToSpellPoints_Implementation(int32 InSpellPoints)
{//TODO:实现增加技能点
}
实现等级升级
接下来,我们在AS里面获得经验后,处理升级相关逻辑。
首先判断是否升级,逻辑是当前经验+获得的经验是否能够升级,获取到增加经验后的等级和原来的等级是否一致,如果增加了,意味着角色等级得到了提升。
//获取获得经验后的新等级
const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
const int32 NumLevelUps = NewLevel - CurrentLevel; //查看等级是否有变化
if(NumLevelUps > 0)
{...
}
在升级逻辑里面,我们首先获取当前等级提供的属性点和技能点的奖励
//获取升级提供的技能点和属性点
int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, CurrentLevel);
int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, CurrentLevel);
然后提升角色等级,并应用奖励
//提升等级,增加角色技能点和属性点
IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);
IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
调用升级表现函数
IPlayerInterface::Execute_LevelUp(Props.SourceCharacter); //升级
升级时,将血量和蓝量填满
//将血量和蓝量填充满
SetHealth(GetMaxHealth());
SetMana(GetMana());
下面列出来完整代码
if(Data.EvaluatedData.Attribute == GetIncomingXPAttribute()){const float LocalIncomingXP = GetIncomingXP();SetIncomingXP(0);// UE_LOG(LogRPG, Log, TEXT("获取传入经验值:%f"), LocalIncomingXP);if(Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>()){//获取角色当前等级和经验const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);//获取获得经验后的新等级const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);const int32 NumLevelUps = NewLevel - CurrentLevel; //查看等级是否有变化if(NumLevelUps > 0){//获取升级提供的技能点和属性点int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, CurrentLevel);int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, CurrentLevel);//提升等级,增加角色技能点和属性点IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);IPlayerInterface::Execute_LevelUp(Props.SourceCharacter); //升级//将血量和蓝量填充满SetHealth(GetMaxHealth());SetMana(GetMana());}//将经验应用给自身,通过事件传递,在玩家角色被动技能GA_ListenForEvents里接收IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);}}
在ui上面显示等级
我们现在角色有了等级,需要在UI上面表现出来。
我们首先处理回调,之前实现了经验的表现,我们在PlayerState类里面设置和升级时,会调用委托广播
那么,我们在UI Controller里面添加一个委托,用于广播给UI监听,等级为整型参数
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChangedSignature, int32, NewValue); //当玩家状态该表回调类型
接着创建对应类型的变量
UPROPERTY(BlueprintAssignable, Category="GAS|Level")FOnPlayerStateChangedSignature OnPlayerLevelChangeDelegate; //等级变动回调
绑定一个匿名函数,用于监听PlayerState的等级回调
//绑定等级相关回调RPGPlayerState->OnLevelChangedDelegate.AddLambda([this](int32 NewLevel){OnPlayerLevelChangeDelegate.Broadcast(NewLevel);});
接下来,我们基于WBP_SpellGlobe复制一份,创建一个用于显示等级的ui
将使用图标设置的部分删除,我们不需要在等级显示UI里面设置图标
在设置控制器回调里面监听等级委托,回调触发修改等级显示文字
为了方便设置,我们可以修改对应怪物提供的经验值
将其拖入到WBP_Overlay上面,并设置对应控制器
接下来,我们就可以运行查看效果
创建角色图标
接下来,我们想在左上角显示对应角色的图标,这里使用WBP_GlobeProgressbar作为模版复制一份
命名为WBP_PictureFrame
移动到对应的文件夹中
删除一些无用的函数
增加一个用于显示头像的节点
设置对应头像
在WBP_Overlay里面应用,并设置好位置
设置升级效果
接下来,我们将上面略过的等级升级效果函数实现一下,我们需要增加一个粒子特效在升级时播放特效。由于特效是平面播放,它会跟随角色的角度,有时候看到是一个竖条的,所以我们还需要将相机在代码里创建,方便后续获取相机的旋转。
我们首先在代码里创建相机和弹簧臂
UPROPERTY(VisibleAnywhere)TObjectPtr<UCameraComponent> TopDownCameraComponent; //相机组件UPROPERTY(VisibleAnywhere)TObjectPtr<USpringArmComponent> CameraBoom; //弹簧臂组件
在初始化时创建它们
//设置相机CameraBoom = CreateDefaultSubobject<USpringArmComponent>("CameraBoom");CameraBoom->SetupAttachment(GetRootComponent());CameraBoom->SetUsingAbsoluteRotation(true);CameraBoom->bDoCollisionTest = false;TopDownCameraComponent = CreateDefaultSubobject<UCameraComponent>("TopDownCameraComponent");TopDownCameraComponent->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);TopDownCameraComponent->bUsePawnControlRotation = false;
然后创建一个NiagaraComponent,用于设置使用的粒子特效,它需要在蓝图中设置使用的粒子资源
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)TObjectPtr<UNiagaraComponent> LevelUpNiagaraComponent; //升级特效组件
在构造函数中初始化,并将自动播放关闭,我们在角色升级的时候让其播放
//设置升级特效组件LevelUpNiagaraComponent = CreateDefaultSubobject<UNiagaraComponent>("LevelUpNiagaraComponent");LevelUpNiagaraComponent->SetupAttachment(GetRootComponent()); //设置附加组件LevelUpNiagaraComponent->bAutoActivate = false; //设置不自动激活
我们还需要创建一个支持多客户端调用的函数,它将在每个客户端都实现调用,这样,在每个客户端都能看到同样的效果
UFUNCTION(NetMulticast, Reliable)void MulticastLevelUpParticles() const; //在多人游戏,每个客户端上播放升级特效
在实现这里,我们将通过相机和粒子位置的朝向,注意,这个朝向是世界坐标系的,然后通过朝向获取旋转,设置给粒子
void ARPGHero::MulticastLevelUpParticles_Implementation() const
{if(IsValid(LevelUpNiagaraComponent)){const FVector CameraLocation = TopDownCameraComponent->GetComponentLocation();const FVector NiagaraSystemLocation = LevelUpNiagaraComponent->GetComponentLocation();const FRotator TopCameraRotation = (CameraLocation - NiagaraSystemLocation).Rotation(); //获取相机位置和离职特效的朝向LevelUpNiagaraComponent->SetWorldRotation(TopCameraRotation); //设置粒子的转向LevelUpNiagaraComponent->Activate(true); //激活特效}
}
并在AS调用函数时,调用此函数,因为AS只在服务器运行,再调用此函数,实现了每个客户端的运行
void ARPGHero::LevelUp_Implementation()
{MulticastLevelUpParticles(); //调用播放升级特效
}
接着编译打开UE,设置粒子特效
并将之前在蓝图中创建的相机和弹簧臂删除掉,将之前的碰撞盒移动到代码创建的弹簧臂上面
删除蓝图创建的相机和弹簧臂
运行查看效果。
创建升级消息提示
接下来,我们优化效果,在升级时提供消息提示UI,并播放音效。
父类选择我们自定义的RPGUserWidget
命名为WBP_LevelUpMessage 为升级提示
我们在里面添加一个覆层,作为全屏显示,并在覆层下面添加一个包裹框,文字超出范围后会自动换行
为了防止文字在最顶部显示,我们在上面添加一个间隔区,用来填充空间,宽度设置的尽量大一些,防止文字在间隔区后面显示。
增加一个文本,设置好样式,让其可以占用一整行
下面添加一个水平框,框内设置两个文本,显示等级和实际等级数字
水平框设置对其和填充空间以及强制换新行,文本向左向右对其,这样,就可以居中显示
实际效果如下
我们也可以用上下设置垂直框,这样更方便设置
接着,我们要在WBP_Overlay里面在设置了控制器后,监听等级的变动,在监听到等级变化后,将现存的等级提示删除,然后接着创建一个新的控件,并将目标等级设置,并添加到视口。这样可以防止连续升级时,多层覆盖的问题。
运行查看效果。
接下来,我们优化一下,制作一个动画
在事件开始后,播放音效和动画,并延迟三秒将UI销毁
接下来查看最终效果