Dev Blog for Controller Support in RagBall
Features:
- Controller support using Unity New Input System
- Multiple controllers connected to the game at once
- Multiple controller types supported with no additional code (Xbox, PlayStation, Generic Gamepads, etc.)
Background:
- RagBall, as of the end of Workshop I, had a very hacky solution to managing controller input. The previous method of listening for input involved a deeply nested if statement structure of inputs to check for on each frame, and then performing a wildly large number of calculations depending on certain game conditions. The default Unity Input System has no support for reading input from multiple controllers separately, so a third party library was brought in to handle this use case. A limitation of this library was that it only worked on Windows machines with DirectX installed, and it only supported wired Xbox 360 controllers.
- This system was completely non-scalable, because all input was handled in the single player manager script that became so infamous. Furthermore, the system was prone to bugs, only supported one controller type, was not portable, and overall was a non-optimal solution.
- For Workshop II, one of the primary goals was refactoring the existing codebase to be more scalable, and at the top of that priority list was fixing the input situation.
Process:
- We needed to identify our options for handling input in a more elegant way. One focus was on controller support. We needed a method of supporting various controller types, due to the nature of RagBall and its multiplayer nature, controllers are typically limited amongst players and devs. We also needed the solution to be portable, as we couldn't afford to deal woth bugs that related to machines that were missing the relevant libraries to function properly. We identified that the optimal solution would be to port the input management to the new Unity Input System.
- An immediate problem we identified when attempting to switch was that the new Input System was still in pre-release. As such, we were on the bleeding edge of development for this library (more on that later). The benefits it brought were too good to ignore, however. The new system was capable of reading input from any XInput or DInput device natively (allowing for greater controller support), and was event driven. This means that, where the old system had to check every frame if an input was performed, the new system could simply throw an event when an input was performed, and skip the polling process entirely. This meant that we could encapsulate all controller event polling into a single class, that any class could then read from to determine if an action was performed.
- We gained additional benefits, as well, such as the ability to read more complex event types, such as button taps, holds, trigger sensitivity, analog stick sensitivity, and more. We also were able to take advantage of the Action Map feature, wherein a controller can be bound to multiple different "maps" depending on the context of the scene. Ergo, we could have a map entirely dedicated to controlling the menus, and a separate map for use in the gameplay. This dramatically reduced complexity in the code for handling inputs.
- This system was not ready to go out the box, though. Frank and I spent two nights straight researching the functionality of this library, and how we could take advantage of it in RagBall. Like I said earlier, the new Input System is in prerelease. As such, one of the first problems we encountered was that the new input system ALSO did not support multiple controllers properly. This limitation meant that any controller that made an input would fire an event to all listeners, regardless of which controller the listener was supposed to be following.
- We identified that the problem laid in the event stream of the New Input System. We were able to diagnose this by scouring the Unity Forums for posts by the current dev team working on the New Input System, who claimed that they had not yet perfected the multiple controller support yet (That was an understatement). When I said we were on the bleeding edge, I meant that we were reading forum discussions with the devs that were ongoing regarding their bugfixes and continued development. As such, we seriously considered dropping support for the new Input System, and scouring the web for another solution.
- But, we had one more idea before then. The new Input System is open source, and as such, we dug through the source code of the current prerelease version to diagnose the bug that was causing our troubles with multiple controllers. We determined that there was a way to separate the streams of the controllers into a single stream for each controller, and so we created a public class to house the controllers, which contained an array of each controller (a class that we also created that housed a public interface to listen for input events), and encapsulated the logic that would have added unnecessary complexity to the rest of the dev team. Upon testing our code, we managed to make it work!
- The final task we had involving the input system was to allow for a method of switching between action maps via code, so that we could on the fly switch between the menu and the gameplay maps. We created a global event system that lived in our preload scene that would listen for signals emitted to switch the action map on the fly, meaning that switching maps was as simple as a one liner, and we prevented some nasty edge cases where the maps would break otherwise (Believe it or not, we diagnosed another bug with the Input System where switching action maps was only possible on Start and on Destroy, meaning this event system was the best remedy for these issues we could have come up with).
Benefits:
- Upon the completion of these tasks, we were met with a ton of benefits, and have honestly had no issues with input at all since the completion of this task. Since the controllers could be abstracted in such a way that no one entity actually had to manually manage controls, and that any entity could listen for inputs on any controller, it made it trivial to create things such as cursors for menus and change control schemes during development. The work done on this input system was amongst my proudest work on this project, alongside the work completed on the menu system and the preload scene.
Bonus Round: Preload Scene
- The other thing that was a great idea for the development of RagBall was to create a preload scene. The idea here is that you create a scene that loads before any other scene, and initializes things that should exist throughout the entire duration of the game. Things here included our controller system, music and sfx systems, pause menu, and events system. With this, we could avoid having inconsistencies between scenes that need the same objects between them (like controllers). Additionally, another great benefit of this was that we had a method of passing data between scenes easily. We had a parent class that lived in the preload scene called Game, which housed access to all of the things that lived in the preload scene. During development, if any developer wanted to play a sound, read input from a device, listen for or trigger an event, it could all be done with this Game class. This allowed for a great separation of responsibility, and allowed for us to decouple things that were very tightly coupled in the past, such as controllers to Ragdolls, audio to scenes, and so forth. And since the game class was static and always loaded in the scene, it was programatically simple to access global things such as the ones listed.
Conclusion:
- I try to keep all the code I write as clean and maintainable as possible. No shortcuts means that future development can go smoother, and that was the goal this quarter, to allow for smooth development to add new features, and to be able to expand and grow dynamically. I don't believe in "quick and dirty", and the consequences of such an approach are what led RagBall to the state it was in by the end of Workshop I. By the end of Workshop II, however, I can say that I am very happy with the quality of code that we have produced. Without the refactoring effort that I pushed for in the beginning, this game would not have gotten much farther at all. We wouldn't have fixed as many bugs or implemented as many new features without this crucial step, and I think it paid off in spades. Furthermore, the effects of the clean code reach even beyond the scope of this class. I have learned so much about code architecture in games that the systems I've worked on in RagBall can be applied to really any kind of game, and I'm sure a lot of the design decisions we made in regards to code will be reused by us in the future. It's even expanded beyond our team, and our controller system and preload scene designs have ended up in Cowduction's code base, which is telling for the versatility of the code written for RagBall.