概要
こんにちは。パンダマスターと申します。
最近、UE4でゲームを作り始めました。
注記
この記事の内容はEpic Gamesとは何の関係もありません。
免責事項
当記事に記載されている内容、サンプルコードを使用することによって被った、いかなる損害に対しても筆者は一切責任を負いません。当記事のサンプルコードの使用に際しては自己責任にてお願いします。
上記に同意していただける方のみ、続きをお読みください。
続きを読むことによって、上記の免責事項に同意したことになります。
今回の目標
アタックムーブの実装。
バージョン情報
Unreal Engine 4.25.3
使用アセット
マーケットプレイスにて下記のアセットを購入し使用しています。
Lowpoly Weapons Kit
Assorted Gun FX
Gun Sound Pack
実装
早速、実装を見ていきましょう!
まだ整理された状態ではないため、コード全体ではなく苦労した点についてかいつまんで書いていきます。
投稿中のコードはプロジェクトからの抜粋ですので、それだけでは動きません。
武器の保持
これはテクニカルな部分よりも位置合わせにかなりの時間を食いました。
UCLASS(minimalapi)
class ACharacterEx: public ACharacter
{
public:
	UPROPERTY()
		class	UStaticMeshComponent* Arm;
}
武器のメッシュを置いておくプロパティを宣言します。
AcharacterEx::AcharacterEx()
{
	ConstructorHelpers::FObjectFinder<UStaticMesh> ArmMeshAsset(TEXT("StaticMesh'/Game/LowPoly_WeaponsKit/Meshes/Weapons/GSB_AssaultRifle.GSB_AssaultRifle'"));
	Arm = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Arm"));
	Arm->SetStaticMesh(ArmMeshAsset.Object);
}
コンストラクタでアセットのメッシュをロードして、コンポーネントの生成とメッシュの設定をします。
void AcharacterEx::Tick(float DeltaTime)
{
	// FVector ThumbLocation = OutlineMesh->GetBoneLocationByName("thumb_03_r", EBoneSpaces::WorldSpace);
	FVector ThumbLocation = GetMesh()->GetBoneLocation("thumb_03_r", EBoneSpaces::WorldSpace);
	// FRotator ThumbRotation = OutlineMesh->GetBoneRotationByName("thumb_03_r", EBoneSpaces::WorldSpace);
	FQuat ThumbRotation = GetMesh()->GetBoneQuaternion("thumb_03_r", EBoneSpaces::WorldSpace);
	Arm->SetRelativeLocation(ThumbLocation);
	Arm->SetRelativeRotation(ThumbRotation);
}
Tickで位置合わせをしています。
手先は動かさない(というかない)ので親指ボーンが位置的にちょうどよく、ボーン座標と回転を武器に適用しています。
OutlineMeshを使用しているのは、アニメーションしているMeshは別スレッドにあるので、取りに行くのが難しいという投稿を見かけたためです。やってみたわけではないので、取れるかもしれません。
普通にSkeletalMeshComponentから取れました。
ほんとはごちゃごちゃ角度を調整しているのですが、あまり意味のないコードなので割愛します。
キャラクターメッシュに武器用のボーンを生やして、武器メッシュの持ち手の部分が原点にくるようにモデリングすれば、それほど苦労しなくても持っているように見えると思います。
前回までのコードに上記のコードを加えると、いい感じに武器を持ってくれます。
走るモーションを変えていないので、片手で持って振り回す感じになってしまいますが(笑)
マズルフラッシュ(パーティクル)とサウンドエフェクト
パーティクルも位置合わせが面倒なだけで、ただパーティクルを生成してアクティベート、ディアクティベートを適時、実行するだけです。
サウンドはCueのブループリントを少々調整しています。
Wave PlayerのLoopingにチェックを入れ、3DサウンドにするためにAttenuation BP(デフォルト状態)を作って設定しています。
コードではパーティクルと同様に生成してON/OFFするだけです。
UCLASS(minimalapi)
class ACharacterEx: public ACharacter
{
public:
	UPROPERTY()
		class UParticleSystem* MuzzleFlash;
	UPROPERTY()
		class UParticleSystemComponent* MuzzleFlashComponent = nullptr;
	UPROPERTY()
		class USoundCue* GunSound;
	UPROPERTY()
		class UAudioComponent* Audio = nullptr;
}
パーティクルとサウンドを置いておくプロパティです。
ACharacterEx::ACharacterEx()
{
	ConstructorHelpers::FObjectFinder<UParticleSystem> MuzzleFlashAsset(TEXT("ParticleSystem'/Game/Assorted_Gun_FX/Particles/Muzzle_Flashes/Rifle/PS_MuzzleFlash_Assault_03.PS_MuzzleFlash_Assault_03'"));
	MuzzleFlash = MuzzleFlashAsset.Object;
	static ConstructorHelpers::FObjectFinder< USoundCue > GunSoundAsset(TEXT("SoundCue'/Game/GunSoundPack/Guns/gun_semi_auto_rifle_shot_04_Cue.gun_semi_auto_rifle_shot_04_Cue'"));
	GunSound = GunSoundAsset.Object;
}
ただロードしてプロパティに置くだけです。
void ACharacterEx::Tick(float DeltaTime)
{
	if (MuzzleFlashComponent)
	{
		FRotator ArmRotator = Arm->GetRelativeRotation();
		Offset = ArmRotator.RotateVector(FVector(/* いい感じの位置 */));
		MuzzleFlashComponent->SetRelativeLocation(Arm->GetRelativeLocation() + Offset);
		MuzzleFlashComponent->SetRelativeRotation(ArmRotator);
	}
}
単に位置合わせしています。
コンポーネントはAI側で生成しています。
我ながら不器用なやり方だとは思います。何かスマートな方法はないでしょうか?
void AAIControllerEx::EnableAttack()
{
	Attack = true;
	StopMovement();
	if (!OwnerChar->MuzzleFlashComponent)
	{
		OwnerChar->MuzzleFlashComponent = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), OwnerChar->MuzzleFlash, OwnerChar->Arm->GetRelativeLocation(), (OwnerChar->Arm->GetRelativeRotation().Quaternion() * FRotator(180.f, 0.f, 0.f).Quaternion()).Rotator());
		OwnerChar->MuzzleFlashComponent->bAutoDestroy = false;
	}
	if (OwnerChar->MuzzleFlashComponent && !OwnerChar->MuzzleFlashComponent->IsActive())
	{
		OwnerChar->MuzzleFlashComponent->Activate();
	}
	if (!OwnerChar->Audio)
	{
		OwnerChar->Audio = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), OwnerChar->GunSound, OwnerChar->GetCapsuleComponent()->GetRelativeLocation(), FRotator::ZeroRotator, 0.01f, 1.f, 0.f, nullptr, nullptr, true);
	}
	if (OwnerChar->Audio && !OwnerChar->Audio->IsPlaying())
	{
		OwnerChar->Audio->Play();
	}
}
Tickで敵のサーチをしていますが、敵発見時にそこから呼ばれる攻撃開始ファンクションです。
それぞれコンポーネントが未生成なら生成しています。
プレイ中にやることもないのですが、BeginPlayだと書く場所が限られるので面倒になり、こんな風にしています(改善の余地あり)。
ON/OFFは、パーティクルの場合はIsActive、サウンドの場合はIsPlayingで状態を判定した後、Activate及びPlayでそれぞれ有効化しています。
ちなみに、パーティクルはbAutoDestroy = falseしておかないと、Deactivateした際にコンポーネントも破棄されてしまうので注意が必要です。
void AAIControllerEx::DisableAttack()
{
	Attack = false;
	if (OwnerChar->MuzzleFlashComponent && OwnerChar->MuzzleFlashComponent->IsActive())
	{
		OwnerChar->MuzzleFlashComponent->Deactivate();
	}
	if (OwnerChar->Audio && OwnerChar->Audio->IsPlaying())
	{
		OwnerChar->Audio->Stop();
	}
	if (Run)
	{
		EPathFollowingRequestResult::Type result = MoveToLocation(OwnerChar->TargetLocation);
	}
}
同様にTickから呼ばれる攻撃中止ファンクションです。
EnableAttackと反対のことをしているだけです。
走っている状態で攻撃し始めた場合は攻撃範囲内に敵がいなくなった後、再び走り始めます(まだヘルスを実装していないので未テスト)。
索敵
敵に当たるキャラクターのTArrayをサーチして、自身との距離が一定未満でかつ、最も近い敵の方を向いて攻撃モーションに移ります。
void AAIController::Tick(float DeltaTime)
{
	bool FoundEnemy = false;
	APlayerControllerEX* PC = Cast<APlayerControllerEX>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
	ACharacterEx* Enemy = nullptr;
	float MinDistance = 1000.f;
	if (PC)
	{
		for (auto& ActorItr : OwnerChar->OpponentTeam->Chars)
		{
			float Distance = FVector::Distance(OwnerChar->GetCapsuleComponent()->GetRelativeLocation()
				, ActorItr->GetCapsuleComponent()->GetRelativeLocation());
			if ((AttackMove || !Run) && Distance < MinDistance)
			{
				Enemy = ActorItr;
				MinDistance = Distance;
				FoundEnemy = true;
			}
		}
	}
	if (FoundEnemy)
	{
		OwnerChar->GetCapsuleComponent()->SetRelativeRotation(FMath::Lerp(OwnerChar->GetCapsuleComponent()->GetRelativeRotation()
			, (Enemy->GetCapsuleComponent()->GetRelativeLocation() - OwnerChar->GetCapsuleComponent()->GetRelativeLocation()).GetSafeNormal().Rotation().RotateVector(FVector(0.f, -90.f, 0.f)).Rotation(), 0.6f));
		EnableAttack();
	}
	else
	{
		DisableAttack();
	}
	if (Run && !FoundEnemy)
	{
		OwnerChar->GetCapsuleComponent()->SetRelativeRotation(FMath::Lerp(OwnerChar->GetCapsuleComponent()->GetRelativeRotation(), OwnerChar->GetCharacterMovement()->Velocity.GetSafeNormal().Rotation().RotateVector(FVector(0.f, -90.f, 0.f)).Rotation(), 0.6f));
	}
}
かなり見づらいコードですが、要は自分と敵の位置の距離を測っているだけです。
そのあと、敵の位置から自分の位置を引き算してRotatorにしています。最後に-90度回転しているのですが、メッシュはY方向が正面にもかかわらず、ゲームはX方向が正面だからです。最初に調整しておきましょう(自戒)。
敵が見つかった場合はEnableAttackします。
敵が見つからなかった場合はDisableAttackして、走っている方向を向きます。
出来上がり
感想
量が増えてきて段々グダったコードになりつつあります。そのうち整理します。
モデリングとモーションに関してはかなり適当で、いかにも素人な感じですが仕方ないですね。
それでも出来上がりは嬉しいものです。
進捗あったらまた投稿します。
