マルチプレイヤー プログラミングのクイック スタート ガイド

C++ でシンプルなマルチプレイヤー ゲームを作成します。

Choose your operating system:

Windows

macOS

Linux

前提トピック

このページは以下のトピックへの知識があることを前提にしています。まず以下のトピックの内容についてご確認をお願いします。

Preview.png

マルチプレイヤー ゲームのゲームプレイを開発するには、ゲームの アクタ レプリケーション を実装する必要があります。また、ゲーム セッションのホストとして機能する サーバー 、またはセッションに接続するプレイヤーを表す クライアント に固有の機能を設計する必要があります。このページでは、いくつかのシンプルなマルチプレイヤー ゲームプレイを作成する方法をプロセスごとに説明します。以下の内容を学習することができます。

  • ベース アクタにレプリケーションを追加する方法。

  • ネットワーク ゲームで Movement コンポーネント を活用する方法。

  • 変数 にレプリケーションを追加する方法。

  • 変数の変更時に RepNotifies を使用する方法。

  • C++ で リモート プロシージャ コール (RPC) を使用する方法。

  • 関数内で実行される呼び出しをフィルタリングするために、アクタの ネットワーク ロール を確認する方法。

最終結果として、プレイヤー同士が爆発物を投げ合うことのできるサードパーソン ゲームが仕上がります。ここで行う作業の大部分は、発射物を作成する作業と、キャラクターにダメージに対する反応を追加する作業です。

作業を開始する前に、「 クライアント サーバー モデル 」ページと「 ネットワーキングの概要 」ページで、基本事項を確認することを強くお勧めします。このガイドの比較参照のためのドキュメントとして、レプリケーションの概念を取り入れていない、「 ファースト パーソン シューティング チュートリアル 」の「ゲームに発射物を追加する」セクションを参照してください。

1. 基本設定

  1. エディタ を開いて 新規プロジェクト を作成します。新規プロジェクトが、次のように設定されていることを確認します。

    • C++ プロジェクト である

    • サードパーソン テンプレート を使用している

    • スターター コンテンツ が含まれている

    • コンソールおよび PC をターゲットにしている

    これらの設定を適用したら、プロジェクトに「 ThirdPersonMP 」という名前を付けて、 [Create (作成)] ボタンをクリックして続行します。プロジェクトの C++ ファイルが作成され、Unreal Editor により「 ThirdPersonExampleMap 」が自動的に開かれます。

  2. このシーンで立っている ThirdPersonCharacter をクリックして 削除 し、2 つの Player Start がマップ内にあることを確認します。これらは、デフォルトでシーンに含まれる手動で配置された ThirdPersonCharacter の代わりに、プレイヤーのスポーンを処理します。

    クリックしてフルサイズ表示。

ほとんどのテンプレート内にあるポーンとキャラクターは、デフォルトでレプリケーションが有効になっています。この例では、ThirdPersonCharacter には、自動的に動きをレプリケートする Character Movement コンポーネント がすでに含まれています。

Character Movement コンポーネント

キャラクターの スケルタルメッシュ やその アニメーション ブループリント などのコスメティック コンポーネントはレプリケートされません。ただし、ゲームプレイや、キャラクターの速度などの動きに関連する変数はレプリケートされ、更新されるごとにアニメーション ブループリントによって読み取られます。そのため、キャラクターの各クライアントのコピーは、ゲームプレイ変数が正確に更新されることを条件として、視覚的表現が一貫した方法で更新されます。同様に、「 ゲームプレイ フレームワーク 」は、Player Start でのキャラクターのスポーンと、キャラクターへの プレイヤー コントローラー の割り当てを自動的に処理します。

このプロジェクトでサーバーを起動してクライアントを参加させると、正しく機能するマルチプレイヤー ゲームができています。しかしながら、このゲームでは、プレイヤーは自分のアバターでしか移動およびジャンプできません。そのため、追加のマルチプレイヤー ゲームプレイを作成します。

2. RepNotifies を使用してプレイヤーのヘルスをレプリケートする

ゲームプレイ中にプレイヤーにダメージを与えるには、プレイヤーにヘルス値が必要です。すべてのクライアントにおいて各プレイヤーのヘルスに関する情報を同期するためには、ヘルス値をレプリケートする必要があります。また、プレイヤーがダメージを受けたときにプレイヤーにフィードバックも提供しなければなりません。このセクションでは、RepNotify を使用して、RPC に依存することなく、変数へのすべての重要な更新を同期する方法を説明します。

'Role' は 'GetLocalRole()' と 'GetRemoteRole()' に変更されています。以下のセクションでは 'Role' が使用されていたので、その箇所は変更されています。

  1. ThirdPersonMPCharacter.h を開きます。 protected の下に次のプロパティを追加します。

    ThirdPersonMPCharacter.h

    /** The player's maximum health.This is the highest that their health can be, and the value that their health starts at when spawned.*/
    UPROPERTY(EditDefaultsOnly, Category = "Health")
    float MaxHealth;
    
    /** The player's current health.When reduced to 0, they are considered dead.*/
    UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
    float CurrentHealth;
    
    /** RepNotify for changes made to current health.*/
    UFUNCTION()
    void OnRep_CurrentHealth();

    プレイヤーのヘルスの変更方法を厳密に制御する必要があるため、これらのヘルス値には次の制約があります。

    • MaxHealth ではレプリケートは行わず、デフォルトでは編集のみ可能。この値はすべてのプレイヤーに対して事前に計算されており、変更されることはありません。

    • CurrentHealth はレプリケートを行うが、ブループリントでは編集またはアクセスを一切行うことはできない。

    • MaxHealth および CurrentHealth はどちらも protected であるため、外部の C++ クラスからのアクセスはできない。MaxHealth および CurrentHealth は、 AThirdPersonMPCharacter 内で、または AThirdPersonMPCharacter から派生した他のクラスでのみ変更できます。

    これにより、ライブのゲームプレイ中にプレイヤーが CurrentHealth または MaxHealth を不要に変更するリスクを最小限に抑えます。これらの値を取得および変更するためのその他のパブリック関数について説明します。

    Replicated 指定子を使用すると、サーバー上のアクタのコピーが、変数の値が変更されるたびに、接続されているすべてのクライアントに変数の値をレプリケートできます。 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

    //Initialize the player's Health
    MaxHealth = 100.0f;
    CurrentHealth = MaxHealth;

    これにより、プレイヤーのヘルスが初期化されます。このキャラクターの新しいコピーが作成されるたびに、現在のヘルスがそのキャラクターの最大ヘルス値に設定されます。

  4. ThirdPersonMPCharacter.h 」で、 AThirdPersonMPCharacter コンストラクタの直後に次のパブリック関数の宣言を追加します。

    ThirdPersonMPCharacter.h

    /** Property replication */
    void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
  5. ThirdPersonMPCharacter.cpp 」で、この関数の次の実装を追加します。

    ThirdPersonMPCharacter.cpp

    //////////////////////////////////////////////////////////////////////////
    // Replicated Properties
    
    void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty> & OutLifetimeProps) const
    {
        Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
        //Replicate current health.
        DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
    }

    GetLifetimeReplicatedProps 関数は、 Replicated 指定子で指定したすべてのプロパティをレプリケートします。また、この関数を使用すると、プロパティのレプリケート方法を設定できます。この例では、 CurrentHealth の最も基本的な実装を使用しています。レプリケートする必要のあるプロパティを追加する場合は常に、そのプロパティをこの関数にも追加する必要があります。

    GetLifetimeReplicatedProps Super バージョンを呼び出す必要があります。これを行わないと、アクタの親クラスから継承されたプロパティが、親クラスでこれらのプロパティをレプリケートするように指定されている場合でもレプリケートされません。

    1. ThirdPersonMPCharacter.h 」で、 Protected の下に次の関数の宣言を追加します。

    ThirdPersonMPCharacter.h

    /** Response to health being updated.Called on the server immediately after modification, and on clients in response to a RepNotify*/
    void OnHealthUpdate();
  6. ThirdPersonMPCharacter.cpp 」で、次の実装を追加します。

    ThirdPersonMPCharacter.cpp

    void AThirdPersonMPCharacter::OnHealthUpdate()
    {
        //Client-specific functionality
        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);
            }
        }
    
        //Server-specific functionality
        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);
        }
    
        //Functions that occur on all machines. 
        /*  
            Any special functionality that should occur as a result of damage or death should be placed here. 
        */
    }

    この関数を使用して、プレイヤーの CurrentHealth への変更に応じて更新を実行します。現在、この関数の機能はオンスクリーン デバッグ メッセージに限定されていますが、死亡アニメーションをトリガーするためにすべてのマシンで呼び出される OnDeath 関数などの機能を追加できます。なお、 OnHealthUpdate はレプリケートされないため、すべてのデバイスで手動で呼び出す必要があります。

  7. ThirdPersonMPCharacter.cpp 」で、 OnRep_CurrentHealth の次の実装を追加します。

    ThirdPersonMPCharacter.cpp

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

    変数は常時レプリケートされるのではなく、値が変更するたびにレプリケートされます。また、RepNotifies はクライアントが変数のレプリケートされた値を正常に受け取るたびに実行されます。そのため、サーバー上のプレイヤーの CurrentHealth を変更するときはいつでも、接続された各クライアントで OnRep_CurrentHealth を実行することが期待されます。このため、 OnRep_CurrentHealth がクライアントのマシンで OnHealthUpdate を呼び出すのに最適です。

3. プレイヤーをダメージに反応させる

プレイヤーのヘルスを実装できたので、今度はこのクラス外からプレイヤーのヘルスを変更する方法をつくる必要があります。

  1. ThirdPersonMPCharacter.cpp 」で、 Public の下に次の関数の宣言を追加します。

    ThirdPersonMPCharacter.h

    /** Getter for Max Health.*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetMaxHealth() const { return MaxHealth; } 
    
    /** Getter for Current Health.*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }
    
    /** Setter for Current Health.Clamps the value between 0 and MaxHealth and calls OnHealthUpdate.Should only be called on the server.*/
    UFUNCTION(BlueprintCallable, Category="Health")
    void SetCurrentHealth(float healthValue);
    
    /** Event for taking damage.Overridden from APawn.*/
    UFUNCTION(BlueprintCallable, Category = "Health")
    float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;

    GetMaxHealth 関数および GetCurrentHealth 関数は、C++ およびブループリントの両方で、 AThirdPersonMPCharacter の外部からプレイヤーのヘルス値にアクセスできる「ゲッター」(値を取得するメソッド) を提供します。GetMaxHealth および GetCurrentHealth は 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 を変更する制御された手段を提供します。これはレプリケートされた関数ではないものの、アクタのネットワーク ロールが 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;
    }

    アクタにダメージを適用するためのビルトイン関数で、そのアクタの基本の TakeDamage 関数を呼び出します。この場合、 SetCurrentHealth を使用してシンプルなヘルスの削減を実装します。

次は、アクタにダメージを適用するための手順を説明します。大まかな流れは次のとおりです。

  • 外部のアクタまたは関数でキャラクターに対して CauseDamage を呼び出す。その結果、その TakeDamage 関数が呼び出されます。

  • TakeDamage SetCurrentHealth を呼び出して、サーバー上のプレイヤーの Current Health (現在のヘルス) 値を変更する。

  • SetCurrentHealth によってサーバー上で OnHealthUpdate を呼び出すことで、プレイヤーのヘルスの変化に応答して発生する機能を実行する。

  • CurrentHealth で、キャラクターのすべての接続されているクライアントのコピーにレプリケートする。

  • 各クライアントがサーバーから新しい CurrentHealth 値を受け取った際に、クライアントが OnRep_CurrentHealth を呼び出す。

  • OnRep_CurrentHealth OnHealthUpdate を呼び出し、各クライアントが新しい CurrentHealth 値に同じ方法で応答することを確実にする。

この実装には主に 2 つの利点があります。まず、この実装では、2 つの主要な関数、すなわち、 SetCurrentHealth および OnHealthUpdate に関する新しい機能を追加するためのワークフローが簡略化されます。これにより、将来にわたりコードをより簡単に保守、拡張できます。次に、この実装ではサーバー、クライアント、または NetMulticast RPC を使用しないため、すべての重要な変更をトリガーする CurrentHealth のレプリケーションのみに応じて、ネットワークを介して送信する情報量が圧縮されます。 CurrentHealth では実装する他の関数に関係なくレプリケートする必要があるため、ヘルスの変更をレプリケートするための最も効率的なモデルと言えます。

4. レプリケーションで発射物を作成する

  1. Unreal Editor 内で、 [File (ファイル)] メニューまたは コンテンツ ブラウザ のいずれかを使用して [New C++ Class (新しい C++ クラス)] を作成します。

    Create New Class

  2. [Choose Parent Class (親クラスを選択)] メニューで、親クラスとして [Actor (アクタ)] を選択して、 [Next (次へ)] をクリックします。

    クリックしてフルサイズ表示。

  3. [Name Your New Actor (新しいアクタに名前を付ける)] メニューで、クラスに 「ThirdPersonMPProjectile」 という名前を付けて、 [Create Class (クラスを作成)] をクリックします。

    クリックしてフルサイズ表示。

  4. ThirdPersonMPProjectile.h 」を開いて、 public の下のクラス定義内に次のコードを追加します。

    ThirdPersonMPProjectile.h

    // Sphere component used to test collision.
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class USphereComponent* SphereComponent;
    
    // Static Mesh used to provide a visual representation of the object.
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class UStaticMeshComponent* StaticMesh;
    
    // Movement component for handling projectile movement.
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
    class UProjectileMovementComponent* ProjectileMovementComponent;
    
    // Particle used when the projectile impacts against another object and explodes.
    UPROPERTY(EditAnywhere, Category = "Effects")
    class UParticleSystem* ExplosionEffect;
    
    //The damage type and damage that will be done by this projectile
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
    TSubclassOf<class UDamageType> DamageType;
    
    //The damage dealt by this projectile.
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
    float Damage;

    class キーワードを使ってこれらの宣言においてそれぞれの型を優先します。これにより、変数宣言されるだけでなく独自クラスの前方宣言にし、それらのクラスがヘッダ ファイル内で確実に認識されるようになります。次のステップでは CPP ファイルでそれらに #include を追加します。

    宣言しているプロパティにより、以下が指定されます。

    • 発射物の視覚的表現として機能する Static Mesh コンポーネント

    • コリジョンを確認する Sphere コンポーネント

    • 発射物を移動する Projectile Movement コンポーネント

    • 後続の手順で爆発エフェクトをスポーンするために使用する パーティクル システム の参照。

    • ダメージ イベントで使用する ダメージ タイプ

    • この発射物によってキャラクターが攻撃されたときにヘルスが減る量を示す Damage の浮動小数値。

    ただし、これらのどれもまだ定義されていません。

    bReplicates True

  5. ThirdPersonMPProjectile.cpp 」を開いて、 #include "ThirdPersonMPProjectile.h" という行の下のファイルの最上部にある #include ステートメントに次のコードを追加します。

    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"

    このチュートリアルでは、これらを一つずつ使用していきます。最初の 4 つは使用するコンポーネントですが、 GamePlayStatics.h は基本的なゲームプレイ関数へのアクセスを提供し、 ConstructorHelpers.h はコンポーネントを設定するための便利なコンストラクタ関数へのアクセスを提供します。

  6. ThirdPersonMPProjectile.cpp 」のコンストラクタ内に次のコードを追加します。

    ThirdPersonMPProjectile.cpp

    bReplicates = true;

    bReplicates 変数では、このアクタがレプリケートを行う必要があることをゲームに伝えます。アクタは、デフォルトでは、そのアクタをスポーンするマシン上にのみ存在します。 bReplicates True に設定されている場合、アクタの権限のあるコピーがサーバー上に存在する限り、アクタは接続されているすべてのクライアントにアクタをレプリケートしようとします。

  7. AThirdPersonMPProjectile のコンストラクタ内に次のコードを追加します。

    ThirdPersonMPProjectile.cpp

    //Definition for the SphereComponent that will serve as the Root component for the projectile and its collision.
    SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
    SphereComponent->InitSphereRadius(37.5f);
    SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
    RootComponent = SphereComponent;

    これにより、オブジェクトの構築時に SphereComponent が定義され、Projectile コリジョンが提供されます。

  8. コンストラクタ内に次のコードを追加します。

    ThirdPersonMPProjectile.cpp

    //Definition for the Mesh that will serve as our visual representation.
    static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
    StaticMesh->SetupAttachment(RootComponent);
    
    //Set the Static Mesh and its position/scale if we successfully found a mesh asset to use.
    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 を定義します。このコンポーネントでは、 StarterContent 内で 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;
    }

    これにより、StarterContent 内の P_Explosion アセットになるように ExplosionEffect のアセットの参照が設定されます。

  10. コンストラクタ内に次のコードを追加します。

    ThirdPersonMPProjectile.cpp

    //Definition for the Projectile Movement Component.
    ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
    ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
    ProjectileMovementComponent->InitialSpeed = 1500.0f;
    ProjectileMovementComponent->MaxSpeed = 1500.0f;
    ProjectileMovementComponent->bRotationFollowsVelocity = true;
    ProjectileMovementComponent->ProjectileGravityScale = 0.0f;

    これにより、発射物の Projectile Movement コンポーネントが定義されます。このコンポーネントはレプリケートされ、サーバー上でこのコンポーネントが実行するすべての動きがクライアント上で再現されます。

  11. コンストラクタ内に次のコードを追加します。

    ThirdPersonMPProjectile.cpp

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

    これらは、発射物がアクタに与えるダメージの量と、ダメージ イベントで使用されるダメージ タイプの両方を初期化します。この例では、新しいダメージ タイプをまだ定義していないため、基本の 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);
    }

    この Destroyed 関数は、アクタが破棄されるたびに呼び出されます。パーティクル エミッタ自体は通常はレプリケートされませんが、アクタの破壊はレプリケートされるため、サーバー上のこの発射物を破壊すると、接続された各クライアント上で自身のコピーを破壊する際に、この関数が各クライアントで呼び出されることがわかります。その結果、発射物が破壊されると、すべてのプレイヤーに爆発エフェクトが表示されます。

  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();
    }

    これは、発射物がオブジェクトに衝突したときに呼び出す関数です。発射物が衝突するオブジェクトが有効なアクタである場合、コリジョンが発生したポイントで、そのオブジェクトにダメージを与える ApplyPointDamage 関数を呼び出します。一方、衝突したサーフェスにかかわらず、あらゆるコリジョンでこのアクタが破壊され、爆発エフェクトが表示されます。

  5. ThirdPersonMPProjectile.cpp で、 RootComponent = SphereComponent という行の下のコンストラクタに次のコードを追加します。

    ThirdPersonMPProjectile.h

    //Registering the Projectile Impact function on a Hit event.
    if (GetLocalRole() == ROLE_Authority)
    {
        SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact);
    }

    これにより、Sphere コンポーネント上の OnComponentHit イベントを使用して OnProjectileImpact 関数が登録されます。これは、発射物の主なコリジョン コンポーネントとして機能します。特に、このゲームプレイ ロジックがサーバーのみで実行されることを確実にするためには、 OnProjectileImpact を登録する前に GetLocalRole() == ROLE_Authority と指定されていることを確認します。

6. 発射物を発射する

  1. エディタ を開いて、画面上部の [Edit (編集)] ドロップダウン メニューをクリックし、 [Project Settings (プロジェクト設定)] を開きます。

    Project Settings

  2. [Engine] セクションで、 [Input (入力)] をクリックして、プロジェクトの入力設定を開きます。 [Bindings (バインド)] セクションを展開して、新しいエントリを追加します。 「Fire」 という名前を付けて、このアクションをバインドするキーとして [Left Mouse Button (マウスの左ボタン)] を選択します。

    クリックしてフルサイズ表示。

  3. ThirdPersonMPCharacter.cpp 」で、 #include "Engine/Engine.h" という行の下に次の #include を追加します。

    ThirdPersonMPCharacter.cpp

    #include "ThirdPersonMPProjectile.h"

    これにより、Character クラスが発射物のタイプを認識して発射物をスポーンできるようになります。

  4. ThirdPersonMPCharacter.cpp 」で、 protected の下に次のコードを追加します。

    ThirdPersonMPCharacter.h

    UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
    TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;
    
    /** Delay between shots in seconds.Used to control fire rate for our test projectile, but also to prevent an overflow of server functions from binding SpawnProjectile directly to input.*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay")
    float FireRate;
    
    /** If true, we are in the process of firing projectiles. */
    bool bIsFiringWeapon;
    
    /** Function for beginning weapon fire.*/
    UFUNCTION(BlueprintCallable, Category="Gameplay")
    void StartFire();
    
    /** Function for ending weapon fire.Once this is called, the player can use StartFire again.*/
    UFUNCTION(BlueprintCallable, Category = "Gameplay")
    void StopFire();  
    
    /** Server function for spawning projectiles.*/
    UFUNCTION(Server, Reliable)
    void HandleFire();
    
    /** A timer handle used for providing the fire rate delay in-between spawns.*/
    FTimerHandle FiringTimer;

    これらは、発射物を発射するために使用する変数と関数です。 HandleFire はこのチュートリアルで実装する唯一の RPC であり、サーバーでの発射物のスポーンを行います。HandleFire には Server 指定子が含まれているため、クライアント上で HandleFire を呼び出そうとすると、呼び出しがネットワーク経由でサーバー上の権限のあるキャラクターに向けられます。

    HandleFire には Reliable 指定子も含まれているため、HandleFire は呼び出されるたびに信頼できる RPC のキューに配置されます。また、サーバーが HandleFire を正常に受信するとキューから削除されます。これにより、サーバーがこの関数呼び出しを確実に受信することが保証されます。ただし、削除することなく一度に多くの RPC を配置すると、信頼性の高い RPC のキューがオーバーフローする恐れがあります。その場合、ユーザーは強制的に接続を解除されます。そのため、プレイヤーがこの関数を呼び出すことのできる頻度に注意する必要があります。

    1. ThirdPersonMPCharacter.cpp 」で、コンストラクタの下に次のコードを追加します。

    ThirdPersonMPCharacter.cpp

    //Initialize projectile class
    ProjectileClass = AThirdPersonMPProjectile::StaticClass();
    //Initialize fire rate
    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, &AThirdPersonMP424Character::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 を呼び出すことができる頻度を制限します。

    • すでに発射中の場合、ユーザーは発射物を発射できない。これは、 StartFire の呼び出し時に bFiringWeapon true に設定されていることで指定されます。

    • StopFire の呼び出し時には、 bFiringWeapon false にのみ設定される。

    • 長さが FireRate であるタイマーが完了すると StopFire が呼び出される。

    つまり、ユーザーが発射物を発射する際は、再度発射できるまでに FireRate 秒以上待機する必要があります。これは、 StartFire のバインド先の入力の種類にかかわらず同じように機能します。例えば、ユーザーが「Fire」コマンドをスクロール ホイールやその他の不適切な入力にバインドしたり、ユーザーが繰り返しボタンを押したりしても、この関数は許容可能な間隔で引き続き実行され、 HandleFire への呼び出しによる信頼性の高い関数のユーザーのキューはオーバーフローしません。

    HandleFire は Server RPC であるため、CPP での実装には、関数名に「 _Implementation 」サフィックスを付ける必要があります。この実装では、キャラクターの回転コントロールを使用してカメラが向いている方向を取得してから発射物をスポーンすることで、プレイヤーが照準を合わせることができるようにしています。そのため、発射物の Projectile Movement コンポーネントがその方向での発射物の動きを処理をします。

  5. ThirdPersonMPCharacter.cpp 」で、関数 SetupPlayerInputComponent の下に次を追加します。

    ThirdPersonMPCharacter.cpp

    // Handle firing projectiles
    PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);

    これで、 StartFire がこのセクションの最初の手順で作成した Fire 入力アクションにバインドされ、ユーザーが StartFire を有効にできます。

  6. ゲームをテストする

  7. エディタでプロジェクトを開きます。 [Edit (編集)] ドロップダウン メニューをクリックして、 [Editor Preferences (エディタの環境設定)] を開きます。

    Open Level Editor/Play

  8. [Level Editor (レベル エディタ)] セクションに移動して、 [Play (プレイ)] メニューをクリックします。 [Multiplayer Options (マルチプレイヤー オプション)] を見つけて、 [Number of Players (プレイヤー数)] を「2」に変更します。

    クリックしてフルサイズ表示。

  9. [Play] ボタンを押します。メインの [Play in Editor (PIE)] ウィンドウでは、サーバーとしてマルチプレイヤー セッションが開始され、2 番目の PIE ウィンドウが開いてクライアントとして接続されます。

最終結果

クリックしてフルサイズ表示。

ゲーム内のプレイヤー同士は、互いの動きを確認できるため、カスタム発射物を撃ち合うことができます。一方のプレイヤーにカスタム発射体がヒットすると、両方のプレイヤーに爆発パーティクルが表示され、被弾したプレイヤーは、受けたダメージと現在のヘルスを示す「hit (ヒット)」というメッセージを受け取りますが、セッションの他のすべてのプレイヤーには何も表示されません。プレイヤーのヘルスが 0 まで低下すると、そのプレイヤーが殺害 (キル) されたことを通知するメッセージが表示されます。

このチュートリアルを完了したので、変数およびコンポーネントのレプリケーションの概要、ネットワーク ロールの使用方法、RPC を使用するのが適切な場合など、C++ でマルチプレイヤー機能を作成する基本についてご理解いただけたと思います。この知識を活用すれば、Unreal のサーバークライアント モデル内で独自のマルチプレイヤー ゲームをビルドすることができます。

応用編

ネットワーク マルチプレイヤー プログラミングのスキルアップのため、以下に挑戦してみましょう。

  • 発射物の OnHit 機能を拡張して、発射物がターゲットにヒットしたときに、球体トレースを作成して爆発半径をシミュレートするなど、追加のエフェクトを作成する。

  • ThirdPersonMPProjectile を拡張し、ProjectileMovement コンポーネントを試して、さまざまなビヘイビアで新しいバリエーションを作成する。

  • ThirdPersonMPCharacter の TakeDamage 関数を拡張して、プレイヤーのポーンを殺害し、再スポーンさせる。

  • HUD をローカルの PlayerController に追加して、レプリケートされた情報を表示するか、クライアント関数に応答する。

  • DamageTypes を使用して、プレイヤーが殺害されたときのパーソナライズされたメッセージを作成する。

  • Game Mode (ゲーム モード)、Player State (プレイヤーの状態)、および Game State (ゲームの状態) の使用状況を調べて、プレイヤーの統計情報とスコアボードによるマッチを調整する一連の完全なルールを作成する。

コード例

ThirdPersonMPProjectile.h

// Copyright 1998-2019 Epic Games, Inc.All Rights Reserved.

#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:
    // Sets default values for this actor's properties
    AThirdPersonMPProjectile();

    // Basic components for this projectile
    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;

    //The damage type and damage that will be done by this projectile
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
    TSubclassOf<UDamageType> DamageType;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
    float Damage;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

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

    void Destroyed() override;

public: 
    // Called every frame
    virtual void Tick(float DeltaTime) override;
};

ThirdPersonMPProjectile.cpp

// Copyright 1998-2019 Epic Games, Inc.All Rights Reserved.

#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"

// Sets default values
AThirdPersonMPProjectile::AThirdPersonMPProjectile()
{
    // 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;

    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;
    }

    //Definition for the SphereComponent that will serve as the Root component for the projectile and its collision.
    SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("RootComponent"));
    SphereComponent->InitSphereRadius(12.5f);
    SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
    RootComponent = SphereComponent;

    //Registering the Projectile Impact function on a Hit event.
    if (GetLocalRole() == ROLE_Authority)
    {
        SphereComponent->OnComponentHit.AddDynamic(this, &AThirdPersonMPProjectile::OnProjectileImpact);
    }

    //Definition for the Mesh that will serve as our visual representation.
    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);
    }

    //Definition for the Projectile Movement Component.
    ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
    ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
    ProjectileMovementComponent->InitialSpeed = 1500.0f;
    ProjectileMovementComponent->MaxSpeed = 1500.0f;
    ProjectileMovementComponent->bRotationFollowsVelocity = true;
    ProjectileMovementComponent->ProjectileGravityScale = 0.0f;
}

// Called when the game starts or when spawned
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);
}

// Called every frame
void AThirdPersonMPProjectile::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

ThirdPersonMPCharacter.h

// Copyright 1998-2019 Epic Games, Inc.All Rights Reserved.

#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()

    /** Camera boom positioning the camera behind the character */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class USpringArmComponent* CameraBoom;

    /** Follow camera */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UCameraComponent* FollowCamera;
public:

    /** Constructor */
    AThirdPersonMPCharacter();

    /** Property replication */
    void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    /** Base turn rate, in deg/sec.Other scaling may affect final turn rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseTurnRate;

    /** Base look up/down rate, in deg/sec.Other scaling may affect final rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseLookUpRate;

protected:
    /** The player's maximum health.This is the highest that their health can be, and the value that their health starts at when spawned.*/
    UPROPERTY(EditDefaultsOnly, Category = "Health")
    float MaxHealth;

    /** The player's current health.When reduced to 0, they are considered dead.*/
    UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)
    float CurrentHealth;

    /** RepNotify for changes made to current health.*/
    UFUNCTION()
    void OnRep_CurrentHealth();

    /** Response to health being updated.Called on the server immediately after modification, and on clients in response to a RepNotify*/
    void OnHealthUpdate();

public:
    /** Getter for Max Health.*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetMaxHealth() const { return MaxHealth; } 

    /** Getter for Current Health.*/
    UFUNCTION(BlueprintPure, Category="Health")
    FORCEINLINE float GetCurrentHealth() const { return CurrentHealth; }

    /** Setter for Current Health.Clamps the value between 0 and MaxHealth and calls OnHealthUpdate.Should only be called on the server.*/
    UFUNCTION(BlueprintCallable, Category="Health")
    void SetCurrentHealth(float healthValue);

    /** Event for taking damage.Overridden from APawn.*/
    UFUNCTION(BlueprintCallable, Category = "Health")
    float TakeDamage( float DamageTaken, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser ) override;

    protected:
    /** Resets HMD orientation in VR. */
    void OnResetVR();

    /** Called for forwards/backward input */
    void MoveForward(float Value);

    /** Called for side to side input */
    void MoveRight(float Value);

    /** 
     * Called via input to turn at a given rate. 
     * @param Rate  This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
     */
    void TurnAtRate(float Rate);

    /**
     * Called via input to turn look up/down at a given rate. 
     * @param Rate  This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
     */
    void LookUpAtRate(float Rate);

    /** Handler for when a touch input begins. */
    void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);

    /** Handler for when a touch input stops. */
    void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);

protected:
    // APawn interface
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    // End of APawn interface

    /** The type of projectile the character is going to fire.*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
    TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;

    /** Delay between shots in seconds.Used to control fire rate for our test projectile, but also to prevent an overflow of server functions from binding SpawnProjectile directly to input.*/
    UPROPERTY(EditDefaultsOnly, Category="Gameplay")
    float FireRate;

    /** If true, this weapon is in the process of being fired. */
    bool bIsFiringWeapon;

    /** Function for beginning weapon fire.This should only be triggered by the local player.*/
    UFUNCTION(BlueprintCallable, Category="Gameplay")
    void StartFire();

    /** Function for ending weapon fire.Once this is called, the player can use StartFire again.*/
    UFUNCTION(BlueprintCallable, Category = "Gameplay")
    void StopFire();  

    /** Server function for spawning projectiles.*/
    UFUNCTION(Server, Reliable)
    void HandleFire();

    /** A timer handle used for providing the fire rate delay in-between spawns.*/
    FTimerHandle FiringTimer;

public:
    /** Returns CameraBoom subobject **/
    FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
    /** Returns FollowCamera subobject **/
    FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

ThirdPersonMPCharacter.cpp

// Copyright 1998-2019 Epic Games, Inc.All Rights Reserved.

#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()
{
    // Set size for collision capsule
    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

    // set our turn rates for input
    BaseTurnRate = 45.f;
    BaseLookUpRate = 45.f;

    // Don't rotate when the controller rotates. Let that just affect the camera.
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // Configure character movement
    GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...   
    GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate
    GetCharacterMovement()->JumpZVelocity = 600.f;
    GetCharacterMovement()->AirControl = 0.2f;

    // Create a camera boom (pulls in towards the player if there is a collision)
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent);
    CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character   
    CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

    // Create a follow camera
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
    FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

    // Note: The skeletal mesh and anim Blueprint references on the Mesh component (inherited from Character) 
    // are set in the derived Blueprint asset named MyCharacter (to avoid direct content references in C++)

    //Initialize the player's Health
    MaxHealth = 100.0f;
    CurrentHealth = MaxHealth;

    //Initialize projectile class
    ProjectileClass = AThirdPersonMPProjectile::StaticClass();
    //Initialize fire rate
    FireRate = 0.25f;
    bIsFiringWeapon = false;
}

//////////////////////////////////////////////////////////////////////////
// Input

void AThirdPersonMPCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
    // Set up gameplay key bindings
    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);

    // We have 2 versions of the rotation bindings to handle different kinds of devices differently
    // "turn" handles devices that provide an absolute delta, such as a mouse.
    // "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
    PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
    PlayerInputComponent->BindAxis("TurnRate", this, &AThirdPersonMPCharacter::TurnAtRate);
    PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
    PlayerInputComponent->BindAxis("LookUpRate", this, &AThirdPersonMPCharacter::LookUpAtRate);

    // handle touch devices
    PlayerInputComponent->BindTouch(IE_Pressed, this, &AThirdPersonMPCharacter::TouchStarted);
    PlayerInputComponent->BindTouch(IE_Released, this, &AThirdPersonMPCharacter::TouchStopped);

    // VR headset functionality
    PlayerInputComponent->BindAction("ResetVR", IE_Pressed, this, &AThirdPersonMPCharacter::OnResetVR);

    // Handle firing projectiles
    PlayerInputComponent->BindAction( "Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
    //PlayerInputComponent->BindAction("Fire", IE_Released, this, &AThirdPersonMPCharacter::StopFire);
}
//////////////////////////////////////////////////////////////////////////
// Replicated Properties

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()
{
    //Client-specific functionality
    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);
        }
    }

    //Server-specific functionality
    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);
    }

    //Functions that occur on all machines. 
    /*  
        Any special functionality that should occur as a result of damage or death should be placed here. 
    */
}

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)
{
    // calculate delta for this frame from the rate information
    AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}

void AThirdPersonMPCharacter::LookUpAtRate(float Rate)
{
    // calculate delta for this frame from the rate information
    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);

        // get forward vector
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(Direction, Value);
    }
}

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

        // get right vector 
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        // add movement in that direction
        AddMovementInput(Direction, Value);
    }
}
Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
閉じる