UE5FPS(4) - Enemy

JUSTICE_DER·2023년 3월 7일
0

🌵UNREAL

목록 보기
25/42
post-thumbnail

Unreal Engine & C++ Tutorial - 1st Person Shooter Game
https://www.youtube.com/watch?v=4HoJIgyclZ4

코드를 막 작성해서 따라치긴하는데
정확히 어떤 원리로 사용이 되는건지 모르겠다.
설명된 문서도 많지 않아서 직접 ctrl로 코드로 타고 들어가서 봐야하는 경우가 대다수다.

그냥 언리얼 유저끼리의 암묵적인 합의인건지
똑같은 구문의 코드를 똑같은 기능을 위해 똑같이 사용하고 있다.

그리고 해당 예약어는 거의 다른곳에서 쓰이지 않는다. (현재까지의 미미한 경험으로 보았을때)
그러니까 특정 기능을 위해서만 존재하는 예약어 같은 느낌도 들고,

언리얼이 거의 모든 것이 만들어져있다고 하는데,
이 만들어진 것을 또 서로 조합해야만 특정 기능을 만들 수 있고,
물약의 연금술도 아니고 개인이 어떻게 찾아서 응용까지 할 수 있을지가 초보자 입장에선 아직 모르겠다.

특히 가장 불편하다고 생각되는 것은 UPROPERTY의 조건을 적는 부분.
오타라도 나면 오류가 발생하는데, 자동완성도 없고, 오류 밑줄도 쳐지지 않는다.


Enemy

  • FPSEnemy라는 CPP Character 클래스를 만든다.
    그리고 다음과 같이 Collider변수와 OnHit함수를 만들어둔다.

  • BP로 해당 클래스를 만들고, 예제 매쉬를 넣고, 스케일을 적절히 수정해준다.
    굳이 CPP에서 하지 않는 이유는 GUI가 없어서 정확한 값을 모르기 때문일 것 같다.

  • 잘 보면 캐릭터가 반투명한데, MAT파일을 Masked로 바꾸면 된다.

  • Content - EnemyAnimations라는 폴더를 만들고,
    Animation을 Blend 1D로 추가한다.

  • 보면 Skeleton을 모두 표시하는데, 예제의 Vampire Skeleton으로 추가한다.

  • 가로축을 Speed라고 이름짓고,
    스켈레톤에 적용할 수 있는 미리 만들어진 애니메이션
    Idle과 Runnig을 양끝에 배치한다.
    ctrl을 눌러서 서서히 변하는 것을 확인할 수 있다.

  • 애니메이션 BP를 만들고, stateMachine을 연결한다.

speed에 맞게 움직이도록 간단하게 설정한다.

  • 그리고 해당 BP를 반드시 Enemy캐릭터에서 연결을 시켜준다.

AI

  • 시작하기전에 Build파일에서 AIModule을 추가해준다.

  • 그리고 아래의 코드를 Enemy 헤더에 추가한다.

  • UAIPerceptionComponent는 액터에게 보거나 듣거나이런 감각을 다루는 클래스라고 한다.
    Sight는 그 중 시각을 담당하는 클래스이고,
    BaseLocation이라는 FVector는 감지후에 플레이어가 적으로부터 벗어나면 돌아갈 곳을 의미한다.

  • 추가로 관련 변수들을 추가한다.

  • CPP에 헤더 2차추가부분을 추가한다.

  • AIPerception객체를 생성한다. Sight객체도 생성한다.
    이중에 PeripheralVisionAngle부분이 특이한데,
    시야각을 의미하는듯 하다.
    Radius와 조합되면 부채꼴의 형태로 캐릭터가 감지되는듯하다.
    0.1초뒤에 탐색된 것을 잊어버리게하는데, 그래도 범위내에 있다면 계속 다시 파악하게 될 것이다.

  • Sight의 설정을 일일이 하고, AIPerception객체에게 전부 넘긴다.
    다른 변수들도 초기화한다.
    현재 Velocity를 일단 0으로, (애니메이션에 쓰일 변수같다)
    속도값은 375로, 그리고 적과 캐릭터 사이의 거리를 그냥큰 수로 둔다.

  • 위까지가 생성자였고,
    BeginPlay에는 아래처럼 탐색이 끝나고 다시 돌아갈 Base위치를,
    처음 시작할때의 위치로 세팅한다.


Enemy Movement

  • 우선 방향을 정하는 함수를 작성한다.
    현재 Actor인 Enemy의 방향을 Target을 보도록하는 Rotator를 구하여
    현재 Actor의 방향으로 설정한다.

  • 아래와 같이 작성하게 된다.
    세부기능은 주석으로 적어놓았고,
    큰 기능을 보면, UpdatedActor 배열에 들어간 오브젝트마다 해당 작업을 반복하는데,
    해당 작업이란, 오브젝트의 위치정보를 읽어들여 속도(방향포함)와 보는 방향을 정하는 것이다.
    위치정보를 끝까지 못읽으면?? 다시 BaseLocation으로 원래대로 돌아간다.
    WasSuccessfullySensed()라는게 무슨 역할인지 쳐도 정확히 나오질 않는다.

  • Tick이라는 Update문에 작성한 코드.
    기본적으로 NewLocation이라는 곳으로 deltatime마다 이동하게 되어있다.

BackToBase플래그 값이 true라면, base로 돌아와야 하는 코드이다.

그런데 의문점이 굉장히 많다.


의문점

  1. OnSensed는 사용자 지정 함수이고, Tick에 넣지도 않았고, 호출하는 다른 함수도 없는데 어떻게 Enemy가 움직일까?
  2. DistanceSquared의 if문이 굳이 필요한 이유는 뭘까?
  3. BackToBaseLocation이 true인 경우, NewLocation으로 이동하게 되어있는데, CurrentVelocity가 0으로 초기화되며 그냥 멈추는 건가?

해결?

  1. 해당 AddDynamic구문으로, AIPerComp가 업데이트 될 때마다 호출되는 듯.
    즉 해당기능이 적용된 액터의 감각기관중 시각에 따라,
    해당 시각 반경에 다른 오브젝트가 있다면 해당 OnSensed함수가 호출되는 원리같다.
[생성자]
	// AIPerComp에 가장 지배적인 감각을 시각으로 둔다.
AIPerComp->SetDominantSense(SightConfig->GetSenseImplementation());
AIPerComp->OnPerceptionUpdated.AddDynamic(this, &AFPSEnemy::OnSensed);
// Base에서 New까지의 거리의 제곱이 DistanceSquared보다 작다면,
// DistanceSquared를 Base에서 New까지의 거리의 제곱으로 둔다. // 사실상 초기화.
// 굳이 필요한가 싶은 코드.
			if ((NewLocation - BaseLocation).SizeSquared2D() < DistanceSquared) {
				DistanceSquared = (NewLocation - BaseLocation).SizeSquared2D();
			}
			
// DistanceSquared보다 크거나 같다면, 
// CurrentVelocity를 탐지하기 전인 0으로 초기화하고, DistanceSquared도 초기화하고, BackToBaseLocation도 초기화.
// 하지만 NewLocation은 남아있음.
// SetNewRotation으로 현재위치에서 정면을 보도록?함? // 왜 Base를 보도록 하지 않지?
			else 
			{
//GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Blue, TEXT(" Im Going Home"));

				CurrentVelocity = FVector::ZeroVector;
				DistanceSquared = BIG_NUMBER;
				BackToBaseLocation = false;

				SetNewRotation(GetActorForwardVector(), GetActorLocation());
			}
		}
		// NewLocation으로 액터를 이동
		SetActorLocation(NewLocation);
	}

우선 DistanceSquared의 if문이 없어도 된다고 생각한 이유는,
DistanceSquared의 생성자에서 초기화된 값이 BIG_NUMBER였고,
그러면 첫번째 if문은 항상 통과한다고 생각했고,
그 이후 if로 들어가는 경우 없이 바로 else로 직행한다고 생각했는데,
생각을 다시해보니 아니었다.
Tick과 OnSensed가 병렬로 동시에 동작하고 있는 느낌이다.


원리가
해당 Enemy객체의 시야에 다른 Actor가 인식이 될때마다, OnSensed가 동작하고,

OnSensed에서는 감지한 Actor를 바라보는 방향벡터에,
MovementSpeed를 곱하여 CurrentVelocity라는 이동할 방향과 값을 나타내는 벡터값을 계산하여 만들어두고,
Actor를 바라보도록 설정한다.

Tick은 현재 Enemy 객체의 CurrentVelocity가 0이 아니면 작동한다.
현재 Enemy객체의 위치에 CurrentVelocity방향,거리값을 더하여 NewLocation이라고 계산해둔다.
그리고 해당 NewLocation의 위치로 Enemy객체의 위치를 옮긴다.

// 0.1f뒤에 탐색된 것을 잊어버리게 함.  0이면 절대 잊어버리지 않음.
SightConfig->SetMaxAge(0.1f);

이 때, 계속 탐색범위내에 있다면, 0.1초마다 OnSensed가 동작할 것이고,
그 말은, 0.1초마다 CurrentVelocity값이 바뀐다는 것이다.
그 말은, 프레임마다 Tick의 NewLocation값도 바뀐다는 것이다.

즉, 1 프레임마다 Enemy객체가 이동하기로 Set되는 Location값이 다르다는 것이다.

그리고 만약 OnSensed에서 감지했으나, 사라져서 Info를 끝까지 읽지못한 경우라면,
Base를 바라보는 방향벡터에 MovementSpeed를 곱하여 CurrentVelocity로 계산한다.
그리고 BackToBaseLocation을 true로 설정한다.
Base를 바라보도록 설정한다.

BackToBaseLocation값이 true라면,
Tick에서는 if문을 타고 계속 들어가게 되고,

if((NewLocation - BaseLocation).SizeSquared2D() < DistanceSquared)

해당 구문을 만나게 되는데,
NewLocation값도 계속 바뀌므로, if문을 검사할 필요가 있는 것이다.
이동해야하는 NewLocation위치값에서, BaseLocation값을 뺀 제곱값이 DistanceSquared라고 봐도 무방한데,

해당 DistanceSquared값이 이전의 값보다 줄어드는 경우라면,
New와 Base의 거리가 가까워졌다는 것을 의미하고,
이는 OnSensed에서 돌아가는 중인 경우라고 볼 수 있을 것이다.

if문을 깨고 else로 가는 방법에는 2가지 방법이 있을 것이다.

1 - 해당 DistanceSquared값이 이전의 값보다 늘어나는 경우

쫓아오게 한 후, 인식범위 외로 멀리갔다가 Base로 가기전에 다시 쫓아오게 하는 경우일 것이다.
BackToBaseLocation = false뿐만이 아니라 모든값이 초기화되어 다시 Actor를 인식하는 특이한 경우일 것이다.

2 - 해당 DistanceSquared값이 이전의 값과 같아지는 경우

NewLocation - BaseLocation의 값이 이전 DistanceSquared값과 일치하여 멈추는 경우는 없을 것이다 float형식이라서.. 라고 생각을 했는데,
어짜피 DistanceSquared도 (NewLocation - BaseLocation).SizeSquared2D()값을 가지게 될 것이고,
CurrentVelocity의 방향벡터도 Base를 보는 방향이고,
New-Base가 0과 비슷한, 어떤 특정한 값이지만 동일할테니 멈춘경우도 의미한다고 볼 수 있을 것이다.
0벡터라서 CurrentVelocity의 방향벡터가 없기에 그런건지 정확히 모르겠다.

위의 2가지 경우라면, CurrnetVelocity가 0이되기 때문에, Tick은 절대 동작하지 않게 된다.
실제로 Tick자체는 동작하지만 해당 기능밖에 없으므로 없는거나 마찬가지인 셈


오류

자꾸 따라오다가 다른쪽으로 이동하는 오류가 생겼다.
원인은 단순했다. FPSCharacter가 배치되어있었다.
HandMesh가 없는 버젼으로 정말 이전에 배치해두었던 것인데 다행히도 20분만에 빨리 해결할 수 있었다.
코드 오타 정독하는데 15분

정상적으로 따라온다.

참고한 사이트

profile
Time Waits for No One

0개의 댓글