무기 시스템 - 활
- 이전과 이어서 시작
화살 발사 / 추가 발사
- 화살은 ‘카메라’가 바라보는 전방으로 발사되어야한다.
- 캐릭터 전방으로 맞추면 화살이 날아가는 방향이 바라보는 시점의 방향과 어긋날 수 있다.
- CDoAction_Bow
- CPP 파일 - ‘엑션 시작’ 함수 확인 - ‘쿼터니온’에 ‘오너 캐릭터’의 ‘카메라 회전값 가져오기’를 호출하여 넣은 호출의 ‘전방 벡터 가져오기’ 함수를 호출하고 ‘전방’ 변수를 선언하여 저장한다. - ‘화살’의 ‘발사’ 함수에 ‘전방’ 변수를 넣고 호출한다. - ‘엑션 종료’ 함수 확인 - ‘화살 생성’ 함수 호출
//CPP //카메라가 바라보는 전방 FVector forward = FQuat(OwnerCharacter->GetControlRotation()).GetForwardVector(); arrow->Shoot(forward); //화살 발사 void UCDoAction_Bow::End_DoAction() { Super::End_DoAction(); CreateArrow(); //화살 생성 } - BP_CArrow
- 속도 조정은 블루프린트에서 실행한다.
- 발사체 컴포넌트 - 디테일 - 초기 속도 : 200
화살 충돌
- 화살의 히트 이벤트들은 ‘DoAction_Bow’에 준다.
- DoAction_Bow에서 화살이 충돌하면 삭제시키는 작업도 할것이기 때문이다.
- 화살을 현재 충돌체에 꽂힌다.
- 해결법
- 맞자마자 삭제되도록 설정한다.
- 충돌 체크를 충돌체와 하는것이 아니라 메쉬와 한다. (복잡 하다)
- 맞은 위치에서 가장 가까운 ‘본’의 위치를 가져와서 그곳에 붙힌다.
- 해결법
- 현재 충돌 후 발사시 문제점 : 화살은 충돌 후 일정시간이 지나면 삭제되도록 했지만, 배열 상으로는 안의 요소가 삭제되어 null이 되는것이지 공간이 삭제되는 것이 아니다.
- 해결 방법 : 충돌 되었다는 사실이 알려지면 리스트도 삭제한다.
- 모든 엑터는 사라지는 이유를 반환한다. : EEndPlayReason::Type
- 즉, 델리게이트를 선언하고 EEndPlayReason::Type를 받도록 매개변수를 설정한다.
- 들어온 매개변수를 이용하여 삭제되었다는 것을 알았고, 그걸 이용해서 배열의 공간을 삭제한다.
- CArrow
- 화살의 충돌 프로파일 설정을 ‘전부 블록’으로 지정했으니, Hit 이벤트로 충돌을 처리한다.
- 충돌된 객체가 ‘Ignores’에 넣어진 객체와 같으면 충돌 시키지 않는다.
- 헤더 파일 - 델리게이션 선언 , ‘화살 충돌’ 이름으로 ‘충돌 무기 엑터, 충돌된 캐릭터’ 를 받는다. - 델리게이션 자료형인 ‘화살 충돌’의 ‘충돌 활성’ 변수 선언 - 캡슐 컴포넌트와 연결할 ‘컴포넌트 히트’ 이벤트 함수 선언 - ‘충돌 후 스폰 시간’ 변수 선언
//header DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FArrowHit, class AActor*, InCauser, class ACharacter*, InOtherCharacter); private: UPROPERTY(EditDefaultsOnly, Category = "LifeSpawn") float LifeSpawnAfterCollision = 1; //객체가 사라지기까지 걸리는 시간 private: UFUNCTION() void OnComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit); public: FArrowHit OnHit; //델리게이트 화살 히트 변수- CPP 파일 - ‘컴포넌트 히트’ 이벤트 함수 정의 - ‘스폰 유지 설정’ 함수에 ‘충돌 후 스폰 시간’을 넣고 호출한다. - ‘무시들’의 요소를 하나씩 ‘엑터’변수에 넣고 반복한다. : ‘엑터’와 ‘충돌된 다른 엑터’가 같은지 확인한다. - ‘충돌된 다른 엑터’를 ‘캐릭터’ 클래스로 ‘캐스트’ 함수를 호출하고 ‘캐릭터’ 변수를 선언하고 저장한다. - ‘캐릭터’가 비어있지 않고, ‘히트 활성’에 ‘함수 연결 확인’이 false가 아니라면 : ‘히트 활성’의 ‘연결된 모든 함수 실행’에 ‘충돌 무기 : 현재 클래스(화살), 충돌된 캐릭터’를 넣고 호출한다. - BeginPlay 함수 확인 - ‘캡슐’의 ‘컴포넌트 히트 활성’의 ‘연결 함수 추가’에 ‘연결될 함수가 있는 클래스 : CArrow, 연결할 함수 : OnCOmponentHit’를 넣고 호출한다. - 타이머 델리게이션을 위해 타이머 핸들 구조체의 ‘핸들’ 변수 선언 - 타이머 델리게이스 구조체의 ‘타이머 델리게이스 선언 후 ‘타이머 델리게이트 : 람다 생성[외부 변수 받기]로 설정한다. : ‘캡슐’의 ‘콜리전 활성 설정’ 함수에 ‘콜리전 활성 열거 :오버랩과 충돌’을 넣고 호출한다. - ‘월드 가져오기’ 함수 호출의 ‘타이머 매니저 가져오기’ 함수 호출의 ‘타이머 설정’ 함수에 ‘핸들, 타이머 델리게이트, 몇초동안 실행할건지 : 0.1초, 반복 여부 : false’를 넣고 호출한다. - ‘컴포넌트 히트 활성’ 함수 확인 - ‘캡슐’의 ‘콜리전 활성 설정’ 함수에 ‘콜리전 활성 : 충돌 안함’을 넣고 호출한다.
//CPP //두 액션의 화살충돌 델리게이션 연결 Capsule->OnComponentHit.AddDynamic(this, &ACArrow::OnComponentHit); void ACArrow::Shoot(const FVector InForward) { //전방 벡터 * 초기 속도 Projectile->Velocity = InForward * Projectile->InitialSpeed; Projectile->SetActive(true); //날아가기 활성 //플레이어 충돌때문에 발사 지연시켜서 충돌 활성화 FTimerHandle handle; FTimerDelegate timerDelegate = FTimerDelegate::CreateLambda([&]() { Capsule->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); //충돌 활성 }); GetWorld()->GetTimerManager().SetTimer(handle, timerDelegate, 0.1f, false); } void ACArrow::OnComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) { //생성한 객체 파괴되기까지의 시간설정 SetLifeSpan(LifeSpawnAfterCollision); for (AActor* actor : Ignores) { CheckTrue(actor == OtherActor); } Capsule->SetCollisionEnabled(ECollisionEnabled::NoCollision); //충돌 비활성화 ACharacter* character = Cast<ACharacter>(OtherActor); if (!!character && OnHit.IsBound()) { OnHit.Broadcast(this, character); } } - DoAction_Bow
- 헤더 파일 - ‘화살 충돌 활성’ 함수에 ‘공격 무기 엑터, 충돌된 캐릭터’를 넣고 선언한다.
//header private: //화살 충돌 활성 함수 UFUNCTION() void OnArrowHit(class AActor* InCauser, class ACharacter* InOtherCharacter);- CPP 파일 - ‘엑션 시작’ 함수 확인 - 화살이 쏴지기 전 손에서 떨어진 화살에 ‘히트’ 이벤트 델리게이션을 연결하기 위해 전방 벡터 저장 전 확인 - ‘화살’의 ‘충돌 활성’ 의 ‘함수연결’ 함수에 ‘연결 함수가 있는 클래스, 연결 함수 : 화살 충돌’을 넣고 호출한다 - ‘화살 충돌’ 함수 정의 - ‘히트 데이터들’의 ‘요소 수 반환’ 함수를 호출한 결과가 0개가 아닌지 확인한다. - ‘히트 데이터즈’의 ‘0번 순서’의 ‘데미지 전달’ 함수에 ‘오너 캐릭터, 공격 무기, 충돌된 캐릭터’를 넣고 호출한다.
//CPP arrow->OnHit.AddDynamic(this, &UCDoAction_Bow::OnArrowHit); //화살 충돌 델리게이션 연결 arrow->OnEndPlay.AddDynamic(this, &UCDoAction_Bow::OnArrowEndPlay); //화살 파괴 알리기 델리게이션 연결 void UCDoAction_Bow::OnArrowHit(AActor* InCauser, ACharacter* InOtherCharacter) { CheckFalse(HitDatas.Num() > 0); HitDatas[0].SendDamage(OwnerCharacter, InCauser, InOtherCharacter); } - CAttachment_Bow
- CPP 파일 - 생성자 확인 - ‘스켈레탈 메쉬’의 ‘콜리전 프리팹 이름으로 설정’ 함수에 ‘충돌 안함’을 넣고 호출한다.
//CPP SkeletalMesh->SetCollisionProfileName("NoCollision"); - 델리게이션 순서 정리
- 화살 클래스에 충돌이 일어나면 다른곳의 함수를 호출하는것을 연결하기 위한 ‘델리게이트’ 와 변수 선언
- 화살 클래스 객체에서 충돌이 발생하면 ‘OnComponentHit’ 이벤트가 실행된다.
- 델리게이트 변수에 연결된 함수가 있다면 그것을 실행시킨다.
- DoAction_Bow에서 충돌 관련 함수를 정의한다.
- 화살 클래스를 가져오고, 그 안의 ‘델리게이트 변수’에 함수를 연결한다.
- 충돌이 시작되면 화살 클래스가 충돌 이벤트를 실행하고 그 안의 다른곳과 연결된 함수가 있는지 확인하고, DoAction_Bow에 연결된 함수가 있으니 그것을 실행시키는 것이다.
화살 충돌 되어 삭제됐다는것을 알리고 화살 리스트에서 삭제하기
- CArrow
- 헤더 파일 - 델리게이트 선언 , ‘발사체 실행 종료’라고 명명하고 ‘삭제할 화살’을 받아 선언한다. - 델리게이트 자료형으로 ‘실행 종료 활성’ 변수 선언 - ‘실행 종료’ 함수에 ‘실행 종료 이유’를 받을 수 있도록 재정의 선언한다.
//header DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FProjectileEndPlay, class ACArrow*, InDestroyer); FProjectileEndPlay OnEndPlay; //화살 삭제 알리기 델리게이트 void EndPlay(const EEndPlayReason::Type EndPlayReason) override;- CPP 파일 - ‘실행 종료’ 함수 정의 - 부모 함수 호출 - ‘실행 종료 활성’ 델리게이트 변수에 ‘연결 함수 확인’ 함수를 호출하고 true 라면 : ‘실행 종료 활성’의 ‘연결 함수 실행’에 ‘삭제할 화살 : 현재 클래스’를 넣고 호출한다.
//CPP void ACArrow::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); //화살 파괴 알리기 연결 되었는지 확인 if (OnEndPlay.IsBound()) { OnEndPlay.Broadcast(this); } } - CDoAction_Bow
- 헤더 파일 - 델리게이트를 연결할 ‘화살 실행 종료 활성’ 함수를 선언하고 화살 클래스 포인터의 ‘삭제할 화살’을 받도록 한다.
//header //화살 파괴 활성 알리기 함수 UFUNCTION() void OnArrowEndPlay(class ACArrow* InDestroyer);- CPP 파일 - ‘화살 실행 종료 활성’ - ‘화살들’의 ‘제거’ 함수에 매개변수인 ‘삭제될 화살’을 넣고 호출한다. - ‘엑션 실행’ 함수 확인 - ‘화살’의 ‘실행 종료 활성’ 함수의 ‘함수 연결’에 ‘연결할 함수가 있는 클래스, 연결할 함수’를 넣고 호출한다.
//CPP void UCDoAction_Bow::OnArrowEndPlay(ACArrow* InDestroyer) { //호출되면 들어온 객체를 화살 리스트에서 삭제한다. Arrows.Remove(InDestroyer); }
화살 충돌 데미지 전달
- CDoAction_Bow
- CPP 파일 - ‘화살 히트 활성’ 함수 확인 - ‘히트 데이터’의 ‘요소 수 반환’ 함수 호출 결과가 0보다 큰지 확인 - ‘히트 데이터’의 ‘0번’의 ‘데미지 전달’에 ‘오너 캐릭터, 공격 무기, 충돌된 캐릭터’를 넣고 호출한다.
//CPP void UCDoAction_Bow::OnArrowHit(AActor* InCauser, ACharacter* InOtherCharacter) { CheckFalse(HitDatas.Num() > 0); HitDatas[0].SendDamage(OwnerCharacter, InCauser, InOtherCharacter); } - DA_Bow
- 활은 스킬이나 ‘대궁’같은 특이한 무기가 아닌 이상 필수적인 것만 넣는다.
- HitData에 데미지나 넉벡 등의 정보를 넣는다.
활 줄 수정
- 활 줄은 노티파이를 이용한다.
- 쏠 때(Begin_DoAction)는 줄을 떨어뜨리고, 끝날때는 다시 붙인다.
- 활 줄을 다시 되돌릴때 사용할 ‘벤딩’ 값은 Attachment에 있다.
- DoAction_Bow에서는 Attachment를 사용할 일이 없기에 Attachment_Bow로 바꿀 수 있다.
- CDoAction_Bow
- 헤더 파일 - ‘줄 붙은지 확인’ 변수를 선언하고 true로 초기화한다. - ‘활 줄 종료’ 함수 선언 - 활 줄을 되돌릴때 사용하기 위해 ‘벤딩’ 변수를 포인터로 선언한다.
- CPP 파일 - 매개변수로 받은 Attachment를 Attachment_Bow로 ‘캐스팅’ 함수를 호출하고 결과를 ‘활’ 변수를 선언하여 저장한다.(이게 다운캐스팅이다.) - ‘벤딩’에 ‘활’의 ‘애님 인스턴스 벤딩 가져오기’ 함수 호출 결과를 저장한다. - ‘엑션 시작’ 함수 확인 - 공격을 시작할 때 ‘벤딩’ 값을 0으로 초기화한다. - 활 줄을 떨어뜨리기 위해 ‘줄 붙은지 확인’ 변수는 false로 설정한다. - ‘활 줄 종료’ 함수 정의 - ‘벤딩’ 값을 100으로 설정한다. (벤딩은 0 ~ 100까지의 값이 들어가고, 100일때 활이 당겨지는 상황이다.) - ‘줄 붙은지 확인’ 변수는 true로 설정한다. - Tick 함수 확인 - ‘줄 붙은지 확인’ 변수가 false라면줄을 손에 붙이는 작업은 실행하지 않는다.
//CPP //활 줄 되돌리기 준비 ACAttachment_Bow* bow = Cast<ACAttachment_Bow>(InAttachment); Bending = bow->GetAnimInstance_Bending(); *Bending = 0.0f; bAttachedString = false; //활 줄 떨어뜨리기 void UCDoAction_Bow::End_BowString() { *Bending = 100.0f; bAttachedString = true; //활 줄 다시 붙이기 } - CAnimNotify_EndBowString
- 헤더 파일 - 이전 노티파이와 같이 ‘노티파이 이름수정’과 ‘노티파이 실행’ 함수를 사용한다.
//header private: FString GetNotifyName_Implementation() const override; void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;- CPP 파일 - 이전 노티파이와 같이 작성하고, 실행할 함수가 있는 클래스만 ‘화살 클래스’로 바꾼다. - ‘화살’의 ‘활 줄 종료’ 함수를 호출한다.
//CPP FString UCAnimNotify_EndBowString::GetNotifyName_Implementation() const { return "EndBowString"; } void UCAnimNotify_EndBowString::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) { Super::Notify(MeshComp, Animation, EventReference); CheckNull(MeshComp); CheckNull(MeshComp->GetOwner()); UCWeaponComponent* weapon = CHelpers::GetComponent<UCWeaponComponent>(MeshComp->GetOwner()); CheckNull(weapon); CheckNull(weapon->GetDoAction()); UCDoAction_Bow* bow = Cast<UCDoAction_Bow>(weapon->GetDoAction()); CheckNull(bow); bow->End_BowString(); } - 활 쏘는 몽타주
- 노티파이 할당
UI
플레이어 무기 타입 출력 - 결과 출력은 안하고 화면에만 올렸다.
- UI 요소 하나하나를 ‘위젯’이라고 부른다.
- C에서 ‘메세지’를 블루프린트로 전달할 수는 있다.
- 단점 : SendMessage가 느리다. / 제약 사항이 심하다.
- 해결책 : C에서 UUserWidget을 상속받아 ‘클래스’를 만들고, 블루프린트에서 재정의할 수 있도록 ‘함수’를 만든다.
- 한글 폰트 넣는법은 인터넷에 많으니 찾아봐라
- 원래는 플레이어의 UI는 플레이어에 작성해야하지만, 무기의 상태를 출력할것이니 웨폰 컴포넌트에 작성한다.
- 하는것은 똑같으니 나중에 플레이어에 넣어라
- CUserWidget_Player
- 생성 - UserWidget 상속
- CPlayer
- 헤더 파일 - 유저 위젯_플레이어로 ‘클래스 제한’을 한 ‘유아이 클래스’ 선언 - 유저 위젯_플레이어 포인터 자료형의 ‘유저 인터페이스’ 변수 선언
//header private: UPROPERTY(EditAnywhere, Category = "Evade") TSubclassOf<class UCUserWidget_Player> UiClass; //플레이어 위젯 클래스 저장 private: class UCUserWidget_Player* UserInterface; //유아이 플레이어 저장- CPP 파일 - 생성자 확인 - 유저 위젯_플레이어 클래스로 ‘클래스 가져오기’ 함수 호출하고 ‘유아이 클래스, 위젯 블루프린트 레퍼런스’ 를 넣는다. - BeginPlay 함수 확인 - ‘유아이 클래스’가 비어있지 않다면 : ‘유저 인터페이스’에 ‘위젯 생성’ 함수에 <유저 위젯_플레이어, 플레이어 컨트롤러>를 넣고 ‘컨트롤러 가져오기를 플레이어 컨트롤러로 지정하고 호출, 유아이 클래스’를 넣고 호출한다. - ‘유저 인터페이스’의 ‘뷰포트에 추가’ 함수를 호출한다.
//CPP CHelpers::GetClass<UCUserWidget_Player>(&UiClass, "WidgetBlueprint'/Game/Widgets/WB_Player.WB_Player_C'"); //위젯 있으면 출력 //유저 인터페이스 설정 if (!!UiClass) { UserInterface = CreateWidget<UCUserWidget_Player, APlayerController>(GetController<APlayerController>(), UiClass); UserInterface->AddToViewport(); } - WB_Player
- 콘텐츠 - 유저 인터페이스 - 위젯 플루프린트
- 캔버스 패널 배치 - 그리드 패널 배치 - 채우기 규칙 : 열 채우기 : 2개 추가 / 행 채우기 : 2개 추가 - 그리드 패널 앵커 ‘우 하단’으로 이동 - 정렬 : 1.25 / 1.5 - ‘콘텐츠 크기에 맞춤’ 체크 - 텍스트 위젯 배치 - 텍스트 위젯 복사 - 텍스트 변경 : Player, Weapon - Player 텍스트 오퍼시티 : 0.5 - Weapon 텍스트 컬러 : 0, 1, 0 - Weapon 텍스트 ‘양쪽맞춤’ - 그리드 패널에 ‘텍스트’ 위젯 추가 - 웨폰 타입 숫자를 출력할것이다. - 텍스트 폰트 크기 변경 - Player 텍스트는 0행을 혼자 쓸것이니 ‘열 범위’를 2로 설정한다. - Weapon 패딩 : 5 - 웨폰 타입 숫자 패딩 : 5
UI 설명
- UMG (Unreal Motion Graphic) : 게임 상에서 사용하는 UI를 부르는 이름이다.
- Slate UI : 에디터 상의 UI를 부르는 이름이다.
- Widget은 UMG나 Slate나 똑같은것을 사용한다.
- 디자인만 다르게 보여지는 것이다.
- 즉, 게임 UI를 다룬다면, 에디터 UI도 다룰 수 있다.
위젯 블루프린트 설명
- DPI (Dot Per Inch) : 인치당 점이 몇개 찍혀있냐를 수치로 나타내는 단위이다.
- 예전에는 ‘픽셀’로 수치를 디자인했지만, 현재는 기기마다 해상도가 달라졌기 때문에 ‘인치’ 단위로 바뀌었다.
- 1인치에 ‘점(dot)’이 몇개 들어가느냐다.
- 보통 1 / 96 (1인치에 96개의 점)으로 잡는다.
- 보통 위젯의 디자인은 1280 * 720 (16 : 9) 비율을 맞춰서 디자인한다.
- ‘패널’ 위젯은 애당 위젯 위에 어떠한 위젯들을 올릴 수 있는 위젯이다.
- 일반 - 패널
- 캔버스 패널 : 실제로 게임 화면에 보여질 화면과 1 : 1로 디자인되는 위젯이다.
- 적측은 캔버스를 가지지 않는다.
- 스크린에 배치될 위젯들은 ‘캔버스 패널’로부터 시작한다.
- 그리드 패널 : 공간을 균등하게 분할하는 패널이다.
- 디테일 - 슬롯의 ‘행’과 ‘열’이 패널 내부의 요소를 행 과 열 로 공간 분할이 가능하다.
- 행 합치기 : 행 스판
- 열 합치기 : 열 스판(열 범위)
- 캔버스 패널 : 실제로 게임 화면에 보여질 화면과 1 : 1로 디자인되는 위젯이다.
- 앵커 : 위젯의 위치 중심점을 옮기는 옵션이다.
- 정렬 : 앵커를 기준으로 위치를 맞추는 옵션이다.
- 콘텐츠 크기에 맞춤 ; 체크하면 콘텐츠의 크기에 맞춰서 패널의 크기가 늘어난다.