Converting Interactable Legacy Input to the New Input System in Unity 2021
In this article, we’ll blow some stuff up (with the new Input System).
The project used the Standalone Input Module component on the EventSystem game object, which isn’t directly related to what we are doing here, but let’s take care of it now that we know there is an issue.
With the new Input System enabled, the old InputManager is disabled as this component is warning us.
Effectively, this component does nothing right now.
Click the “Replace with InputSystemUIInputModule” button.
The component should be replaced with the “Input System UI Input Module”.
Yea…it probably wouldn’t hurt for them to spend a little time brainstorming a new name for that component.
For more information on this component, see the Unity documentation here:
You can use the Input System package to control any in-game UI created with the Unity UI package. The integration…
Back to the Task at Hand
Let’s add another Action to our existing Character Action Map.
Name it Interact, give this new Action the Interaction type Hold, and assign it a key binding.
Save the asset.
I Like Big Booms and I Cannot Lie
If we run our game and look for the first interaction, we find a place to pickup some C4, plant it, and then trigger it.
Now that we have a good idea of what we are looking for, let’s look over our Hierarchy so we can narrow down where changes need to be made to convert this to the new Input System.
Both of these game objects use a script called “Interactable Zone”.
Alright, now we are getting somewhere.
Looking at the InteractableZone script, we can see how the input is handled, starting with an enum for press or holding down a key.
The variables also reference key state and just beyond that we can see an event system with subscribers similar to the new Unity Input System.
Frankly, I can imagine many messier input systems in old projects, but either way is not really an issue.
Whatever the current code is, all we NEED to know is what the in-game input effects and reactions should be.
From there we can simply create our own script and implement these input events from scratch with the new Input System.
It will be much easier to use the current InteractableZones as a reference but create our own script than to modify this script.
I’ve created a new script called “ZoneInteractions” to replace the old InteractableZone script.
For each zone associated with the C4 sequence, pick-up, plant, and detonate, I’ve added the new script and disabled the old.
It will also be helpful to place your scripts side-by-side while you are working on the new script in your code editor.
I’ve copied over any variables that I think will still be necessary for this script.
Next, I’ve added a variable for our new InputActions generated script.
We’ll need to assign _inputActions in our Awake() method to make use of it later in the script.
Continuing to take this on piece by piece, I’ve brought over the OnTriggerEnter() method from the old script.
The first thing I notice is that the “UIManager” lines were throwing errors, so I added the using statement for the existing Game.Scripts.UI namespace the game has been using to display UI.
Next, instead of the _zoneKeyInput variable that was previously used, we want to access the binding key name for interactions through our InputActions script as shown above.
I’ve also gone back and added HoldAction to the ZoneType enum.
I believe I could rewrite this to reflect that the Zone Type doesn’t need to differentiate between action and hold action, but that isn’t necessary to make this work.
Now, I’d like to test the OnTriggerEnter() method is working correctly and displaying the correct button binding in the interaction message.
But first, I need to assign some values to the new script to reflect the old script as shown above.
Swing and a miss.
I’ve added a reference to the first element of the array to my code in the hopes that will resolve this issue.
Well, that’s getting closer.
The binding appears to contain the input source (keyboard) in addition to the key.
Since I’m going to have to chop up this binding.tostring result anyways, I’ll just create a variable for it and do that work in the Awake() method.
By accessing the binding.tostring() and then using .substring() and .toUpper(), I should have just the key as an uppercase result now.
And yes, when I run the game, the message displayed is exactly what I was hoping for!
Before I move on, I’m going to go ahead and copy over the OnTriggerExit() method before I forget.
Alright, moving on to the existing Update() method as shown above.
The if/else-if statements here can all be handled using the new Input System’s events and subscriber methods.
To make use of the Event/Subscriber system built into the new Input System, I first want to make sure that the Character Action Map is enabled in the Awake() method.
I then will use the .performed event and the resulting InteractOnperformed() subscriber method.
As you can see, the InteractOnPerformed() method contains the original Update() method code without all of the if/then statements that were for determining key input.
From here, we can see that there are 3 methods called (CollectItems(), PerformAction(), and PerformHoldAction()) which we have yet to implement in the new script.
Once those methods are added we can see that another method, CompleteTask(), is referenced and will need to be added.
The onZoneInteractionComplete, onHoldStarted code should be unnecessary so I will remove those lines of code.
After removing those lines and adding CompleteTask, we get the code shown above.
Now that makes our Update() method code transfer complete.
There are a few more methods in the original script that may be accessed elsewhere unbeknownst to us and are unrelated to the input system so I will bring those over now.
Above, we can see the remaining methods from the original InteractableZones script that we have yet to implement.
I’ve simply copied over the GetItems(), GetZoneID() and ResetAction() methods to the new script without modification.
Now, the OnDisable() event uses an event that I hadn’t brought over to the new script yet, as I wasn’t sure whether it would be necessary.
At this time, I think it probably will be so let’s knock that out.
Above is the code related to onZoneInteractionComplete.
We can see it references the original script name of “InteractableZone”, so I’ll change that to the new script name “ZoneInteractions”.
With the script name references updated, I can see that I have missed one method from the original script, SetMarker().
With the SetMarker() method added to our new script that leaves one bit of housekeeping.
Currently the old script still has the namespace assignment and the new script does not.
Now I’ll go ahead and comment out the original script’s namespace and uncomment the namespace in the new script.
Running the game, the functionality was clearly not working.
So I selected the onZoneInteractionComplete and noted where it was referenced in the original script.
I had removed several invoke calls to it earlier and needed to update these methods with it.
This included the CollectItems(), PerformAction(), and CompleteTasks() methods.
While playtesting, I realized I had setup the Interact action to always be hold when the game calls for both a normal press and hold interactions.
Opening up the InputActions asset, I removed the Hold interaction from the Interact Action, and added a new InteractHold Action with the Hold interaction.
In the Awake() method, we need to add the InteractHold.performed event and subscriber method.
We then need to cut the case ZoneType.HoldAction code from the normal InteractOnperformed subscriber method and paste it into our new InteractHoldOnperformed subscriber method.
When attempting to run the game and go through the c4 scenario, I found that the detonation was not occurring.
It took a while to track down where these game objects were in the hierarchy, and once found, left me wondering if this is where they should be.
They were buried in the skeleton game objects of the player’s character model.
I created a fresh project and imported the starter files for the project, and yes, this is where it is by default in the project.
I moved them out of the Model hierarchy to directly be childed to the Player GO in the active project.
I then checked that the detonator game object reference on the Player’s script component was still referenced correctly.
Going back to the project with all original scripts and settings, I looked through the relevant scripts:
The above screenshot is from the original Player script.
This screenshot is from the original detonator script.
And finally this is from the original C4 script.
What is really confusing me is where the connect from the InteractableZone script is.
In the original InteractableZone script, we can see the PerformAction() method which is what is triggered when we are in the trigger zone for detonating the C4.
All this does is take a list of items (game objects) tied to this zone because they have relevant uses and sets them to active at which point their own scripts take over.
Here’s the rub though.
There are no game objects tied to the Interactable Detonate C4 Zone.
So when you hit the interaction button and it triggers the explosion, its not triggering from PerformAction() method.
So, how come this works (because it does go boom with this setup)?
Well, it is somehow tied to the Player script, which just seems like a confusing way to implement this considering there is an entire interaction system already setup and used for literally everything else.
In the original Player script, we can see an event system setup for onZoneInteractionComplete referencing the InteractableZone script.
This is where the actual explosion is triggered.
Back to the Work-In-Progress Update
Bottom line, we need to modify the player script to get our explosion working again.
The player script is still referencing the old InteractableZone script instead of our new ZoneInteractions script.
So I’ve created a new subscriber method as shown above that will mimick the old subscriber method.
Above we can see the old subscriber method, commented out.
Above we can see the new subscriber method modified to reflect the old subscriber method’s code.
The generated name for the method above looked atrocious, so I renamed it to oneInteractions_OnZoneInteractionComplete.
I was going to test the scene, but a console error points to another reference to the old interaction script.
In our Player script, OnDisable() method.
Above we can see modified OnDisable() method of the Player script to phase out the old interaction script and reference the new.
Running the game, we successfully pick up the c4, place the c4, but when we hit “E” to trigger the explosion, a couple Unassigned Reference exceptions are generated in the console.
I mean, this error, goes back to my original point that there are no zone items assigned.
Yet it works like that in the original project so…
Actually, looking at the Zone Interactions script component on the Interactable_Detonate_C4_Zone game object, I had at some point added the C4 game object to the Zone Items.
Removing that to match the blank Zone Items from the original script removed these console errors.
But….still no explosion.
Back to Feeling My Way Around in the Dark
I’ve reopened the original unmodified project so that I can look around for any discrepancies from it to mine that might give me a clue.
Finally during playtesting I noticed a difference.
In the original project, a C4 game object is created after placing the C4 on the car, so completing the Interactable_Place_C4_Zone.
Now, I just had to figure out how and why that happened.
And here we have another issue where the Detonator script was still referencing the old InteractableZone script.
So again, we create a reference to ZoneInteractions and copy paste the relevant code into the new subscriber method.
Also in the Detonator script, there was one more reference to the old InteractableZone script in the OnDisable() method which was fixed as shown above.
When running the game however, we get a new error.
The error referenced line 61 of the Detonator script with an “IndexOutOfRangeException”.
These are usually easy to track down errors because they are simply some input to reference the element of an array that does not exist.
For instance, if your array has 2 elements, their index numbers are 0 and 1.
If you attempt to read index number 2 or higher, you will get this IndexOutOfRange exception.
In our case though, what we notice is yet another reference to the old interactions script, InteractableZone which is not in use.
Thus any attempt to access an element from the disabled script will fail.
Renaming the variable array from the old script to the new script is the first step.
In the Rider code editor, bad variable references turn red, but Visual Studio will highlight these as well.
So now that we have replaced the old script references with the new, we can attempt to run our game once more.
Unfortunately, we get the same error for the same line.
If we examine our Detonator game object, detonator script component, it is clear that we should get an IndexOutOfRange error for this array since nothing is assigned here.
Looking at the original project, I can see what was intended to be assigned here for the functionality to work as intended.
We add these two assignments and fire up the game once again.
And I get the exact same error.
The mistake I made was to modify the existing Detonator game object that is childed to the Player game object.
This is apparently not the game object that is being used in this situation.
Once again we assign these 2 zones, but to a different Detonator game object.
Well, this article was much longer than I expected.
At least it was a…