多人游戏编程快速入门指南

用C++创建简单的多人游戏。

Choose your operating system:

Windows

macOS

Linux

前置主题

为了理解并使用本文中的内容,请确保您已掌握以下主题:

Preview.png

开发多人游戏的游戏进程需要在游戏的 Actor 中实现 复制 。还必须设计特定于 服务器 (充当游戏会话的主机)或 客户端 (代表连接到会话的玩家)的功能。本分步指南将介绍创建简单多人游戏进程的流程,包括以下内容:

  • 如何向基本Actor添加复制。

  • 如何利用网络游戏中的 移动组件

  • 如何向 变量 添加复制。

  • 如何在变量更改时使用 RepNotify

  • 如何在C++环境下使用 远程过程调用(RPC)

  • 如何检查Actor的 网络角色 ,以过滤在函数中执行的调用。

最终将形成第三人称游戏,玩家可以向对方投掷爆炸性投射物。我们的主要工作是创建投射物并向角色添加伤害响应。

在开始之前,强烈建议查看 客户端-服务器模型 网络概述 页面上的要点。作为本指南的比对点,可参见 第一人称射击游戏教程 的向游戏添加投射物章节,但其中未介绍复制概念。

1.基本设置

  1. 打开 编辑器 ,创建一个 新项目 。确保使用以下设置:

    • C++项目

    • 使用 第三人称模板

    • 包括 初学者内容包

    • 针对 主机和PC

    应用这些设置后,将项目命名为 ThirdPersonMP ,然后单击 创建(Create) 按钮继续。将创建项目的C++文件,且虚幻编辑器将自动打开 ThirdPersonExampleMap

  2. 单击此场景中站立的 ThirdPersonCharacter 删除 它,然后确保地图中存在两个 玩家出生点 。这些出生点会生成玩家,而非手动放置场景默认包括的ThirdPersonCharacter。

    单击图像以查看大图。

大多数模板中的Pawn和角色默认启用了复制。在我们的示例中,ThirdPersonCharacter已拥有会自动复制移动的 角色移动组件

角色移动组件

化妆组件,如角色的 骨架网格体 及其 动画蓝图 ,不会被复制。但与游戏进程和移动相关的变量(如角色的速度)则会被复制,且动画蓝图会在变量更新时读取这些变量。因此,角色在每个客户端上的副本都会更新其视觉呈现,只要游戏进程变量准确更新,这种更新就是一致的。同样, 游戏进程框架 自动处理角色在玩家出生点的生成操作,并向角色分配 玩家控制器

若使用此项目启动服务器,并有客户端加入该服务器,这就已经是一个正常的多人游戏。但玩家仅可让其游戏化身移动和跳跃。因此要创建一些其他的多人游戏进程。

2.使用RepNotify复制玩家的生命值

玩家需要生命值,才能在游戏进程受到伤害。该值需要复制,使所有客户端都拥有各玩家生命值的同步信息,并需要在玩家受到伤害时向其提供反馈。本节将演示如何在不依赖RPC的情况下,利用RepNotify同步变量的所有必要更新。

注意,'Role' 已经被相应替换为 'GetLocalRole()' 和 'GetRemoteRole()'。你会注意到下述小节中有些地方之前使用的是 'Role',请注意更改。

  1. 打开 ThirdPersonMPCharacter.h 。在 protected 下添加以下属性:

    ThirdPersonMPCharacter.h

    /** 玩家的最大生命值。这是玩家的最高生命值,也是出生时的生命值。*/
    UPROPERTY(EditDefaultsOnly, Category = "Health")
    float MaxHealth;
    
    /** 玩家的当前生命值。降到0就表示死亡。*/
    UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
    float CurrentHealth;
    
    /** RepNotify,用于同步对当前生命值所做的更改。*/
    UFUNCTION()
    void OnRep_CurrentHealth();

    我们要严格控制玩家生命值的变化,因此这些生命值有以下约束:

    • MaxHealth 不复制,仅可在默认值中编辑。此值是针对所有玩家预先计算得出的,不会更改。

    • CurrentHealth 复制,但无法在蓝图的任何地方编辑或访问。

    • MaxHealth CurrentHealth 都是 受保护 的,以防被外部C++类访问。仅可在 AThirdPersonMPCharacter 或其派生类中进行修改。

    这降低了实时游戏进程中玩家的 CurrentHealth MaxHealth 发生意外更改的风险。在稍后的步骤中,我们会提供其他公共函数,用于获取和修改这些值。

    Replicated 说明符在服务器上启用Actor的副本,以在变量值更改时,将该变量值复制到所有连接的客户端。 ReplicatedUsing 也有同样的功能,但还能设置 RepNotify 函数,此函数将在客户端成功接收复制数据时触发。将基于此变量的更改,使用 OnRep_CurrentHealth 执行各个客户端的更新。

  2. 打开 ThirdPersonMPCharacter.cpp 。在顶部的 #include "GameFramework/SpringArmComponent.h" 一行下添加以下 #include 语句:

    ThirdPersonMPCharacter.cpp

    #include "Net/UnrealNetwork.h"
    #include "Engine/Engine.h"

    它们提供用于复制变量以及访问 GEngine 中的 AddOnscreenDebugMessage 函数(用于将消息输出至屏幕)的必要功能。

  3. ThirdPersonMPCharacter.cpp 中,在构造函数底部添加以下代码:

    ThirdPersonMPCharacter.cpp

    //初始化玩家生命值
    MaxHealth = 100.0f;
    CurrentHealth = MaxHealth;

    这将初始化玩家的生命值。创建此角色的新副本时,角色当前生命值将设为其最大生命值。

  4. ThirdPersonMPCharacter.h 中,在 AThirdPersonMPCharacter 构造函数之后添加以下公共函数声明:

    ThirdPersonMPCharacter.h

    /** 属性复制 */
    void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
  5. ThirdPersonMPCharacter.cpp 中,为此函数添加以下实现:

    ThirdPersonMPCharacter.cpp

    //////////////////////////////////////////////////////////////////////////
    // 复制的属性
    
    void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty> & OutLifetimeProps) const
    {
        Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
        //复制当前生命值。
        DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
    }

    GetLifetimeReplicatedProps 函数负责复制我们使用 Replicated 说明符指派的任何属性,并可用于配置属性的复制方式。这里使用 CurrentHealth 的最基本实现。一旦添加更多需要复制的属性,也必须添加到此函数。

    必须调用 GetLifetimeReplicatedProps Super 版本,否则从Actor父类继承的属性不会复制,即便该父类指定要复制。

  6. ThirdPersonMPCharacter.h 中,在 Protected 下添加以下函数声明:

    ThirdPersonMPCharacter.h

    /** 响应要更新的生命值。修改后,立即在服务器上调用,并在客户端上调用以响应RepNotify*/
    void OnHealthUpdate();
  7. ThirdPersonMPCharacter.cpp 中,添加以下实现:

    ThirdPersonMPCharacter.cpp

    void AThirdPersonMPCharacter::OnHealthUpdate()
    {
        //客户端特定的功能
        if (IsLocallyControlled())
        {
            FString healthMessage = FString::Printf(TEXT("You now have %f health remaining."), CurrentHealth);
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
    
            if (CurrentHealth <= 0)
            {
                FString deathMessage = FString::Printf(TEXT("You have been killed."));
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage);
            }
        }
    
        //服务器特定的功能
        if (GetLocalRole() == ROLE_Authority)
        {
            FString healthMessage = FString::Printf(TEXT("%s now has %f health remaining."), *GetFName().ToString(), CurrentHealth);
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
        }
    
        //在所有机器上都执行的函数。 
        /*  
            因任何因伤害或死亡而产生的特殊功能都应放在这里。 
        */
    }

    我们将使用此函数执行更新,以响应玩家的 CurrentHealth 所发生的更改。目前它的功能仅限于提供屏幕调试消息,但后续可添加更多功能,例如在所有机器上调用的OnDeath函数,用于触发死亡动画。注意, OnHealthUpdate 不复制,需要在所有设备上手动调用。

  8. ThirdPersonMPCharacter.cpp 中,为 OnRep_CurrentHealth 添加以下实现:

    ThirdPersonMPCharacter.cpp

    void AThirdPersonMPCharacter::OnRep_CurrentHealth()
    {
        OnHealthUpdate();
    }

    变量在其值发生更改时复制,而非持续不断地复制,RepNotify在客户端成功收到变量的复制值时运行。因此,只要在服务器上更改玩家的 CurrentHealth OnRep_CurrentHealth 就会在所有连接的客户端上运行。这就使 OnRep_CurrentHealth 成为在客户端机器上调用 OnHealthUpdate 的最佳场所。

3.使玩家响应伤害

现在我们已实现玩家的生命值,接下来需要想办法在此类之外修改玩家生命值。

  1. ThirdPersonMPCharacter.h 中,在 Public 下添加以下函数声明:

    ThirdPersonMPCharacter.h

    /** 最大生命值的取值函数。*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetMaxHealth() const { return MaxHealth; } 
    
    /** 当前生命值的取值函数。*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }
    
    /** 当前生命值的存值函数。将此值的范围限定在0到MaxHealth之间,并调用OnHealthUpdate。仅在服务器上调用。*/
    UFUNCTION(BlueprintCallable, Category="Health")
    void SetCurrentHealth(float healthValue);
    
    /** 承受伤害的事件。从APawn覆盖。*/
    UFUNCTION(BlueprintCallable, Category = "Health")
    float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;

    GetMaxHealth GetCurrentHealth 函数取值函数,可在C++环境和蓝图中从 AThirdPersonMPCharacter 外部访问玩家生命值。它们作为 const 函数,能安全地获取这些值,而且不允许修改。我们还声明了用于设置玩家生命值和承受伤害的函数。

  2. ThirdPersonMPCharacter.cpp 中,为 SetCurrentHealth 添加以下实现:

    ThirdPersonMPCharacter.cpp

    void AThirdPersonMPCharacter::SetCurrentHealth(float healthValue)
    {
        if (GetLocalRole() == ROLE_Authority)
        {
            CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
            OnHealthUpdate();
        }
    }

    SetCurrentHealth 提供了一种可控的办法从 AThirdPersonMPCharacter 外部修改玩家 CurrentHealth 。此函数并非复制而来,但是通过检查确认Actor的网络角色为 ROLE_Authority ,我们限制仅在托管游戏的服务器上调用此函数时,此函数才会执行。它将 CurrentHealth 值的范围限定于0到玩家 MaxHealth 之间,因此无法将 CurrentHealth 设置为无效值,并且它调用 OnHealthUpdate 以确保服务器和客户端都有对此函数的并行调用。由于服务器不会收到RepNotify,因此此功能是必要的。

    虽然并非所有变量都需要这样的"存值"函数,但对于游戏期间会频繁变化的敏感游戏进程变量,尤其是可被很多不同源修改的变量,这种函数很有用。对于类似的单机游戏和多人游戏,使用存值函数都是最佳做法,因为它使这些变量的实时更改更加一致,更便于调试,也更容易扩展新功能。

  3. ThirdPersonMPCharacter.cpp 中,为 TakeDamage 添加以下实现:

    ThirdPersonMPCharacter.cpp

    float AThirdPersonMPCharacter::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
    {
        float damageApplied = CurrentHealth - DamageTaken;
        SetCurrentHealth(damageApplied);
        return damageApplied;
    }

    这些内置函数用于对Actor施加伤害,会调用该Actor的 TakeDamage 基本函数。本例中使用 SetCurrentHealth 实现简单的生命值扣减。

若已学到本节此处,那么以下应是对Actor施加伤害的流程:

  • 外部Actor或函数对角色调用 CauseDamage ,而角色又调用其 TakeDamage 函数。

  • TakeDamage 调用 SetCurrentHealth 以在服务器上更改玩家的当前生命值。

  • SetCurrentHealth 在服务器上调用 OnHealthUpdate ,导致执行功能,响应玩家生命值的更改。

  • CurrentHealth 复制到所有已连接的客户端的角色副本。

  • 各个客户端从服务器收到 CurrentHealth 的新值时,会调用 OnRep_CurrentHealth

  • OnRep_CurrentHealth 调用 OnHealthUpdate ,确保各个客户端以相同方式响应 CurrentHealth 的新值。

此实现有两个优势。首先,它浓缩了围绕两大关键函数 SetCurrentHealth OnHealthUpdate 添加的新功能的工作流程,这两个函数会使将来的代码维护和扩展工作变得更加容易。其次,由于此实现不使用任何服务器、客户端或NetMulticast RPC,它浓缩了在整个网络上发送的信息量,仅依靠复制 CurrentHealth 以触发所有必要更改。由于无论实现了任何其他函数, CurrentHealth 都需要复制,因此这是复制生命值更改的最有效模型。

4.使用复制创建投射物

  1. 在虚幻编辑器中,使用 文件(File) 菜单或 内容浏览器 创建 新C++类

    Create New Class

  2. 选择父类(Choose Parent Class) 菜单中,选择 Actor 作为父类,并单击 下一步(Next)

    单击图像以查看大图。

  3. 命名新Actor(Name Your New Actor) 菜单,将类命名为 ThirdPersonMPProjectile ,然后单击 创建类(Create Class)

    单击图像以查看大图。

  4. 打开 ThirdPersonMPProjectile.h ,并将以下代码添加到类定义中的 public 下:

    ThirdPersonMPProjectile.h

    // 用于测试碰撞的球体组件。
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class USphereComponent* SphereComponent;
    
    // 用于提供对象视觉呈现效果的静态网格体。
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class UStaticMeshComponent* StaticMesh;
    
    // 用于处理投射物移动的移动组件。
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class UProjectileMovementComponent* ProjectileMovementComponent;
    
    // 在投射物撞击其他对象并爆炸时使用的粒子。
    UPROPERTY(EditAnywhere, Category = "Effects")
    class UParticleSystem* ExplosionEffect;
    
    //此投射物将造成的伤害类型和伤害。
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
    TSubclassOf<class UDamageType> DamageType;
    
    //此投射物造成的伤害。
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
    float Damage;

    这些声明中的各个类型前面都冠以 class 关键字。这样,这些声明除了是变量声明之外,还是各自类的前向声明,从而确保各自的类会在头文件中被识别。下一个步骤中,我们会在CPP文件中为它们添加 #include

    我们正在声明的属性将提供以下项目:

    • 静态网格体组件 ,作为投射物的视觉呈现。

    • 球体组件 ,用于检查碰撞。

    • 投射物移动组件 ,用于移动投射物。

    • 粒子系统 ,引用我们要使用的内容,在后续步骤中生成爆炸效果。

    • 伤害类型 ,用于伤害事件。

    • 伤害 的浮点值,表示角色被此投射物击中时应扣减的生命值。

    但以上各项都尚未定义。

    bReplicates 设为 True

  5. 打开 ThirdPersonMPProjectile.cpp ,将以下代码添加到文件顶部的 #include 语句中的 #include "ThirdPersonMPProjectile.h" 一行下:

    ThirdPersonMPProjectile.cpp

    #include "Components/SphereComponent.h"
    #include "Components/StaticMeshComponent.h"
    #include "GameFramework/ProjectileMovementComponent.h"
    #include "GameFramework/DamageType.h"
    #include "Particles/ParticleSystem.h"
    #include "Kismet/GameplayStatics.h"
    #include "UObject/ConstructorHelpers.h"

    我们将在本演示中使用这些语句。前四个是我们使用的组件,而 GamePlayStatics.h 可用于访问基本游戏进程函数, ConstructorHelpers.h 可用于访问一些有用的构造函数以便设置组件。

  6. 将以下代码添加到 ThirdPersonMPProjectile.cpp 中的构造函数中:

    ThirdPersonMPProjectile.cpp

    bReplicates = true;

    bReplicates 变量告知游戏此Actor应复制。Actor默认仅存在于生成它的机器上。当 bReplicates 设为 True ,只要Actor的权威副本存在于服务器上,就会尝试将该Actor复制到所有已连接的客户端。

  7. 将以下代码添加到 AThirdPersonMPProjectile 的构造函数中:

    ThirdPersonMPProjectile.cpp

    //定义将作为投射物及其碰撞的根组件的SphereComponent。
    SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
    SphereComponent->InitSphereRadius(37.5f);
    SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
    RootComponent = SphereComponent;

    这会在构造对象时定义SphereComponent,使投射物碰撞。

  8. 在构造函数内添加以下代码:

    ThirdPersonMPProjectile.cpp

    //定义将作为视觉呈现的网格体。
    static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    StaticMesh->SetupAttachment(RootComponent);
    
    //若成功找到要使用的静态网格体资产,则设置该静态网格体及其位置/比例。
    if (DefaultMesh.Succeeded())
    {
        StaticMesh->SetStaticMesh(DefaultMesh.Object);
        StaticMesh->SetRelativeLocation(FVector(0.0f, 0.0f, -37.5f));
        StaticMesh->SetRelativeScale3D(FVector(0.75f, 0.75f, 0.75f));
    }

    这将定义要作为视觉呈现的StaticMeshComponent。将自动尝试在 初学者内容包 中查找 Shape_Sphere 网格体,并自行填充。球体也将调整尺寸,与SphereComponent尺寸一致。

  9. 在构造函数内添加以下代码:

    ThirdPersonMPProjectile.cpp

    static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));
    if (DefaultExplosionEffect.Succeeded())
    {
        ExplosionEffect = DefaultExplosionEffect.Object;
    }

    This will set the asset reference for our ExplosionEffect to be the P_Explosion asset inside of StarterContent.

  10. 在构造函数内添加以下代码:

    ThirdPersonMPProjectile.cpp

    //定义投射物移动组件。
    ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
    ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
    ProjectileMovementComponent->InitialSpeed = 1500.0f;
    ProjectileMovementComponent->MaxSpeed = 1500.0f;
    ProjectileMovementComponent->bRotationFollowsVelocity = true;
    ProjectileMovementComponent->ProjectileGravityScale = 0.0f;

    这将定义投射物的投射物移动组件。该组件会复制,在服务器上执行的任何移动都将复制到客户端上。

  11. 在构造函数内添加以下代码:

    ThirdPersonMPProjectile.cpp

    DamageType = UDamageType::StaticClass();
    Damage = 10.0f;

    这些代码会初始化投射物将对Actor造成的伤害量以及将在伤害事件中使用的伤害类型。本例中由于尚未定义任何新伤害类型,因此使用基本 UDamageType 进行初始化。

5.使投射物造成伤害

若已按本指南学到此处,应可在服务器上生成投射物,且将在所有客户端上显示并移动。但若撞到墙壁或阻挡物,就会停止。我们需要它对玩家造成伤害,并需要对会话中所有已连接的客户端显示爆炸效果。

  1. ThirdPersonMPProjectile.h 中,在 Protected 下添加以下代码:

    ThirdPersonMPProjectile.h

    virtual void Destroyed() override;
  2. ThirdPersonMPProjectile.cpp 中,为此函数添加以下实现:

    ThirdPersonMPProjectile.cpp

    void AThirdPersonMPProjectile::Destroyed()
    {
        FVector spawnLocation = GetActorLocation();
        UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
    }

    每当有Actor被摧毁时,就会调用 Destroyed 函数。粒子发射器自身通常不复制,但由于Actor摧毁会复制,我们知道若在服务器上摧毁此投射物,则各个连接客户端在摧毁各自的投射物副本时将调用此函数。结果,所有玩家都会看到投射物被摧毁时的爆炸效果。

  3. ThirdPersonMPProjectile.h 中,在 Protected 下添加以下代码:

    ThirdPersonMPProjectile.h

    UFUNCTION(Category="Projectile")
    void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
  4. ThirdPersonMPProjectile.cpp 中,为此函数添加以下实现:

    ThirdPersonMPProjectile.cpp

    void AThirdPersonMPProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
    {   
        if ( OtherActor )
        {
            UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, GetInstigator()->Controller, this, DamageType);
        }
    
        Destroy();
    }

    这是在投射物撞击对象时要调用的函数。若撞击对象是有效Actor,将调用 ApplyPointDamage 函数,在碰撞处对该对象造成伤害。同时,无论撞击表面是什么,任何碰撞都将摧毁该Actor,导致爆炸效果显示。

  5. ThirdPersonMPProjectile.cpp 中,将以下代码添加到构造函数中的 RootComponent = SphereComponent 一行下:

    ThirdPersonMPProjectile.h

    //在击中事件上注册此投射物撞击函数。
    if (GetLocalRole() == ROLE_Authority)
    {
        SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact);
    }

    这将在球体组件上向 OnComponentHit 事件注册 OnProjectileImpact ,该球体组件作为投射物的主碰撞组件。为了专门确保仅有服务器运行此游戏进程逻辑,注册 OnProjectileImpact 之前检查确认 GetLocalRole() == ROLE_Authority

6.发射投射物

  1. 打开 编辑器(Editor) ,然后单击屏幕顶部的 编辑(Edit) 下拉菜单,并打开 项目设置(Project Settings)

    Project Settings

  2. 引擎(Engine) 部分中,单击 输入(Input) 打开项目的输入设置。展开 绑定(Bindings) 部分,添加新条目。将它命名为" Fire ",并选择 鼠标左键 作为此Actor的绑定键。

    单击图像以查看大图。

  3. ThirdPersonMPCharacter.cpp 中,在 #include "Engine/Engine.h" 一行下方添加以下 #include

    ThirdPersonMPCharacter.cpp

    #include "ThirdPersonMPProjectile.h"

    这使角色类能够识别投射物类型并生成投射物。

  4. ThirdPersonMPCharacter.h 中,在 Protected 下添加以下代码:

    ThirdPersonMPCharacter.h

    UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
    TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;
    
    /** 射击之间的延迟,单位为秒。用于控制测试发射物的射击速度,还可防止服务器函数的溢出导致将SpawnProjectile直接绑定至输入。*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay")
    float FireRate;
    
    /** 若为true,则正在发射投射物。*/
    bool bIsFiringWeapon;
    
    /** 用于启动武器射击的函数。*/
    UFUNCTION(BlueprintCallable, Category="Gameplay")
    void StartFire();
    
    /** 用于结束武器射击的函数。一旦调用这段代码,玩家可再次使用StartFire。*/
    UFUNCTION(BlueprintCallable, Category = "Gameplay")
    void StopFire();  
    
    /** 用于生成投射物的服务器函数。*/
    UFUNCTION(Server, Reliable)
    void HandleFire();
    
    /** 定时器句柄,用于提供生成间隔时间内的射速延迟。*/
    FTimerHandle FiringTimer;

    这些是将用于发射投射物的变量和函数。 HandleFire 是将在本教程中实现的唯一RPC,它将负责在服务器上生成投射物。因为它拥有 Server 说明符,如若尝试在客户端上调用它,都会导致该调用通过网络直接被定向到服务器上的权威角色。

    因为 HandleFire 也拥有 Reliable 说明符,因此一旦调用,它就会被放置到可靠RPC的队列中,并在服务器成功收到之后从队列中移除。这保证服务器一定会收到此函数调用。但若可靠RPC的队列中一次性放入太多RPC却不移除它们,则该队列可能会溢出,如果是这种情况,用户将被强制断开连接。因此,我们主要注意允许玩家调用此函数的频率。

    1. ThirdPersonMPCharacter.cpp 中,在构造函数底部添加以下代码:

    ThirdPersonMPCharacter.cpp

    //初始化投射物类
    ProjectileClass = AThirdPersonMPProjectile::StaticClass();
    //初始化射速
    FireRate = 0.25f;
    bIsFiringWeapon = false;

    这些代码将初始化处理投射物发射所需的变量。

    1. ThirdPersonMPCharacter.cpp 中,添加以下实现:

    ThirdPersonMPCharacter.cpp

    void AThirdPersonMPCharacter::StartFire()
    {
        if (!bIsFiringWeapon)
        {
            bIsFiringWeapon = true;
            UWorld* World = GetWorld();
            World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false);
            HandleFire();
        }
    }
    
    void AThirdPersonMPCharacter::StopFire()
    {
        bIsFiringWeapon = false;
    }
    
    void AThirdPersonMPCharacter::HandleFire_Implementation()
    {
        FVector spawnLocation = GetActorLocation() + ( GetControlRotation().Vector()  * 100.0f ) + (GetActorUpVector() * 50.0f);
        FRotator spawnRotation = GetControlRotation();
    
        FActorSpawnParameters spawnParameters;
        spawnParameters.Instigator = GetInstigator();
        spawnParameters.Owner = this;
    
        AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters);
    }

    StartFire 是玩家在本地机器上调用的函数,用于初始化发射流程,它基于以下条件限制用户调用 HandleFire 的频率:

    • 若用户正在发射投射物,则不可发射。这是用 bFiringWeapon 指派的,在调用 StartFire 时, bFiringWeapon 设为 true

    • 调用 StopFire 时, bFiringWeapon 仅可设为 false

    • 时长为 FireRate 的定时器结束时,会调用 StopFire

    这意味着用户发射投射物时,必须等待数秒(等于 FireRate ),之后方可继续发射。无论 StartFire 绑定到何种输入,这种情况始终一致。例如,若用户将"Fire"命令绑定到滚轮或类似的不当输入,或用户反复狂按按钮,此函数仍会按可接受的时间间隔执行,不会使 HandleFire 调用导致用户的可靠函数队列溢出。

    因为 HandleFire 是服务器RPC,其在CPP文件中的实现必须在函数名前面添加前缀 _Implementation 。本指南中的实施使用角色的控制旋转获取摄像机的朝向,然后生成面朝该方向的投射物,以便玩家瞄准。接下来,投射物的投射物移动组件使其朝该方向移动。

  5. ThirdPersonMPCharacter.cpp 中,在函数 SetupPlayerInputComponent 底部添加以下代码:

    ThirdPersonMPCharacter.cpp

    // 处理发射投射物
    PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);

    这段代码将 StartFire 绑定到本节第一个步骤中创建的 Fire 输入操作,以便用户将其激活。

7.测试游戏

  1. 在编辑器中打开项目。单击 编辑(Edit) 下拉菜单,并打开 编辑器首选项(Editor Preferences)

    Open Level Editor/Play

  2. 导航至 关卡编辑器(Level Editor) 部分,并单击 运行(Play) 菜单。找到 多人游戏选项(Multiplayer Options) ,并将 玩家数量(Number of Players) 更改为2。

    单击图像以查看大图。

  3. 运行(Play) 按钮。 在编辑器中运行(Play in Editor)(PIE) 主窗口将作为服务器启动多人游戏会话,之后第二个PIE窗口打开,作为客户端连接。

最终结果

单击图像以查看大图。

游戏中的两位玩家都应能够看到对方移动,并可向对方发射自定义投射物。若其中一位玩家被自定义投射物击中,应向两位玩家同时显示爆炸粒子,且被击中的玩家将收到一条"命中"消息,告知伤害量和当前生命值,而会话中的所有其他玩家不会看到任何消息。若玩家生命值降至0,会看到一条消息,通知已被杀死。

本演练到此完成,你应已掌握在C++环境下构建多人游戏功能的一些基础知识,包括变量和组件复制的概述、网络角色的使用方式,以及使用RPC的适当时机。了解这些信息后,应可在虚幻的服务器-客户端模型中构建自己的多人游戏 。

看你的了

要继续扩展网络多人游戏编程方面的技能,尝试完成以下项目:

  • 展开投射物的OnHit功能,以在投射物击中目标时创建额外的效果,例如创建球形轨迹以模拟爆炸半径。

  • 扩展ThirdPersonMPProjectile并对其ProjectileMovement组件进行试验,以创建采取不同行为的新变体。

  • 扩展ThirdPersonMPCharacter中的TakeDamage函数,以杀死玩家的pawn并使它们重新生成。

  • 向本地PlayerController添加HUD,使其显示复制的信息或响应客户端函数。

  • 使用DamageType在玩家被杀死时创建个性化消息。

  • 探索游戏模式、玩家状态和游戏状态的用途,以创建一套完整的规则来利用玩家统计数据和记分板主持比赛。

代码示例

ThirdPersonMPProjectile.h

// 版权所有 1998-2019 Epic Games, Inc。保留所有权利。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ThirdPersonMPProjectile.generated.h"

class UParticleSystem;
class UStaticMeshComponent;
class USphereComponent;
class UProjectileMovementComponent;
class UDamageType;

UCLASS()
class THIRDPERSONMP_API AThirdPersonMPProjectile : public AActor
{
    GENERATED_BODY()

public: 
    // 为此Actor的属性设置默认值
    AThirdPersonMPProjectile();

    // 此投射物的基本组件
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    USphereComponent* SphereComponent;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    UStaticMeshComponent* StaticMesh;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    UProjectileMovementComponent* ProjectileMovementComponent;
    UPROPERTY(EditAnywhere, Category = "Effects")
    UParticleSystem* ExplosionEffect;

    //此投射物将造成的伤害类型和伤害。
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
    TSubclassOf<UDamageType> DamageType;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
    float Damage;

protected:
    // 当游戏开始或生成时调用
    virtual void BeginPlay() override;

    UFUNCTION()
    void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

    void Destroyed() override;

public: 
    // 每一帧调用
    virtual void Tick(float DeltaTime) override;
};

ThirdPersonMPProjectile.cpp

// 版权所有 1998-2019 Epic Games, Inc。保留所有权利。

#include "ThirdPersonMPProjectile.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "GameFramework/DamageType.h"
#include "Particles/ParticleSystem.h"
#include "Kismet/GameplayStatics.h"
#include "UObject/ConstructorHelpers.h"
#include "IDamageInterface.h"

// 设置默认值
AThirdPersonMPProjectile::AThirdPersonMPProjectile()
{
    // 将此Actor设置为每一帧调用Tick()。如果不需要,可以关闭此选项来提高性能。
    PrimaryActorTick.bCanEverTick = true;

    bReplicates = true;

    DamageType = UDamageType::StaticClass();
    Damage = 10.0f;

    static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));
    if (DefaultExplosionEffect.Succeeded())
    {
        ExplosionEffect = DefaultExplosionEffect.Object;
    }

    //定义将作为投射物及其碰撞的根组件的SphereComponent。
    SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
    SphereComponent->InitSphereRadius(12.5f);
    SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
    RootComponent = SphereComponent;

    //在击中事件上注册此投射物撞击函数。
    if (GetLocalRole() == ROLE_Authority)
    {
        SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact);
    }

    //定义将作为视觉呈现的网格体。
    static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    StaticMesh->SetupAttachment(RootComponent);

    if (DefaultMesh.Succeeded())
    {
        StaticMesh->SetStaticMesh(DefaultMesh.Object);
        StaticMesh->RelativeLocation = FVector(0.0f, 0.0f, -12.5f);
        StaticMesh->RelativeScale3D = FVector(0.25f, 0.25f, 0.25f);
    }

    //定义投射物移动组件。
    ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
    ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
    ProjectileMovementComponent->InitialSpeed = 1500.0f;
    ProjectileMovementComponent->MaxSpeed = 1500.0f;
    ProjectileMovementComponent->bRotationFollowsVelocity = true;
    ProjectileMovementComponent->ProjectileGravityScale = 0.0f;
}

// 当游戏开始或生成时调用
void AThirdPersonMPProjectile::BeginPlay()
{
    Super::BeginPlay();

}

void AThirdPersonMPProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{   
    if ( OtherActor )
    {
        UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, Instigator->Controller, this, DamageType);
    }

    Destroy();
}

void AThirdPersonMPProjectile::Destroyed()
{
    FVector spawnLocation = GetActorLocation();
    UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}

// 每一帧调用
void AThirdPersonMPProjectile::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

ThirdPersonMPCharacter.h

// 版权所有 1998-2019 Epic Games, Inc。保留所有权利。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "IDamageInterface.h"
#include "ThirdPersonMPCharacter.generated.h"

UCLASS(config=Game)
class AThirdPersonMPCharacter : public ACharacter 
{
    GENERATED_BODY()

    /** 摄像机吊杆将摄像机置于角色身后 */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class USpringArmComponent* CameraBoom;

    /** 跟随摄像机 */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UCameraComponent* FollowCamera;
public:

    /** 构造函数 */
    AThirdPersonMPCharacter();

    /** 属性复制 */
    void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    /** 基础旋转速度,单位为度/秒。其他缩放比例可能会影响最终旋转速度。*/
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseTurnRate;

    /** 基础向上/下看速度,单位为度/秒。其他缩放比例可能会影响最终速度。*/
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseLookUpRate;

protected:
    /** 玩家的最大生命值。这是玩家的最高生命值,也是出生时的生命值。*/
    UPROPERTY(EditDefaultsOnly, Category = "Health")
    float MaxHealth;

    /** 玩家的当前生命值。降到0就表示死亡。*/
    UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
    float CurrentHealth;

    /** RepNotify,用于同步对当前生命值所做的更改。*/
    UFUNCTION()
    void OnRep_CurrentHealth();

    /** 响应要更新的生命值。修改后,立即在服务器上调用,并在客户端上调用以响应RepNotify*/
    void OnHealthUpdate();

public:
    /** 最大生命值的取值函数。*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetMaxHealth() const { return MaxHealth; } 

    /** 当前生命值的取值函数。*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }

    /** 当前生命值的存值函数。将此值的范围限定在0到MaxHealth之间,并调用OnHealthUpdate。仅在服务器上调用。*/
    UFUNCTION(BlueprintCallable, Category="Health")
    void SetCurrentHealth(float healthValue);

    /** 承受伤害的事件。从APawn覆盖。*/
    UFUNCTION(BlueprintCallable, Category = "Health")
    float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;

    protected:
    /** 在VR中重置HMD方向。*/
    void OnResetVR();

    /** 调用用于向前/向后输入 */
    void MoveForward(float Value);

    /** 调用用于侧向输入 */
    void MoveRight(float Value);

    /** 
     * 通过输入调用,以给定速度旋转。 
     * @param Rate  这是标准化速度,即1.0表示100%的所需旋转速度
     */
    void TurnAtRate(float Rate);

    /**
     * 通过输入调用,以给定速度向上/下看。 
     * @param Rate  这是标准化速度,即1.0表示100%的所需旋转速度
     */
    void LookUpAtRate(float Rate);

    /** 触控输入开始时使用的处理程序。*/
    void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);

    /** 触控输入停止时使用的处理程序。*/
    void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);

protected:
    // APawn界面
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    // APawn界面结束

    /** 角色要发射的发射物类型。*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
    TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;

    /** 射击之间的延迟,单位为秒。用于控制测试发射物的射击速度,还可防止服务器函数的溢出导致将SpawnProjectile直接绑定至输入。*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay")
    float FireRate;

    /** 若为true,此武器正在发射过程中。*/
    bool bIsFiringWeapon;

    /** 用于启动武器发射的函数。应仅可由本地玩家触发。*/
    UFUNCTION(BlueprintCallable, Category="Gameplay")
    void StartFire();

    /** 用于结束武器射击的函数。一旦调用这段代码,玩家可再次使用StartFire。*/
    UFUNCTION(BlueprintCallable, Category = "Gameplay")
    void StopFire();  

    /** 用于生成投射物的服务器函数。*/
    UFUNCTION(Server, Reliable)
    void HandleFire();

    /** 定时器句柄,用于提供生成间隔时间内的射速延迟。*/
    FTimerHandle FiringTimer;

public:
    /** 返回CameraBoom子对象 **/
    FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
    /** 返回FollowCamera子对象 **/
    FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

ThirdPersonMPCharacter.cpp

// 版权所有 1998-2019 Epic Games, Inc。保留所有权利。

#include "ThirdPersonMPCharacter.h"
#include "HeadMountedDisplayFunctionLibrary.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
#include "ThirdPersonMPProjectile.h"
#include "TimerManager.h"

//////////////////////////////////////////////////////////////////////////
// AThirdPersonMPCharacter

AThirdPersonMPCharacter::AThirdPersonMPCharacter()
{
    // 设置碰撞胶囊体的大小
    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

    // 设置输入的旋转速度
    BaseTurnRate = 45.f;
    BaseLookUpRate = 45.f;

    // 控制器旋转时不旋转。只影响摄像机。
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // 配置角色移动
    GetCharacterMovement()->bOrientRotationToMovement = true; // 角色朝输入的方向移动...  
    GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...采用此旋转速度
    GetCharacterMovement()->JumpZVelocity = 600.f;
    GetCharacterMovement()->AirControl = 0.2f;

    // 创建摄像机吊杆(发生碰撞时向玩家拉近)
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent);
    CameraBoom->TargetArmLength = 300.0f; // 摄像机以这个距离跟在角色身后 
    CameraBoom->bUsePawnControlRotation = true; // 基于控制器旋转吊臂

    // 创建跟随摄像头
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // 将摄像机连接到吊杆末端,调节吊杆以匹配控制器方向
    FollowCamera->bUsePawnControlRotation = false; // 摄像机不相对于吊臂旋转

    // 注意:骨架网格体和网格体组件上的动画蓝图引用(继承自角色) 
    // 都在名为MyCharacter的派生蓝图资产中设置(以避免C++环境下的直接内容引用)

    //初始化玩家生命值
    MaxHealth = 100.0f;
    CurrentHealth = MaxHealth;

    //初始化投射物类
    ProjectileClass = AThirdPersonMPProjectile::StaticClass();
    //初始化射速
    FireRate = 0.25f;
    bIsFiringWeapon = false;
}

//////////////////////////////////////////////////////////////////////////
// 输入

void AThirdPersonMPCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
    // 设置游戏进程键绑定
    check(PlayerInputComponent);
    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

    PlayerInputComponent->BindAxis("MoveForward", this, &AThirdPersonMPCharacter::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AThirdPersonMPCharacter::MoveRight);

    // 我们有两个旋转绑定版本,可以用不同的方式处理不同类型的设备
    // "turn"处理提供绝对增量的设备。
    // "turnrate"用于选择视为变化速度的设备,例如模拟操纵杆
    PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
    PlayerInputComponent->BindAxis("TurnRate", this, &AThirdPersonMPCharacter::TurnAtRate);
    PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
    PlayerInputComponent->BindAxis("LookUpRate", this, &AThirdPersonMPCharacter::LookUpAtRate);

    // 处理触控设备
    PlayerInputComponent->BindTouch(IE_Pressed, this, &AThirdPersonMPCharacter::TouchStarted);
    PlayerInputComponent->BindTouch(IE_Released, this, &AThirdPersonMPCharacter::TouchStopped);

    // VR头戴设备功能
    PlayerInputComponent->BindAction("ResetVR", IE_Pressed, this, &AThirdPersonMPCharacter::OnResetVR);

    // 处理发射投射物
    PlayerInputComponent->BindAction( "Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
    //PlayerInputComponent->BindAction("Fire", IE_Released, this, &AThirdPersonMPCharacter::StopFire);
}
//////////////////////////////////////////////////////////////////////////
// 复制的属性

void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty> & OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
}

void AThirdPersonMPCharacter::StartFire()
{
    if (!bIsFiringWeapon)
    {
        bIsFiringWeapon = true;
        UWorld* World = GetWorld();
        World->GetTimerManager().SetTimer(FiringTimer, this, &AThirdPersonMPCharacter::StopFire, FireRate, false);
        SpawnProjectile();
    }
}

void AThirdPersonMPCharacter::StopFire()
{
    bIsFiringWeapon = false;
}

void AThirdPersonMPCharacter::SpawnProjectile_Implementation()
{
    FVector spawnLocation = GetActorLocation() + ( GetActorRotation().Vector()  * 100.0f ) + (GetActorUpVector() * 50.0f);
    FRotator spawnRotation = GetActorRotation();

    FActorSpawnParameters spawnParameters;
    spawnParameters.Instigator = Instigator;
    spawnParameters.Owner = this;

    AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(ProjectileClass, spawnLocation, spawnRotation, spawnParameters);
}

void AThirdPersonMPCharacter::OnHealthUpdate()
{
    //客户端特定的功能
    if (IsLocallyControlled())
    {
        FString healthMessage = FString::Printf(TEXT("You now have %f health remaining."), CurrentHealth);
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);

        if (CurrentHealth <= 0)
        {
            FString deathMessage = FString::Printf(TEXT("You have been killed."));
            GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, deathMessage);
        }
    }

    //服务器特定的功能
    if (GetLocalRole() == ROLE_Authority)
    {
        FString healthMessage = FString::Printf(TEXT("%s now has %f health remaining."), *GetFName().ToString(), CurrentHealth);
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, healthMessage);
    }

    //在所有机器上都执行的函数。 
    /*  
        因任何因伤害或死亡而产生的特殊功能都应放在这里。 
    */
}

void AThirdPersonMPCharacter::OnRep_CurrentHealth()
{
    OnHealthUpdate();
}

void AThirdPersonMPCharacter::SetCurrentHealth(float healthValue)
{
    if (GetLocalRole() == ROLE_Authority)
    {
        CurrentHealth = FMath::Clamp(healthValue, 0.f, MaxHealth);
        OnHealthUpdate();
    }
}

float AThirdPersonMPCharacter::TakeDamage(float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    float damageApplied = CurrentHealth - DamageTaken;
    SetCurrentHealth(damageApplied);
    return damageApplied;
}

void AThirdPersonMPCharacter::OnResetVR()
{
    UHeadMountedDisplayFunctionLibrary::ResetOrientationAndPosition();
}

void AThirdPersonMPCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
    Jump();
}

void AThirdPersonMPCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
        StopJumping();
}

void AThirdPersonMPCharacter::TurnAtRate(float Rate)
{
    // 根据速度信息计算此帧的增量
    AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}

void AThirdPersonMPCharacter::LookUpAtRate(float Rate)
{
    // 根据速度信息计算此帧的增量
    AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}

void AThirdPersonMPCharacter::MoveForward(float Value)
{
    if ((Controller != NULL) && (Value != 0.0f))
    {
        // find out which way is forward
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // 获取前向矢量
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(Direction, Value);
    }
}

void AThirdPersonMPCharacter::MoveRight(float Value)
{
    if ( (Controller != NULL) && (Value != 0.0f) )
    {
        // 找出正确的道路
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // 获取正确的矢量 
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        // add movement in that direction
        AddMovementInput(Direction, Value);
    }
}
欢迎帮助改进虚幻引擎文档!请告诉我们该如何更好地为您服务。
填写问卷调查
取消