CoopGame05-游戏网络

type: Post
status: Published
date: 2022/10/05
slug: CoopGame05
summary: UE4 C++多人游戏入门
category: Unreal

跟随B站up主“技术宅阿棍儿”的教程制作的笔记。教程链接

变成网络游戏

  1. 创建继承CoopGameGameModeBaseC++类的蓝图类BP_GameMode
  2. 在世界场景设置指定游戏模式重载为BP_GameMode,指定默认Pawn为自己的角色蓝图类BP_SCharacter
  3. 修改游戏的启动模式,网络模式:以聆听服务器运行,玩家数量:2

从服务器发枪

  1. 让服务器角色才能生成武器,判断是否为权威角色,运行结果为Server端角色有武器,Chient端角色无武器
void ASCharacter::BeginPlay(){
...
    if (GetLocalRole() == ROLE_Authority)//武器只在服务器生成
        {
             FActorSpawnParameters SpawnParameters;//生成参数
         SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;//总是生成
        CurrentWeapon = GetWorld()->SpawnActor<ASWeapon>(StartWeaponClass, FVector::ZeroVector, FRotator::ZeroRotator,
        SpawnParameters);//生成武器Actor并拿到实例
        if (CurrentWeapon)
        {
            CurrentWeapon->SetOwner(this);//设置Actor拥有者为当前角色
            CurrentWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale,
            WeaponAttachSocketName);//将生成的Actor实例附到网格体组件的骨骼插槽名上
        }
    }
...
}
  1. 使Server端角色的武器复制给Client端角色,运行结果为Server端和Chient端的角色都有武器

    • 注意BP_SWeapon蓝图类中的复制属性是否默认勾选上。
ASWeapon::ASWeapon()
{
...
    SetReplicates(true);//复制武器
...
}
  1. 使两端都能开枪。运行结果为Server端和Chient端的角色都可以开枪,但是两边窗口看不到对方开枪

客户端不能开枪的原因:鼠标左键按下调用StartFire()函数,松开调用StopFire()函数,但此时两个函数都必须满足CurrentWeapon这个变量存在,而这个变量是在上面限制武器只在Server端生成的地方赋值的,所以CurrentWeapon为空,客户端无法调用武器的Fire()函数。

1.在SCheracter.h的CurrentWeapon变量添加宏参数Replicated,用于复制。
UPROPERTY(Replicated)
class ASWeapon* CurrentWeapon; //声明武器类
2.添加网络同步函数

SCharacter.h

//重写同步函数
virtual void GetLifetimeReplicatedProps( TArray< class FLifetimeProperty > & OutLifetimeProps ) const override;

SCharacter.cpp

void ASCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(ASCharacter,CurrentWeapon);//同步条件
}

从服务器开枪

1.让Clinet端的角色不仅在本地开枪,也在Server端上开枪。运行结果:Chient端的角色开枪时所有特效在两端窗口都能看到

原理:客户端窗口里的客户端角色开枪请求服务端,让服务端里的客户端角色开火,然后服务端里的客户端角色开火后复制给客户端里的客户端角色,最后服务端里的客户端角色和客户端里的客户端角色都能开火。`

SWeapon.h
//Server RPC的大致原理:如果当前的端是Server(Authority),那么就是直接在自身执行。如果没有Authority,就是Client发送请求到Server上,然后在Server上执行这些函数。
UFUNCTION(Server,Reliable,WithValidation)//UE自带的同步机制Reliable
void ServerFire();
SWeapon.cpp
void ASWeapon::ServerFire_Implementation()
{
    Fire();
}

bool ASWeapon::ServerFire_Validate()
{
    return true;
}

void ASWeapon::Fire()
{

    //客户端上的客户端角色x调用Fire()函数时会调用ServerFire(),请求其对应的服务端里的那个客户端角色x调用Fire()函数。
    if (GetLocalRole()<ROLE_Authority)ServerFire();
}

让客户端开枪(同步枪口特效,弹道特效,击中特效)

现状:服务端窗口里的服务端角色和客户端角色都能开枪,客户端窗口上只有客户端角色能开枪,不能看到服务端角色开枪,我们要让客户端窗口里的服务端角色拿到复制过来的特效,模拟开枪。

1.同步开枪特效的属性,让服务器下发,因为之前整理的特效代码和教程不一致,这里以我自己整理的为准。运行结果:Server端的角色开枪时击中特效在两端端窗口都能看到

修改CooGame.Build.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "NavigationSystem", "PhysicsCore", "DeveloperSettings" });
SWeapon.h
//一个包含击中信息的结构体,物理表面和弹道终点
USTRUCT()
struct FHitScanTrace
{
    GENERATED_BODY()

public:
    UPROPERTY()
    FHitResult Hit;

    UPROPERTY()
    FVector_NetQuantize TraceTo;

};

class COOPGAMEPLUS_API ASWeapon : public AActor
{
    GENERATED_BODY()

public:
    //声明结构体变量和绑定调用时复制的方法,作用:当服务器中的结构体变量改变,就让客户端中的角色调用OnRep_HitScanTrace()函数?
    UPROPERTY(ReplicatedUsing=OnRep_HitScanTrace)
    FHitScanTrace HitScanTrace;
    UFUNCTION()
    void OnRep_HitScanTrace();
    //重写同步函数
    virtual void GetLifetimeReplicatedProps( TArray< class FLifetimeProperty > & OutLifetimeProps ) const override;
}
SWeapon.cpp
ASWeapon::ASWeapon()
{
//...
    //网络参数,改善延迟
    NetUpdateFrequency = 66;
    MinNetUpdateFrequency = 33;
//...
}

void ASWeapon::Fire()
{
    //...
    AActor* WeaponOwner = GetOwner();
    if (WeaponOwner)
    {
        //...
        //如果是服务端上的角色,就把弹道终点和打击信息赋值到结构体。
        if (GetLocalRole() == ROLE_Authority)
            {
                HitScanTrace.TraceTo = TraceEnd;
                HitScanTrace.Hit = Hit;
            }
    }
}

void ASWeapon::OnRep_HitScanTrace()
{
    //当结构体的值改变时,播放特效复制到客户端中的角色
    PlayFireEffects(HitScanTrace.TraceTo);
    PlayImpactEffects(HitScanTrace.Hit);
}

//同步函数
void ASWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME_CONDITION(ASWeapon,HitScanTrace,COND_SkipOwner);//结构体的同步条件:跳过拥有者(既服务端角色)
}

死亡同步

1.给生命值组件添加同步,给生命值的宏添加复制参数,设置生命值组件是否复制为true,设置只能由服务器角色绑定伤害事件,同时角色类中的bDried也得被复制到客户端上。运行结果:两端击杀的死亡动画已同步

SHealthComponent.h
UPROPERTY(Replicated,BlueprintReadOnly,Category="HealthComponent")
float Health;

//重写同步函数
    virtual void GetLifetimeReplicatedProps( TArray< class FLifetimeProperty > & OutLifetimeProps ) const override;
SHealthComponent.cpp
USHealthComponent::USHealthComponent()
{
    SetIsReplicated(true);
}

void USHealthComponent::BeginPlay()
{
    Super::BeginPlay();
    //只有服务器上的角色才能绑定伤害事件
    if (GetOwnerRole() == ROLE_Authority)
    {
        AActor* MyOwner = GetOwner();
        //当Actor收到伤害时调用OnTakeAnyDamage绑定的HandleTakeAnyDamage函数
        if (MyOwner) MyOwner->OnTakeAnyDamage.AddDynamic(this,&USHealthComponent::HandleTakeAnyDamage);
    }
}

//重写同步函数,设置同步条件
void USHealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(USHealthComponent,Health);//Health属性同步条件
}
SCharacter.h
UPROPERTY(Replicated,EditDefaultsOnly, BlueprintReadOnly, Category="Player")
 bool bDied;
SCharacter.cpp
void ASCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//...
    DOREPLIFETIME(ASCharacter,bDied);//同步条件
}

挑战:爆炸油桶的网络同步

1.修改必要的同步属性和视觉效果。运行结果:两端油桶爆炸和移动已同步

SExplosiveBarrel.h
    UPROPERTY(ReplicatedUsing = OnRep_Exploded)
    bool bExploded;

    UFUNCTION()
    void Onrep_Exploded();

    virtual void GetLifetimeReplicatedProps( TArray< class FLifetimeProperty > & OutLifetimeProps ) const override;
SExplosiveBarrel.cpp
ASExplosiveBarrel::ASExplosiveBarrel()
{
//...
    SetReplicates(true);
    SetReplicateMovement(true);
//...
}

void ASExplosiveBarrel::OnHealthChanged(USHealthComponent* OwningHealthComp, float Health, float HealthDelta,
    const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
    if (bExploded)return;
    if (Health<=0)
    {
        bExploded = true;
        MeshComp->AddImpulse(ExplosionImpulse * FVector::UpVector,NAME_None,true);//添加向上的力,通过组件自己的同步属性同步
        //UGameplayStatics::SpawnEmitterAtLocation(GetWorld(),ExplosionEffect,GetActorLocation());//生成爆炸特效
      //MeshComp->SetMaterial(0,ExplodedMaterial);//改变爆炸后的材质

        OnRep_Exploded();//将生成爆炸特效和修改材质放入同步函数并调用。
        RadialForceComp->FireImpulse();//半径力发射,如果是物体,可以通过勾选物体自己的移动同步属性来同步
    }
}

void ASExplosiveBarrel::OnRep_Exploded()
{
    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(),ExplosionEffect,GetActorLocation());//生成爆炸特效
    MeshComp->SetMaterial(0,ExplodedMaterial);//改变爆炸后的材质
}

void ASExplosiveBarrel::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(ASExplosiveBarrel,bExploded);
}
文章目录