1. Override
Weapon을 장착하기 위해서 Item 클래스에서 파생된 클래스를 생성해야 한다.
그리고 overlap 관련 함수들을 override하여 재정의해야 한다.
우선 Item class에서 파생된 Weapon class를 생성한다.
Item class를 우클릭하면 생성 가능하다.
// Item.h virtual void OnSphereBeginOverlap(...); virtual void OnSphereEndOverlap(...);
Weapon class에서 overlap 관련 함수를 override하기 위해선 Item.h에서 관련 함수 앞에 virtual을 붙여주어야 한다.
// Weapon.h UCLASS() class STUDY_API AWeapon : public AItem { GENERATED_BODY() protected: virtual void OnSphereBeginOverlap(...) override; virtual void OnSphereEndOverlap(...) override; ... };
Weapon의 경우 override하기 때문에 UMACRO지정을 할 수 없고, 마지막에 override 키워드를 넣어주어야 한다.
// Weapon.cpp void AWeapon::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { } void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { }
cpp파일은 구현만 해두고 실행하면 override에 의한 함수 재정의 때문에 좌측 상단에 debugmessage가 발생하지 않는 것을 확인할 수 있다.
부모의 함수 그대로 적용시키고 싶다면// Weapon.h void AWeapon::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { Super::OnSphereBeginOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult); } void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { Super::OnSphereEndOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex); }
Super::를 이용하여 코드를 작성하면 된다.
Socket
무기를 장착하려면 장착할 위치에 대한 transform이 있어야 한다. 이를 얻기 위해서 필요한 것이 socket이다.
skeletal mesh를 열어보면 skeletal tree를 확인할 수 있다.
hand_r
에 우클릭하고Add Socket
을 클릭하면 Socket을 추가할 수 있다.Blueprint에서 구현
캐릭터가 구체에 오버랩된 경우 무기 장착이 가능하도록 하기 위해 좌측의 컴포넌트 패널에서 overlap sphere를 선택하고
On Component Begin Overlap
노드를 생성한다.
구체가 캐릭터와 겹치는지 확인해야 하므로 Other Actor 핀에서 끌고 나와 cast BP_WraithChracter 노드를 생성한다.
cast 성공시, 캐릭터의 RightHandSocket에 Weapon을 부착할 것이므로Attach Component to Component
노드를 생성해서 연결한다.
이때 Target은 Attach될 대상, 즉 weapon이고, Parent는 캐릭터의 메시, Socket Name은 attach할 캐릭터의 소켓명이다.
Location Rule, Rotation Rule, Scale Rule은 각각 3개의 옵션이 있다.
Snap to Target을 선택해야 목표에 맞게 작동한다.
World Simulated Bodies는 물리적인 시뮬레이션을 진행할지에 대한 여부를 선택하게 한다.(여기서는 true, false 상관없으므로 초기값 그대로 유지)
- Keep Relative
부모 오브젝트의 변화가 발생해도, 자식 오브젝트는 이전의 상대적인 위치, 회전 및 스케일을 유지하는 것 같음.- Keep World
새로운 부모에 부착된 컴포넌트가 이전과 동일한 전역 위치와 방향(월드기준)을 유지하도록 상대적인 변환을 자동으로 게산하는 것 같음.- Snap to Target
transform을 부착 지점에 맞추는 것을 의미.
컴파일 후 실행하면 손에 무기가 부착되는 것을 확인할 수 있다.(자세, 모션등은 추후 따로 정리)cpp에서 구현
// Weaon.cpp ... #include "Characters/WraithChracter.h" ... void AWeapon::OnSphereBeginOverlap(...) { Supper::OnSphereBeginOverlap(...); // OtherActor가 AWraithCharacter 오브젝트일 경우 Cast는 AWraithCharacter 리턴 AWraithCharacter* WraithCharacter = Cast<AWraithCharacter>(OtherActor); if(WraithCharacter) { // blueprint에서 AttachmentComponentToComponent 노드의 Attachment rule FAttachmentTransformRules TransformRule(EAttachmentRule::SnapToTarget, true); // blueprint에서 AttachmentComponentToComponent 노드와 같은 역할 WeaponMesh->AttachToComponent(WraithCharacter->GetMesh(), TransformRule, FName("RightHandSocket")); } ... }
임시로 확인하기 위해 캐릭터 블루프린트에 들어가서 Details 패널에 animation을 검색하고 Use Animation Asset을 선택한 후 idle 애니메이션을 플레이시킨다.
RightHandSocket의 transform을 적절히 수정하여 실제로 칼을 쥐고 있는 것처럼 수정한다.
Item PickUp
지금까지 구현한 것들은 바닥에 있는 아이템을 overlap되자마자 전부 획득하게 되는 문제가 발생한다.
원하는 것들만 캐릭터가 장착할 수 있도록 PickUp 기능을 구현해야 한다.
먼저 프로젝트 세팅에서 액션 매핑을 지정해준다.
액션매핑에 바인딩하기 위한 콜백함수를 만들어야 하고, 아이템에 대한 정보를 저장할 변수와 설정할 setter가 필요하다.// WraithCharacter.h ... class AItem; ... protected: // Item PickUp 함수 void PickUp(); ... private: // 캐릭터와 overlap하는 아이템 // 아이템에서 캐릭터 캐스팅을 통해 정할 것이므로 setter 필요 UPROPERTY(VisibleAnywhere) AItem* OverlappingItem; ... public: // 인라인화해서 효율적으로 컴파일되도록(그냥 inline은 컴파일러가 최종적으로 인라인결정) FORCEINLINE void SetOverlappingItem(AItem* Item) { OverlappingItem = Item; } ...
이어서 Item.cpp 파일에서 캐릭터를 캐스팅하여 setter함수를 통해 Overlap된 아이템을 넘기도록 한다.
... #include "Characters/WraithCharacter.h" ... // Item.cpp ... void AItem::OnSphereBeginOverlap(...) { AWraithCharacter* WraithCharacter = Cast<AWraithCharacter>(OtherActor); if(WraithCharacter) { WraithCharacter->SetOverlappingItem(this); } } void AItem::OnSphereEndOverlap(...) { AWraithCharacter* WraithCharacter = Cast<AWraithCharacter>(OtherActer); if(WraithCharacter) { // overlap이 끝난 상태이므로 nullptr 전달 WraithChracter->SetOverlappingItem(nullptr); } } ...
이어서 Weapon.h에 Equip 함수를 선언한다(Weapon에서 오버랩시 캐릭터에 부착되도록 코드가 짜여있음).
// Weapon.h ... public: // WraithCharacter에서 weapon을 캐스팅해서 사용할 예정이므로 public에 작성 // AttachmentToComponent의 parameter // (USceneComponent* Inparent, const FAttachmentTransformRules &AttachmentRules, FName InSocketName = NAME_None) void EquipWeapon(USceneComponent* Inparent, FName InSocketName); ... ...
이어서 Weapon.cpp에서 EquipWeapon 함수를 구현한다.
// Weapon.cpp ... void Aweapon::EquipWeapon(USceneComponent* InParent, FName InSocketName) { FAttachmentTransformRules TransformRules(EAttachmentRules::SnapToTarget, true); ItemMesh->AttachmentToComponent(InParent, TransformRules, InSocketName); } void AWeapon::OnSphereBeginOverlap(...) { Super::OnSphereBeginOverlap(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult); // 필요없어진 부분 삭제 /* AWraithCharacter* WraithCharacter = Cast<AWraithCharacter>(OtherActor); if (WraithCharacter) { FAttachmentTransformRules TransformRule(EAttachmentRule::SnapToTarget, true); WeaponMesh->AttachToComponent(WraithCharacter->GetMesh(), TransformRule, FName("RightHandSocket"));*/ } } ...
이어서 WraithChracter.cpp에 필요한 부분을 수정한다.
// WraithCharacter.cpp ... #include "Items/Item.h" #include "Items/Weapons/Weapon.h" ... void AWraithCharacter::PickUp() { AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem); if(OverlappingWeapon) { OverlappingWeapon->EquipWeapon(GetMesh(), FName("RightHandSocket")); } } ... void AWraithCharacter::SetupPlayerInpuComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); ... // ActionBinding PlayerInputComponent->BindAction(FName("PickUp"), IE_Pressed, this, &AWraithCharacter::PickUp); ... }
간단요약
1. 캐릭터가 오버랩된 아이템을 가질 수 있도록 ActionMapping과 바인딩 관련 코드 작성.
2. 캐릭터가 오버랩된 아이템을 알수있도록 AItem 타입의 OverlappingItem과 setter 생성
3. 아이템은 델리게이트를 통해 오버랩된 아이템을 캐릭터의 setter를 통해 전달
4. 웨폰은 EquipWeapon이란 함수를 캐릭터가 사용할 수 있도록 선언 및 구현
5. 캐릭터가 item을 weapon으로 캐스팅해서 weapon의 EquipWeapon에 값 전달, Weapon에서는 EquipWeapon 함수를 통해 캐릭터가 무기를 장착하도록 함.
추가로 발생하는 오류: Item이 StaticMeshComponent인데 Weapon이 SkeletalMeshComponent이고 SkeletalMesh를 AttachToComponent할 경우 발생함.
Weapon의 StaticMesh를 비워두고 따로 SkeletalMesh 생성후 SkeletalMesh만 넣어줘도 작동함.
Character State 추가
캐릭터가 무기를 소유한 상태에서 추가로 바닥에 있는 무기를 집으면 안된다.
또한 무기를 장착하지 않은 상태에서 무기를 착용해제하는 것도 안된다.
이를 보완하기 위해 enum을 이용해서 캐릭터 상태를 추가하고, 상태에 따라 캐릭터가 행동을 취할 수 있도록 한다.// WraithCharacter.h // 상태가 몇백개씩 되는 게 아니므로 최적화를 위한 uint8 // UENUM() 매크로를 사용해 블루프린트에서도 사용 가능 // UMETA() 매크로를 사용해 블루프린트에서 편안하게 사용 가능 ... UENUM(BlueprintType) enum class ECharacterStates : uint8 { // 실제로는 integer 목록들(UnEquipped는 0, EquippedOneHandWeapon은 1) ECS_UnEquipped UMETA(DisplayName = "UnEquipped"); ECS_EuippedOneHandWeapon UMETA(DisplayName = "EquippedOneHandedWeapon"); }; ... private: ECharacterStates CharacterState = ECharacterStates::ECS_UnEquipped; ...
cpp파일에서 무기를 잡을 때 캐릭터 상태가 변하도록 코드를 수정한다.
// WraithCharacter.cpp void AWraithCharacter::PickUp() { AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem); if(OverlappingWeapon) { OverlappingWeapon->EquipWeapon(GetMesh(), FName("RightHandSocket")); CharacterState = ECharacterStates::EquippedOneHandWeapon; } }
이제 AnimInstance파일에 상태에 따른 캐릭터 동작을 구현하면 되는데, 한가지 중요한 점이 있다.
AnimiInstance에서 ECharacterStates 타입의 변수를 사용하기 위해서 캐릭터의 헤더파일을 include해야되는데, 캐릭터 헤더파일은 ECharacterStates말고도 여러 헤더파일이 포함되어 있다.
컴파일을 실행하면 컴파일러가 캐릭터 헤더파일에 include된 모든 헤더파일을 읽어들여야 하므로 최적화가 필요하다.
그러므로 캐릭터 상태만 나타나는 헤더파일을 하나 만들어주면 최적화가 가능하다.
캐릭터 관련 파일이 있는 곳에 CharacterTypes라는 헤더파일을 하나 만들어준다.
// CharacterTypes.h // WraithCharacter.h에 작성해둔 코드 복사후 붙여넣기 #pragma once // 캐릭터 무기 장착여부 enum UENUM(BlueprintType) enum class ECharacterStates { ECS_UnEquipped, ECS_EquippedOneHandedWeapon, ECS_EquippedTwoHandedWeapon };
그리고 캐릭터에서 CharacterStates 사용 가능하도록 헤더파일을 include시켜준다.
추가로 ECharacterStates는 private에 있으므로 다른 cpp파일에서 캐릭터 상태를 참조하려면 Getter가 필요하다.// WraithCharacter.h ... #include "CharacterTypes.h" ... public: FORCEINLINE ECharacterStates GetCharacterState() const { return CharacterState; }
그리고 AnimInstance에서 CharacterStates를 사용하기 위해 헤더파일을 추가해주고, 변수를 선언한다.
// WraithAnimaInstance.h ... #include "CharacterTypes.h" ... private: // 캐릭터 상태 // 캐릭터 상태에 따라 animation을 변경하는 것이므로 read만 필요 UPROPERTY(BlueprintReadOnly, Category = "Movement | Character State", meta = (AllowPrivateAccess = "true")) ECharacterStates CharacterState; ...
마지막으로 NativeUpdateAnimation쪽에 캐릭터의 상태를 정해주면 끝이다.
// WraithAnimInstance.cpp void UWraithAnimInstance::NativeUpdateAnimation() { Super::NativeUpdateAnimmation(DeltaTime); if(WraithCharacterMovement) { ... CharacterState = WraithCharacter->GetCharacterState(); ... } }
이제 CharacterState에 따라 애니메이션을 변경하도록 수정한다.
ECharacterStates에 따라 애니메이션 변경이 가능하도록 blend poase한다.그냥 실행하면 총이 보이므로 매테리얼 복사본을 만들어 적절히 수정해준다.
무기 장착시 idle모션도 정상적으로 나온다.
Item State
Weapon은 아이템으로부터 파생된 것이므로 호버링이 적용된다.
문제는 무기 장착시에도 호버링이 적용되는 것인데, 해결하기 위해Item State
를 추가한다.// Item.h UENUM(BlueprintType) enum class EItemStates : uint8 { EIS_Hovering UMETA(DisplayName = "Hovering"), EIS_Equipped UMETA(DisplayName = "Equipped") }; ... protected: EItemStates ItemState = EItemStates::EIS_Hovering; ...
cpp파일도 수정한다.
// Item.cpp ... void AItem::Tick(float DeltaTime) { ... if(ItemState == EItemStates::EIS_Hovering) { // hovering 구현 AddActorWorldOffset(FVector(0.f, 0.f, TransformedSin())); } } ...
weapon 클래스에서 EquipWeaon 함수가 실행되면 아이템 상태가 바뀌도록 코드를 수정한다.
void AWeapon::EquipWeapon() { ... ItemState = EItemStates::EIS_Equipped; }