Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Question How to manage a large number of cutscenes?

Discussion in 'Scripting' started by mayamay3, Apr 26, 2023.

  1. mayamay3

    mayamay3

    Joined:
    Sep 26, 2022
    Posts:
    9
    I'm making a 2D RPG farming sim (think Stardew Valley as an example) and I'd like to know how this type of games would typically approach handling cutscenes (i.e. keeping track of when to trigger them, which ones have already been seen, etc.). I assume I'd need something along the lines of a game manager script but for cutscenes, but is it better to have one master manager or to have one cutscene manager per scene? Are there any tutorials/examples that I could look at, or anything that would point me in the right direction? All I seem to be able to find are tutorials to make one individual cutscene and then play it as soon as gameplay starts, but nothing on actually managing a number of cutscenes.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,945
    Personally I would make each cutscene its own separate scene, either loaded and chained through your normal play, or else loaded additively, then unloaded when it completes.

    This lets you just use the name of the scene (keep it unique) in a
    HashSet<string>
    of cutscenes that the player has seen. A central manager would receive a call to "Show cutscene FOO!" and it would check if it has seen "FOO!" yet and act accordingly.
     
  3. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209
    Well it's really all camera based. So. You have a few moves the camera will follow. depends how detailed it is. If they are crude cutscenes zooming in on a part of an environment and displaying a text or playing a sound files then it really depends. Because you could achieve all that with just a few points and delays. If the camera was busy delayed for however long the camera is told to take while lerping. The it could follow a list of points.

    Some old games have editors and trigger editors about how to put the camera and how it should move from point to point. You'd set up something similar but private.

    But If you are going for the metal gear solid snake cut scenes then you want to be implementing camera scripts.

    Using other scenes may be nice for a fanciful cutscene such one that uses much higher poly than the game uses while playing.

    Though you can add scenes which may have camera movements on animators, while disabling the game cam.

    Quite a few ways to do depends how deep it needs to be.
     
  4. mayamay3

    mayamay3

    Joined:
    Sep 26, 2022
    Posts:
    9
    Creating a scene and loading it additively sounds like a feasible idea, especially since the game is already structured around having a persistent scene with everything that's needed across the board and then loading each scene on top of that. I'll give it a go. Thanks for the suggestions!
     
  5. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    If I may offer a bit of general advice (apologies if this comes off a bit d***ish or unasked-for)...

    I'm working on a similar game in my spare time (well, not farming, but the tons of text-based cutscenes thing). I remember how easy it was in RPG Maker to add a cutscene with "camera" movement, character movement/amnimation, characters entering/exiting/etc. In RPG maker, going from your intention (whatever you have in your head about how the interaction will play out) to implementation was a breeze. And so instead of spending 2 days on a tiny bit of content, you can do 10 or 20 interactions in that time, freeing you to add a lot more content, and to easily tweak/improve that content instead of edits becoming a burden. So I got to thinking... "how do I make it as easy to write a cutscene in Unity as it is in RPG maker?"

    Note, not all games - even commercial games - do this. Eg Pillars of Eternity has like 5 cutscenes where characters move on screen; it's otherwise all told through text or full-screen art. And it didn't stop that from being a success. However, if you want to produce a lot of content, it's worth spending a lot of time upfront improving your workflow, so that when you go to actually produce content it's fast and dynamic.

    In my case, I spent a timeboxed couple weeks implementing a custom scripting language (as a JetBrains Rider plugin; can share if you're interested) which generates C#. That lets me write small scripts that write out the intention, which turn into much longer and more complex C# scripts. Eg if I write...

    Code (csharp):
    1. namespace burningmime.unity.drama
    2. sceneid school
    3. mode interactive
    4. char LN t=Leena
    5. char DU t=Duncan
    6. char LO self
    7.  
    8. if $day1_logan
    9.     PL <<mood=null>> That's Logan, chosen of Enki.
    10.    end
    11.  
    12. > $day1_logan = true;
    13. bg(cd) clook
    14. adjustpp
    15. spawn DU LN
    16. turn PL,LO LN,LO,1 DU,LO
    17. wait cd
    18. LN Hey, Logan!| Sweet ride!
    19. LO Thanks, Blueberry.| Why, you're lookin' all sorts o' fine today.
    20. dsub emoteEmbarassed(dLN)
    21. look LO,DU
    22. LO Howdy, Duncan; we ain't gone a-braying and a-BASE-jumpin' in a fortnight.
    23. DU Dude, I'm not sure I want to do that until you get some new parachutes.| Tying together garbage bags didn't work as well as you said it would.
    24. LO You just got no sense of adventure, boy.| It's like my pa always said, "If it hurts, you ain't drunk enough."
    25. LN Solid father-son advice.
    26. look LO,PL
    27. LO Who's this fellow?| I haven't seen you 'round these parts.
    28. PL I'm [n] and — || hey, you think you should be smoking that here?
    29. LO Now, [n], you seem like a right good sort, but don't go moralizin' on me now.| A man's got a right to smoke.
    30. PL It's not that.| It's just, you're standing in a puddle of gasoline.
    31. LO I'd better put this out then.
    32. PL Wait, don't just drop—
    33. dsub fireAndExplosion
    34. # ....
    35.  
    That turns into...

    Code (CSharp):
    1.  
    2. // WARNING: This file is automatically generated. Any changes made will be lost.
    3. // ReSharper disable UnusedVariable, RedundantUsingDirective, PartialTypeWithSinglePart
    4. using System;
    5. using System.Collections.Generic;
    6. using burningmime.unity.data;
    7. using burningmime.unity.dude;
    8. using burningmime.unity.gameplay;
    9. using burningmime.unity.ui;
    10. using burningmime.unity.graphics;
    11. using UnityEngine;
    12. using UDebug = UnityEngine.Debug;
    13. using UObject = UnityEngine.Object;
    14. using URandom = UnityEngine.Random;
    15. using UAssert = UnityEngine.Assertions.Assert;
    16.  
    17. namespace burningmime.unity.drama
    18. {
    19.     [MimeScriptGenerated(generatedBy="JetBrains Rider Plugin", generatedByVersion="0.1-SNAPSHOT", sourceFile="Assets/_/drama/ep1/Day1_MeetLogan.mime")]
    20.     [RelevantScenes("school")]
    21.     partial class Day1_MeetLogan : Interactive
    22.     {
    23.         protected override IEnumerator<IActivity> main(IScriptContext ctx, IActivityFactory act)
    24.         {
    25.             IAdventure vars = Inject.get<IAdventure>();
    26.             IStringProvider strings = Inject.get<ILocalizationService>().getStringProvider("burningmime.unity.drama.Day1_MeetLogan.g", "Assets/_/drama/ep1/Day1_MeetLogan.g.en-us.stringpack");
    27.             ctx.addDisposable(strings);
    28.             IStringProvider.Locals locals = new(strings);
    29.             Transform tDU = ctx.findTarget("Duncan");
    30.             ctx.recordAnimationStates(tDU);
    31.             IDialogue dDU = act.dialogueBox(tDU);
    32.             Transform tLN = ctx.findTarget("Leena");
    33.             ctx.recordAnimationStates(tLN);
    34.             IDialogue dLN = act.dialogueBox(tLN);
    35.             Transform tLO = ScriptUtils.unproxyTransform(gameObject);
    36.             ctx.recordAnimationStates(tLO);
    37.             IDialogue dLO = act.dialogueBox(tLO);
    38.             Transform tPL = ctx.player;
    39.             ctx.recordAnimationStates(tPL);
    40.             IDialogue dPL = act.dialogueBox(tPL);
    41.             if(vars["day1_logan"]) {
    42.                 yield return dPL.say(new MessageSpec{text=strings.format("PL000")});
    43.                 yield break; }
    44.             vars["day1_logan"] = true;
    45.             yield return act.adjustCamera(transform).background(out BackgroundActivityToken cd);
    46.             yield return ScriptUtils.adjustPlayerPosition(act, tPL, ctx.findTarget("TARGET_Day1_MeetLogan_PL").transform);
    47.             yield return act.multi(act.genieSpawn(tDU, tPL, ctx.findTarget("TARGET_Day1_MeetLogan_DU")), act.genieSpawn(tLN, tPL, ctx.findTarget("TARGET_Day1_MeetLogan_LN")));
    48.             dPL.turn(tLO);
    49.             dLN.turn(tLO);
    50.             dLO.turn(tLN);
    51.             dDU.turn(tLO);
    52.             yield return act.waitFor(cd);
    53.             yield return dLN.say(new MessageSpec{text=strings.format("LN001"), idleAnimation=IdleDialogue.DEFAULT});
    54.             yield return dLO.say(new MessageSpec{text=strings.format("LO002"), idleAnimation=IdleDialogue.DEFAULT});
    55.             yield return act.sub(Emotes.embarssed(dLN));
    56.             dLO.look(tDU);
    57.             yield return dLO.say(new MessageSpec{text=strings.format("LO003"), idleAnimation=IdleDialogue.DEFAULT});
    58.             yield return dDU.say(new MessageSpec{text=strings.format("DU004"), idleAnimation=IdleDialogue.DEFAULT});
    59.             yield return dLO.say(new MessageSpec{text=strings.format("LO005"), idleAnimation=IdleDialogue.DEFAULT});
    60.             yield return dLN.say(new MessageSpec{text=strings.format("LN006"), idleAnimation=IdleDialogue.DEFAULT});
    61.             dLO.look(tPL);
    62.             yield return dLO.say(new MessageSpec{text=strings.format("LO007"), idleAnimation=IdleDialogue.DEFAULT});
    63.             yield return dPL.say(new MessageSpec{text=strings.format("PL008"), idleAnimation=IdleDialogue.DEFAULT});
    64.             yield return dLO.say(new MessageSpec{text=strings.format("LO009"), idleAnimation=IdleDialogue.DEFAULT});
    65.             yield return dPL.say(new MessageSpec{text=strings.format("PL010"), idleAnimation=IdleDialogue.DEFAULT});
    66.             yield return dLO.say(new MessageSpec{text=strings.format("LO011"), idleAnimation=IdleDialogue.DEFAULT});
    67.             yield return dPL.say(new MessageSpec{text=strings.format("PL012"), idleAnimation=IdleDialogue.DEFAULT});
    68.             yield return act.sub(fireAndExplosion);
    69.             // etc; you get the idea
    70.  
    It also generates a separate file with the strings, in case I ever want to localize.

    Even that function is fairly abstracted from anything Unity does, since the "activity" system allows multiple simultaneous executing activities from the same coroutine -- in that example, player moves and the camera moves at the same time and it waits until both are done before resuming the cutscene. Although for that, https://github.com/Cysharp/UniTask is probably a better choice than trying to cobble together something like the above.

    That's definitely not the only approach. My approach is quite text-centric (see also: RenPy, ONScripter, Naninovel). An animation-first approach would be more in line with Untiy Timeline -- and certainly if you have voice acting and fixed-length "movie like" cutscenes which interrupt gameplay, that type of approach is better. AAA games tend to take approaches where animators will work with tools they're used to (3DS Max, Maya, etc) and the engine can import those. It all depends on the size of your team and project goals.

    And if you're only ever going to have 20 cutscenes in your game; all this is a waste of time. Just make those 20 cutscenes instead of spending a month on infrastructure work.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,945
    Fascinating! Well, writing a transcoder like that is certainly a solution. Very nice.

    I think I would have done this just as interpreting the original stuff line by line, but hey, potayto, potahto...
     
  7. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    Yeah; that's probably simpler. Also has some advantages like no compile times, easier to update things project-wide, etc.

    The plugin gives you a lot of stuff for "free" or at least low-effort (syntax highlighting, error checking as you type, etc). You can also just write out your parser in BNF instead of doing all the tokenization and stuff manually (although I'm not convinced BNF is much easier). So since I was already knee-deep in JetBrains plugin land, it wasn't a huge leap to output the C#. The strings being separated for localization is a nice bonus, too.
     
    Kurt-Dekker likes this.