무기 시스템 예제
- WeaponComponent : WeaponAsset에 필요한 명령을 내린다.
- 즉, 모든 명령은 ‘웨폰 컴포넌트’를 통해서 전달된다.
- WeaponAsset : 각 기능들을 가진 C클래스 파일들을 하나로 모은다.
- Attachment : 장착될 무기 모양(메쉬)를 넣을 파일 이다.
- Equipment :
- WeaponStructures :
- WeaponAsset에서 각 클래스들을 사용하기 위해 cpp 파일에 각 클래스의 ‘헤더 파일’을 include 해줘야한다.
데이터 테이블과 데이터 에셋의 설명
- 데이터 테이블 : 행과 열로 데이터를 관리한다.
- 데이터들을 자유 자재로 다루기에는 조금 불편하다. (ex. 클래스 타입 등)
- 못다루지는 않지만 무기와 아이템들에 관한 ‘동작들’ 정도만 테이블로 관리한다.
- ‘기획 데이터’같은 실제 데이터들은 ‘데이터 에셋’으로 다룬다.
- 무기와 아이템의 데이터는 ‘데이터 에셋’에 있고, 유니티에도 있는 ‘에셋 스트리밍’ 기술을 사용한다.
- 서버에 에셋이 존재하고, 클라이언트 에셋이 존재하지 않을 경우에 자동으로 끌어다 쓰는 기능이다.
- 데이터 테이블도 지원이 가능하지만, ‘데이터 에셋’에 더 효과적이다.
데이터 에셋 파일 생성
- 무기마다 쓰는 데이터 에셋이 다르므로, 각 무기마다 폴더를 생성해서 사용한다.
- DA_Sword - 하위 클래스들을 모아놓을 데이터 에셋 파일
- BP_CAttachment_Sword - 하위에 들어갈 블루프린트 클래스
- DA_Sword - 하위 클래스들을 모아놓을 데이터 에셋 파일
- 데이터 에셋 클래스는 ‘액터’나 ‘컴포넌트’가 아니기 때문에 ‘BeginPlay’ 함수가 없다.
- 실행 타이밍을 맞추기 위해 이름을 통일시킨 ‘임의로 생성한 함수’다.
- Asset 파일을 생성하고 그 안에 넣을 클래스 파일들을 상속받는 블루프린트들이 들어간다.
- 순서 : 데이터 에셋 C++ 클래스 생성 - 데이터 에셋 파일 생성 : 생성한 데이터 클래스 선택 - 데이터 에셋에 넣을 C++ 클래스 작성 - 데이터 에셋에 들어갈 C++ 클래스를 상속받은 ‘블루프린트 클래스’ 생성
- 데이터 에셋 파일 생성
- 콘텐츠 폴더 - 우클릭 - 기타 - 데이터 에셋 클릭 - ‘데이터 에셋’클래스를 상속한 클래스들만이 항목에 뜬다. - 생성한 ‘데이터 에셋’ 클래스 파일 클릭 - 생성할 무기의 이름으로 파일의 이름을 작성한다.
- 데이터 에셋에 넣을 클래스를 상속받은 블루프린트 생성
- 콘텐츠 폴더 - 우클릭 - 블루프린트 클래스 클릭 - 모든 클래스 : 데이터 에셋에 들어갈 클래스 검색 - 생성할 무기 이름으로 이름 변경
- 무기 블루프린트 파일 - 추가 - ‘스켈레탈 메쉬 컴포넌트’ 검색 - 디테일 - 메시 - 스켈레탈 메시 : 해당 무기의 스켈레탈 메쉬 검색
- 추가 - ‘캡슐 콜리전’ 검색 - 콜리전 위치 조절 - 콜리전 회전 조절 - 셰이프 - 캡슐 절반 높이 조절 - 캡슐 반경 조절
웨폰 컴포넌트 생성
- 데이터 에셋들을 모으는것은 ‘데이터 테이블’ 형식으로 가져올 수도 있다.
- 테이블 형태로 하면 코드가 복잡해지지만 무기 번호와 상관없이 해당 EWeaponType을 가지고 가져올 수 있다..
- 배열 형태로 하면 코드가 단순해지지만 배열의 순서와 데이터 에셋의 순서를 맞춰야한다.
- 클래스 파일 생성
- 새 C++ 클래스 추가 - 액터 컴포넌트 클릭
- 앞으로 생성할 ‘무기 타입’ 정의
- 헤더 파일 - UENUM 메크로 생성 - 무기 타입 : 맨손 공격, 검, 망치, 순간 이동, 마법, 활 등..
- 데이터 에셋 : 사용 준비
- 데이터 에셋 클래스는 ‘액터’나 ‘컴포넌트’가 아니기 때문에 ‘BeginPlay’ 함수가 없다.
- 실행 타이밍을 맞추기 위해 이름을 통일시킨 ‘임의로 생성한 함수’다.
- 생성자 : 리턴타입이 없고, 클래스명과 ‘이름이 같다.’
- 데이터 에셋 파일을 생성했을때 아무런 기본값이 설정되지 않았다면 플레이시 ‘터진다.’
- 미리 ‘기본값’을 넣어야한다.
- 데이터 에셋 파일을 생성했을때 아무런 기본값이 설정되지 않았다면 플레이시 ‘터진다.’
- 클래스 파일 변수에 기본값을 넣는 순서
- 헤더에 ‘EditAnywhere’로 지정하고 ‘TSubclassOf<클래스>’로 클래스를 제한한다.
- 생성자 정의 - 클래스::StaticClass(); 를 선언한 TSUbclassOf<클래스> 변수에 저장한다.
- 헤더 파일 - 생성자 선언 - 호출되는 타이밍을 통일시키기 위해 ‘BeginPlay’ 함수를 ‘오너 캐릭터’를 받아 선언한다. - cpp 파일 - 생성자 정의 - ‘붙임 클래스’ 변수에 기본값을 넣기 위해 ‘클래스타입을 변수로 사용’ 함수를 호출하여 기본값을 저장한다. - 임의의 BeginPlay 함수 정의
//header private: //Attachment 클래스만 받으라고 제한을 걸었다. UPROPERTY(EditAnywhere) TSubclassOf<class ACAttachment> AttachmentClass; public: UCWeaponAsset(); //비긴 플레이가 없지만 임의로 이런 이름으로 지정했다. void BeginPlay(class ACharacter* InOwner); //cpp AttachmentClass = ACAttachment::StaticClass(); - 데이터 에셋 클래스는 ‘액터’나 ‘컴포넌트’가 아니기 때문에 ‘BeginPlay’ 함수가 없다.
- 데이터 에셋의 저장된 데이터 가져오기
- 헤더 파일 - 명령을 내리기 위해 웨폰 에셋 클래스의 ‘데이터 에셋’ 배열 변수를 ‘무기 타입 수’만큼 지정하여 선언한다. - CPP 파일 - 웨폰 컴포넌트를 넣은 오너 캐릭터를 가져오기 위해 ‘오너 가져오기’ 함수를 ‘캐릭터 클래스’ 형변환으로 호출하고 ‘오너 캐릭터’ 변수에 저장한다. - ‘무기 타입 수’만큼 반복한다. - 현재 순서에서의 ‘데이터 에셋’이 없다면 : 현재 순서의 ‘데이터 에셋’의 ‘임의의 BeginPlay’ 함수에 ‘오너 캐릭터’를 넣어 호출한다.
//header private: UPROPERTY(EditAnywhere, Category = "DataAsset") class UCWeaponAsset* DataAssets[(int32)EWeaponType::Max]; //cpp OwnerCharacter = Cast<ACharacter>(GetOwner()); for (int32 i = 0; i < (int32)EWeaponType::Max; i++) { //테이블 형태로 해도 되지만, 지금은 배열 형태로 한다. if (!!DataAssets[i]) { DataAssets[i]->BeginPlay(OwnerCharacter); } }
웨폰 컴포넌트 : 검 설명
- 웨폰 컴포넌트를 검에 붙일 것이다.
- ACharacter → ACPlayer → BP_CPlayer - CWeaponComponent - CWeaponAsset (- DataAssets 배열 변수)
- 블루프린트 플레이어의 ‘비긴 플레이’보다 먼저 ‘웨폰컴포넌트의 ‘비긴플레이’가 먼저 실행된다.
- 웨폰 컴포넌트에 있는 ‘비긴 플레이’에서 ‘웨폰 에셋’에 있는 ‘임의로 작성한 비긴 플레이’에 ‘오너 캐릭터’를 넣고 호출한다.
- 웨폰 에셋의 ‘어테치 먼트’에서 BP_Attachment_Sword를 붙인다.
- ‘웨폰 컴포넌트’의 ‘BeginPlay’는 자동으로 호출되는 함수이고, 이 함수에서 작성한 코드로 ‘웨폰 에셋’의 ‘임의의 BeginPlay’ 함수를 호출한다.
- 웨폰 에셋은 ‘Attachment’ 클래스를 변수로 가지고 있다.
- 이렇게 되면 웨폰 에셋에 있는 ‘어테치 먼트’에서 ‘무기’를 스폰시키는 것이다.
웨폰 컴포넌트 - 웨폰 에셋 연결 확인
- ‘플레이어’와 ‘애님 인스턴스’ 및 ‘노티파이’ 파일들에 있는 검 관련 함수 제거
- 컴포넌트 끼리는 BeginPlay 호출 순서는 ‘랜덤’이다.
- 그러므로 컴포넌트 끼리는 서로간에 영향을 미치는 코드는 넣으면 안된다.
- 플레이어 정리
- 헤더 파일 - ‘웨폰 컴포넌트’ 변수 선언 - CPP 파일 - 웨폰 컴포넌트 ‘컴포넌트 생성’ 호출
- 플레이어 - 웨폰 컴포넌트 - 웨폰 에셋 연결 확인 테스트
- 웨폰 에셋 CPP 파일 - ‘임의의 BeginPlay’ 함수에 CLog 작성
- 플레이어 블루프린트 파일 - 디테일 - 데이터 에셋 - 해당 무기 항목에 ‘데이터 에셋 파일’ 넣기
검을 월드에 스폰 시키기
- WeaponAsset
- 웨폰 에셋은 액터로부터 상속받은것이 아니기 때문에 가져올 수 있는 ‘월드’가 없다.
- 월드를 가져올 수 있는 클래스→GetWorld() 를 해야한다.
- 지금은 BeginPlay를 호출할때 받아오는 ‘오너 캐릭터’의 ‘월드’를 사용한다.
- 실제로 스폰시킬 클래스 : AttachmentClass 변수
- AttachmentClass에 CAttachment를 상속시킨 블루프린트를 넣었다.
- 여기에 ‘Params’ 변수로 ‘액터 스폰 파라미터’를 넣는다.
- cpp 파일 - ‘AttachmentClass’ 변수가 비어있지 않다면 : 무기 데이터가 들어있다는 의미이다. - ‘액터 스폰 파라미터’ 변수 선언 - 만들어진 객체를 저장할 Attachment 클래스타입의 ‘붙임’ 변수 선언 - ‘파라미터’ 변수의 ‘오너’에 매개변수로 들어온 ‘오너 캐릭터’를 저장한다. - ‘붙임’ 변수에 매개변수로 받아온 ‘오너 캐릭터’의 ‘월드 가져오기’ 함수에서 ‘스폰 액터’를 ‘CAttachment’ 타입을 지정하고 ‘AttachmentClass (=실제로 스폰시킬 클래스) , ‘파라미터’(=액터 스폰 파라미터)‘를 넣어 호출시킨다.
//CPP if (!!AttachmentClass) { FActorSpawnParameters params; params.Owner = InOwner; //소환 및 붙이기 Attachment = InOwner->GetWorld()->SpawnActor<ACAttachment>(AttachmentClass, params); } - 웨폰 에셋은 액터로부터 상속받은것이 아니기 때문에 가져올 수 있는 ‘월드’가 없다.
무기를 소켓에 붙이기
- 장착 관련은 무기마다 달라질 것이다.
- 블프에서 처리한다.
- 블프의 Attach Actor To Component 함수를 코드로 만든다.
- 블루프린트에 있는 함수를 코드에서 사용하는 순서
- 해당 블루프린트가 있는 부모 함수로 들어간다.
- 같은 기능을 가진 C 코드용 함수가 근처에 있다.
- CAttachment 클래스
- 헤더 파일 - ‘어디에 붙이기’ 함수를 ‘소켓 이름’을 받아 선언하고, BlueprintCallable 메크로로 블루프린트에서 호출할 수 있도록 지정한다. - CPP 파일 - ‘어디에 붙이기’ 함수 정의 - ‘컴포넌트에 붙이기’ 함수에 ‘뭍일 위치, 붙이기 룰 : 붙이기 룰 : 기본값, weild : true, 매개변수로 받은 소켓 이름 변수’
//header protected: UFUNCTION(BlueprintCallable, Category = "Attach") void AttachTo(FName InSocketName); //cpp //OwnerCharacter->GetMesh() : 붙을 부모 AttachToComponent(OwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true), InSocketName);- 붙이기 : 검 블루프린트 파일 - ‘어디에 붙이기’ 함수 검색 - 소켓 이름 지정 : Holster_Sword
장착 관리 클래스 생성
- 구조체 자료형은 포인터가 아니기 때문에 ‘헤더 파일’을 추가해야한다.
- BlueprintNativeEvent : 코드에서 먼저 정의해놓고, 필요하면 블루프린트에서 재정의하하는 기능이다.
- ImplementableEvent : 호출만 해줄테니 블루프린트에서 정의하라는 기능이다.
- CEquipment
- 헤더 파일 - 임의의 ‘BeginPlay’ 함수를 ‘오너 캐릭터’와 웨폰 스트럭쳐 클래스의 열거 자료형인 ‘장착 데이터’ 레퍼런스 변수를 넣고 선언 - 구조체 타입인 ‘장착 데이터’를 쓰기 위해 ‘웨폰 스트럭쳐’ 클래스의 ‘헤더 파일’을 include 해야한다. - 장착 중 이동을 위해 ‘움직임’ 포인터 변수 선언 - 상태 변화 확인을 위해 ‘상태’ 포인터 변수 선언 - 장착 데이터 사용을 위해 ‘장착 데이터’ 변수 선언 - ‘오너 캐릭터’ 변수 선언 - ‘장착’ 함수를 BlueprintNativeEvent 메크로로 선언한다. - Native이벤트를 정의할 ‘장착 : 임플리먼트’ 함수 선언 - CPP 파일 - 임의의 ‘BeginPlay’ 함수 정의 - ‘오너 캐릭터’ 변수에 매개변수로 들어온 ‘오너 캐릭터’를 저장한다. - ‘장착 데이터’ 변수에 매개변수로 들어온 ‘장착 데이터’ 를 저장한다. - ‘움직임’ 변수와 ‘상태’ 변수에 각 컴포넌트를 ‘컴포넌트 가져오기’ 함수를 매개변수로 들어온 ‘오너 캐릭터’를 넣어 호출한다. (오너 캐릭터에 있는 각 컴포넌트를 가져와 저장하는 것이다.) - ‘장착 : 임플리먼트’ 함수 정의 - ‘상태’ 변수의 ‘장착 모드’ 호출 - ‘장착 데이터’ 변수의 ‘움직임 가능 확인’ 변수가 false가 아니라면? : ‘움직임’ 변수의 ‘멈춤’ 함수를 호출한다. - ‘데이터’ 변수의 ‘카메라 회전 사용 확인’ 변수가 True 라면? : ‘움직임’ 변수의 ‘카메라 회전 사용’ 함수 호출 - ‘데이터’ 변수의 ‘몽타주’가 비어있지 않다면? : ‘오너 캐릭터의 ‘몽타주 실행’ 함수에 ‘데이터’ 변수의 ‘몽타주’를 넣어 호출한다. - 장착 몽타주가 없다면 ‘경고 메세지’를 띄워라
//header public: //임의의 비긴플레이 선언 void BeginPlay(class ACharacter* InOwner, const FEquipmentData& InData); public: //Native : 정의는 내가 할게, 블프 니는 필요하면 재정의해 UFUNCTION(BlueprintNativeEvent) void Equip(); void Equip_Implementation(); private: class ACharacter* OwnerCharacter; private: FEquipmentData Data; private: class UCMovementComponent* Movement; //장착중 움직일 수 있으니까 class UCStateComponent* State; //CPP OwnerCharacter = InOwner; Data = InData; Movement = CHelpers::GetComponent<UCMovementComponent>(InOwner); State = CHelpers::GetComponent<UCStateComponent>(InOwner); State->SetEquipMode(); if (Data.bCanMove == false) { Movement->Stop(); } if (Data.bUseControlRotation) { Movement->EnableControlRotation(); } if (!!Data.Montage) { OwnerCharacter->PlayAnimMontage(Data.Montage, Data.PlayRate); } else { //GLog : 지정한 타입의 메세지 띄운다. GLog->Log(ELogVerbosity::Error, "Equip Montate == nullptr"); } - CStateComponent
- 헤더 파일 - ‘장착 모드 설정’ 함수 선언 - CPP 파일 - ‘장착 모드 설정’ 함수 정의 - ‘타입 변경’ 함수에 ‘상태 타입 : 장착’을 넣어 호출한다.
//header void SetEquipMode(); //cpp ChangeType(EStateType::Equip); - CWeaponAsset
- WeaponStruces에 있는 FEquipmentData 구조체를 사용하기 위해 헤더 파일에 include 해야한다.
- 헤더 파일 - WeaponStruces include - FEquipmentData 구조체 자료형 변수 선언 - Attachment처럼 Equipment 클래스 변수 선언 - Equipment 클래스 제한 변수 선언 - CPP 파일 - 생성자 확인 - ‘장착 클래스’에 ‘클래스 할당’ 함수 호출하고 저장 - ‘장착 클래스’ 변수가 비어있지 않다면 : ‘장착 클래스’ 변수를 넣은 ‘새 오브젝트’ 함수를 호출하고 ‘Equipment’ 변수에 저장한다.(즉, 변수가 가지고 있는 타입의 클래스를 생성해서 저장하는 코드다.) - ‘Equipment’에 있는 ‘BeginPlay’ 함수를 ‘매개변수로 들어온 오너 캐릭터, ‘장착 데이터’ 변수’를 넣고 호출한다.
//header UPROPERTY() class UCEquipment* Equipment; //장착 관리 담당 //CPP //NewObject(생성하는 자기자신, 어떤 클래스를 생성할거냐) : 직렬화에 의해 가비지컬렉터로 관리되는 객체를 생성한다. //리플렉션 : 클래스 타입 자체를 변수로 사용하는 기능 Equipment = NewObject<UCEquipment>(this, EquipmentClass); Equipment->BeginPlay(InOwner, EquipmentData); //위에서 어테치먼트가 생성 되었다면 if (!!Attachment) { Equipment->OnEquipmentBeginEquip.AddDynamic(Attachment, &ACAttachment::OnBeginEquip); Equipment->OnEquipmentUnequip.AddDynamic(Attachment, &ACAttachment::OnUnequip); }
무기 장착 명령을 내려서 실제로 무기 장착 - 무기 타입 변경 전까지
- CPlayer
- 플레이어에서 장착명령 전달 → 웨폰컴포넌트에서 검 장착 모드 실행
- cpp 파일 - InputComponent 확인 - 검 장착 명령 확인 - ‘액션 연결’ 함수에 ‘액션 맵 이름, 버튼 상태, 명령이 내려질 클래스, 실행할 함수 이름’을 넣어 호출한다.
//CPP PlayerInputComponent->BindAction("Sword", EInputEvent::IE_Pressed, Weapon, &UCWeaponComponent::SetSwordMode); - CWeaponComponent
- 헤더 파일 - ‘검 모드 설정’ 함수 선언 - CPP 파일 - ‘검 모드 설정’ 함수 정의 - 연결 되었는지 테스트
//header void SetSwordMode(); //검 모드 설정 //CPP void UCWeaponComponent::SetSwordMode() { //2번에 연결되었는지 확인 CLog::Log("Sword"); }- 헤더 파일 - ‘기본 모드 확인’ 함수 선언 - CPP 파일 - ‘기본 모드 확인’ 함수 정의 - ‘컴포넌트 가져오기’ 함수를 ‘스테이트 컴포넌트’로 설정하고 ‘오너 캐릭터’를 넣어 호출한 뒤 그 안에있는 ‘기본 상태 확인’ 함수를 호출하고 반환한다. ⇒ 기본 상태일때만 검을 장착하니까
//header private: bool IsIdleMode(); //CPP bool UCWeaponComponent::IsIdleMode() { return CHelpers::GetComponent<UCStateComponent>(OwnerCharacter)->IsIdleMode(); }- CPP 파일 - ‘검 모드 설정’ 함수 정의 - ‘기본 모드 확인’ 함수를 호출하고 false라면 실행하지 않는다. - ‘모드 설정’ 함수에 ‘무기 타입 : 검’을 넣고 호출한다.
//CPP //아이들 모드여야 실행한다. CheckFalse(IsIdleMode()); SetMode(EWeaponType::Sword);- 헤더 파일 - ‘모드 설정’ 함수를 ‘웨폰 타입’ 변수를 넣어 선언한다. - 웨폰 타입 자료형으로 변수를 선언하고 ‘웨폰 타입 : Max’로 기본값을 준다. - ‘무기 장착 안함 모드 확인’ 함수를 선언하고 현재 무기타입이 ‘웨폰 타입 : Max’와 같다는 결과로 정의한다. - Equipment 클래스 자료형의 ‘Equipment 가져오기’ 함수를 선언하고 ‘Equipment’ 반환으로 정의한다. - ‘타입 변경’ 함수에 ‘웨폰타입’ 변수를 넣고 선언한다. - CPP 파일 - Equipment 클래스를 사용할 것이니 include한다. - ‘모드 설정’ 함수 정의 - 현재의 ‘웨폰 타입’과 매개변수로 들어온 ‘웨폰 타입’이 같다면 무기를 해제하고, ‘무기 장착 안함 모드 확인’ 함수가 false라면 : 이미 장착되어있는 무기를 해제하고 다른 무기로 바꾼다. - ‘데이터 에셋’ 배열 변수에 ‘현재 무기 타입’의 순서에 무기가 없다면 : ‘데이터 에셋’ 배열 변수에 ‘현재 무기 타입’의 순서의 ‘Equipment 가져오기’ 함수를 호출하고 ‘장착’ 함수를 호출한다. - ‘타입 변경’ 함수에 ‘매개변수로 들어온 웨폰 타입’을 넣고 호출한다.
//header private: void SetMode(EWeaponType InType); void ChangeType(EWeaponType InType); FORCEINLINE bool ISUnarmedMode() { return Type == EWeaponType::Max; } FORCEINLINE class UCEquipment* GetEquipment() { return Equipment; }; //CPP //현재 들고 있는 무기와 같다면 : 무기 해제 if (Type == InType) { return; } else if (ISUnarmedMode() == false) //이미 장착 되어있다는 뜻이다. { //현재 장착 무기를 해제한다. => 현재무기를 다른 무기로 바꾼다. } if (!!DataAssets[(int32)InType]) { //무기 장착 DataAssets[(int32)InType]->GetEquipment()->Equip(); //타입 변경 //현재 장착된 무기 타입을 전달해서 다른걸로 바꾼다. }
언리얼 동적 할당 주의점
- int* a = new int(); ⇒ 기본형으로 맞는 문법이지만 ‘언리얼’에서는 이렇게 사용하지 않는다.
- C에서만 관리하는 동적할당이 된다.
- 이렇게 선언된 변수의 값은 원래 동적할당처럼 ‘ delete a; ‘처럼 삭제해야한다.
- UObject로부터 상속받은 클래스들은 ‘직렬화’로 관리되는 데이터들이다.
- GENERATED_BODY() 로 직렬화 기능을 가져올 수 있게 된다.
- 또한, 언리얼에서 가비지 컬렉터로 변수를 관리하기 위해서는 ‘반드시’ UPROPERTY 같은 메크로들을 작성해야한다.
- 직렬화를 안하면 생기는 문제점 : 에디터하고 연결이 끊어지면 언리얼이 터진다.
컴포넌트와 플레이어 주의점
- 플레이어에 ‘캐릭터’가 상속되어 있다.
- 플레이어는 ‘웨폰 컴포넌트’와 ‘스테이트 컴포넌트’를 가지고있다.
- 각 컴포넌트는 플레이어의 오너(폰)이나 상속받는 캐릭터를 가질 수 있지만, 플레이어 자체는 가지면 안된다. ⇒ 커플링 문제
- 플레이어는 컴포넌트들을 호출해도 되지만, 각 컴포넌트들은 플레이어를 호출하면 안된다.
- 상속관계에서 자식은 부모를 소유하지 않아야한다.
- 동등한 레벨의 컴포넌트(웨폰, 상태 같은 레벨)은 서로간을 소유하면 안된다.
- 해결방법 : 델리게이션(가장 좋은 방법), 우회(검색해서 리턴하는 방식)
- 코드의 흐름 한방향으로만 흘러야한다.
- WeaponAsset이 가지고있는 Equipment에서는 다른 컴포넌트를 가지고 있는데 원칙에 어긋나는것은 아닌가? ⇒ 아니다.
- Component와 WeaponAsset은 서로 독립적인 관계이기 때문에 소유해도 괜찮다.
- 0레벨 : CPlayer / 1레벨 : CWeaponComponent / 2레벨 ; CWeaponAsset
SOLID
- S : 단일 책임 원칙 - 모든 클래스는 하나의 책임만 가져야한다.
- O : 개방 폐쇄 원칙 - 수정에는 닫혀있고, 추가에는 열려있어야한다.
- L : 라스코프 치환 원칙 - 부모를 빼더라도 같은 레벨의 객체들은 서로를 소유하지 않는다.