Threaded Mouse Library for Games

Share your advanced PureBasic knowledge/code with the community.
Axeman
User
User
Posts: 89
Joined: Mon Nov 03, 2003 5:34 am

Threaded Mouse Library for Games

Post by Axeman »

This library is used to update the mouse state via a threaded function. This is necessary to prevent issues with high poll-rate mice.

Make sure that the parent program is compiled in thread-safe mode. You'll also need to call 'InitMouse()' at the start of the program to initialize the mouse library.

See the comments at the top of the file for usage information.

Code: Select all


; === Threaded Mouse Library for Games ===


; - This library is used to update the mouse state via a threaded function. This is necessary to prevent issues with high poll-rate mice.

; - Make sure that the parent program is compiled in thread-safe mode. You'll also need to call 'InitMouse()' at the start of the program to initialize the mouse library.

; -

; - Use 'StartPBMouse()' to start the mouse update thread (when starting the program) and 'StopPBMouse()' to stop it once the mouse is no longer needed (on program shutdown).

; - Use 'PausePBMouse()' to pause the mouse update thread, and 'UnPausePBMouse()' to resume it again. You'll normally do this when the graphics screen loses focus. You can detect focus loss using 'IsScreenActive()'.

; - When the graphics screen loses focus you can use 'ReleasePBMouse()' to release the mouse for the user to use, and 'CapturePBMouse()' to capture it again when the screen regains the focus.

; - You can optionally use 'SetPBMouseLookingMode()' when switching to mouselook mode or a similar mode that relies on the mouse delta globals, and 'SetPBMouseGUIMode()' when switching to GUI menu mode
; - or a similar mode that relies on the absolute mouse position globals. Doing this ensures that the mouselook direction isn't affected when you pause an active game to use the game menus.

; -

; - Use the 'G_mouse_delta_x' and 'G_mouse_delta_y' integer globals to get the current mouse delta state for your mouselook or similar routine. Make sure that you zero them after you use them. You can also use
; - 'LockMutex( G_pb_mouse_mutex )' at the start of your mouselook routine and 'UnLockMutex( G_pb_mouse_mutex )' at the end to preven issues, though this may not be necessary.
; -
; - Example:-
; ; == Start Mouselook Code.
; LockMutex( G_pb_mouse_mutex )
; G_player_yaw.f + ( G_mouse_delta_x * G_mouselook_speed.f )
; G_player_pitch.f + ( G_mouse_delta_y * G_mouselook_speed.f )
; G_mouse_delta_x = 0 : G_mouse_delta_y = 0
; UnlockMutex( G_pb_mouse_mutex )
; G_player_yaw.f = WrapAngle( G_player_yaw.f ) ; Wrap the yaw angle to stay within 360 degrees.
; If G_player_pitch.f > 90.0 : G_player_pitch.f = 90.0 : EndIf
; If G_player_pitch.f < -90.0 : G_player_pitch.f = -90.0 : EndIf
; RotateNode( #PLAYER_NODE, 0.0, G_player_yaw.f, 0.0 )
; RotateCamera( #PLAYER_CAMERA, G_player_pitch.f, 0.0, 0.0 )
; ; == End Mouselook Code.

; -

; - Use the 'G_pb_mouse_x' and 'G_pb_mouse_y' globals to get the position of the mouse. No need to zero them after use. You can also use the standard 'MouseLocate()' function if you need to set the mouse position.

; -

; - Use 'GetLeftMouseButtonClicks()', 'GetRightMouseButtonClicks()', and 'GetMiddleMouseButtonClicks()' to get the number of times that these buttons have been clicked since your last call to these functions.
; - A click in this case means the the button was pressed after not being pressed in the previous mouse update.

; - You can also get the number of mouse clicks using 'GetMouseButtonClicks( button_index )' where 'button_index' = 1 (left button), 2 (right button), or 3 (middle button).

; - Another way to get the number of mouse clicks for each button is to get the state value directly from the 'G_pb_mouse_button_1_clicks' (for left-mouse button), 'G_pb_mouse_button_2_clicks' (for right-mouse button), and
; - 'G_pb_mouse_button_3_clicks' (for middle-mouse button) globals. You'll need to zero them afterwards (eg. clicks = G_pb_mouse_button_1_clicks : G_pb_mouse_button_1_clicks = 0).

; -

; - Use 'GetLeftMouseButtonPressed()', 'GetRightMouseButtonPressed()', and 'GetMiddleMouseButtonPressed()' to find out if the respective button is being pressed (held down). 0 = Not pressed. 1 = Pressed.

; - You can also get the mouse pressed state using 'GetMouseButtonPressed( button_index )' where 'button_index' = 1 (left button), 2 (right button), or 3 (middle button).

; - Another way to get the mouse button pressed state is to get the state value directly from the 'G_pb_mouse_button_1_pressed' (for left-mouse button), 'G_pb_mouse_button_2_pressed' (for right-mouse button), and
; - 'G_pb_mouse_button_3_pressed' (for middle-mouse button) globals. 0 = Not pressed. 1 = Pressed. No need to zero them afterwards.

; -

; - Use 'GetMouseWheelDelta()' to get the amount that the mouse-wheel has been turned since your last call to this function. A positive value represents the mouse-wheel being rolled away from the user.
; - A negative value represents the mouse-wheel being rolled towards the user.

; - Another way to get the mouse-wheel delta value is to get the state value directly from the 'G_pb_mousewheel_delta' global.
; - You'll need to zero it afterwards (eg. mousewheel_delta = G_pb_mousewheel_delta : G_pb_mousewheel_delta = 0).

; -

; - Use 'WaitPBMouseNullInput()' to wait until no mouse buttons are being pressed and the mousewheel is not being turned. This function is useful to prevent miss-clicks when starting a match and when switching to and from the menus.
; - This also calls 'FlushPBMouse()' to zero the mouse event globals.

; - Use 'FlushPBMouse()' to zero the mouse event globals (except for G_pb_mouse_x, G_pb_mouse_y). This function is mainly for internal use, but if you find a use for it then it is here.

; - NOTES

; - Note that if you find that the mouse movement in your game is a bit jagged then try switching to using delta-timing to regulate the game's update rate rather than using 'SetFrameRate()' or a similar method.
; - I found that this made both mouselooking and the general dynamic game-state a lot smoother in my test code.

; - Here's an example function that I use for dealing with the graphics screen losing the focus (alt-tab, etc). Note that this particular function is designed for use with a borderless-fullscreen window, so
; - you will probably need to adapt it for your own use case. Call this function after the 'FlipBuffers()' command in your main loop and in any 3D menu event handling loops.
; - Example:-
; Procedure HandleScreenActiveState()
; ; Handles the screen becoming inactive and being reactivated.
; ; This should be run following the 'FlipBuffers()' command, as this is where 'IsScreenActive()' is updated.
; 
; If IsScreenActive() = 0
; 	; *** Pause anything that needs pausing here.
; 	PausePBMouse()
; 	ReleasePBMouse()
; 	SetCursorPos_( G_graphics_center_x, G_graphics_center_y ) ; Set the cursor position to the center of the screen. 'MouseLocate' doesn't seem to work for this so we use 'SetCursorPos' (Windows API) instead.
; 	If GetWindowState( #MAIN_WINDOW ) <> #PB_Window_Minimize : SetWindowState( #MAIN_WINDOW, #PB_Window_Minimize ) : EndIf
; 	Repeat
; 		While WindowEvent() : Wend ; Clear the event que. Required for windowed mode.
; 		Delay( 20 )
; 		FlipBuffers() ; Call 'FlipBuffers()' to update 'IsScreenActive()'.
; 	Until IsScreenActive()
; 	; *** Sync and resume anything that needs syncing and resuming here.
; 	If GetWindowState( #MAIN_WINDOW ) <> #PB_Window_Normal : SetWindowState( #MAIN_WINDOW, #PB_Window_Normal ) : EndIf
; 	CapturePBMouse()
; 	UnPausePBMouse()
; 	RenderWorld(0) : FlipBuffers() ; Make sure that the current graphics frame displayed is the correct one.
; EndIf
; EndProcedure


; == PBMouse Library Declarations ==

#PB_MOUSE_GUI_MODE = 0 : #PB_MOUSE_MOUSELOOK_MODE = 1
Global G_pb_mousewheel_delta, G_pb_mouse_x, G_pb_mouse_y, G_mouse_delta_x, G_mouse_delta_y;, G_pb_mouse_button_1, G_pb_mouse_button_2, G_pb_mouse_button_3
Global G_pb_mouse_mode = 0 ; 0 = GUI mode is enabled, mouselooking is disabled (default). 1 = Mouselooking is enabled, GUI mode is disabled.
Global G_pb_mouse_thread
Global G_pb_mouse_pause_thread ; 0 = Don't pause the thread. 1 = Pause the thread. 2 = The thread is paused.
Global G_pb_mouse_end_thread ; 0 = Don't end the thread. 1 = End the thread. 2 = The thread has ended.
Global G_pb_mouse_mutex
Global G_pb_mouse_button_1_pressed, G_pb_mouse_button_1_clicks
Global G_pb_mouse_button_2_pressed, G_pb_mouse_button_2_clicks
Global G_pb_mouse_button_3_pressed, G_pb_mouse_button_3_clicks


; == PBMouse Library Functions ==

Procedure PBMouseUpdate( dummy )
; This function is used for the mouse update thread
; This function will run constantly when the program window is active, but will be paused when it is inactive.
; Note that there is no need to center the mouse for mouselooking as the captured mouse will return useful delta values even if the mouse position is blocked by the screen border.

Repeat
	Delay( 1 )
	If ExamineMouse()
		LockMutex( G_pb_mouse_mutex )
		
		G_pb_mousewheel_delta + MouseWheel() ; Update mousewheel.
		
		; Update left mouse button.
		old_button_1_pressed = G_pb_mouse_button_1_pressed
		If MouseButton( #PB_MouseButton_Left )
			G_pb_mouse_button_1_pressed = 1
			If G_pb_mouse_button_1_pressed <> old_button_1_pressed : G_pb_mouse_button_1_clicks + 1 : EndIf
		Else
			G_pb_mouse_button_1_pressed = 0
		EndIf
		
		; Update right mouse button.
		old_button_2_pressed = G_pb_mouse_button_2_pressed
		If MouseButton( #PB_MouseButton_Right )
			G_pb_mouse_button_2_pressed = 1
			If G_pb_mouse_button_2_pressed <> old_button_2_pressed : G_pb_mouse_button_2_clicks + 1 : EndIf
		Else
			G_pb_mouse_button_2_pressed = 0
		EndIf
		
		; Update middle mouse button.
		old_button_3_pressed = G_pb_mouse_button_3_pressed
		If MouseButton( #PB_MouseButton_Middle )
			G_pb_mouse_button_3_pressed = 1
			If G_pb_mouse_button_3_pressed <> old_button_3_pressed : G_pb_mouse_button_3_clicks + 1 : EndIf
		Else
			G_pb_mouse_button_3_pressed = 0
		EndIf
		
		; Update the globals used to return the absolute mouse position.
		G_pb_mouse_x = MouseX()
		G_pb_mouse_y = MouseY()
		
		; Update the globals used to return the mouse position delta values, if mouselook mode is enabled.
		If G_pb_mouse_mode = #PB_MOUSE_MOUSELOOK_MODE ; Ensure this doesn't get updated in menu mode. Otherwise the mouselook direction will be changed when returning to a game.
			G_mouse_delta_x - MouseDeltaX() : G_mouse_delta_y - MouseDeltaY()
		EndIf
		
		UnlockMutex( G_pb_mouse_mutex )
	EndIf
	If G_pb_mouse_pause_thread : G_pb_mouse_pause_thread = 2 : PauseThread( G_pb_mouse_thread ) : EndIf
	If G_pb_mouse_end_thread : G_pb_mouse_end_thread = 2 : Break : EndIf
ForEver
EndProcedure



Procedure.i GetMouseWheelDelta()
; Returns the mousewheel delta since the last call to this function.

LockMutex( G_pb_mouse_mutex )
mousewheel_delta = G_pb_mousewheel_delta
G_pb_mousewheel_delta = 0
UnlockMutex( G_pb_mouse_mutex )

ProcedureReturn mousewheel_delta
EndProcedure



Procedure.i GetMouseButtonPressed( button_index )
; Returns 1 if the specified mouse button is currently being pressed, and 0 if it is not.
; -
; button_index: Should be 1 (or #PB_MouseButton_Left) for the left mouse button, 2 (or #PB_MouseButton_Right) for the right mouse button, or 3 (or #PB_MouseButton_Middle) for the middle mouse button.

Select button_index
	Case 1 : ProcedureReturn G_pb_mouse_button_1_pressed
	Case 2 : ProcedureReturn G_pb_mouse_button_2_pressed
	Case 3 : ProcedureReturn G_pb_mouse_button_3_pressed
	Default : ProcedureReturn 0
EndSelect
EndProcedure



Procedure.i GetMouseButtonClicks( button_index )
; Returns the number of times that the specified mouse button has been clicked since the last call to this function.
; A click in this case means the button being pressed after it was in the released state, rather than a press followed by a release.
; -
; button_index: Should be 1 (or #PB_MouseButton_Left) for the left mouse button, 2 (or #PB_MouseButton_Right) for the right mouse button, or 3 (or #PB_MouseButton_Middle) for the middle mouse button.

LockMutex( G_pb_mouse_mutex )
Select button_index
	Case 1 : clicks = G_pb_mouse_button_1_clicks : G_pb_mouse_button_1_clicks = 0
	Case 2 : clicks = G_pb_mouse_button_2_clicks : G_pb_mouse_button_2_clicks = 0
	Case 3 : clicks = G_pb_mouse_button_3_clicks : G_pb_mouse_button_3_clicks = 0
EndSelect
UnlockMutex( G_pb_mouse_mutex )
ProcedureReturn clicks
EndProcedure



Procedure.i GetLeftMouseButtonPressed()
; Returns 1 if the left mouse button is currently being pressed, and 0 if it is not.

ProcedureReturn G_pb_mouse_button_1_pressed
EndProcedure



Procedure.i GetLeftMouseButtonClicks()
; Returns the number of times that the left mouse button has been clicked since the last call to this function.
; A click in this case means the button being pressed after it was in the released state, rather than a press followed by a release.

LockMutex( G_pb_mouse_mutex )
clicks = G_pb_mouse_button_1_clicks
G_pb_mouse_button_1_clicks = 0
UnlockMutex( G_pb_mouse_mutex )
ProcedureReturn clicks
EndProcedure



Procedure.i GetRightMouseButtonPressed()
; Returns 1 if the right mouse button is currently being pressed, and 0 if it is not.

ProcedureReturn G_pb_mouse_button_2_pressed
EndProcedure



Procedure.i GetRightMouseButtonClicks()
; Returns the number of times that the right mouse button has been clicked since the last call to this function.
; A click in this case means the button being pressed after it was in the released state, rather than a press followed by a release.

LockMutex( G_pb_mouse_mutex )
clicks = G_pb_mouse_button_2_clicks
G_pb_mouse_button_2_clicks = 0
UnlockMutex( G_pb_mouse_mutex )
ProcedureReturn clicks
EndProcedure



Procedure.i GetMiddleMouseButtonPressed()
; Returns 1 if the middle mouse button is currently being pressed, and 0 if it is not.

ProcedureReturn G_pb_mouse_button_3_pressed
EndProcedure



Procedure.i GetMiddleMouseButtonClicks()
; Returns the number of times that the middle mouse button has been clicked since the last call to this function.
; A click in this case means the button being pressed after it was in the released state, rather than a press followed by a release.

LockMutex( G_pb_mouse_mutex )
clicks = G_pb_mouse_button_3_clicks
G_pb_mouse_button_3_clicks = 0
UnlockMutex( G_pb_mouse_mutex )
ProcedureReturn clicks
EndProcedure



Procedure PausePBMouse()
; Pause the PBMouse update thread if it is unpaused.
; This function can be called blind.

Delay(1) ; Delay to give any threaded actions time to be applied.
If G_pb_mouse_pause_thread = 0
	G_pb_mouse_pause_thread = 1
	Repeat : Delay( 20 ) : Until G_pb_mouse_pause_thread = 2 : Delay( 20 )
EndIf
EndProcedure



Procedure UnPausePBMouse()
; Unpause the PBMouse update thread if it is paused.
; This function can be called blind.

Delay(1) ; Delay to give any threaded actions time to be applied.
If G_pb_mouse_pause_thread = 2 ; If the thread is currently paused.
	G_pb_mouse_pause_thread = 0
	ResumeThread( G_pb_mouse_thread )
EndIf
EndProcedure



Procedure FlushPBMouse()
; Zero all the mouse data values except for the 'G_pb_mouse_x', 'G_pb_mouse_y' absolute mouse position values.

LockMutex( G_pb_mouse_mutex )
G_pb_mousewheel_delta = 0
G_pb_mouse_button_1_pressed = 0
G_pb_mouse_button_2_pressed = 0
G_pb_mouse_button_3_pressed = 0
G_pb_mouse_button_1_clicks = 0
G_pb_mouse_button_2_clicks = 0
G_pb_mouse_button_3_clicks = 0
G_mouse_delta_x = 0
G_mouse_delta_y = 0
UnlockMutex( G_pb_mouse_mutex )
EndProcedure



Procedure WaitPBMouseNullInput()
; Waits until no mouse buttons are being pressed and the mousewheel is not being turned. This function is useful to prevent miss-clicks when starting a match and when switching to and from the menus.
; This function also calls 'FlushPBMouse()' to zero the mouse event globals.

PausePBMouse()
Repeat
	Delay( 20 )
	result = 1
	If ExamineMouse()
		result = MouseButton( #PB_MouseButton_Left ) + MouseButton( #PB_MouseButton_Right ) + MouseButton( #PB_MouseButton_Middle ) + ( Bool( MouseWheel() <> 0 ) )
	EndIf
Until result = 0
FlushPBMouse() ; Ensure that the mouse values are zeroed.
UnPausePBMouse()
EndProcedure



Procedure CapturePBMouse()
; Capture the PBMouse to the game program window. This is only normally needed after a call to ReleasePBMouse() as the first call to ExamineMouse() will normally capture the mouse automatically.

ReleaseMouse( 0 )
ExamineMouse() ; Call this here to ensure that the system mouse is hidden.
EndProcedure



Procedure ReleasePBMouse()
; Uncapture the PBMouse to the program window. This allows the mouse to be used when the program window loses focus.

ReleaseMouse( 1 )
EndProcedure



Procedure SetPBMouseLookingMode()
; Switches to mouselooking mode. Normally called when starting a match and when returning from using the menu system.
; This allows the mouse update thread to resume updating the 'G_mouse_delta_x' and 'G_mouse_delta_y' mouse position delta globals used for mouse-looking, etc.

; Enable the mouselooking, disable the GUI mouse actions, , and unpause the thread.
LockMutex( G_pb_mouse_mutex )
G_mouse_delta_x = 0 : G_mouse_delta_y = 0
G_pb_mouse_mode = 1
UnlockMutex( G_pb_mouse_mutex )
UnpausePBMouse()
EndProcedure



Procedure SetPBMouseGUIMode()
; Switches to GUI mode. Normally called before using the menu system.
; This stops the mouse update thread from updating the 'G_mouse_delta_x' and 'G_mouse_delta_y' mouse position delta globals used for mouse-looking, etc.

LockMutex( G_pb_mouse_mutex ) : G_pb_mouse_mode = 0 : UnlockMutex( G_pb_mouse_mutex ) : UnpausePBMouse() ; Enable the GUI mouse actions, disable the mouselooking, and unpause the thread.
EndProcedure



Procedure StartPBMouse()
; Start the PBMouse update thread. No PBMouse related functions should be called before this function, as the thread won't yet exist. The thread will begin unpaused and with the mouse captured.
; Note that the mouse is captured automatically once the program calls ExamineMouse() for the first time.

G_pb_mouse_mutex = CreateMutex()
FlushPBMouse() ; Zero all the mouse data values.
ExamineMouse() ; Call this here to ensure that the system mouse is captured and hidden.
G_pb_mouse_end_thread = 0 : G_pb_mouse_pause_thread = 0 ; Initialize the PBMouse message globals.
G_pb_mouse_thread = CreateThread( @PBMouseUpdate(), 0 ) ; Start the PBMouse update thread.
If G_pb_mouse_thread = 0 : FatalError( "The mouse update thread couldn't be created." ) : EndIf
EndProcedure



Procedure StopPBMouse()
; End the PBMouse update thread. No further PBMouse related functions should be called after this function exits.

UnpausePBMouse() ; Make sure that the PBMouse thread is not paused so that it can process the code below.
G_pb_mouse_end_thread = 1 ; End the PBMouse update thread.
Repeat : Delay( 20 ) : Until G_pb_mouse_end_thread = 2 : Delay( 20 ) ; Wait until the thread is confirmed to have been ended.
FreeMutex( G_pb_mouse_mutex )
EndProcedure