Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Proper Input Implementation

$
0
0

Theory


A input system in a simulation becomes difficult to visualize. Even with tons of informations around the web about input handling in a game, the correct way it's rare to found.

There are tons of low-level informations that should be considered in a input system article, but here I'll explain it more directly—since it's not a book. You'll be able to apply to your project having knowledge of simple resources. 

The intuition says that input events should be requested each time before we update the simulation, and handled at game-logical side via callbacks or virtual functions, making the player entity to jump or shoot in a enemy. Requesting inputs in a certain time it's also known as polling. Such thing it's something that we can't avoid—since we poll all inputs that ocurred since the last request in a certain time, but there are a lot of drawbacks doing it in such brute-force methodology. Here we use the word polling as meaning requesting input events at any time—polling input events at any time

Not using Google Tradutor here, but what matters it's that you understand the article.

What is the problem with polling inputs every frame? The problem is that we don't know how fast our game it's being updated, and you may not want to update faster than the real time; updating the game faster than the real time (or slower) means that you're advancing the game simulation in a variant way; the game will run slower in a computer when it could be running faster or vice-versa, and its entities behaviours won't be updated at fixed increments; the concept of fixed time-step it's mandadory to apply in modern games (and very unified too), and its implementation details are out of topic. Here, I'll assume that you know what a fixed-time step simulation is. A fixed-time step update will be referred here as one logical update (a game logical update).

A logical update updates our current game time by a fixed-time step—which it's generally 16.6ms or 33.3ms. If you know what a logical update in respect to the real time elapsed is, you know that we can update our game by N times each frame, where N approaches to the current elapsed time—the game logic time should be very close to the current time but updated in fixed steps, meaning that we've updated the game as faster that we could (we did all possible logical updates up to the current time).

The basic loop of a fixed-time step game simulation follows:

UINT64 ui64CurTime = m_tRenderTime.Update();
while ( ui64CurTime - m_tLogicTime.CurTime() > FIXED_TIME_STEP ) {
        m_tLogicTime.UpdateBy( FIXED_TIME_STEP );
        //Update the logical-side of the game.
}

where m_tRenderTime.Update() updates the render timer by the real elapsed time converted to microseconds (we want maximum precision for time accumulation), and m_tLogicTime.UpdateBy( FIXED_TIME_STEP ) updates the game by FIXED_TIME_STEP microseconds.

Returning to inputs... what happens if we press a button at any time in the game, poll all inputs in the beginning of a frame (before the loop start) and we release that button during the game logical update? The answer it's that if we update N times, and that button changes its state in between, the button will be seen as if got pressed during the entire frame. This is not a problem if you're updating small steps because you'll transit to the next frame faster, but if the current time it's considerable larger than the time step, you can get into problems by just knowing the state of the button on the start of that frame. To avoid that issue, you may want to time-stamp the input events when they ocurred to measure it's duration, and synchronize it with the simulation.

We saw that polling inputs any time and time-stamping it it's a damn good information to keep; it's not only necessary but it's mandatory—to eat the right amount of inputs at any time. With all these informations in hands, our input system should be able to request inputs anywhere (can be a frame update, a logical update, etc.) and process that somewhere later. You may have noticed that it's basically buffering inputs, which it's a good idea because if we can keep time-stamped inputs at any time and process that means that we can process all inputs before takes place at the logical updates, fire input actions, measure it's duration, at the same time being synchronized with the game logical time.

So, what's the ideal solution to keep our input system synchronized with our game simulation? The answer it's to consume inputs that ocurred up to the current game time each logical update. Example:

Current time = 1000ms.

Fixed time-step = 100ms.

Total game updates = 1000ms/100ms = 10.

Game time = 0.

Input Buffer:

X-down at 700ms;

X-up at 850ms;

Y-down at 860ms;

Y-up at 900ms;


1st logical update eats 100ms of input. No inputs until that, go to the next logical update(s).

...

7st logical update eats 100ms of input. Because the logical game time was updated 6 times by 100ms, our game time is: 600ms, but there are no inputs up to that time, so, we continue with the remaining updates...

8st update. Game time = 800ms. X-down along with its time-stamp can be eated, so we eat it. The current duration of X it is the current game time subtracted by the time key got pressed, that is, the duration of button X = 800ms - 700ms = 100ms. Now, the game it's able to check if a button it's being held for certain amount of time, which it's a good thing for every type of game. Also, we know that (in the example) we can fire an input action here, because it's the first time that we've pressed the X button (in the example, of course, because there was no X-down/up before). Since we get all inputs in this logical update, we can re-map X to some game-side input action.

9st update. Game time = 900ms. X-up, and Y-down along with its time-stamps can be eated, so we eat those. Wait, the X button was released, that means that the total duration of X it's the current game time subtracted by the time key got pressed, that is, the duration of the button X = 900ms - 700ms = 300ms, since we got an key-hold termination event we may want to log that. Y was pressed, we repeat the same thing we did to X in the last update, but now for Y.

and finally...

10st update. Game time = 1000ms. We repeat the same thing we did to X in the last update for Y and we're done.

Practice


If you're running Windows®, you may have noticed that you can poll pre-processed or raw input events using the message queue. It's mandatory to keep input polling in the same thread that the window was created, but it's not mandatory to keep our game simulation running in another. In order to try to increase our input frequency, what we can do it's to let our pre-processed input system polling input events in the main thread, and run our game simulation and rendering in another thread(s). Since only the game affects what will be rendered, we don't need synchronization.

I hope what each class do stay clear as your read.

Example:

INT CWindow::ReceiveWindowMsgs() {
	::SetThreadPriority( ::GetCurrentThread(), THREAD_PRIORITY_HIGHEST ); //Just an example.
	MSG mMsg;
	m_bReceiveWindowMsgs = true;
	while ( m_bReceiveWindowMsgs ) {
		::WaitMessage(); //Yields control to other threads when a thread has no other messages in its message queue.
		while ( ::PeekMessage( &mMsg, m_hWnd, 0U, 0U, PM_REMOVE ) ) {
			::DispatchMessage( &mMsg );
		}
	}
	return static_cast(mMsg.wParam);
}

To request up-to current game time in the game thread, we must synchronize our thread-safe input buffer timer with all game timers, so no time it's more advanced than other, and we can measure it's correct intervals (time-stamps for our case) at any time.

//Before the game start. InputBuffer it's every possible type of device. We synchronize every timer.
void CEngine:Init() {
        //(...)
        m_pwWindow->InputBuffer().m_tTime = m_pgGame->m_tRenderTime = m_pgGame->m_tLogicTime;
}

Here, we'll use the keyboard as the input system that I've described, but it can be translated to every another type of device if you want (GamePad, Touch, etc). Let's call our thread-safe input buffer as keyboard buffer.

When a key gets pressed (or released), we should process that window message in the window procedure. That can be done as following:

class CKeyboardBuffer {
public :
	enum KB_KEY_EVENTS {
		KE_KEYUP,
		KE_KEYDOWN
	};

	enum KB_KEY_TOTALS {
		KB_TOTAL_KEYS = 256UL
	};

	void CKeyboardBuffer::OnKeyDown(unsigned int _ui32Key) {
	   CLocker lLocker(m_csCritic);
	   m_tTime.Update();
	   KB_KEY_EVENT keEvent;
	   keEvent.keEvent = KE_KEYDOWN;
	   keEvent.ui64Time = m_tTime.CurMicros();
	   m_keKeyEvents[_ui32Key].push_back(keEvent);
        }

        void CKeyboardBuffer::OnKeyUp(unsigned int _ui32Key) {
	   CLocker lLocker(m_csCritic);
	   m_tTime.Update();
	   KB_KEY_EVENT keEvent;
	   keEvent.keEvent = KE_KEYUP;
	   keEvent.ui64Time = m_tTime.CurMicros();
	   m_keKeyEvents[_ui32Key].push_back(keEvent);
        }
        //(...)
protected :
	struct KB_KEY_EVENT {
		KB_KEY_EVENTS keEvent;
		unsigned long long ui64Time;
	};

	CCriticalSection m_csCritic;
	CTime m_tTime;
	std::vector m_keKeyEvents[KB_TOTAL_KEYS];
};

LRESULT CALLBACK CWindow::WindowProc(HWND _hWnd, UINT _uMsg, WPARAM _wParam, LPARAM _lParam) {
	switch (_uMsg) {
	case WM_KEYDOWN: {
		m_kbKeyboardBuffer.OnKeyDown(_wParam);
		break;
	}
	case WM_KEYUP: {
		m_kbKeyboardBuffer.OnKeyUp(_wParam);
		break;
	}
    //(...)
}

Now we have time-stamped events. The thread that listens to inputs it's aways running on the background, so it cannot interfere directly in our simulation.

We have a buffer that holds keyboard information, what we need now it's to process that in our game logical update. We saw that the responsability of the keyboard buffer was to buffer inputs. What we want know it's a way of using the keyboard in the game-side, requesting information such "for how long the key was pressed?", "what is the current duration of the key?", etc. Instead of logging inputs (which it's the correct way, and trivial too!), we keep it simple here and use the keyboard buffer to update our keyboard, which has all methods for our simple keyboard interface that can be seen by the game (the game-state, of course).

class CKeyboard {
        friend class CKeyboardBuffer;
public :
	CKeyboard();

	inline bool KeyIsDown(unsigned int _ui32Key) const {
               return m_kiCurKeys[_ui32Key].bDown;
        }
        unsigned long long CurKeyDuration(unsigned int _ui32Key) const {
               return m_kiCurKeys[_ui32Key].ui64Duration;
        }
        //(...)
protected :
	struct KB_KEY_INFO {
		/* The key is down.*/
		bool bDown;

		/* The time the key was pressed. This is needed to calculate its duration. */
		unsigned long long ui64TimePressed;

		/* This should be logged but it's here just for simplicity. */
		unsigned long long ui64Duration;
	};

	KB_KEY_INFO m_kiCurKeys[CKeyboardBuffer::KB_TOTAL_KEYS];
	KB_KEY_INFO m_kiLastKeys[CKeyboardBuffer::KB_TOTAL_KEYS];
};

The keyboard it's now able to be used as our final keyboard interface on the game-state, but we still need to transfer the data coming from the keyboard buffer. We will give our game an instance of the CKeyboardBuffer. So, each logical update we request all keyboard events up to the current game logical time from the thread-safe window-side keyboard buffer, and transfer that to our game-side keyboard buffer, then we update the game-side keyboard that will be used by the game. We'll implement two functions in our keyboard buffer. One that transfer thread-safe inputs and other that just update a keyboard with it's current keyboard events.

void CKeyboardBuffer::UpdateKeyboardBuffer(CKeyboardBuffer& _kbOut, unsigned long long _ui64MaxTimeStamp) {
	CLocker lLocker(m_csCritic); //Enter in our critical section.

	for (unsigned int I = KB_TOTAL_KEYS; I--;) {
		std::vector& vKeyEvents = m_keKeyEvents[I];

		for (std::vector::iterator J = vKeyEvents.begin(); J != vKeyEvents.end();) {
			const KB_KEY_EVENT& keEvent = *J;
			if (keEvent.ui64Time < _ui64MaxTimeStamp) {
				_kbOut.m_keKeyEvents[I].push_back(keEvent);
				J = vKeyEvents.erase(J); //Eat key event. This is not optimized.
			}
			else {
				++J;
			}
		}
	}
} //Leave our critical section.
void CKeyboardBuffer::UpdateKeyboard(CKeyboard& _kKeyboard, unsigned long long _ui64CurTime) {
	for (unsigned int I = KB_TOTAL_KEYS; I--;) {
		CKeyboard::KB_KEY_INFO& kiCurKeyInfo = _kKeyboard.m_kiCurKeys[I];
		CKeyboard::KB_KEY_INFO& kiLastKeyInfo = _kKeyboard.m_kiLastKeys[I];

		std::vector& vKeyEvents = m_keKeyEvents[I];

		for (std::vector::iterator J = vKeyEvents.begin(); J != vKeyEvents.end(); ++J) {
			const KB_KEY_EVENT& keEvent = *J;

			if ( keEvent.keEvent == KE_KEYDOWN ) {
				if ( kiLastKeyInfo.bDown ) {
				}
				else {
					//The time that the key was pressed.
					kiCurKeyInfo.bDown = true;
					kiCurKeyInfo.ui64TimePressed = keEvent.ui64Time;
				}
			}
			else {
				//Calculate the total duration of the key event.
				kiCurKeyInfo.bDown = false;
				kiCurKeyInfo.ui64Duration = keEvent.ui64Time - kiCurKeyInfo.ui64TimePressed;
			}

			kiLastKeyInfo.bDown = kiCurKeyInfo.bDown;
			kiLastKeyInfo.ui64TimePressed = kiCurKeyInfo.ui64TimePressed;
			kiLastKeyInfo.ui64Duration = kiCurKeyInfo.ui64Duration;
		}

		if ( kiCurKeyInfo.bDown ) {
			//The key it's being held. Update its duration.
			kiCurKeyInfo.ui64Duration = _ui64CurTime - kiCurKeyInfo.ui64TimePressed;
		}

		//Clear the buffer for the next request.
		vKeyEvents.swap(std::vector());
	}
}

Now we're able to request up-to-time inputs, and we can use that in a game logical update. Example:

bool CGame::Tick() {
	m_tRenderTime.Update(); //Update by the rela elapsed time.
	UINT64 ui64CurMicros = m_tRenderTime.CurMicros();
	while (ui64CurMicros - m_tLogicTime.CurTime() > FIXED_TIME_STEP) {
		m_tLogicTime.UpdateBy(FIXED_TIME_STEP);
		UINT64 ui64CurGameTime = m_tLogicTime.CurTime();
		m_pkbKeyboardBuffer->UpdateKeyboardBuffer( m_kbKeyboardBuffer, ui64CurGameTime ); //The window keyboard buffer pointer.
		m_kbKeyboardBuffer.UpdateKeyboard(m_kKeyboard, ui64CurGameTime); //Our non thread-safe game-side buffer will update our keyboard with its key events.
		
                UpdateGameState();//We can use m_kKeyboard now at any time in our game-state.
	}
	return true;
}

What we have done here was dividing our input system in small pieces synchronizing it with our logical game simulation. After you got all informations you can start re-mapping those and logging, etc—what matters is that it's synchronized with the logical game time and the game it's able to interface with that without losing input information.

It's not optimized because was not my intention to do any production code here, but some of the articles never get into implementation because it depends how many devices you have or for some other reason.

Send me a message if you have any question and I'll answer as possible I can with or without code, but try to visualize the solution by yourself for a moment.

Cheers,
Irlan.

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>