A modular, JSON-driven visual novel engine built with React and Vite, inspired by Ren'Py.
- Clone the repository
- Run
npm install
- Run
npm run dev
to start the development server
-
Create JSON files in the
public/data
folder:start.json
- Your first sceneorder.json
- Controls the flow of your story
-
Minimal
start.json
example:[ { "text": "Welcome to my visual novel!", "speaker": "Narrator", "background": "room.jpg" }, { "text": "Would you like to continue?", "speaker": "Narrator", "type": "choice", "choices": [ { "text": "Yes", "value": "Yes" }, { "text": "No", "value": "No" } ] } ]
-
Minimal
order.json
example:[ { "event": "start.json" }, { "condition": "user_choice == 'Yes'", "event": "continue.json" }, { "condition": "user_choice == 'No'", "event": "end.json" } ]
-
Add assets:
- Place background images in
public/assets/backgrounds/
- Place audio files in
public/assets/audio/music/
andpublic/assets/audio/sfx/
- Place background images in
-
Run your visual novel with
npm run dev
That's it! You now have a working visual novel with branching paths.
The Visual Novel Engine is built with a modular architecture that separates concerns:
- State Management: Global game state with choice tracking
- Event System: JSON-based event processing with conditional logic
- Rendering Layer: React components for UI elements
- Audio System: Background music and sound effects management
Each event file is an array of nodes that define the visual novel's content:
[
{
"type": "dialogue",
"text": "The text to display",
"speaker": "Character name or null for narration",
"background": "background-image.jpg",
"music": "background-music.mp3",
"sfx": "sound-effect.mp3",
"effect": "\"fade\" OR { \"variableName\": \"value\" }"
}
]
Property | Type | Description | Example |
---|---|---|---|
type |
string | Node type (dialogue, choice, jump, etc.) | "dialogue" |
text |
string | The text to display | "Hello world" |
speaker |
string|null | Character name or null for narration | "Alice" |
background |
string | Background image filename | "forest.jpg" |
music |
string | Background music filename or "stop" | "theme.mp3" |
sfx |
string|array | Sound effect(s) to play | "click.mp3" |
effect |
string|object | Visual effect or variable assignment | "fade" or {"var1": "value"} |
choices |
array | Array of choices (for choice nodes) | See below |
target |
string | Target event file (for jump nodes) | "chapter2.json" |
{
"text": "Choice text",
"value": "Value to store"
}
The order.json
file defines the flow of events:
[
{
"event": "start.json"
},
{
"condition": "variable == 'value'",
"event": "conditional-event.json"
}
]
Property | Type | Description | Example |
---|---|---|---|
event |
string | Event file to load | "start.json" |
condition |
string | Condition to evaluate | "user_choice == 'Yes'" |
Conditions use a simple expression syntax:
variable == value
: Equality checkvariable != value
: Inequality checkvariable === value
: Strict equality checkvariable !== value
: Strict inequality checkvariable > value
: Greater thanvariable < value
: Less thanvariable >= value
: Greater than or equalvariable <= value
: Less than or equal
Variables are stored in the global choice state:
- Set variables with the
effect
property as an object:"effect": {"varName": "value"}
- Access variables in conditions:
"condition": "varName == 'value'"
- dialogue: Standard text display
- choice: Presents options to the player
- jump: Jumps to another event file
- effect: Executes effects without displaying text
"fade"
: Fade transition between backgrounds"glitch"
: Glitch effect for scene transitions
- Set with the
music
property - Use
"music": "stop"
to stop the current music - Music will loop automatically
- Set with the
sfx
property - Can be a single string or an array of strings
- Will play once when the node is displayed
Events are tracked with the following statuses:
completed
: Player has viewed all nodesskipped
: Event was skipped due to conditionspending
: Currently in progressunseen
: Not yet viewed
The engine provides a comprehensive save/load system:
- Saves are stored in localStorage with the prefix
vn_save_
- Each save contains the full game state, including:
- Current event
- Current node index
- All choice variables
- Event status tracking
- Timestamp
The UI can be customized by modifying the CSS in src/App.css
. Key CSS variables:
:root {
--primary-color: #6a4c93;
--secondary-color: #8a5cf5;
--text-color: #f8f9fa;
--bg-color: #1a1a2e;
--dialog-bg: rgba(26, 26, 46, 0.85);
--dialog-border: #8a5cf5;
--choice-bg: rgba(106, 76, 147, 0.8);
--choice-hover: rgba(138, 92, 245, 0.9);
}
Auto mode automatically advances dialogue after a delay calculated based on text length and reading speed.
Adjust the typing speed of the text with the text speed control in the settings panel.
The event viewer shows all events and their status:
- White: Unseen
- Orange: In Progress
- Green: Completed
- Red: Skipped
Click on completed events in the event viewer to replay them.
For large visual novels, consider:
- Lazy Loading: Load event files only when needed
- Asset Preloading: Preload assets for upcoming scenes
- Memory Management: Unload unused assets
- Add the type to the
handleAdvance
function inNovelEngine.tsx
- Create a new component for the node type
- Add rendering logic in the main component
- Add the effect name to the
SceneBackground.tsx
component - Implement the effect CSS in
App.css
- Create a new
CharacterSprite.tsx
component - Add character positioning logic
- Update the JSON schema to include character properties
- Images not loading: Ensure they're in the correct directory and the filename matches exactly
- Audio not playing: Check browser autoplay policies and ensure files exist
- Conditions not working: Verify variable names and values match exactly
Enable debug mode by adding localStorage.setItem('vn_debug', 'true')
in the browser console.
visual-novel-engine/
├── public/
│ ├── data/ # JSON event files
│ └── assets/
│ ├── backgrounds/ # Background images
│ └── audio/
│ ├── music/ # Background music
│ └── sfx/ # Sound effects
├── src/
│ ├── components/ # React components
│ ├── hooks/ # Custom React hooks
│ └── utils/ # Utility functions
useGameState()
: Manages the global game stateuseAudioManager()
: Handles audio playback and management
NovelEngine
: Main component that orchestrates the visual novelDialogueBox
: Displays text with typewriter effectChoiceSystem
: Renders and handles player choicesSceneBackground
: Manages background images and transitionsEventViewer
: Displays event completion statusControlPanel
: Provides UI controls for the engineSaveLoadMenu
: Handles saving and loading game state
[
{
"type": "choice",
"text": "What path will you choose?",
"speaker": "Narrator",
"choices": [
{
"text": "Path A",
"value": "A"
},
{
"text": "Path B",
"value": "B"
},
{
"text": "Path C",
"value": "C"
}
]
},
{
"type": "dialogue",
"text": "This will only show if Path A was chosen",
"speaker": "Narrator",
"effect": {
"pathTaken": "A",
"karma": 10
},
"condition": "user_choice == 'A'"
}
]
[
{
"type": "dialogue",
"text": "Your karma increases!",
"speaker": "System",
"effect": {
"karma": 10,
"reputation": 5
}
},
{
"type": "dialogue",
"text": "High karma response",
"speaker": "NPC",
"condition": "karma > 5"
},
{
"type": "dialogue",
"text": "Low karma response",
"speaker": "NPC",
"condition": "karma <= 5"
}
]
- Organize JSON files by chapter or scene
- Use consistent naming conventions for variables
- Test all branches of your story
- Back up save data regularly
- Document your variable system for complex stories
- Optimize image sizes for better performance
- Use audio sparingly to avoid overwhelming the player
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.