概要
こんにちは。パンダマスターと申します。
最近、UE4でゲームを作り始めました。
注記
この記事の内容はEpic Gamesとは何の関係もありません。
免責事項
当記事に記載されている内容、サンプルコードを使用することによって被った、いかなる損害に対しても筆者は一切責任を負いません。当記事のサンプルコードの使用に際しては自己責任にてお願いします。
上記に同意していただける方のみ、続きをお読みください。
続きを読むことによって、上記の免責事項に同意したことになります。
今回の目標
キャラクター選択。
バージョン情報
Unreal Engine 4.25.3
追記
2020/8/8:デフォルトのInputModeだと入力が毎フレーム入ってこないので改善しました。
追加コードは「// 追記:2020/8/8」の記載あり。
謝辞
マウスカーソルの移動に関して改善方法を提供してくれたY.I氏に感謝。
実装
早速、実装を見ていきましょう!
まだ整理された状態ではないため、コード全体ではなく苦労した点についてかいつまんで書いていきます。
投稿中のコードはプロジェクトからの抜粋ですので、それだけでは動きません。
アウトライン(輪郭)
ぶっちゃけ、ここのサイトに全て書いてあります。自分では特に工夫してません。
UE4 誰でもわかるアウトライン入門
輪郭を描く原理は、原色マテリアルを適用した、もう1つのメッシュを重ねて描画するというものです。
ただ重ねただけだと被さってしまうので、法線を反転して内側が描画されるようにします。
カメラ正面の面は法線がカメラの方を向いていないので描画されません。

原色マテリアルのBPは参考サイトとほぼ同じです。
パラメータを若干調整しています。
UCLASS(minimalapi)
class ACharacterEx: public ACharacter
{
public:
	UPROPERTY()
		class UPoseableMeshComponent* OutlineMesh;
	void SetSelected(bool Select);
}
プロパティとしてUPoseableMeshComponentを宣言しています。
また、選択時に輪郭を表示するためのメソッドを宣言しています。
AcharacterEx::AcharacterEx()
{
	ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshAsset(TEXT("SkeletalMesh'/Game/Mesh/Panda.Panda'"));
	ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("Material'/Game/Mesh/Outline_Material.Outline_Material'"));
	GetMesh()->SetSkeletalMesh(MeshAsset.Object);
	OutlineMesh = CreateDefaultSubobject<UPoseableMeshComponent>(TEXT("OutlineMesh"));
	OutlineMesh->SetSkeletalMesh(MeshAsset.Object);
	OutlineMesh->SetMaterial(0, MaterialAsset.Object);
	OutlineMesh->SetMaterial(1, MaterialAsset.Object);
	OutlineMesh->SetMaterial(2, MaterialAsset.Object);
	OutlineMesh->SetMaterial(3, MaterialAsset.Object);
	OutlineMesh->SetVisibility(false);
}
void AcharacterEx::SetSelected(bool Select)
{
	Selected = Select;
	OutlineMesh->SetVisibility(Select);
}
CreateDefaultSubobjectで初期化した後、ロードした本体と同じメッシュ、原色マテリアルをそれぞれ設定してあげるだけ。
選択された際に表示したいので、最初は非表示にしてます。
SetSelectedメソッドで輪郭メッシュの描画有無を切り替えられます。
アニメーションの同期
アニメーションした際、本体メッシュと輪郭メッシュが同期するようにUPoseableMeshComponentを使用します。
このクラスはCopyPoseFromSkeletalComponentというメソッドを持っていて、本体のスケルタルメッシュを引数として渡してあげると、勝手に現在のポーズをコピーしてくれます。
これをTickで呼び出せば同期してくれるという仕組みです。
ただし、コピー先は1フレーム遅れて描画されるので、速い動きの場合は残像っぽくなってしまいます。
void UAnimInstanceEx::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);
	ACharacterEx* _Pawn = Cast<ACharacterEx>(TryGetPawnOwner());
	if (_Pawn)
	{
		_Pawn->OutlineMesh->CopyPoseFromSkeletalComponent(_Pawn->GetMesh());
	}
}
以前、作成したAnimInstanceの派生クラスのNativeUpdateAnimationをオーバーライドしてCopyPoseFromSkeletalComponentを呼んであげます。
描画に関してはこれだけです。簡単ですね。
マウスによる選択
入力の基本はマウスボタンと移動、あとついでにカメラ移動用のホイール操作をマッピングしてあげるだけ。
これも参考サイトに載ってる通りにやるとできてしまいます。
UnrealC++ でキーをアサインする方法、VR(Vive)版
目的が若干違いますが、やることは同じです。
UCLASS(config=Game)
class APawnEx : public APawn
{
	virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
	void OnLeftMouseButtonPress();
	void OnLeftMouseButtonRelease();
	void OnRightMouseButtonPress();
	void OnRightMouseButtonRelease();
	void OnMouseAxisX(const float X);
	void OnMouseAxisY(const float Y);
	void TriggerWheel(const float Delta);
}
SetupPlayerInputComponentをオーバーライドして、マッピングしたイベントのハンドラを宣言します。
void APawnEx::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	check(PlayerInputComponent);
	UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("Pause", EKeys::Pause));
	UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("LeftMouseButton", EKeys::LeftMouseButton));
	UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MouseWheel", EKeys::MouseWheelAxis));
	UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MouseMoveX", EKeys::MouseX));
	UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MouseMoveY", EKeys::MouseY));
	PlayerInputComponent->BindAction("Pause", IE_Pressed, this, &APawnEx::OnPause);
	PlayerInputComponent->BindAction("LeftMouseButton", IE_Pressed, this, &APawnEx::OnLeftMouseButtonPress);
	PlayerInputComponent->BindAction("LeftMouseButton", IE_Released, this, &APawnEx::OnLeftMouseButtonRelease);
	PlayerInputComponent->BindAxis("MouseWheel", this, &APawnEx::TriggerWheel);
	PlayerInputComponent->BindAxis("MouseMoveX", this, &APawnEx::OnMouseAxisX);
	PlayerInputComponent->BindAxis("MouseMoveY", this, &APawnEx::OnMouseAxisY);
}
void APawnEx::OnLeftMouseButtonPress()
{
	UE_LOG(LogTemp, Log, TEXT("OnLeftMouseButtonPress"));
	if (APlayerControllerEx* PC = Cast<APlayerControllerEx>(GetController()))
	{
		PC->HandleLeftMouseButtonDown();
	}
}
void APawnEx::OnLeftMouseButtonRelease()
{
	UE_LOG(LogTemp, Log, TEXT("OnLeftMouseButtonRelease"));
	if (APlayerControllerEx* PC = Cast<APlayerControllerEx>(GetController()))
	{
		PC->HandleLeftMouseButtonUp();
	}
}
void APawnEx::OnMouseAxisX(float Delta)
{
	if (APlayerControllerEx* PC = Cast<APlayerControllerEx>(GetController()))
	{
		// PC->HandleMouseMove();
		// 追記:2020/8/8
		PC->HandleMouseMoveX();
	}
}
void APawnEx::OnMouseAxisY(float Delta)
{
	if (APlayerControllerEx* PC = Cast<APlayerControllerEx>(GetController()))
	{
		// PC->HandleMouseMove();
		// 追記:2020/8/8
		PC->HandleMouseMoveY();
	}
}
void APawnEx::TriggerWheel(float Delta)
{
	if (APlayerControllerEx* PC = Cast<APlayerControllerEx>(GetController()))
	{
		PC->HandleWheel(Delta);
		WheelDelta = Delta;
	}
}
ひたすらマッピングします。
そして、イベントハンドラではプレイヤーコントローラのメソッドを呼んでいるだけです。
UCLASS()
class APlayerControllerEx : public APlayerController
{
	GENERATED_BODY()
public:
	APlayerControllerEx();
	void HandleWheel(float Delta);
	void HandleLeftMouseButtonDown();
	void HandleLeftMouseButtonUp();
	void HandleMouseMove();
	AActor* SelectActor(ECollisionChannel Channel);
	UPROPERTY()
		ACameraActor* WorldCamera;
	UPROPERTY()
		float CameraZ = 1000.0f;
};
イベントハンドラから呼ばれるメソッドの宣言が主です。
APlayerControllerEx::APlayerControllerEx()
{
	bShowMouseCursor = true;
	bEnableClickEvents = true;
	bEnableTouchEvents = true;
	DefaultMouseCursor = EMouseCursor::Crosshairs;
}
コンストラクタは生成されたコードそのままですね。
// 追記:2020/8/8
void APlayerControllerEx::BeginPlay()
{
	Super::BeginPlay();
	SetInputMode(FInputModeGameAndUI());
}
SetInputModeでInput Modeを変更してあげることでマウス移動のDeltaが毎フレーム返ってきます。
マウスカーソルの動きを滑らかにできます。
ただし、マウスボタン押下時にマウスカーソルが消えるので、別途表示が必要です(未対応)。
void APlayerControllerEx::HandleWheel(float Delta)
{
		FConstCameraActorIterator Iterator = GetWorld()->GetAutoActivateCameraIterator();
		WorldCamera = Iterator->Get();
		CameraZ -= Delta * 100.f;
		FVector Location = FVector(-2000.f, 0.f, CameraZ);
		WorldCamera->SetActorLocation(Location);
}
カメラを取得して、SetActorLocationしてますが、なんか違う気がします(笑)。
でも動きます。
void APlayerControllerEx::HandleLeftMouseButtonDown()
{
	FVector2D Position;
	GetMousePosition(Position.X, Position.Y);
	AHUDEx* HUD = Cast<AHUDEx>(GetHUD());
	if (HUD)
	{
		HUD->LeftMouseButtonDown = true;
		HUD->StartMousePosition = Position;
		HUD->EndMousePosition = Position;
	}
	for (TActorIterator<ACharacterEx> ActorItr(GetWorld()); ActorItr; ++ActorItr)
	{
		ActorItr->SetSelected(false);
	}
}
左マウスボタンを押したときの処理です。
キャラクターを範囲選択できるようにしたので、ドラッグ開始に当たるこのイベントで、マウスカーソルの位置をHUDに設定しておきます。
また、前回の選択したキャラクターを全て選択解除します。
void APlayerControllerEx::HandleLeftMouseButtonUp()
{
	AHUDEx* HUD = Cast<AHUDEx>(GetHUD());
	if (HUD)
	{
		HUD->LeftMouseButtonDown = false;
		if (APlayerControllerEx* Controller = Cast<APlayerControllerEx>(this))
		{
			FVector2D ScreenLocationTop;
			FVector2D ScreenLocationBottom;
			for (TActorIterator<ACharacterEx> ActorItr(GetWorld()); ActorItr; ++ActorItr)
			{
				FVector WorldLocationTop = ActorItr->GetMesh()->GetRelativeLocation() + ActorItr->GetCharacterMovement()->GetActorLocation();
				FVector WorldLocationBottom = ActorItr->GetMesh()->GetRelativeLocation() + ActorItr->GetCharacterMovement()->GetActorLocation();
				WorldLocationTop.Z = 50.f;
				WorldLocationBottom.Z = 450.f;
				float StartPositionX = HUD->StartMousePosition.X < HUD->EndMousePosition.X ? HUD->StartMousePosition.X : HUD->EndMousePosition.X;
				float StartPositionY = HUD->StartMousePosition.Y < HUD->EndMousePosition.Y ? HUD->StartMousePosition.Y : HUD->EndMousePosition.Y;
				float EndPositionX = HUD->StartMousePosition.X > HUD->EndMousePosition.X ? HUD->StartMousePosition.X : HUD->EndMousePosition.X;
				float EndPositionY = HUD->StartMousePosition.Y > HUD->EndMousePosition.Y ? HUD->StartMousePosition.Y : HUD->EndMousePosition.Y;
				Controller->ProjectWorldLocationToScreen(WorldLocationTop, ScreenLocationTop);
				Controller->ProjectWorldLocationToScreen(WorldLocationBottom, ScreenLocationBottom);
				if (StartPositionX < ScreenLocationTop.X && (StartPositionY < ScreenLocationTop.Y || StartPositionY < ScreenLocationBottom.Y)
					&& EndPositionX > ScreenLocationTop.X && (EndPositionY > ScreenLocationTop.Y || EndPositionY > ScreenLocationBottom.Y))
				{
					ActorItr->SetSelected(true);
					//ActorItr->Selected = true;
					UE_LOG(LogTemp, Display, TEXT("%s"), *ActorItr->GetName());
				}
			}
		}
		if (HUD->StartMousePosition.X == HUD->EndMousePosition.X
			&& HUD->StartMousePosition.Y == HUD->EndMousePosition.Y)
		{
			AActor* Actor = SelectActor(ECollisionChannel::ECC_Pawn);
			if (Actor && Actor->IsA(ACharacterEx::StaticClass()))
			{
				UE_LOG(LogTemp, Display, TEXT("%s"), *Actor->GetClass()->GetName());
				Cast<ACharacterEx>(Actor)->SetSelected(true);
			}
		}
		SetMouseLocation(HUD->EndMousePosition.X, HUD->EndMousePosition.Y);
	}
}
左マウスボタンを離したときの処理です。
ドラッグ終了に当たるので、ワールド内のキャラクターの足元と頭のてっぺんあたりのワールド座標2点を、スクリーン座標に変換してドラッグ範囲内にいるかどうか判定しています。
判定の際、小さい方が開始点になるように三項演算子で入れ替えを行っています。
ほんとはActorクラスの型も判定しないといけないのですが、忘れました。
最後に、ドラッグではなかった場合(ドラッグ開始点=終了点)、クリックした座標にあるキャラクターを選択しています。
ここではちゃんと型チェックをして落ちないようにしています。
追記:2020/8/8
最後のキャラクター選択の後、カーソル位置にDeltaの移動量を反映しています。
/*
 * 廃止:2020/8/8
void APlayerControllerEx::HandleMouseMove()
{
	FVector2D Position;
	GetMousePosition(Position.X, Position.Y);
	AHUDEx* HUD = Cast<AHUDEx>(GetHUD());
	if (HUD)
	{
		HUD->EndMousePosition = Position;
	}
}
*/
マウス移動した際、イベントハンドラのDeltaの加算だとうまくいかなかったので、座標を取得して終了点に設定しています。
InputModeを変更してDeltaで選択範囲を動かします。
// 追記:2020/8/8
void APlayerControllerEx::HandleMouseMoveX(float Delta)
{
	AHUDEx* HUD = Cast<AHUDEx>(GetHUD());
	if (HUD)
	{
		HUD->EndMousePosition.X += Delta * 15.f;
	}
}
// 追記:2020/8/8
void APlayerControllerEx::HandleMouseMoveY(float Delta)
{
	AHUDEx* HUD = Cast<AHUDEx>(GetHUD());
	if (HUD)
	{
		HUD->EndMousePosition.Y += Delta * -15.f;
	}
}
AActor* APlayerControllerEx::SelectActor(ECollisionChannel Channel)
{
	if (UCameraComponent* OurCamera = GetViewTarget()->FindComponentByClass<UCameraComponent>())
	{
		FVector Dir;
		FVector Start;
		FVector End = Start + (OurCamera->GetComponentRotation().Vector() * 8000.0f);
		DeprojectMousePositionToWorld(Start, Dir);
		End = Start + (Dir * 8000.0f);
		FHitResult HitResult;
		GetWorld()->LineTraceSingleByObjectType(HitResult, Start, End, FCollisionObjectQueryParams(Channel));
		UE_LOG(LogTemp, Display, TEXT("%0.0f, %0.0f, %0.0f"), HitResult.Location.X, HitResult.Location.Y, HitResult.Location.Z);
		return HitResult.Actor.Get();
	}
	return nullptr;
}
これも参考サイト見て作ったのですが、どこだか忘れました。
クリック時にマウス座標からのラインとキャラクターのあたり判定をしています。
UCLASS(minimalapi)
class AHUDEx : public AHUD
{
	GENERATED_BODY()
public:
	virtual void DrawHUD() override;
	UPROPERTY()
		bool LeftMouseButtonDown = false;
	UPROPERTY()
		bool RightMouseButtonDown = false;
	UPROPERTY()
		FVector2D StartMousePosition;
	UPROPERTY()
		FVector2D EndMousePosition;
};
変数とDrawHUDのオーバーライドを宣言します。
void AHUDEx::DrawHUD()
{
	Super::DrawHUD();
	if (LeftMouseButtonDown)
	{
		Draw2DLine(StartMousePosition.X, StartMousePosition.Y, EndMousePosition.X, StartMousePosition.Y, FColor::Green);
		Draw2DLine(EndMousePosition.X, StartMousePosition.Y, EndMousePosition.X, EndMousePosition.Y, FColor::Green);
		Draw2DLine(EndMousePosition.X, EndMousePosition.Y, StartMousePosition.X, EndMousePosition.Y, FColor::Green);
		Draw2DLine(StartMousePosition.X, EndMousePosition.Y, StartMousePosition.X, StartMousePosition.Y, FColor::Green);
	}
}
ただ線を引いているだけです(笑)
出来上がり
感想
今回も色々なサイトにお世話になりました。先達に感謝。
また進捗あれば投稿します。
