[TIL] 250820 (챕터3 발표)

김세희·2025년 8월 20일
post-thumbnail

✍️Today I Learned

📅 2025-08-20

  • 챕터3: 팀프로젝트 완료
  • UE: Non-Uobject 스마트 포인터 & 소유권

챕터3: 팀프로젝트 완료

UE 슈터 게임 프로젝트

게임 기획

  • GAS를 활용한 확장성 있는 스킬 시스템 구현
  • 데이터 중심 설계 / 모듈식 설계
  • 다양한 스킬과 패시브가 중첩되는 재미

-> 뱀서라이크 "드래곤 서바이벌"

게임 핵심 목표

  • 스킬/패시브 시스템의 중첩과 시너지 체험
  • GAS 기반의 안정적이고 확장 가능한 구조 설계

AI / 몬스터

이번 프로젝트에서 AI / 몬스터 파트를 맡아 구현했다.

1. 스포너

오브젝트 풀링
빈번한 몬스터 객체 생성/파괴로 인한 성능 저하와 메모리 단편화 방지
활성화 풀, 비활성화 풀로 객체를 관리

스폰 타이머
웨이브별로 다른 스폰 타이머를 실행하도록 웨이브 데이터를 테이블로 관리

스폰 위치 탐색
플레이어를 중심으로 최소~최대 반경 사이에서 무작위로 지점을 선택하여 네비 메시 위의 유효한 위치인지 확인

캐릭터 데이터 랜덤 적용
캐릭터 활성화 시 해당 웨이브에 맞는 몬스터데이터 에셋을 랜덤 적용하여 같은 객체를 재사용 하더라도 다른 타입의 몬스터로 활성화할 수 있다.

2. AI 캐릭터

오브젝트 풀링을 위해 몬스터 객체를 Spawn, Destroy하지 않고 비활성화 / 활성화로 관리했다.
플레이어 캐릭터와 공통적인 로직은 부모 클래스인 베이스 캐릭터에서 구현하고 AI 캐릭터에서 추가로 필요한 로직은 오버라이딩하여 구현했다.

비활성화
사용한 AI 컨트롤러 저장 후 연결 해제

활성화
사용하던 컨트롤러가 있다면 재사용
메시 스케일을 랜덤 적용하고 그 스케일에 맞게 캡슐 컴포넌트 크기 조정

OnDeathCleanup()
사망 어빌리티 종료시 호출되는 함수
스포너가 구독하여 해당 AI캐릭터를 비활성화 풀로 반환

3. AI 컨트롤러

AI 캐릭터가 활성화 될 때 컨트롤러가 빙의하고 이 때 블랙보드 데이터를 초기화하고 몬스터별로 다른 비해비어 트리를 실행한다.
빙의 해제 시 비해비어 트리를 종료한다.

4. 몬스터

단계별 몬스터를 구현하여 웨이브가 넘어갈 수록 난이도를 높이고 마지막에 보스전을 할 수 있도록했다.
일반 몬스터
몰려오는 몬스터

엘리트 몬스터
총 5종류로 다 다른 행동패턴과 공격 스킬을 갖는다. 높은 스탯을 갖고 많은 경험치 젬 드랍한다.
몬스터 채널을 만들어 엘리트 몬스터가 일반 몬스터를 넘어서 플레이어에게 바로 갈 수 있도록 구현했다.

보스 몬스터
최종 보스로 아주 높은 스탯을 갖고 6개의 범위별 공격을 하며 플레이어가 일정 거리 멀어지면 그 위치로 순간 이동을 할 수 있다.

UE: Non-Uobject 스마트 포인터 & 소유권

1. 기존 메모리 관리의 문제점

원시 포인터의 위험성
수동으로 해제하지 않으면 메모리 누수 발생
이중 삭제나 삭제된 메모리에 접근하면 크래시 발생

스마트 포인터
특정 조건이 맞으면(스코프에서 벗어나거나 참조 카운트가 0이거나 등) 메모리 자동 해제

2. TSharedPtr

2-1. 공유 소유권
2-2. 참조 카운팅 시스템의 내부 구조

이 객체를 참조하는 포인터가 몇개인지 세다가 0이되면 메모리를 해제한다.

template<typename T>
class TSharedPtr 
{
private:
    T* ObjectPtr;                     // 실제 객체
    FReferenceController* RefController; // 참조 카운트와 제어 정보
};
  • FReferenceController 안에 들어 있는 값
    • SharedRefCount : 강한 참조 (TSharedPtr)가 몇 개인지
    • WeakRefCount : 약한 참조 (TWeakPtr)가 몇 개인지

처음 생성했을때는 각각 1씩 올라감
복사하면 강한 참조만 1씩 올라감
참조를 해제하여 강한참조 카운트가 0이되면 약한참조 카운트도 즉시 0이되어 메모리가 해제된다.

2-3. 게임에서 사용 예시
퀘스트와 같이 여러 매니저나 시스템에서 같은 데이터를 공유하야 하는 객체를 복사해서 받는게 아니라 쉐어드 포인터로 공유하여 메모리를 절약하고 동기화를 용이하게 한다.

2-4. TSharedRef vs TSharedPtr
TSharedPtr: nullptr일 수 있다. 사용할 때 항상 유효성 체크가 필요하다.
TSharedRef: 항상 생성 시 초기화해야하고 비어있을 수 없다. null 체크가 필요없다. 반드시 있어야하는 객체를 다룰 때 사용한다.

3. TWeakPtr

3-1. 순환 참조
서로 강한 참조로만 가리키고 있으면 참조 카운트가 항상 0보다 크기 때문에 메모리가 해제되지 않는다. 스코프를 벗어나 객체가 사라지면 객체에 접근할 방법은 사라지지만 메모리가 해제되지 않아 메모리 누수가 발생한다.

3-2. 순환 참조를 해결하는 방법
한 쪽을 약한 참조로 바꿔주면 약한 참조는 참조 카운트에 포함되지 않아 순환을 끊을 수 있다.

3-3. Pin()
약한 참조는 객체에 바로 접근할 수 없다(컴파일 에러 ). Pin()을 사용하여 약한 참조를 강한 참조로 바꾸어 접근해야한다.

4. TUniquePtr

4-1. 독점 소유권
한 포인터만 소유할 수 있다. 복사하면 컴파일 에러가 발생한다. 소유권 이동(Move)은 가능하다.
메모리 효율성이 뛰어나고 소유권이 명확하다. 자동 메모리 해제가 확실하다.
RAII: 객체의 생명주기 = 자원의 생명주기

4-2. 게임에서 사용 예시
텍스처, 사운드 매니저에서만 독점 관리할 때 사용.
클라이언트별로 네트워크 연결 객체 독점 관리.

5. 스마트 포인터 사용 가이드

  1. UObject인가
    UObject 기반 클래스이면 UPROPERTY 사용하면 엔진이 GC로 알아서 메모리 관리해준다.
  2. 소유권 파악
    독점 소유 -> TUniquePtr
    소유권 공유 -> TSharedPtr
    소유하지 않고 참조만 -> 일반 변수나 참조 사용
  3. 접근 패턴 분석
    소유는 한 곳에서만 하고 다른 곳에서는 참조만 하는 경우 -> 일반 변수나 참조 사용
  4. 생명주기 안전성
    공유가 확실히 필요할 때
    null가능 -> TSharedPtr
    항상 유효 -> TSharedRef
  5. 관계 구조 분석
    순환 참조 위험 있음 -> TWeakPtr
    단방향 관계 -> TSharedPtr

💡 느낀 점 (What I Felt)


출처: 스파르타코딩 내일배움캠프

0개의 댓글