Why every write goes through a function
UInkPageDocument::Pages is BlueprintReadOnly on purpose. It is not an oversight and
there is no node to write it directly.
Every page a display shows is baked: the page’s visible layers are built into a Slate
widget and painted into a render target, which the page material multiplies over the paper.
That bake is cached per world, keyed on document path + page index + content revision
(a page’s back is cached under its own key beside the front’s). ContentRevision is a
transient counter on the document, bumped by every mutation.
If the Pages array were writable from Blueprint, a graph could change a page without the
revision moving, and every display would keep showing the stale bake forever with no way to
tell that it was stale. So the array is read-only, and the mutation functions each call
Bump Revision for you. Change a page through one of them and the next bake request
fails to match any cached key and re-bakes.
Two consequences worth internalizing:
- The revision is never saved. It is
Transient. It exists to invalidate caches within a session, not to version an asset. - Displays do not watch their document. Invalidating the cache does not repaint anything already on screen — the material is still holding the render target it was handed. After mutating, call Refresh on whatever is showing the document.
Set Document already refreshes, so assigning a freshly built document needs no extra
call.
The page API
Everything here is on the Ink Page Document asset itself, category Inkwell | Page Document. Drag off a document reference to find them.
Normalization. Add Page, Set Page, and Set Page Layers run the page through
the same repair pass the loader uses: if the page’s front has no Text layer, one is
appended. This is what makes
Make Ink Page Spec → Add Page → Set Page Markup
work — a default-constructed spec has no layers at all, and a page with no Text layer can
never show markup. Set Page Markup runs the same pass, so it also repairs a page whose
text layer was removed. The repair applies to the front only — see the back-of-sheet
section below for why.
The layer API
A page side is an ordered back-to-front stack of layers: index 0 is drawn first (the bottom), the last entry is drawn last (the top). The full model is in Layers; these are the runtime entry points. Every one takes a Side input (default Front) selecting which face of the sheet it operates on.
Reads that are useful alongside them: Get Page Size Cm (the page’s physical size —
Page Width Cm with the height derived from the Page Size Pixels aspect), Get
Paper For Page (the texture that page’s given side will actually use, including a
per-page override or generated paper), and Has Back (below).
Writing the back of a sheet
A page has two layer stacks:Layers (the front) and BackLayers (the back of the same
physical sheet). An empty back means the back was never authored: the sheet renders
bare paper there, and costs no bake and no render target. That is why the normalization
pass never touches the back — repairing a Text layer into it would give every page a
phantom back that starts paying for render targets.
So building a back at runtime is explicit, and the order matters:
- Make Ink Page Layer — Type = Text (fill its Text payload, or leave the markup for step 3).
- Add Page Layer — Page Index, that layer, Side = Back. Note the returned index.
- Set Layer Markup — Page Index, that index, your string, Side = Back.
The structs
Blueprint sees these as ordinary structs — build them with Make Ink Page Spec, Make Ink Page Layer, and so on, and read them apart with Break. Ink Page Spec (FInkPageSpec) — one page (one physical sheet):
Ink Page Layer (
FInkPageLayer) — one entry in a stack:
A layer carries all three payloads and draws only the one matching
Type, so switching a
layer’s type throws nothing away.
Ink Page Text (FInkPageText) — an independent text box:
Ink Page Image (
FInkPageImage) — a placed image, all coordinates normalized 0–1:
Ink Drawing (
FInkDrawing) holds an array of Ink Stroke (FInkStroke: Brush,
Color, Width Px 6.0, Opacity 1.0, and an array of Ink Stroke Point with a
normalized Position and a 0–1 Pressure). Strokes are drawn oldest first. Generating
strokes procedurally works, but it is a niche use — see Drawing for
what each brush does.
Document-level look and layout — paper mode and its settings, paper texture and material,
back paper, tints, ink strength, default font, typeface, size, color, page size in pixels,
margins, line height — are all
BlueprintReadOnly. They are authoring decisions, and
Blueprint can read them but not write them. The one exception is Page Width Cm, which
is writable. The per-page escape hatches are Paper Override / Back Paper Override
on the spec and per-run styling in the markup.Worked example: a note whose text comes from gameplay
The scenario: a body in an alley carries a note that has to name the district the player is in and the number of witnesses they have already found. The best-looking result comes from authoring the look and generating only the words.1
Author the template
Create an Ink Page Document —
PD_CoronerNote — in the Page Editor. Set its paper,
default font, size, color, and margins, and type a placeholder body so you can see the
layout. One page, one text layer, filling the page.2
Point a display at it
On the note actor, add an Ink Page Display and set
Document to
PD_CoronerNote. Nothing about the actor is special — see
Integration.3
Build the string
In the actor’s Event Graph (Begin Play, or whenever the facts are known), assemble the
text with Format Text or Append:
The body was found in {District}. {Count} others saw it happen.Plain text is fine and is what you want here: a markup string with no tags renders
entirely in the document’s default text style. Newlines in the string become new lines
on the page.4
Write it into the page
Drag off the Ink Page Display component, get its Document, then call Set Page
Markup with Page Index 0 and your string. The return value is false only if the page
index is out of range — worth a Branch while you are wiring it up.
5
Refresh the display
Call Refresh on the Ink Page Display. The page re-bakes with the new text and the
material picks it up the same frame.
A document per actor
Ink Page Document is aBlueprintType object, so you can build one in play:
1
Construct it
Construct Object from Class, Class = Ink Page Document, Outer = Self. Store
the result in a variable.
2
Fill page 0
A newly constructed document is not empty — its constructor gives it one page.
Call Set Page Markup (index 0): its repair pass gives that page a page-filling
Text layer and the text lands on it.
3
Add more pages
Make Ink Page Spec (Layers empty is fine) → Add Page → note the returned index →
Set Page Markup with it. Repeat per page.
4
Assign it
Set Document on the Ink Page Display, Stack, actor, or widget. No Refresh is
needed: Set Document re-bakes on its own, and it broadcasts On Page Changed as
well.
A document built this way carries the class defaults, not the defaults the asset
factory applies. Creating a Page Document asset in the Content Browser seeds its page size,
paper texture, and default font from Project Settings → Plugins → Inkwell;
Construct Object from Class does not. So a runtime document starts at 1024 × 1400 px,
21 cm wide, 90 px margins, line height 1.18, 28 pt warm near-black text, paper mode
Texture with no paper texture (the page material’s flat paper color) and no default
font — which falls through to the project’s Inkwell default font, and to the engine’s
Roboto if that is unset. Give each page a Paper Override in its spec if you want real
paper on a runtime-built document.
Adding a second text box
To put a heading in its own box above a body that fills the page:- Make Ink Page Text — Markup = your heading string, Justification = Center, Position = (0.1, 0.06), Size = (0.8, 0.12).
- Make Ink Page Layer — Type = Text, Name =
Heading, Visible = true, Opacity = 1.0, Text = the struct from step 1. - Add Page Layer — Page Index 0, Layer = the struct from step 2, Side = Front. It lands at the top of the stack and returns its index.
The markup format
Markup is the standard UE rich-text format with one tag,TextStyle. The Page Editor writes
it; you can write it too.
Plain text. A string with no tags renders entirely in the document’s default style —
default font, typeface, size, and color. For generated content, localized strings, and
journal entries, this is normally all you need.
Styled runs. A styled run looks like this, and closes with the rich-text short close
tag:
Runs sit side by side to mix styles inside a paragraph; any attribute a run omits falls back
to the document’s defaults. These attribute keys are the on-disk format and are frozen — the
same markup is read by the in-game bake and by the Page Editor’s WYSIWYG box, which is what
keeps the two pixel-identical.
The fastest way to get a styled string right is to style one in the Page Editor first and
copy what it produced.
Performance, caching, and cost
What a bake costs. Baking one page side builds a Slate widget for its visible layers and paints it into a render target — twice. (Slate’s auto-wrap measures against the geometry cached by the previous paint, and an offscreen widget has no previous paint; the first pass primes it, the second draws the correctly wrapped layout.) It runs on the game thread, in linear space, at the document’s page size clamped proportionally to Max Page Bake Dimension (Project Settings → Plugins → Inkwell, default 4096). At the default 1024 × 1400 that is roughly 5.7 MB of render target per distinct page side. What is cached, and on what. Bakes live in a per-world subsystem keyed ondocument path | page index | content revision, with a page’s back cached under its own
key beside the front. Game and PIE worlds have it; editor worlds do not, and bake uncached
through the tools path instead. Twenty sheets showing the same page of the same document
cost one bake. A page with no authored back never bakes a back and never allocates a
render target for one — an unauthored back is free by design.
What invalidates. Any mutation function, Bump Revision, and any editor-side edit to
the document. Nothing else — a page turn, a flip, a Refresh, or a new display of an
already-baked page all hit the cache.
Pre-warming. Bakes happen on demand, on the game thread, so the first display of a long
generated document can hitch. Call Get Page Ink Texture for the pages you expect during
a load screen or a fade — once per side you will show, via its Side input — and the
cache will already hold them when the player opens the note.
Procedural paper is cached separately. Generated paper is rasterized once per distinct
set of procedural settings and held on the document itself, keyed on a hash of those
settings plus the clamped page size — not on the content revision. A back that generates
its own sheet is cached the same way. Twenty pages sharing a document share one sheet of
paper, and mutating page text never regenerates it.
Runtime pages are not save data. The revision is transient and the pages you write live
only in memory. If generated pages must survive a reload, save the source facts — the strings
you built, the district name, the entry list — in your own save game and rebuild the document
on load. Reference documents from save data by soft object path, never by hard pointer.
Multiplayer. Documents are not replicated, and a bake needs a Slate renderer, so a
dedicated server bakes nothing. Treat pages as a client-side visual: replicate the facts, and
build the page locally on each client that needs to read it.
Next
Blueprint API
Every Inkwell node in one place, grouped by the class it lives on.
Layers
The stacking model these layer functions operate on, and the Layers panel.
Rendering
The bake pipeline, the page material, and its parameters end to end.