Gameplay Classes
Tips
- PlayerController只存在于本地客户端和服务器上,不在其他客户端上存在
- GameMode只存在于服务器上
- 所有的Pawn会在全部客户端和服务器上拷贝一份
- 需要将数据转换为以服务器为权威的
- RepNotify适用于状态发生改变
- 当新玩家加入游戏,会将已改变的状态全部同步正常
- 对应用范围外事件优化时也有帮助
- NetMulticast适用于作为一个过渡函数 - Momentary Event
- 当新玩家加入游戏,则不会同步已改变的状态
- Meta:
- ExposeOnSpawn - 在蓝图创建时的节点上可以修改
- 当在蓝图中使用RPC时需要注意后续其他功能设置延迟 // 例如DestroyActor,可使用SetLifeSpan替代
项目代码
GitHub: https://github.com/yufeige4/ActionRoguelike
- 修复PlayerController未判断是否为本地导致的报错
- 将刷新道具变为Replicated的
- 使用NetMulticast方法将AttributeComponent支持多人游戏
- 怪物AI简单修复 // 将projectile变成replicated
- 对ActionComponent(ActorComp)的StartAction函数进行了简单的处理,支持多人游戏
// GAttributeComponent.h
// 代理声明
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnHealthChanged, AActor*, InstigatorActor, UGAttributeComponent*, OwningComp, float, NewHealth, float, Delta);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FOnRageChanged, AActor*, InstigatorActor, UGAttributeComponent*, OwningComp, int32, NewRage, int32, Delta, bool, IsSuccess);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API UGAttributeComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UGAttributeComponent();
protected:
// 当前生命值
UPROPERTY(Replicated, EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
float CurrHealth;
// 最大生命值
UPROPERTY(Replicated, EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
float MaxHealth;
// 当前怒气
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
int32 CurrRage;
// 愤怒值变化量
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
int32 RageGrowthRate;
// 最大怒气
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attributes")
int32 MaxRage;
public:
// 判断怒气值是否足够
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool HaveEnoughRage(int32 RageCost);
// 对怒气值进行修改
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool ApplyRageChange(AActor* Instigator, int32 Delta);
// 包含代理
UPROPERTY(BlueprintAssignable, Category = "Attributes")
FOnRageChanged OnRageChanged;
// 对生命值进行修改
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool ApplyHealthChange(AActor* Instigator, float Delta);
// 包含代理
UPROPERTY(BlueprintAssignable, Category = "Attributes")
FOnHealthChanged OnHealthChanged;
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool IsAlive() const;
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool IsFullHealth() const;
UFUNCTION(BlueprintCallable, Category = "Attributes")
static UGAttributeComponent* GetAttributeComponent(AActor* Actor);
UFUNCTION(BlueprintCallable, Category = "Attributes", meta = (DisplayName = "IsAlive"))
static bool IsActorAlive(AActor* Actor);
UFUNCTION(BlueprintCallable, Category = "Attributes")
float GetCurrHealth();
UFUNCTION(BlueprintCallable, Category = "Attributes")
float GetMaxHealth();
UFUNCTION(BlueprintCallable, Category = "Attributes")
float GetMaxRage();
UFUNCTION(BlueprintCallable, Category = "Attributes")
bool Kill(AActor* Instigator);
protected:
UFUNCTION(NetMulticast, Reliable) // @FIXME: mark as unreliable once moved the "state" of PlayerCharacter
void MulticastHealthChanged(AActor* Instigator, float NewHealth, float Delta);
};
// GAttributeComponent.cpp
static TAutoConsoleVariable<float> CVarDamageMultiplier(TEXT("ARPG.DamageMultiplier"),1.0f,TEXT("Global ratio of damage dealing"),ECVF_Cheat);
// Sets default values for this component's properties
UGAttributeComponent::UGAttributeComponent()
{
CurrHealth = 100.0f;
MaxHealth = 100.0f;
CurrRage = 0;
RageGrowthRate = 5;
MaxRage = 100;
SetIsReplicatedByDefault(true);
}
bool UGAttributeComponent::HaveEnoughRage(int32 RageCost)
{
return (CurrRage-RageCost)>=0;
}
bool UGAttributeComponent::ApplyRageChange(AActor* Instigator, int32 Delta)
{
if(Delta==0)
{
return false;
}
int32 NewRage = FMath::Clamp(CurrRage+Delta,0,MaxRage);
int32 ActualDelta = NewRage-CurrRage;
bool bSuccess = true;
if(Delta<0)
{
bSuccess = Delta==ActualDelta ? true : false;
}
if(bSuccess)
{
CurrRage = NewRage;
}
OnRageChanged.Broadcast(Instigator,this,CurrRage,ActualDelta,bSuccess);
return bSuccess;
}
bool UGAttributeComponent::ApplyHealthChange(AActor* Instigator, float Delta)
{
if(!GetOwner()->CanBeDamaged() && Delta<0.0f)
{
return false;
}
float Ratio = 1;
if(Delta<0.0f)
{
Ratio = CVarDamageMultiplier.GetValueOnGameThread();
Delta *= Ratio;
}
float PreviousHealth = CurrHealth;
CurrHealth = FMath::Clamp(CurrHealth+Delta,0.0f,MaxHealth);
float AcutualDelta = CurrHealth - PreviousHealth;
if(AcutualDelta<0.0f)
{
ApplyRageChange(Instigator,RageGrowthRate);
}
// 调用代理
// OnHealthChanged.Broadcast(Instigator,this,CurrHealth,AcutualDelta);
if(AcutualDelta!=0.0f)
{
MulticastHealthChanged(Instigator,CurrHealth,AcutualDelta);
}
// Died
if(AcutualDelta<0.0f && CurrHealth==0.0f)
{
auto Owner = GetOwner();
auto EventManager = UGEventManager::GetEventManager(Owner);
if(EventManager)
{
EventManager->OnActorKilled(Owner,Instigator);
}
}
return true;
}
bool UGAttributeComponent::IsAlive() const
{
return CurrHealth > 0.0f;
}
bool UGAttributeComponent::IsFullHealth() const
{
return CurrHealth==MaxHealth;
}
UGAttributeComponent* UGAttributeComponent::GetAttributeComponent(AActor* Actor)
{
if(Actor)
{
return Cast<UGAttributeComponent>(Actor->GetComponentByClass(UGAttributeComponent::StaticClass()));
}
return nullptr;
}
// return false if no AttributeComp or not alive
bool UGAttributeComponent::IsActorAlive(AActor* Actor)
{
UGAttributeComponent* AttributeComp = GetAttributeComponent(Actor);
return (AttributeComp && AttributeComp->IsAlive());
}
float UGAttributeComponent::GetCurrHealth()
{
return CurrHealth;
}
float UGAttributeComponent::GetMaxHealth()
{
return MaxHealth;
}
float UGAttributeComponent::GetMaxRage()
{
return MaxRage;
}
bool UGAttributeComponent::Kill(AActor* Instigator)
{
return ApplyHealthChange(Instigator,-MaxHealth);
}
void UGAttributeComponent::MulticastHealthChanged_Implementation(AActor* Instigator, float NewHealth, float Delta)
{
OnHealthChanged.Broadcast(Instigator,this,NewHealth,Delta);
}
void UGAttributeComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UGAttributeComponent,CurrHealth);
DOREPLIFETIME(UGAttributeComponent,MaxHealth);
// if MaxHealth is changed, only the owner can see it, for saving bandwidth
// DOREPLIFETIME_CONDITION(UGAttributeComponent,CurrHealth,COND_OwnerOnly);
}
// GActionComponent.h
class UGAction;
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API UGActionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UGActionComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags")
FGameplayTagContainer ActiveGameplayTags;
protected:
UPROPERTY()
TArray<UGAction*> Actions;
UPROPERTY(EditAnywhere, Category = "Actions")
TArray<TSubclassOf<UGAction>> DefaultActions;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
UFUNCTION(BlueprintCallable, Category = "Actions")
UGAction* AddAction(TSubclassOf<UGAction> ActionClass, AActor* Instigator);
UFUNCTION(BlueprintCallable, Category = "Actions")
void RemoveAction(UGAction* Action);
UFUNCTION(BlueprintCallable, Category = "Actions")
bool StartActionByName(AActor* Instigator, FName ActionName);
UFUNCTION(BlueprintCallable, Category = "Actions")
bool StopActionByName(AActor* Instigator, FName ActionName);
UFUNCTION(BlueprintCallable, Category = "Actions")
static UGActionComponent* GetActionComponent(AActor* Actor);
protected:
virtual void BeginPlay() override;
UFUNCTION(Server, Reliable)
void ServerStartAction(AActor* Instigator, FName ActionName);
UFUNCTION(Server, Reliable)
void ServerStopAction(AActor* Instigator, FName ActionName);
};
// GActionComponent.cpp
UGActionComponent::UGActionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
SetIsReplicatedByDefault(true);
}
void UGActionComponent::BeginPlay()
{
Super::BeginPlay();
for(TSubclassOf<UGAction> Action : DefaultActions)
{
// add from default actions should consider owner of comp as Instigator
AddAction(Action,GetOwner());
}
}
void UGActionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if(DebugActionTag.GetValueOnGameThread())
{
if(GEngine)
{
FString DebugMsg = GetNameSafe(GetOwner()) + " : " + ActiveGameplayTags.ToStringSimple();
GEngine->AddOnScreenDebugMessage(-1,0.0,FColor::White,DebugMsg);
}
}
}
UGAction* UGActionComponent::AddAction(TSubclassOf<UGAction> ActionClass, AActor* Instigator)
{
if(!ensure(ActionClass))
{
return nullptr;
}
// UE创建新的UObject的方式
UGAction* NewAction = NewObject<UGAction>(this,ActionClass);
if(ensure(NewAction))
{
Actions.Add(NewAction);
if(NewAction->bAutoStart && ensure(NewAction->CanStart(Instigator)))
{
NewAction->StartAction(Instigator);
}
}
return NewAction;
}
void UGActionComponent::RemoveAction(UGAction* Action)
{
if(!ensure(Action && !Action->IsRunning()))
{
return;
}
Actions.Remove(Action);
}
bool UGActionComponent::StartActionByName(AActor* Instigator, FName ActionName)
{
for(UGAction* Action : Actions)
{
if(Action && Action->ActionName==ActionName)
{
if(!Action->CanStart(Instigator))
{
return false;
}
// after checking conditions, if client then do server RPC
if(!GetOwner()->HasAuthority())
{
ServerStartAction(Instigator,ActionName);
}
Action->StartAction(Instigator);
return true;
}
}
return false;
}
bool UGActionComponent::StopActionByName(AActor* Instigator, FName ActionName)
{
for(UGAction* Action : Actions)
{
if(Action && Action->ActionName==ActionName)
{
if(Action->IsRunning())
{
Action->StopAction(Instigator);
return true;
}
}
}
return false;
}
UGActionComponent* UGActionComponent::GetActionComponent(AActor* Actor)
{
return Cast<UGActionComponent>(Actor->GetComponentByClass(UGActionComponent::StaticClass()));
}
void UGActionComponent::ServerStartAction_Implementation(AActor* Instigator, FName ActionName)
{
StartActionByName(Instigator,ActionName);
}
void UGActionComponent::ServerStopAction_Implementation(AActor* Instigator, FName ActionName)
{
}