How to write a Dialog system with Unreal 5.1

Create a new Widget Blueprint

To display a Dialog Box, we first need to create a Widget to display on the screen that contains a text box. In Unreal Engine, you can create a new Widget Blueprint by right-clicking in the Content Browser, selecting “User Interface,” and then “Widget Blueprint.” Give your new Widget Blueprint a name, such as “DialogWidget.”

Once you’ve created your new Widget Blueprint, you can add a Text Box to it by dragging and dropping the Text Box widget from the Palette panel onto the Canvas panel of your Widget Blueprint. You can customize the appearance of the Text Box by adjusting its properties in the Details panel, such as its font, color, and alignment.

In your code, you’ll need to create a subclass of UUserWidget that implements the behavior of your Dialog Widget. You can do this by adding a new C++ class to your project and making it a subclass of UUserWidget. Then, add a public function to your new class called “SetDialogText” that takes an FText parameter and sets the text of the Text Box in your Widget Blueprint.

UCLASS()
class UDialogWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    void SetDialogText(const FText& InText);

private:
    UPROPERTY(meta = (BindWidget))
    UTextBlock* DialogText;
};

void UDialogWidget::SetDialogText(const FText& InText)
{
    if (DialogText)
    {
        DialogText->SetText(InText);
    }
}

Implementation note

I chose to use a subsystem for a few reasons. First, a subsystem is a good way to encapsulate functionality that is independent of any particular actor or component. This makes it easy to reuse the dialog system in different parts of the game without having to duplicate code.

By making the subsystem abstract, I ensured that it wouldn’t be automatically instanced, which gives me greater control over when and where the dialog system is used. By making it Blueprintable, I allowed myself to create blueprints from the system and modify default values as needed.

Also, I chose to use DataTables to store dialog lines because they provide an easy way to manage large amounts of text data. DataTables allow us to store data in rows and edit them in CSV or JSON, which is very convenient when working with text-heavy content like dialog lines. Additionally, DataTables are easy to use in code, allowing me to retrieve and display the dialog lines in the dialog box widget.

Start your dialog from anywhere
Use CSV import or edit from the Unreal editor

Code

// Header

// Represent one dialog line
USTRUCT(BlueprintType)
struct FL0DialogLine : public FTableRowBase
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString Line;
};

// Abstract so it won't be automatically instanced. Blueprintable so we can create blueprint from this system and edit default values.
/**
 * System to handle dialogs.
 */
UCLASS(Abstract, Blueprintable, meta=(DisplayName="Dialog System"))
class THEPATHTOL0_API UL0DialogSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	
	/**
	 * @brief When user ask for dialog to continue. Go to next sentence or close the dialog box.
	 */
	UFUNCTION(BlueprintCallable)
	void DialogContinue();

	/**
	 * @brief Open the Dialog box, display the related dialog.
	 * @param DialogTable
	 */
	UFUNCTION(BlueprintCallable)
	void StartDialog(TSoftObjectPtr<UDataTable> DialogTable);

protected:
	
	UPROPERTY()
	class UDialogWidget* DialogBox = nullptr;

	// the configurable dialog widget to that will display the dialog.
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TSubclassOf<UUserWidget> DialogWidgetClass;

private:
	/**
	 * @brief Assign the next dialog line to the text box.
	 * @return Return true it there was a dialog line to show, false otherwise (need to close the dialog box).
	 */
	bool ShowNextDialogLine();
	// The line of the dialog currently show.
	int32 CurrentRowIndex;

private:
	UPROPERTY()
	UDataTable* SelectedDialogTable = nullptr;
};


// cpp

void UL0DialogSubsystem::DialogContinue()
{
	if (DialogBox)
	{
		if (ShowNextDialogLine())
		{
			// Advance in dialog
			return;
		}
		else
		{
			// Need to close the dialog box.	

			DialogBox->RemoveFromParent();
			DialogBox->ConditionalBeginDestroy();
			DialogBox = nullptr;

			APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();
			if (PlayerController)
			{
				PlayerController->SetPause(false);
			}
		}
	}
}

void UL0DialogSubsystem::StartDialog(TSoftObjectPtr<UDataTable> DialogTable)
{
	if ((ensure(DialogWidgetClass) == false) || DialogTable.IsValid() == false)
	{
		return;
	}

	SelectedDialogTable = DialogTable.Get();
	if (SelectedDialogTable)
	{
		DialogBox = CreateWidget<UDialogWidget>(GetWorld(), DialogWidgetClass);

		if (DialogBox)
		{
			// Reset line counter
			CurrentRowIndex = 0;
			ShowNextDialogLine();

			// Add the dialog box to the viewport
			DialogBox->AddToViewport();

			// Pause the game
			GetWorld()->GetTimerManager().SetTimerForNextTick(
				FTimerDelegate::CreateLambda([this]()
				{
					// Pause the game
					APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();
					if (PlayerController)
					{
						PlayerController->SetPause(true);
					}
				})
			);
		}
	}
}

bool UL0DialogSubsystem::ShowNextDialogLine()
{
	if (CurrentRowIndex < SelectedDialogTable->GetRowMap().Num() && DialogBox)
	{
		const FName RowName = SelectedDialogTable->GetRowNames()[CurrentRowIndex];
		if (const FL0DialogLine* Row = SelectedDialogTable->FindRow<FL0DialogLine>(RowName, ""))
		{
			// Increment for the next call.
			CurrentRowIndex++;
			// Set the text of the dialog box
			DialogBox->SetDialogText(FText::FromString(Row->Line));
			return true;
		}
	}
	return false;
}

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *