The world inside of a video game only captures a player if it feels responsive, and user interaction is a key part of that process.
In this blog post, I'll be going over a very simple and quick implementation of some basic user interaction framework that would allow any developer to easily trigger interaction as well as give the player text-based feedback on a widget.
We'll be doing this in Unreal Engine 5.0.1. Let's dive in.
INVOLVED CLASSES
First, let's talk about everything that's getting involved here.
BP_PlayerController (APlayerController)
BP_FirstPersonCharacter (ACharacter)
WBP_InteractionScreen (UUserWidget)
BPI_InteractionMessenger (UInterface)
BP_InteractableActor (AActor)
BP_ChildInteractableActor (BP_InteractableActor)
All of these classes will have some involvement in this process. Please note that some of the code I implement may be better suited in other classes based on where we set and get variables, but I've chosen implementation based on what I believe is the best organizational habit, not necessarily the most performance-optimized.
INITIAL STEPS
The first thing we'll do is create the widget. Mine will look a bit like this:
It's not much, but for now it's enough. You can certainly build more complex behavior from this root, though, and that's what the goal is: to establish a solid base from which to build on.
We need to make that text element its own variable, and set up a function that looks like this:
We'll call this OnConstruct as well to keep the screen off and empty when it's spawned and added to the viewport. We're using an FText variable instead of an FString so that we can Localize these interaction prompts later in development.
We set the text before we set the text to visible so that we don't have a chance to watch the text change, as we're not setting it to empty when we hide the widget. (It's just one less thing to do).
Next up is the BP_PlayerController. This is where we'll be handling all of the back-and-forth with the widget. I won't go through the setup for getting this as our default controller.
Here, we're going to spawn the interaction widget on begin play, store that variable, and add it to the viewport. We'll add an event to call from elsewhere that runs the widget event we made earlier.
Lastly, we'll create our Blueprint Interface, which I'll explain more about once we implement it. It's a getter that'll return an FText variable.
Right, so now we're done with our basic setup. We've already created the BP_InteractableActor and its child, which inherits from Actor. Let's move on to how we're implementing this system and what the point is.
IMPLEMENTING THE LINETRACE
We'll start in the character BP. Firstly, we need a reference to our BP_PlayerController, so we'll set that up on BeginPlay.
Now, our primary method of interaction is going to be based on where the player is looking. The best tool for this, then, is to use a LineTraceByChannel, since we should only want to interact with objects we can physically see.
We'll start the trace from camera position and cast it out ~ 250 units from there, using a variable for the interaction reach so we can change this easily.
Now, we need to talk to interactable actors that we hit. There are about half a dozen effective ways to do this. You could make interaction go through a component and see if the hit actor has it. You could cast to a type that inherits from AActor from which all interactable actors would inherit (which fits with what we've set up). However, I like the option I've gone with the best, for a couple of reasons.
Here's how we'll handle talking to interactable actors.
If HitActor is valid, it means we've hit something in the scene. If it's not valid, we need to hide the text in our WBP_InteractionScreen.
However, if the actor is valid, we'll send it an interface message to get its InteractionText that we want to show up on the screen. We can implement this interface function inside of any actor we want to interact with, and it can return whatever we need it to. That's the flexibility of interfaces.
Interfaces will silently fail if the object we're messaging doesn't implement it, so we need to use another IsValid-style check, DoesImplementInterface. If we only message the actor without checking, we won't know whether the message has silently failed or not. So if the actor implements the interface, we show the interaction screen; otherwise, we hide it.
We're almost done. Now we need to implement the interface.
IMPLEMENTING THE BLUEPRINT INTERFACE (BPI)
We've got two actors we plan to use: BP_InteractableActor (the cube) and BP_ChildInteractableActor (the sphere). We'll turn the tick off for both at the moment, as what we're doing doesn't require the tick.
Let's implement the BPI in the BP_InteractableActor. All we need to do here is create a member variable inside of the actor for us to return, like so:
Now, this implementation will be inherited by all of BP_InteractableActor's children. The designer can simple change the value of the InteractionText member variable so that different children can return different interaction prompts. You can see this working in the gif below. I've turned the text red so that it's easier to see against the white background of the objects.
And that does it! It's a pretty quick, simple implementation, but it provides a base for easy interaction prompts, and you can probably see how the use of an interface can lead to interaction implementation on a larger scale! But we'll explore that more difficult implementation in another post. That's all for now!
Commentaires