I got asked on Twitter if I could go deeper into the interaction system I made for Marie's Room. Alright! More reasons for blog posts, so here we go!
First of all let me clarify a few things. I'm not going to just copy paste some images of my blueprints in here, I guess that nobody (including me) would benefit from that. Also, I'm primarily an artist and I set up this system all by myself, so there are probably a thousand other (and better) approaches. But this one worked for me and I'll try to explain the how, what and why as best as I can. Enough talk, let's start!
The Base actor blueprint
I use a system called polymorphism. What this means is that all interactive props in Marie's Room inherit from the same base actor 'InteractiveObject'. This blueprint doesn't contain a lot, but is quiet important as you'll see later. What it does contain, is the following:
- Function 'OnActivate' - This function is empty
- String variable 'ObjectText' - Default value is 'Interact'
- Float variable 'InteractionDistance' - Default value
Don't worry about them too much for now, more on all of this later.
I have two kinds of blueprint child classes. For people who didn't play the game. There are two kinds of objects in the game that each, behave in a certain way.
You've got objects where, when you interact, a piece of monologue starts. Playing voice over audio and displaying subtitles. These will be called 'StoryObjects' from now on.
The other type of object is called a 'ContainerObject'. Most of the time an object that stores another object (eg. a deskdrawer). This one plays voice over audio, displays subtitles and displays another kind of blueprint which shows the object in pop-up fashion. Which can be turned around etc.
Both of these inherit from the base class 'InteractiveObject' like previously mentioned. For now I'm going to focus on what I did for the StoryObjects. I might focus on the ContainerObjects another time, since they're a little harder to explain.
What does the StoryObject blueprint contain and do?
The StoryObject blueprint doesn't add a lot to what's already there (because of the inheritance). It adds:
- A struct variable 'StoryEventData' - A struct is a collection of variables that can be past on as one variable in a package (super handy! more info in linked documentation). Contains:
- An array of strings 'Text' - The subtitles, cut up according to what should be on screen
- An array of sound waves 'VO_Audio' - Matching the subtitles
- A sound wave 'BackgroundMusic' - Optional, if there's one it will play as background music
- A mesh. This mesh get overridden by the child class blueprints
That's it for data. There's a little more in there, but that's not important for what we're explaining here.
Now, remember that empty 'OnActivate' function? In this blueprint it gets overridden and just fires the 'StarStorySequence' passing on the StoryEventData as you can see in this picture.
The UIMaster is the one class that gets communicated with a lot. He ties it all together. It's just an empty actor in the scene, with a lot of data, functions, events and so on. (maybe this is where I could've done it a bit better)
So all StoryObjects are children from this parent class and they fill in their respective data, override the visual, etc. Every interactive prop does also contain a collider and is given an Object Type (eg.: 'InteractiveProp'). More on that later.
Now there's just two more important things to cover. What does the 'StartStorySequence' function in 'UIMaster' do? And where does the 'OnActivate' function get called? First up:
'StartStorySequence', what does it exactly do?
In essence what it does is pretty simple. It's a custom made for loop. I couldn't use the for loop the UE4 blueprints provide since you can't use a delay node inside a loop. Nevertheless, the thing that happens here is going over all the strings inside the 'Text' array, changing the text of the subtitle widget to that string. Also, playing the corresponding sound wave of the 'VO_Audio' variable and waiting (with the delay node) for the length of that audio file. Then it starts over with the next index, if the array is done the function basically stops. That's the gist of it. Pretty simple. Underneath you can find the complete function.
But when does all this activate?
So, we've covered everything what will happen when you interact with a StoryObject now. But what does interacting with an object mean? Let's take a look.
The meat of this system is a ray tracer. This ray gets defined in the Event Tick (update function) of the 'RayTracer' class I made. Again, just an empty actor in the scene with a few functions.
I use the function LineTraceForObjects. This is pretty handy because now I only get a successful hit when the given object types (that I mentioned earlier) are being hit. So the rest of the collision meshes get ignored. The ray starts from the actor's eye viewpoint and a forward vector multiplied with a maximum interaction distance defines the end of the ray. Like you see in the picture here.
Now we can get all the information about the actor that got hit, if any.
Now the previously mentioned 'polymorphism' comes into play. I cast the actor that got hit to the base class 'InteractiveObject'. What this means is that every 'InteractiveObject' (both StoryObjects and ContainerObjects) will result in a successful cast. If it's another type of object than that's not relevant to this function.
Than I check the actor's own InteractionDistance. When the distance of the start of the ray to the impact point is lower than its InteractionDistance, then I know that the object is both in range and view of the player. This is also the point where I communicate with the crosshair widget, so it knows it should change form and use the variable 'ObjectText' to change the text beneath the crosshair. But I'm not going too deep into that here.
Now whenever the 'Interact' button gets pushed, a timer starts and whenever the timer has reached the end and the button is still being pushed + the object is still in view and in range, the 'OnActivate' function gets called.
Because of the polymorphism the OnActivate of all InteractiveObjects can be called.
anything else than starting monologue or a pop up widget?
Yes! I started wondering this when this system was already set up. And it actually didn't need any change at all! Like mentioned earlier most props are either a 'StoryObject' or a 'ContainerObject'. But what about the television in Marie's Room? The screen turns on and a sound effect gets played. Well, since it was possible to override the 'OnActivate' function in every blueprint that inherited from it, I did just that in the television blueprint. It's a child of the StoryObject class but inside of the television blueprint itself I overwrite the OnActivate function again. This allowed me to do whatever I wanted in there! Although I kept the special props at a minimum.
Polymorphism is your friend! :) Okay, when you're a programmer and you're reading this you're probably like "Well of course you dummy!" But I wanted this post to be understandable for people who're maybe not so familiar with all this as well and I hope some people find it useful. Setting up this system taught me a lot, and in the end I'm very happy with the result. It appeared to be very versatile what I really liked. Also, using structs instead of separate variables was a blessing. Very late in the project I decided to pass around another value, I just had to add it to the already existing struct and done!
If you've got any feedback or further questions don't hesitate to drop them here. I'll gladly read your comments.
Have a great day!