[UE5] SCompoundWidget에 Brush 추가, 노노그램 입력개선
지난번 포스팅에서 이어진다.
https://mstone8370.tistory.com/5
[UE5] Slate를 이용해서 UI의 마우스 입력을 원하는대로 받기
사용자 위젯으로 UI를 만들때 마우스 이벤트를 내가 원하는 대로 받고싶을 때가 있다. 예를 들면 마우스 호버 이벤트를 버튼이 아닌 위젯에서도 받아오고 싶다거나, 클릭 이벤트를 마우스 오른
mstone8370.tistory.com
결과 비교
먼저 결과부터 비교해본다.
왼쪽이 이전에 포스팅 했던 결과고, 오른쪽이 현재 상태다.
변경된점은 다음과 같다.
- 선택된 영역을 표시하는 방식을 결과 미리 보기에서 외곽선과 색 채우기로 변경했다.
- 현재 호버링 하고있는 칸을 십자 형태로 강조 표시해서 어느 칸을 가리키는지 쉽게 알아볼 수 있게했다. 이전 상태의 마우스 커서 주변에 나타나는 노란색은 녹화 프로그램의 마우스 위치 강조 기능이다.
- 마우스 사용 시 마우스 왼쪽 버튼 클릭은 일반적인 칸 채우기, 마우스 오른쪽 버튼 클릭은 X로 표시하도록 했다.
- 칸과 칸 사이 구분선도 마우스 입력 영역으로 포함시켰다. 영상으로 볼 때에는 티나지 않지만 직접 플레이하는 경우에는 체감된다. 전에는 구역 선택 후에 칸 위에 커서를 정확히 올리고 마우스 버튼을 떼야 Release 이벤트가 발생했다. 그래서 실수로 칸을 벗어나 구분선 위에서 마우스 버튼을 떼면 Release 이벤트를 받지 못해서 칸을 채우지 못하는 경우가 종종 있었다. 하지만 이제는 그런 문제가 발생하지 않는다.
하나씩 어떻게 구현했는지 설명하겠다.
1. 외곽선
이전에는 새로운 위젯을 따로 만들어서 각 칸의 Geometry를 통해 뷰포트에서의 위치와 크기를 계산해서 외곽선을 표현하려고 했다.
블로그에 포스팅을 시작하기 전부터 계속 고민했던 방식이지만, 여러 방법을 사용해봐도 좌표에 오차가 계속 발생했고 외곽선의 크기도 맞지 않았다.
뷰포트의 테두리 두께도 좌표 계산에 영향을 주는듯 했다.
그리고 외곽선의 위치와 크기는 계속 변할텐데, 그 모든 경우에 알맞게 표현이 될지도 확실하지 않았다.
그래서 다른 방법이 필요했고, 찾아낸 방법이 각 칸에 외곽선을 추가해서 숨겨두는 방법이다.
대신에 각 칸은 그리드 패널에 들어있으므로 외곽선은 크기를 차지하면 안된다.
만약 외곽선이 그리드 패널의 공간을 차지한다면 각 칸들의 거리가 외곽선의 두께만큼 멀어질 것이다.
캔버스 패널을 적절히 이용하면 쉽게 해결할 수 있다.
여러 칸을 선택한 경우 외곽선은 그 칸들을 감싼것처럼 묘사되어야 하는데, 이를 위해서 외곽선을 나누고 상황에 맞게 외곽선으로 감싸져야할 부분만 표시되게 한다.
계층구조에서 OutlinePannel
은 그리드 패널이고 가로 3칸, 세로 3칸으로 되어있다.
위처럼 구분되어있으며, 각 칸은 이미지이므로 외곽선의 코너부분은 둥근 모양의 텍스쳐를 넣어서 자연스럽게 만들어줬다.
Outline 이미지들을 상황에 맞게 표시하거나 숨기면 된다.
경우에 따라 표시하거나 숨길 외곽선은 아래 이미지처럼 정리된다.
Horizontal은 가로로 선택되었는지를 나타내며, Start와 End는 각각 외곽선이 시작된 칸과 끝난 칸이다.
그리드의 왼쪽 상단을 (0, 0)으로 하고, 오른쪽과 아래로 갈 수록 인덱스가 늘어난다고 했을 때, Start의 인덱스는 End의 인덱스보다 항상 작거나 같다.
2. 십자모양으로 칸 위치 강조
퍼즐의 크기가 커질수록 내가 지금 어느 칸을 보고있고, 어느 힌트를 봐야는지 헷갈리기가 쉽다.
이럴때 가로 세로 십자 모양으로 현재 가리키고있는 칸이 어느 칸인지 보여준다면 퍼즐을 푸는데에만 집중할 수 있도록 도와준다.
이 경우에는 외곽선처럼 이미지를 하나 더 추가해서 할 수 있지만, 이전에 공간만 차지하고있는 SCompoundWidget
을 추가했으니 여기에 브러쉬를 추가해서 사각 영역을 채우는 방법을 선택했다.
다른 위젯들에는 ColorAndOpacity
가 있어서 이것을 변경하면 색이 바뀌었는데 이것과 동일하게 작동하도록 만들었다.
일단 C++ 코드로 슬레이트 위젯에 사각형 모양으로 영역을 채우려면 슬레이트 위젯의 OnPaint
함수 내부에 FSlateDrawElement::MakeBox
함수를 호출하면 된다.
이것이 최종 목표다.
이 함수의 인자는 이것저것 많은데 OnPaint
함수에서 인자로 받는 값들과 겹치는 것들이 많다.
그래서 추가적으로 필요한 것은 FSlateBrush
와 FLinearColor
만 있어도 충분하다.
먼저 슬레이트 위젯인 SNonogramCell
클래스에 FInvalidatableBrushAttribute
와 FLinearColor
타입 변수를 추가한다.
// SNonogramCell.h
private:
FInvalidatableBrushAttribute Brush;
FLinearColor Color;
FInvalidatableBrushAttribute
타입의 경우 슬레이트 이미지인 SImage
의 코드를 참고해서 가져왔다.
참고로 SCompoundWidget
에 ColorAndOpacity
라는 변수를 가지고있지만 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;
}
그 다음 SNonogramCell
에 OnPaint
함수를 추가해서 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
에서 호출해서 브러쉬와 색을 지정하면 된다.
UWidget
인 UNonogramCell
에는 블루프린트로 노출할 변수와 함수, 그리고 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);
}
이제 위젯 블루프린트에서 NonogramCell
의 SetColor
노드를 통해 색을 지정하면 위젯에 색을 채울 수 있다.
최종적으로 마우스로 클릭 가능한 영역이 지정된 색으로 채워지게 된다.
블루프린트에서 상황에 맞게 색을 지정하면 된다.
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