타겟팅 시스템
가장 가까운 적 반환
- 헤더 파일 - ‘정면 각도에서 가장 가까운 적 가져오기’ 함수 선언 : ‘히트 결과들’ 레퍼런스 변수를 넣고, 내부 값이 변하지 않도록 const를 붙인다.
//header
private:
class ACharacter* GetNearlyFrontAngle(const TArray<FHitResult>& InHitResults); //전방에서 가장 가까운 적 저장
- CPP 파일 - 이전의 ‘히트 결과들’을 매개변수로 받은 ‘히트 결과들’로 바꾼다. - 이전의 플레이어의 위치를 저장한 변수를 ‘오너 캐릭터’의 ‘액터 위치 가져오기’ 함수 호출로 바꾼다. - 가장 가까운 적은 ‘타겟팅 후보’ 에 저장되어있으니 이것을 반환시킨다. - ‘정면 각도에서 가장 가까운 적 가져오기’ 함수에 ‘히트 결과들’ 을 넣어 호출한다.
//CPP
ACharacter* UCTargetComponent::GetNearlyFrontAngle(const TArray<FHitResult>& InHitResults)
{
//dot이 나올 수 있는 최저값 : -2 (원래 -1 ~ 1이 맞다.)
float angle = -2.0f;
ACharacter* candidate = nullptr; //발견된 적
for (int i = 0; i < InHitResults.Num(); i++)
{
FVector targetLocation = InHitResults[i].GetActor()->GetActorLocation(); //타겟 위치
FVector direction = targetLocation - OwnerCharacter->GetActorLocation(); //플레이어가 타겟을 바라보는 방향
direction.Normalize();
//GetControlRotation : 플레이어에 붙어있는 카메라의 회전값
FRotator rotator = OwnerCharacter->GetControlRotation(); //플레이어 카메라 회전값
FVector forward = FQuat(rotator).GetForwardVector(); //회전한 회전자의 전방벡터 값
//Dot : 변수 하나로 내적할때 사용한다.
//DotProduct : Dot과 같지만, 이건 변수가 두개일때 사용한다.
float dot = FVector::DotProduct(direction, forward);
if (dot >= angle)
{
angle = dot;
candidate = Cast<ACharacter>(InHitResults[i].GetActor());
}
}
//전방에 가까운 적 확인용
//DrawDebugLine(GetWorld(), location, candidate->GetActorLocation(), FColor::Red, true, 5);
return candidate;
}
//플레이어 전방에서 가장 가까운 적 하나 반환
ACharacter* candidate = GetNearlyFrontAngle(hitResults);
타겟팅 된 적에 파티클 발생시키기
- 파티클의 위치 조정은 소켓에서 하고, 파티클은 그냥 띄워주는 역한만 맡긴다.
- 헤더 파일 - ‘타겟팅 후보’ 변수를 받는 ‘바꾸기’ 함수를 선언한다. - 파티클을 저장할 파티클 시스템 변수 선언
//header
UPROPERTY(EditAnywhere, Category = "Settings")
class UParticleSystem* ParticleAsset;
private:
void Change(class ACharacter* InCandidate);
- CPP 파일 - ‘타겟팅 후보’에 값이 없다면 : 타겟팅을 종료한다. - ‘에셋 가져오기’ 함수에 ‘파티클 저장할 변수, 파티클 에셋 경로’를 넣어 호출한다. - ‘파티클 붙이기’ 함수에 ‘파티클이 저장된 변수, 파티클일 붙일곳, 소켓 이름, 위치 보정값, 회전 보정값, 붙일 위치 설정’를 넣어 호출한다. - 들어온 적을 ‘타겟’ 에 저장한다.
//CPP
void UCTargetComponent::Change(class ACharacter* InCandidate)
{
//만약 타겟팅 된 적이 없다면
if (InCandidate == nullptr)
{
//타겟팅을 종료한다.
End();
return;
}
//이전에 타겟팅해서 파티클이 남아있다면
if (!!Particle)
{
Particle->DestroyComponent();
}
//타겟된 적에게 파티클 띄우기
//(파티클, 붙일 캐릭터의 메쉬, 소켓 이름, 파티클 생성 보정값, 파티클 출력 보정값
Particle = UGameplayStatics::SpawnEmitterAttached(ParticleAsset, InCandidate->GetMesh(), "Targetting", FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::KeepRelativeOffset);
Target = InCandidate; //발견된 적을 타겟에 저장
}
타겟팅 된 적 바라보기 - 보간 사용
- 타겟의 상태가 Dead 모드일 경우 타겟팅이 되면 안된다.
- 오너 캐릭터와 타겟의 거리를 판단해야한다.
- 두 엑터의 거리차가 탐지 거리를 벗어나게 된다면 타겟팅을 해제할 것이다.
- 타겟을 바라볼때 확 회전하지 않고 서서히 회전해야한다. - 보간을 사용한다.
- 타겟까지의 회전값(피치 야 롤) 설명
- 피치 : 피치는 몸의 좌우 기울기이다. → 플레이어의 카메라 피치와 맞춰야한다.
- 요 : 타겟을 바라봐야하기 때문에 타깃을 바라보는 요값이 필요하다.
- 마우스를 움직여도 요값으로 계속 모이게 된다.
- 롤 : 롤은 실질적으로 쓸모가 없으므로 따라가게만 조절한다.
- 즉, 카메라의 피치값, 타겟까지의 요 ‘ 롤값
- CStateComponent
- 헤더 파일 - ‘사망 모드 확인’ 함수를 선언하고 ‘현재 상태 타입’이 ‘사망’ 상태인지 확인하는 코드를 반환하는것으로 정의한다.
//header
FORCEINLINE bool IsDeadMode() { return Type == EStateType::Dead; }
- CTargetComponent
- 헤더 파일 - ‘보간 속도’ 변수를 선언한다.
//header
UPROPERTY(EditAnywhere, Category = "Settings")
float InterpSpeed = 5.0f; //회전 보간 속도
- CPP 파일 - ‘틱 컴포넌트’ 함수 확인 - ‘타겟’이 들어왔는지 확인 - ‘타겟’의 ‘스테이트 컴포넌트’를 가져와 변수를 선언하고 저장한다. - ‘스테이트’가 들어왔는지 확인한다. - ‘스테이트’의 ‘사망 모드 확인’ 함수를 호출하여 확인한다. - ‘오너 캐릭터’의 ‘거리차 가져오기’ 함수에 ‘타겟’을 넣어 호출한 결과를 ‘거리’ 변수를 선언하고 저장한다. - ‘거리’가 ‘탐지 거리’보다 크다면(멀다면) : ‘종료’ 함수를 호출하고 끝낸다. - ‘오너 캐릭터’의 ‘컨트롤러 회전값 가져오기’ 함수를 호출하여 ‘컨트롤러 회전값’ 변수를 선언하고 저장한다. - ‘바라보는 회전값 찾기’ 함수에 ‘오너캐릭터의 엑터 위치 가져오기 함수호출, 타겟의 엑터 위치 가져오기 함수 호출’을 넣고 호출한 결과를 ‘오너에서 타겟’ 변수를 선언하고 저장한다. - ‘컨트롤러 회전값’의 ‘X 회전’ 값을 ‘오너에서 타겟’의 ‘X 회전’에 저장한다. - ‘오너 캐릭터’의 ‘컨트롤러 가져오기’에 ‘플레이어 컨트롤러’를 지정하고 호출하여 플레이어 컨트롤러 자료형의 ‘컨트롤러’ 변수를 선언하고 저장한다. - 회전하기 위한 ‘타겟의 회전값’ 변수를 선언하고 ‘컨트롤러 회전값의 피치값, 오너에서 타겟의 요값, 오너에서 타겟의 롤값’을 넣어 저장한다. - ‘회전 보간’ 함수에 ‘컨트롤 회전값, 타겟 회전값, 델타 타임, 보간 속도’를 넣어 호출한 결과(회전자)를 ‘결과’ 변수를 선언하고 저장한다. - ‘컨트롤러’의 ‘컨트롤 회전값 설정’ 함수를 호출하고 ‘결과’를 넣는다.
//CPP
CheckNull(Target); //타겟이 없으면 타겟을 바라보고있을 필요가 없다.
//타겟이 죽은상태인지 확인하기위해 상태 컴포넌트를 가져온다.
UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(Target);
CheckNull(state);
CheckTrue(state->IsDeadMode());
//GetDistanceTo : 두 엑터의 거리차를 반환하는 함수
float distance = OwnerCharacter->GetDistanceTo(Target);
//추적거리보다 탐지거리가 멀다면
if (distance > TraceDistance)
{
//타겟팅 하지 않는다.
End();
return;
}
//타겟팅 된 적 바라보게 하기 => 피치 회전은 사용하면 안된다
FRotator controlRotation = OwnerCharacter->GetControlRotation();
//오너가 타겟을 바라보는 회전값
FRotator OwnerToTarget = UKismetMathLibrary::FindLookAtRotation(OwnerCharacter->GetActorLocation(), Target->GetActorLocation());
OwnerToTarget.Pitch = controlRotation.Pitch; //피치값을 고정하기 위해 넣는다.
//OwnerToTarget.Yaw = yaw;
APlayerController* controller = OwnerCharacter->GetController<APlayerController>();
//피치와 롤은 카메라와 오너를 사용하면 되고, 요는 오너의 요를 사용한다.
//RInterpTo : 리니어 회전(SLerp)를 써서 서서히 타겟된 적을 바라보게 회전시킨다.
FRotator targetRotation = FRotator(controlRotation.Pitch, OwnerToTarget.Yaw, OwnerToTarget.Roll); //타겟의 회전값
FRotator result = UKismetMathLibrary::RInterpTo(controlRotation, targetRotation, DeltaTime, InterpSpeed);
//보간된 값 확인
CLog::Print(result, 9999);
//OwnerCharacter->GetController()->SetControlRotation(result);
//보간된 회전값 설정
controller->SetControlRotation(result);
타겟팅의 보간이 계속되지 않고 완료 되게끔 설정하기
- 보간값까지 천천히 계속 보간하는 특징이 있다.
- 성능에 문제가 되지 않도록 목표치의 근삿값에 도달하면 보간이 완료되게 설정한다.
- CTargetComponent
- 헤더 파일 - ‘도착 각도’ 변수를 설정하고 초기화한다.
//header
UPROPERTY(EditAnywhere, Category = "Settings")
float FinishAngle = 2.0f; //보간 값 근삿값
- CPP 파일 - 보간 코드 직전 확인 - ‘컨트롤 회전값’의 ‘같다’ 함수에 ‘오너캐릭터, 도착 각도’를 넣어 호출한 결과가 비슷하게 같아지면 : ‘컨트롤러’의 ‘컨트롤 회전값 설정’ 함수에 ‘오너에서 타겟’ 변수를 넣어 호출하고 반환한다.
//CPP
//보간되는 회전값이 근사치 안이라면 타겟팅 된거로 친다.
if (controlRotation.Equals(OwnerToTarget, FinishAngle))
{
controller->SetControlRotation(OwnerToTarget);
return;
}
타겟팅 목표 변경
- CTargetComponent
- 헤더 파일 - ‘이동 왼쪽’ ‘이동 오른쪽’ ‘이동’ 함수 선언
//header
void MoveLeft();
void MoveRight();
void Move(bool bRight); //타겟 변경
- CPP 파일 - ‘이동 왼쪽’ 함수 정의 - ‘이동’함수에 False를 넣어 호출한다. - ‘이동 오른쪽’ 함수 정의 - ‘이동’ 함수에 True를 넣어 호출한다.
- CPlayer
- CPP 파일 - ‘이동 왼쪽’과 ‘이동 오른쪽’ 액션을 연결한다.
//CPP
PlayerInputComponent->BindAction("Target_Left", EInputEvent::IE_Pressed, Target, &UCTargetComponent::MoveLeft);
PlayerInputComponent->BindAction("Target_Right", EInputEvent::IE_Pressed, Target, &UCTargetComponent::MoveRight);
블루프린트에서 반환 함수 사용하기
- 블루프린트 파일 - 내 블루프린트 창 - 함수 목록에서 + 버튼 클릭 - ‘반환 노드 추가’ 검색
- 또는, 디테일의 출력 항목의 + 버튼을 눌러도 된다.
- 반환 함수 노드 - 디테일 - 퓨어 : 반환 전용 노드로 바뀌게 된다.
- 이전 실행 핀이 사라지고 순수하게 반환값만 내보내는 노드로 사용할 수 있다.
- C에서는 메크로 함수를 만들때 BlueprintPure를 체크하면 퓨어 노드로 만들 수 있다.
타겟팅 = 외적
- 내적 : 두 방향벡터의 내각을 반환받는다.
- 외적 : 두 방향벡터의 수직 벡터를 반환한다.
- 방향벡터를 넣을때 순서가 중요하다.
- 왼쪽에 출발 벡터, 오른쪽에 도착 벡터를 놓는다.
- 시계방향으로 회전한다 : 위로 수직인 벡터가 생긴다.
- 반 시계 방향으로 회전한다 : 아래로 수직인 벡터가 생긴다.
- 이때, 생성되는 수직 벡터는 외적한 두 방향 벡터의 거리와 동일하다.
- 기준축 : 외적의 시작 벡터의 방향을 의미한다. (지금은 정면벡터를 기준축으로 썼다.)
- 도착 지점이 되는 타겟의 벡터 중 기준축과 평행한 축의 이동은 외적에 아무 영향도 주지 않는다. ⇒ 지금은 정면(X축)이 기준이므로, 타겟의 X축 이동은 외적에 영향을 주지 않는다.
- 반대로, 기준축에 수직인 벡터(Y축)의 이동은 수직 벡터의 길이와 1 대 1로 대응한다.
- 외적의 특징 : 시계방향 회전 : +1, 기준축 시작지점과 동일한 위치 : 0, 반시계방향 회전 : -1이다.
- 외적의 결과 출력 : X, Y, Z가 나오고, 수직 벡터의 길이와 시작 위치에서 타깃까지의 거리가 1 대 1 대응하므로 타깃의 수직벡터를 이동시키면 Z 값이 변한다. ⇒ 우측에 있을경우 +, 좌측에 있을경우 - 값이 나온다.
- 외적의 결과와 ‘Vector.Up’(0, 0, 1)을 내적한 결과 출력 : 내적은 각 축을 곱하고 나온 각 결과를 더한 값이 결과인데, X와 Y가 0이므로 Z의 결과만 출력된다. ⇒ Z의 결과만 필요하기 때문에 이렇게 내적해서 결과를 출력하는 것이다.
- 내적의 결과(Z값)이 -1인지 +1인지만 필요하다. ⇒ Sign 함수를 이용하면 +값일때는 +1이, -값일때는 -1이 출력된다.
- 타겟팅의 왼쪽 적을 찾는 방법
- 플레이어가 바라보는 타겟을 기준으로 왼쪽을 찾아야한다.
- 객체의 왼쪽은 정면 벡터로 보는게 아니라 ‘절대 좌표’에서 ‘객체의 위치’로 확인하는것이다.
- 즉, 플레이어 객체가 있을때 객체의 좌표를 기준으로 왼쪽 오른쪽을 보는것이다.