C#調用系統API存取遊戲手把操作

用最簡單的方式,透過winmm.dll讓c#直接存取系統api,達成遊戲手把的操作.

感覺上C#使用遊戲手把應該是一件很簡單直覺的事情,但錯了...官方並沒有支援的很好,或是說並沒有支援(從我以前踏入C#就常常發現很多這種例子),study過後大概就下面幾種常見方案

1.調用directx mamanged dll來使用,你從很多教學都會看到....但這是爛方法,現在也幾乎不能用了,原因是 .net版本的差異以及無法在x64上執行,這種方法多數是存在於.net framework 2.0 3.5 x86電腦為主流時代的做法,現在還有沒有辦法這樣使用是一個問號,如果真的要這樣做,需要花些時間處理相容問題,不建議.

(不騙你,當初浪費很多時間在這上面,總之不建議.....看到 using directx 之類的教學就別浪費時間了)

2.c#直接call directx unmanaged dll,這也不是不行,前提是你熟悉directx原本c++的操作,以及c#直接存取c++ dll的經驗,道行高的可以試試看,但我覺得即使成功也不是好方法,理論上是一個可行方式,實際上沒看過這樣的教學.

3.第三人家包好的c# sdk,這應該是目前最推薦的方式,最常聽到就是sharpdx或是slimdx以opentk這三款,這算是最穩步的做法,只是如果只是做簡單的小事情,總覺得這些東西肥大累贅,不過如果你的目標是開發遊戲,應該選擇這方案.

4.透過系統API來做 winmm.dll ,這是我選擇的方案,下面會針對方法3.4優缺點提出一些討論.

5.XNA??? 這東西幾年前被微軟拋棄了,要放下,別浪費自己時間.

方法3是透過人家第三方完整的sdk達成(骨子裡多數也是包directx就是),理論上若是要開發一個完整的遊戲,就使用人家第三方sdk吧...不用多想了,但我這邊提醒的是由於遊戲手把不同,針對xbox的遊戲手把有自己一套api,所以你寫的遊戲若是想完整支援所有種類手把,除了一般遊戲手把的api外,還需要處理xinput這東西.

那如果我只是要做一些很簡單的事情,簡單的小2d遊戲,遊戲幾百KB,怎麼相關的SDK組件就好幾MB甚至10多MB塞了一堆東西...看了很礙眼,而且多數東西用不到...就算把相關DLL精簡化留下來的東西還是不小,能不能不要殺雞用牛刀呢?

如果你跟我一樣有類似的想法和狀況,那麼方法四是最佳解答.

這方法還有額外的一個好處,就是不管你是什麼遊戲手把,都是通過相同api介面去讀取,xbox360的xinput 手把資訊一樣可以讀到,缺點是它不像透過directx拿到的裝置一樣,有一個完整的裝置GUID名稱,但手把這種東西也不會換來換去,換了通常使用者也會重新設定一下配置,裝置名稱的嚴謹度似乎也不太會是議題(?).

這邊分享兩篇教學,我的範例是參考下面兩篇教學來的

http://www.cnblogs.com/kingthy/archive/2009/03/25/1421838.html

https://yal.cc/c-sharp-joystick-tracking-via-winmm-dll

以前有使用sharpdx來存取遊戲手把服務的經驗,憑著當時的感覺大概依照那種風格以native api重寫一個小sample,沒包成元件的型式,可以自己來,跟上面那篇大陸教學相比採用不同相同的策略,主要是透過joySetCapture來採集訊息在我一個usb手把轉接器上似乎有些問題,再加上這種方式得透過message訊息交流道winform視窗,還得自己處理event routing,反其道而行採用主動polling的方式來處理,減化很多,也把joySetCapture對我某個usb搖感的相容性問題解決了.

ps.joySetCapture這是一個系統api,正確呼叫後,系統會定時間send message event到你的winform handle去,你要自己去處理,比較不喜歡這種方式就是.

下面這sample大小出來12KB...至於第三方dll檔呢? 一個dll幾MB跑不掉,而且通常最少最少都得引用2.3個並用,乾淨.簡潔.單純.直白,快又有效.....

不過功能當然也是比第三方DLL SDK少上不少,好比說力回饋支援.Z軸3D操作等等或是取得搖感進階硬體資訊,這些就缺乏了,就看你自已的需要有沒有需要去用到那些東西.

https://github.com/erspicu/GamePad

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading;


namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            InitJoy();
        }

        //REF http://www.cnblogs.com/kingthy/archive/2009/03/25/1421838.html
        //REFVhttps://yal.cc/c-sharp-joystick-tracking-via-winmm-dll/
        [StructLayout(LayoutKind.Sequential)]
        public struct JOYCAPS
        {
            public ushort wMid;
            public ushort wPid;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string szPname;
            public int wXmin;
            public int wXmax;
            public int wYmin;
            public int wYmax;
            public int wZmin;
            public int wZmax;
            public int wNumButtons;
            public int wPeriodMin;
            public int wPeriodMax;
            public int wRmin;
            public int wRmax;
            public int wUmin;
            public int wUmax;
            public int wVmin;
            public int wVmax;
            public int wCaps;
            public int wMaxAxes;
            public int wNumAxes;
            public int wMaxButtons;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string szRegKey;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string szOEMVxD;
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct JOYINFOEX
        {
            public Int32 dwSize; // Size, in bytes, of this structure.
            public Int32 dwFlags; // Flags indicating the valid information returned in this structure.
            public Int32 dwXpos; // Current X-coordinate.
            public Int32 dwYpos; // Current Y-coordinate.
            public Int32 dwZpos; // Current Z-coordinate.
            public Int32 dwRpos; // Current position of the rudder or fourth joystick axis.
            public Int32 dwUpos; // Current fifth axis position.
            public Int32 dwVpos; // Current sixth axis position.
            public Int32 dwButtons; // Current state of the 32 joystick buttons (bits)
            public Int32 dwButtonNumber; // Current button number that is pressed.
            public Int32 dwPOV; // Current position of the point-of-view control (0..35,900, deg*100)
            public Int32 dwReserved1; // Reserved; do not use.
            public Int32 dwReserved2; // Reserved; do not use.
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct JOYINFO
        {
            public Int32 wXpos; // Current X-coordinate.
            public Int32 wYpos; // Current Y-coordinate.
            public Int32 wZpos; // Current Z-coordinate.
            public Int32 wButtons; // Current state of joystick buttons.
        }
        [DllImport("winmm.dll")]
        public static extern Int32 joyGetPos(Int32 uJoyID, ref JOYINFO pji);
        [DllImport("winmm.dll")]
        public static extern Int32 joyGetPosEx(Int32 uJoyID, ref JOYINFOEX pji);
        [DllImport("winmm.dll")]
        public static extern int joyGetDevCaps(int uJoyID, ref JOYCAPS pjc, int cbjc);

        public struct DeviceJoyInfo
        {
            public bool JoyEx;
            public int ButtonCount;
            public int ID;
            public int Button_old;
            public int Way_X_old;
            public int Way_Y_old;
        }

        public struct joystickEvent
        {
            public int event_type; //0:方向鍵觸發 1:一般按鈕觸發
            public int joystick_id;//發生於哪個遊戲手把

            public int button_id;//如果是一般按鈕觸發,發生在哪顆按鈕
            public int button_event;//0:鬆開 1:壓下

            public int way_type; //0:x方向鍵盤 1:y方向鍵盤
            public int way_value;

        }

        List<DeviceJoyInfo> joyinfo_list = new List<DeviceJoyInfo>();
        JOYCAPS joycap = new JOYCAPS();
        JOYINFO js = new JOYINFO();
        JOYINFOEX jsx = new JOYINFOEX();
        int JOYCAPS_size;
        int PeriodMin = 0;
        unsafe public void InitJoy()
        {
            Stopwatch st = new Stopwatch();
            st.Restart();
            JOYCAPS_size = Marshal.SizeOf(typeof(JOYCAPS));

            for (int i = 0; i < 256; i++)
            {
                if (joyGetDevCaps(i, ref joycap, JOYCAPS_size) == 0)
                {
                    DeviceJoyInfo info = new DeviceJoyInfo();

                    //set id
                    info.ID = i;

                    //check joyex
                    if (joyGetPosEx(i, ref jsx) == 0)
                    {
                        info.JoyEx = true;
                        info.Way_X_old = jsx.dwXpos;
                        info.Way_Y_old = jsx.dwYpos;
                    }
                    else if (joyGetPos(i, ref js) == 0)
                    {
                        info.JoyEx = false;
                        info.Way_X_old = js.wXpos;
                        info.Way_Y_old = js.wYpos;
                    }
                    else continue; //裝置功能失效

                    //set button count
                    info.ButtonCount = joycap.wNumButtons;

                    info.Button_old = 0;

                    if (joycap.wPeriodMin > PeriodMin)
                        PeriodMin = joycap.wPeriodMin;

                    joyinfo_list.Add(info);
                }
            }
            //取出所有目前連線遊戲手把中最慢的PeriodMin然後+5ms
            PeriodMin += 5;
            new Thread(polling_listener).Start();
            st.Stop();
            Console.WriteLine("init joypad infor : " + st.ElapsedMilliseconds + " ms");
        }

        List<joystickEvent> joy_event_captur()
        {
            List<joystickEvent> event_list = new List<joystickEvent>();
            for (int i_button = 0; i_button < joyinfo_list.Count(); i_button++)
            {
                DeviceJoyInfo button_inf = joyinfo_list[i_button];
                int button_id = button_inf.ID;
                int button_count = button_inf.ButtonCount;

                int button_now, X_now, Y_now;
                if (button_inf.JoyEx == false)
                {
                    joyGetPos(button_id, ref js);
                    button_now = js.wButtons;
                    X_now = js.wXpos;
                    Y_now = js.wYpos;
                }
                else
                {
                    joyGetPosEx(button_id, ref jsx);
                    button_now = jsx.dwButtons;
                    X_now = jsx.dwXpos;
                    Y_now = jsx.dwYpos;
                }

                int button_old = button_inf.Button_old;
                int X_old = button_inf.Way_X_old;
                int Y_old = button_inf.Way_Y_old;

                button_inf.Button_old = button_now;
                button_inf.Way_X_old = X_now;
                button_inf.Way_Y_old = Y_now;

                joyinfo_list[i_button] = button_inf;
                if (button_old != button_now || button_now != 0)
                {
                    for (int i = 0; i < button_count; i++)
                    {
                        if ((button_now & 1) != 0)
                        {
                            joystickEvent event_item = new joystickEvent();
                            event_item.event_type = 1;
                            event_item.joystick_id = button_inf.ID;
                            event_item.button_id = i + 1;
                            event_item.button_event = 1;
                            event_list.Add(event_item);
                        }
                        else
                        {
                            if ((button_now & 1) != (button_old & 1))
                            {
                                joystickEvent event_item = new joystickEvent();
                                event_item.event_type = 1;
                                event_item.joystick_id = button_inf.ID;
                                event_item.button_id = i + 1;
                                event_item.button_event = 0;
                                event_list.Add(event_item);
                            }
                        }
                        button_now >>= 1;
                        button_old >>= 1;
                    }
                }

                if (X_old != X_now || (X_now != 32767 && X_now != 32511 && X_now != 32254))
                {
                    if ((X_now != 32767 && X_now != 32511))
                    {
                        joystickEvent event_item = new joystickEvent();
                        event_item.event_type = 0;
                        event_item.joystick_id = button_inf.ID;
                        event_item.way_type = 0;
                        event_item.way_value = X_now;
                        event_list.Add(event_item);

                    }
                    else
                    {
                        joystickEvent event_item = new joystickEvent();
                        event_item.event_type = 0;
                        event_item.joystick_id = button_inf.ID;
                        event_item.way_type = 0;
                        event_item.way_value = X_now;
                        event_list.Add(event_item);
                    }
                }

                if (Y_old != Y_now || (Y_now != 32767 && Y_now != 32511 && Y_now != 32254))
                {
                    if ((Y_now != 32767 && Y_now != 32511))
                    {
                        joystickEvent event_item = new joystickEvent();
                        event_item.event_type = 0;
                        event_item.joystick_id = button_inf.ID;
                        event_item.way_type = 1;
                        event_item.way_value = Y_now;
                        event_list.Add(event_item);
                    }
                    else
                    {
                        joystickEvent event_item = new joystickEvent();
                        event_item.event_type = 0;
                        event_item.joystick_id = button_inf.ID;
                        event_item.way_type = 1;
                        event_item.way_value = Y_now;
                        event_list.Add(event_item);
                    }
                }

            }
            return event_list;
        }

        bool app_running = true;
        void polling_listener()
        {
            while (app_running)
            {
                Thread.Sleep(PeriodMin);
                List<joystickEvent> event_list = joy_event_captur();

                foreach (joystickEvent joy_event in event_list)
                {

                    if (joy_event.event_type == 0) //方向鍵觸發
                    {
                        if (joy_event.way_type == 0) //x
                        {
                            Console.WriteLine("裝置 " + joy_event.joystick_id + " ,X " + joy_event.way_value);
                        }
                        else//y
                        {
                            Console.WriteLine("裝置 " + joy_event.joystick_id + " ,Y " + joy_event.way_value);
                        }

                    }
                    else //一般按鈕觸發
                    {
                        if (joy_event.button_event == 0)
                        {
                            Console.WriteLine("裝置 " + joy_event.joystick_id + " ,按鈕 " + joy_event.button_id + " 放開");
                        }
                        else
                        {
                            Console.WriteLine("裝置 " + joy_event.joystick_id + " ,按鈕 " + joy_event.button_id + " 壓下");
                        }
                    }
                }
            }
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            app_running = false;
        }
    }
}