This chapter will specifically walk through using scripted Songs to implement Dialogue Cutscenes into a mod, like those seen in Week 6.
Assets
To start off you must make a few assets (or use the default ones). Let's go over the json for the default conversation box.1
{ "version": "1.1.0", "name": "Default", "assetPath": "speech_bubble_talking", "isPixel": true, "flipX": false, "flipY": false, "offsets": [-20, 20], "scale": 5.4, "text": { "offsets": [225, 450], "width": 900, "color": "#3F2021", "fontFamily": "Pixel Arial 11 Bold", "shadowColor": "#D89494", "shadowWidth": 2 }, "animations": [ { "name": "enter", "prefix": "speech bubble normal0", "offsets": [0, 0], "frameRate": 24 }, { "name": "idle", "prefix": "speech bubble normal0", "offsets": [0, 0] } ] }
The most important properties here are going to be the asset path, the textbox, and the animations. Fill these out to incorperate your custom assets, and put them into your mod under assets/mods/MyDialogueMod/data/dialogue/boxes/myBox.json.
Next we can define some speakers with their own individual json, let's look at specifically boyfriend's pixel speaker.2
{ "version": "1.0.0", "name": "Boyfriend (Pixel)", "assetPath": "weeb/portrait-boyfriend", "flipX": false, "isPixel": true, "scale": 5.4, "offsets": [800, 248], "animations": [ { "name": "talkEnter", "prefix": "portraitEnter0", "frameRate": 12 }, { "name": "talk", "prefix": "portraitTalk0" } ] }
As you can see, the data is very simple with not that many properties. Fill these out with your custom sprites, and make sure the offsets are correct!
In conclusion, the assets you need are:
-
A font
-
A dialogue box
- With an enter and normal animation
-
A speaker
- With an enter and normal animation
Conversations
Conversations, as the name implies are the conversations between two or more characters in a dialogue window. We can define these through json as well, as shown by the senpai conversation.3
{ "version": "1.0.0", "backdrop": { "type": "solid", "fadeTime": 2.0, "color": "#BFB3DFD8" }, "music": { "asset": "Lunchbox", "fadeTime": 2.0, "looped": true }, "outro": { "type": "fade", "fadeTime": 1.0 }, "dialogue": [ { "speaker": "senpai", "speakerAnimation": "talkEnter", "box": "roses", "boxAnimation": "enter", "text": ["Ah, a new fair maiden has come in search of true love!"] }, { "speaker": "senpai", "speakerAnimation": "talk", "box": "roses", "boxAnimation": "speaking", "text": [ "A serenade between gentlemen shall decide where her beautiful heart shall reside." ] }, { "speaker": "bf-pixel", "speakerAnimation": "talkEnter", "box": "roses", "boxAnimation": "speaking", "text": ["Beep bo bop"] } ] }
With this you can now make your own custom dialogue, but we now need to actually show it in-game!
Scripted Dialogue
As shown in the previous chapter, we will use the same technique, but this time with dialogue! Let's take a look at the senpai dialogue as an example.4
// ... // Line 30 public override function onCountdownStart(event:CountdownScriptEvent):Void { super.onCountdownStart(event); // Skip the cutscene unless we are in Story Mode, or Pico Mix specifically. if (PlayState.instance.currentVariation != 'pico' && !PlayStatePlaylist.isStoryMode) hasPlayedCutscene = true; // Skip the cutscene if we're playtesting in the Chart Editor. if (PlayState.instance.isChartingMode && !hasPlayedCutscene) hasPlayedCutscene = true; if (!hasPlayedCutscene) { trace('Pausing countdown to play cutscene.'); hasPlayedCutscene = true; event.cancel(); // CANCEL THE COUNTDOWN! transitionToDialogue(); } } // ... // Line 75 function transitionToDialogue() { trace('Transitioning to dialogue.'); PlayState.instance.disableKeys = true; PlayState.instance.camCutscene.visible = true; var black:FlxSprite = new FunkinSprite(-20, -20).makeSolidColor(FlxG.width * 1.5, FlxG.height * 1.5, 0xFF000000); black.cameras = [PlayState.instance.camCutscene]; black.zIndex = 1000000; PlayState.instance.add(black); black.alpha = 1.0; var tweenFunction = function(x) { var xSnapped = Math.floor(x * 8) / 8; // black.alpha = 1.0 - xSnapped; }; new FlxTimer().start(0.25, _ -> { PlayState.instance.remove(black); RetroCameraFade.fadeBlack(PlayState.instance.camGame, 12, 2); }); FlxTween.num(0.0, 1.0, 2.0, { ease: FlxEase.linear, startDelay: 0.25, onComplete: function(input) { startDialogue(); } }, tweenFunction); } function startDialogue() { PlayState.instance.disableKeys = false; var targetDialogue = PlayState.instance.currentVariation == 'pico' ? 'senpai-pico' : 'senpai'; PlayState.instance.startConversation(targetDialogue); } // ...
As you can see, we define the same variables, create the same black rectangle, but we call a different end function and do some transitions.
In a minimal scenario, we really would just need to disable the input of the song, and then start the dialogue with PlayState.instance.startConversation(string)
-
https://github.com/FunkinCrew/funkin.assets/blob/main/preload/data/dialogue/boxes/default.json ↩
-
https://github.com/FunkinCrew/funkin.assets/blob/main/preload/data/dialogue/speakers/bf-pixel.json ↩
-
https://github.com/FunkinCrew/funkin.assets/blob/main/preload/data/dialogue/conversations/senpai.json ↩
-
https://github.com/FunkinCrew/funkin.assets/blob/main/preload/scripts/songs/senpai.hxc ↩