[Release][Update 4: 12 Apr 2024] Replacement for Roblox's ContextActionService

After some internal debating, I have decided to release my rendition of a full replacement for Roblox’s rather limited ContextActionService. This improves upon Roblox’s own implementation in the following ways:

  • No limit to the number of actions that can be defined (Roblox limits to 7).
  • No guesswork as to where touch buttons are placed.
  • Binding an action can also define a touch button in the same function call.
  • Multiple actions with their associated touch buttons can be defined in one function call.
  • Although the parameters for binding actions vary slightly from Roblox’s implementation, I have made every effort to retain the function header definition of the callback functions. This means that there is NO code modifications needed for callbacks.
  • The callbacks are wrapped in pcall statements for error handling. If a callback has a problem, a warning is printed in the console along with a backtrace.

The entire module is contained in one script. Just place it where you keep your library scripts and require it as usual. There is quite a bit of documentation inside the script itself.

I don’t have much support for gamepads, but anything that’s an Enum.KeyCode is supported as is a number of mouse input functions.

Here’s a screenshot of my game (in development) using this:


Each circular button with a black icon and a white background is a button that was created by this module. As you can see on screen, there are 11 buttons. The two larger buttons with a red outline demonstrates the colored ring background which you will read about in the documentation that is included in the module.

A note about Actors:
Due to the nature of actors, any bindings defined within an actor are isolated to that actor. So functions like ContextActionService:getAllBindings() will only return the bindings that were made within the same LUA memory space. However, all instances of the module will use the same GUI in game.Players.PlayerGui. As such, with the enhancement of gamepad controls and their limited number of buttons, some facilities for communication between LUA memory spaces have been implemented. Primarily the ability to enable or disable actions using an index (See V2 update notes below.)




Files

And here’s the script:
[Version 1: Initial Release] ContextActionServiceV1.txt (38.8 KB)
[Version 2: 16 Nov 2023] ContextActionServiceV2.txt (52.9 KB)
[Version 3: 2 Dec 2023] ContextActionServiceV3.txt (51.2 KB)
[Version 4: 12 Apr 2024] ContextActionServiceV4.txt (52.0 KB)




[Update V4: 12 Apr 2024]

New Features

  • Added an Existing keyword for the touch button table. If this exists, then the existing GUI Instance is used instead for the touch button and the other touch button rendering parameters are ignored.

Bug Fixes

  • A long standing bug where if a character dies more than once, internal data isn’t cleared and upon the character reloading, a warning is thrown about a duplicate action index which aborts the multi-button bind function which in turn causes the user interface to be in an inconsistent state (UI action buttons not rendered or attached). The cause was that the event handlers for when the character dies wasn’t being reconnected to the new character properly after the character respawned. The character death event handlers are responsible for clearing all binding data. This has been corrected.

[Update V3: 2 Dec 2023]

  • An issue was identified which would cause touch inputs to fail with various errors. This has been corrected.
  • Removed duplicate input diagnostic code and made it into a single function which is called by the event handlers.

[Update V2: 16 Nov 2023]

  • The first version was basically for my own use so the code was sloppy in places, features half implemented, etc…
  • The code has been cleaned up. More documentation and comments were added for those looking to modify the script.
  • Removing a specific function from an action has been fully implemented although I haven’t really tested it yet (It was partially implemented before.). There are two functions for this. One will take an action name and the function reference. The other one will take a handle. A handle is a number that is tied to the function callback and is unique for every callback function that the module sees.
  • Updated and corrected the documentation.
    • Added the fact that bindAction returns a function handle and bindActionMultiple returns a table of function handles.
    • Fixed a number of typos and omissions.
  • Two new functions for enabling and disabling actions. Use the new optional parameter inputIndex for bindAction or Table.Index for bindActionMultiple. The parameter is a number and it must be unique across all instances and actors that require this module.
    • The module will create a new BindableEvent under the player that is used to signal to all of the other instances of the module that the state of the action that is referenced by the index number has changed.
    • This was the only viable method that I could come up with and keep it all in one script. If someone has a better method, I’m listening.
  • Gamepad Update: Turns out that Roblox has included Enum.KeyCode values for all gamepad functions, so no changes to accommodate a gamepad is needed.

If you look at the code in V1, you might have noticed two settings. One is named DEBUG. When it’s set to true, it will force the rendering of the touch buttons regardless if a touch screen is detected or not. The other one is named INPUT_TEST_MODE. Normally it’s false. But if it’s set to a number [0, 3], it will ignore all input and print the input object data to the console. The numbers are the input class or input device type. The are as follows:

0. Touch screen
1. Mouse
2. Keyboard
3. Gamepad

Depending on what feedback that I receive, I may add the other input types as well. In any case, please beware that this will print a huge amount of data as it’s unfiltered except for the input type.




Examples

Here’s some examples on how to use this:

Binding a single action:

-- Sets the hotkey to use.
local function setHotKey()
	local touchButton = {
		Position = contextActionService.setGridPosition(-1, 5, 1, 0.5);
		Size = contextActionService.setIconSize(2);
		Image = "rbxassetid://xxxxxxxxxx";
		BaseImage = false;
	}
	contextActionService.bindAction("showLeaderboard", actionShowLeaderboard,
		Enum.ContextActionPriority.Medium, touchButton, { Enum.KeyCode.Tab; })
end

Binding multiple actions:

-- Button Data
local buttonList = {
	buttonCrawl = {
		ActionName = "actionCrawl";
		Callback = actionCrawl;
		Position = contextActionService.setGridPosition(-3, 1, 1, 0.5);
		Size = contextActionService.setIconSize(2);
		Priority = Enum.ContextActionPriority.High;
		Image = "rbxassetid://xxxxxxxxxx";
		BaseImage = false;
		KeyBind = {
			Enum.KeyCode.LeftControl;
		};
	};
	buttonCrouch = {
		ActionName = "actionCrouch";
		Callback = actionCrouch;
		Position = contextActionService.setGridPosition(-2, 1, 1, 0.5);
		Size = contextActionService.setIconSize(2);
		Priority = Enum.ContextActionPriority.High;
		Image = "rbxassetid://xxxxxxxxxx";
		BaseImage = false;
		KeyBind = {
			Enum.KeyCode.C;
		};
	};
	buttonSprint = {
		ActionName = "actionSprint";
		Callback = actionSprint;
		Position = contextActionService.setGridPosition(-1, 1, 1, 0.5);
		Size = contextActionService.setIconSize(2);
		Priority = Enum.ContextActionPriority.High;
		Image = "rbxassetid://xxxxxxxxxx";
		BaseImage = false;
		KeyBind = {
			Enum.KeyCode.LeftShift;
		};
	};
}

contextActionService.bindActionMulti(buttonList)

As mentioned in the documentation, the names of each individual action in the table such as buttonCrawl, buttonCrouch, and buttonSprint do not matter. Name them anything that you want that makes sense to you. I prefer to name things that are descriptive of their purpose.

An example of using the background ring and coloring it.

inputButtons.fireWeapon = {
	fire1 = {
		ActionName = "FireWeapon1";
		Callback = fireWeaponTouch1;
		Position = contextActionService.setGridPosition(-2, -4, 1, 1);
		Size = contextActionService.setIconSize(4);
		Priority = Enum.ContextActionPriority.High;
		Image = "rbxassetid://xxxxxxxxxxxx";
		BaseImage = contextActionService.enums.CASImageCode.Background;
		BaseImageColor = BrickColor.new("Crimson");
		KeyBind = {
		};
	};
	fire2 = {
		ActionName = "FireWeapon2";
		Callback = fireWeaponTouch2;
		Position = contextActionService.setGridPosition(2, -4, 0, 1);
		Size = contextActionService.setIconSize(4);
		Priority = Enum.ContextActionPriority.High;
		Image = "rbxassetid://xxxxxxxxxxxx";
		BaseImage = contextActionService.enums.CASImageCode.Background;
		BaseImageColor = BrickColor.new("Crimson");
		KeyBind = {
		};
	};
}

As always, if you have questions, comments, or issues, reply here and I will get to back to you when I’m able.

20 Likes