vJoy
vJoy は、Windows で仮想ジョイスティック (ゲームパッド) を提供するフリーソフトである。
- Home (アーカイブ)
- GitHub - jshafer817/vJoy: Virtual Joystick
- vJoy - Browse /Beta 2.x/SDK at SourceForge.net
この vJoy が提供する仮想ジョイスティックは、フィーダーというプログラムから操作を行う。
vJoy では、フィーダーから仮想ジョイスティックを操作するための DLL も提供している。
今回は、C# でこのフィーダーを作成する方法の基礎を紹介する。
操作用のDLLの位置を取得する
まず、操作用のDLLを読み込むため、DLLの位置を取得する。
vJoy のSDKのドキュメントより、DLLの位置 (ディレクトリ) はレジストリのキー
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1
に格納されていることがわかる。
32ビット用のDLLの位置は値 DllX86Location
に、64ビット用のDLLの位置は値 DllX64Location
に格納されている。
今実行しているのが32ビットなのか64ビットなのかは、IntPtr.Size
プロパティで判定できる。
このプロパティの値が 4
なら32ビット、8
なら64ビットである。
レジストリからデータを1個取得するには、Registry.GetValue
メソッドが便利である。
このメソッドでは、値を取得するキーのパス・値の名前・デフォルト値を指定してデータ読み出しを行う。
public class GetDllLocation
{
public static void Main()
{
bool is64bit = System.IntPtr.Size == 8;
object dllLocationRaw = Microsoft.Win32.Registry.GetValue(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
is64bit ? "DllX64Location" : "DllX86Location",
null
);
if (dllLocationRaw is string)
{
string dllLocation = (string)dllLocationRaw;
System.Console.WriteLine(dllLocation);
}
else
{
System.Console.WriteLine("エラー");
}
}
}
操作用のDLLを読み込む
操作用のDLLは、前章で取得したディレクトリ内にある vJoyInterface.dll
である。
このDLLを読み込み、関数を使用する準備をする。
DLLに含まれる関数の名前・引数・戻り値については、SDKのドキュメントを参照すること。
VJD_STAT_*
系の定数の定義は、inc/vjoyinterface.h
にある。
HID_USAGE_*
系の定数の定義は、inc/public.h
にある。
DLLを読み込む用のAPIを用意する。
警告を避けるため、DllImport
を用いた関数宣言は NativeMethods
クラスの中に入れるのがいいらしい。
private static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryW")]
internal static extern IntPtr LoadLibrary([In] string fileName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
internal static extern IntPtr GetProcAddress(IntPtr hModule, [In] string procName);
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeLibrary(IntPtr hModule);
}
読み込んだDLLのハンドルを格納する変数と、DLLから関数のアドレスを取得する関数を用意する。
private IntPtr hDll;
private T LoadProc<T>(string procName)
{
IntPtr procAddress = NativeMethods.GetProcAddress(hDll, procName);
if (procAddress == IntPtr.Zero)
{
throw new Exception("failed to find " + procName + "()");
}
return (T)(Object)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T));
}
DLLの関数を呼び出すためのデレゲート型・デレゲート変数・デレゲートを呼び出すラッパー関数を用意する。(一例)
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int GetVJDButtonNumberDelegateType(uint rID);
private GetVJDButtonNumberDelegateType GetVJDButtonNumberDelegate;
public int GetVJDButtonNumber(uint rID)
{
return GetVJDButtonNumberDelegate(rID);
}
前章で確認した方法で読み込むべきDLLの位置を取得し、DLLを読み込む。
bool is64bit = System.IntPtr.Size == 8;
object dllLocation = Microsoft.Win32.Registry.GetValue(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
is64bit ? "DllX64Location" : "DllX86Location",
null
);
if (!(dllLocation is string))
{
throw new Exception("failed to read DLL location");
}
hDll = NativeMethods.LoadLibrary(System.IO.Path.Combine((string)dllLocation, "vJoyInterface.dll"));
if (hDll == IntPtr.Zero)
{
throw new Exception("failed to load DLL");
}
DLLから関数を取得する。(一例)
GetVJDButtonNumberDelegate = LoadProc<GetVJDButtonNumberDelegateType>("GetVJDButtonNumber");
コード全体 (VJoyFeederApi
)
このクラスは今回のデモで使用する関数を用意したものであり、SDKで定義されている関数を網羅していない。
using System;
using System.Runtime.InteropServices;
public class VJoyFeederApi : IDisposable
{
private static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryW")]
internal static extern IntPtr LoadLibrary([In] string fileName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
internal static extern IntPtr GetProcAddress(IntPtr hModule, [In] string procName);
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeLibrary(IntPtr hModule);
}
private IntPtr hDll;
private T LoadProc<T>(string procName)
{
IntPtr procAddress = NativeMethods.GetProcAddress(hDll, procName);
if (procAddress == IntPtr.Zero)
{
throw new Exception("failed to find " + procName + "()");
}
return (T)(Object)Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T));
}
public enum VjdStat
{
VJD_STAT_OWN,
VJD_STAT_FREE,
VJD_STAT_BUSY,
VJD_STAT_MISS,
VJD_STAT_UNKN
}
public enum Axis
{
HID_USAGE_X = 0x30,
HID_USAGE_Y = 0x31,
HID_USAGE_Z = 0x32,
HID_USAGE_RX = 0x33,
HID_USAGE_RY = 0x34,
HID_USAGE_RZ = 0x35,
HID_USAGE_SL0 = 0x36,
HID_USAGE_SL1 = 0x37,
HID_USAGE_WHL = 0x38
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
private delegate bool VJoyEnabledDelegateType();
private VJoyEnabledDelegateType VJoyEnabledDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
private delegate bool DriverMatchDelegateType(ref ushort dllVer, ref ushort drvVer);
private DriverMatchDelegateType DriverMatchDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate VjdStat GetVJDStatusDelegateType(uint rID);
private GetVJDStatusDelegateType GetVJDStatusDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
private delegate bool AcquireVJDDelegateType(uint rID);
private AcquireVJDDelegateType AcquireVJDDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void RelinquishVJDDelegateType(uint rID);
private RelinquishVJDDelegateType RelinquishVJDDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int GetVJDButtonNumberDelegateType(uint rID);
private GetVJDButtonNumberDelegateType GetVJDButtonNumberDelegate;
// SDK のドキュメントでは BOOL を返すと主張しているが、実際は以下の値を返すようである (2.1.9)
// デバイスが存在しない (VJD_STAT_MISS) : -2
// デバイスが存在し、軸が存在しない : -10
// デバイスが存在し、軸が存在する : 1
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int GetVJDAxisExistDelegateType(uint rID, Axis axis);
private GetVJDAxisExistDelegateType GetVJDAxisExistDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
private delegate bool SetAxisDelegateType(int value, uint rID, Axis axis);
private SetAxisDelegateType SetAxisDelegate;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
private delegate bool SetBtnDelegateType([MarshalAs(UnmanagedType.Bool)] bool value, uint rID, byte nBtn);
private SetBtnDelegateType SetBtnDelegate;
public VJoyFeederApi()
{
bool is64bit = System.IntPtr.Size == 8;
object dllLocation = Microsoft.Win32.Registry.GetValue(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{8E31F76F-74C3-47F1-9550-E041EEDC5FBB}_is1",
is64bit ? "DllX64Location" : "DllX86Location",
null
);
if (!(dllLocation is string))
{
throw new Exception("failed to read DLL location");
}
hDll = NativeMethods.LoadLibrary(System.IO.Path.Combine((string)dllLocation, "vJoyInterface.dll"));
if (hDll == IntPtr.Zero)
{
throw new Exception("failed to load DLL");
}
VJoyEnabledDelegate = LoadProc<VJoyEnabledDelegateType>("vJoyEnabled");
DriverMatchDelegate = LoadProc<DriverMatchDelegateType>("DriverMatch");
GetVJDStatusDelegate = LoadProc<GetVJDStatusDelegateType>("GetVJDStatus");
AcquireVJDDelegate = LoadProc<AcquireVJDDelegateType>("AcquireVJD");
RelinquishVJDDelegate = LoadProc<RelinquishVJDDelegateType>("RelinquishVJD");
GetVJDButtonNumberDelegate = LoadProc<GetVJDButtonNumberDelegateType>("GetVJDButtonNumber");
GetVJDAxisExistDelegate = LoadProc<GetVJDAxisExistDelegateType>("GetVJDAxisExist");
SetAxisDelegate = LoadProc<SetAxisDelegateType>("SetAxis");
SetBtnDelegate = LoadProc<SetBtnDelegateType>("SetBtn");
}
public void Dispose()
{
NativeMethods.FreeLibrary(hDll);
}
public bool VJoyEnabled()
{
return VJoyEnabledDelegate();
}
public bool DriverMatch(ref ushort dllVer, ref ushort drvVer)
{
return DriverMatchDelegate(ref dllVer, ref drvVer);
}
public VjdStat GetVJDStatus(uint rID)
{
return GetVJDStatusDelegate(rID);
}
public bool AcquireVJD(uint rID)
{
return AcquireVJDDelegate(rID);
}
public void RelinquishVJD(uint rID)
{
RelinquishVJDDelegate(rID);
}
public int GetVJDButtonNumber(uint rID)
{
return GetVJDButtonNumberDelegate(rID);
}
public bool GetVJDAxisExist(uint rID, Axis axis)
{
return GetVJDAxisExistDelegate(rID, axis) > 0;
}
public bool SetAxis(int value, uint rID, Axis axis)
{
return SetAxisDelegate(value, rID, axis);
}
public bool SetBtn(bool value, uint rID, byte nBtn)
{
return SetBtnDelegate(value, rID, nBtn);
}
}
SDKのドキュメントでは、指定の軸が存在するかを判定する GetVJDAxisExist
関数は BOOL
を返すことになっている。
しかし、実験を行ったところ、(少なくともバージョン 2.1.9 では) 指定の軸が存在しない場合に負の値が返り、UnmanagedType.Bool
を用いて bool
型にマーシャリングすると正しく真偽 (軸の有無) を判定できないことがわかった。
したがって、今回は返り値の型を int
とし、ラッパー関数で真偽値に変換している。
使用した主なAPI
- DllImportAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn
- UnmanagedFunctionPointerAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn
- MarshalAsAttribute クラス (System.Runtime.InteropServices) | Microsoft Learn
- Marshal.GetDelegateForFunctionPointer メソッド (System.Runtime.InteropServices) | Microsoft Learn
- LoadLibraryW 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
- GetProcAddress 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
- FreeLibrary 関数 (libloaderapi.h) - Win32 apps | Microsoft Learn
参考サイト
- LoadLibraryを使ってC#からC++DLLを動的ロードして、DLL内の関数を呼び出す方法 - SKSP-TECH
- 【Windows/C#】なるべく丁寧にDllImportを使う #.NET - Qiita
- How to marshal a C++ enum in C# - Stack Overflow
操作を行ってみる
vJoy の仮想ジョイスティックの操作は、以下の手順で行う。
- APIを初期化する
- vJoy が有効かを確認する
- vJoy のインターフェイス (DLL) とドライバのバージョンが合っているかを確認する
- 操作を行うデバイス (仮想ジョイスティック) を選択する
- デバイスの状態が
VJD_STAT_FREE
であることを確認する - デバイスのボタンの数や軸の有無が、アプリケーションが要求する条件に合っていることを確認する
- デバイスの状態が
- デバイスの操作を開始する (acquire)
- デバイスを操作する (ボタンや軸の状態を設定する)
- デバイスの操作を終了する (relinquish)
SDKのドキュメントより、仮想ジョイスティックの番号は、1 から 16 まで (両端を含む) であることがわかる。
この例では、条件に合う最初の仮想ジョイスティックのY軸とボタン1を操作する。
using System;
using System.Threading;
public class VJoyFeederApiUser
{
public static void Main(string[] args)
{
// APIを初期化する
VJoyFeederApi api = new VJoyFeederApi();
if (!api.VJoyEnabled())
{
Console.WriteLine("vJoy is not enabled");
return;
}
ushort dllVer = 0, drvVer = 0;
if (!api.DriverMatch(ref dllVer, ref drvVer)) {
Console.WriteLine("DLL version 0x{0:x} != driver version 0x{0:x}", dllVer, drvVer);
return;
}
Console.WriteLine("using vJoy version 0x{0:x}", drvVer);
// 操作を行うデバイスを選択する
uint deviceToUse = 0;
for (uint i = 1; i <= 16; i++) {
VJoyFeederApi.VjdStat status = api.GetVJDStatus(i);
int numButtons = api.GetVJDButtonNumber(i);
bool yAxisExists = api.GetVJDAxisExist(i, VJoyFeederApi.Axis.HID_USAGE_Y);
Console.WriteLine("device #{0}:", i);
Console.WriteLine(" status: {0}", status);
Console.WriteLine(" has {0} button(s)", numButtons);
Console.WriteLine(" {0} Y axis", yAxisExists ? "has" : "doesn't have");
if (
deviceToUse == 0 &&
status == VJoyFeederApi.VjdStat.VJD_STAT_FREE &&
numButtons >= 1 &&
yAxisExists
)
{
deviceToUse = i;
}
}
Console.WriteLine("-----------");
if (deviceToUse == 0)
{
Console.WriteLine("no device to use found");
return;
}
Console.WriteLine("using device #{0}", deviceToUse);
// デバイスの操作を開始する
if (!api.AcquireVJD(deviceToUse))
{
Console.WriteLine("failed to acquire the device");
return;
}
// デバイスを操作する
Thread.Sleep(1000);
Console.WriteLine("moving Y axis to maximum");
api.SetAxis(0x8000, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
Thread.Sleep(1000);
Console.WriteLine("moving Y axis to minimum");
api.SetAxis(0x1, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
Thread.Sleep(1000);
Console.WriteLine("moving Y axis to center");
api.SetAxis(0x4000, deviceToUse, VJoyFeederApi.Axis.HID_USAGE_Y);
Thread.Sleep(1000);
Console.WriteLine("pressing button 1");
api.SetBtn(true, deviceToUse, 1);
Thread.Sleep(1000);
Console.WriteLine("releasing button 1");
api.SetBtn(false, deviceToUse, 1);
Thread.Sleep(1000);
// デバイスの操作を終了する
api.RelinquishVJD(deviceToUse);
}
}
vJoy に付属している「Monitor vJoy」や、Gamepad Tester により、軸やボタンを操作できていることが確認できた。
まとめ
今回行ったこと
- C# でDLLのパスを指定して読み込み、関数を使用する方法を確認した
- vJoy の操作用DLLの関数を読み込み、実際に操作を行った
今回行っていない主なこと
- POVスイッチ (≒十字キー) の処理
- FFB (フォースフィードバック) の処理
- vJoy の構成が変化した際のコールバックの受信
- vJoy 公式の C# ラッパーの使用
Comments
Let's comment your feelings that are more than good