Gun.cpp
AGun::AGun()
{
// 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;
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(Root);
//Gun의 root component를 만든다
Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(Root);
//gun의 root component에 Gun의 skeletal mesh를 child object로 상속시켜준다
}
Gun은 여러가지 가있고 Character은 여러가지 Gun을 변경해가며 들수 있어야 하기 때문에 독립적인 actor로 만들어진다.
위같이 구현해주면 skeletalmesh에 다른 Gun의 skeletalmesh를 넣어주면 쉽게 총을 변경해줄수 있다.
ShooterCharacter.h
UPROPERTY(EditDefaultsOnly)
TSubclassOf<AGun> GunClass; //blueprint to choose AGun subclasses
UPROPERTY()
AGun* Gun;
ShooterCharacter.ccp
void AShooterCharacter::BeginPlay()
{
Super::BeginPlay();
Health = MaxHealth;
Gun=GetWorld()->SpawnActor<AGun>(GunClass);
GetMesh()->HideBoneByName(TEXT("weapon_r"), EPhysBodyOp::PBO_None); //hide original weapon character was holding
Gun->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("WeaponSocket")); //attach to socket
Gun->SetOwner(this); //set gun's owner this
}
SpawnActor를 사용하면 actor이 맵 어딘가에 생성된다.
하지만 우리가 원하는 것은 character가 actor을 들고있는것이기 때문에
Character에 자식 mesh로 원래있던 총은 HideBoneByName 을 사용해 숨겨주었다.
그리고 우리가 spawn한 Gun을 AttachToComponent을 사용해 WeaponSocket에 붙여준다.
Character의 손에있는 bone에 WeaponSocket을 붙여주면 charater가 뛰거나 총을쏘면서 손을 움직이면 총이 같이 움직일수 있게된다.
마지막으로 setowner을 통해 Gun의 owner을 Character로 설정한다.
(추후에 damage나 여러가지 설정할때 owner을 설정할 필요가 있다)
Gun.ccp
AController* AGun::GetOwnerController() const
{
APawn* OwnerPawn = Cast<APawn>(GetOwner()); //get owner of the gun because ratracing should start from controller not from charater at third view
if (OwnerPawn == nullptr) return false;
return OwnerPawn->GetController();
}
bool AGun::GunTrace(FHitResult& Hit, FVector& ShotDirection)
{
AController* OwnerController = GetOwnerController(); //GetOwnerController() 호출, Character의 Controller를 받아온다
if (OwnerController == nullptr) return false;
FVector Location;
FRotator Rotation;
OwnerController->GetPlayerViewPoint(Location, Rotation);
ShotDirection = -Rotation.Vector();
FVector End = Location + Rotation.Vector() * MaxRange;
//LineTrace
//AI hitself bug fix
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
Params.AddIgnoredActor(GetOwner());
return GetWorld()->LineTraceSingleByChannel(Hit, Location, End, ECollisionChannel::ECC_GameTraceChannel1, Params); //param 추가 버그픽스
}
총알이 나가는 경로를 계산하기 위해서 라인트레이싱을 한다.
third person view이기때문에 캐릭터 기준이 아닌 카메라 기준으로 라인트레이싱 하기위해 GetOwnerController() 함수에서 GetOwner을 통해 owner인 Character을 가져온다.
그리고 GetController()를 통해 Character의 controller를 return해준다.
GetPlayerViewPoint 를 통해 Controller의 Location정보와 Rotation 정보를 받아온다
FVector End = Location + Rotation.Vector() * MaxRange;
Location에 controller의 rotation쪽으로 vector로 변환해 최대 사정거리인 maxrange를 곱하면 linetracing이 끝나는 지점인 maxrange를 얻어올수 있다.
ShotDirection은 총알을 맞은 입장에서의 벡터이기때문에 -Rotation.Vector() 으로 값을 넣어준다
return값으로는 LineTraceSingleByChannel함수에 위에서 구한 Hit, location과 end를 인자값으로 넣어 단일 히트 결과를 반환해준다.
Hit에는 맞은 actor를 반환
ShooterCharacter.cpp
void AShooterCharacter::Shoot()
{
Gun->PullTrigger();
}
Gun.cpp
void AGun::PullTrigger()
{
//particle effect attach
UGameplayStatics::SpawnEmitterAttached(MuzzleFlash, Mesh, TEXT("MuzzleFlashSocket")); //spawn shooting particle effect
UGameplayStatics::SpawnSoundAttached(MuzzleSound, Mesh, TEXT("MuzzleFlashSocket")); //
FHitResult Hit;
FVector ShotDirection;
bool bSuccess = GunTrace(Hit, ShotDirection);
if (bSuccess) {
//DrawDebugPoint(GetWorld(), Hit.Location, 20,FColor::Red, true);
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactEffect, Hit.Location, ShotDirection.Rotation()); //총 맞은지점에 파티클
UGameplayStatics::PlaySoundAtLocation(GetWorld(), ImpactSound, Hit.Location); //총 맞은 지점에 소리
AActor* HitActor = Hit.GetActor();
if (HitActor != nullptr) { //send damage to actor.
FPointDamageEvent DamageEvent(Damage, Hit, ShotDirection, nullptr);
AController* OwnerController = GetOwnerController();
HitActor->TakeDamage(Damage, DamageEvent, OwnerController, this);
}
}
}
Character 에서 호출되는 pulltriger의 구현 방식이다.
Character 클래스에서 Shoot함수는 왼쪽 마우스클릭에 바인딩 되어있어 왼쪽 클릭으로 pulltriger을 호출할수 있다.
pulltriger 는 우리가 위에 구현한 GunTrace를 호출해 linetrace가 사거리안에서 actor를 만났는지 그리고 Hit값과 ShotDirection값을 받아온다. 여기서 c++에 레퍼런스를 사용하는 이유를 알수있다.
hit과 ShotDirection 값으로 소리와 이펙트를 생성하고
만약 actor를 만났다면 actor의 TakeDamage 함수에 Damage값과 DamageEvent, 총을 들고있는 Controller 그리고 데미지를 준 주체인 Gun을 같이 넘겨준다.
(DamageEvent 는 여러가지 데미지 타입을 절정할때 필요하다 우리는 총에의해 어떤 방향에서 어느부분에 맞았다빡에 없기때문에 FPointDamageEvent를 사용한다. 그외에 그냥 데미지를 전달하는 ApplyDamage와 광역데미지를 입었을때의 ApllyRadialDamage등이 있다.)
ShooterCharacter.ccp
float AShooterCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float DamagetoApply = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
DamagetoApply = FMath::Min(Health, DamagetoApply); //남은 Health와 Damage중 작은값을 구해
Health -= DamagetoApply; //Health에서 빼준다 ->Health가 음수값이 되는것을 방지한다
UE_LOG(LogTemp, Warning, TEXT("health left %f"), Health);
return DamagetoApply;
}
총을 맞은 Character쪽에서 Health에서 맞은 데미지만큼 빼준다.
이때 TakeDamage는 virtual method이다
virtual method는 이미 unreal에 구현된 method를 내가 구현한 클래스에어 override하여 확장성있게 사용하기 위함이다.
나중에 이 override된 함수가 호출 될때마다 죽었는지 확인하고 죽은 character의 capsule을 삭제하는등 추가 기능을 확장할것이다.