When I first discovered Pokemon Red I was fascinated by its dialog system. The messages stream out at a moderate pace allowing enough time for the some players to read them and letting others skip to the end and get right back to the action.
Streaming partial messages to the screen is a UX mechanism that gets used in a lot of other places to feign interactivity and speed. This is something that’s pervasive in all apps that are hooked up to an LLM nowadays. As the data being returned from the models is slow, showing partial responses effectively replaces a loading spinner.
I’ve worked on some apps that use text streaming but for this post I wanted to explore building a streaming dialog system for a game I wrote recently - Flashlight.
Here’s what the streaming dialogs look like:
The can be strung together with in game interactions like turning the flashlight on for the first time.
For Flashlight, I’ve constrained myself to not using any external UI libraries or state management tools. So to build the browser side of the game I wrote a toy observable stream library called Rex.
Rex
First let’s look at the basic building blocks that Rex provides.
It can do a handful of things:
-
create a stream
1.1. from an event
1.2. from a timer
-
filter a stream
-
map over a stream
-
merge two streams
-
cause side effects via stream subscriptions
These are all the primitives I need to build a Pokemon like dialog system.
const keyUpInput$ = Stream.fromEvent("keyup", document.body);
const fKeyInput$ = keyUpInput$.filter((e) =>
keyIsF(e as KeyboardEvent)
) as Stream<KeyboardEvent>;