-
-
Save kwalkerxxi/7309e49d8604d1c8745f36710c4946b3 to your computer and use it in GitHub Desktop.
RawInput Wrapper for Unity3d : Handles multiple cursors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://pastebin.com/4h3CqpYy | |
// Unity PINVOKE interface for pastebin.com/0Szi8ga6 | |
// Handles multiple cursors | |
// License: CC0 | |
using UnityEngine; | |
using System.Collections; | |
using System.Runtime.InteropServices; | |
using System; | |
using System.Collections.Generic; | |
using UnityEngine.UI; | |
public class MouseInputManager : MonoBehaviour | |
{ | |
public static MouseInputManager instance; | |
[DllImport("LibRawInput")] | |
private static extern bool init(); | |
[DllImport("LibRawInput")] | |
private static extern bool kill(); | |
[DllImport("LibRawInput")] | |
private static extern IntPtr poll(); | |
public const byte RE_DEVICE_CONNECT = 0; | |
public const byte RE_MOUSE = 2; | |
public const byte RE_DEVICE_DISCONNECT = 1; | |
public string getEventName(byte id) | |
{ | |
switch (id) | |
{ | |
case RE_DEVICE_CONNECT: return "RE_DEVICE_CONNECT"; | |
case RE_DEVICE_DISCONNECT: return "RE_DEVICE_DISCONNECT"; | |
case RE_MOUSE: return "RE_MOUSE"; | |
} | |
return "UNKNOWN(" + id + ")"; | |
} | |
public GameObject cursor; | |
public Color[] colors; | |
public float defaultMiceSensitivity = 1f; | |
public float accelerationThreshold = 40; | |
public float accelerationMultiplier = 2; | |
public int screenBorderPixels = 16; | |
[StructLayout(LayoutKind.Sequential)] | |
public struct RawInputEvent | |
{ | |
public int devHandle; | |
public int x, y, wheel; | |
public byte press; | |
public byte release; | |
public byte type; | |
} | |
public class MousePointer | |
{ | |
public GameObject obj; | |
public Vector2 position; | |
public int deviceID; | |
public int playerID; | |
public float sensitivity; | |
} | |
Dictionary<int, MousePointer> pointersByDeviceId = new Dictionary<int, MousePointer>(); | |
Dictionary<int, MousePointer> pointersByPlayerId = new Dictionary<int, MousePointer>(); | |
int nextPlayerId = 1; | |
int miceCount = 0; | |
Canvas canvas; | |
RectTransform canvasRect; | |
float width, height; | |
void Start() | |
{ | |
instance = this; | |
bool res = init(); | |
Debug.Log("Init() ==> " + res); | |
Debug.Log(Marshal.SizeOf(typeof(RawInputEvent))); | |
canvas = GetComponent<Canvas>(); | |
canvasRect = GetComponent<RectTransform>(); | |
//enterSingleMode(); | |
} | |
public void OnDestroy() | |
{ | |
instance = null; | |
} | |
int addCursor(int deviceId) | |
{ | |
if(!isInit) | |
{ | |
Debug.LogError("Not initialized"); | |
return -1; | |
} | |
MousePointer mp = null; | |
pointersByDeviceId.TryGetValue(deviceId, out mp); | |
if(mp != null) | |
{ | |
Debug.LogError("This device already has a cursor"); | |
return -1; | |
} | |
Debug.Log("Adding DeviceID " + deviceId); | |
mp = new MousePointer(); | |
mp.playerID = nextPlayerId++; | |
pointersByDeviceId[deviceId] = mp; | |
pointersByPlayerId[mp.playerID] = mp; | |
mp.position = new Vector3(width / 2, height / 2, 0); | |
mp.obj = Instantiate(cursor, transform) as GameObject; | |
var rt = mp.obj.GetComponent<RectTransform>(); | |
rt.position = mp.position; | |
var spriteComp = mp.obj.GetComponent<Image>(); | |
if (spriteComp) spriteComp.color = colors[mp.playerID % colors.Length]; | |
++miceCount; | |
return mp.playerID; | |
} | |
void deleteCursor(int deviceId) | |
{ | |
--miceCount; | |
var mp = pointersByDeviceId[deviceId]; | |
pointersByDeviceId.Remove(mp.deviceID); | |
pointersByPlayerId.Remove(mp.playerID); | |
Destroy(mp.obj); | |
} | |
bool _isMultiplayer = true; | |
MousePointer _spPointer; | |
[SerializeField] | |
public bool isMultiplayer | |
{ | |
set | |
{ | |
if (!value) enterSingleMode(); else enterMultipleMode(); | |
_isMultiplayer = value; | |
} | |
get { return _isMultiplayer; } | |
} | |
void enterSingleMode() | |
{ | |
clearCursorsAndDevices(); | |
--nextPlayerId; | |
addCursor(0); | |
_spPointer = pointersByDeviceId[0]; | |
Cursor.lockState = CursorLockMode.None; | |
Cursor.visible = false; | |
} | |
void enterMultipleMode() | |
{ | |
_spPointer = null; | |
nextPlayerId = 0; | |
clearCursorsAndDevices(); | |
Cursor.lockState = CursorLockMode.Locked; | |
Cursor.visible = false; | |
} | |
void clearCursorsAndDevices() | |
{ | |
pointersByDeviceId.Clear(); | |
pointersByPlayerId.Clear(); | |
nextPlayerId = 1; | |
miceCount = 0; | |
foreach (Transform t in transform) Destroy(t.gameObject); | |
} | |
public MousePointer getByPlayerId(int id) | |
{ | |
MousePointer res = null; | |
pointersByPlayerId.TryGetValue(id, out res); | |
return res; | |
} | |
// Update is called once per frame | |
int lastEvents = 0; | |
bool isInit = true; | |
void Update() | |
{ | |
// Keyboard controls debug | |
if (Input.GetKeyDown(KeyCode.R)) | |
{ | |
if (isInit) | |
{ | |
clearCursorsAndDevices(); | |
kill(); | |
isInit = false; | |
} | |
else | |
{ | |
init(); | |
isInit = true; | |
} | |
} | |
if (Input.GetKeyDown(KeyCode.M)) | |
{ | |
isMultiplayer = !isMultiplayer; | |
} | |
// SP | |
if (!_isMultiplayer) | |
{ | |
var rt = _spPointer.obj.GetComponent<RectTransform>(); | |
rt.position = Input.mousePosition; | |
} | |
else | |
{ | |
// MP | |
width = canvasRect.rect.width; | |
height = canvasRect.rect.height; | |
var left = -width / 2; | |
var right = width / 2; | |
var top = -height / 2; | |
var bottom = height / 2; | |
//Debug.Log("left=" + left + ", right=" + right + ", top=" + top + ", bottom=" + bottom); | |
// Poll the events and properly update whatever we need | |
IntPtr data = poll(); | |
int numEvents = Marshal.ReadInt32(data); | |
if (numEvents > 0) lastEvents = numEvents; | |
for (int i = 0; i < numEvents; ++i) | |
{ | |
var ev = new RawInputEvent(); | |
long offset = data.ToInt64() + sizeof(int) + i * Marshal.SizeOf(ev); | |
ev.devHandle = Marshal.ReadInt32(new IntPtr(offset + 0)); | |
ev.x = Marshal.ReadInt32(new IntPtr(offset + 4)); | |
ev.y = Marshal.ReadInt32(new IntPtr(offset + 8)); | |
ev.wheel = Marshal.ReadInt32(new IntPtr(offset + 12)); | |
ev.press = Marshal.ReadByte(new IntPtr(offset + 16)); | |
ev.release = Marshal.ReadByte(new IntPtr(offset + 17)); | |
ev.type = Marshal.ReadByte(new IntPtr(offset + 18)); | |
//Debug.Log(getEventName(ev.type) + ": H=" + ev.devHandle + "; (" + ev.x + ";" + ev.y + ") Down=" + ev.press + " Up=" + ev.release); | |
if (ev.type == RE_DEVICE_CONNECT) addCursor(ev.devHandle); | |
else if (ev.type == RE_DEVICE_DISCONNECT) deleteCursor(ev.devHandle); | |
else if (ev.type == RE_MOUSE) | |
{ | |
MousePointer pointer = null; | |
if (pointersByDeviceId.TryGetValue(ev.devHandle, out pointer)) | |
{ | |
float dx = ev.x * defaultMiceSensitivity; | |
float dy = ev.y * defaultMiceSensitivity; | |
if (Mathf.Abs(dx) > accelerationThreshold) dx *= accelerationMultiplier; | |
if (Mathf.Abs(dy) > accelerationThreshold) dy *= accelerationMultiplier; | |
pointer.position = new Vector2( | |
Mathf.Clamp(pointer.position.x + dx, screenBorderPixels, width - screenBorderPixels), | |
Mathf.Clamp(pointer.position.y - dy, screenBorderPixels, height - screenBorderPixels)); | |
RectTransform rt = pointer.obj.GetComponent<RectTransform>(); | |
rt.position = pointer.position; | |
} | |
else | |
{ | |
Debug.Log("Unknown device found"); | |
addCursor(ev.devHandle); | |
} | |
} | |
} | |
Marshal.FreeCoTaskMem(data); | |
} | |
} | |
void OnApplicationQuit() | |
{ | |
kill(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://pastebin.com/0Szi8ga6 | |
/* A RawInput implementation with HWND_MESSAGE window and Device State detection. Uses separate thread and accumulates deltas */ | |
#include <atomic> | |
#include <Windows.h> | |
#include "stdafx.h" | |
#include "MessageToString.h" | |
#include <iostream> | |
#include <vector> | |
#include <mutex> | |
#include <map> | |
#include <sstream> | |
#include <deque> | |
using namespace std; | |
// State | |
bool isInitialized = false; | |
HANDLE runningThread = 0; | |
HWND messageWindow = 0; | |
// Multi threading | |
using Mutex = std::mutex; | |
using MutexLocker = std::lock_guard<std::mutex>; | |
#define EnsureMutexLocked(T) { if(T.try_lock()) Crash(); } | |
#define Crash() { std::cout << "CRASH()"; ((void(*)())0)(); } | |
#define StrongAssert(T) { if(!(T)) cout << "Assertion Failed" << ##T << "\n"; Crash(); } | |
// RawInput stuff | |
const uint8_t RE_DEVICE_CONNECT = 0; | |
const uint8_t RE_DEVICE_DISCONNECT = 1; | |
const uint8_t RE_MOUSE = 2; | |
struct RawInputEvent | |
{ | |
int32_t devHandle; | |
int32_t x, y, wheel; | |
// pressed button | |
uint8_t press; | |
//released button | |
uint8_t release; | |
// event type | |
uint8_t type; | |
}; | |
vector<RawInputEvent> generatedEvents; // deltas must be accumulated | |
Mutex dataMutex; | |
// Mouse state for now | |
struct DeviceState | |
{ | |
int32_t x, y, z; // x,y are delts movement, z is WheelDelta | |
uint8_t buttonStates; // bad idea for polling | |
wstring name; | |
}; | |
// Global buffer for worker thread | |
std::vector<char> m_RawInputMessageData; // Buffer | |
map<HANDLE, DeviceState> devices; | |
void printEvent(RawInputEvent e) | |
{ | |
stringstream ss; | |
switch (e.type) | |
{ | |
case RE_DEVICE_CONNECT: ss << "RE_DEVICE_CONNECT"; break; | |
case RE_DEVICE_DISCONNECT: ss << "RE_DEVICE_DISCONNECT"; break; | |
case RE_MOUSE: ss << "RE_MOUSE"; break; | |
default: ss << "UNKNOWN(" << e.type << ")"; | |
} | |
ss << " " << e.devHandle << " (" << e.x << "; " << e.y << ") DOWN=" << int(e.press) << " UP=" << int(e.release) << " w=" << e.wheel << "\n"; | |
cout << ss.str(); | |
} | |
inline void AddEvent(uint8_t type, int32_t devHandle, uint8_t press, uint8_t release) | |
{ | |
MutexLocker locker(dataMutex); | |
RawInputEvent e; | |
e.x = 0; | |
e.y = 0; | |
e.wheel = 0; | |
e.type = type; | |
e.devHandle = devHandle; | |
e.press = press; | |
e.release = release; | |
generatedEvents.push_back(e); | |
printEvent(e); | |
} | |
inline void AddEvent(RawInputEvent& ev) | |
{ | |
MutexLocker locker(dataMutex); | |
generatedEvents.push_back(ev); | |
printEvent(ev); | |
} | |
void OnRawInput(HRAWINPUT handle) | |
{ | |
// Determine the size | |
UINT dataSize; | |
GetRawInputData(handle, RID_INPUT, NULL, &dataSize, sizeof(RAWINPUTHEADER)); // get Size | |
if (dataSize == 0) return; | |
if (dataSize > m_RawInputMessageData.size()) m_RawInputMessageData.resize(dataSize); | |
// Get the Data | |
void* dataBuf = &m_RawInputMessageData[0]; | |
GetRawInputData(handle, RID_INPUT, dataBuf, &dataSize, sizeof(RAWINPUTHEADER)); // get Data | |
const RAWINPUT *raw = (const RAWINPUT*)dataBuf; | |
// Mouse | |
//if (raw->header.dwType == RIM_TYPEMOUSE) | |
HANDLE deviceHandle = raw->header.hDevice; | |
const RAWMOUSE& mouseData = raw->data.mouse; | |
USHORT flags = mouseData.usButtonFlags; | |
short wheelDelta = (short)mouseData.usButtonData; | |
LONG x = mouseData.lLastX, y = mouseData.lLastY; | |
// Some events are critical | |
if(flags & RI_MOUSE_LEFT_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 1, 0); | |
if(flags & RI_MOUSE_LEFT_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 1); | |
if(flags & RI_MOUSE_MIDDLE_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 3, 0); | |
if(flags & RI_MOUSE_MIDDLE_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 3); | |
if(flags & RI_MOUSE_RIGHT_BUTTON_DOWN ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 2, 0); | |
if(flags & RI_MOUSE_RIGHT_BUTTON_UP ) AddEvent(RE_MOUSE, int32_t(raw->header.hDevice), 0, 2); | |
// Some are to be accumulated | |
auto& dev = devices[raw->header.hDevice]; | |
dev.x += x; | |
dev.y += y; | |
dev.z += wheelDelta; | |
/* | |
wprintf( | |
L"Mouse: Device=0x%08X, Flags=%04x, WheelDelta=%d, X=%d, Y=%d\n", | |
deviceHandle, flags, wheelDelta, x, y); | |
/**/ | |
} | |
void OnDeviceChange(HRAWINPUT handle, bool connected) | |
{ | |
if (!connected) | |
{ | |
RawInputEvent ev; | |
ev.devHandle = int32_t(handle); | |
ev.type = connected ? RE_DEVICE_CONNECT : RE_DEVICE_DISCONNECT; | |
ev.x = 0; | |
ev.y = 0; | |
AddEvent(ev); | |
MutexLocker locker(dataMutex); | |
devices.erase(handle); | |
return; | |
} | |
// Determine the size, Get Device Name | |
std::vector<wchar_t> deviceNameData; | |
wstring deviceName; | |
UINT dataSize; | |
SetLastError(0); | |
GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, nullptr, &dataSize); | |
if (GetLastError()) return; | |
if (dataSize) | |
{ | |
deviceNameData.resize(dataSize); | |
UINT result = GetRawInputDeviceInfo(handle, RIDI_DEVICENAME, &deviceNameData[0], &dataSize); | |
if (result != UINT_MAX) | |
{ | |
deviceName.assign(deviceNameData.begin(), deviceNameData.end()); | |
wprintf(L" Name=%s\n", deviceName.c_str()); | |
} | |
} | |
RID_DEVICE_INFO deviceInfo; | |
deviceInfo.cbSize = sizeof deviceInfo; | |
dataSize = sizeof deviceInfo; | |
UINT result = GetRawInputDeviceInfo(handle, RIDI_DEVICEINFO, &deviceInfo, &dataSize); | |
if (result != UINT_MAX) | |
{ | |
wprintf(L" Id=%u, Buttons=%u, SampleRate=%u, HorizontalWheel=%s\n", | |
deviceInfo.mouse.dwId, | |
deviceInfo.mouse.dwNumberOfButtons, | |
deviceInfo.mouse.dwSampleRate, | |
deviceInfo.mouse.fHasHorizontalWheel ? L"1" : L"0"); | |
// At this perfect moment, add OR remove the device | |
RawInputEvent ev; | |
ev.devHandle = int32_t(handle); | |
ev.type = RE_DEVICE_CONNECT; | |
ev.x = 0; | |
ev.y = 0; | |
AddEvent(ev); | |
MutexLocker locker(dataMutex); | |
devices[handle].name = deviceName; | |
} | |
} | |
LRESULT CALLBACK RawInputWndProc(HWND wh, UINT msg, WPARAM wp, LPARAM lp) | |
{ | |
// Debugging Message pumps | |
//cout << WMMessageToStr(msg, true) << ": W= " << wp << "; L= " << lp << "\n"; | |
// 254: WM_INPUT_DEVICE_CHANGE | |
// wp = 1 GIDC_ARRIVAL | |
// wp = 2 GIDC_REMOVAL | |
// 255: WM_INPUT | |
if (msg == WM_INPUT_DEVICE_CHANGE) | |
{ | |
if (wp==1) | |
{ | |
OnDeviceChange((HRAWINPUT)lp, true); | |
} | |
else if (wp == 2) | |
{ | |
OnDeviceChange((HRAWINPUT)lp, false); | |
} | |
} | |
else if (msg == WM_INPUT) | |
{ | |
OnRawInput((HRAWINPUT)lp); | |
} | |
return DefWindowProc(wh, msg, wp, lp); | |
} | |
static const wchar_t* class_name = L"PI_DEV_RAWINPUT"; | |
void RawInputThread(LPVOID params) | |
{ | |
WNDCLASSEX wx = {}; | |
wx.cbSize = sizeof(WNDCLASSEX); | |
wx.lpfnWndProc = RawInputWndProc; | |
wx.hInstance = NULL; | |
wx.lpszClassName = class_name; | |
HWND wh; | |
if (RegisterClassEx(&wx)) | |
{ | |
wh = CreateWindowEx(0, class_name, L"Pi-Dev RawInput [NS]", 0, 0, 0, 0, 0, HWND_DESKTOP, NULL, NULL, NULL); | |
messageWindow = wh; | |
ShowWindow(wh, SW_SHOWMINNOACTIVE); | |
RAWINPUTDEVICE device[4]; | |
// Mouse | |
device[0].usUsagePage = 0x01; | |
device[0].usUsage = 0x02; | |
device[0].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; | |
device[0].hwndTarget = wh; | |
// Gamepad | |
device[1].usUsagePage = 0x01; | |
device[1].usUsage = 0x05; | |
device[1].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; | |
device[1].hwndTarget = wh; | |
// Joystick | |
device[2].usUsagePage = 0x01; | |
device[2].usUsage = 0x04; | |
device[2].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; | |
device[2].hwndTarget = wh; | |
// Keyboard | |
device[3].usUsagePage = 0x01; | |
device[3].usUsage = 0x06; | |
device[3].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; | |
device[3].hwndTarget = wh; | |
// Register ONLY Mice | |
RegisterRawInputDevices(device, 1, sizeof RAWINPUTDEVICE); | |
MSG msg; | |
while (GetMessage(&msg, 0, 0, 0) > 0) | |
{ | |
DispatchMessage(&msg); | |
} | |
} | |
} | |
extern "C" __declspec(dllexport) int kill() | |
{ | |
SetLastError(0); | |
PostThreadMessage(GetThreadId(runningThread), WM_QUIT, 0, 0); | |
cout << "PostThreadMessage = " << GetLastError() << "\n"; | |
SetLastError(0); | |
UnregisterClass(class_name, NULL); | |
cout << "UnregisterClass = " << GetLastError() << "\n"; | |
/**/ | |
messageWindow = 0; | |
runningThread = 0; | |
isInitialized = false; | |
return GetLastError(); | |
} | |
extern "C" __declspec(dllexport) bool init() | |
{ | |
kill(); | |
// this is actually reinit() | |
cout << "init()"; | |
MutexLocker locker(dataMutex); | |
devices.clear(); | |
generatedEvents.clear(); | |
if (!isInitialized) | |
{ | |
isInitialized = true; | |
runningThread = CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(RawInputThread), NULL, 0, 0); | |
messageWindow = 0; | |
return true; | |
} | |
return false; | |
} | |
extern "C" __declspec(dllexport) void* poll() | |
{ | |
MutexLocker locker(dataMutex); | |
//cout << "==== Deltas =====\n"; | |
int numItems = generatedEvents.size(); | |
stringstream ss; | |
for (auto& d : devices) | |
{ | |
RawInputEvent e; | |
e.devHandle = int32_t(d.first); | |
auto& data = d.second; | |
e.press = 0; | |
e.release = 0; | |
e.type = RE_MOUSE; | |
e.wheel = data.z; | |
e.x = data.x; | |
e.y = data.y; | |
if (e.x != 0 || e.y != 0) | |
{ | |
ss.write((char*)&e, sizeof(RawInputEvent)); | |
++numItems; | |
} | |
cout << e.x << "; " << e.y << "\n"; | |
// Zero accumulation fields | |
data.x = 0; | |
data.y = 0; | |
data.z = 0; | |
} | |
ss.write((char*)generatedEvents.data(), sizeof(RawInputEvent)*generatedEvents.size()); | |
uint8_t* buf = (uint8_t*)CoTaskMemAlloc(4 + numItems*sizeof(RawInputEvent)); | |
memcpy(buf, &numItems, 4); | |
memcpy(buf+4, ss.str().data(), numItems*sizeof(RawInputEvent)); | |
generatedEvents.clear(); | |
return buf; | |
} | |
int main() | |
{ | |
cout << "sz = " << sizeof(RawInputEvent) << "\n"; | |
init(); | |
while (true) | |
{ | |
if (GetAsyncKeyState(VK_HOME)) cout << "init() = " << init() << "\n"; | |
if (GetAsyncKeyState(VK_END)) cout << "kill() = " << kill() << "\n"; | |
Sleep(1000); | |
void* data = poll(); | |
int d = 0; | |
memcpy(&d, data, 4); | |
//cout << d << "\n"; | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment