在C#中记忆和重播击键:同步问题

问题描述 投票:3回答:1

我是个人电脑游戏中的速度奔跑者(喜欢以最快的方式完成游戏的人,我想在比赛中记录自己的输入,以便以后自动重播一次比赛。因此,我创建了一个C#程序来执行此操作:基本上,它启动了一个计时器,每次按下/释放键时,它会保存操作(键上/键下),键以及执行该操作的毫秒时间。然后,当我想再次播放它时,它会启动一个计时器,并在发生击键的毫秒级时,将其重现。

并且有效!嗯...实际上,它[[nearly起作用:密钥被很好地复制,但是有时它们有些不同,导致我之前成功的意外死亡。

以下是显示问题的视频:https://www.youtube.com/watch?v=4RPkcx68hpw&feature=youtu.be

顶部的视频是复制的按键,底部的视频是原始播放的视频。一切似乎都差不多,直到第三个房间,原始剧本击中了“蜘蛛”并使其转回,而重现的琴键却没有碰到它,所以它分散了其余的进度。当然,游戏的这一部分是100%确定性的,因此相同的输入将导致相同的结果。通过在我的视频编辑器中逐帧推进视频,当角色爬到第一个箱子上时,我清楚地看到了2帧的间隙,并且该间隙继续增大。

这是我的(已被大量评论的)代码:

KeysSaver.cs,保存我的输入的类

class KeysSaver { public static IntPtr KEYUP = (IntPtr)0x0101; // Code of the "key up" signal public static IntPtr KEYDOWN = (IntPtr)0x0100; // Code of the "key down" signal private Stopwatch watch; // Timer used to trace at which millisecond each key have been pressed private Dictionary<long, Dictionary<Keys, IntPtr>> savedKeys; // Recorded keys activity, indexed by the millisecond the have been pressed. The activity is indexed by the concerned key ("Keys" type) and is associated with the activity code (0x0101 for "key up", 0x0100 for "key down"). private IntPtr hookId; // Hook used to listen to the keyboard private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); // Imported type : LowLevelKeyboardProc. Now we can use this type. /* * Constructor */ public KeysSaver() { this.savedKeys = new Dictionary<long, Dictionary<Keys, IntPtr>>(); this.watch = new Stopwatch(); } /* * method Start() * Description : starts to save the keyboard inputs. * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms644990%28v=vs.85%29.aspx */ public void Start() { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) // Get the actual thread { // Installs a hook to the keyboard (the "13" params means "keyboard", see the link above for the codes), by saying "Hey, I want the function 'onActivity' being called at each activity. You can find this function in the actual thread (GetModuleHandle(curModule.ModuleName)), and you listen to the keyboard activity of ALL the treads (code : 0) this.hookId = SetWindowsHookEx(13, onActivity, GetModuleHandle(curModule.ModuleName), 0); } this.watch.Start(); // Starts the timer } /* * method Stop() * Description : stops to save the keyboard inputs. * Returns : the recorded keys activity since Start(). */ public Dictionary<long, Dictionary<Keys, IntPtr>> Stop() { this.watch.Stop(); // Stops the timer UnhookWindowsHookEx(this.hookId); //Uninstalls the hook of the keyboard (the one we installed in Start()) return this.savedKeys; } /* * method onActivity() * Description : function called each time there is a keyboard activity (key up of key down). Saves the detected activity and the time at the moment it have been done. * @nCode : Validity code. If >= 0, we can use the information, otherwise we have to let it. * @wParam : Activity that have been detected (keyup or keydown). Must be compared to KeysSaver.KEYUP and KeysSaver.KEYDOWN to see what activity it is. * @lParam : (once read and casted) Key of the keyboard that have been triggered. * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms644985%28v=vs.85%29.aspx (for this function documentation) * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms644974%28v=vs.85%29.aspx (for CallNextHookEx documentation) */ private IntPtr onActivity(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) //We check the validity of the informations. If >= 0, we can use them. { long time = this.watch.ElapsedMilliseconds; //Number of milliseconds elapsed since we called the Start() method int vkCode = Marshal.ReadInt32(lParam); //We read the value associated with the pointer (?) Keys key = (Keys)vkCode; //We convert the int to the Keys type if (!this.savedKeys.ContainsKey(time)) { // If no key activity have been detected for this millisecond yet, we create the entry in the savedKeys Dictionnary this.savedKeys.Add(time, new Dictionary<Keys, IntPtr>()); } this.savedKeys[time].Add(key, wParam); //Saves the key and the activity } return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); //Bubbles the informations for others applications using similar hooks } // Importation of native libraries [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); }

KeysPlayer.cs,用于模拟键事件的那个。

class KeysPlayer { private Dictionary<long, Dictionary<Keys, IntPtr>> keysToPlay; // Keys to play, with the timing. See KeysSaver.savedKeys for more informations. private Dictionary<long, INPUT[]> playedKeys; // The inputs that will be played. This is a "translation" of keysToPlay, transforming Keys into Inputs. private Stopwatch watch; // Timer used to respect the strokes timing. private long currentFrame; // While playing, keeps the last keysToPlay frame that have been played. /* * Constructor */ public KeysPlayer(Dictionary<long, Dictionary<Keys, IntPtr>> keysToPlay) { this.keysToPlay = keysToPlay; this.playedKeys = new Dictionary<long, INPUT[]>(); this.watch = new Stopwatch(); this.currentFrame = 0; this.loadPlayedKeys(); //Load the keys that will be played. } /* * method Start() * Description : starts to play the keyboard inputs. */ public void Start() { this.currentFrame = 0; //currentFrame is 0 at the beginning. this.watch.Reset(); //Resets the timer this.watch.Start(); //Starts the timer (yeah, pretty obvious) IEnumerator<long> enumerator = this.playedKeys.Keys.GetEnumerator(); //The playedKeys enumerator. Used to jump from one frame to another. long t; //Will receive the elapsed milliseconds, to track desync. while (enumerator.MoveNext()) //Moves the pointer of the playedKeys dictionnary to the next entry (so, to the next frame). { Thread.Sleep((int)(enumerator.Current - this.currentFrame - 1)); //The thread sleeps until the millisecond before the next frame. For exemple, if there is an input at the 42th millisecond, the thread will sleep to the 41st millisecond. Seems optionnal, since we have a "while" that waits, but it allows to consume less ressources. Also, in a too long "while", the processor tends to "forget" the thread for a long time, resulting in desyncs. while (this.watch.ElapsedMilliseconds < enumerator.Current) { } //We wait until the very precise millisecond that we want t = this.watch.ElapsedMilliseconds; //We save the actual millisecond uint err = SendInput((UInt32)this.playedKeys[enumerator.Current].Length, this.playedKeys[enumerator.Current], Marshal.SizeOf(typeof(INPUT))); //Simulate the inputs of the actual frame if (t != enumerator.Current) // We compare the saved time with the supposed millisecond. If they are different, we have a desync, so we log some infos to track the bug. { Console.WriteLine("DESYNC : " + t + "/" + enumerator.Current + " - Inputs : " + err); } this.currentFrame = enumerator.Current; //Updates the currentFrame to the frame we just played. } } /* * method Stop() * Description : stops to play the keyboard inputs. */ public void Stop() { this.watch.Stop(); //Stops the timer. } /* * method loadPlayedKeys() * Description : Transforms the keysToPlay dictionnary into a sequence of inputs. Also, pre-load the inputs we need (loading takes a bit of time that could lead to desyncs). */ private void loadPlayedKeys() { foreach (KeyValuePair<long, Dictionary<Keys, IntPtr>> kvp in this.keysToPlay) { List<INPUT> inputs = new List<INPUT>(); //For each recorded frame, creates a list of inputs foreach (KeyValuePair<Keys, IntPtr> kvp2 in kvp.Value) { inputs.Add(this.loadKey(kvp2.Key, this.intPtrToFlags(kvp2.Value))); //Load the key that will be played and adds it to the list. } this.playedKeys.Add(kvp.Key, inputs.ToArray());//Transforms the list into an array and adds it to the playedKeys "partition". } } /* * method intPtrToFlags() * Description : Translate the IntPtr which references the activity (keydown/keyup) into input flags. */ private UInt32 intPtrToFlags(IntPtr activity) { if (activity == KeysSaver.KEYDOWN) //Todo : extended keys { return 0; } if (activity == KeysSaver.KEYUP) { return 0x0002; } return 0; } /* * method loadKey() * Description : Transforms the Key into a sendable input (using the above structures). */ private INPUT loadKey(Keys key, UInt32 flags) { return new INPUT { Type = 1, //1 = "this is a keyboad event" Data = { Keyboard = new KEYBDINPUT { KeyCode = (UInt16)key, Scan = 0, Flags = flags, Time = 0, ExtraInfo = IntPtr.Zero } } }; } // Importation of native libraries [DllImport("user32.dll", SetLastError = true)] public static extern UInt32 SendInput(UInt32 numberOfInputs, INPUT[] inputs, Int32 sizeOfInputStructure); [DllImport("kernel32.dll")] static extern uint GetLastError(); } }

[SendInput使用的所有结构(从InputSimulator脚本复制的结构):

/* * Struct MOUSEINPUT * Mouse internal input struct * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms646273(v=vs.85).aspx */ internal struct MOUSEINPUT { public Int32 X; public Int32 Y; public UInt32 MouseData; public UInt32 Flags; public UInt32 Time; public IntPtr ExtraInfo; } /* * Struct HARDWAREINPUT * Hardware internal input struct * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms646269(v=vs.85).aspx */ internal struct HARDWAREINPUT { public UInt32 Msg; public UInt16 ParamL; public UInt16 ParamH; } /* * Struct KEYBDINPUT * Keyboard internal input struct (Yes, actually only this one is used, but we need the 2 others to properly send inputs) * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms646271(v=vs.85).aspx */ internal struct KEYBDINPUT { public UInt16 KeyCode; //The keycode of the triggered key. See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx public UInt16 Scan; //Unicode character in some keys (when flags are saying "hey, this is unicode"). Ununsed in our case. public UInt32 Flags; //Type of action (keyup or keydown). Specifies too if the key is a "special" key. public UInt32 Time; //Timestamp of the event. Ununsed in our case. public IntPtr ExtraInfo; //Extra information (yeah, it wasn't that hard to guess). Ununsed in our case. } /* * Struct MOUSEKEYBDHARDWAREINPUT * Union struct for key sending * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms646270%28v=vs.85%29.aspx */ [StructLayout(LayoutKind.Explicit)] internal struct MOUSEKEYBDHARDWAREINPUT { [FieldOffset(0)] public MOUSEINPUT Mouse; [FieldOffset(0)] public KEYBDINPUT Keyboard; [FieldOffset(0)] public HARDWAREINPUT Hardware; } /* * Struct INPUT * Input internal struct for key sending * See : https://msdn.microsoft.com/en-us/library/windows/desktop/ms646270%28v=vs.85%29.aspx */ internal struct INPUT { public UInt32 Type; //Type of the input (0 = Mouse, 1 = Keyboard, 2 = Hardware) public MOUSEKEYBDHARDWAREINPUT Data; //The union of "Mouse/Keyboard/Hardware". Only one is read, depending of the type. }

以及我的主要形式:

public partial class Taslagrad : Form { private KeysSaver k; private KeysPlayer p; //Initialisation public Taslagrad() { InitializeComponent(); this.k = new KeysSaver(); } /* * method launchRecording() * Description : Starts to record the keys. Called when the "record" button is triggered. */ private void launchRecording(object sender, EventArgs e) { this.k.Start(); //Starts to save the keys startButton.Text = "Stop"; //Updates the button startButton.Click -= launchRecording; startButton.Click += stopRecording; } /* * method stopRecording() * Description : Stops to record the keys and logs the recorded keys in the console. Called when the "record" button is triggered. */ private void stopRecording(object sender, EventArgs e) { startButton.Text = "Record";//Updates the button startButton.Click += launchRecording; startButton.Click -= stopRecording; Dictionary<long, Dictionary<Keys, IntPtr>> keys = this.k.Stop(); //Gets the recorded keys foreach (KeyValuePair<long, Dictionary<Keys, IntPtr>> kvp in keys) { foreach (KeyValuePair<Keys, IntPtr> kvp2 in kvp.Value) { //Displays the recorded keys in the console if (kvp2.Value == KeysSaver.KEYDOWN) { Console.WriteLine(kvp.Key + " : (down)" + kvp2.Key); } if (kvp2.Value == KeysSaver.KEYUP) { Console.WriteLine(kvp.Key + " : (up)" + kvp2.Key); } } } this.p = new KeysPlayer(keys); //Creates a new player and gives it the recorded keys. } /* * method launchPlaying() * Description : Starts to play the keys. Called when the "play" button is triggered. */ private void launchPlaying(object sender, EventArgs e) { this.p.Start(); //Starts to play the keys. } }

当然,我所有的调试似乎都可以正常工作:记录器保存所有输入(我通过输入长文本进行测试),当我比较记录键和播放键的毫秒数时,我没有差异...

所以我的录制/播放方式有问题吗?秒表不够精确吗?有没有更精确/有效的方法?

c# keyboard-events keystroke keyboard-hook recorder
1个回答
0
投票
我碰巧欣赏MetalFoxDoS的工作。乍一看,它的边缘有些粗糙,但是工作的要旨在那里。

诚实的答案是肯定的,如果用户提供输入有延迟,他的精度受到限制,并且运行CPU的百分比要比NO-OP所需的要多。在采用异步方法的情况下解决该问题,使我获得了微秒级的精度-远远超出了人类所能提供的精度。

我继续改进这项工作,以尝试进一步完善它,因为我意识到有必要根据按键事件之间的延迟来创建“框架”。我还纠正了过去几年的差距所要求的一些错误/更改。

https://github.com/houseofcat/WinKeyRecorder

© www.soinside.com 2019 - 2024. All rights reserved.