tutorial · 2026-01-15

Building a Readable Book and Note System from a DataTable in UE5

Drive in-game journals, letters and lore books from a single DataTable, then spawn a readable widget from any row.

Assassin Dialogue Lore Pack
Free on Fab Assassin Dialogue Lore Pack Free assassin & rogue NPC dialogue for your fantasy RPG.
Free Get it free →
85
Written-content items in the pack
5
DataTables in the project
UE 5.3
Minimum engine version
Free
Price

Why a DataTable beats scattering text across Blueprints

Most teams start their readable documents the hard way: a journal page typed into a Widget Blueprint, a contract letter hard-coded in a quest node, a recipe stuffed into a string variable on an actor. It works for three documents and collapses at thirty. There is no single place to audit the writing, no clean way to localise it, and no consistent way to spawn a reader for an arbitrary piece of text.

An Unreal Engine readable book and note system built on a DataTable fixes that. One DataTable holds every document as a row; one row struct defines the columns; one widget renders any row you hand it. Designers edit prose in a spreadsheet-like grid without touching Blueprints, and your gameplay code only ever talks to row names. This guide walks through that pattern using the DT_WrittenContent DataTable shipped in the free Assassin Dialogue Lore Pack, which arrives pre-populated with 85 written-content items so you see a real content layer rather than an empty template.

The pack is a UE5 project whose Content folder you migrate into your game. Among its five DataTables is DT_WrittenContent, the one we care about here. The approach works whether you author your own rows from scratch or build on the ones already in the pack.

The DT_WrittenContent schema

Every DataTable is backed by a row struct, and the row struct is where you decide what a document actually is. DT_WrittenContent in the Assassin Dialogue Lore Pack uses a small, deliberate set of fields, and they are worth understanding before you add your own.

The schema carries a 'Name' field that serves as the FName row key, a title column exposed as 'ContentTitle' (also referred to as 'Title'), and the document body itself in 'BodyText' (also referred to as 'Body'). Two more fields make the system queryable: 'ContentType', a string that classifies the document, and 'ContextTags', which slots a document into the same hierarchical tagging scheme the pack uses for its dialogue. Finally there is 'WordCount', stored as an int32, handy for pagination, reading-time estimates, or deciding whether a note fits on one page or needs a scroll.

If you are authoring your own struct to match, create a Blueprint Structure or a C++ struct deriving from FTableRowBase with those columns: title and body as text fields, type and tags as strings, word count as an integer. The discipline that pays off later is keeping the body field the single source of truth for the prose, so there is exactly one place a writer edits a journal entry.

To inspect the shipped table, open DT_WrittenContent in the Content Browser after migrating the pack. The DataTable editor shows every row in a grid; click any row to see its title, body and type.

Document types: journals, letters, notes, recipes, poetry and books

The 85 written-content items in the pack are not an undifferentiated blob of text. They span journals, letters, notes, recipes, poetry and books, and the 'ContentType' field is what tells them apart at runtime. The pack labels each row with a descriptive type string, with values such as 'note_reminder', 'letter_authority', 'journal_old', 'book_history' and 'poetry_war'. The pattern is a base category followed by a flavour, which gives you a coarse type and a finer subtype in a single column.

Those type strings are what let one reader widget present six different kinds of document sensibly. A 'book_history' row might warrant a two-column page layout and a chaptered scroll, while a 'note_reminder' is a single scrap of paper and a 'letter_authority' wants a wax-seal frame and a signature line. You read 'ContentType', branch on the base category, and pick the visual treatment — the text itself always comes from the same 'BodyText' field.

Because the type lives in data rather than in code, adding a seventh kind of document later is a content task, not an engineering task. You add rows with a new type string and teach the widget one more branch. Nothing else in your spawning or querying logic changes.

Spawning a readable UI widget from a row

The core interaction is simple: the player interacts with a book or note in the world, and you open a reader widget populated from one DataTable row. Here is the Blueprint flow, step by step.

1. Build a reader widget. Create a User Widget — call it WBP_DocumentReader — with a title 'Text Block', a scrollable body 'Text Block' inside a 'Scroll Box', and a close button. Add two variables exposed on spawn: a 'Text' for the title and a 'Text' for the body. Bind the two text blocks to those variables.

2. Give each readable actor a row reference. On your pickup or interactable actor, add a 'Name' variable called 'DocumentRow' and set it per-instance in the level to the row key you want that object to show. A bookshelf book points at one row; a dropped note points at another.

3. On interact, read the row. In the actor's interaction event, call 'Get Data Table Row' against DT_WrittenContent using the 'DocumentRow' name. Check the 'Row Found' boolean before continuing so a mistyped row key fails loudly rather than showing an empty page.

4. Create and fill the widget. Call 'Create Widget' for WBP_DocumentReader, pass the row's 'ContentTitle' to the title input and 'BodyText' to the body input, then 'Add to Viewport'. Set input mode to UI-only and show the mouse cursor so the player can scroll and close.

5. Optionally branch on type. Before adding to the viewport, read 'ContentType' from the same row and switch the widget's frame or layout based on the base category, so a letter and a book do not look identical.

If you prefer C++, the equivalent is to call LoadObject on the DataTable, use FindRow with your row name and the FWrittenContentRow type, then construct the widget with CreateWidget and assign the title and body before AddToViewport. Either way, the gameplay side only ever knows a row name, never the prose.

Tagging documents by category for quests and collections

Hard-coding a single row name per object is fine for set-dressing, but real games want to pull documents by category — a random warning note for a guard post, every recipe the blacksmith ever wrote, the next unread journal entry in a chain. That is what 'ContentType' and 'ContextTags' are for, and the query mirrors the one the pack uses for dialogue.

To pull a document by category in Blueprint: call 'Get Data Table Row Names' on DT_WrittenContent, loop the names with 'Get Data Table Row', and keep only the rows whose 'ContentType' matches the base category you want — for example any row whose type begins with 'journal' or 'letter'. From that filtered set, pick a random entry, or track an index to walk a journal in order. Hand the chosen row to the same WBP_DocumentReader you already built.

The 'ContextTags' field gives you a second, finer axis. Because it follows the pack's category/subcategory convention, you can ask for documents that belong to a particular place, faction or quest beat without adding new columns. Filter on a tag substring exactly as you would for a dialogue line, which means a single helper function can serve both your spoken barks and your readable lore.

For performance, cache the DataTable reference once rather than resolving it every interaction, and pre-filter rows into category lists at level load if you query them in a hot path. The shipped tables are small enough that a synchronous lookup is cheap, but caching keeps a busy collectible system from re-scanning the whole table on every pickup.

Where to take it next

Once the reader works, the obvious upgrades are content-driven and cost almost no new code. Use 'WordCount' to paginate long books or to gate an achievement on total words read. Add a 'discovered' set keyed by row name to build a journal or codex that fills in as the player finds documents. Localise by swapping the DataTable per language, since every string the player sees lives in 'BodyText' and 'ContentTitle' rather than in a Blueprint.

Because the Assassin Dialogue Lore Pack ships its written lore alongside a full context-aware voice line layer in the same project, you can also voice what the player reads. The pack's other DataTable, DT_Dialogue, references its audio through a TSoftObjectPtr so nothing loads until first played, which means narrating a journal entry is as simple as playing the matching line while the reader is open. The pack is free under the Fab Standard licence, so it is a low-risk way to get a populated content layer and a working dialogue system in one migration.

If you outgrow a single archetype, the same five-DataTable layout and the same DT_WrittenContent schema repeat across the rest of the Lore Pack collection and are consolidated in the Fantasy NPC Voices megabundle, so the reader widget you build here keeps working unchanged across every character you add.

DT_WrittenContent fields

FieldTypePurpose
NameFName (row key)Unique row identifier you reference from gameplay
ContentTitle / TitleTextHeadline shown at the top of the reader
BodyText / BodyTextThe document prose — the single source of truth
ContentTypeStringClassifies the document (e.g. note_reminder, letter_authority)
ContextTagsStringHierarchical category/subcategory tags for querying
WordCountint32Length, for pagination and reading-time estimates

The row struct columns that back every readable document in DT_WrittenContent.

ContentType examples and how to render them

ContentTypeBase categorySuggested reader treatment
note_remindernoteSingle scrap of paper, no scroll
letter_authorityletterSealed-letter frame with a signature line
journal_oldjournalAged page, entries walked in order
book_historybookChaptered scroll, paginated by word count
poetry_warpoetryCentred verse layout

Real type strings from the pack and a sensible reader treatment for each base category.

FAQ

How do I build an Unreal Engine readable book and note system from a DataTable?

Define a row struct with title, body, type and tag columns, store every document as a row in one DataTable, then build a single reader widget that takes a title and body on spawn. On interact, call Get Data Table Row for the row you want, pass its ContentTitle and BodyText into the widget, and Add to Viewport. The Assassin Dialogue Lore Pack's DT_WrittenContent ships 85 rows already in this shape.

What fields does DT_WrittenContent use?

It uses Name as the FName row key, ContentTitle (also called Title) for the headline, BodyText (also called Body) for the prose, ContentType to classify the document, ContextTags for hierarchical category tagging, and WordCount stored as an int32.

How do I show different document types with one widget?

Read the ContentType string from the row and branch on its base category. The pack uses values like note_reminder, letter_authority, journal_old, book_history and poetry_war, so you switch the widget's frame or layout on note, letter, journal, book or poetry while the text always comes from the same BodyText field.

Can I pull a random document by category?

Yes. Call Get Data Table Row Names, loop the rows, keep those whose ContentType matches the base category you want, then pick a random match or track an index for an ordered chain. Cache the DataTable reference and pre-filter at level load if you query it frequently.

Can the documents be voiced?

The Assassin Dialogue Lore Pack ships its written lore in the same project as a context-aware voice line layer in DT_Dialogue, whose audio is referenced through a TSoftObjectPtr so clips load only on first play. You can play a matching line while the reader widget is open to narrate what the player reads.

Free on Fab

Assassin Dialogue Lore Pack

A free lore pack of dark, stealthy assassin and rogue dialogue — 72 minutes of professionally delivered lines covering combat, lore and ambient barks. Drop-in audio cues for any UE5 RPG.

Report a bug