Yes. If you need some more reference as to how it’d look in production, I can offer more reference material. This is a cutscene from our experience. Everything you see is triggered by an event based on the animation. The only loops we use are to set up the cutscene, not to advance it. Advancing is controlled purely by events and natural animation running. It is possible.
So, I can animate the dialogue? How would I do that? Would I have a separate animation for enabling and disabling the dialogue gui? Honestly, I don’t even know how to animate things other than humanoids.
For dialogue we got a little lazy and just added a marker to the camera’s animation for that too lol. The camera is basically the main part of the cutscene that controls everything else and the other NPCs follow along with a high degree of accuracy, if not pinpoint. Nigh parallel if you will.
When a dialogue marker in the camera’s animation is reached, we then send that information to a script that’s responsible for displaying dialogue on the screen (not only cutscenes can trigger dialogue which is why we keep it separate). Just like that. Fades, dialogue and all that are all handled in the exact same manner: as marker events on the camera’s animation.
So, there would be a script somewhere else that writes out the text that is given to it when the animation reaches that keyframe? I do have a moduleScript in ServerScriptService that can do that. Another thing, addMarker()
is a built-in function, right? I’ve never made an animation using a script before, I don’t really know anything about it.
Yes, that animateText function passes the arguments over to another module that writes out the text at the bottom of the screen. As for addMarker, that’s a function we created which abstracts away adding keyframe markers to an animation. This is what it’s comprised of:
local function addMarker(markerName, callback)
loadedAnimation:GetMarkerReachedSignal(markerName):Connect(callback)
end
Okay, looks like I’m going to have to do some research on how the heck animations within scripts work, but either way, I should hopefully be able to take it from here, but I just have one last question. How is the moduleScript structured? I only know one way to structure them, but I don’t know if it’s the way I should be.
moduleThing.thisFunction = function(arguments)
--Code goes here
end
Our syntax is kind of wonk and improper, but yeah basically like that, just with colon syntax.
-- Any variables the module needs here
local module = {}
function module:monitor(model, loadedAnimation)
local function addMarker(markerName, callback)
loadedAnimation:GetMarkerReachedSignal(markerName):Connect(callback)
end
-- addMarker called as many times as needed here
end
return module
This module is called by the cutscene module on a new rig as well as an animation we run on it, passed by LoadAnimation.
So there’s just a random animation sitting around that doesn’t do anything except reach keyframes that cause things to happen? Is loadedAnimation
said animation, or is it just some empty animation that gets keyframes added? Either way, how would you even name keyframes?
EDIT: Oh yeah, turns out I can’t really take it from there lol.
The animation isn’t random. In the video a bit above as well as from my explanations in posts previously, I mentioned that we animate a dummy object that serves as the camera’s position. All the camera work seen in that video is done by an object representing the camera. The camera’s CFrame is updated to that dummy object to get the screen effect you see.
Beyond that, yes, loadedAnimation is said animation. Each rig (camera and NPCs) each exist as separate entities in the cutscene’s resource folder and have animations played on them after being loaded with LoadAnimation. Before we do that, we require a ModuleScript of the same name as the rig that contains the same boilerplate in my previous post. The rig as well as the loaded animation are then given to the monitor function, then we call Play on the loaded animation.
Well, we’re able to do all this because all of a character’s actions in a cutscene are just one long animation, not several.
This all makes perfect sense to me, except for this part:
I have no idea what this means.
So, each module script except for the camera one are only for animating the different models?
The camera is also an animated model.
Each of those ModuleScripts handles some kind of visual effect in the cutscene; for example there might be some sounds or particles we want to be heard or shown as the cutscene plays. The camera’s module, on the other hand, controls meta properties for the cutscene (dialogue, transitions, background music, finished flag, etc).
Alright, I just have two more questions:
How do you make the animations in the first place?
How do you name the keyframes?
We make our animations in Blender and import them into Roblox, since it allows us more flexibility and we can also animate multiple rigs at one time easily. The keyframe markers are added through the Roblox Animation Editor or Moon Animation Suite (we do not name our keyframes, at least anymore). The main idea behind the animation is that you animate the whole cutscene as one cutscene.
Sounds painful? Yeah maybe a bit. It’s definitely saved our team in the long run though for being able to implement nice looking cutscenes in the first place with skipping functionality and everything.
If you don’t name your keyframes, then how does this work?
Keyframe markers can be named. GetMarkerReachedSignal works by being given a marker name and then returning a signal that you can connect a function to be ran when that marker is reached in the animation. addMarker is just an abstraction for this process.
So… You don’t name your keyframes, but you are able to call them by their name?
We use keyframe markers which can be named. We don’t name the keyframes. Markers are things that you attach on a keyframe; a named keyframe is a keyframe with a name.
Oh, I see. Alright, time to start remaking my entire cutscene I guess. Thanks for the help!
Alright, just finished the starting cutscene, and everything seems to be working!