Делаем простое удержание курсора в окне Warcraft 3

от автора

Приветствую тебя, читатель. У меня есть хобби — это старый добрый Warcraft 3. На хабре уже был цикл статей, посвященный этой замечательной игре. Хочу поделиться с комьюнити одной утилитой, пригодившейся мне при проведении стримов. Всех заинтересовавшихся прошу пройти под кат.

Предисловие

Все началось с того, что в один из выходных на фоне непрекращающегося ремонта я решил посмотреть стрим по Warcraft III. Площадок на данный момент достаточно, но мои предпочтения относятся к сайту www.goodgame.ru (не реклама). Был разочарован, что ничего интересного на тот момент не транслировалось. И тогда возникла мысль — почему бы не сделать свой стрим с блэкджэком и т.д.

Сопутствующее ПО

Для проведения трансляции, кроме всего прочего, потребуется приложение для захвата контента. На данный момент можно выделить два из них: xsplit и openbroadcaster. Честно скажу, первым не пользовался. В бесплатной версии доступен базовый функционал. Но для скачивания базовой версии придется пройти обязательную регистрацию (не то что бы это было проблемой, но…). Ко второму варианту склонила лицензия GPL и соответственно доступность исходного кода. На openbroadcaster я и остановился.

Трудности

С установкой и настройкой OBS проблем не возникло. Но запущенная игра никак не хотела захватываться в рекомендованном режиме Game capture (вероятно это связано с использованием старой версии directx при разработке игры). Поигравшись с другими режимами захвата, удалось найти два, которые обеспечивали необходимое поведение — Monitor capture и Window capture.
Первый достаточно сильно аффектит перформанс. Ощущается во время игры. Но это был рабочий вариант, что называется «из коробки».
Второй вариант приводил к дискомфорту в процессе игры — курсор постоянно выходил за границы окна. В общем, было абсолютно неиграбельно.

Решение

Был выбран второй вариант и принято решение написать утилиту для устранения описанного выше дискомфорта.
Изначально Warcraft III запускается в полноэкраном режиме.
Для запуска в оконном режиме необходимо использовать ключ "-window" в команде запуска приложения, это как раз позволит выполнить захват в режиме Windows capture.

Для удержания курсора в рамках клиентской области окна была написана первая версия утилиты. Основной цикл ее работы приведен ниже:

/* polling version */ void Controller::RunPollingLoop() {		 	while (true) 	{ 		HWND activeWindow		= GetForegroundWindow(); 		HWND requiredWindow		= FindRequiredWindow(m_className, m_winTitle, 5);  		if (requiredWindow == NULL) 			throw std::runtime_error("Required window not found"); 		 		m_fullScreen.Init(requiredWindow); 		m_clipHelper.Init(requiredWindow);  		if (activeWindow == requiredWindow) 		{ 			if (m_clipHelper.IsClipped() || !CursorInClientArea(requiredWindow)) 			{ 				Sleep(g_SleepTimeOut); 				continue; 			}  			if (m_fullScreen.Enter())  			{	 				DEBUG_TRACE("EnterFullscreen success");  				m_clipHelper.Clip(); 				DEBUG_TRACE("Clip"); 			} 			else 			{	DEBUG_TRACE("EnterFullscreen failed"); } 		} 		else 		{ 			if (m_clipHelper.IsClipped()) 			{ 				if (m_fullScreen.Leave()) 				{ DEBUG_TRACE("LeaveFullscreen success"); } 				else 				{ DEBUG_TRACE("LeaveFullscreen failed"); }  				m_clipHelper.UnClip(); 				DEBUG_TRACE("UnClip"); 			}  			Sleep(g_SleepTimeOut); 		} 	} } 

Здесь используется вспомогательный класс ClipHelper для управления процессом удержания курсора и класс FullScreen для управления процессом перехода в полноэкранный режим и восстановления из него. Сам цикл реализует алгоритм поллинга активного окна с таймаутом в 500 мс. Этот момент мне не понравился сразу, но для движения дальше требовалось проверить всю концепцию, а потом заняться оптимизацией.

В процессе использования утилиты сразу возникли следующие хотелки:
— Clip проводить только в случае клика (удержания для поллинг версии) по клиентской области, чтобы иметь возможность перетаскивать окно;
— раздражал вид taskbar во время игры (актуально, если она зафиксирована). Первой мыслью было скрыть ее программно. Но в таком случае необходимо было бы отслеживать моменты выхода пользователя из игры и показывать taskbar обратно. Повышался риск оставить пользователя без панели задач. Поэтому реализацию fullscreen я решил сделать изменением размеров игрового окна до размеров разрешения монитора, за которым это окно закреплено:

bool FullScreen::Enter() { 	if (m_fullScreen) 		return true;  	assert(m_hwnd); 	if (m_hwnd == NULL) 		return false; 	 	HMONITOR hmon = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST); 	MONITORINFO mi = { sizeof(mi) };  	if (!GetMonitorInfo(hmon, &mi))  		return false;  	if (!GetWindowRect(m_hwnd, &m_origWindowRect)) 	{ 		SecureZeroMemory(&m_origWindowRect, sizeof(m_origWindowRect)); 		return false; 	}  	if (!SetWindowPos(m_hwnd, HWND_TOPMOST,  					   mi.rcMonitor.left, 					   mi.rcMonitor.top, 					   mi.rcMonitor.right - mi.rcMonitor.left, 					   mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_SHOWWINDOW)) 		return false;  	m_fullScreen = true; 	 	return true; } 
Оптимизация

Во второй версии утилиты поллинг активного окна был заменен хуком сообщений WM_ACTIVATE и WM_LBUTTONDOWN. Для этого я использовал два типа хуков: WH_CALLWNDPROC и WH_MOUSE. Суть в том, что мы отслеживаем требуемые события игрового окна и уведомляем нашу утилиту через окно-сервер. Хук вешался только для процесса игры. Таким образом, игра должна быть запущена до утилиты:

BOOL SetWinHook(HWND hWnd, DWORD threadId) { 	if (g_hWndSrv != NULL) 		return FALSE; //already hooked 	 	g_hCallWndHook = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC)CallWndHookProc, g_hInst, threadId); 	if (g_hCallWndHook != NULL) 	{  		g_hMouseHook = SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseHookProc, g_hInst, threadId); 		if (g_hMouseHook != NULL) 		{ 			g_hWndSrv = hWnd; 			return TRUE; 		} 		ClearWinHook(); 	}  	return FALSE; } 

А основной цикл работы свелся к следующей процедуре:

LRESULT CALLBACK Controller::MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { 	if (uMsg == WM_ACTIVATE)      {  		switch (wParam) 		{ 		case WA_ACTIVE: 			DEBUG_TRACE("WA_ACTIVE"); 			gs_ActivateClip = true; 			break; 		case WA_CLICKACTIVE: 			DEBUG_TRACE("WA_CLICKACTIVE"); 			gs_ActivateClip = true; 			break; 		case WA_INACTIVE: 			DEBUG_TRACE("WA_INACTIVE"); 			gs_ActivateClip = false; 			if (g_ControllerPtr->ClipCursorHelper().IsClipped()) 			{ 				if (g_ControllerPtr->FullScreenHelper().Leave()) 				{ DEBUG_TRACE("LeaveFullscreen success"); } 				else 				{ DEBUG_TRACE("LeaveFullscreen failed"); }  				g_ControllerPtr->ClipCursorHelper().UnClip(); 				DEBUG_TRACE("UnClip"); 			} 			break; 		} 		return 0; 	} 	else if (uMsg == WM_LBUTTONDOWN) 	{ 		DEBUG_TRACE("WM_LBUTTONDOWN"); 		 		if (!gs_ActivateClip) 			return 0;  		if (g_ControllerPtr->ClipCursorHelper().IsClipped()) 			return 0;  		if (g_ControllerPtr->FullScreenHelper().Enter())  		{	 			DEBUG_TRACE("EnterFullscreen success");  			g_ControllerPtr->ClipCursorHelper().Clip(); 			DEBUG_TRACE("Clip"); 		} 		else 		{	DEBUG_TRACE("EnterFullscreen failed"); } 		 		return 0; 	} 	     return DefWindowProc(hwnd, uMsg, wParam, lParam);  } 

Используемые вспомогательные классы те же, что и в первой версии. Данная функция является оконной процедурой окна-сервера утилиты. Для захвата курсора и перехода в полный экран необходимо активировать окно и кликнуть левой кнопкой по клиентской области. Когда окно перестает быть активным, то оно восстанавливается до исходных размеров и положения, а курсор больше не удерживается в нем.

Послесловие

Была разработана утилита, призванная сделать процесс стрима любимой игры более комфортным, чем предлагаемый рабочий вариант «из коробки». Буду рад, если кто-то почерпнет для себя что-то интересное. Весь исходный код залит на github WinClipCursor.

ссылка на оригинал статьи http://habrahabr.ru/post/194782/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *