언리얼엔진/노노그램

[UE5] SCompoundWidget에 Brush 추가, 노노그램 입력개선

mstone8370 2023. 8. 31. 21:51

 

 

지난번 포스팅에서 이어진다.

https://mstone8370.tistory.com/5

 

[UE5] Slate를 이용해서 UI의 마우스 입력을 원하는대로 받기

사용자 위젯으로 UI를 만들때 마우스 이벤트를 내가 원하는 대로 받고싶을 때가 있다. 예를 들면 마우스 호버 이벤트를 버튼이 아닌 위젯에서도 받아오고 싶다거나, 클릭 이벤트를 마우스 오른

mstone8370.tistory.com

 


 

결과 비교

먼저 결과부터 비교해본다.

 

좌: 이전 상태, 우: 현재 상태

왼쪽이 이전에 포스팅 했던 결과고, 오른쪽이 현재 상태다.

변경된점은 다음과 같다.

  1. 선택된 영역을 표시하는 방식을 결과 미리 보기에서 외곽선과 색 채우기로 변경했다.
  2. 현재 호버링 하고있는 칸을 십자 형태로 강조 표시해서 어느 칸을 가리키는지 쉽게 알아볼 수 있게했다. 이전 상태의 마우스 커서 주변에 나타나는 노란색은 녹화 프로그램의 마우스 위치 강조 기능이다.
  3. 마우스 사용 시 마우스 왼쪽 버튼 클릭은 일반적인 칸 채우기, 마우스 오른쪽 버튼 클릭은 X로 표시하도록 했다.
  4. 칸과 칸 사이 구분선도 마우스 입력 영역으로 포함시켰다. 영상으로 볼 때에는 티나지 않지만 직접 플레이하는 경우에는 체감된다. 전에는 구역 선택 후에 칸 위에 커서를 정확히 올리고 마우스 버튼을 떼야 Release 이벤트가 발생했다. 그래서 실수로 칸을 벗어나 구분선 위에서 마우스 버튼을 떼면 Release 이벤트를 받지 못해서 칸을 채우지 못하는 경우가 종종 있었다. 하지만 이제는 그런 문제가 발생하지 않는다.

하나씩 어떻게 구현했는지 설명하겠다.

 

 


 

 

1. 외곽선

이전에는 새로운 위젯을 따로 만들어서 각 칸의 Geometry를 통해 뷰포트에서의 위치와 크기를 계산해서 외곽선을 표현하려고 했다.

블로그에 포스팅을 시작하기 전부터 계속 고민했던 방식이지만, 여러 방법을 사용해봐도 좌표에 오차가 계속 발생했고 외곽선의 크기도 맞지 않았다.

뷰포트의 테두리 두께도 좌표 계산에 영향을 주는듯 했다.

그리고 외곽선의 위치와 크기는 계속 변할텐데, 그 모든 경우에 알맞게 표현이 될지도 확실하지 않았다.

 

그래서 다른 방법이 필요했고, 찾아낸 방법이 각 칸에 외곽선을 추가해서 숨겨두는 방법이다.

대신에 각 칸은 그리드 패널에 들어있으므로 외곽선은 크기를 차지하면 안된다.

만약 외곽선이 그리드 패널의 공간을 차지한다면 각 칸들의 거리가 외곽선의 두께만큼 멀어질 것이다.

캔버스 패널을 적절히 이용하면 쉽게 해결할 수 있다.

 

위젯의 계층구조. 그리드 패널에서 이 위젯은 초록색 선으로 표시된 오버레이의 크기만큼만 차지할 것이다.

여러 칸을 선택한 경우 외곽선은 그 칸들을 감싼것처럼 묘사되어야 하는데, 이를 위해서 외곽선을 나누고 상황에 맞게 외곽선으로 감싸져야할 부분만 표시되게 한다.

계층구조에서 OutlinePannel은 그리드 패널이고 가로 3칸, 세로 3칸으로 되어있다.

 

외곽선의 분리된 영역들

위처럼 구분되어있으며, 각 칸은 이미지이므로 외곽선의 코너부분은 둥근 모양의 텍스쳐를 넣어서 자연스럽게 만들어줬다.

Outline 이미지들을 상황에 맞게 표시하거나 숨기면 된다.

 

경우에 따라 표시하거나 숨길 외곽선은 아래 이미지처럼 정리된다.

Horizontal은 가로로 선택되었는지를 나타내며, Start와 End는 각각 외곽선이 시작된 칸과 끝난 칸이다.

그리드의 왼쪽 상단을 (0, 0)으로 하고, 오른쪽과 아래로 갈 수록 인덱스가 늘어난다고 했을 때, Start의 인덱스는 End의 인덱스보다 항상 작거나 같다.

 

대충 이렇게 정리해봤더니 패턴이 쉽게 보인다.

 

 

 

2. 십자모양으로 칸 위치 강조

퍼즐의 크기가 커질수록 내가 지금 어느 칸을 보고있고, 어느 힌트를 봐야는지 헷갈리기가 쉽다.

이럴때 가로 세로 십자 모양으로 현재 가리키고있는 칸이 어느 칸인지 보여준다면 퍼즐을 푸는데에만 집중할 수 있도록 도와준다.

이 경우에는 외곽선처럼 이미지를 하나 더 추가해서 할 수 있지만, 이전에 공간만 차지하고있는 SCompoundWidget을 추가했으니 여기에 브러쉬를 추가해서 사각 영역을 채우는 방법을 선택했다.

다른 위젯들에는 ColorAndOpacity가 있어서 이것을 변경하면 색이 바뀌었는데 이것과 동일하게 작동하도록 만들었다.

 

일단 C++ 코드로 슬레이트 위젯에 사각형 모양으로 영역을 채우려면 슬레이트 위젯의 OnPaint 함수 내부에 FSlateDrawElement::MakeBox 함수를 호출하면 된다.

이것이 최종 목표다.

 

FSlateDrawElement::MakeBox

이 함수의 인자는 이것저것 많은데 OnPaint 함수에서 인자로 받는 값들과 겹치는 것들이 많다.

그래서 추가적으로 필요한 것은 FSlateBrushFLinearColor만 있어도 충분하다.

 

먼저 슬레이트 위젯인 SNonogramCell 클래스에 FInvalidatableBrushAttributeFLinearColor 타입 변수를 추가한다.

// SNonogramCell.h

private:
	FInvalidatableBrushAttribute Brush;
	FLinearColor Color;

FInvalidatableBrushAttribute 타입의 경우 슬레이트 이미지인 SImage의 코드를 참고해서 가져왔다.

참고로 SCompoundWidgetColorAndOpacity라는 변수를 가지고있지만 Deprecate 됐으므로 새로 추가해야한다.

여기서는 ColorAndOpacity와 구분하기 위해서 변수 이름을 Color로 지정했다.

 

그리고 Argument로 값을 받아 Construct에서 설정하게 한다.

참고로 기존의 코드도 포함되어있다.

// SNonogramCell.h

SLATE_BEGIN_ARGS(SNonogramCell)
	: _Color(FLinearColor::Transparent)
	{
	}
	SLATE_ARGUMENT(FSlateBrush*, Brush)

	SLATE_ARGUMENT(FLinearColor, Color)

	SLATE_EVENT(FPressedSignature, OnPressedArg)

	SLATE_EVENT(FSimpleDelegate, OnReleasedArg)

	SLATE_EVENT(FSimpleDelegate, OnHoveredArg)

	SLATE_EVENT(FSimpleDelegate, OnUnhoveredArg)
SLATE_END_ARGS()



// SNonogram.cpp

void SNonogramCell::Construct(const FArguments& InArgs)
{
	Brush = FInvalidatableBrushAttribute(InArgs._Brush);
	Color = InArgs._Color;
	OnPressed = InArgs._OnPressedArg;
	OnReleased = InArgs._OnReleasedArg;
	OnHovered = InArgs._OnHoveredArg;
	OnUnhovered = InArgs._OnUnhoveredArg;
}

 

그 다음 SNonogramCellOnPaint 함수를 추가해서 override한다.

// SNonogramCell.h

public:
	//~ SWidget overrides
	virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;



// SNonoCell.cpp

int32 SNonogramCell::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
	const FSlateBrush* SlateBrush = Brush.Get();
	FLinearColor FinalColor = Color;
	FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), SlateBrush, ESlateDrawEffect::None, FinalColor);
	return LayerId;
}

 

참고로 SImage.cpp에서는 FinalColorAndOpacity를 다음과 같이 지정한다.

// SImage.cpp

const FLinearColor FinalColorAndOpacity( InWidgetStyle.GetColorAndOpacityTint() * ColorAndOpacityAttribute.Get().GetColor(InWidgetStyle) * ImageBrush->GetTint( InWidgetStyle ) );

SNonogramCell에서는 지정한 색 그대로 칠해주는것으로 충분하니 간단하게 Color 그대로 지정하겠다.

 

이렇게 하면 SWidget에서 OnPaint가 호출될 때마다 사각 영역을 Color의 색으로 채워준다.

 

그 다음에는 사용자가 UWidget을 통해 블루프린트에서 지정한 색을 SCompoundWidget에도 지정해줘야한다.

그러기 위해서 SCompoundWidget에 public 함수를 추가해서 UWidget에서 접근 가능하게 한다.

// SNonogramCell.h

public:
	void SetBrush(FSlateBrush* InBrush);
	void SetColor(FLinearColor InColorAndOpacity);
    


// SNonogramCell.cpp

void SNonogramCell::SetBrush(FSlateBrush* InBrush)
{
	Brush.SetImage(*this, InBrush);
}

void SNonogramCell::SetColor(FLinearColor InColorAndOpacity)
{
	Color = InColorAndOpacity;
}

이 함수를 UWidget에서 호출해서 브러쉬와 색을 지정하면 된다.

 

UWidgetUNonogramCell에는 블루프린트로 노출할 변수와 함수, 그리고 SynchronizeProperties 함수를 override한다.

// UNonogramCell.h

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
    FSlateBrush Brush;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Appearance")
    FLinearColor Color;

    UFUNCTION(BlueprintCallable)
    void SetColor(FLinearColor InColor);

    //~ Begin UWidget Interface
    virtual void SynchronizeProperties() override;
    //~ End UWidget Interface



// UNonogramCell.cpp

TSharedRef<SWidget> UNonogramCell::RebuildWidget()
{
    Cell = SNew(SNonogramCell)
        .Brush(&Brush)
        .Color(Color)
        .OnPressedArg_UObject(this, &ThisClass::SlateHandlePressed)
        .OnReleasedArg(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleased))
        .OnHoveredArg_UObject(this, &ThisClass::SlateHandleHovered)
        .OnUnhoveredArg_UObject(this, &ThisClass::SlateHandleUnhovered)
        ;
    return Cell.ToSharedRef();
}

void UNonogramCell::ReleaseSlateResources(bool bReleaseChildren)
{
    Super::ReleaseSlateResources(bReleaseChildren);

    Cell.Reset();
}

void UNonogramCell::SetColor(FLinearColor InColor)
{
    Color = InColor;
    if (Cell.IsValid())
    {
        Cell->SetColor(Color);
    }
}

void UNonogramCell::SynchronizeProperties()
{
    Super::SynchronizeProperties();

    Cell->SetBrush(&Brush);
    Cell->SetColor(Color);
}

이제 위젯 블루프린트에서 NonogramCellSetColor 노드를 통해 색을 지정하면 위젯에 색을 채울 수 있다.

 

최종적으로 마우스로 클릭 가능한 영역이 지정된 색으로 채워지게 된다.

블루프린트에서 상황에 맞게 색을 지정하면 된다.

 

 

 

3. 마우스 왼쪽 버튼 클릭, 오른쪽 버튼 클릭 구분

SWidget의 마우스 이벤트 함수들은 FPointerEvent를 인자로 받는다.

// SNonogramCell.h

//~ SWidget overrides
virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;

FPointerEvent를 델리게이트로 블루프린트까지 전달하고, 블루프린트에서 눌린 버튼에 따라 어느것으로 칸을 채울지 결정한다.

이전에 있던 OnPressed를 다시 정의해서 값을 전달해 줄 수 있게 했다.

그리고 OnMouseButtonDown 함수에서 OnPressed 델리게이트를 Execute한다.

// SNonogramCell.h

DECLARE_DELEGATE_OneParam(FPressedSignature, FKey);

// ...

private:
	/** The delegate to execute when the button is pressed */
	FPressedSignature OnPressed;
    


// SNonogramCell.cpp

FReply SNonogramCell::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
	FReply Reply = FReply::Unhandled();
	FKey EffectingButton = MouseEvent.GetEffectingButton();
	if (EffectingButton == EKeys::LeftMouseButton || EffectingButton == EKeys::RightMouseButton || MouseEvent.IsTouchEvent())
	{
		OnPressed.ExecuteIfBound(EffectingButton);
		Reply = FReply::Handled();
	}
	return Reply;
}

눌린 버튼을 마우스 왼쪽 버튼, 오른쪽 버튼 등으로 제한해놓지 않으면 마우스의 휠 버튼 클릭이나 엄지 버튼 클릭에도 OnPressed 델리게이트를 Execute하게 되므로, 경우를 제한했다.

 

그 다음 UNonogramCell에서도 SnonogramCell에서 받은 FPointerEvent를 블루프린트까지 전달할 수 있게 하면 된다.

// UNonogramCell.h

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnClickedEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPressedEvent, FKey, EffectingButton);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnReleasedEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnHoverEvent);

// ...

protected:
	void SlateHandlePressed(FKey EffectingButton);
	void SlateHandleReleased();
	void SlateHandleHovered();
	void SlateHandleUnhovered();



// UNonogramCell.cpp

TSharedRef<SWidget> UNonogramCell::RebuildWidget()
{
    Cell = SNew(SNonogramCell)
        .Brush(&Brush)
        .Color(Color)
        .OnPressedArg_UObject(this, &ThisClass::SlateHandlePressed)
        .OnReleasedArg(BIND_UOBJECT_DELEGATE(FSimpleDelegate, SlateHandleReleased))
        .OnHoveredArg_UObject(this, &ThisClass::SlateHandleHovered)
        .OnUnhoveredArg_UObject(this, &ThisClass::SlateHandleUnhovered)
        ;
    return Cell.ToSharedRef();
}

void UNonogramCell::SlateHandlePressed(FKey EffectingButton)
{
    OnPressed.Broadcast(EffectingButton);
}

그 다음 블루프린트에서 상황에 맞게 작동하도록 구현하면 된다.

 

 

 

4. 마우스 입력 영역 변경

이전의 각 칸들의 배열을 그림으로 묘사하면 아래와 같다.

 

배경의 검은색이 그리드 패널의 배경, 즉 구분선 역할을 한다.

주황색 영역은 칸을 보여주는 이미지 영역이다.

그리고 파란색 선은 마우스 입력을 받을 수 있는 슬레이트 위젯의 영역을 외곽선으로 표현한 것이다.

 

이전에는 이렇게 배열되어있어서 마우스 커서가 검은색 영역 위에 있으면 마우스 입력을 받지 못한다.

그러면 칸을 선택하고 마우스 버튼을 떼도 선택중인 상태가 계속 유지되는 문제가 생겼다.

 

그래서 아래와 같은 방식으로 변경했다.

 

이전에는 각 칸의 전체에 패딩을 추가했다면 이번에는 칸을 표시하는 이미지에만 패딩을 추가했다.

그래서 마우스 입력을 받는 슬레이트 위젯은 빈칸 없이 채워졌고, 검은색 배경위에서도 마우스 입력을 받게 되었다.

실제로 화면에 보이는것은 검은색과 주황색뿐이므로, 이전과 동일하게 격자 형태로 보인다.

 

구분선을 흰색으로 변경해보면 다음과 같이 된다.

 

이전에 마우스를 칸에 호버하면 십자형태로 화면에 강조 표시를 하게 했었다.

그 기능은 마우스 입력을 받는 영역인 슬레이트 위젯에 색을 칠하는 방식으로 구현했다.

그렇다는것은 위의 이미지에 파란색으로 표현되는 십자모양 파란색 선은 마우스 입력을 받을 수 있는 정사각형 영역들이 이어져있다는 뜻이다.

그리고 그 영역들이 완전히 이어져있는 것을 확인할 수 있다.

그리고 칸을 표현하는 이미지는 구분선의 두께만큼 서로 떨어져있는 것도 확인할 수 있다.

 

 


 

 

게임 플레이와 직접 연관된 부분에 시각적으로 보이는게 추가되니 게임이 더 다듬어진 느낌이다.

 

아래 영상으로 전체적으로 어떻게 작동하는지 확인할 수 있다.

https://youtu.be/jnPBtcDW9zQ?si=yczZ_aBQuWa-ZoWL