[UPDATE] Switch Statements/Match Tables And Their Uses in ContextActionService

Hey all, this is a bit of an update on a previous post of mine:
Tables As Pseudo-Switch Statements Over Lengthy/Nested Elseif Statements

In which, I demonstrated a method of replacing if statements with match tables to replicate the functionality of switch statements in Lua(u).

It was something I sort of came up with on the spot and in a bit of a rush for demonstration purposes, and so it wound up having the exact opposite effect, particularly on performance than what was intended to be showcased.


What I sought to achieve before:

  • reduce the amount of checks made by threads that otherwise use lengthy elseifs
  • reduce memory writes by compiling match tables with a constant lookup time
  • easier readability & forward compatibility

Where it failed:

  • context exclusivity and near-total adherence to lambda architecture when constructing tables, resulting in large memory writes each time a thread used a switch statement
  • table keys limited to boolean matches (kind of a big ick, looking back)

I recently went back to the very same script I made before, and fiddled with it to see what could be done to not only better improve performance, but allow the tables to have a slightly wider range of options for implementation.


Here’s what I have in its entirety, in the form of a basic tank-style movement ControlScript (with comments to help explain what’s going on):

-- [[ControlScript]]
UserInputSVC = game:GetService('UserInputService')
ConActSVC = game:GetService('ContextActionService')
RunSVC = game:GetService('RunService')
TagSVC = game:GetService('CollectionService')

-- key to work with Default Case function
local defInputState = Enum.UserInputState['None' or 'Cancel' or 'Change']
-- "None", "Cancel", and "Change" InputStates are to be ignored for now,
-- as they have somewhat limited usage in ContextActionService.
-- functions can simply be unbound/rebound
-- when doing something like opening or closing a menu

local turnDir, Xdir, Zdir = 0, 0, 0
local moving = false

local player = game:GetService('Players').LocalPlayer

local char, root, hum -- to be assigned and erased as the character spawns/dies

-- On player death, erase all character-related variables
function forgetChar()
	char, root, hum = nil
end

-- Assign variables to character objects on spawn-in/death events
player.CharacterAdded:Connect(function(addedChar)
	hum = addedChar:FindFirstChildOfClass('Humanoid') or addedChar:WaitForChild('Humanoid')
	
	hum.AutoRotate = false
	
	char = addedChar
	root = addedChar:FindFirstChild('HumanoidRootPart') or addedChar:WaitForChild('HumanoidRootPart')
	
	hum.Died:Connect(forgetChar)
end)

-- store directions to be assigned based on
-- which action is referenced by ContextActionService
local walkDirection {
	Forward = 1;
	Back = -1;
}

local walkToArgs = {
	-- defInputState key associates with task.wait to "skip" running the walk function
	-- first key is a stand-in for the "default" method in a standard switch case
	[defInputState] = task.wait;
	
	-- the table's cases will be compared to the current button/key being pressed.
	-- functions associated with cases rely on which action is being performed
	[Enum.UserInputState.Begin] = function(action)
		moving = true
		
		while task.wait()  do
			if not moving then break end
			
			-- changes the forward/backward momentum of the character
			-- based on which direction they are facing
			-- and which action is being performed
			local rotation, modifier = (0.0174532925*root.Orientation.Y), walkDirection[action]
			
			Xdir = math.sin(rotation) * modifier
			Zdir = math.cos(rotation) * modifier
		end
		
	end;
	[Enum.UserInputState.End] = function(action)
		moving = false
		Xdir = 0
		Zdir = 0
	end;
}

-- for turning, the direction in which to turn is based on
-- whether the turn action is starting or ending,
-- as well as which action is being referenced
local turnBeginAngle = {
	Left = 1;
	Right = -1;
}

local turnEndAngle = {
	Left = -1;
	Right = 1;
}

-- next match table follows the same format as above
-- first key is the default, the rest that follows are the case methods
local turnToArgs = {
	-- don't do anything when not pressing/releasing a button
	[defInputState] = task.wait;

	-- when pressing or releasing a button,
	-- add to/subtract from the turn direction based on inputState and action
	[Enum.UserInputState.Begin] = function(action)
		turnDir += turnBeginAngle[action]
	end;
	[Enum.UserInputState.End] = function(action)
		turnDir += turnEndAngle[action]
	end;
}

local quickTurnArgs = {
	[defInputState] = task.wait;

	-- if the player presses the QuickTurn button down, turn the character around
	-- if the button is released, reset movement direction (not necessary but grounding)
	[Enum.UserInputState.Begin] = function()
		root.CFrame *= CFrame.Angles(0, math.rad( 180 ), 0)
	end;
	[Enum.UserInputState.End] = function()
		Xdir = 0
		Zdir = 0
	end;
}

-- functions don't need to iterate through multiple elseifs to reach the intended result
-- simply make a reference to the stored key and call its associated function
function Walk(action, inputState)
	walkToArgs[inputState](action)
end

function Turn(action, inputState)
	turnToArgs[inputState](action)
end

function QuickTurn(action, inputState)
	quickTurnArgs[inputState]()
	-- no variables required by QuickTurn function, can run independently of CAS if desired
end

function UpdateMovement()
	if not hum or hum.Health <= 0 then return end
	
	root.CFrame *= CFrame.Angles(0, math.rad( turnDir * 2 ), 0)
	hum:Move(Vector3.new(Xdir, 0, Zdir), false)
end

RunSVC:BindToRenderStep('Control', Enum.RenderPriority.Input.Value, UpdateMovement)

ConActSVC:BindAction('Left', Turn, false, Enum.KeyCode.A)
ConActSVC:BindAction('Right', Turn, false, Enum.KeyCode.D)
ConActSVC:BindAction('Forward', Walk, false, Enum.KeyCode.W)
ConActSVC:BindAction('Back', Walk, false, Enum.KeyCode.S)
ConActSVC:BindAction('QuickTurn', QuickTurn, false, Enum.KeyCode.LeftControl)

And of course, a short video to show that it indeed works:


You could add different control methods, such as touch/controller layouts at your discretion, and the runtime of it all should be relatively stable, almost no matter how much larger each match table gets. That’s where the benefit of using a switch statement comes in.

It may be a bit slower than a two-to-three argument if statement, but the runtime gets just a little longer for each option down the line the game reads through/skips over when comparing arguments.

If each Switch Statement above was instead an Elseif, overhead would vary wildly depending on where in the statement each button, action, move direction, etc. are checked, as well as how long that elseif chain is.

Really to summarize, the key difference between an elseif and a swtich statement is this:

  • Elseif - “Is the argument this? No? Then is it that? No? Well then how about this? Yes? Is it also this? No? Well, let’s take a step back. How about…?” (cont.)
  • Switch - “Here’s option C from the table you told me to grab it from.”

If anybody has any thoughts or questions, feel free to share and ask, and I’ll do my best to reply to them eventually if I can.

Otherwise, I’d encourage you to play around with the concept and see what other crazy things you can do with this faux Switch Statement architecture, in or out of ContextActionService.

8 Likes