ROBOT-SB is essentially a “one-tap” game – tap to change direction, trying to dodge and/or shoot the various obstacles. But it need not be just a “one-tap”, as there are a number of control schemes that could work with it.
I debated long and hard whether to allow alternate control schemes, primarily because I felt it might unfairly tip the leaderboards in favor of one control scheme or another.
Plus, there was also a bit of irrational desire to preserve some sort of “artistic purity” imagined to be present in the one-tap scheme – but that was easily shed.
I had mocked-up various control schemes while “hacking” the game with my son (aside: actually, we’ve hacked in all sorts of wacky stuff, like super-guns, invincibility, even reskinned it with some ‘borrowed’ Minecraft art) but never truly tested them.
The only way to truly settle the issue was to properly implement and test these alternate control schemes. And you know what? The choice of control scheme doesn’t have that much effect on scoring potential after all, so the argument regarding leaderboard preservation went up in smoke.
So why not just give the users what they want? For example, here’s the “stick” control (the only one that needs a visual):
I figured I’d talk about how I structure the code to make this “easy”, as maybe it’d be of use to someone else.
The way the code works is that each control scheme is its own class, following a common interface and referencing a common “control state” where it’s “output” is stored. So all that’s really necessary to implement the various schemes is to just create an instance of the correct class.
The control classes are kept in a table, keyed in a way that matches the persistent user-preference value. (note: all of the below is technically “pseudo-code”, but functionally describes actual game code)
1 2 3 4 5 6 7 |
local ControlSchemes = { onetap = require("game.controls.OneTap"), twotap = require("game.controls.TwoTap"), drag = require("game.controls.Drag"), swipe = require("game.controls.Swipe"), stick = require("game.controls.Stick"), } |
Then, when it comes to create an actual instance, it’s just:
1 2 |
-- creation of the actual controls (within game class) self.controls = ControlSchemes[options.controls]:new() |
Then, there’s a bit of wiring that needs to occur between the Player and the control scheme’s “state”. There are any number of ways to “connect” the two (event/message system, player-refs-state and “observes”, state-refs-player and “controls”, state passed to player as needed, etc), and I won’t get into the why of my decision, but Player get a reference to the control state and is wired up as so:
1 2 |
-- create a player with reference to controls (within game class) self.player = Player:new({controlState=self.controls.state}) |
Another advantage of the tabled control schemes, and the way the player references the controls, is that it would be easy to drop in a “game.controls.DemoAI” control scheme, for example, if I later decide to do the “game plays itself in demo mode underneath the main title screen” thingy.
Then just create a game instance that overrides the user-preference setting with a request for the “demoai” controls, and presto! instant demo mode. It doesn’t do this at present, and probably won’t ever, mainly because of low-resolution “clutter” concerns, though it’s something that’s worked successfully for me in other games.