Jump to content

More Control On Controllers



The Leadwerks API has standard Boolean functions that detect when the end user has pressed a key. While this is very simple and easy to understand, the issue comes when you wish to support binding of actions. Instead calling functions when a certain key was pressed or held, a better way to detect key events is to assign a key to an action. (e.g: Is the Jump key pressed). In Luawerks, I've written an action script in which returns the window call if the key defined in the user's settings is hit. The downsides is that you had to do this for every action in your game and that this was a system written for Luawerks so it wasn't very portable for non-lua/Leadwerks projects.

function Action:MoveForward()
	if self.suspend==true then return false end
	local key_forward = GetKeyInt(System:GetProperty("key_forward","w"))
	return window:KeyDown(key_forward)

For those who don't know, SDL is an Open Source library that comes in handy for things like Window management, input, and such. So over the weekend, I've decided to sit down and create my own Input System in which would be allow for action bindings, portable for any type of project while supporting Game Pads outside of Steam.

The first step was to manually poll all inputs and treat them all the same. For the mouse and keyboard, this was just seeing if a key was hit, and registering that event to a map.

namespace RInput_KM
	void SimulateButton(const Sint32& pKey, bool bDown);
	const float ButtonDown(const Sint32& pKey);
	void ShowMouse();
	void HideMouse();
	void SetMousePosition(const int x, const int y, bool bGlobal = false);
	void ModMousePosition(const int x, const int y, bool bGlobal = false);
	void UpdateMousePosition();
	void SimulateMouse(const int x, const int y);
	const int GetMouseX();
	const int GetMouseY();
	void SetMouseWheelPosition(const int y);
	const bool OnMouseWheelUp();
	const bool OnMouseWheelDown();

	// Returns the long to a character.
	const char* GetButtonName(const Sint32& pButton);

	// Returns the character to a long
	const Sint32 GetButtonIndex(const char* pButton);

	void Enable();
	void Disable();
	void FlushKeyboard();
	void FlushMouse();
	void Flush();	

When it comes to buttons, it doesn't matter if the mouse button was pressed or a key stroke. What matters is that a button on the keyboard and mouse combo was pressed. I treat the keyboard and mouse as one controller. You can also "Turn off" the mouse and keyboard if you want.

namespace RInput_GamePad
	typedef enum
	} GamePadIndex;

	typedef struct
		SDL_GameController* controller;
		const char* pszDeviceName;
		bool bEnabled;
		std::map<Uint8, bool> mControllerButtons;

	} gamepad_t;

	void Connect(const Sint32& pWhich);
	void Disconnect(const Sint32& pWhich);

	gamepad_t GetDeviceFromPort(const GamePadIndex& pPort);

	// Digital input:
	void SimulateButton(const Sint32& pWhich, const Uint8& pButton, bool bDown);

	const float ButtonDown(const Uint8& pButton, const GamePadIndex& iIndex = ENUM_GAMEPAD_ONE);

	const char* GetButtonName(const Uint8& pButton);
	const Uint8 GetButtonIndex(const char* pButton);

	// Analog input:
	void UpdateAxisMotions(const Sint32& pWhich, const Uint32& pAxis);
	const Sint16 GetAxisValue(const Sint32& pWhich, const Uint32& pAxis, bool bFlip = false);
	const float GetAxisFloat(const Sint32& pWhich, const Uint32& pAxis, bool bFlip = false);

	void Flush(const Sint32& pWhich);
	void FlushAll();

Next was the game pads. which were a bit more challenging as you need to consider multiple game pads and the valves of analog inputs.

If you were paying attention, You would notice that the ButtonDown functions for both the keyboard+mouse and the game pad are returning floats. While it may be limiting for certain applications, I've created "fake buttons" for events for when the trigger is pressed, or if the left stick is to the left. All input returns a float ranging from 0.0 to 1.0. For digital inputs like buttons, this will be a 1 or 0, but for analog the value depending the range from the resting point to the max will called. So if the left stick is all the way to the left, you'd get 1; half way 0.5. This is much better then dealing with direct Uint32 values.

Last was to merge both controllers into one entry point in which we use in our app to register our actions, and check to see if they are being called.

namespace RInput
	typedef enum
	} Controllers_t;

	void Init(SDL_Window* pWindow);
	void InitSDL(const void *data);

	void SetActiveDevice(const Controllers_t& pDevice);
	Controllers_t GetActiveDevice();
	const char* GetActiveDeviceAsString();
	const char* GetGamePadDeviceAsString(const int pPort);

	const Sint8 GetGamePadCount();

	void TestEvents(const SDL_Event& pEvent);
	void PollEvents(); // <- Use this function if you're not using SDL event polling.

	void Flush(const Controllers_t& pController);
	void FlushAll();

	typedef struct
		Sint32 key;
		Uint8 button;
		bool bDown;
		bool bHit;

	} action_t;

	const float GetActionInput(action_t& pButton);
	void RegisterAction(const std::string& pActionName, Sint32 iKey, Uint8 iButton, bool bConistant);
	void ModifyAction(const std::string& pActionName, Sint32 iKey, Uint8 iButton);
	action_t& GetAction(const std::string& pActionName);

	bool LoadActionsFromFile(const char* pszPath);

	void UpdateGamePadStickAsMouse(const Sint32& pWhich, const Sint8& pAxis);

Actions can be bind to a with key and a button. bDown is used to check if the action is being held down while bHit is a flag to check if this is something that should be called once per pressed (Like a key to pick up a box or something.) One top of all this, Actions can have their key/button bindings changed via an XML file. Here's an example how to use this with Leadwerks.

#include "Leadwerks.h"
using namespace Leadwerks;

#include "rinput/rinput.h"

int main()
	Leadwerks::Window* window = Window::Create();

	Leadwerks::Context* context = Context::Create(window);

	World* world = World::Create();
	Camera* camera = Camera::Create();
	camera->SetRotation(35, 0, 0);
	camera->Move(0, 0, -6);
	Light* light = DirectionalLight::Create();
	light->SetRotation(35, 35, 0);

	Model* model = Model::Box();
	model->SetColor(1.0, 0.0, 0.0);
	model->SetPosition(0, 0, 0);
	RInput::RegisterAction("moveforward", KEYBOARD_W, GAMEPAD_BUTTON_LSTICK_UP, true);
	RInput::RegisterAction("movebackward", KEYBOARD_S, GAMEPAD_BUTTON_LSTICK_DOWN, true);
	RInput::RegisterAction("moveleft", KEYBOARD_A, GAMEPAD_BUTTON_LSTICK_LEFT, true);
	RInput::RegisterAction("moveright", KEYBOARD_D, GAMEPAD_BUTTON_LSTICK_RIGHT, true);
	RInput::RegisterAction("jump", KEYBOARD_SPACE, GAMEPAD_BUTTON_A, false);

	while (window->Closed() == false)

		float x = 0;
		float z = 0;

		float u = RInput::GetActionInput(RInput::GetAction("moveforward"));
		float d = RInput::GetActionInput(RInput::GetAction("movebackward"));
		float l = RInput::GetActionInput(RInput::GetAction("moveleft"));
		float r = RInput::GetActionInput(RInput::GetAction("moveright"));

		if (u != 0)
			z += 0.05 + (u /10);

		if (d != 0)
			z -= 0.05 + (d / 10);

		if (l != 0)
			x -= 0.05 + (l / 10);

		if (r != 0)
			x += 0.05 + (r / 10);

		model->Move(x, 0, z);
		x = 0;
		z = 0;
		context->DrawStats(0, 0, false);

	return 0;


The Result:


There are many things I didn't add for the sake of time or I wasn't sure how to implement like supporting action for multiple controllers on the front end. But this does what I was set out to do, and although I haven't tried, I'm confident that with little to no work, this code will work on macOS and Linux. As long as there is SDL2 on the platform, this will work.

Now no matter if the project is Leadwerks, Turbo or just an SDL2 project, I now have a portable input system with controller support which you can find here.

  • Like 2


Recommended Comments

There are no comments to display.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 0
      I'm back from I/ITSEC. This conference is basically like the military's version of GDC. VR applications built with Leadwerks took up about half of Northrop Grumman's booth. There were many interesting discussions about new technology and I received a very warm reception. I feel very positive about our new technology going forward.

      I am currently reworking the text field widget script to work with our persistent 2D objects. This is long and boring but needs to be done. Not much else to say right now.
    • By Josh in Josh's Dev Blog 4
      Here are some screenshots showing more complex interface items scaled at different resolutions. First, here is the interface at 100% scaling:

      And here is the same interface at the same screen resolution, with the DPI scaling turned up to 150%:

      The code to control this is sort of complex, and I don't care. GUI resolution independence is a complicated thing, so the goal should be to create a system that does what it is supposed to do reliably, not to make complicated things simpler at the expense of functionality.
      function widget:Draw(x,y,width,height) local scale = self.gui:GetScale() self.primitives[1].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) self.primitives[2].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) --Tabs local n local tabpos = 0 for n = 1, #self.items do local tw = self:TabWidth(n) * scale if n * 3 > #self.primitives - 2 then self:AddRect(iVec2(tabpos,0), iVec2(tw, self.tabsize.y * scale), self.bordercolor, false, self.itemcornerradius * scale) self:AddRect(iVec2(tabpos+1,1), iVec2(tw, self.tabsize.y * scale) - iVec2(2 * scale,-1 * scale), self.backgroundcolor, false, self.itemcornerradius * scale) self:AddTextRect(self.items[n].text, iVec2(tabpos,0), iVec2(tw, self.tabsize.y*scale), self.textcolor, TEXT_CENTER + TEXT_MIDDLE) end if self:SelectedItem() == n then self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos, 0) self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) + iVec2(0,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 2].color = self.selectedtabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,-1) self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,0) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,0) else self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) self.primitives[2 + (n - 1) * 3 + 2].color = self.tabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,2) if n == self.hovereditem then self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor else self.primitives[2 + (n - 1) * 3 + 3].color = self.textcolor end self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 3) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,2) end self.primitives[2 + (n - 1) * 3 + 3].text = self.items[n].text tabpos = tabpos + tw - 2 end end  
    • By 💎Yue💎 in Dev Log 5
      The prototype of a four-wheeled vehicle is completed, where the third person player can get on and off the vehicle by pressing the E key.  To move the vehicle either forward or backward, is done with the keys W, and the key S, to brake with the space key.  And the principle is the same as when driving the character, a third person camera goes behind the car orbiting 360 degrees.

      I don't think the vehicle is that bad, but I'm absolutely sure it can be improved.  The idea is that this explorer works with batteries, which eventually run out during the night when there is no sunlight.
      Translated with www.DeepL.com/Translator
      Mechanics of the game.
      I'm going to focus on the mechanics of the game, establish starting point (Landing area), after the orbiter accident on Mars where all your companions died, now, to survive, you will have to repair your suit, oxygen runs out, good luck.  This involves replacing the oxygen condenser that is failing and the suit is stuck.

      On the ground and performance.
      The rocks, the terrain and the vehicle kill the SPF, but there is a solution, and everything is related to the chassis of the vehicle. That is to say that if I put a simple collision bucket for the vehicle, the yield recovers, something that does not happen if I put a collider of precise calculation for the car. This has the advantage of better performance but is not very accurate, especially when the car crashes with an object in front, because the horn of the car has no collision. And the solution to this, is to put a sliding joint, as was done with the area in which the player climbs the car and descends from it.

      On the rocks, I am trying to make them with the slightest polygons and the most distant from each other. 
      Obviously on Mars I can not create canyons, high mountains, is because the terrain does not produce shadows on itself, that's why the terrain tries to be as flat as possible, simulating a desert with dunes. 

      That's all for now.
  • Create New...