이번에 만든 것

목차
- 머티리얼
- 위젯 블루프린트
머티리얼을 사용해서 해당 도넛을 만들 것이라 머티리얼을 생성한다.
머티리얼 도메인: User Interface
블렌드 모드: Masked 또는 Translucent

Opaque는 불투명으로 투명도를 넣을 수 없어서 사용할 수 없다.
Masked를 대신 사용해도 되는데, 안티에일리어싱이 적용되지 않는다.
Translucent
Masked
도넛을 그리기 위해 우선 원부터 그려야 한다. 이를 위해서 사용하는 중요한 함수가 있다
RadialGradientExponential

여기서 사용할 것이 Radius와 Density이다
Radius는 원의 넓이를 의미한다. 그러나 0.5을 초과하면 표현 범위를 초과해버리니 0.5 이하가 되도록 한다.



Density(밀도)값이 올라갈수록 경계선이 명확해진다


결과 
체커보드를 켜두자
오파시티에 연결하지 않으나 연결하나 똑같은 화면인데 체커보드를 켜두면 투명한 배경이 표시된다. 오파시티에 연결하지 않으면 체커보드를 켜두고도 검정색 배경인데, 실제로 돌아갈 때 검정색 배경도 함께 나타난다는 의미다.
이건 별로 어렵지 않다.
방금 한 작업을 복제하여 만들고 원의 크기를 줄인 다음 빼주자

두께를 파라미터로 받아 유동성 있게 만들려고 한다.
Thickness 라는 Constant를 하나 생성한다.

값의 범위는 0~100 이고 100 이면 부채꼴, 0이면 아예 사라져야 한다. 위에서 말했다시피 Radius는 0.5를 벗어나면 안되고 이 값은 지울 원을 그리는 것이니
Thickness 100에선 Radius가 0이 되며
Thickness 0에선 Radius가 0.5가 되어야 한다.

VectorToRadialValue 이 함수를 사용한다.

이 함수에 대한 상세한 설명은 여기 참조
Radial Coordinates가 0~1사이의 값을 가진다

하지만 사진을 보면 B의 크기는 0.2인데 그려지는 건 거의 80% 그려지는 것 같고, 단면도 생각과는 다르다.
이것은 TexCoord가 왼쪽 위가 기준이라 왼쪽위이기 때문이다. 즉, 우리가 보는 화면은 0~0.25 사이의 공간이다.
좌표계는 0.0~1.0 사이이므로 0.5를 빼줘야 한다.

이 각도 범위를 유동적으로 만들어보자
DegreeSize라는 파라미터를 만들고 이 값이 0~360도 사이 범위를 지니게 만들고 크기가 늘어나면 반시계 방향으로 길어지게 만들어 본다.

회전은 굳이 여기서 안하고 이미지를 불러와서 돌려도 되지만 하는 김에 머티리얼에서 회전 시키는 것까지 적용해본다.
이번에 필요한 함수는 CurstomRotator

이번엔 시작부터 StartDegree라는 파라미터를 둔다. 0도에서 시작하는 회전 위치는 아래 방향(+270도)으로 둔다.
이를 위해 Rotation Angle은 0.0~1.0 범위를 가지므로 ( (초기값+270) % 360 ) / 360

이 값을 Rotation Angle에 연결하고
UVs(V2)에는 아까 TexCoord에 -0.5 한 결과를 넣어주며
기준 점은 변경된 좌표계 기준으로 (0, 0)

색깔도 넣을 수 있게 베이스 컬러를 따로 빼고 계산 값은 Opacity(오파시티)에 연결하자
전체 머티리얼

머티리얼 인스턴스로 필요한 것을 만들어두자

- 머티리얼 원본
- 배경도넛
- 배경도넛의 구분선(필수 아님)
- 크리티컬 라인
- 크리티컬 범위
- 크리티컬 범위 구분선(필수 아님)
사용자 위젯을 생성하고 다음과 같이 만들어 두자

이제 C++로 사용자위젯 상속 받는 클래스 하나를 생성한다.
이후 위젯 블루프린트의 부모 클래스를 방금 만든 것으로 변경한다.
중요 멤버 함수 역할을 설명하자면
NativeConstruct: 멤버 변수에다가 위젯 포인터를 넣어준다. GetWidgetFromName 사용
SetAlpha, SetLineAlpha: 플레이어가 상태(조준/비조준)에 따라 변경된다. 크리티컬 라인이 너무 투명해진 것 같아서 분리한 것이지 하나로 통합해도 상관 없다. UImage의 SetOpacity로 투명도 설정
SetCriticalRange: 치명타 크기가 주어지면 해당 범위에 맞는 머티리얼을 생성하면서 중심이 상단에 위치하도록 조절한다. 다이나믹 머티리얼을 생성한다.
SetCriticalLine: 크리티컬 라인을 회전 시킨다. UImage의 SetRenderTransformAngle으로 회전
그 외에 크라티컬 라인이 움직이는 것은 컨트롤러가 틱마다 호출하는데, 크리티컬 범위 안에서 사격 되었을 경우, 일정 시간 동안 회전 속도를 감소시킨다
.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "../Character/Soldier.h"
#include "CrosshairWidget.generated.h"
UCLASS()
class PROJECTA_API UCrosshairWidget : public UUserWidget
{
GENERATED_BODY()
private:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reference", meta = (AllowPrivateAccess = "true"))
UMaterialInterface* RangeMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reference", meta = (AllowPrivateAccess = "true"))
UMaterialInterface* RangeLineMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting", meta = (AllowPrivateAccess = "true"))
float ActivateAlpha;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting", meta = (AllowPrivateAccess = "true"))
float DisactivateAlpha;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Setting", meta = (AllowPrivateAccess = "true"))
float LineDisactivateAlpha;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Data", meta = (AllowPrivateAccess = "true"))
float CriticalRangeSize;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Data", meta = (AllowPrivateAccess = "true"))
float CriticalLineDegree;
class UImage* m_CriticalBackgroundImg;
class UImage* m_CriticalRangeImg;
class UImage* m_CriticalLineImg;
class UImage* m_Border1;
class UImage* m_Border2;
class UImage* m_RangeLine1;
class UImage* m_RangeLine2;
class UTextBlock* m_Rounds;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Data", meta = (AllowPrivateAccess = "true"))
float EmphasizeTime;
bool bEmphasize;
float RemainEmphasizeTime;
float CurAlpha;
public:
UFUNCTION(BlueprintCallable)
void SetCriticalRange(float Degree);
UFUNCTION(BlueprintCallable)
void SetCriticalLine(float Degree);
UFUNCTION(BlueprintCallable)
void PullCriticalLine(float Degree);
void SetAlpha(float _Alpha);
void SetLineAlpha(float _Alpha);
void SetCursorActivate(bool _Activiate);
void EmphasizeCritical();
void SetRounds(int _Rounds);
protected:
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& _geo, float _DT) override;
virtual void PostLoad() override;
};
.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "CrosshairWidget.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
#include "Kismet/GameplayStatics.h"
void UCrosshairWidget::SetCriticalRange(float Degree) {
if (Degree < 0) Degree = 0;
if (Degree > 360) Degree = 360;
if (IsValid(RangeMaterial)) {
UMaterialInstanceDynamic* DynMaterial = UMaterialInstanceDynamic::Create(RangeMaterial, this);
DynMaterial->SetScalarParameterValue(FName(TEXT("StartDegree")), 180 - Degree / 2);
DynMaterial->SetScalarParameterValue(FName(TEXT("DegreeSize")), Degree);
m_CriticalRangeImg->SetBrushFromMaterial(DynMaterial);
}
if (IsValid(RangeLineMaterial)) {
UMaterialInstanceDynamic* DynMaterial1 = UMaterialInstanceDynamic::Create(RangeLineMaterial, this);
DynMaterial1->SetScalarParameterValue(FName(TEXT("StartDegree")), 180 - Degree / 2);
m_RangeLine1->SetBrushFromMaterial(DynMaterial1);
UMaterialInstanceDynamic* DynMaterial2 = UMaterialInstanceDynamic::Create(RangeLineMaterial, this);
float RangeLineSize;
RangeLineMaterial->GetScalarParameterValue(FHashedMaterialParameterInfo(FName(TEXT("DegreeSize"))), RangeLineSize);
DynMaterial2->SetScalarParameterValue(FName(TEXT("StartDegree")), 180 + Degree / 2 - RangeLineSize);
m_RangeLine2->SetBrushFromMaterial(DynMaterial2);
}
}
void UCrosshairWidget::SetCriticalLine(float Degree) {
CriticalLineDegree = Degree;
m_CriticalLineImg->SetRenderTransformAngle(CriticalLineDegree);
}
void UCrosshairWidget::PullCriticalLine(float Degree) {
CriticalLineDegree -= Degree;
}
void UCrosshairWidget::SetAlpha(float _Alpha)
{
CurAlpha = _Alpha;
if (IsValid(m_CriticalBackgroundImg)) m_CriticalBackgroundImg->SetOpacity(_Alpha);
if (IsValid(m_CriticalRangeImg)) m_CriticalRangeImg->SetOpacity(_Alpha);
if (IsValid(m_Border1)) m_Border1->SetOpacity(_Alpha);
if (IsValid(m_Border2)) m_Border2->SetOpacity(_Alpha);
}
void UCrosshairWidget::SetLineAlpha(float _Alpha)
{
if (IsValid(m_CriticalLineImg)) m_CriticalLineImg->SetOpacity(_Alpha);
if (IsValid(m_RangeLine1)) m_RangeLine1->SetOpacity(_Alpha);
if (IsValid(m_RangeLine2)) m_RangeLine2->SetOpacity(_Alpha);
}
void UCrosshairWidget::SetCursorActivate(bool _Activiate)
{
SetAlpha(_Activiate ? ActivateAlpha : DisactivateAlpha);
SetLineAlpha(_Activiate ? ActivateAlpha : LineDisactivateAlpha);
}
void UCrosshairWidget::EmphasizeCritical()
{
if (IsValid(m_CriticalRangeImg)) {
RemainEmphasizeTime = EmphasizeTime;
bEmphasize = true;
m_CriticalRangeImg->SetColorAndOpacity(FLinearColor(1.f, .5f, .5f));
}
}
void UCrosshairWidget::SetRounds(int _Rounds)
{
if (IsValid(m_Rounds)) {
FString text = FString::Printf(TEXT("%d"), _Rounds);
m_Rounds->SetText(FText::FromString(text));
}
}
void UCrosshairWidget::NativeConstruct() {
Super::NativeConstruct();
m_CriticalBackgroundImg = Cast<UImage>(GetWidgetFromName(FName("CriticalBackground")));
m_CriticalRangeImg = Cast<UImage>(GetWidgetFromName(FName("CriticalRange")));
m_CriticalLineImg = Cast<UImage>(GetWidgetFromName(FName("CriticalLine")));
m_RangeLine1 = Cast<UImage>(GetWidgetFromName(FName("CriticalRangeLine1")));
m_RangeLine2 = Cast<UImage>(GetWidgetFromName(FName("CriticalRangeLine2")));
m_Border1 = Cast<UImage>(GetWidgetFromName(FName("CriticalBackgroundBorder1")));
m_Border2 = Cast<UImage>(GetWidgetFromName(FName("CriticalBackgroundBorder2")));
m_Rounds = Cast<UTextBlock>(GetWidgetFromName(FName("Rounds")));
SetCursorActivate(false);
}
void UCrosshairWidget::NativeTick(const FGeometry& _geo, float _DT) {
Super::NativeTick(_geo, _DT);
//CriticalLineDegree += _DT * 360;
//CriticalLineDegree = FMath::Fmod(CriticalLineDegree, 360.f);
//SetCriticalLine(CriticalLineDegree);
if (bEmphasize) {
RemainEmphasizeTime -= _DT;
if (RemainEmphasizeTime < 0) {
bEmphasize = false;
m_CriticalRangeImg->SetColorAndOpacity(FLinearColor(1.f, 1.f, 1.f));
}
}
}
void UCrosshairWidget::PostLoad()
{
Super::PostLoad();
SetCursorActivate(false);
}
(잡담) 만들게 된 배경
학원에서 하는 교육 외에 학습용 프로젝트를 시작했다. 적어도 차이점이 있어야 의미가 있다고 여겨 시점을 3인칭이 아닌 쿼터뷰로 정했다.
개인적으로 쿼터뷰 시점에서의 사격은 손맛?이 없었다.
마우스를 흔들리게 해서 맞추기 어렵게 하는 것은 거슬리는 느낌이었고, 실제 FPS게임들의 헤드샷처럼 하이리스크 하이리턴, 몸통 쪽에 사격하는 로우리스크 로우리턴 두 개를 넣고 싶었다.
고민 중 생각난 것이 블리츠1941 구축전차 공격인데, 조준선이 와이퍼 마냥 왔다갔다 하면서 중간에 도달할 때, 발사하면 꽤 강력한 데미지가 들어갔다. 이걸 응용해서 만들기로 했다.
(잡담) 아니 설계!
대체 크리티컬 판정을 어디서 해야 하지?
Character - 컨트롤러, 애니메이션, 무기의 연결고리, 즉 중심
Rifle - 크리티컬 확률
Controller - 케릭터 제어 및 위젯 제어
위젯은 사용자에게 보여주기 위한 UI이니까 판정을 하지 않음
지금 크리티컬 하나 때문에 결합도가 엄청 올라간 것 같아서 많이 거슬린다.
또 사격도 바로 나가는 게 아니라 애니메이션 기준으로 특정 노티파이에서 사격하는데, 난 애니메이션이 데이터 흐름의 주도권을 가졌다는 느낌이 든다. 이게 올바른 건지는 잘 모르겠다