开发端游的同学都知道,鼠标按在窗口标题栏上进行拖动,游戏就会卡住不能更新不能渲染。
还有另外一种情况也会导致游戏卡住,就是弹出窗口的系统菜单时,例如下图:
“弹出系统菜单导致游戏卡住”,这种情况比较容易解决,我先讨论这种情况。
有两种操作会弹出系统菜单:
1,鼠标左击标题栏左侧的图标
2,鼠标右击标题栏
当用户发生上面两种操作时,我们拦截掉Windows消息,不要通知给默认的消息处理函数即可。
LRESULT CALLBACK WindowMsgProcess(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SYSCOMMAND:
{
switch (wParam & 0xfff0)
{
case SC_KEYMENU:
case SC_MOUSEMENU:
//拦截第一种操作,不要通知给默认的消息处理函数
return 0;
}
}
break;
case WM_NCRBUTTONDOWN:
{
//拦截第二种操作,不要通知给默认的消息处理函数
return 0;
}
break;
}
//执行默认的消息处理函数
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
接下来讨论“鼠标拖动标题栏导致游戏卡住”。
当鼠标在标题栏上按住不放时,系统会发送 WM_SYSCOMMAND 消息,携带的参数为 SC_MOVE ,当DefWindowProc收到 SC_MOVE 后,会发送 WM_ENTERSIZEMOVE ,这个时候整个消息循环就会卡止,直到DefWindowProc处理完成返回。
解决办法也是拦截相应的Windows消息,不要通知给默认的消息处理函数。仅仅拦截消息还不够,用户的目的是移动窗口,我们要写代码跟随鼠标的移动来移动窗口;否则的话,用户无论怎样拖动标题栏,窗口都一动不动。
定义下面几个全局变量,用于我们的“鼠标拖动窗口标题栏,不要阻塞消息循环”逻辑。
一旦 g_bNcLButtonDown 的值为true,表示要跟随鼠标的移动来移动窗口。
bool g_bNcLButtonDown = false;
POINT g_kLastMousePos;
POINT g_kCurMousePos;
RECT g_kCurWindowRect;
响应Windows消息,来更改 g_bNcLButtonDown 的值
LRESULT CALLBACK WindowMsgProcess(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SYSCOMMAND:
{
switch (wParam & 0xfff0)
{
case SC_MOVE:
{
if (g_bNcLButtonDown == false)
{
//鼠标开始拖动标题栏,我们的逻辑开始执行
g_bNcLButtonDown = true;
::GetCursorPos(&g_kLastMousePos);
::GetWindowRect(hWnd, &g_kCurWindowRect);
}
//拦截,不要通知给默认的消息处理函数
return 0;
}
break;
}
}
break;
case WM_NCLBUTTONUP:
{
//在窗口的非客户区(主要是标题栏),鼠标抬起了,表示不再拖动窗口,我们的逻辑停止执行
//不要拦截,交给DefWindowProc去处理。
g_bNcLButtonDown = false;
} break;
}
//执行默认的消息处理函数
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
在游戏循环里面,做“跟随鼠标的移动来移动窗口”的逻辑
//“鼠标拖动游戏窗口,不要卡住游戏循环”逻辑。
if (g_bNcLButtonDown_NoBlockLoop)
{
//有时候客户端会卡顿,一旦不能收到窗口消息,g_bNcLButtonDown_NoBlockLoop可能会一直为true,导致bug。
//解决办法是,这里总是判断鼠标左键是否按下。从系统层面获取鼠标左键是否按下。
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
{
::GetCursorPos(&g_kCurMousePos_NoBlockLoop);
const int nDeltaX = g_kCurMousePos_NoBlockLoop.x - g_kLastMousePos_NoBlockLoop.x;
const int nDeltaY = g_kCurMousePos_NoBlockLoop.y - g_kLastMousePos_NoBlockLoop.y;
if (nDeltaX < -1 || nDeltaX > 1 || nDeltaY < -1 || nDeltaY > 1)
{
g_kCurWindowRect_NoBlockLoop.left += nDeltaX;
g_kCurWindowRect_NoBlockLoop.top += nDeltaY;
g_kLastMousePos_NoBlockLoop = g_kCurMousePos_NoBlockLoop;
//执行移动操作的时候不要更改窗口size。
::SetWindowPos(g_hWnd_NoBlockLoop, HWND_NOTOPMOST, g_kCurWindowRect_NoBlockLoop.left, g_kCurWindowRect_NoBlockLoop.top, g_kCurWindowRect_NoBlockLoop.right, g_kCurWindowRect_NoBlockLoop.bottom, SWP_NOSIZE);
}
}
else
{
g_bNcLButtonDown_NoBlockLoop = false;
}
}
至此,大功告成。
-----------------------------------------------------------------------------
回复留言
回复supercolin:我最近在做“渲染模块和更新模块分拆成两个线程”的工作,感悟到你的说法不对。鼠标拖动游戏窗口,消息循环就会卡住,虽然渲染线程不会卡住,但是渲染数据都是旧的,渲染出来的画面都是过时的画面;当鼠标不再拖动,更新模块会执行一个时间跨度比较大的更新,渲染出来的画面会“瞬移”,“抖动”一下。