一、输入模块结构
游戏输入模块包括对鼠标和键盘两种输入设备的处理,此模块主要为游戏框架提供鼠标操作和键盘操作能力,这些功能均以事件形式提供从而达到模块间消息传递的目的。
模块间的消息传递结构图:
①、CGame(派生)对象订阅CKeyboard事件(如keydown、keyup等)。
②、CGame(派生)对象订阅CMouse事件(如mousemove、mousedown、mouseaway等)。
③、新的键盘消息抵达CKeyboard对象。
④、新的鼠标消息抵达CMouse对象。
⑤、CKeyboard或CMouse对象把相应的通知送到已订阅对象,这些对象按期望的方式处理键盘或鼠标的消息。
二、输入模块实现
为了
能
在控制台实现鼠标和键盘的操作,首先引入几个Win32 API,供游戏输入类所用。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
函数原型:HWND FindWindow(LPCSTR lpClassName,LPCSTR lpWindowName)
函数描述:寻找窗口列表中第一个符合指定条件的顶级窗口;
参数信息:lpClassName指向包含了窗口类名的空中止(C语言)字串的指针;或设为零,表示接收任何类;
lpWindowName指向包含了窗口文本(或标签)的空中止(C语言)字串的指针;或设为零,表示接收任何窗口标题;
返回值: 找到窗口的句柄。如未找到相符窗口,则返回零。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
函数原型:SHORT GetAsyncKeyState(int vKey);
函数描述:判断函数调用时指定虚拟键的状态;
参数信息:vKey欲测试的虚拟键的键码;
返回值: 自对GetAsyncKeyState函数的上一次调用以来,如键已被按过,则位0设为1;否则设为0。如键目前处于按下状态,
则位15设为1;如抬起,则为0。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
函数原型:BOOL GetCursorPos(LPPOINT lpPoint);
函数描述:该函数检取光标的位置,以屏幕坐标表示;
参数信息:POINT结构指针,该结构接收光标的屏幕坐标;
返回值: 非零表示成功,零表示失败。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
函数原型:BOOL ScreenToClient(HWND hWnd, LPPOINT lpPoint);
函数描述:该函数把屏幕上指定点的屏幕坐标转换成用户坐标;
参数信息:hwnd一个窗口的句柄,该窗口定义了要使用的客户区坐标系统;
lpPoint屏幕坐标系统中包含了屏幕点的结构;
返回值: 非零表示成功,零表示失败。
//////////////////////////////////////////////////////////////////////////////////////////////////////////
///CInput类实现
- using System;
- using System.Runtime.InteropServices;
- using CGraphics;
-
- namespace CEngine
- {
- internal abstract class CInput
- {
- internal const Int32 KEY_STATE = 0x8000;
-
-
-
-
-
- [DllImport("User32.dll")]
- protected static extern Int16 GetAsyncKeyState(System.Int32 vKey);
-
-
-
-
-
- [DllImport("User32.dll")]
- protected static extern Boolean GetCursorPos(out CPoint lpPoint);
-
-
-
-
-
-
- [DllImport("User32.dll")]
- protected static extern Int16 ScreenToClient(IntPtr hwnd, out CPoint lpPoint);
- }
- }
以上引用了以后讲解到的游戏渲染模块的相关结构CPoint,是一个表示位置的结构,既然这里要用到,就提前把它也写到这里。
///CPoint结构实现
- using System;
-
- namespace CGraphics
- {
-
-
-
- public struct CPoint
- {
- private Int32 m_x;
- private Int32 m_y;
-
- public CPoint(Int32 x, Int32 y)
- {
- this.m_x = x;
- this.m_y = y;
- }
-
- public Int32 getX()
- {
- return this.m_x;
- }
-
- public Int32 getY()
- {
- return this.m_y;
- }
-
- public void setX(Int32 x)
- {
- this.m_x = x;
- }
-
- public void setY(Int32 y)
- {
- this.m_y = y;
- }
-
- public static Boolean operator ==(CPoint p1, CPoint p2)
- {
- return (p1.m_x == p2.m_x) && (p1.m_y == p2.m_y);
- }
-
- public static Boolean operator !=(CPoint p1, CPoint p2)
- {
- return (p1.m_x != p2.m_x) || (p1.m_y != p2.m_y);
- }
-
- public override bool Equals(object obj)
- {
- return this == (CPoint)obj;
- }
-
- public override int GetHashCode()
- {
- return base.GetHashCode();
- }
-
- public static CPoint operator -(CPoint p1, CPoint p2)
- {
- return new CPoint(p1.getX() - p2.getX(), p1.getY() - p2.getY());
- }
-
- public static CPoint operator +(CPoint p1, CPoint p2)
- {
- return new CPoint(p1.getX() + p2.getX(), p1.getY() + p2.getY());
- }
-
- public override string ToString()
- {
- return string.Format("[{0},{1}]", m_x, m_y);
- }
- }
- }
///CKeyboard类实现
- using System;
-
- namespace CEngine
- {
-
-
-
- public enum CKeys
- {
- A = 0x41,
- Add = 0x6b,
- Alt = 0x40000,
- Apps = 0x5d,
- Attn = 0xf6,
- B = 0x42,
- Back = 8,
- BrowserBack = 0xa6,
- BrowserFavorites = 0xab,
- BrowserForward = 0xa7,
- BrowserHome = 0xac,
- BrowserRefresh = 0xa8,
- BrowserSearch = 170,
- BrowserStop = 0xa9,
- C = 0x43,
- Cancel = 3,
- Capital = 20,
- CapsLock = 20,
- Clear = 12,
- Control = 0x20000,
- ControlKey = 0x11,
- Crsel = 0xf7,
- D = 0x44,
- D0 = 0x30,
- D1 = 0x31,
- D2 = 50,
- D3 = 0x33,
- D4 = 0x34,
- D5 = 0x35,
- D6 = 0x36,
- D7 = 0x37,
- D8 = 0x38,
- D9 = 0x39,
- Decimal = 110,
- Delete = 0x2e,
- Divide = 0x6f,
- Down = 40,
- E = 0x45,
- End = 0x23,
- Enter = 13,
- EraseEof = 0xf9,
- Escape = 0x1b,
- Execute = 0x2b,
- Exsel = 0xf8,
- F = 70,
- F1 = 0x70,
- F10 = 0x79,
- F11 = 0x7a,
- F12 = 0x7b,
- F13 = 0x7c,
- F14 = 0x7d,
- F15 = 0x7e,
- F16 = 0x7f,
- F17 = 0x80,
- F18 = 0x81,
- F19 = 130,
- F2 = 0x71,
- F20 = 0x83,
- F21 = 0x84,
- F22 = 0x85,
- F23 = 0x86,
- F24 = 0x87,
- F3 = 0x72,
- F4 = 0x73,
- F5 = 0x74,
- F6 = 0x75,
- F7 = 0x76,
- F8 = 0x77,
- F9 = 120,
- FinalMode = 0x18,
- G = 0x47,
- H = 0x48,
- HanguelMode = 0x15,
- HangulMode = 0x15,
- HanjaMode = 0x19,
- Help = 0x2f,
- Home = 0x24,
- I = 0x49,
- IMEAccept = 30,
- IMEAceept = 30,
- IMEConvert = 0x1c,
- IMEModeChange = 0x1f,
- IMENonconvert = 0x1d,
- Insert = 0x2d,
- J = 0x4a,
- JunjaMode = 0x17,
- K = 0x4b,
- KanaMode = 0x15,
- KanjiMode = 0x19,
- KeyCode = 0xffff,
- L = 0x4c,
- LaunchApplication1 = 0xb6,
- LaunchApplication2 = 0xb7,
- LaunchMail = 180,
- LControlKey = 0xa2,
- Left = 0x25,
- LineFeed = 10,
- LMenu = 0xa4,
- LShiftKey = 160,
- LWin = 0x5b,
- M = 0x4d,
- MediaNextTrack = 0xb0,
- MediaPlayPause = 0xb3,
- MediaPreviousTrack = 0xb1,
- MediaStop = 0xb2,
- Menu = 0x12,
- Modifiers = -65536,
- Multiply = 0x6a,
- N = 0x4e,
- Next = 0x22,
- NoName = 0xfc,
- None = 0,
- NumLock = 0x90,
- NumPad0 = 0x60,
- NumPad1 = 0x61,
- NumPad2 = 0x62,
- NumPad3 = 0x63,
- NumPad4 = 100,
- NumPad5 = 0x65,
- NumPad6 = 0x66,
- NumPad7 = 0x67,
- NumPad8 = 0x68,
- NumPad9 = 0x69,
- O = 0x4f,
- Oem1 = 0xba,
- Oem102 = 0xe2,
- Oem2 = 0xbf,
- Oem3 = 0xc0,
- Oem4 = 0xdb,
- Oem5 = 220,
- Oem6 = 0xdd,
- Oem7 = 0xde,
- Oem8 = 0xdf,
- OemBackslash = 0xe2,
- OemClear = 0xfe,
- OemCloseBrackets = 0xdd,
- Oemcomma = 0xbc,
- OemMinus = 0xbd,
- OemOpenBrackets = 0xdb,
- OemPeriod = 190,
- OemPipe = 220,
- Oemplus = 0xbb,
- OemQuestion = 0xbf,
- OemQuotes = 0xde,
- OemSemicolon = 0xba,
- Oemtilde = 0xc0,
- P = 80,
- Pa1 = 0xfd,
- Packet = 0xe7,
- PageDown = 0x22,
- PageUp = 0x21,
- Pause = 0x13,
- Play = 250,
- Print = 0x2a,
- PrintScreen = 0x2c,
- Prior = 0x21,
- ProcessKey = 0xe5,
- Q = 0x51,
- R = 0x52,
- RControlKey = 0xa3,
- Return = 13,
- Right = 0x27,
- RMenu = 0xa5,
- RShiftKey = 0xa1,
- RWin = 0x5c,
- S = 0x53,
- Scroll = 0x91,
- Select = 0x29,
- SelectMedia = 0xb5,
- Separator = 0x6c,
- Shift = 0x10000,
- ShiftKey = 0x10,
- Sleep = 0x5f,
- Snapshot = 0x2c,
- Space = 0x20,
- Subtract = 0x6d,
- T = 0x54,
- Tab = 9,
- U = 0x55,
- Up = 0x26,
- V = 0x56,
- VolumeDown = 0xae,
- VolumeMute = 0xad,
- VolumeUp = 0xaf,
- W = 0x57,
- X = 0x58,
- XButton1 = 5,
- XButton2 = 6,
- Y = 0x59,
- Z = 90,
- Zoom = 0xfb
- }
-
-
-
-
- internal sealed class CKeyboard : CInput
- {
-
-
-
-
-
- internal delegate void CKeyboardHandler(TEventArgs e);
-
-
-
- private event CKeyboardHandler m_keyDown;
-
-
-
- private event CKeyboardHandler m_keyUp;
-
-
-
- private CKeys m_oldKey=CKeys.None;
-
-
-
-
- public CKeyboard()
- {
-
- }
-
-
-
-
-
-
- private Boolean isKeyDown(CKeys vKey)
- {
- return 0 != (GetAsyncKeyState((Int32)vKey) & KEY_STATE);
- }
-
-
-
-
-
- private CKeys getCurKeyboardDownKey()
- {
- CKeys vKye = CKeys.None;
- foreach (Int32 key in Enum.GetValues(typeof(CKeys)))
- {
- if (isKeyDown((CKeys)key))
- {
- vKye = (CKeys)key;
- break;
- }
- }
- return vKye;
- }
-
-
-
-
-
- private void onKeyDown(CKeyboardEventArgs e)
- {
- CKeyboardHandler temp = m_keyDown;
- if (temp != null)
- {
- temp.Invoke(e);
- }
- }
-
-
-
-
-
- private void onKeyUp(CKeyboardEventArgs e)
- {
- CKeyboardHandler temp = m_keyUp;
- if (temp != null)
- {
- temp.Invoke(e);
- }
- }
-
-
-
-
-
- public void addKeyDownEvent(CKeyboardHandler func)
- {
- m_keyDown += func;
- }
-
-
-
-
-
- public void addKeyUpEvent(CKeyboardHandler func)
- {
- this.m_keyUp += func;
- }
-
-
-
-
- public void keyboardEventsHandler()
- {
- CKeyboardEventArgs e;
- CKeys vKeyDown = getCurKeyboardDownKey();
-
- if (vKeyDown != CKeys.None)
- {
- this.m_oldKey = vKeyDown;
- e = new CKeyboardEventArgs(vKeyDown);
- this.onKeyDown(e);
- }
- else if (m_oldKey != CKeys.None && !isKeyDown(this.m_oldKey))
- {
- e = new CKeyboardEventArgs(this.m_oldKey);
- this.onKeyUp(e);
- this.m_oldKey = CKeys.None;
- }
- }
- }
- }
很明显,键盘类事件委托用到一个CKeyboardEventArgs类型事件参数,CKeyboardEventArgs类定义为:
///CKeyboardEventArgs类实现
- using System;
- namespace CEngine
- {
- public sealed class CKeyboardEventArgs : EventArgs
- {
- private CKeys m_keys;
-
- public CKeyboardEventArgs(CKeys keys)
- {
- this.m_keys = keys;
- }
-
- public CKeys getKey()
- {
- return m_keys;
- }
- }
- }
CKeyboard提供了addKeyDownEvent和addKeyUpEvent函数,用于其他对象订阅键盘事件,keyboardEventsHandler捕获键盘消息并响应相应的事件,把通知发送给订阅事件的对象,这样订阅者就可以按照它的期望处理相应的事件行为了。
了解了键盘类的实现,下面再看看鼠标类,和键盘类雷同,鼠标类也为其他对象提供几个鼠标事件,以供其他对象订阅,并自行处理鼠标事件。
///CMouse类实现
- using System;
- using CGraphics;
-
- namespace CEngine
- {
-
-
-
- [Flags]
- public enum CMouseButtons
- {
- Left = 0x01,
- Middle = 0x04,
- None = 0,
- Right = 0x02
- }
-
-
-
-
- internal sealed class CMouse : CInput
- {
-
-
-
-
-
- internal delegate void CMouseHandler(TEventArgs e);
-
-
-
- private event CMouseHandler m_mouseMove;
-
-
-
- private event CMouseHandler m_mouseAway;
-
-
-
- private event CMouseHandler m_mouseDwon;
-
-
-
-
- private readonly Int32 MAX_X = 639;
-
-
-
- private readonly Int32 MAX_Y = 400;
-
-
-
-
- private IntPtr m_hwnd = IntPtr.Zero;
-
-
-
- private CPoint m_oldPoint;
-
-
-
- private Boolean m_leave;
-
-
-
-
- public CMouse(IntPtr hwnd)
- {
- this.m_hwnd = hwnd;
- this.m_oldPoint = new CPoint(0, 0);
- this.m_leave = false;
-
- this.MAX_X = (Console.WindowWidth <<3) - 1;
- this.MAX_Y = Console.WindowHeight <<4;
- }
-
- #region 鼠标函数
-
-
-
-
-
-
- private Boolean isMouseDown(CMouseButtons vKey)
- {
- return 0 != (GetAsyncKeyState((Int32)vKey) & KEY_STATE);
- }
-
-
-
-
-
- private CMouseButtons getCurMouseDownKeys()
- {
- CMouseButtons vKey = CMouseButtons.None;
- foreach (Int32 key in Enum.GetValues(typeof(CMouseButtons)))
- {
- if (isMouseDown((CMouseButtons)key))
- {
-
- vKey |= (CMouseButtons)key;
- }
- }
- return vKey;
- }
-
-
-
-
-
- private Int32 getMouseX()
- {
- return getMousePoint().getX();
- }
-
-
-
-
-
- private Int32 getMouseY()
- {
- return getMousePoint().getY();
- }
-
-
-
-
-
- private Boolean isLeave()
- {
- return m_leave;
- }
-
-
-
-
-
- private CPoint getMousePoint()
- {
- CPoint point;
-
- if (GetCursorPos(out point))
- {
- if (m_hwnd != IntPtr.Zero)
- {
-
- ScreenToClient(m_hwnd, out point);
-
- if ((point.getX() >= 0 && point.getX() <= MAX_X)
- && point.getY() >= 0 && point.getY() <= MAX_Y)
- {
- this.m_oldPoint = point;
- this.m_leave = false;
- }
- else
- {
- m_leave = true;
- }
- }
- }
- return m_oldPoint;
- }
-
- #endregion
-
- #region 鼠标事件
-
-
-
-
-
- private void onMouseMove(CMouseEventArgs e)
- {
- CMouseHandler temp = m_mouseMove;
- if (temp != null)
- {
- temp.Invoke(e);
- }
- }
-
-
-
-
-
- private void onMouseAway(CMouseEventArgs e)
- {
- CMouseHandler temp = m_mouseAway;
- if (temp != null)
- {
- temp.Invoke(e);
- }
- }
-
-
-
-
-
- private void onMouseDown(CMouseEventArgs e)
- {
- CMouseHandler temp = m_mouseDwon;
- if (temp != null)
- {
- temp.Invoke(e);
- }
- }
-
-
-
-
-
- public void addMouseMoveEvent(CMouseHandler func)
- {
- m_mouseMove += func;
- }
-
-
-
-
-
- public void addMouseAwayEvent(CMouseHandler func)
- {
- m_mouseAway += func;
- }
-
-
-
-
-
- public void addMouseDownEvent(CMouseHandler func)
- {
- m_mouseDwon += func;
- }
-
-
-
-
- public void mouseEventsHandler()
- {
- CMouseEventArgs e;
-
- CPoint point = getMousePoint();
-
- CMouseButtons vKey = getCurMouseDownKeys();
- if (!isLeave())
- {
- if (vKey != CMouseButtons.None)
- {
- e = new CMouseEventArgs(point.getX(), point.getY(), vKey);
- this.onMouseDown(e);
- }
-
- e = new CMouseEventArgs(point.getX(), point.getY(), false);
- this.onMouseMove(e);
- }
- else
- {
- e = new CMouseEventArgs(-1, -1, true);
- this.onMouseAway(e);
- }
- }
-
- #endregion
- }
- }
鼠标类事件委托用到一个CMouseEventArgs类型事件参数,用来传递鼠标当前的位置等信息,CMouseEventArgs类定义为:
///CMouseEventArgs类实现
- using System;
-
- namespace CEngine
- {
-
-
-
- public sealed class CMouseEventArgs : EventArgs
- {
- private Int32 m_x;
- private Int32 m_y;
- private Boolean m_leave;
- private CMouseButtons m_vKey;
-
- public CMouseEventArgs(Int32 x, Int32 y, Boolean leave)
- {
- this.m_x = x;
- this.m_y = y;
- this.m_leave = leave;
- }
-
- public CMouseEventArgs(Int32 x, Int32 y, CMouseButtons key)
- {
- this.m_x = x;
- this.m_y = y;
- this.m_vKey = key;
- }
-
- public Int32 getX()
- {
- return m_x;
- }
-
- public Int32 getY()
- {
- return m_y;
- }
-
- public Boolean isLeave()
- {
- return m_leave;
- }
-
- public CMouseButtons getKey()
- {
- return m_vKey;
- }
-
- public bool containKey(CMouseButtons key)
- {
- return (getKey() & key) == key;
- }
-
- public override string ToString()
- {
- return string.Format("{0},{1}", getX(), getY());
- }
- }
- }
三、完善游戏框架
至此,游戏输入模块基本完成了,应该能满足多数游戏的控制需求,我们把这些功能封装到游戏框架中去,以方便我们控制游戏中的逻辑。回到游戏框架类,进一步完善之:
///CGame类实现
由于篇幅有限,与上章相同部分代码就以“略”来替代,此处增加了游戏输入类的函数gameInput,在游戏循环中监听键盘和鼠标的输入;订阅了鼠标和键盘的相应事件,以虚拟函数形式提供给派生类处理将要发生的事件行为。
四、测试输入模块
修改上章的TestGame类,以测试游戏的输入控制功能。
///TestGame类实现
- using System;
- using CEngine;
-
- namespace Game
- {
- public class TestGame : CGame
- {
- private Boolean m_bkeydown = false;
-
-
-
-
- protected override void gameInit()
- {
-
- setTitle("游戏框架测试");
-
- setUpdateRate(30);
-
- setCursorVisible(false);
-
- Console.WriteLine("游戏初始化成功!");
- }
-
-
-
-
- protected override void gameLoop()
- {
-
- }
-
-
-
-
- protected override void gameExit()
- {
- Console.WriteLine("游戏结束!");
- Console.ReadLine();
- }
-
- protected override void gameKeyDown(CKeyboardEventArgs e)
- {
- if (!m_bkeydown)
- {
- Console.WriteLine("按下键:" + e.getKey());
-
- m_bkeydown = true;
- }
-
- if (e.getKey() == CKeys.Escape)
- {
- setGameOver(true);
- }
- }
-
- protected override void gameKeyUp(CKeyboardEventArgs e)
- {
- Console.WriteLine("释放键:" + e.getKey());
- m_bkeydown = false;
- }
-
- protected override void gameMouseAway(CMouseEventArgs e)
- {
- setTitle("鼠标离开了工作区!");
- }
-
- protected override void gameMouseDown(CMouseEventArgs e)
- {
- if (e.getKey() == CMouseButtons.Left)
- {
- Console.SetCursorPosition(15, 2);
- Console.WriteLine("鼠标工作区坐标:" + e.ToString() + " " + e.getKey().ToString());
- }
- else if (e.getKey() == CMouseButtons.Right)
- {
- Console.SetCursorPosition(15, 3);
- Console.WriteLine("鼠标工作区坐标:" + e.ToString() + " " + e.getKey().ToString());
- }
- else if (e.getKey() == CMouseButtons.Middle)
- {
- Console.SetCursorPosition(15, 4);
- Console.WriteLine("鼠标工作区坐标:" + e.ToString() + " " + e.getKey().ToString());
- }
- }
-
- protected override void gameMouseMove(CMouseEventArgs e)
- {
- setTitle("鼠标回到了工作区!");
- }
- }
- }
测试结果截图:
以上结果为依次按下方向键、ESC键和在工作区内单击鼠标左键、右键和中间键引发相应事件所得到的数据,当鼠标离开工作区范围时,控制台标题将会变成“鼠标离开了工作区”。
五、结语
本章实现了游戏的输入,包括了键盘的输入和鼠标的输入,完善了游戏框架的功能,在下一章,我们将要实现游戏的渲染模块,从而告别这毫无色彩的世界,并将进一步完善我们的游戏框架。