LUA Declarative Cutscene & Dialog System
by Macielos
Let's say you wrote (or you plan to write) SHITLOADS of dialogs for your map or campaign in a form like below:
Arthas: I'm glad you could bake it, Uther.
Uther: Watch your tone with me, boy...
How much work do you need to actually turn it into a full-fledged in-game dialog or cutscene? Well, hell of a lot. That was my thought too and because I'm lazy, I decided to code something that will automagically manage cutscenes and dialogs for me, based on some configuration (a list of messages with some additional properties when necessary). In time, I added a lot more features to create a more interactive, RPG-style dialog structure in which you can make choices, react differently, have some extra topics you can ask NPCs about.
It's one of several larger pieces of code I'm working on for Exodus 2 campaign, the first bigger thing I coded in LUA (before I coded in Java, C#, Python and more) and so far the only one polished to a degree I'm not ashamed to show it to others. It still has some issues and you can surely expect me to work on it, but it's usable and already really time-saving, so I decided to share it to gather some feedback before you can see it in all its glory in Exodus 2.
===========
Features:
Declarative Dialog System
- Send lists of unit transmissions one after another, with custom dialog window, auto-management of display times and nice text rendering animation
- Automated scrolling up/down when your text is too long to fit
- Skip single lines immediately with RIGHT arrow when you're done reading or just want to get through a dialog faster without skipping the entirety of it
- Easily declare order of messages, branching, loops, resolving next message based on LUA functions or variables, allowing you to easily prepare highly interactive and non-linear dialog structures
- Involve choices in your dialogs - display a list of options and make a scene halt until you pick a dialog option (UP/DOWN arrows) and confirm it (RIGHT arrow)
- On each message, with delays if required, you can run your own trigger, custom function or even a list of actions
- Every message is logged into a transmission log (under F12) so you can re-read them later
Automated Cutscenes
- Playing a dialog with cutscene configuration requires FAR less code/trigger actions than preparing a cutscene trigger by hand
- You can handle cutscene logic (e.g. camera movements, units moving or turning to one another) by attaching triggers or custom functions to specific messages (with delays if needed)
- Unlike standard cutscenes, automated ones have user controls enabled so that the player can skip lines or make choices. A pleasant "side effect" is that you can pause a game, change options, see transmission log (with previous messages you missed), save and load a game during cutscenes. So far - surprisingly enough - I encountered no issues when loading a game in cutscene. You can easily disable this feature in configuration, but that way you won't have skipping lines or choices
- Automated cutscene skipping - you can either skip single lines with RIGHT arrow, or an entire cutscene with ESC. You no longer need to have if-skipped-then-skip-actions-else-do-nothing after every action in a cutscene trigger. You only need to implement a dialog ending trigger and the mid-game logic is attached to specific dialog lines. For actions happening in the midst of a dialog line you can use utils.timedSkippable(delay, function() ... end). You can also make certain parts of a dialog unskippable (I plan to add some icon to show when a player can skip).
- Fadeins/fadeouts handled by simply defining e.g. fadeOutDuration = 2.0 in a message and fadeInDuration = 2.0 in a following one
Dialog types and customization
- Use a prepared screenplay in a cutscene, in an RPG-style dialog or a simple non-interrupting list of messages.
- Use one of predefined screenplay variants, modify them or prepare your own. You can modify size and positions of UI elements, as well as dialog behaviour like whether or not to lock camera, pause units, show unit flashes or make cutscene interactive
- Just fair warning - not every combination of configuration parameters has been tested, some options are not intended to use together, e.g. simple dialogs will act weird with choices because right arrow also moves the camera right. I suggest to start with predefined configs, but feel free to experiment at your own risk and report suggestions how to make configuration more intuitive and less prone to errors
===========
Sources:
The source code and screenplays from demo are available on Github:
https://github.com/Macielos/Warcraft3LuaLibs
Getting Started:
I suggest to familiarize with the system on a demo map which tells a story of a fellow footman on an epic quest to find
beer. See the screenplays within the map triggers or on Github, then try to modify them and experiment. It's best to do
it in some IDE (I personally use IntelliJ Idea with LUA plugin) or text editor that highlights syntax, brackets,
does formatting etc. You can find a list of available fields and their descriptions in a file ScreenplaySystem.lua,
near the beginning. If there's demand, I can prepare some more detailed docs.
Usage:
- Download a demo map
- Copy Import folder
- Open your map, make sure you have LUA as your script language in map options
- Paste Import folder
- Prepare your screenplays and use them in triggers
- Enjoy
You can speed up working on screenplays by using a small tool I wrote (requires Java 18+, so far you need to build the
project yourself using Maven, I can provide a jar if there's any interest in the tool):
https://github.com/Macielos/ScreenplayGenerator/
Notes:
- This library overrides InitBlizzard() function to call custom initialization code
- This library was never meant to be used in multiplayer. Scenes are displayed for local player only and there is no data syncing mechanism. My guess would be that non-pausing config variants will work, but for pausing ones you'd have to handle pausing game for other players and events like: one player has a choice window open and the other one attacks him.
Compatibility:
- I'm currently using the library on Reforged only, but it should work on 1.31 as it only uses very basic native functions. Just make sure your map uses LUA. I remember that I ran some early versions on 1.31.
Known issues:
- You can unlock a camera in cutscene by pressing e.g. F10. To fix it I periodically 'adjust' the camera.
- Because of above, instead of standard camera functions/trigger actions you should use ScreenplayUtils.interpolateCamera(cameraFrom, cameraTo, duration) or ScreenplayUtils.interpolateCameraFromCurrent(cameraTo, duration). See elf screenplay in demo for examples.
- For some rare camera angles in cutscenes camera does not get locked at all, so you can freely move it. EDIT: I think it happens when you interpolate between cameras with the same target position (so they differ with only e.g. rotation)
- During in-game dialogs you can click on a window which disrupts a game a little. I wanted to make it not register clicks, but so far I didn't find an option for that (maybe someone here will give me a hint). In the worst case I'll just add a config to display in-game dialogs like in vanilla, without a dialog window.
Credits:
@Planetary - my system began as a modification of this system:
https://www.hiveworkshop.com/threads/lua-jrpg-dialogue-system-v1-3.327674/
In time I expanded and reworked it to a degree there's hardly any original code left, but I still use its UI files and some basic code structure. Planetary gave me permission to use fragments of his system. He said he doesn't need any credits, but his system is cool and gave me lots of inspiration, so I'm giving him credits anyway
.
Changelog:
0.0.1
- initial version
0.0.2
[not published in demo map, only available on github]
0.0.3
note: changed some naming for clarity -
skipping now means current mechanism of skipping single lines,
rewinding means interrupting the whole scene
- New options in screenplay variants:
skippable - true/false - allows to skip single lines by RIGHT arrow
1. rewindable - true/false - allows to rewind to the next interactive element OR exits the dialog if there is none
2. disableSelection -true/false - disable selecting and highlightin units, showing healthbars etc. during a dialog or cutscene
3. interruptExisting - true/false - if true, starting a scene of this variant when another one is active will interrupt the existing one, if false, new scene won't be started. This was previously only available in a parametr when starting a scene. I typically have this to true for cutscenes and major dialogs, to false for minor interactions, plus I can still pass a parameter to starting a scene to override this one
4. removed lockControls flag as it was already covered by other flags
- New options for dialog items:
1. onRewindGoTo - used to rewind a dialog with choices, contains an index of next line like in goTo, thenGoTo etc. If there is a cutscene that is generally linear, but allows the player to ask some extra questions or get straight to the mission, we might want pressinc ESC not to take us to these choices, but simply interrupt the whole scene like in WC3 cutscenes. You can now do this but setting this parameter in choices.
2. stopOnRewind - true/false. You might want an opposite behaviour for any line, thus this new field. When set to true, rewinding will stop at this particular item so you need to click esc again to rewind further
3. skipTimers - true/false, now timers added via timedSkippable() function will be interrupted only for lines with skipTimers = true. In practice - set this to true for a first line in a new shot or scene
- Did some cleanups and refactoring after old libs I incorporated in the past - split old utils class onto several smaller utility classes, got rid of majority of global functions and moved 2 remaining ones to GlobalFunctions.lua, enclosed local functions in classes because apparently they are not actually local anyway
- Added Elkonium's DebugUtils and GameConsole to demo map. One I get familiarize with them more, I'll probably remove my own debugFuncs[/note]
0.0.4
- Added possibility to create actors without a physical unit by: ScreenplayFactory.createActorFromType(unitType, player, customName)
- Added important API functions summary and some more docs
0.0.5
- Got rid of two remaining global functions
- Changed funcs in ScreenplaySystem into local where possible
- Added scopes for main classes so that local functions are actually local
- Removed last usage of legacy interface, removed legacy interfaces from demo map (I still need them for now, but you don't
)