The Interaction System
The interaction system starts with interactables which are classes that derive from a main abstract Interactable class. They recieve both key down and key up events from the bound interact key or mouse button. Based on that input it will perform any logic specified in that derived class. It can also, in some cases trigger an output which is just a delegate that any external system(s) can subscribe to, to perform additional custom logic when that delegate gets called.
The InteractableRaycaster MonoBehaviour is where everything starts, its purpose is to shoot a ray (every frame) from the camera (in the camera’s forward direction) and handle any interactables that are hit. If an interactable is hit, the method “IsCurrentlyInteractable” (in the Interactable class) gets called which returns a boolean. This allows each type of interactable to have its own implementation for when it should be interactable and not. For example, this allows for a maximum range between the interactable and the player to be specified, which can be different for each instance of interactable, instead of being completely dependent on just the raycast range for more fine-tuning.
If the interactable is currently interactable it will be outlined and cached as the current interactable in range. The crosshair will also turn blue to indicate you are currently looking at an interactable object. When looking away from the interactable, the cached interactable will be set to null, the crosshair will revert and the outline will be reset.
When pressing or releasing the interact key the Interact method in the raycaster will be called. The reason for passing both down/up states into the Interact method is that some interactables may require this behavior. For instance, when implementing a button, it should turn on when key is pressed and off when released. While switches should only toggle the output on key down. To keep the code clean, I implemented a class attribute that can be added to Interactables. The attribute specifies which key states the interactables should be triggered on. The implementation looks like this:
This makes the code a lot cleaner, the InteractableToggle class is pretty much empty, instead of having a bool check to only toggle the output every other input (the solution I had before implementing the attribute).
There are a fair few implementations of Interactables. Here is a list of some of them:
- InteractableToggle, used most often in DreamWalker, toggles on/off at key down.
- InteractableMovable, used in the statue and pyramid puzzles. Specifies a starting state of an object, an array of possible states, and an index of a correct state. If the first state in the array is occupied by another movable, the current movable will try to move to the next, etc. If the movable is in the correct state it will give an output.
- InteractableMoveToPlatform, the platforms that when clicked, the player will move to them. There are options to specify the arc in which the player travels (using bezier curves), as well as the speed using an animation curve.
- InteractableTrigger, an interactable that cannot be clicked but will only trigger the output when the player walks into its trigger collider.
- MovingPlatforms, the moving platforms just before the pyramid puzzle (made by Sebastian).
Some of these interactables make up the framework for the inputs in the puzzles, while others are more standalone (such as the MoveToPlatform). Now there’s only one key thing missing, what these inputs trigger. For puzzles, the PuzzleHandler lays the foundations of that. It’s quite simple, it takes an array of Interactables as input, and when they are all outputting true, the output will be triggered.
The output is quite hard to make correctly because it has to do a lot of things, for example, when completing the statue puzzle, a cutscene has to be triggered, a sound must play, the platforms you used to traverse to the island have to be disabled, the bridge to lead to the next part of the game must become interactable, etc. Other puzzles have completely other outputs that must be triggered. I realized that there were two ways to solve this, either I make a script for every puzzle that will do the things mentioned, which would get very messy very quickly. Or I make a generic system where all events can be customized completely in the inspector. This would mean that I wouldn’t have to do any additional coding, I can just give all the power to the designers to customize exactly what will happen when either a puzzle is completed or an interactable is triggered. I of course made the latter which I named the InteractableEventSystem.
This is how the inspector looks:
You attach either a puzzle handler, an interactable, or a UI button to the field, the event system will subscribe to a delegate in the relevant object that will be triggered when the interactable or puzzle handler’s output switches to true. The event system can also be customized to trigger as soon as it is enabled. The events will be executed from top to down (as configured in the inspector), you can add a delay to events and some events will wait until they have finished executing before running the next event. For example, when showing subtitles (makes it easy to make a subtitle sequence without having to think about delay etc) and waiting for the player to close the diary (in the beginning), etc.
Because of the complexity of the script and the number of things you can do with it, it had to have a custom inspector to make sure everything has the correct layout and that only relevant controls (based on the event type) are shown at one time. Luckily, I had worked a lot on a tool just before the project so I had even made an editor library that I could just import into the project. This enabled me to get the event system up and running in just a single day because I was still in the tool-making mindset.
The tool was really fun to make, and it was a joy to see how much it was used. There are over a hundred instances of the event system across all scenes in the game (including test scenes). I can just imagine how much time would’ve been lost during the production for both programmers and designers, if all of that would’ve been hardcoded.