Integrating dialogues with Flame (part 1)

Lim Chee Keen
4 min readFeb 28, 2024

--

This article is part of the series Building a 2d-top down RPG with Flutter and Flame.

RPGs, like Gomiland, need character dialogue. The Jenny API is Flame’s dialogue system. You can find the documentation for Jenny for Flame here. The following are code samples used in Gomiland. Let’s use them to translate the ideas in the docs to a working game.

Before diving in, a quick introduction to .yarn files. Jenny API parses .yarn files and converts them to Dialogues that can be used in the Flame engine. The file format comes from the YarnSpinner library and more information can be found in Flame’s docs here. The format looks quite different from Dart code so it took a bit of understanding, but once you get it, it works really well for any situation that you, the aspiring game creator, need!

Here is a sample of a .yarn file from Gomiland. This file was used for a character in the game. The character’s name is Anri and she has a dialogue called “unbloomed”.

<<character Anri>>
title: unbloomed
---
Anri: Greetings to you! I'm Anri.
Anri: I'm waiting for the Sakura trees to bloom.
Anri: It should be any day now!
===

We can trigger a dialogue with the following code.

YarnProject yarnProject = YarnProject();

// get access to a custom component called DialogueControllerComponent
DialogueControllerComponent dialogueControllerComponent =
game.dialogueControllerComponent;

yarnProject.parse(await rootBundle.loadString(path_to_yarn_file));
DialogueRunner dialogueRunner = DialogueRunner(
yarnProject: yarnProject,
dialogueViews: [dialogueControllerComponent],
);
// start dialogue with this code
await dialogueRunner.startDialogue('unbloomed');

The YarnProject class is a special class from the Jenny package that will parse .yarn files and translate them into workable nodes and lines for dialogues in the Flame component system. The dialogueControllerComponent (see below) is a component that extends DialogueView. DialogueView is a special abstract mixin class that can receive these nodes and lines as callbacks.

class DialogueControllerComponent extends Component
with DialogueView, HasGameReference<GomilandGame> {
Completer<void> _forwardCompleter = Completer();
Completer<int> _choiceCompleter = Completer<int>();
Completer<void> _closeCompleter = Completer();
bool _isFirstLine = true;
late final DialogueBoxComponent _dialogueBoxComponent =
DialogueBoxComponent();

@override
Future<void> onNodeStart(Node node) async {
_isFirstLine = true;
_closeCompleter = Completer();
_addDialogueBox();
}

void _addDialogueBox() {
List<DialogueBoxComponent> list =
game.cameraComponent.viewport.children.query<DialogueBoxComponent>();
if (list.isEmpty) {
game.cameraComponent.viewport.add(_dialogueBoxComponent);
}
}

@override
Future<void> onNodeFinish(Node node) async {
_dialogueBoxComponent.showCloseButton(_onClose);
await _returnClosed();
}

Future<void> _returnClosed() async {
return _closeCompleter.future;
}

void _onClose() {
List<DialogueBoxComponent> list =
game.cameraComponent.viewport.children.query<DialogueBoxComponent>();
if (list.isNotEmpty) {
game.cameraComponent.viewport.removeAll(list);
}
_completeClose();
}

Future<void> _completeClose() async {
if (!_closeCompleter.isCompleted) {
_closeCompleter.complete();
}
}

Future<void> _advance() async {
return _forwardCompleter.future;
}

@override
FutureOr<bool> onLineStart(DialogueLine line) async {
_forwardCompleter = Completer();
_changeTextAndShowNextButton(line, _isFirstLine);
await _advance();
_isFirstLine = false;
return super.onLineStart(line);
}

void _changeTextAndShowNextButton(DialogueLine line, bool isFirstLine) {
String characterName = line.character?.name ?? '';
String dialogueLineText = '$characterName: ${line.text}';
_dialogueBoxComponent.changeText(dialogueLineText, isFirstLine, _goNextLine);
}

void _goNextLine() {
if (!_forwardCompleter.isCompleted) {
_forwardCompleter.complete();
}
}

@override
FutureOr<int?> onChoiceStart(DialogueChoice choice) async {
_forwardCompleter = Completer();
_choiceCompleter = Completer<int>();
_dialogueBoxComponent.showOptions(
onChoice: _onChoice,
option1: choice.options[0],
option2: choice.options[1],
);
await _advance();
return _choiceCompleter.future;
}

void _onChoice(int optionNum) {
if (!_forwardCompleter.isCompleted) {
_forwardCompleter.complete();
}
if (!_choiceCompleter.isCompleted) {
_choiceCompleter.complete(optionNum);
}
}
}

There are 4 callback flows handled in the component above: i) onNodeStart; ii) onLineStart; iii) onChoiceStart; iv) onNodeFinish. These relate to each step of the dialogue and custom methods are required to display and handle the dialogue node until it ends.

Next we need a UI component to display the dialogue. Here is the Flame SpriteComponent that have the dialogue UI components as children. It takes commands from the DialogueControllerComponent and controls the appearance of the text and sprites accordingly.

class DialogueBoxSpriteComponent extends SpriteComponent {
DialogueTextBox _textBox = DialogueTextBox(text: '', showFullText: true,);
late final ButtonRow _buttonRow = ButtonRow(size: _size);
final Vector2 _size = Vector2(dialogueBoxWidth, dialogueBoxHeight);

@override
Future<void> onLoad() async {
sprite = await Sprite.load(
Assets.assets_images_ui_dialogue_box_png,
srcSize: _size,
);
add(_buttonRow);
return super.onLoad();
}

void removeTextBox() {
remove(_textBox);
}

void changeText(String text, bool isFirstLine, Function goNextLine) {
if (!isFirstLine) {
removeTextBox();
}
_textBox = DialogueTextBox(text: text, showFullText: true,);
add(_textBox);
_buttonRow.showNextButton(goNextLine);
}


void showOptions({
required Function onChoice,
required DialogueOption option1,
required DialogueOption option2,
}) {
_buttonRow.showOptionButtons(
onChoice: onChoice,
option1: option1,
option2: option2,
);
}

void showCloseButton(Function onClose) {
void closeDialogue () {
removeTextBox();
onClose();
}
_buttonRow.showCloseButton(closeDialogue);
}
}

DialogueTextBox and ButtonRow are also custom spriteComponents. You can add your own custom components to respond to calls from DialogueBoxSpriteComponent.

The result looks like this in Gomiland.

Talking to Anri

We have covered the basics of dialogues in Flame. If you just need a simple dialogue setup, then that’s all you need! However there are many more features of the Jenny API. For instance, choices, conditional dialogues, calling functions during dialogues and added variables. We will cover these and more in part 2.

--

--

Lim Chee Keen

Former Navy Captain Turned Software Engineer | Flutter & React developer | ML & AI programmer | Co-founder for Group Buy service