マルチプレイヤー ゲームのゲームプレイを開発するには、ゲームの アクタ に レプリケーション を実装する必要があります。また、ゲーム セッションのホストとして機能する サーバー、またはセッションに接続するプレイヤーを表す クライアント に固有の機能を設計する必要があります。このガイドでは、いくつかのシンプルなマルチプレイヤー ゲームプレイを作成する方法をプロセスごとに説明します。以下の内容を学習することができます。
ベース アクタにレプリケーションを追加する方法。
ネットワーク ゲームで Movement コンポーネント を活用する方法。
変数 にレプリケーションを追加する方法。
変数の変更時に RepNotifies を使用する方法。
C++ で リモート プロシージャ コール (RPC) を使用する方法。
関数内で実行される呼び出しをフィルタリングするために、アクタの ネットワーク ロール を確認する方法。
最終結果として、プレイヤー同士が爆発物を投げ合うことのできるサードパーソン ゲームが仕上がります。ここで行う作業の大部分は、発射物を作成する作業と、ダメージに対する反応をキャラクターに追加する作業です。
作業を開始する前に、「Dedicated Servers (専用サーバー)」ページと「ネットワーキングの概要」ページで、基本事項を確認することを強くお勧めします。このガイドの比較参照のためのドキュメントとして、レプリケーションの概念を取り入れていない、「
1.基本設定
エディタ を開いて 新規プロジェクト を作成します。新規プロジェクトが、次のように設定されていることを確認します。
C++ プロジェクト である
**「Third-Person」テンプレート**を使用している
スターター コンテンツ が含まれている
デスクトップ をターゲットにしている
これらの設定を適用したら、プロジェクトに「ThirdPersonMP」という名前を付けて、[Create (作成)] ボタンをクリックして続行します。プロジェクトの C++ ファイルが作成され、Unreal Editor により ThirdPersonExampleMap が自動的に開かれます。
このシーンで立っている ThirdPersonCharacter をクリックして 削除 し、2 つの Player Start がマップ内にあることを確認します。これらは、デフォルトでシーンに含まれる手動で配置された ThirdPersonCharacter の代わりに、プレイヤーのスポーンを処理します。
ほとんどのテンプレート内にあるポーンとキャラクターは、デフォルトでレプリケーションが有効になっています。この例では、ThirdPersonCharacter には、自動的に動きをレプリケートする Character Movement コンポーネント がすでに含まれています。
Character Movement コンポーネントがレプリケーションを処理する方法とその機能を拡張する方法については、「Character Movement コンポーネント)」ガイドを参照してください。
キャラクターの スケルタルメッシュ やその アニメーション ブループリント などのコスメティック コンポーネントはレプリケートされません。ただし、ゲームプレイや、キャラクターの速度などの動きに関連する変数はレプリケートされます。アニメーション ブループリントは、これらの変数が更新されると読み取ります。このように、各クライアントのキャラクターのコピーがそのビジュアルを更新します。プロセスは、ゲームプレイ変数の正確な更新と一貫性が確保されるように実行されます。同様に、
このプロジェクトでサーバーを起動してクライアントを参加させると、正しく機能する作成済みのマルチプレイヤー ゲームがありますが、このゲームでは、プレイヤーは自分のアバターでしか移動およびジャンプできません。そのため、追加のマルチプレイヤー ゲームプレイを作成します。
2.RepNotifies を使用してプレイヤーのヘルスをレプリケートする
ゲームプレイ中にプレイヤーにダメージを与えるには、プレイヤーにヘルス値が必要です。ヘルス値をレプリケートする必要があり、すべてのクライアントにおいて各プレイヤーのヘルスに関する情報が同期されています。また、プレイヤーがダメージを受けたときにプレイヤーにフィードバックも提供しなければなりません。このセクションでは、RepNotify を使用して、RPC に依存することなく、変数へのすべての重要な更新を同期する方法を説明します。
「Role」は「GetLocalRole()」と「GetRemoteRole()」に変更されています。以下のセクションでは「Role」が使用されていたので、その箇所は変更されています。
ThirdPersonMPCharacter.h
を開きます。protected
の下に次のプロパティを追加します。ThirdPersonMPCharacter.h
を開きます。protected
の下に次のプロパティを追加します。protected: /** The player's maximum health. This is the highest value of their health can be. This value is a value of the player's health, which 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` を使用して、この変数への変更に基づいて、各クライアントに対する更新を実行します。
ThirdPersonMPCharacter.cpp
を開きます。#include "GameFramework/SpringArmComponent.h"
という行の下に、次の#include
ステートメントを追加します。#include "Net/UnrealNetwork.h" #include "Engine/Engine.h"
これらは、変数のレプリケーションに必要な機能と、
GEngine
のAddOnscreenDebugMessage
関数へのアクセスを提供します。これを使用して、画面にメッセージを出力します。ThirdPersonMPCharacter.cpp
で、AThirdPersonMPCharacter
コンストラクタの下に次のコードを追加します。//Initialize the player's Health MaxHealth = 100.0f; CurrentHealth = MaxHealth;
これにより、プレイヤーのヘルスが初期化されます。このキャラクターの新しいコピーが作成されるたびに、現在のヘルスがそのキャラクターの最大ヘルス値に設定されます。
ThirdPersonMPCharacter.h
で、AThirdPersonMPCharacter
コンストラクタの直後に次のパブリック関数の宣言を追加します。/** Property replication */ void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
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
バージョンを呼び出す必要があります。これを行わないと、アクタの親クラスから継承されたプロパティが、親クラスでこれらのプロパティをレプリケートするように指定されている場合でもレプリケートされません。ThirdPersonMPCharacter.h
で、次の関数宣言をProtected
の下に追加します。protected: /** Response to health being updated. Called on the server immediately after modification, and on clients in response to a RepNotify*/ void OnHealthUpdate();
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
はレプリケートされないため、すべてのデバイスで手動で呼び出す必要があります。ThirdPersonMPCharacter.cpp
で、次のようにOnRep_CurrentHealth
の実装を追加します。void AThirdPersonMPCharacter::OnRep_CurrentHealth() { OnHealthUpdate(); }
変数は常時レプリケートされるのではなく、値が変更するたびにレプリケートされます。また、
RepNotifies
はクライアントが変数のレプリケートされた値を正常に受け取るたびに実行されます。そのため、サーバー上のプレイヤーのCurrentHealth
を変更するときはいつでも、接続された各クライアントでOnRep_CurrentHealth
を実行することが期待されます。このため、OnRep_CurrentHealth
がクライアントのマシンでOnHealthUpdate
を呼び出すのに最適です。
3.プレイヤーをダメージに反応させる
プレイヤーのヘルスを実装できたので、今度はこのクラス外からプレイヤーのヘルスを変更する方法をつくる必要があります。
ThirdPersonMPCharacter.h
で、次の関数宣言をPublic
の下に追加します。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;
GetMaxHealth
関数およびGetCurrentHealth
関数は、C++ およびブループリントの両方で、AThirdPersonMPCharacter
の外部からプレイヤーのヘルス値にアクセスできるゲッター (値を取得するメソッド) を提供します。GetMaxHealth および GetCurrentHealth はconst
関数として、これらの値を変更できるようにすることなく、値を取得できる安全な手段を提供します。また、プレイヤーのヘルスを設定し、ダメージを与えるための関数を宣言しています。ThirdPersonMPCharacter.cpp
で、次のようにSetCurrentHealth
の実装を追加します。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 を受け取らないため、これが必要になります。このような「セッター」関数はすべての変数に必要なわけではありませんが、特に多くの異なるソースから変更される可能性がある場合に、プレイ中に頻繁に変更される反応性の高いゲームプレイ変数に適しています。これは、このような変数のライブでの変更の一貫性を高め、デバッグを容易にし、新しい機能による拡張をより簡単にするため、シングルプレイヤー ゲームおよびマルチプレイヤー ゲームで使用することをお勧めします。
ThirdPersonMPCharacter.cpp
で、次のようにTakeDamage
の実装を追加します。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.レプリケーションで発射物を作成する
Unreal Editor 内で、[Tools (ツール)] メニューまたは コンテンツ ブラウザ のいずれかを使用して 新しい C++ クラス を作成します。
[Choose Parent Class (親クラスを選択)] メニューで、親クラスとして [Actor (アクタ)] を選択して、[Next (次へ)] をクリックします。
画像をクリックしてフルサイズで表示
[Name Your New Actor (新しいアクタに名前を付ける)] メニューで、クラスに ThirdPersonMPProjectile という名前を付けて、[Create Class (クラスを作成)] をクリックします。
画像をクリックしてフルサイズで表示
ThirdPersonMPProjectile.h
を開いて、public
の下のクラス定義内に次のコードを追加します。public: // 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 の浮動小数値。
ただし、これらのどれもまだ定義されていません。
Character Movement コンポーネントと同様に、Projectile Movement コンポーネントは、アクタの
bReplicates
がTrue
に設定されている場合、このコンポーネントが属するアクタの移動時にレプリケーションを自動的に処理します。ThirdPersonMPProjectile.cpp
を開いて、#include "ThirdPersonMPProjectile.h"
という行の下のファイルの最上部にある#include
ステートメントに次のコードを追加します。#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"
このウォークスルーでは、これらを 1 つずつ使用する必要があります。最初の 4 つは使用するコンポーネントですが、
GamePlayStatics.h
は基本的なゲームプレイ関数へのアクセスを提供し、ConstructorHelpers.h
はコンポーネントを設定するための便利なコンストラクタ関数へのアクセスを提供します。AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。bReplicates = true;
bReplicates
変数では、このアクタがレプリケートを行う必要があることをゲームに伝えます。アクタは、デフォルトでは、そのアクタをスポーンするマシン上にのみ存在します。bReplicates
がTrue
に設定されている場合、アクタの権限のあるコピーがサーバー上に存在する限り、アクタは接続されているすべてのクライアントにアクタをレプリケートしようとします。AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。//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 コリジョンが提供されます。
AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。//Definition for the Mesh that will serve as your 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 you 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 に合うようにスケーリングされます。
AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion")); if (DefaultExplosionEffect.Succeeded()) { ExplosionEffect = DefaultExplosionEffect.Object; }
これにより、StarterContent 内の P_Explosion アセットになるように
ExplosionEffect
のアセットの参照が設定されます。AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。//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 コンポーネントが定義されます。このコンポーネントはレプリケートされ、サーバー上でこのコンポーネントが実行するすべての動きがクライアント上で再現されます。
AThirdPersonMPProjectile
のコンストラクタ内に次のコードを追加します。DamageType = UDamageType::StaticClass(); Damage = 10.0f;
これらは、発射物がアクタに与えるダメージの量と、ダメージ イベントで使用されるダメージ タイプの両方を初期化します。この例では、新しいダメージ タイプをまだ定義していないため、基本の
UDamageType
で初期化します。
5.発射物によるダメージを作成する
これまで説明した手順をすべて実行してきた場合は、現時点でサーバー上で発射物をスポーンすることができ、すべてのクライアント上で発射物が表示されて、移動可能になります。ただし、壁など遮断するオブジェクトにぶつかると発射物は停止します。プレイヤーにダメージを与えるために必要なビヘイビアであり、セッションで接続されているすべてのクライアント上で爆発エフェクトを発生させる必要があります。
ThirdPersonMPProjectile.h
で、Protected
の下に、次のコードを追加します。protected: virtual void Destroyed() override;
ThirdPersonMPProjectile.cpp
で、この関数に次の実装を行います。void AThirdPersonMPProjectile::Destroyed() { FVector spawnLocation = GetActorLocation(); UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease); }
この
Destroyed
関数は、アクタが破棄されるたびに呼び出されます。パーティクル エミッタ自体は通常レプリケートされませんが、アクタの破壊がレプリケートされるため、サーバー上のこの発射物が破壊されます。自身のコピーを破壊する際に、この関数が接続された各クライアント上で呼び出されます。その結果、発射物が破壊されると、すべてのプレイヤーに爆発エフェクトが表示されます。ThirdPersonMPProjectile.h
で、Protected
の下に、次のコードを追加します。UFUNCTION(Category="Projectile") void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
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
関数を呼び出します。一方、衝突したサーフェスにかかわらず、あらゆるコリジョンでこのアクタが破壊され、爆発エフェクトが表示されます。ThirdPersonMPProjectile.cpp
で、RootComponent = SphereComponent
という行の下のAThirdPersonMPProjectile
コンストラクタに次のコードを追加します。//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.発射物を発射する
Unreal Editor を開いて、画面上部の [Edit (編集)] ドロップダウン メニューをクリックし、[Project Settings (プロジェクト設定)] を開きます。
[Engine (エンジン)] セクションで、[Input (入力)] をクリックして、プロジェクトの入力設定を開きます。[Bindings (バインド)] セクションを展開して、新しいエントリを追加します。「Fire」という名前を付けて、このアクションをバインドするキーとして [Left Mouse Button (左マウス ボタン)] を選択します。
画像をクリックしてフルサイズで表示
ThirdPersonMPCharacter.cpp
で、#include "Engine/Engine.h"
という行の下に次の#include
を追加します。#include "ThirdPersonMPProjectile.h"
これにより、Character クラスが発射物のタイプを認識して発射物をスポーンできるようになります。
ThirdPersonMPCharacter.h
で、protected
の下に、次のコードを追加します。protected: UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile") TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass; /** Delay between shots in seconds. Used to control fire rate for your 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, you 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 のキューがオーバーフローする恐れがあります。その場合、ユーザーは強制的に接続を解除されます。そのため、プレイヤーがこの関数を呼び出すことのできる頻度に注意する必要があります。ThirdPersonMPCharacter.cpp
で、AThirdPersonMPCharacter
コンストラクタの下に次のコードを追加します。//Initialize projectile class ProjectileClass = AThirdPersonMPProjectile::StaticClass(); //Initialize fire rate FireRate = 0.25f; bIsFiringWeapon = false;
これらにより、発射物の発射を処理するために必要な変数が初期化されます。
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() + ( GetActorRotation().Vector() * 100.0f ) + (GetActorUpVector() * 50.0f); FRotator spawnRotation = GetActorRotation(); 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 コンポーネントがその方向での発射物の動きを処理をします。ThirdPersonMPCharacter.cpp
で、SetupPlayerInputComponent
関数の下に次のコードを追加します。// Handle firing projectiles PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
これで、
StartFire
がこのセクションの最初の手順で作成した Fire 入力アクションにバインドされ、ユーザーが StartFire を有効にできます。
7.ゲームをテストする
エディタでプロジェクトを開きます。[Edit (編集)] ドロップダウン メニューをクリックして、[Editor Preferences (エディタの環境設定)] を開きます。
[Level Editor (レベル エディタ)] セクションに移動して、[Play (プレイ)] メニューをクリックします。[Multiplayer Options (マルチプレイヤー オプション)] を見つけて、[Play Net Mode (プレイ ネット モード)] を [Play As Listen Server (リッスン サーバーとしてプレイ)] に変更します。また、[Play Number of Clients (プレイ クライアント数)] を 2 に設定します。
画像をクリックしてフルサイズで表示
[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 (ゲームの状態) の使用状況を調べて、プレイヤーの統計情報とスコアボードによるマッチを調整する一連の完全なルールを作成する。
コード例
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ThirdPersonMPProjectile.generated.h"
UCLASS()
class THIRDPERSONMP_API AThirdPersonMPProjectile : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AThirdPersonMPProjectile();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
public:
// 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;
protected:
virtual void Destroyed() override;
UFUNCTION(Category = "Projectile")
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
};
// Copyright 1998-2022 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"
// 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;
//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;
//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 your 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 you 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));
}
static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));
if (DefaultExplosionEffect.Succeeded())
{
ExplosionEffect = DefaultExplosionEffect.Object;
}
//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;
DamageType = UDamageType::StaticClass();
Damage = 10.0f;
}
// Called when the game starts or when spawned
void AThirdPersonMPProjectile::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AThirdPersonMPProjectile::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AThirdPersonMPProjectile::Destroyed()
{
FVector spawnLocation = GetActorLocation();
UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}
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();
}
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.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:
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=Input)
float TurnRateGamepad;
protected:
/** 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);
// APawn interface
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// End of APawn interface
public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
protected:
/** The player's maximum health. This is the highest value of their health can be. This value is a value of the player's health, which 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:
UPROPERTY(EditDefaultsOnly, Category = "Gameplay|Projectile")
TSubclassOf<class AThirdPersonMPProjectile> ProjectileClass;
/** Delay between shots in seconds. Used to control fire rate for your 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, you 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;
};
// Copyright 1998-2022 Epic Games, Inc. All Rights Reserved.
#include "ThirdPersonMPCharacter.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"
//////////////////////////////////////////////////////////////////////////
// AThirdPersonMPCharacter
AThirdPersonMPCharacter::AThirdPersonMPCharacter()
{
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
// set our turn rate for input
TurnRateGamepad = 50.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, 500.0f, 0.0f); // ...at this rotation rate
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
// instead of recompiling to adjust them
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
// Create a camera boom (pulls in towards the player if there is a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.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 ThirdPersonCharacter (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("Move Forward / Backward", this, &AThirdPersonMPCharacter::MoveForward);
PlayerInputComponent->BindAxis("Move Right / Left", 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 Right / Left Mouse", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("Turn Right / Left Gamepad", this, &AThirdPersonMPCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("Look Up / Down Mouse", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("Look Up / Down Gamepad", this, &AThirdPersonMPCharacter::LookUpAtRate);
// handle touch devices
PlayerInputComponent->BindTouch(IE_Pressed, this, &AThirdPersonMPCharacter::TouchStarted);
PlayerInputComponent->BindTouch(IE_Released, this, &AThirdPersonMPCharacter::TouchStopped);
// Handle firing projectiles
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AThirdPersonMPCharacter::StartFire);
}
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 * TurnRateGamepad * GetWorld()->GetDeltaSeconds());
}
void AThirdPersonMPCharacter::LookUpAtRate(float Rate)
{
// calculate delta for this frame from the rate information
AddControllerPitchInput(Rate * TurnRateGamepad * GetWorld()->GetDeltaSeconds());
}
void AThirdPersonMPCharacter::MoveForward(float Value)
{
if ((Controller != nullptr) && (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 != nullptr) && (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);
}
}
//////////////////////////////////////////////////////////////////////////
// Replicated Properties
void AThirdPersonMPCharacter::GetLifetimeReplicatedProps(TArray <FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
//Replicate current health.
DOREPLIFETIME(AThirdPersonMPCharacter, CurrentHealth);
}
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::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() + (GetActorRotation().Vector() * 100.0f) + (GetActorUpVector() * 50.0f);
FRotator spawnRotation = GetActorRotation();
FActorSpawnParameters spawnParameters;
spawnParameters.Instigator = GetInstigator();
spawnParameters.Owner = this;
AThirdPersonMPProjectile* spawnedProjectile = GetWorld()->SpawnActor<AThirdPersonMPProjectile>(spawnLocation, spawnRotation, spawnParameters);
}