换弹技能
/EqZeroCore/Weapons/Pistol/AbilitySet_ShooterPistol.AbilitySet_ShooterPistol
这个技能集里面配置 换弹技能
/Game/Weapons/Pistol/GA_Weapon_Reload_Pistol.GA_Weapon_Reload_Pistol 这个主要配置一下换弹蒙太奇,基于武器有所不同
/Game/Weapons/GA_Weapon_ReloadMagazine.GA_Weapon_ReloadMagazine 换弹的核心逻辑
再父类就是 C++ Ability From Equipment
这个类核心是方便拿到 技能所对应的武器对象
完成这个激活
播放换弹蒙太奇,然后wait event,注意这个event在动画蒙太奇里面用 anim notify 触发的
第一块逻辑,应该是有剩余子弹的时候,不阻塞开火。
开火打断换弹?具体逻辑在哪
void UEqZeroGameplayAbility_ReloadMagazine::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
if (auto InventoryItemInstance = GetAssociatedItem())
{
DidBlockFiring = InventoryItemInstance->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo) == 0;
if (DidBlockFiring)
{
if (UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get())
{
ASC->AddLooseGameplayTag(TAG_WeaponFireBlocked);
}
}
}
// 蒙太奇任务:播放换弹动画
UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this,
FName("ReloadMontage"),
ReloadMontage,
PlayRate,
NAME_None,
false
);
if (MontageTask)
{
// 动画在客户端播放完成时,不要结束服务器上的技能,让其自行结束
MontageTask->OnCompleted.AddDynamic(this, &ThisClass::OnMontageCompleted);
MontageTask->OnInterrupted.AddDynamic(this, &ThisClass::OnMontageInterrupted);
MontageTask->OnCancelled.AddDynamic(this, &ThisClass::OnMontageCancelled);
MontageTask->ReadyForActivation();
}
// 事件任务:等待动画 Notify 发出 GameplayEvent.ReloadDone
UAbilityTask_WaitGameplayEvent* EventTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
TAG_GameplayEvent_ReloadDone,
nullptr,
true,
true
);
if (EventTask)
{
EventTask->EventReceived.AddDynamic(this, &ThisClass::OnReloadEventReceived);
EventTask->ReadyForActivation();
}
}
到这里,能换弹,具体调一下,子弹更新是没问题的。
void UEqZeroGameplayAbility_ReloadMagazine::OnReloadEventReceived(FGameplayEventData Payload)
{
// 仅在服务器端执行实际换弹逻辑
if (HasAuthority(&CurrentActivationInfo))
{
ReloadAmmoIntoMagazine(Payload);
}
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
void UEqZeroGameplayAbility_ReloadMagazine::ReloadAmmoIntoMagazine(FGameplayEventData Payload)
{
UEqZeroInventoryItemInstance* Item = GetAssociatedItem();
if (!Item)
{
return;
}
const int32 MagSize = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineSize); // 弹匣容量
const int32 MagAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo); // 当前弹匣中的子弹数量
const int32 SpareAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo); // 当前备用弹药数量
// 计算本次需要装填的子弹数量,不超过备用弹药
const int32 Needed = FMath::Min(MagSize - MagAmmo, SpareAmmo);
if (Needed > 0)
{
Item->AddStatTagStack(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo, Needed);
Item->RemoveStatTagStack(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo, Needed);
DebugLog();
}
}
然后没子弹也能换。
bool UEqZeroGameplayAbility_ReloadMagazine::CanActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags,
OUT FGameplayTagContainer* OptionalRelevantTags) const
{
if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
{
return false;
}
const UEqZeroInventoryItemInstance* Item = GetAssociatedItem();
if (!Item)
{
return false;
}
const int32 MagAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineAmmo);
const int32 MagSize = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_MagazineSize);
// 弹匣已满,无需装填
if (MagAmmo >= MagSize)
{
if (OptionalRelevantTags)
{
OptionalRelevantTags->AddTag(TAG_ActivateFail_MagazineFull);
}
return false;
}
const int32 SpareAmmo = Item->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);
// 备用弹药为空,无法装填
if (SpareAmmo <= 0)
{
if (OptionalRelevantTags)
{
OptionalRelevantTags->AddTag(TAG_ActivateFail_NoSpareAmmo);
}
return false;
}
return true;
}
场景有枪能捡起来能切换
发现这下要把第二把武器配出来了
动画蓝图配置
/Game/Characters/Heroes/Mannequin/Animations/Locomotion/Rifle/ABP_RifleAnimLayers.ABP_RifleAnimLayers
/Game/Characters/Heroes/Mannequin/Animations/Locomotion/Rifle/ABP_RifleAnimLayers_Feminine.ABP_RifleAnimLayers_Feminine
步枪 武器实例
/EqZeroCore/Weapons/Rifle/B_WeaponInstance_Rifle.B_WeaponInstance_Rifle
步枪Actor蓝图(提供mesh)
/EqZeroCore/Weapons/Rifle/B_Rifle.B_Rifle
装备定义
/EqZeroCore/Weapons/Rifle/WID_Rifle.WID_Rifle
物品定义
/EqZeroCore/Weapons/Rifle/ID_Rifle.ID_Rifle
配置枪的定义
/EqZeroCore/Weapons/Rifle/WeaponPickupData_Rifle.WeaponPickupData_Rifle
=》
然后在场景创建这个Actor 配置好
服务器碰撞检测
void AEqZeroWeaponSpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
APawn* OverlappingPawn = Cast<APawn>(OtherActor);
if (GetLocalRole() == ROLE_Authority && bIsWeaponAvailable && OverlappingPawn != nullptr)
{
AttemptPickUpWeapon(OverlappingPawn);
}
}
尝试把武器给角色
void AEqZeroWeaponSpawner::AttemptPickUpWeapon_Implementation(APawn* Pawn)
{
if (GetLocalRole() == ROLE_Authority && bIsWeaponAvailable && UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Pawn))
{
TSubclassOf<UEqZeroInventoryItemDefinition> WeaponItemDefinition = WeaponDefinition ? WeaponDefinition->InventoryItemDefinition : nullptr;
if (WeaponItemDefinition != nullptr)
{
// 尝试将武器给予 Pawn
if (GiveWeapon(WeaponItemDefinition, Pawn))
{
// 武器成功拾取:隐藏武器、播放特效、开始冷却
bIsWeaponAvailable = false;
SetWeaponPickupVisibility(false);
PlayPickupEffects();
StartCoolDown();
}
}
}
}
给武器,设置暂时不可用,隐藏mesh,特效。开后开启CD
注意 bIsWeaponAvailable 变量在服务器修改回触发同步。
void AEqZeroWeaponSpawner::OnRep_WeaponAvailability()
{
if (bIsWeaponAvailable)
{
// 武器重新可用:播放重生特效并显示网格体
PlayRespawnEffects();
SetWeaponPickupVisibility(true);
}
else
{
// 武器已被拾取:隐藏网格体、开始冷却并播放拾取特效
SetWeaponPickupVisibility(false);
StartCoolDown();
PlayPickupEffects();
}
}
give weapon的逻辑,有两种,已经有武器就加子弹,否则就是加上这边枪
bool AEqZeroWeaponSpawner::GiveWeapon_Implementation(TSubclassOf<UEqZeroInventoryItemDefinition> WeaponItemClass, APawn* ReceivingPawn)
{
auto Controller = ReceivingPawn->GetController();
if (!Controller)
{
return false;
}
auto QuickBarComponent = Controller->FindComponentByClass<UEqZeroQuickBarComponent>();
auto InventoryManager = Controller->FindComponentByClass<UEqZeroInventoryManagerComponent>();
if (!QuickBarComponent || !InventoryManager)
{
return false;
}
auto ExistingWeaponInstance = InventoryManager->FindFirstItemStackByDefinition(WeaponItemClass);
if (ExistingWeaponInstance && bGiveAmmoForDuplicatedWeapon)
{
// 计算默认最大备用弹药量与当前剩余量的差值,补充到上限
const int32 DefaultSpareAmmo = GetDefaultStatFromItemDef(WeaponItemClass, EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);
const int32 CurrentSpareAmmo = ExistingWeaponInstance->GetStatTagStackCount(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo);
const int32 AmmoToAdd = DefaultSpareAmmo - CurrentSpareAmmo;
if (AmmoToAdd > 0)
{
ExistingWeaponInstance->AddStatTagStack(EqZeroGameplayTags::Lyra_Weapon_SpareAmmo, AmmoToAdd);
}
return true;
}
auto NextSlot = QuickBarComponent->GetNextFreeItemSlot();
if (NextSlot >= 0)
{
// 还有空位,直接添加新武器实例
// TODO gameplay cue
if (auto Ins = InventoryManager->AddItemDefinition(WeaponItemClass, 1))
{
QuickBarComponent->AddItemToSlot(NextSlot, Ins);
// 源项目有一个捡起是否直接切换的逻辑,不打算做
return true;
}
}
return false;
}
然后是CD
项目配置的是4s后刷新武器
void AEqZeroWeaponSpawner::StartCoolDown()
{
if (UWorld* World = GetWorld())
{
World->GetTimerManager().SetTimer(CoolDownTimerHandle, this, &AEqZeroWeaponSpawner::OnCoolDownTimerComplete, CoolDownTime);
}
}
事件到了,显示武器,相关的效果。这里延迟了一下,让 bIsWeaponAvailable 客户端把武器重新显示出来后,才CheckForExistingOverlaps
void AEqZeroWeaponSpawner::ResetCoolDown()
{
UWorld* World = GetWorld();
if (World)
{
World->GetTimerManager().ClearTimer(CoolDownTimerHandle);
}
if (GetLocalRole() == ROLE_Authority)
{
bIsWeaponAvailable = true;
PlayRespawnEffects();
SetWeaponPickupVisibility(true);
// 延迟后检查重叠,给客户端 OnRep 足够时间先行触发
if (World)
{
World->GetTimerManager().SetTimer(CheckOverlapsDelayTimerHandle, this, &AEqZeroWeaponSpawner::CheckForExistingOverlaps, CheckExistingOverlapDelay);
}
}
CoolDownPercentage = 0.0f;
}
按键切换武器


这线真的是怎么连都不好看。。。
这个带TAG的Event会发到哪里呢?
/EqZeroCore/Game/GA_QuickbarSlots.GA_QuickbarSlots

这是一个OnSpawn就加到角色上的 Wait Gameplay Event,他会等待对应的Event
到现在我们有多少事件了
MessageRouter
WaitGameplayEvent
技能的阻塞
/EqZeroCore/Game/TagRelationships_ShooterHero.TagRelationships_ShooterHero
这个配置嵌入到GAS的流程
ApplyAbilityBlockAndCancelTags
GetAdditionalActivationTagRequirements
中
开镜技能完善
UEqZeroGameplayAbility_ADS 核心C++代码
/EqZeroCore/Input/Abilities/GA_ADS.GA_ADS 【配置】
检查所有配置项是否都已经有了
【添加CameraMode】
/EqZeroCore/Camera/CM_ThirdPersonADS.CM_ThirdPersonADS
/EqZeroCore/Camera/ThirdPersonADSOffsetCurve
【IMG 】
看起来是通过这个缩放改输入的速度,不确定
/EqZeroCore/Input/Mappings/IMC_ADS_Speed.IMC_ADS_Speed

这个配置和 settings shared 里面的东西有关,所以现在可能起不了作用。
最后完成配置
emmmmm

又炸了,视角明显不对。Y轴反转,
先去掉IMG看看,不反转了,其他的晚点看吧。
位置不对比较关键
void AEqZeroPlayerCameraManager::DisplayDebug(UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplay, float& YL, float& YPos)
{
check(Canvas);
FDisplayDebugManager& DisplayDebugManager = Canvas->DisplayDebugManager;
DisplayDebugManager.SetFont(GEngine->GetSmallFont());
DisplayDebugManager.SetDrawColor(FColor::Yellow);
DisplayDebugManager.DrawString(FString::Printf(TEXT("EqZeroPlayerCameraManager: %s"), *GetNameSafe(this)));
Super::DisplayDebug(Canvas, DebugDisplay, YL, YPos);
const APawn* Pawn = (PCOwner ? PCOwner->GetPawn() : nullptr);
if (const UEqZeroCameraComponent* CameraComponent = UEqZeroCameraComponent::FindCameraComponent(Pawn))
{
CameraComponent->DrawDebug(Canvas);
}
}
曲线配置错了。改为OK了