NC MapPad – How I Did it (Part 1)

สวัสดีครับ ไม่ได้แวะมาเขียนอยู่พักใหญ่ๆ เพราะว่าช่วงนี้มีเกม “The Crew” กลายเป็นงานอดิเรกผมไปเลย กำลังเห่อมาก เล่นไปซะจนเลเวลตันละ (Level 50) พอดีเป็นคนบ้าเกมขับรถมาก ขับข้ามรัฐไปข้ามรัฐมากับหลานชาย จน EXP กระจายเลยทีเดียว

เมื่อวันก่อนนี้ผมเห็นในเว็บ Lazada มี Bluetooth Controller ขึ้นมาขายแว๊บๆ (http://www.lazada.co.th/ipega-pg-9023-490492.html) และผมมีโค๊ดลดราคา 100 บาท ที่ต้องซือของราคา 1,000 บาทอยู่ ไม่ได้ใช้ซะที ก็เลยตัดสินใจ จัดมาเสีย 1 ตัว เพราะกำลังคิดอยู่ว่าอยากจะลอง Stream Game ผ่าน In-Home Streaming ของ Steam (อ่านแล้วงงดีนะ) ดูผ่าน Tablet Windows 8 ของผมด้วยว่าจะเป็นอย่างไร แล้วก็ถ้าเอามาใช้เล่นเกมอย่าง Asphalt 8 คงจะเข้าที่ดีเหมือนกัน ซึ่งตัว Controller ผมกำลังทำวีดีโอรีวิวอยุ่ เดี๋ยวจะเอามาให้ชมกันนะครับ

แต่ทีนี้ ปัญหาก็คือเรื่องของ Software ครับ ผมเข้าใจว่า App Windows 8 รองรับ Controller อยู่แล้วนะ ดูเหมือนว่า จะต้องเป็น Controller บางประเภทเท่านั้น ซึ่ง iPEGA 9023 ไม่อยู่ในประเภทพวกนั้น แย่ละสิผม จะเสียเงิน 1,000 บาทไปฟรีๆ เหรอนี่!? ไม่ใช่ผมแน่ครับ! ด้วยความงก และความบ้าคลั่งการเขียนโค๊ดของผม จึงออกมาเป็น App ที่ชื่อ MapPad ถ้าอยากรู้ว่าผมทำได้อย่างไร ลองตามมาดูกันครับ (มี Source Code ใน GitHub นะครับ)

image

GlovePie, x360ce และ DS4 to XInput

จากตอนที่ผมเล่น Mass Effect 1,2 และ 3 ซึ่งเป็นเกม PC ที่ไม่รองรับ Controller อย่างจริงจังมาก แม้ว่าจะมีเสียงกร่นด่าแค่ไหน ทั้ง 3 ภาคก็ไม่ยอมทำให้รองรับ Controller ทำให้ผมต้องเสาะแสวงหาหนทางที่จะเล่นด้วย Controller ให้ได้ (พอดีอยากนั่งเล่นกับจอใหญ่ ที่กองหมอนของผมครับ) ก็เลยทำให้ผมรู้จักกับ GlovePie ซึ่งเป็นโปรแกรมที่ Advance มาก มันสามารถจำลองการกดปุ่มตั้งแต่ Mouse, Keyboard ไปจนถึง GamePad ได้ แถมยังเขียนโปรแกรมคุมได้ตามที่เราต้องการด้วย อย่างตอนเล่น Mass Effect ผมก็เขียน Script ให้มัน Map ตัว Analog ทางขวา ให้เป็นการขยับเมาส์ และปุ่ม Left Trigger เป็นการเล็งปืน โดยทีถ้าเกิดว่า กำลังเล็งปืนอยู่ ให้เมาส์เลื่อนช้าลง 30% เพื่อให้เล็งได้ง่ายขึ้น อะไรแบบนี้

แน่นอนว่า GlovePie จึงเป็นตัวเลือกแรก ที่ผมใช้ในการ Map iPEGA 9023 ของผม เพื่อใช้เล่นเกม แต่ก็พบว่า แต่ละเกม มันก็ต้อง Map ไม่เหมือนกันอีก ทำให้ผมต้องมานั่งแก้ Script ทุกครั้งเวลาเปลี่ยนเกม ก็ไม่ไหวนะแบบนี้

ผมจึงลองค้นหาไปเรื่อยๆ ก็มาเจอกับ x360ce ซึ่งสามารถแปลง GamePad ทั่วไป ที่ Windows จะมองเห็นเป็น “DirectInput” ให้เป็น “XInput” ซึ่งเป็นโหมดการทำงานของจอย Xbox360 ได้ แต่ว่า ถ้าจะใช้งาน เราจะต้องทำการเอา DLL ไปไล่วางตามเกมต่างๆ เพื่อให้ใช้งานได้ ก็ไม่ไหวอีกนั่นแหละ และที่สำคัญก็คือ บางเกมที่ผมจะเล่น ก็ไม่รองรับ Controller ด้วยอีกต่างหาก อย่างเช่นเกม The Swapper ที่ผมทำยังไง มันก็ไม่ยอมให้ผมเล่นด้วย Controller (ทั้งที่มันก็บอกว่า Support)

ผมจึงตัดสินใจว่าทางที่ดีที่สุด เราควรจะ Map ให้ออกมาเป็น Mouse/Keyboard เพื่อให้สามารถเล่นได้ทุกเกม แล้วก็สามารถเอา Controller ไปกดปุ่มเข้าเกมได้ด้วย ผมก็ค้นหาจนมาถึง DS4 to XInput ซึ่งเป็น Open Source ที่ทำการ Map DS4 เป็น Mouse Keyboard หรือ XInput (Xbox360 Controller) https://code.google.com/p/ds4-tool/ เพื่อดูแนวทางว่า เขาทำการ Map ได้อย่างไร จึงทำให้ผมรู้จักกับ API ของ Windows ที่ชื่อ “SendInput” ครับ ก่อนที่เราจะเริ่มการรับ Input จาก Joystick/GamePad ผมจึงต้องทดสอบก่อนว่า มันใช้งานได้หรือไม่

การเรียกใช้งาน SendInput

ในการใช้งาน API SendInput ซึ่งเป็น API ของ Windows และไม่ได้อยู่ใน .NET และก็ไม่ได้มาอยู่ในรูปของไฟล์ .dll ที่เราสามารถ Add Reference… มาได้นั้น เราจะใช้กระบวนการที่เรียกว่า P/Invoke หรือ Platform Invocation โดยการ P/Invoke นี้ เป็นเทคนิคในการเรียกใช้ฟังก์ชั่นจากไฟล์ .dll ที่ไม่ได้ถูกสร้างด้วย .NET ได้ครับ

สำหรับการสร้าง P/Invoke Signature นั้น จะต้องอาศัยความรู้ความเข้าในการทำงานเบื้องหลังของ .NET พอสมควร แค่ต้องอ่านก็รู้สึกปวดหัวแล้วครับ แต่โชคดีที่มีคนทำเว็บไซต์ชื่อว่า http://www.pinvoke.net เอาไว้ให้ (ปัจจุบัน RedGate เข้ามาสนับสนุนแล้ว) เป็นเว็บที่เปิดให้ทุกคนสามารถเข้ามาแชร์โค๊ดในการเรียก API ของ Windows ด้วย P/Invoke ได้

และที่เยี่ยมไปกว่านั้น ก็คือ API SendInput นี้ มีคนทำเป็น NuGet เอาไว้ให้แล้วอีกต่างหาก http://inputsimulator.codeplex.com/ แต่จากการทดสอบ พบว่า ผมไม่สามารถใช้วิธีที่ Input Simulator เขียนไว้ให้กับเกมได้ ผมจึงลองค้นหาเพิ่มอีกหน่อย จึงได้วิธีที่ถูกต้องมา ดังนั้น ผมจึงต้องเริ่มเขียนเรียก SendInput เอง ลองมาดูกันว่า จะต้องทำอย่างไรบ้างกันครับ

การใช้งาน P/Invoke.net

อย่างแรกเลย เราก็หา Add-On มาติดตั้งก่อน สามารถดาวน์โหลดได้จากลิงค์ ของ Visual Studio Gallery ได้ครับ เมื่อติดตั้งแล้ว ก็จะมีเมนูแสดงขึ้นมาใน Visual Studio เพื่อให้เราสามารถหาโค๊ด P/Invoke ที่เราต้องการได้เลย (SendInput นั้นมีอยู่ใน coredll และ user32 ซึ่ง coredll นี้เป็น API ของ PocketPC ครับ - นี่เมื่อ 10 ปีที่แล้วมีความต้องการที่จะจำลอง Input บน Device แล้วหรือนี่!!?)

image

image

จากนั้น จะเห็นว่า เราต้องการ Struct ที่ชื่อ INPUT ด้วย ก็สามารถทำการค้นหาจาก Extension นี้ได้ด้วยเช่นกัน แต่ว่าบางทีก็จะเป็นแบบนี้ครับ คือเราจะต้องเปิดเว็บไป Copy มาเอง

image

แต่ก็ไม่ต้องไป Copy เองให้เสียเวลา เพราะว่า ผมมี Source Code ให้เอาไปใช้งานได้เลยครับ

Union

การเรียกใช้งานนั้นก็ค่อนข้างจะตรงไปตรงมา มีจุดที่น่าสนใจจุดเดียวคือ “InputUnion” ผมเชื่อว่า หลายๆ ท่านอาจจะไม่ทัน Union นะครับ เจ้า Union นี้เป็น Data Structure ชนิดหนึ่ง ที่เอาไว้ใช้ส่งข้อมูล แต่ว่าความแปลกของมันก็คือ เราสามารถตั้งค่า Field ของมันได้ครั้งละ 1 ตัวเท่านั้น (สังเกต FieldOffset เป็น 0 ทั้งสามตัว) เขียนให้เข้าใจง่ายก็คือ InputUnion เหมือนเป็น Base Class ส่วน MouseInput, KeyboardInput และ HardwareInput นั้น เหมือนเป็น Subclass ของ InputUnion อีกทีหนึ่ง หลังจากที่เรากำหนดค่าให้ Union ไปแล้ว ด้วยตัวใดตัวหนึ่ง เราจะสามารถอ่านค่าที่กำหนดไว้ ในรูปของ MouseInput ก็ได้ KeyboardInput ก็ได้ หรือจะเป็น Hardware Input ก็ได้ (Polymorphism!!!) เพราะทั้งสามตัว มันเริ่มอ่านจาก Memory ช่องเดียวกัน แต่ถ้าเราอ่านจาก MouseInput เราก็จะได้ค่าแบบ MouseInput ถึงแม้ว่าตอนเซ็ทค่า เราจะเซ็ทค่าด้วย KeyboardInput ก็ตาม แต่ว่าความหมายที่ได้ อาจจะผิดเพี้ยนไป เพราะ Union ก็ไม่เช็คให้ด้วยว่า จะอ่านมาออกหรือเปล่า

ผมเข้าใจว่า Union นั้น มีไว้เพื่อสื่อความหมายว่า ฟังก์ชั่นนี้ สามารถรับค่าเป็นค่าใดก็ได้ หนึ่งใน 3 ประเภทที่กำหนดนี้ โดยคนที่ออกแบบ ไม่อยากใช้คลาส และไม่อยากจะเขียน Overload ด้วย ไม่ว่าจะด้วยเหตุผลอะไรก็ตาม เนื่องจากผมเริ่มเขียนโปรแกรม ตอนที่ไม่มีการสอนเรื่อง Use Case ของ Union แล้ว ว่าทำไมต้องใช้ นี่คือที่ผมเดาได้นะครับ :)

Scan Code

จากใน Source Code ก็จะเห็นว่า ผมมี Comment เอาไว้แล้วว่า ทำไมผมถึงไม่สามารถใช้งาน NuGet Windows Input Simulator ตรงๆ ได้ นั่นก็เพราะว่า เกมนั้น ใช้วิธีอ่าน Input จาก Keyboard คนละแบบกับโปรแกรมบน Windows ครับ โดยถ้าจะให้การ SendInput ของเรา สามารถใช้งานกับเกมได้ จะต้องส่งค่า Key ด้วย Scan Code ซึ่งเป็นค่า RAW Input ประจำแต่ละปุ่มบน Keyboard ที่ตัว Controller ของ Keyboard เป็นคนส่งมา โดย Microsoft มีการกำหนด Spec ของ Scan Code เอาไว้ด้วย (เผื่อสงสัยว่า OEM_1 คือปุ่มอะไร) ดาวน์โหลดได้จากหน้านี้ครับ Keyboard Scan Code Specification

ถ้าท่านใดที่เคยใช้ API ที่ชื่อ SendKey อาจจะสงสัยว่า แล้วทำไมเราใช้ SendKey ไม่ได้เหรอ? ผมได้ทดสอบแล้ว ก็สามารถใช้งานได้เหมือนกัน (ทดสอบกับ AutoIt) โดยใช้งานได้ง่ายดี ไม่ต้องยุ่งกับ Scan Code ด้วย แต่ปัญหาก็คือ เวลาที่เราเล่นเกม เราต้องการจะกดปุ่มค้างครับ เช่น การกดปุ่ม “W” เพื่อเดินไปข้างหน้า ซึ่งการใช้ SendKey ส่งปุ่ม W ไปบ่อยๆ จะมีค่าเท่ากับการกดปุ่ม W รัวๆ ซึ่งมันจะได้ผลในเกม ต่างกับการกดปุ่ม W ค้างครับ ใน API ของ SendInput ถ้าเราส่งปุ่ม W ไป ในเกมจะมองเห็นว่าปุ่มนั้นโดนกดอยู่ จนกว่าจะมีการส่ง KEYUP ไป Cancel จึงได้ผลอย่างที่เราต้องการครับ

สำหรับการจำลอง Mouse ผมทดสอบ Windows Input Simulator แล้ว สามารถใช้งานได้ดี ผมจึงเลือกใช้ของเขาครับ

การรับ Input จาก Joystick/GamePad

เมื่อเราได้ทดสอบแล้วว่า การใช้ SendInput สามารถใช้งานได้บนเกม และบน Desktop ผมจึงเริ่มมาดูในขั้นต่อไป คือการอ่านค่าจาก GamePad โดยผมรู้อยู่แล้วว่า GamePad ของผมนั้น เป็นแบบ DirectInput ผมก็ต้องเลือกใช้ DirectInput ในการอ่านค่าจากมันครับ

image

สำหรับการใช้ DirectInput ซึ่งก็เป็น API ที่อยู่นอก .NET อีกเช่นกัน เราก็สามารถใช้ P/Invoke ได้ครับ แต่ว่ามี Project Open Source ที่ก็ได้ทำการ Map API ของ DirectX ทั้งหมด ให้อยู่ในโลก .NET เอาไว้เรียบร้อยแล้วอีกเหมือนกัน (นี่ตกลงได้ทำอะไรเองบ้างเนี่ย) แถมยังมีประสิทธิภาพดีมาก เร็วกว่า Managed DirectX ของ Microsoft เองเสียอีก โปรเจคนี้ชื่อว่า SharpDx ครับ

SharpDx นั้น สามารถติดตั้งในทันทีในรูปของ NuGet Package โดยตัวที่เราต้องการคือ SharpDx.DirectInput ครับ

image

สำหรับการอ่านค่าจาก Joystick/GamePad ก็มีการอ่านค่าตรงตัวมาก ซึ่งผมดูตัวอย่างจาก CodeProject มาว่า จะต้องเขียนเรียกอย่างไรบ้าง ขั้นตอนโดยสรุปก็คือ

  • New DirectInput ขึ้นมา
  • ใช้ GetDevices เพื่อหา Joystick/GamePad ที่ต่ออยู่
  • สร้าง Object JoyStick ใหม่ สั่ง Acquire แล้วก็ตั้ง Loop ขึ้นมาอ่านค่าจากข้อมูลที่ Joystick Buffer ไว้ ยิ่งเราอ่านบ่อย ก็จะยิ่งมี Input Lag น้อย แต่ถ้าอ่านบ่อย ก็จะกิน CPU มากไป ผมเลยตั้งไว้ที่ประมาณ 50 ครั้ง/วินาทีครับ

ค่าที่ได้จากคลาส JoyStick จะเป็นค่าการเปลี่ยนแปลงของตัว Joystick/GamePad ครับ โดยปุ่มบน Joystick/GamePad ทั้งหมด จะถูก Normalize ให้อยู่ในรูปของ “Offset” ทั้งที่ปุ่มนั้นเป็นปุ่มแบบ Digital คือมีแค่กด กับปล่อย ไม่มีกดเบาๆ เช่น การกดปุ่ม ผมจะได้ค่าออกมาสองครั้ง คือ “ปุ่มถูกกด” เช่น Buttons0, Value 128 และ”ปุ่มถูกปล่อย” เช่น Buttons0, Value 0 ส่วนการขยับ Analog (Thumbstick) จะได้ค่าออกมาทุกครั้งที่ผมค่อยๆ ขยับตัว Analog ไป เช่น X, Value 0; X, Value 16384; X, Value 32768 เป็นต้น

ดังนั้นเราก็จึงจะต้องออกแบบการ Map ค่าจาก Joystick/GamePad ตามสิ่งที่เราได้รับด้วย ซึ่งผมออกแบบไว้ดังนี้ครับ

  • คลาส GamePadMapper เป็น BaseClass เพื่อให้ระบบการ Map “มองเห็น” ทุกวิธีการ Map เป็นตัวเดียวกัน แต่มีการทำงานคนละวิธี (Polymorphism) เป็นแนวทางแบบ “State/Strategy Pattern” ครับ
  • วิธีการ Map นั้น จากการลองกดเองแล้ว มีความเป็นไปได้อยู่ 3 แบบครับ
    • DigitalButtonToKey - แบบนี้ง่ายสุด คือ กดปุ่มบน Joystick/GamePad แล้วได้ปุ่มบน Keyboard
    • DigitalButtonToMouseButton - คล้านกันกับ ToKey แต่ว่าอันนี้กดปุ่มเมาส์แทน ที่ต้องแยก เพราะว่าเวลาสั่ง SendInput นั้น ใช้คนละวิธีกัน ถ้าเราเอารวมกัน เราจะต้องมานั่ง If ว่า อันนี้เป็น Mouse หรือเป็น Keyboard ครับ
    • AxisToAcceleration - แบบนี้คือการ Map ค่าจากการดัน Analog (Thumbstick) ให้กลายเป็นความเร่ง ดันเยอะ เร่งเยอะ ดันน้อย เร่งน้อย โดยมันจะคำนวณอัตราเร่งออกมา เป็น % (0.0, 0.1, 0.2….0.9, 1) แต่ว่าไม่ได้ทำอะไร เก็บเอาไว้ก่อน
      • AxisToMouseX, AxisToMouseY ก็คือ การแปลอัตราเร่งที่ได้ ออกมาเป็นการเคลื่อนที่ของ Mouse ครับ โดยผมตั้งอัตราเอาไว้คงที่ เพราะยังไม่มีหน้าจอให้ปรับ คือ ถ้าเร่งสุดคือ 5 Pixel ถ้าเร่งน้อย ก็ลดหลั่นลงไปตามสัดส่วน เช่น เลื่อน Analog มา 50% ก็คือ เลื่อนไปแค่ 0.5 x 5 = 2 Pixel (เพราะว่าต้องเป็น int) ที่ต้องแยกไว้สองตัว ก็เพราะว่า เวลาเราจับ Offset เราจะได้แยกกันกันอยู่แล้วครับ
      • AxisToMouse นี่มาทีหลังครับ เพราะผมพบว่า ถ้าส่งค่าแกน X/Y แยกกัน เวลาเลื่อน Thumbstick แบบเฉียงๆ เมาส์มันส่งไปแปลกๆ (มันจะขยับทีละแกน แต่ว่าสลับกันเร็วๆ) ถ้าส่งไปพร้อมกัน จะเนียนกว่า AxisToMouseX/AxisToMouseY เลยจะส่งค่ามารวมกันที่คลาสนี้ แล้วให้คลาสนี้ส่ง X/Y พร้อมกันทีเดียว โดยผมต้องเปิดอีก Thread หนึ่ง เพื่อสร้างจังหวะในการส่งอีกที เหมือนการที่เครื่องคอมพิวเตอร์ไป Poll ค่าของเมาส์ออกมา โดยผมกำหนดไว้ที่ 100 ครั้ง/วินาที ครับ ผลออกมาก็น่าพอใจเลยทีเดียว

Class Diagram

สำหรับ Source Code ทั้งหมด ก็ดูได้จาก GitHub นะครับ

โปรดติดตามต่อภาค 2

ถ้าเกิดว่าได้ลองโหลด App มาติดตั้งแล้ว จะพบว่า ตัว App ประกอบไปด้วยสองส่วน คือตัวที่เป็น Store App ที่ใช้ในการ Config ค่า และตัวที่เป็น Desktop App รันอยู่ใน System Tray ซึ่งผมสามารถให้ Store App อ่านค่าจาก Joystick/GamePad เพื่อทำการ Config ตัว Mapping ได้ ทั้งๆ ที่มันไม่ควรจะทำได้ เดี๋ยวมาดูกันว่าผมใช้เทคนิคอะไรในการอ่านค่า Joystick บน Store App ผ่านตัว Desktop App ครับ (และเทคนิคนี้ สามารถไปใช้ในการทำให้ Store App สามารถทำอะไรได้อีกหลายอย่าง ที่ปกติมันทำไม่ได้ด้วยนะ)