no image
[ UE ] 적 공격할 때 카메라 깜빡이는 문제
오늘은 작업을 하다가 적이 캐릭터에게 공격을 할 때, 화면이 깜빡거리는 현상을 발견했다. 디버그를 해본 결과 원인은 무기 콜리젼이 스프링암 경로를 지나면서 카메라가 잠깐 적을 확대해서 보여주고 다시 원래대로 돌아가면서 깜빡이는 것처럼 보여지는 것 때문이라 생각했다. 이렇게 무기가 비스듬히 지나가면 괜찮은데 스프링암과 부딪히면서 카메라가 앞으로 확대되었다가 다시 돌아갔다.  뭔가 콜리젼 때문이라 생각하고 검색해봤는데, trace channels이라는 것을 알게되었다.  trace channels라인 트레이스나 스윕(sweep) 등의 충돌 검사 기능을 사용할 때, 어떤 객체와의 충돌을 검사할지 결정하는 기준이다.특정 객체의 충돌 검사를 할지, 말지 설정할 수 있다. 먼저 내가 사용하고 있는 Enemy 캐릭터..
2025.02.28
[ UE, C++ ] 오늘 배운 내용 정리 및 애니메이션에 대해 알게 된 꿀팁
오늘배운내용 - AI Possess할 때는 코드의 옵션과 블루프린트의 옵션이 동일해야 인식된다. - 언리얼 엔진에서는 C++의 TakeDamage가 호출될 때, 블루프린트 쪽에 Event AnyDamage가 자동으로 브로드캐스팅된다.- 적 ai 모션을 구현할 때도 Anim Instance를 만드는게 좋다- 캐릭터가 죽었는지 살았는지 변수 받아서, 적 애니메이션 블루프린트에 캐릭터가 살았는지, 죽었는지 판단해서 처리하는게 좋음 - 공격이나 그런 연속적인 호출? 느낌의 모션은 몽타주가 맞고, idle 사망 같은거는 state를 나눠서 처리하는게 좋음 - additive를 설정해줘야하는 이유 마지막 말적 사망을 구현할 때, 사망 모션이 잘 되다가 갑자기 기본 포즈로 바뀌는 현상을 발견했다.원인은 몽타주로 사망..
2025.02.26
no image
[ UE, C++ ] 적 AI 구현 및 애니메이션 추가
저번 포스팅에서는 NavigaitonSystem을 사용해서 적의 랜덤 이동을 구현했는데, 이번에 Behavior 시스템을 공부하면서 얘를 이용하는 것으로 이동 방식을 바꿔주었다.물론 NavigaitonSystem을 계속 써도 되지만 아직 코드로 접근하는 방법에 대해 익숙치 않아 에디터와 같이 사용하는게 더 이해가 잘 될 것 같기 때문이다. 먼저 BehaviorTree와 BlackBoard의 이론부터 공부했다.빠르게 구현하면서 이해해도 좋지만 그러면 뭐부터 해야할 지 꼬일 것 같기도 하고 확실하게 이해를 하고 가야 다른 예제에서 한 것들을 나에게 맞게 응용할 수 있을 것 같았다. BehaviorTreeBehaviorTree는 AI의 행동 로직을 계층적으로 구성할 수 있게 도와주는 시각적 스크립팅 도구이다...
2025.02.24
no image
[ UE, C++ ] 적 Navigation 랜덤 이동 구현하기
저번 포스팅에서는 적의 이동을 타겟포인트로 지정해주었다.하지만 그렇게 되면 내가 이동하려는 타겟포인트의 위치들을 다 지정해줘야하고 생성되는 액터와 리소스 관리가 굉장히 복잡해지고 귀찮게 될 것이다.. 그래서 오늘은 랜덤으로 이동가능한 범위 내에서 적이 순찰을 도는 부분을 구현하려고 한다.  UNavigationSystemV1오늘 사용한 것 중에 가장 중요한 것은 UNavigationSystemV1이다.월드 내에서 활성화된 내비게이션 시스템을 가져온다.그리고 이동가능한 범위를 검색할 수 있게 해준다.   GetRandomPointInNavigableRadius()다음으로는 GetRandomPointInNavigableRadius()이다.매개변수로는랜덤 위치를 찾을 기준이 되는 중심점반경랜덤 위치 찾았는지 ..
2025.02.21
no image
[ UE, C++ ] NavMeshVolume 추가 및 적 AI 추격 구현하기
언리얼 엔진에서 Navmesh와 적의 경로 탐색을 구현하는 방법에 대해 공부했다게임에서 적들이 환경을 효율적으로 탐색하고 플레이어를 추적하는 것은 사용자의 몰입감을 증가시키는데 중요한 역할을 한다고 생각한다.오늘은 Navmesh를 활용하여 적이 움직이는 경로에 대해 학습하였다. NavMesh 적용 방법1. NavMeshBoundsVolume을 추가 2. 순회를 돌 타겟 포인트들을 맵에 생성 3. 적 캐릭터 클래스 생성 (이것은Character 클래스를 상속받아 생성)4. 적 AIController 생성    Navmesh Baking 동적 구현맵을 제작하다 보면 회전하는 객체가 있을수도 있는데 얘네들을 피해가려면 동적으로 길을 계속 만들어줘야할 것이다. 동적 경로 생성은, 환경이 항상 변화하며 예측할 수..
2025.02.20
[ C++ ] 프로그래머스 로또의 최고 순위와 최저 순위
문제 링크https://school.programmers.co.kr/learn/courses/30/lessons/77484 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.co.kr  나의 풀이아웃풋으로 구해야하는 것은 결국 나의 최대 등수, 최소 등수만 구하면 된다.그래서 0의 카운트도 0이 다 맞았을 경우, 0이 다 틀렸을 경우 이렇게 2개의 상황만 추가해주면된다.- 1등부터 6등까지 몇 개를 맞추면 되는지 map을 사용해서 키, 밸류를 설정- lottos를 순회하면서 win_nums의 번호가 있는지 확인하고 있다면 최소 카운트 증가 - 그리고 0이면 0을 관리하는 카운트 증가- 순회가 끝나고 최대 갯수에 최소..
2025.02.19
no image
[ UE, C++ ] 웨이브 시스템을 활용한 간단한 게임 만들기 (2)
저번에 구현해놨던 아이템 스폰을 이어서 정리해보려고 한다.과제를 하면서 이미 구현된 부분이 많지만, 제대로 이해되지 않은 부분이나 시간 때문에 쫒겨 깊게 보지 못한 부분들을 다시 이해하고 공부한다는 마음으로 작성하였다.블루프린트도 많이 쓰고 생소한 클래스들도 많이 다뤄서 재밌었던 경험이었다.  구현해야 할 목록들- 캐릭터- 캐릭터 애니메이션- 아이템 스폰- 게임 흐름 설계- 웨이브 설계- UI 제작 아이템 스폰에서 현재 스폰될 볼륨까지 구현해놓았다.이제 구현한 기능을 연결시켜줘야한다. 아이템 랜덤 생성 구현 과정1. C++ 클래스를 상속받은 블루프린트를 만들어준다.Create Blueprint class based on "클래스이름"을 클릭하면 해당 클래스를 기반으로 한 블루프린트가 생성된다.아 참고로 ..
2025.02.18
[ UE, C++ ] 웨이브 시스템을 활용한 간단한 게임 만들기 (1)
오늘은 Wave를 구현해서 코인을 먹고 높은 점수를 쌓는 간단한 게임을 만들어서 공부하려고 한다.과제 제출도 할 겸.. 먼저 어떤 걸 해야할 지 고민해보았다. 구현해야 할 목록들- 캐릭터- 캐릭터 애니메이션- 아이템 스폰- 게임 흐름 설계- 웨이브 설계- UI 제작이렇게 크게 총 5개 정도로 나눠보았다. 캐릭터와 애니메이션은 저번 포스팅에서 공부한 내용을 그대로 가져갈 것이다.오늘은 아이템 스폰에 대해 자세하게 공부해보려고 한다. 아이템 목록- 코인- 피격 아이템- 회복 아이템일단 이렇게 3개의 종류의 아이템을 설정했다.그리고 공부하면서 만들어놨던 ItemInterface와 BaseItem 클래스 등을 가져와서 사용할 것이다.(링크 참고) 아이템의 기반이 완성되었으니, 이제 아이템을 맵에서 랜덤으로 생성..
2025.02.13
no image
[ C++ ] 프로그래머스 카드 뭉치
https://school.programmers.co.kr/learn/courses/30/lessons/159994 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.co.kr 문제  나의 풀이- goal에 있는 단어를 cards1과 cards2의 첫번째 글자와 각각 비교한다.- 비교했을 때 둘 다 없으면 No를 바로 반환해버린다.- 있으면 있는 쪽의 0번째 단어를 삭제한다.이렇게 비교하면 순차적으로 비교할 수 있으면서 깔끔하게 코드를 작성할 수 있을 것 같아 이렇게 로직을 설계하였다. 내 코드#include #include #include #include using namespace std;string soluti..
2025.02.10

오늘은 작업을 하다가 적이 캐릭터에게 공격을 할 때, 화면이 깜빡거리는 현상을 발견했다.

 

디버그를 해본 결과 원인은 무기 콜리젼이 스프링암 경로를 지나면서 카메라가 잠깐 적을 확대해서 보여주고 다시 원래대로 돌아가면서 깜빡이는 것처럼 보여지는 것 때문이라 생각했다.

 

이렇게 무기가 비스듬히 지나가면 괜찮은데 스프링암과 부딪히면서 카메라가 앞으로 확대되었다가 다시 돌아갔다.

 

 

뭔가 콜리젼 때문이라 생각하고 검색해봤는데, trace channels이라는 것을 알게되었다.

 

trace channels

라인 트레이스나 스윕(sweep) 등의 충돌 검사 기능을 사용할 때, 어떤 객체와의 충돌을 검사할지 결정하는 기준이다.

특정 객체의 충돌 검사를 할지, 말지 설정할 수 있다.

 

먼저 내가 사용하고 있는 Enemy 캐릭터 클래스에서 충돌체를 확인했고, collsion 타입을 Capsule Component는 Pawn으로 설정되어있었고, Mesh는 CharacterMesh인 것을 확인했다.

 

Edit -> Project Settings에 들어가서 Engine에 Collsion 카테고리에 들어가면 Trace Channels를 설정할 수 있는 메뉴가 나온다.

 

여기에서 내가 사용하고 있는 Pawn과 CharacterMesh는 카메라에 대한 충돌을 ignore로 설정해주었다.

 

그랬더니 화면이 깜빡거리는 현상없이 잘 나오는 것을 확인했다!

오늘배운내용


- AI Possess할 때는 코드의 옵션과 블루프린트의 옵션이 동일해야 인식된다.
- 언리얼 엔진에서는 C++의 TakeDamage가 호출될 때, 블루프린트 쪽에 Event AnyDamage가 자동으로 브로드캐스팅된다.
- 적 ai 모션을 구현할 때도 Anim Instance를 만드는게 좋다
- 캐릭터가 죽었는지 살았는지 변수 받아서, 적 애니메이션 블루프린트에 캐릭터가 살았는지, 죽었는지 판단해서 처리하는게 좋음
- 공격이나 그런 연속적인 호출? 느낌의 모션은 몽타주가 맞고, idle 사망 같은거는 state를 나눠서 처리하는게 좋음
- additive를 설정해줘야하는 이유

 

마지막 말

적 사망을 구현할 때, 사망 모션이 잘 되다가 갑자기 기본 포즈로 바뀌는 현상을 발견했다.

원인은 몽타주로 사망을 구현했기 때문인데, 콤보 공격이나 피격 같은 부분은 연속적으로 특정 이벤트가 호출될 때마다 발생할 수 있기 때문에 몽타주로 구현하는 것이 유리하다.

하지만 Idle이나 사망 모션 같은 경우에는 하나의 애니메이션이 지속되는 형태이기 때문에 이런 경우에는 State Machine을 사용하는 것이 더 효율적이라는 것을 알게되었다.

저번 포스팅에서는 NavigaitonSystem을 사용해서 적의 랜덤 이동을 구현했는데, 이번에 Behavior 시스템을 공부하면서 얘를 이용하는 것으로 이동 방식을 바꿔주었다.

물론 NavigaitonSystem을 계속 써도 되지만 아직 코드로 접근하는 방법에 대해 익숙치 않아 에디터와 같이 사용하는게 더 이해가 잘 될 것 같기 때문이다.

 

먼저 BehaviorTree와 BlackBoard의 이론부터 공부했다.

빠르게 구현하면서 이해해도 좋지만 그러면 뭐부터 해야할 지 꼬일 것 같기도 하고 확실하게 이해를 하고 가야 다른 예제에서 한 것들을 나에게 맞게 응용할 수 있을 것 같았다.

 

BehaviorTree

BehaviorTree는 AI의 행동 로직을 계층적으로 구성할 수 있게 도와주는 시각적 스크립팅 도구이다.

AI가 상황에 따라 어떤 행동을 취할지 결정하는 노드인 Select, Sequence, Decorator 등을 배치하여 복잡한 행동패턴을 눈으로 쉽게 설계할 수 있다.

 

시퀀스(Sequence)

- 자식 노드를 순서대로 실행
- 모든 자식 노드가 성공해야 이 시퀀스 자체가 성공했다고 판정됨

 

셀렉터(Selector)

- 자식 노드를 순서대로 평가
- 하나라도 성공하면 전체가 성공했다고 판정

 

데코레이터(Decorator)

- 특정 조건을 검사
- 행동을 반복하거나, 반전 등의 추가 제어를 위해 다른 노드에 부착해서 실행하는 형태

 

서비스(Service)

- BehaviorTree에 특정 브랜치가 활성화되어 있는 동안 주기적으로 실행
- Blackboard 값을 업데이트하거나 지속적인 환경 체크를 수행

 

태스크(Task)

- 실제 행동을 실행하는 가장 기본적인 노드
- 이동, 공격, 애니메이션 재생 등의 원하는 행동에 대한 블루프린트 연결로 실행
- Success, Failure, Running 상태를 반환하여 상위 노드에 전달

 

 

BlackBoard

Blackboard는 BehaviorTree를 동작하는데 필요한 데이터를 저장하는 키-값 데이터 저장소 같은 역할이다.

예시로 위치나 상태, 감지 여부 등을 기록해두면 BehaviorTree의 각 노드가 이 데이터를 참조해서 조건을 판단하고 각 행동을 실행시키는데 활용한다.

 

 

이제 이론은 이해했으니 실제 프로젝트에 적용해볼 것이다.

 

베이스 세팅

Content 폴더 밑에 AI 폴더를 만들어서 BehaviorTree와 Blackboard를 생성해주었다.
(폴더 위치는 자신의 폴더 구조에 맞게 알잘딱깔센)

 

그리고 NewKey를 눌러서 내가 사용할 값들을 추가해주었다.

 

나는 이렇게  IsOverRange와 타겟의 위치인 PlayerLocation, 시야를 벗어났는지를 확인하는 IsTargetFindRange를 추가해줬다.
(IsOverRange와 DefaultLocation은 추후 적의 이동 반경을 벗어났을 때를 구현하기 위해 미리 만들어두었으니, 굳이 생성하지 않아도 된다)

 

그리고 BehaviorTree를 켜서 만들어둔 블랙보드를 연결시켜준다.

이제 BehaviorTree에서 블랙보드의 값을 알 수 있게 되었다!

 

 

추격을 구현할 시퀀스 노드를 하나 생성하였다. (파란색은 데코레이션 노드인데 지금은 일단 무시, 이따가 추가할 것이다)

 

전체 노드는 다음과 같다.(밑에서 과정을 설명 예정)

 

그리고 태스크를 추가할 때는 상단의 New Task를 선택하면 된다.

 

 

Patrol 구현

이렇게 먼저 시퀀스를 연결시켜주고 이름은 한 눈에 파악하기 쉽게 Patrol Sequence로 변경해줬다.
(아 참고로 가장 왼쪽에 있는 move to 노드는 무시해도 된다. 지금 추가하는 내용인데 정리를 깜빡했다..ㅎㅎ)

그리고 순찰을 수행할 태스크를 하나 생성해주었다.

 

태스크 노드는 이렇게 구상하였다.

Get Random Reachable Point in Radius 라는 노드가 있어서 사용해보았다.

설정한 반경에 맞는 랜덤 포인트를 반환해주는 노드다.

그 반환값을 ai move to에 연결해주었고, 값이 잘 반환이 되었다면 Success를 반환시켜줬다.

 

그리고 자연스러운 움직임을 위해 적 AI가 움직이고 2초 동안 대기하는 Wait 노드를 추가해주었다.

 

 

Chase 구현

Chase하는 태스크를 하나 생성해주고,

 

블루프린트를 다음과 같이 연결해줬다.

플레이어 컨트롤러를 가져와서 Target Actor를 플레이어로 연결해줬다.

Acceptance Radius는 원하는 값으로 입력해주고 finish excute에서는 항상 부울 체크 확인하는 것을 잊지않기.

 

컴파일 후 저장한다 <- 이건 그냥 노드 추가할 때마다 하게 습관이 되었다..

 

이렇게 Chase 태스크를 시퀀스에 연결시켜준다.

 

 

애니메이션 몽타주

이제 공격을 구현할건데 트리를 연결하기 전에 공격 애니메이션 몽타주를 만들어주었다.

 

근데 그 전에 그냥 State Machine으로 애니메이션을 연결하면되는데 왜 몽타주를 쓸까?

State Machine은 캐릭터의 상태에 따라 애니메이션을 지속적으로 블렌딩해준다.

몽타주는 특정 애니메이션을 동적으로 재생하고 블렌딩, 인터럽트, 부분 애니메이션 등을 지원한다고 한다.

 

내가 구현할 공격같은 애니메이션은 특정 조건에서만 재생하고 돌아갈거기 때문에 몽타주를 선택했다.

 

이렇게 몽타주를 생성하고 원하는 공격 모션을 추가해준다.

자세한 내용은 점부된 블로그를 참고하면 좋을 것 같다.

https://makerejoicegames.tistory.com/382

 

언리얼엔진5 애니메이션 몽타주

https://docs.unrealengine.com/5.0/en-US/animation-montage-in-unreal-engine/ Animation Montage Animation Montages are animation assets that enable you to combine animations in a single asset and control playback using Blueprints. docs.unrealengine.com ▣

makerejoicegames.tistory.com

 

 

Attack 구현

다음으로는 공격을 구현할 것이다.

적 캐릭터 블루프린트에 들어가서 이벤트 그래프로 들어간다.

 

Attack이라는 커스텀 블루프린트를 만들고 play montage를 연결시켜준다.

 

그리고 On Completed와 On Interrupted에 Call On Attack End를 연결해주고 디버그를 위해 Print String을 연결해주었다.

 

Attack 태스크에 들어가서 다음과 같이 연결해주었다.

적 캐릭터에 있는 Attack 함수를 불러와서 실행시켜주었다.

 

 

만들어둔 공격 태스크를 behaviorTree로 가져와서 연결해줬다. (추격의 우선순위를 더 높게 잡았기 때문에 추격의 오른쪽으로 배치해줬다)

 

 

애니메이션 연결

애니메이션은 파라곤 에셋에 들어있던 블렌드 스페이스를 이용해주었다.

해당 애니메이션을 살펴보니까 Speed에 따라 idle, walk, run이 출력되는 것을 확인할 수 있었다.

그래서 speed 값만 연결시켜주고, 몽타주 슬롯을 연결해주었다.

 

몽타주 클릭해서 내가 만든 슬롯을 설정해주면 끝!

 

 

최종 Animation Graph

최종적으로 애니메이션은 다음과 같다.

 

이벤트 그래프는 언리얼의 기본 캐릭터인 마네퀸의 노드들을 그대로 가져오고 위의 부분에서 Shuld Move를 set 해주는 노드만 추가해주었다.

 

 

결과

이렇게 캐릭터가 시야에 보이지 않으면 천천히 순찰하다가 캐릭터가 시야에 들어오면 추격 후 공격하는 것을 볼 수 있다

 

 

트러블슈팅

이슈 1번 -> 공격 애니메이션 몽타주 실행안되는 현상

상황 : 블루프린트는 디버깅을 해봤을 때 모두 잘 연결되는 것을 확인했지만 공격 모션이 안뜸..

작업 순서
1. enemyCharacter에서 커스텀으로 Attack 이벤트 노드 생성(몽타주 연결해줌)
2. 생성한 Attack 이벤트 노드를 attack 태스크에 연결
3. 비헤이비어트리에 attack 태스크 연결(IsTargetFineRange)가 true이면 추격 후 공격

해결 : 적 애니메이션 그래프에 몽타주를 저장한 슬롯 연결로 해결..

 

이슈 2번 -> 'D:\UE5_project\2nd-Team9-CH3-Project\Intermediate\Build\BuildRulesProjects\HelloWorldModuleRules\..\..\..\..\Plugins\Developer\RiderLink\Source\RiderLink\RiderLink.Build.cs' 소스 파일을 찾을 수 없습니다. 라는 에러 상황

상황 : visual studio를 사용하고 있고, 다른 팀원의 깃 브랜치에 들어갔다가 아무것도 안하고 다시 내 브런치로 전환했더니 이런 에러가 떴다...

원인 : 브랜치 바꾸면서 날 수도 있고, 다른 엔진의 버전을 사용하고 있어서 그런 거일수도 있고, 원인은 다양하다고 한다.
나 같은 경우에는 라이더를 사용하고 있는 브랜치로 왔다갔다하면서 꼬인 것 같다.

해결 : 프로젝트 폴더 내에서 Intermediate 폴더, Binaries 폴더, 비주얼스튜디오 솔루션을 삭제한 뒤, 다시 솔루션을 생성하고 빌드해줬더니 해결되었다.

 

 

 

마지막 말

- 코드로 작업할 때 할 일들을 TODO로 주석처리해서 관리했는데, 이렇게 하니까 뭐부터 해야할 지 헷갈렸다.
그래서 오늘 이후부터는 우선순위를 정해서 TODO 1, TODO 2 이런식으로 넘버링해서 관리하는 것이 좋을 것 같다.

- 애니메이션 적용할 때, 기본적인 것들을 놓쳐서 시간을 많이 소모했다.. 몽타주를 만들어놓고서 연결을 안했다던지..
적의 속도로 애니메이션 상태를 바꾸는데, 애니메이션 이벤트 노드에서 이벤트 노드가 이상하게 들어가 있었다던지..
차근차근 설계한 내용대로 차분하게 보는 것이 좋을 것 같다.

 

 

저번 포스팅에서는 적의 이동을 타겟포인트로 지정해주었다.

하지만 그렇게 되면 내가 이동하려는 타겟포인트의 위치들을 다 지정해줘야하고 생성되는 액터와 리소스 관리가 굉장히 복잡해지고 귀찮게 될 것이다..

 

그래서 오늘은 랜덤으로 이동가능한 범위 내에서 적이 순찰을 도는 부분을 구현하려고 한다.

 

UNavigationSystemV1

오늘 사용한 것 중에 가장 중요한 것은 UNavigationSystemV1이다.

월드 내에서 활성화된 내비게이션 시스템을 가져온다.

그리고 이동가능한 범위를 검색할 수 있게 해준다.

 

 

GetRandomPointInNavigableRadius()

다음으로는 GetRandomPointInNavigableRadius()이다.

매개변수로는

  • 랜덤 위치를 찾을 기준이 되는 중심점
  • 반경
  • 랜덤 위치 찾았는지 여부

이렇게 들어간다.

이 함수가 true를 반환하게되면 맵 안에서 성공적으로 적 AI가 이동가능한 랜덤 위치를 찾았다고 판단할 수 있다,

 

 

 

NavModifierVolume

이미지를 보면 지나갈 수 없는 영역이 너무 작아서 적이 저기를 지나가다가 큐브와 적의 메쉬가 끼여서 적이 멈춰버리는 현상이 발생했다.

그래서 갈 수 없는 영역의 반지름을 키우고 싶어서 찾아보다가 NavModifierVolume이라는 것을 발견했다.

저 빈 네모 테두리가 NavModifierVolume이고, 큐브 옆에 작게 빈 영역이 Navmesh로 Baking된 영역이다.

처음에는 두 영역이 똑같이보여서 NavModifierVolume도 AI가 가지 못하게 막는 볼륨인 줄 알았지만 그게 아니었다.

 

NavModifierVolume은 이동가능한 영역을 판별할 때의 우선순위를 지정할 수 있게 해주는 것이다. 

이게 무슨 말이냐면 만약 맵에 갈 수 있는 영역이 이 NavModifierVolume 밖에 없을 때, Null로 지정해도 AI가 그 영역으로 다니기는 한다는 소리이다.

 

디테일 패널에서 Area Class 설정을 살펴보면 여러 설정들이 나오는데 각각 우선 순위를 지정할 수 있다.

Null이면 가장 낮게, Obstacle이면 높게 이런 식으로 설정된다고한다.

 

설정 별로 비교해보면 더 잘보일건데 다음과 같다.

 

- NavArea_Null

 

  • 역할: 해당 영역은 내비게이션 메시 생성 시 완전히 무시(제외)
  • 우선순위: 비용이 매우 높게(거의 무한대에 가까운 값) 설정되어 있어, 다른 영역보다 항상 우선 적용된다.
    즉, 겹치는 영역 중 하나라도 NavArea_Null이 있다면 해당 영역은 네비게이션이 불가능해진다.

 

-  NavArea_Default

 

  • 역할: 특별한 수정 없이 기본 네비게이션 영역으로 사용된다.
  • 비용/우선순위: 기본 비용이 1로 설정되어 있어, 특별히 비용을 조정하지 않은 경우 이 영역이 적용된다.

 

 - NavArea_LowHeight

 

  • 역할: 낮은 높이 제한으로 인해 통과가 어려운 영역에 사용된다
  • 비용/우선순위: 기본적으로 NavArea_Default보다 높은 비용(예: 5 또는 그 이상)으로 설정되어, 경로 탐색 시 회피하려는 성향을 갖게 된다.

 

- NavArea_Obstacle

 

 
  • 역할: AI가 장애물로 인식하여 경로 탐색 시 반드시 회피한다. 즉,  AI가 절대로 통과하지 않고 우회해야 하는 영역으로 설정된다
  • 비용/우선순위: 비용이 매우 높게 설정되어, 경로 계산 시 해당 영역을 피하는 우선순위가 가장 높다. 그리고 AI가 장애물을 우회하는 경로를 선택하도록 유도한다.
 

 

**결론**

(우선순위 높음 -> 낮음)
NavArea_Obstacle -> NavArea_Null -> NavArea_LowHeight -> NavArea_Default 

 

 

 

MoveToLocation()

그리고 위치로 이동하는 함수는 MoveToActor라는 함수를 사용했었는데, 어쨋건 이 Actor를 참조해도 내부에서는 Actor의 좌표를 가져와서 그 곳으로 이동하기 때문에 MoveToLocation도 있을거라 생각해서 검색해보았다.

 

역시! 있었다!

 

사용할 때는 파라미터는 MoveToActor()와 같지만 0번째를 Actor에서 Vector로 변경해주면 된다.

void AMeleeEnemyAIController::MoveToCurrentPatrolPoint()
{
	float PatrolRadius = 2000.0f;
	AMeleeEnemyCharacter* EnemyCharacter = Cast<AMeleeEnemyCharacter>(GetPawn());
	
	if (!EnemyCharacter) return;
	else EnemyCharacter->GetCharacterMovement()->MaxWalkSpeed = 700.0f;

	FVector CurrentLocation = EnemyCharacter->GetActorLocation();
	FNavLocation RandomNavLocation;
	UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
	if (NavSys && NavSys->GetRandomPointInNavigableRadius(CurrentLocation, PatrolRadius, RandomNavLocation))
	{
		MoveToLocation(
			RandomNavLocation.Location,
			5.0f,
			true,
			true,
			false,
			true,
			nullptr,
			true
		);
	}
}

 

아 그리고 원래는 이동 스피드가 없었는데 적이 너무 빠르게 움직이는 것 같아서 조금 천천히 움직이게 하고 싶었다.

찾아보니까 CharacterMovement를 가져와서 속도를 조절할 수 있는 부분이 있길래 700.0f로 설정해주었다.

 

이 부분은 순찰 중에는 느린 속도로 다니다가 플레이어를 발견했을 때는 조금 더 높여주는 방식으로 하면 더 자연스러운 추격이 될 것 같다.

 

void AMeleeEnemyAIController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
	Super::OnMoveCompleted(RequestID, Result);
	
	if (Result.Code == EPathFollowingResult::Success)
	{
		FTimerHandle TimerHandle;
		GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMeleeEnemyAIController::MoveToCurrentPatrolPoint, 0.2f, false);
	}
}

 

그리고 OnMoveCompleted라는 함수를 이용해서 타겟 지점에 도착했으면 0.2초 멈춰주는 부분을 만들었다.

이건 타이머를 활용했고, 타겟에 도착후 다시 이동하는 방식으로 플레이어를 발견하기 전까지 계속 움직여주도록 설계했다.

 

 

결과

 

 

 

마지막 말

NameshVolume을 랜덤 범위로 잡으니까 너무 큰 느낌이 들었다.

스폰할 수 있는 볼륨을 만들어 그 안에서 랜덤으로 움직이던가, Trigger Volume이라는 것이 있던데 둘 중 어떤 방식으로 랜덤 범위를 효율적으로 설정할 수 있을지 고민해봐야겠다.

 

 

트러블슈팅

원인 : 아마도.. 라이더를 사용하다가(언리얼 에디터 내에서도 라이더 쓴다고 설정 마친 상태)

디버깅 상태로 켜놓은 상태에서 비주얼스튜디오 업데이트를 했다.

업데이트 도중에 msBuild 머시기라는 안내창이 떴지만 별 신경 안쓰고 0.2초만에 닫기를 눌렀고...

그리고 비주얼 스튜디오를 기본 편집기로 바꾸고 다시 디버깅하니까 해당 오류창 발생했다.

 

해결방법 : 초간단 ㅎㅎ..

 

언리얼 프로젝트에서 내 솔루션을 시작 프로젝트로 다시 설정해주니까 잘 작동했다.. !...ㅎ

(혹시 그런 분은 없겠지만 이런 상황에서 시간을 많이 소비하지 않았으면 하는 마음에서 바보같은 오류였지만 공유한다..)

 

 

 

언리얼 엔진에서 Navmesh와 적의 경로 탐색을 구현하는 방법에 대해 공부했다

게임에서 적들이 환경을 효율적으로 탐색하고 플레이어를 추적하는 것은 사용자의 몰입감을 증가시키는데 중요한 역할을 한다고 생각한다.

오늘은 Navmesh를 활용하여 적이 움직이는 경로에 대해 학습하였다.

 

NavMesh 적용 방법

1. NavMeshBoundsVolume을 추가

 

2. 순회를 돌 타겟 포인트들을 맵에 생성

 

3. 적 캐릭터 클래스 생성 (이것은Character 클래스를 상속받아 생성)

4. 적 AIController 생성

 

 

 

Navmesh Baking 동적 구현

맵을 제작하다 보면 회전하는 객체가 있을수도 있는데 얘네들을 피해가려면 동적으로 길을 계속 만들어줘야할 것이다.

 

동적 경로 생성은, 환경이 항상 변화하며 예측할 수 없는 장애물이 등장하기 때문에, 그에 맞춰 실시간으로 안전하고 최적의 경로를 찾아내기 위해 꼭 필요하다.

 

Edit -> Project Setting -> Engine -> Navigation Mesh -> Runtime -> Runtime Generation 세팅이 원래는 Static으로 되어있을 것인데 이것을 Dynamic으로 변경해준다.

그럼 이제 런타임 중에도 동적으로 NavMeshVolume이 베이킹되는 것을 볼 수 있을 것이다!

 

 

 

 

결과

 

 

 

트러블 슈팅

이슈 1: EnemyAIController 구현 중, "Navigation/PathFollowingComponent.h 파일을 include할 수 없는 문제 발생

해결방법 : 라이더를 사용하고 있는데 에디터와 라이더 모두 껐다가 키니까 해결되었다..?

 

 

 

 

마지막 말

원래는 비주얼 스튜디오 2022를 사용하다가 라이더도 괜찮다고 해서 써봤는데 좋은 것 같다.

라이더에서는 언리얼 생명주기 함수를 사용할 때 자동으로 Super::beginplay() 같은 것들을 적어준다.

그리고 확실히 비주얼 스튜디오보다 인텔리센스가 빠르고 종류도 더 많은 느낌이 들었다.

솔루션 빌드 속도도 더 빠른 것 같긴한데 이건 내 체감이라 정확하지 않다.

근데 라이더를 처음 써봐서 플러그인 관리나 적응이 좀 귀찮게 느껴지긴 했다.

비주얼 스튜디오 vs 라이더 를 비교하는 글을 몇몇 블로그에 읽었는데 아직은 둘 다 비슷한 느낌이라고 한다.

문제 링크

https://school.programmers.co.kr/learn/courses/30/lessons/77484

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

 

나의 풀이

아웃풋으로 구해야하는 것은 결국 나의 최대 등수, 최소 등수만 구하면 된다.

그래서 0의 카운트도 0이 다 맞았을 경우, 0이 다 틀렸을 경우 이렇게 2개의 상황만 추가해주면된다.

- 1등부터 6등까지 몇 개를 맞추면 되는지 map을 사용해서 키, 밸류를 설정
- lottos를 순회하면서 win_nums의 번호가 있는지 확인하고 있다면 최소 카운트 증가
- 그리고 0이면 0을 관리하는 카운트 증가
- 순회가 끝나고 최대 갯수에 최소 개수 + 제로 카운트
- 각 최대, 최소 개수의 밸류값을 answer에 넣어줌

 

 

나의 코드
#include <string>
#include <vector>
#include <algorithm>
#include <map>

using namespace std;

vector<int> solution(vector<int> lottos, vector<int> win_nums) {
    vector<int> answer;
    int min_count = 0, max_count = 0;
    map<int, int> score;
    score[6] = 1;
    score[5] = 2;
    score[4] = 3;
    score[3] = 4;
    score[2] = 5;
    score[1] = 6;
    score[0] = 6;

    for (int value : lottos)
    {
        if (value == 0)
            max_count++;

        if (find(win_nums.begin(), win_nums.end(), value) - win_nums.begin() != win_nums.size())
            min_count++;
    }
    max_count+=min_count;
    
    answer.push_back(score[max_count]);
    answer.push_back(score[min_count]);
    
    return answer;
}

 

저번에 구현해놨던 아이템 스폰을 이어서 정리해보려고 한다.

과제를 하면서 이미 구현된 부분이 많지만, 제대로 이해되지 않은 부분이나 시간 때문에 쫒겨 깊게 보지 못한 부분들을 다시 이해하고 공부한다는 마음으로 작성하였다.

블루프린트도 많이 쓰고 생소한 클래스들도 많이 다뤄서 재밌었던 경험이었다.

 

 

구현해야 할 목록들

- 캐릭터
- 캐릭터 애니메이션
- 아이템 스폰
- 게임 흐름 설계
- 웨이브 설계
- UI 제작

 

아이템 스폰에서 현재 스폰될 볼륨까지 구현해놓았다.

이제 구현한 기능을 연결시켜줘야한다.

 

아이템 랜덤 생성 구현 과정

1. C++ 클래스를 상속받은 블루프린트를 만들어준다.

Create Blueprint class based on "클래스이름"을 클릭하면 해당 클래스를 기반으로 한 블루프린트가 생성된다.

아 참고로 이렇게 만든 블루프린트에서 여러 변수나 프로퍼티들을 수정하려면 리플렉션에 등록해줘야하고, 관련 매크로들을 채워줘야한다.

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
// 에디터에서 수정할 수 있지만, 블루프린트 내에서는 값을 변경할 수 없음
또는
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
// 에디터와 블루프린트 모두 값 읽고 변경 가능

 

 

2. 생성한 블루프린트를 Level에 올린 후 맵의 영역에 맞게 크기를 조절해준다.

 

3. 생성할 아이템의 정보와 확률을 저장하고 있는 Item Data 구조체를 적용한다.

아이템의 생성확률을 코드에서 적용하면 아이템의 종류가 추가될 때마다 계속 수정해줘야하고 굉장히 번거로운 작업이 될 것이다.

그래서 언리얼에서 데이터 테이블이라는 구조체를 제공한다.

이 데이터 테이블은 저장된 정보를 csv나 json으로 관리해서 간편하게 정보를 불러오고 저장할 수 있는 장점이 있다!

 

 

Item Data 구조체 만들기

먼저 데이터 테이블의 각 행(Row)를 구조체로 매핑해줘야한다.

언리얼에서는 FTableRowBase 라는 기본 구조체를 제공한다.

#pragma once

#include "CoreMinimal.h"
#include "ItemSpawnRow.generated.h"

USTRUCT(BlueprintType)
struct FItemSpawnRow : public FTableRowBase
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FName ItemName;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSubclassOf<AActor> ItemClass;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float SpawnChance;
};

이렇게 저장할 데이터의 행을 추가해준다.

아 그리고 언리얼 엔진에서 추가하게되면 cpp 파일도 같이 생성되는데 이 파일은 비어있어도 삭제는 하지 않는 것이 좋다고 한다.

언리얼에서 파일을 읽어올 때 헤더와 cpp 파일을 같이 읽어오는데 여기서 하나라도 없으면 오류가 날 가능성이 조금이도 증가한다는데 이 부분은 아직 잘 모르겠다.

 

이제부터 실제 데이터 입력을 해야하는데 방법이 총 2가지가 있다.

1. 엑셀 파일에 직접 데이터들을 입력해주는 방법
2. 언리얼이 제공하는 데이터 데이블에서 데이터 입력해주는 방법

상황에 맞게 2가지 방법 중 하나를 택해도 되고, 둘 다 사용해도 된다.

 

나의 경우는 아이템의 종류가 아직 4개밖에 없어서 2번 방법을 택하였다.

 

데이터 테이블을 생성할 경로에 마우스 오른쪽 클릭하여 Miscellaneous -> Data Table 클릭

 

테이블을 생성하고 ItemName과 해당 아이템을 구현한 클래스를 상속받은 블루프린트를 연결해주었다.

그리고 Spawn Chance는 스폰될 확률인데 총 100.00이 되게 잘 조절해주면 된다.

Row Name은 말 그래도 행의 이름이라 특별히 다른 곳에 영향을 미치지는 않지만 쉽게 구별하게 위해 아이템의 이름과 같게 적어주었다.

 

이렇게 아이템 정보 및 스폰 확률에 대한 입력을 완료하였다!

 

 

Item Data 구조체 연결

이제 아까 생성해둔 SpawnVolum 블루프린트를 다시 열어준다.

Spawing 카테고리(내가 C++ 코드에서 직접 생성한 카테고리)에 생성한 아이템 테이블을 추가해준다.

 

그리고 Event Graph 탭으로 들어가서 이렇게 연결시켜준다.

그리고 For Loop 노드의 Last Index의 값에는 아이템이 스폰될 원하는 최대값을 넣어준다.

 

 

결과

그럼 이렇게 내가 지정한 확률별로 아이템이 잘 생성된 것을 볼 수 있다.

 

 

마지막 말

아이템 스폰에 관한 과정을 경험해볼 수 있어서 좋았던 것 같다.

특히 Json이나 csv로 데이터를 가져오는 부분은 실무에서도 많이 사용될 것 같았는데 언리얼에서 제공하는 방식으로 접근하니까 개발자의 입장에서는 단순 입력(데이터를 직접 작성)하는 부분이 조금 귀찮긴 했지만 편했던 것 같다.

하지만 현업에서는 이런 작업을 혼자하는게 아니라 여러 명이서 하게 될텐데 그럼 아무래도 csv를 많이 사용할 것 같아서 추후에 csv로 데이터를 받아오는 작업을 보충해서 해봐야겠다.

오늘은 Wave를 구현해서 코인을 먹고 높은 점수를 쌓는 간단한 게임을 만들어서 공부하려고 한다.

과제 제출도 할 겸..

 

먼저 어떤 걸 해야할 지 고민해보았다.

 

구현해야 할 목록들

- 캐릭터
- 캐릭터 애니메이션
- 아이템 스폰
- 게임 흐름 설계
- 웨이브 설계
- UI 제작

이렇게 크게 총 5개 정도로 나눠보았다.

 

캐릭터와 애니메이션은 저번 포스팅에서 공부한 내용을 그대로 가져갈 것이다.

오늘은 아이템 스폰에 대해 자세하게 공부해보려고 한다.

 

아이템 목록

- 코인
- 피격 아이템
- 회복 아이템

일단 이렇게 3개의 종류의 아이템을 설정했다.

그리고 공부하면서 만들어놨던 ItemInterface와 BaseItem 클래스 등을 가져와서 사용할 것이다.
(링크 참고)

 

아이템의 기반이 완성되었으니, 이제 아이템을 맵에서 랜덤으로 생성하는 부분을 구현하려고 한다.

맵에서 먼저 얼마큼의 영역에서 아이템이 스폰될 지 설정해준다.

그리고 지정된 영역에서 랜덤으로 아이템을 반환하는 클래스를 작성해주었다.

#pragma once

#include "CoreMinimal.h"
#include "ItemSpawnRow.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"

class UBoxComponent;

UCLASS()
class PRACTICE_API ASpawnVolume : public AActor
{
	GENERATED_BODY()
	
public:	
	ASpawnVolume();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	UBoxComponent* SpawningBox;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
	UDataTable* ItemDataTable;

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	AActor* SpawnRandomItem();

	FItemSpawnRow* GetRandomItem() const;
	AActor* SpawnItem(TSubclassOf<AActor> ItemClass);  // TSubclassOf<Type> - Type의 하위 클래스가 아니면 오류가 남
	FVector GetRandomPointInVolume() const;
};
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"

// Sets default values
ASpawnVolume::ASpawnVolume()
{
	PrimaryActorTick.bCanEverTick = false;
	// 씬 컴포넌트
	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);
	// 박스 컴포넌트 생성
	SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
	SpawningBox->SetupAttachment(Scene);

	ItemDataTable = nullptr;
}

FVector ASpawnVolume::GetRandomPointInVolume() const
{
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();  // GetScaledBoxExtent - Box의 딱 절반의 크기를 반환
	FVector BoxOrigin = SpawningBox->GetComponentLocation();  // 박스의 중심 좌표 반환
	
	return BoxOrigin + FVector(
		FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
		FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
		FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
	);
}

AActor* ASpawnVolume::SpawnRandomItem()
{
	if (FItemSpawnRow* SelectedRow = GetRandomItem())
	{
		if (UClass* ActualClass = SelectedRow->ItemClass.Get())  // TSoftClass로 받어오기 때문에 UClass 타입으로 받아옴
		{
			return SpawnItem(ActualClass);
		}
	}
	
	return nullptr;
}

FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
	if (!ItemDataTable) return nullptr;

	TArray<FItemSpawnRow*> AllRows;
	static const FString ContextString(TEXT("ItemSpawnContext"));  // DataTable의 디버깅용 로그
	ItemDataTable->GetAllRows(ContextString, AllRows);

	if (AllRows.IsEmpty()) return nullptr;

	float TotalChance = 0;
	for (const FItemSpawnRow* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;
		}
	}

	const float RandValue = FMath::FRandRange(0.0f, TotalChance);
	float AccumulateChance = 0.0f;  // 누적확률 초기화

	for (FItemSpawnRow* Row : AllRows)
	{
		AccumulateChance += Row->SpawnChance;
		if (RandValue <= AccumulateChance)
			return Row;

	}

	return nullptr;
}

AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
	if (!ItemClass) return nullptr;

	AActor* SpawnedActor = GetWorld()->SpawnActor<AActor>(
		ItemClass,
		GetRandomPointInVolume(),
		FRotator::ZeroRotator
	);

	return SpawnedActor;
}

 

 

마지막 말

처음에는 아이템을 랜덤으로 어떻게 생성할 지에 대해 막막했었는데, 하나씩 해보니까 조금 이해가 되는 것 같다.

그리고 각 아이템 별 확률을 설정할 수 있는데 이 부분을 코드로도 접근할 수 있는지 살펴보려고 한다.

https://school.programmers.co.kr/learn/courses/30/lessons/159994

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

문제

 

 

나의 풀이
- goal에 있는 단어를 cards1과 cards2의 첫번째 글자와 각각 비교한다.
- 비교했을 때 둘 다 없으면 No를 바로 반환해버린다.
- 있으면 있는 쪽의 0번째 단어를 삭제한다.

이렇게 비교하면 순차적으로 비교할 수 있으면서 깔끔하게 코드를 작성할 수 있을 것 같아 이렇게 로직을 설계하였다.

 

내 코드
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>

using namespace std;

string solution(vector<string> cards1, vector<string> cards2, vector<string> goal) {
    for (int i = 0; i < goal.size(); i++)
    {
        if (cards1.size() > 0 && goal[i] == cards1[0])
            cards1.erase(remove(cards1.begin(), cards1.end(), goal[i]));
        else if (cards2.size() > 0 && goal[i] == cards2[0])
            cards2.erase(remove(cards2.begin(), cards2.end(), goal[i]));
        else
            return "No";
    }
    
    return "Yes";
}

 

 

remove와 erase를 같이 쓴 이유

remove와 erase를 다룬 게시물 첨부 예정