Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] fire projectiles in multiplayer #115

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,4 @@ DerivedDataCache/*
# Rider user specific files
.idea/
/Plugins/Developer/RiderLink
.vsconfig
13 changes: 0 additions & 13 deletions .vsconfig

This file was deleted.

2 changes: 2 additions & 0 deletions Config/DefaultEditor.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ bReplaceBlueprintWithClass= true
bDontLoadBlueprintOutsideEditor= true
bBlueprintIsNotBlueprintType= true

[/Script/AdvancedPreviewScene.SharedProfiles]

2 changes: 1 addition & 1 deletion Config/DefaultEngine.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ EditorStartupMap=/Game/GASDocumentation/Maps/Map_Startup.Map_Startup
GlobalDefaultGameMode="/Script/GASDocumentation.GASDocumentationGameMode"

[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
MinimumiOSVersion=IOS_12
MinimumiOSVersion=IOS_15

[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/GASDocumentation")
Expand Down
Binary file modified Content/AnimStarterPack/Fire_Rifle_Hip_Montage.uasset
Binary file not shown.
Binary file modified Content/AnimStarterPack/Fire_Rifle_Ironsights_Montage.uasset
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified Content/GASDocumentation/Maps/Map_Startup.umap
Binary file not shown.
Binary file modified Content/GASDocumentation/UI/UI_HUD.uasset
Binary file not shown.
2 changes: 1 addition & 1 deletion GASDocumentation.uproject
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"FileVersion": 3,
"EngineAssociation": "5.2",
"EngineAssociation": "5.3",
"Category": "",
"Description": "",
"Modules": [
Expand Down
410 changes: 211 additions & 199 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenForAttributeChange

if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid())
{
/** Remove an object from the root set. */
WaitForAttributeChangedTask->RemoveFromRoot();
return nullptr;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ void UAsyncTaskEffectStackChanged::EndTask()

if(ActiveEffectHandle.IsValid())
{
ASC->OnGameplayEffectStackChangeDelegate(ActiveEffectHandle)->RemoveAll(this);
if (FOnActiveGameplayEffectStackChange* OnActiveGameplayEffectStackChange = ASC->OnGameplayEffectStackChangeDelegate(ActiveEffectHandle))
{
OnActiveGameplayEffectStackChange->RemoveAll(this);
}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,7 @@ void UGDAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCall

if (Hit)
{
EGDHitReactDirection HitDirection = TargetCharacter->GetHitReactDirection(Data.EffectSpec.GetContext().GetHitResult()->Location);
switch (HitDirection)
switch (EGDHitReactDirection HitDirection = TargetCharacter->GetHitReactDirection(Data.EffectSpec.GetContext().GetHitResult()->Location))
{
case EGDHitReactDirection::Left:
TargetCharacter->PlayHitReact(HitDirectionLeftTag, SourceCharacter);
Expand All @@ -154,6 +153,7 @@ void UGDAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCall
case EGDHitReactDirection::Back:
TargetCharacter->PlayHitReact(HitDirectionBackTag, SourceCharacter);
break;
default: ;
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ void AGDCharacterBase::Die()
EffectTagsToRemove.AddTag(EffectRemoveOnDeathTag);
int32 NumEffectsRemoved = AbilitySystemComponent->RemoveActiveEffectsWithTags(EffectTagsToRemove);

/**
* The Sample Project uses a `LooseGameplayTag` for `State.Dead` so that the owning clients can immediately respond to when their health drops to zero.
* Respawning manually sets the `TagMapCount` back to zero.
*/
AbilitySystemComponent->AddLooseGameplayTag(DeadTag);
}

Expand Down
7 changes: 4 additions & 3 deletions Source/GASDocumentation/Private/Characters/GDProjectile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ AGDProjectile::AGDProjectile()
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;

bReplicates = true;

// bReplicates = true;
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(FName("ProjectileMovement"));
}

// Called when the game starts or when spawned
void AGDProjectile::BeginPlay()
{
Super::BeginPlay();


// SetReplicateMovement(true);
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ UGDGA_FireGun::UGDGA_FireGun()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;

FGameplayTag Ability1Tag = FGameplayTag::RequestGameplayTag(FName("Ability.Skill.Ability1"));
const FGameplayTag Ability1Tag = FGameplayTag::RequestGameplayTag(FName("Ability.Skill.Ability1"));
AbilityTags.AddTag(Ability1Tag);
ActivationOwnedTags.AddTag(Ability1Tag);

Expand All @@ -22,7 +22,8 @@ UGDGA_FireGun::UGDGA_FireGun()
Damage = 12.0f;
}

void UGDGA_FireGun::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData * TriggerEventData)
void UGDGA_FireGun::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
Expand All @@ -38,7 +39,8 @@ void UGDGA_FireGun::ActivateAbility(const FGameplayAbilitySpecHandle Handle, con
}

// Play fire montage and wait for event telling us to spawn the projectile
UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(
this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Expand Down Expand Up @@ -68,36 +70,37 @@ void UGDGA_FireGun::EventReceived(FGameplayTag EventTag, FGameplayEventData Even
return;
}

// Only spawn projectiles on the Server.
// Predicting projectiles is an advanced topic not covered in this example.
if (GetOwningActorFromActorInfo()->GetLocalRole() == ROLE_Authority && EventTag == FGameplayTag::RequestGameplayTag(FName("Event.Montage.SpawnProjectile")))
AGDHeroCharacter* Hero = Cast<AGDHeroCharacter>(GetAvatarActorFromActorInfo());
if (!Hero)
{
AGDHeroCharacter* Hero = Cast<AGDHeroCharacter>(GetAvatarActorFromActorInfo());
if (!Hero)
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}

FVector Start = Hero->GetGunComponent()->GetSocketLocation(FName("Muzzle"));
FVector End = Hero->GetCameraBoom()->GetComponentLocation() + Hero->GetFollowCamera()->GetForwardVector() * Range;
FRotator Rotation = UKismetMathLibrary::FindLookAtRotation(Start, End);

FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());

// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}

const FVector Start = Hero->GetGunComponent()->GetSocketLocation(FName("Muzzle"));
const FVector End = Hero->GetCameraBoom()->GetComponentLocation() + Hero->GetFollowCamera()->GetForwardVector() * Range;
const FRotator Rotation = UKismetMathLibrary::FindLookAtRotation(Start, End);


FTransform MuzzleTransform = Hero->GetGunComponent()->GetSocketTransform(FName("Muzzle"));
MuzzleTransform.SetRotation(Rotation.Quaternion());
MuzzleTransform.SetScale3D(FVector(1.0f));

FTransform MuzzleTransform = Hero->GetGunComponent()->GetSocketTransform(FName("Muzzle"));
MuzzleTransform.SetRotation(Rotation.Quaternion());
MuzzleTransform.SetScale3D(FVector(1.0f));
FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

FActorSpawnParameters SpawnParameters;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AGDProjectile* Projectile = GetWorld()->SpawnActorDeferred<AGDProjectile>(ProjectileClass, MuzzleTransform, GetOwningActorFromActorInfo(),
Hero, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

AGDProjectile* Projectile = GetWorld()->SpawnActorDeferred<AGDProjectile>(ProjectileClass, MuzzleTransform, GetOwningActorFromActorInfo(),
Hero, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

if (GetOwningActorFromActorInfo()->GetLocalRole() == ROLE_Authority && EventTag == FGameplayTag::RequestGameplayTag(
FName("Event.Montage.SpawnProjectile")))
{
const FGameplayEffectSpecHandle DamageEffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageGameplayEffect, GetAbilityLevel());
// Pass the damage to the Damage Execution Calculation through a SetByCaller value on the GameplayEffectSpec
DamageEffectSpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), Damage);
Projectile->DamageEffectSpecHandle = DamageEffectSpecHandle;
Projectile->Range = Range;
Projectile->FinishSpawning(MuzzleTransform);
}

Projectile->Range = Range;
Projectile->FinishSpawning(MuzzleTransform);
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ void AGDHeroCharacter::PossessedBy(AController * NewController)
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

// 在服务器上设置 ASC(ASC 在 PlayerState 的情况)
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);

Expand All @@ -97,6 +98,8 @@ void AGDHeroCharacter::PossessedBy(AController * NewController)

// Respawn specific things that won't affect first possession.

// 在服务器上将 DeadTag 的计数手动设为 0
// TODO 为什么不使用 RemoveLooseGameplayTag()
// Forcibly set the DeadTag count to 0
AbilitySystemComponent->SetTagMapCount(DeadTag, 0);

Expand Down Expand Up @@ -275,6 +278,7 @@ void AGDHeroCharacter::OnRep_PlayerState()
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

// 在客户端上设置 ASC(ASC 在 PlayerState 上的情况),这个函数可以保证此时客户端上存在 PlayerState
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
AbilitySystemComponent->InitAbilityActorInfo(PS, this);

Expand All @@ -300,6 +304,8 @@ void AGDHeroCharacter::OnRep_PlayerState()

// Respawn specific things that won't affect first possession.

// 在服务器上将 DeadTag 的计数手动设为 0
// TODO 为什么不使用 RemoveLooseGameplayTag()
// Forcibly set the DeadTag count to 0
AbilitySystemComponent->SetTagMapCount(DeadTag, 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ AGDMinionCharacter::AGDMinionCharacter(const class FObjectInitializer& ObjectIni
// Set our parent's TWeakObjectPtr
AbilitySystemComponent = HardRefAbilitySystemComponent;

// AttributeSet 是默认复制的
// 当 AttributeSet 作为拥有 AbilitySystemComponent 的 OwningActor 的子对象时,会自动注册到 AbilitySystemComponent 上。
// Create the attribute set, this replicates by default
// Adding it as a subobject of the owning actor of an AbilitySystemComponent
// automatically registers the AttributeSet with the AbilitySystemComponent
Expand Down Expand Up @@ -50,6 +52,7 @@ void AGDMinionCharacter::BeginPlay()

if (AbilitySystemComponent.IsValid())
{
// 设置 ASC(ASC 在 Pawn 上的情况)
AbilitySystemComponent->InitAbilityActorInfo(this, this);
InitializeAttributes();
AddStartupEffects();
Expand Down Expand Up @@ -77,6 +80,7 @@ void AGDMinionCharacter::BeginPlay()
// Attribute change callbacks
HealthChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDMinionCharacter::HealthChanged);

// 给眩晕标签的新增和移除添加回调
// Tag change callbacks
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDMinionCharacter::StunTagChanged);
}
Expand Down
4 changes: 2 additions & 2 deletions Source/GASDocumentation/Private/Player/GDPlayerController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ void AGDPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);

AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
if (AGDPlayerState* PS = GetPlayerState<AGDPlayerState>())
{
// 在服务器上设置 ASC(ASC 在 PlayerState 上的情况)TODO 感觉没有必要,因为 AI 没有 PlayerController ,而在 AGDHeroCharacter::PossessedBy 一定会设置好
// Init ASC with PS (Owner) and our new Pawn (AvatarActor)
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, InPawn);
}
Expand Down
13 changes: 10 additions & 3 deletions Source/GASDocumentation/Private/Player/GDPlayerState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ AGDPlayerState::AGDPlayerState()
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);

// Mixed 表示 GE 只复制给 autonomous proxy ,不复制给 simulated proxy ,GameplayCues 和 GameplayTags 复制给所有客户端
// Mixed mode means we only are replicated the GEs to ourself, not the GEs to simulated proxies. If another GDPlayerState (Hero) receives a GE,
// we won't be told about it by the Server. Attributes, GameplayTags, and GameplayCues will still replicate to us.
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
Expand Down Expand Up @@ -147,6 +148,7 @@ void AGDPlayerState::BeginPlay()

if (AbilitySystemComponent)
{
// 注册属性改变的回调
// Attribute change callbacks
HealthChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
MaxHealthChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetMaxHealthAttribute()).AddUObject(this, &AGDPlayerState::MaxHealthChanged);
Expand All @@ -161,21 +163,26 @@ void AGDPlayerState::BeginPlay()
GoldChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetGoldAttribute()).AddUObject(this, &AGDPlayerState::GoldChanged);
CharacterLevelChangedDelegateHandle = AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetCharacterLevelAttribute()).AddUObject(this, &AGDPlayerState::CharacterLevelChanged);

// 给眩晕标签的新增和移除添加回调
// Tag change callbacks
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
}
}

/**
* @brief Health Attribute 改变时的回调
* @param Data FOnAttributeChangeData 只会在服务器上设置
*/
void AGDPlayerState::HealthChanged(const FOnAttributeChangeData & Data)
{
float Health = Data.NewValue;
const float Health = Data.NewValue;

// 更新血条
// Update floating status bar
AGDHeroCharacter* Hero = Cast<AGDHeroCharacter>(GetPawn());
if (Hero)
{
UGDFloatingStatusBarWidget* HeroFloatingStatusBar = Hero->GetFloatingStatusBar();
if (HeroFloatingStatusBar)
if (UGDFloatingStatusBarWidget* HeroFloatingStatusBar = Hero->GetFloatingStatusBar())
{
HeroFloatingStatusBar->SetHealthPercentage(Health / GetMaxHealth());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@

DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue);

/**
* 把所有属性变化都注册了同一个回调
*
* BlueprintType
*
* 可以作为蓝图中的变量的类型
*
* meta=(ExposedAsyncProxy = AsyncTask)
*
* 加上这个之后,蓝图中的 EndTask 节点会暴露一个 AsyncTask 的输入,然后在 EndTask 中调用 SetReadyToDestroy(); MarkAsGarbage(); 就可以
* 结束 AsyncTask ,否则在蓝图中调用 EndTask 是无效的。
* https://forums.unrealengine.com/t/is-there-a-way-to-manually-terminate-ublueprintasyncactionbase/135399
*/
/**
* Blueprint node to automatically register a listener for all attribute changes in an AbilitySystemComponent.
* Useful to use in UI.
Expand All @@ -22,6 +35,21 @@ class GASDOCUMENTATION_API UAsyncTaskAttributeChanged : public UBlueprintAsyncAc
UPROPERTY(BlueprintAssignable)
FOnAttributeChanged OnAttributeChanged;

/**
* 以下是两个工厂方法,作为蓝图中的异步节点
*
* BlueprintCallable
*
* 蓝图中可调用
*
* meta = (BlueprintInternalUseOnly = "true")
*
* 一般情况下引擎会为所有 BlueprintCallable 自动生成一个蓝图节点,这个 meta 会让引擎跳过这一步骤,UBlueprintAsyncActionBase 中的静
* 态方法,有一个特殊的生成过程,这是因为这不是一般的函数节点,而是一个 “latent node”。如果不加上这个 meta ,蓝图中会有两个节点(一个按照
* 一般过程生成,一个做了特殊处理(节点的右上角有一个表),只有后者能用)。
* https://www.reddit.com/r/unrealengine/comments/vwqjms/what_does_blueprintinternaluseonly_do_for/
*/

// Listens for an attribute changing.
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute);
Expand All @@ -31,6 +59,9 @@ class GASDOCUMENTATION_API UAsyncTaskAttributeChanged : public UBlueprintAsyncAc
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static UAsyncTaskAttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray<FGameplayAttribute> Attributes);

/**
* 必须手动调用这个方法来结束异步任务
*/
// You must call this function manually when you want the AsyncTask to end.
// For UMG Widgets, you would call it in the Widget's Destruct event.
UFUNCTION(BlueprintCallable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class GASDOCUMENTATION_API UGDAttributeSetBase : public UAttributeSet
FGameplayAttributeData Armor;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Armor)

// Damage 是一个用来计算最终伤害的暂时属性,只存在于服务器上,不需要复制。
// Damage is a meta attribute used by the DamageExecution to calculate final damage, which then turns into -Health
// Temporary value that only exists on the Server. Not replicated.
UPROPERTY(BlueprintReadOnly, Category = "Damage")
Expand Down