エンジニアとしての市場価値を測りませんか?PR

企業からあなたに合ったオリジナルのスカウトを受け取って、市場価値を測りましょう

0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#Advent Calendar 2024

Day 11

C# で vJoy の操作を行う (フィーダーを作る)

Posted at

vJoy

vJoy は、Windows で仮想ジョイスティック (ゲームパッド) を提供するフリーソフトである。

この 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 メソッドが便利である。
このメソッドでは、値を取得するキーのパス・値の名前・デフォルト値を指定してデータ読み出しを行う。

操作用のDLLがあるディレクトリを取得する
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

参考サイト

操作を行ってみる

vJoy の仮想ジョイスティックの操作は、以下の手順で行う。

  1. APIを初期化する
    • vJoy が有効かを確認する
    • vJoy のインターフェイス (DLL) とドライバのバージョンが合っているかを確認する
  2. 操作を行うデバイス (仮想ジョイスティック) を選択する
    • デバイスの状態が VJD_STAT_FREE であることを確認する
    • デバイスのボタンの数や軸の有無が、アプリケーションが要求する条件に合っていることを確認する
  3. デバイスの操作を開始する (acquire)
  4. デバイスを操作する (ボタンや軸の状態を設定する)
  5. デバイスの操作を終了する (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# ラッパーの使用
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Comments

No comments

Let's comment your feelings that are more than good

Qiita Advent Calendar is held!

Qiita Advent Calendar is an article posting event where you post articles by filling a calendar 🎅

Some calendars come with gifts and some gifts are drawn from all calendars 👀

Please tie the article to your calendar and let's enjoy Christmas together!

0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address