Luna UI provides a Visual Novel controller that extends the DialogueController with character portraits, names, and navigation controls for creating visual novel-style dialogue sequences.

Visual Novel

Features

  • Character Portraits: Left and right avatar positions with breathing animations
  • Character Names: Display speaker name above dialogue
  • Text Effects: Full support for Text Effects
  • Input Support: Continue, Skip, and Restart buttons with input binding
  • Animated Indicators: Pulsing "next" icon using TransitionToggleRepeat

Required UXML Structure

Create a UXML with the following named elements:

xml
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:luna="CupkekGames.Luna"> <!-- Character Avatars --> <ui:VisualElement name="AvatarLeftContainer"> <ui:VisualElement name="AvatarLeft" /> </ui:VisualElement> <ui:VisualElement name="AvatarRightContainer"> <ui:VisualElement name="AvatarRight" /> </ui:VisualElement> <!-- Dialogue Box --> <ui:VisualElement name="DialogueBox"> <ui:Label name="CharacterName" /> <ui:Label name="Speech" /> <luna:InputPrompt name="NextIcon" /> </ui:VisualElement> <!-- Control Buttons --> <ui:Button name="ContinueButton" /> <luna:InputPrompt name="RestartButton" /> <luna:InputPrompt name="SkipButton" /> </ui:UXML>

USS Animation Classes

Define transition animations for avatars and the next icon:

css
.vn_avatar_anim { scale: 1.02; transition: scale 0.5s ease-in-out; } .vn_next_icon_anim { translate: 0 5px; transition: translate 0.3s ease-in-out; }

Setup

1. Create UIDocument

Add a UIDocument with your visual novel UXML.

2. Add VisualNovelController

Add the VisualNovelController component to the same GameObject.

3. Configure Input Actions (Optional)

The controller reads input action names from the InputPrompt components:

  • RestartButton.InputActionName → Restart input
  • SkipButton.InputActionName → Skip input
  • NextIcon.InputActionName → Continue/Next input

Public Methods

Continue (with name)

Advances dialogue with character name.

csharp
public bool Continue(string text, string name, bool skipCurrent)
ParameterDescription
textDialogue text (supports text effects)
nameCharacter name to display
skipCurrentIf true, skips current animation; if false, completes it

Returns true if new text started playing.

Continue (with avatars)

Advances dialogue with character name and portraits.

csharp
public bool Continue( string text, string name, Sprite avatarLeft, Sprite avatarRight, bool skipCurrent )
ParameterDescription
textDialogue text
nameCharacter name
avatarLeftLeft character sprite (null to hide)
avatarRightRight character sprite (null to hide)
skipCurrentSkip current animation

Avatar Control

csharp
public void ShowAvatarLeft(Sprite sprite) public void HideAvatarLeft() public void ShowAvatarRight(Sprite sprite) public void HideAvatarRight()

Next Icon Control

csharp
public void ShowNext() // Shows and animates the "next" indicator public void HideNext() // Hides and stops the animation

Character Name

csharp
public void SetCharacterName(string name)

Events

EventDescription
OnContinueFired when continue button is pressed
OnRestartFired when restart button is pressed
OnSkipFired when skip button is pressed

Inherited Events (from DialogueController)

EventDescription
OnTextStartFired when text animation begins
OnTextCompleteFired when text animation completes

Example Implementation

csharp
using UnityEngine; using CupkekGames.Luna; using Ink.Runtime; public class VisualNovelManager : MonoBehaviour { [SerializeField] private VisualNovelController _vnController; [SerializeField] private TextAsset _inkJSON; private Story _story; private void Start() { _story = new Story(_inkJSON.text); _vnController.OnContinue += ContinueStory; _vnController.OnTextComplete += OnDialogueComplete; _vnController.OnSkip += SkipToEnd; _vnController.OnRestart += RestartStory; ContinueStory(); } private void ContinueStory() { if (!_story.canContinue) { EndStory(); return; } _vnController.HideNext(); string text = _story.Continue(); string speaker = GetCurrentSpeaker(); Sprite leftAvatar = GetLeftAvatar(); Sprite rightAvatar = GetRightAvatar(); bool started = _vnController.Continue( text, speaker, leftAvatar, rightAvatar, skipCurrent: false ); if (!started) { // Text was already playing, just showed the full text // Will call OnTextComplete which shows the next icon } } private void OnDialogueComplete() { _vnController.ShowNext(); } private void SkipToEnd() { while (_story.canContinue) { _story.Continue(); } EndStory(); } private void RestartStory() { _story.ResetState(); _vnController.HideAvatarLeft(); _vnController.HideAvatarRight(); ContinueStory(); } private void EndStory() { Debug.Log("Story complete!"); } private string GetCurrentSpeaker() { /* Parse from Ink tags */ } private Sprite GetLeftAvatar() { /* Get from character database */ } private Sprite GetRightAvatar() { /* Get from character database */ } private void OnDestroy() { _vnController.OnContinue -= ContinueStory; _vnController.OnTextComplete -= OnDialogueComplete; _vnController.OnSkip -= SkipToEnd; _vnController.OnRestart -= RestartStory; } }

Text Effects in Visual Novel

The VisualNovelController inherits text effect support from DialogueController:

csharp
// Dialogue with effects _vnController.Continue( "I'm so <shake maxxamplitude=\"3\">angry</shake> right now!", "Character Name", skipCurrent: false ); // Rainbow effect for emphasis _vnController.Continue( "This is <rainb durationperchar=\"1.0\">magical</rainb>!", "Wizard", skipCurrent: false );

See Text Effects for all available effects.

Animation Timing

Avatar breathing animations start with different delays:

  • Left avatar: 1ms delay (immediate)
  • Right avatar: 500ms delay
  • Next icon: 1ms delay (immediate)

This creates a subtle offset between animations for visual interest.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL