C++でゲーム用の入力検知クラスを作る
はじめに
この記事は、神戸電子専門学校 ゲーム技研部 Advent Calendar 2023の21日目の記事です。
ゲームを作っていると、コントローラー対応やキーコンフィグを実装したくなることがあると思います。
そんな時にあらかじめ入力検知を管理するクラスを作っておかないと、入力に関するコードを片っ端から大改修する羽目になってしまいます。
なので今回は、拡張性と汎用性が高い入力検知クラスをC++で実装する方法を紹介しようと思います。
入力を検知
まずは、キーボードやコントローラーから入力を取得するのに必要な要素を実装していきます。
設計はこのようになっています。
Device
キーボード&マウスやコントローラーなど、一般的に入力デバイスとして区切られているものをDeviceとします。
Format
1つの入力デバイスの中にあるボタンや軸などの様々な入力形式をFormatとします。
Windowsだとキーボードとマウス、コントローラーだとボタンとスティックみたいな感じです。
今回の入力検知システムでは、ボタンと軸の2つの形式を用意します。
実装
実装はこのようになっています。
Format
テンプレートを用いて、それぞれの入力デバイスのAPIで使用されているキーコードの型を設定できるようにしています。(KeyCodeType)
そしてそのキーコードに名前を付けれるようにすることで、文字列で入力の名前を管理できるようにします。(m_umButtonNameToKeyCode、m_umAxisNameToKeyCode)
また、入力形式ごとに入力関数を実装できるようにすることで、自由に入力方法を設定することができるようになります。Spaceキーとマウスホイールの押し込みなど、入力を受け取る方法が違う時に便利です。(CheckButtonState、CheckAxisState)
以下は、ボタンと軸のフォーマットの実装です。
// ボタンの状態を表す列挙型
enum KeyStatus
{
Free = 1 << 0, // 何も押して無い
Press = 1 << 1, // 押した瞬間
Hold = 1 << 2, // 押している間
Release = 1 << 3 // 離した瞬間
};
// ボタンのフォーマット
template<typename KeyCodeType>
class InputButtonFormatBase
{
public:
// ボタンを追加する
inline void AddButton(std::string_view a_buttonName, KeyCodeType a_keyCode)
{
m_umButtonNameToKeyCode[a_buttonName.data()] = a_keyCode;
}
// ボタンを削除する
inline void RemoveButton(std::string_view a_buttonName)
{
m_umButtonNameToKeyCode.erase(a_buttonName.data());
}
// ボタンの状態を確認する関数を実装するための仮想関数
virtual const bool CheckButtonState(KeyCodeType a_keyCode) = 0;
// 型ごとのフォーマットのIDを取得
static inline const inputTypeID GetTypeID()
{
// まだIDが発行されてなければ発行する
if (!m_buttonFormatTypeID)
{
InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID = InputTypeManager::GenerateID();
}
return InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID;
}
inline const std::unordered_map<std::string, KeyCodeType>& GetButtonNameToKeyCode()
{
return m_umButtonNameToKeyCode;
}
private:
// ボタンの名前とキーコードを紐づけるコンテナ
std::unordered_map<std::string, KeyCodeType> m_umButtonNameToKeyCode;
// このフォーマットの型ID
static inputTypeID m_buttonFormatTypeID;
};
// 型ごとのFormatTypeIDの初期化
template<typename KeyCodeType>
inputTypeID InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID = 0;
// 軸の状態を表す構造体(DirectX::SimpleMath::Vector2などがあればそちらを使用してください)
struct AxisStatus
{
AxisStatus() = default;
AxisStatus(const float a_width, const float a_height)
:m_width(a_width)
, m_height(a_height)
{}
float m_width; // 横
float m_height; // 縦
AxisStatus operator+(const AxisStatus& a_value)
{
return AxisStatus(m_width + a_value.m_width, m_height + a_value.m_height);
}
AxisStatus operator-(const AxisStatus& a_value)
{
return AxisStatus(m_width - a_value.m_width, m_height - a_value.m_height);
}
AxisStatus operator+=(const AxisStatus& a_value)
{
m_width += a_value.m_width;
m_height += a_value.m_height;
return *this;
}
};
// 軸のフォーマット
template<typename KeyCodeType>
class InputAxisFormatBase
{
public:
// 軸を追加する
inline void AddAxis(std::string_view a_axisName, KeyCodeType a_keyCode)
{
m_umAxisNameToKeyCode[a_axisName.data()] = a_keyCode;
}
// 軸を削除する
inline void RemoveAxis(std::string_view a_axisName)
{
m_umAxisNameToKeyCode.erase(a_axisName.data());
}
// 軸の状態を確認する
virtual const AxisStatus CheckAxisState(KeyCodeType a_keyCode) = 0;
// 型ごとのフォーマットのIDを取得
static inline const inputTypeID GetTypeID()
{
// まだIDが発行されてなければ発行する
if (!m_axisFormatTypeID)
{
InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID = InputTypeManager::GenerateID();
}
return InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID;
}
inline const std::unordered_map<std::string, KeyCodeType>& GetAxisNameToKeyCode()
{
return m_umAxisNameToKeyCode;
}
private:
// 軸の名前とキーコードを紐づけるコンテナ
std::unordered_map<std::string, KeyCodeType> m_umAxisNameToKeyCode;
// このフォーマットの型ID
static inputTypeID m_axisFormatTypeID;
};
// 型ごとのFormatTypeIDの初期化
template<typename KeyCodeType>
inputTypeID InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID = 0;
Device
テンプレートを用いて、それぞれの入力デバイスのAPIで使用されているキーコードの型を設定できるようにしています。私はchar型以外のキーコードを使用している入力デバイスを知らないのですが、一応用意しておきます。(KeyCodeType)
先程実装したフォーマットを登録できるようにすることで、一つの入力デバイスに複数の入力形式を設定できるようになります。(m_umNameToButtonFormat、m_umNameToAxisFormat)
そして、検知した入力を取得できるようにすることで、デバイスからの入力を受け取ることができるようになります。(IsButton〇〇〇、GetAxisState)
以下は、デバイスとデバイスを管理するクラスの実装です。
class IInputDeviceBase
{
public:
virtual void Update() = 0;
virtual short GetButtonState(std::string_view a_buttonName) = 0;
virtual const bool IsButtonFree(std::string_view a_buttonName) = 0;
virtual const bool IsButtonPress(std::string_view a_buttonName) = 0;
virtual const bool IsButtonHold(std::string_view a_buttonName) = 0;
virtual const bool IsButtonRelease(std::string_view a_buttonName) = 0;
virtual const std::unordered_map<std::string, short>& GetAllButtonResult() = 0;
virtual const AxisStatus GetAxisState(std::string_view a_axisName) = 0;
virtual const std::unordered_map<std::string, AxisStatus>& GetAllAxisResult() = 0;
};
template<typename KeyCodeType>
class InputDeviceBase :public IInputDeviceBase
{
public:
// 入力状態を取得する関数
virtual void Update()override final
{
// ボタン入力の検知
//-----------------------------------------------------------
// 全てのボタンフォーマットを検索
for (auto&& format : m_umNameToButtonFormat)
{
// 全てのボタンを検索
for (auto&& button : format.second->GetButtonNameToKeyCode())
{
// 前のフレームの入力状態を取得
short result = m_umButtonToResult[button.first];
// ボタンが押されていた時の処理
if (format.second->CheckButtonState(button.second))
{
// 前フレームでボタンが押されて無ければ
if (result & KeyStatus::Free)
{
// 結果にPressを追加
result |= KeyStatus::Press;
}
// 前フレームでもボタンが押されていれば
else
{
// 結果からPressを除外
result &= ~KeyStatus::Press;
}
// 結果からFreeを除外
result &= ~KeyStatus::Free;
// 結果にHoldを追加
result |= KeyStatus::Hold;
}
// ボタンが押されてないときの処理
else
{
// 前フレームでボタンが押されていれば
if (result & KeyStatus::Hold)
{
// 結果にReleaseを追加
result |= KeyStatus::Release;
}
// 前フレームでもボタンが押されてなければ
else
{
// 結果からReleaseを削除
result &= ~KeyStatus::Release;
}
// 結果にHoldを追加
result &= ~KeyStatus::Hold;
// 結果からFreeを除外
result |= KeyStatus::Free;
}
// ボタン入力の結果をコンテナに格納
m_umButtonToResult[button.first] = result;
}
}
// 軸入力の検知
//-----------------------------------------------------------
// 全ての種類の軸入力を検索
for (auto&& format : m_umNameToAxisFormat)
{
// 軸の状態を検索
for (auto&& axis : format.second->GetAxisNameToKeyCode())
{
// 軸入力の結果をコンテナに格納
m_umAxisToResult[axis.first] = format.second->CheckAxisState(axis.second);
}
}
}
//--------------------------------------------------
// Button
//--------------------------------------------------
// ボタンの状態を取得
short GetButtonState(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second;
}
return 0;
}
// ボタンが押されてなければtrue
const bool IsButtonFree(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Free;
}
return false;
}
// ボタンが押された瞬間ならtrue
const bool IsButtonPress(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Press;
}
return false;
}
// ボタンが押されていればtrue
const bool IsButtonHold(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Hold;
}
return false;
}
// ボタンが離れた瞬間ならtrue
const bool IsButtonRelease(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Release;
}
return false;
}
// 全てのボタン入力の結果を取得
const std::unordered_map<std::string, short>& GetAllButtonResult()override final
{
return m_umButtonToResult;
}
//--------------------------------------------------
// Axis
//--------------------------------------------------
// 軸の状態を取得
const AxisStatus GetAxisState(std::string_view a_axisName)override final
{
auto itr = m_umAxisToResult.find(a_axisName.data());
if (itr != m_umAxisToResult.end())
{
return itr->second;
}
return AxisStatus();
}
// 全ての軸入力の結果を取得
const std::unordered_map<std::string, AxisStatus>& GetAllAxisResult()override final
{
return m_umAxisToResult;
}
//--------------------------------------------------
// 登録、取得
//--------------------------------------------------
// ボタンのフォーマットを追加
template<typename FormatType>
void AddInputButtonFormat(std::string_view a_formatName)
{
inputTypeID type = InputButtonFormatBase<FormatType>::GetTypeID();
m_umFormatTypeToName[type] = a_formatName.data();
m_umNameToFormatType[a_formatName.data()] = type;
m_umNameToButtonFormat[a_formatName.data()] = std::make_shared<FormatType>();
}
// 軸のフォーマットを追加
template<typename FormatType>
void AddInputAxisFormat(std::string_view a_formatName)
{
inputTypeID type = InputAxisFormatBase<FormatType>::GetTypeID();
m_umFormatTypeToName[type] = a_formatName.data();
m_umNameToFormatType[a_formatName.data()] = type;
m_umNameToAxisFormat[a_formatName.data()] = std::make_shared<FormatType>();
}
// デバイスの型IDを取得
static inline const inputTypeID GetTypeID()
{
// 型IDが発行されてなければ発行
if (!InputDeviceBase<KeyCodeType>::m_deviceTypeID)
{
InputDeviceBase<KeyCodeType>::m_deviceTypeID = InputTypeManager::GenerateID();
}
return InputDeviceBase<KeyCodeType>::m_deviceTypeID;
}
protected:
// 入力の種類と管理クラスを紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<InputButtonFormatBase<KeyCodeType>>> m_umNameToButtonFormat;
std::unordered_map<std::string, std::shared_ptr<InputAxisFormatBase<KeyCodeType>>> m_umNameToAxisFormat;
// 入力形式の型と名前を紐づけるコンテナ
std::unordered_map<inputTypeID, std::string> m_umFormatTypeToName;
std::unordered_map<std::string, inputTypeID> m_umNameToFormatType;
// 入力の結果を格納するコンテナ
std::unordered_map<std::string, short> m_umButtonToResult;
std::unordered_map<std::string, AxisStatus> m_umAxisToResult;
// デバイスの型のID
static inputTypeID m_deviceTypeID;
};
template<typename KeyCodeType>
inputTypeID InputDeviceBase<KeyCodeType>::m_deviceTypeID = 0;
class InputDeviceCentor
{
public:
// 入力デバイスを追加
template<typename DeviceType>
inline std::shared_ptr<DeviceType> AddDevice(std::string_view a_deviceName)
{
// インスタンスを作成してコンテナにセット
std::shared_ptr<DeviceType> spNewDevice = std::make_shared<DeviceType>();
m_umNameToDevice[a_deviceName.data()] = spNewDevice;
// デバイスの型IDと名前を紐づけ
m_umDeviceTypeToName[DeviceType::GetTypeID()] = a_deviceName.data();
return spNewDevice;
}
// 入力デバイスを取得
template<typename DeviceType>
inline std::shared_ptr<DeviceType> GetDevice()
{
inputTypeID type = DeviceType::GetTypeID();
auto itr = m_umDeviceTypeToName.find(type);
if (itr != m_umDeviceTypeToName.end())
{
return std::static_pointer_cast<DeviceType>(m_umNameToDevice[itr->second]);
}
return nullptr;
}
// 入力デバイスを取得
inline std::shared_ptr<IInputDeviceBase> GetDevice(std::string_view a_name)
{
auto itr = m_umNameToDevice.find(a_name.data());
if (itr != m_umNameToDevice.end())
{
return itr->second;
}
return nullptr;
}
// 全てのデバイスを格納するコンテナの参照を取得
inline const std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>>& GetAllDevice()
{
return m_umNameToDevice;
}
// 各入力デバイスからの入力を検知
void Update()
{
for (auto&& device : m_umNameToDevice)
{
device.second->Update();
}
}
private:
// デバイスの名前と実体を紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>> m_umNameToDevice;
// デバイスの型と名前を紐づけるコンテナ
std::unordered_map<inputTypeID, std::string> m_umDeviceTypeToName;
};
これで、入力デバイスから直接入力を検知するのに必要な要素が揃いました。
入力を管理
次は、入力を管理するのに必要な要素を実装していきます。
設計はこのようになっています。
Collector
タイトル画面の左クリックとゲーム画面の左クリックのように、ゲームの場面ごとにボタンの役割が変わることがあると思います。そんなときのために、Collectorというクラスを作りました。
Work
ボタンの名前(SpaceキーやEnterキーなど)とは別に、ボタンの役割(攻撃、選択、キャンセルなど)を設定し、名前を付けられるようにしました。名前をWorkとしています。
Workには複数のボタンや軸を設定できるようになっているため、複数の入力デバイスから同時に入力された際にも問題なく対応することができます。
実装
実装はこのようになっています。
Work
ワークはコレクタ内のコンテナで管理します。(m_umWorkToButtons、m_umWorkToAxises)
class InputCollector
{
public:
// コレクタが有効か調べる
inline const bool CheckActive()
{
return m_bActive;
}
// コレクタの有効状態をセット
inline void SetActive(const bool a_bActive)
{
m_bActive = a_bActive;
}
// コレクタの名前を取得
inline std::string_view GetName()
{
return m_name;
}
// コレクタの名前をセット
inline void SetName(std::string_view a_name)
{
m_name = a_name;
}
//--------------------------------------------------
// Button
//--------------------------------------------------
// ボタンワークを追加する
inline void AddButtonWork(std::string_view a_work)
{
m_umWorkToButtons[a_work.data()];
}
// ボタンワークにボタンを追加する
inline void AddButtonToButtonWork(std::string_view a_work, std::string_view a_device, std::string_view a_button)
{
m_umWorkToButtons[a_work.data()][a_device.data()].emplace(a_button.data());
}
// ボタンワークを削除
inline void RemoveButtonWork(std::string_view a_work)
{
m_umWorkToButtons.erase(a_work.data());
}
// ボタンワークからボタンを削除
inline void RemoveButtonFromButtonWork(std::string_view a_work, std::string_view a_device, std::string a_button)
{
m_umWorkToButtons[a_work.data()][a_device.data()].erase(a_button.data());
}
// 全てのボタンワークを取得
inline const std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>>& GetAllButtonWork()
{
return m_umWorkToButtons;
}
//--------------------------------------------------
// Axis
//--------------------------------------------------
// 軸ワークを追加する
inline void AddAxisWork(std::string_view a_work)
{
m_umWorkToAxises[a_work.data()];
}
// 軸ワークに軸を追加する
inline void AddAxisToAxisWork(std::string_view a_work, std::string_view a_device, std::string_view a_axis)
{
m_umWorkToAxises[a_work.data()][a_device.data()].emplace(a_axis.data());
}
// 軸ワークを削除
inline void RemoveAxisWork(std::string_view a_work)
{
m_umWorkToAxises.erase(a_work.data());
}
// 軸ワークから軸を削除
inline void RemoveAxisFromAxisWork(std::string_view a_work, std::string_view a_device, std::string_view a_axis)
{
m_umWorkToAxises[a_work.data()][a_device.data()].erase(a_axis.data());
}
// 全ての軸ワークを取得
inline const std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>>& GetAllAxisWork()
{
return m_umWorkToAxises;
}
private:
// 有効状態
bool m_bActive = false;
// このコレクタの名前
std::string m_name;
// Workとキーを紐づけるコンテナ
std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>> m_umWorkToButtons;
std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>> m_umWorkToAxises;
};
Input
最後に、デバイスとコレクタを紐づけるクラスを実装することで、入力検知システムとして機能するようにします。
class Input
{
public:
//-------------------------------------------
// デバイス
//-------------------------------------------
// デバイスを追加
template<typename DeviceType>
inline void AddDevice(std::string_view a_deviceName)
{
m_deviceCentor.AddDevice<DeviceType>(a_deviceName);
}
// デバイスを取得
template<typename DeviceType>
inline std::shared_ptr<DeviceType> GetDevice()
{
return m_deviceCentor.GetDevice<DeviceType>();
}
// デバイスを取得
std::shared_ptr<IInputDeviceBase> GetDevice(std::string_view a_deviceName)
{
return m_deviceCentor.GetDevice(a_deviceName);
}
// 全てのデバイスを取得
inline const std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>>& GetAllDevice()
{
return m_deviceCentor.GetAllDevice();
}
//-------------------------------------------
// コレクタ
//-------------------------------------------
// コレクタを追加
inline std::shared_ptr<InputCollector> AddCollector(std::string_view a_collectorName)
{
std::shared_ptr<InputCollector> spCollector = std::make_shared<InputCollector>();
m_umNameToCollector[a_collectorName.data()] = spCollector;
spCollector->SetName(a_collectorName);
spCollector->SetActive(true);
return spCollector;
}
// コレクタを取得
inline std::shared_ptr<InputCollector>GetCollector(std::string_view a_collectorName)
{
return m_umNameToCollector[a_collectorName.data()];
}
// 全てのコレクタを取得
inline std::unordered_map<std::string, std::shared_ptr<InputCollector>>& GetAllCollector()
{
return m_umNameToCollector;
}
//-------------------------------------------
// Work
//-------------------------------------------
// ワークの状態を取得
inline const short GetButtonWorkState(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()];
}
// ボタンワークが何も押されてないならtrue
inline const bool IsButtonWorkFree(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Free;
}
// ボタンワークが押された瞬間ならtrue
inline const bool IsButtonWorkPress(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Free;
}
// ボタンワークが押されていればtrue
inline const bool IsButtonWorkHold(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Hold;
}
// ボタンワークが離された瞬間ならtrue
inline const bool IsButtonWorkRelease(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Release;
}
// 軸の状態を取得
inline const AxisStatus GetAxisStatus(std::string_view a_work)
{
return m_umWorkToAxisResult[a_work.data()];
}
//-------------------------------------------
// 更新
//-------------------------------------------
void Update()
{
// デバイスの入力を検知
m_deviceCentor.Update();
// 前フレームのワークのリザルトをリセット
m_umWorkToButtonResult.clear();
m_umWorkToAxisResult.clear();
// コレクタの入力結果をセット
for (auto&& collector : m_umNameToCollector)
{
if (!collector.second->CheckActive())
{
continue;
}
// キーの入力結果をセット
for (auto&& work : collector.second->GetAllButtonWork())
{
for (auto&& deviceToButtons : work.second)
{
for (auto&& button : deviceToButtons.second)
{
m_umWorkToButtonResult[work.first] |= m_deviceCentor.GetDevice(deviceToButtons.first)->GetButtonState(button);
}
}
}
// 軸の入力結果をセット
for (auto&& work : collector.second->GetAllAxisWork())
{
for (auto&& deviceToAxises : work.second)
{
for (auto&& axis : deviceToAxises.second)
{
m_umWorkToAxisResult[work.first] += m_deviceCentor.GetDevice(deviceToAxises.first)->GetAxisState(axis);
}
}
}
}
}
private:
// デバイスを管理するクラスのインスタンス
InputDeviceCentor m_deviceCentor;
// コレクタの名前とインスタンスを紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<InputCollector>> m_umNameToCollector;
// Workと結果を紐づけるコンテナ
std::unordered_map<std::string, short> m_umWorkToButtonResult;
std::unordered_map<std::string, AxisStatus> m_umWorkToAxisResult;
};
以上で実装は完了です。お疲れさまでした。
サンプルコード
今回作成した入力検知クラスのサンプルコードです。
サンプルコード
#include <string>
#include <unordered_map>
#include <vector>
#include <unordered_set>
#include <memory>
using inputTypeID = size_t;
// 軸の状態を表す構造体
struct AxisStatus
{
AxisStatus() = default;
AxisStatus(const float a_width, const float a_height)
:m_width(a_width)
, m_height(a_height)
{}
float m_width; // 横
float m_height; // 縦
AxisStatus operator+(const AxisStatus& a_value)
{
return AxisStatus(m_width + a_value.m_width, m_height + a_value.m_height);
}
AxisStatus operator+=(const AxisStatus& a_value)
{
m_width += a_value.m_width;
m_height += a_value.m_height;
return *this;
}
};
// ボタンの状態を表す列挙型
enum KeyStatus
{
Free = 1 << 0, // 何も押して無い
Press = 1 << 1, // 押した瞬間
Hold = 1 << 2, // 押している間
Release = 1 << 3 // 離した瞬間
};
// 次に発行するフォーマットのIDを管理するクラス
class InputTypeManager
{
public:
static inline const inputTypeID GenerateID()
{
return ++m_nextInputTypeID;
}
private:
static inputTypeID m_nextInputTypeID;
};
inputTypeID InputTypeManager::m_nextInputTypeID = 0;
class InputCollector
{
public:
// コレクタが有効か調べる
inline const bool CheckActive()
{
return m_bActive;
}
// コレクタの有効状態をセット
inline void SetActive(const bool a_bActive)
{
m_bActive = a_bActive;
}
// コレクタの名前を取得
inline std::string_view GetName()
{
return m_name;
}
// コレクタの名前をセット
inline void SetName(std::string_view a_name)
{
m_name = a_name;
}
//--------------------------------------------------
// Key
//--------------------------------------------------
// ボタンワークを追加する
inline void AddButtonWork(std::string_view a_work)
{
m_umWorkToButtons[a_work.data()];
}
// ボタンワークにボタンを追加する
inline void AddButtonToButtonWork(std::string_view a_work, std::string_view a_device, std::string_view a_button)
{
m_umWorkToButtons[a_work.data()][a_device.data()].emplace(a_button.data());
}
// ボタンワークを削除
inline void RemoveButtonWork(std::string_view a_work)
{
m_umWorkToButtons.erase(a_work.data());
}
// ボタンワークからボタンを削除
inline void RemoveButtonFromButtonWork(std::string_view a_work, std::string_view a_device, std::string a_button)
{
m_umWorkToButtons[a_work.data()][a_device.data()].erase(a_button.data());
}
// 全てのボタンワークを取得
inline const std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>>& GetAllButtonWork()
{
return m_umWorkToButtons;
}
//--------------------------------------------------
// Axis
//--------------------------------------------------
// 軸ワークを追加する
inline void AddAxisWork(std::string_view a_work)
{
m_umWorkToAxises[a_work.data()];
}
// 軸ワークに軸を追加する
inline void AddAxisToAxisWork(std::string_view a_work, std::string_view a_device, std::string_view a_axis)
{
m_umWorkToAxises[a_work.data()][a_device.data()].emplace(a_axis.data());
}
// 軸ワークを削除
inline void RemoveAxisWork(std::string_view a_work)
{
m_umWorkToAxises.erase(a_work.data());
}
// 軸ワークから軸を削除
inline void RemoveAxisFromAxisWork(std::string_view a_work, std::string_view a_device, std::string_view a_axis)
{
m_umWorkToAxises[a_work.data()][a_device.data()].erase(a_axis.data());
}
// 全ての軸ワークを取得
inline const std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>>& GetAllAxisWork()
{
return m_umWorkToAxises;
}
private:
// 有効状態
bool m_bActive = false;
// このコレクタの名前
std::string m_name;
// Workとキーを紐づけるコンテナ
std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>> m_umWorkToButtons;
std::unordered_map<std::string, std::unordered_map<std::string, std::unordered_set<std::string>>> m_umWorkToAxises;
};
template<typename KeyCodeType>
class InputButtonFormatBase
{
public:
// ボタンを追加する
inline void AddButton(std::string_view a_buttonName, KeyCodeType a_keyCode)
{
m_umButtonNameToKeyCode[a_buttonName.data()] = a_keyCode;
}
// ボタンを削除する
inline void RemoveButton(std::string_view a_buttonName)
{
m_umButtonNameToKeyCode.erase(a_buttonName.data());
}
// ボタンの状態を確認する
virtual const bool CheckButtonState(KeyCodeType a_keyCode) = 0;
// 型ごとのフォーマットのIDを取得
static inline const inputTypeID GetTypeID()
{
// まだIDが発行されてなければ発行する
if (!m_buttonFormatTypeID)
{
InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID = InputTypeManager::GenerateID();
}
return InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID;
}
inline const std::unordered_map<std::string, KeyCodeType>& GetButtonNameToKeyCode()
{
return m_umButtonNameToKeyCode;
}
private:
// ボタンの名前とキーコードを紐づけるコンテナ
std::unordered_map<std::string, KeyCodeType> m_umButtonNameToKeyCode;
// このフォーマットの型ID
static inputTypeID m_buttonFormatTypeID;
};
// 型ごとのFormatTypeIDの初期化
template<typename KeyCodeType>
inputTypeID InputButtonFormatBase<KeyCodeType>::m_buttonFormatTypeID = 0;
template<typename KeyCodeType>
class InputAxisFormatBase
{
public:
// 軸を追加する
inline void AddAxis(std::string_view a_axisName, KeyCodeType a_keyCode)
{
m_umAxisNameToKeyCode[a_axisName.data()] = a_keyCode;
}
// 軸を削除する
inline void RemoveAxis(std::string_view a_axisName)
{
m_umAxisNameToKeyCode.erase(a_axisName.data());
}
// 軸の状態を確認する
virtual const AxisStatus CheckAxisState(KeyCodeType a_keyCode) = 0;
// 型ごとのフォーマットのIDを取得
static inline const inputTypeID GetTypeID()
{
// まだIDが発行されてなければ発行する
if (!m_axisFormatTypeID)
{
InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID = InputTypeManager::GenerateID();
}
return InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID;
}
inline const std::unordered_map<std::string, KeyCodeType>& GetAxisNameToKeyCode()
{
return m_umAxisNameToKeyCode;
}
private:
// 軸の名前とキーコードを紐づけるコンテナ
std::unordered_map<std::string, KeyCodeType> m_umAxisNameToKeyCode;
// このフォーマットの型ID
static inputTypeID m_axisFormatTypeID;
};
// 型ごとのFormatTypeIDの初期化
template<typename KeyCodeType>
inputTypeID InputAxisFormatBase<KeyCodeType>::m_axisFormatTypeID = 0;
class IInputDeviceBase
{
public:
virtual void Update() = 0;
virtual short GetButtonState(std::string_view a_buttonName) = 0;
virtual const bool IsButtonFree(std::string_view a_buttonName) = 0;
virtual const bool IsButtonPress(std::string_view a_buttonName) = 0;
virtual const bool IsButtonHold(std::string_view a_buttonName) = 0;
virtual const bool IsButtonRelease(std::string_view a_buttonName) = 0;
virtual const std::unordered_map<std::string, short>& GetAllButtonResult() = 0;
virtual const AxisStatus GetAxisState(std::string_view a_axisName) = 0;
virtual const std::unordered_map<std::string, AxisStatus>& GetAllAxisResult() = 0;
};
template<typename KeyCodeType>
class InputDeviceBase :public IInputDeviceBase
{
public:
// 入力状態を取得する関数
virtual void Update()override final
{
// ボタン入力の検知
//-----------------------------------------------------------
// 全てのボタンフォーマットを検索
for (auto&& format : m_umNameToButtonFormat)
{
// 全てのボタンを検索
for (auto&& button : format.second->GetButtonNameToKeyCode())
{
// 前のフレームの入力状態を取得
short result = m_umButtonToResult[button.first];
// ボタンが押されていた時の処理
if (format.second->CheckButtonState(button.second))
{
// 前フレームでボタンが押されて無ければ
if (result & KeyStatus::Free)
{
// 結果にPressを追加
result |= KeyStatus::Press;
}
// 前フレームでもボタンが押されていれば
else
{
// 結果からPressを除外
result &= ~KeyStatus::Press;
}
// 結果からFreeを除外
result &= ~KeyStatus::Free;
// 結果にHoldを追加
result |= KeyStatus::Hold;
}
// ボタンが押されてないときの処理
else
{
// 前フレームでボタンが押されていれば
if (result & KeyStatus::Hold)
{
// 結果にReleaseを追加
result |= KeyStatus::Release;
}
// 前フレームでもボタンが押されてなければ
else
{
// 結果からReleaseを削除
result &= ~KeyStatus::Release;
}
// 結果にHoldを追加
result &= ~KeyStatus::Hold;
// 結果からFreeを除外
result |= KeyStatus::Free;
}
// ボタン入力の結果をコンテナに格納
m_umButtonToResult[button.first] = result;
}
}
// 軸入力の検知
//-----------------------------------------------------------
// 全ての種類の軸入力を検索
for (auto&& format : m_umNameToAxisFormat)
{
// 軸の状態を検索
for (auto&& axis : format.second->GetAxisNameToKeyCode())
{
// 軸入力の結果をコンテナに格納
m_umAxisToResult[axis.first] = format.second->CheckAxisState(axis.second);
}
}
}
//--------------------------------------------------
// Button
//--------------------------------------------------
// ボタンの状態を取得
short GetButtonState(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second;
}
return 0;
}
// ボタンが押されてなければtrue
const bool IsButtonFree(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Free;
}
return false;
}
// ボタンが押された瞬間ならtrue
const bool IsButtonPress(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Press;
}
return false;
}
// ボタンが押されていればtrue
const bool IsButtonHold(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Hold;
}
return false;
}
// ボタンが離れた瞬間ならtrue
const bool IsButtonRelease(std::string_view a_buttonName)override final
{
auto itr = m_umButtonToResult.find(a_buttonName.data());
if (itr != m_umButtonToResult.end())
{
return itr->second & KeyStatus::Release;
}
return false;
}
// 全てのボタン入力の結果を取得
const std::unordered_map<std::string, short>& GetAllButtonResult()override final
{
return m_umButtonToResult;
}
//--------------------------------------------------
// Axis
//--------------------------------------------------
// 軸の状態を取得
const AxisStatus GetAxisState(std::string_view a_axisName)override final
{
auto itr = m_umAxisToResult.find(a_axisName.data());
if (itr != m_umAxisToResult.end())
{
return itr->second;
}
return AxisStatus();
}
// 全ての軸入力の結果を取得
const std::unordered_map<std::string, AxisStatus>& GetAllAxisResult()override final
{
return m_umAxisToResult;
}
//--------------------------------------------------
// 登録、取得
//--------------------------------------------------
// ボタンのフォーマットを追加
template<typename FormatType>
void AddInputButtonFormat(std::string_view a_formatName)
{
inputTypeID type = InputButtonFormatBase<FormatType>::GetTypeID();
m_umFormatTypeToName[type] = a_formatName.data();
m_umNameToFormatType[a_formatName.data()] = type;
m_umNameToButtonFormat[a_formatName.data()] = std::make_shared<FormatType>();
}
// 軸のフォーマットを追加
template<typename FormatType>
void AddInputAxisFormat(std::string_view a_formatName)
{
inputTypeID type = InputAxisFormatBase<FormatType>::GetTypeID();
m_umFormatTypeToName[type] = a_formatName.data();
m_umNameToFormatType[a_formatName.data()] = type;
m_umNameToAxisFormat[a_formatName.data()] = std::make_shared<FormatType>();
}
// デバイスの型IDを取得
static inline const inputTypeID GetTypeID()
{
// 型IDが発行されてなければ発行
if (!InputDeviceBase<KeyCodeType>::m_deviceTypeID)
{
InputDeviceBase<KeyCodeType>::m_deviceTypeID = InputTypeManager::GenerateID();
}
return InputDeviceBase<KeyCodeType>::m_deviceTypeID;
}
protected:
// 入力の種類と管理クラスを紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<InputButtonFormatBase<KeyCodeType>>> m_umNameToButtonFormat;
std::unordered_map<std::string, std::shared_ptr<InputAxisFormatBase<KeyCodeType>>> m_umNameToAxisFormat;
// 入力形式の型と名前を紐づけるコンテナ
std::unordered_map<inputTypeID, std::string> m_umFormatTypeToName;
std::unordered_map<std::string, inputTypeID> m_umNameToFormatType;
// 入力の結果を格納するコンテナ
std::unordered_map<std::string, short> m_umButtonToResult;
std::unordered_map<std::string, AxisStatus> m_umAxisToResult;
// デバイスの型のID
static inputTypeID m_deviceTypeID;
};
template<typename KeyCodeType>
inputTypeID InputDeviceBase<KeyCodeType>::m_deviceTypeID = 0;
class InputDeviceCentor
{
public:
// 入力デバイスを追加
template<typename DeviceType>
inline std::shared_ptr<DeviceType> AddDevice(std::string_view a_deviceName)
{
// インスタンスを作成してコンテナにセット
std::shared_ptr<DeviceType> spNewDevice = std::make_shared<DeviceType>();
m_umNameToDevice[a_deviceName.data()] = spNewDevice;
// デバイスの型IDと名前を紐づけ
m_umDeviceTypeToName[DeviceType::GetTypeID()] = a_deviceName.data();
return spNewDevice;
}
// 入力デバイスを取得
template<typename DeviceType>
inline std::shared_ptr<DeviceType> GetDevice()
{
inputTypeID type = DeviceType::GetTypeID();
auto itr = m_umDeviceTypeToName.find(type);
if (itr != m_umDeviceTypeToName.end())
{
return std::static_pointer_cast<DeviceType>(m_umNameToDevice[itr->second]);
}
return nullptr;
}
// 入力デバイスを取得
inline std::shared_ptr<IInputDeviceBase> GetDevice(std::string_view a_name)
{
auto itr = m_umNameToDevice.find(a_name.data());
if (itr != m_umNameToDevice.end())
{
return itr->second;
}
return nullptr;
}
// 全てのデバイスを格納するコンテナの参照を取得
inline const std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>>& GetAllDevice()
{
return m_umNameToDevice;
}
// 更新
void Update()
{
for (auto&& device : m_umNameToDevice)
{
device.second->Update();
}
}
private:
// デバイスの名前と実体を紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>> m_umNameToDevice;
// デバイスの型と名前を紐づけるコンテナ
std::unordered_map<inputTypeID, std::string> m_umDeviceTypeToName;
};
class Input
{
public:
//-------------------------------------------
// デバイス
//-------------------------------------------
// デバイスを追加
template<typename DeviceType>
inline void AddDevice(std::string_view a_deviceName)
{
m_deviceCentor.AddDevice<DeviceType>(a_deviceName);
}
// デバイスを取得
template<typename DeviceType>
inline std::shared_ptr<DeviceType> GetDevice()
{
return m_deviceCentor.GetDevice<DeviceType>();
}
// デバイスを取得
std::shared_ptr<IInputDeviceBase> GetDevice(std::string_view a_deviceName)
{
return m_deviceCentor.GetDevice(a_deviceName);
}
// 全てのデバイスを取得
inline const std::unordered_map<std::string, std::shared_ptr<IInputDeviceBase>>& GetAllDevice()
{
return m_deviceCentor.GetAllDevice();
}
//-------------------------------------------
// コレクタ
//-------------------------------------------
// コレクタを追加
inline std::shared_ptr<InputCollector> AddCollector(std::string_view a_collectorName)
{
std::shared_ptr<InputCollector> spCollector = std::make_shared<InputCollector>();
m_umNameToCollector[a_collectorName.data()] = spCollector;
spCollector->SetName(a_collectorName);
spCollector->SetActive(true);
return spCollector;
}
// コレクタを取得
inline std::shared_ptr<InputCollector>GetCollector(std::string_view a_collectorName)
{
return m_umNameToCollector[a_collectorName.data()];
}
// 全てのコレクタを取得
inline std::unordered_map<std::string, std::shared_ptr<InputCollector>>& GetAllCollector()
{
return m_umNameToCollector;
}
//-------------------------------------------
// Work
//-------------------------------------------
// ワークの状態を取得
inline const short GetButtonWorkState(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()];
}
// ボタンワークが何も押されてないならtrue
inline const bool IsButtonWorkFree(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Free;
}
// ボタンワークが押された瞬間ならtrue
inline const bool IsButtonWorkPress(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Free;
}
// ボタンワークが押されていればtrue
inline const bool IsButtonWorkHold(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Hold;
}
// ボタンワークが離された瞬間ならtrue
inline const bool IsButtonWorkRelease(std::string_view a_work)
{
return m_umWorkToButtonResult[a_work.data()] & KeyStatus::Release;
}
// 軸の状態を取得
inline const AxisStatus GetAxisStatus(std::string_view a_work)
{
return m_umWorkToAxisResult[a_work.data()];
}
//-------------------------------------------
// 更新
//-------------------------------------------
void Update()
{
// デバイスの入力を検知
m_deviceCentor.Update();
// 前フレームのワークのリザルトをリセット
m_umWorkToButtonResult.clear();
m_umWorkToAxisResult.clear();
// コレクタの入力結果をセット
for (auto&& collector : m_umNameToCollector)
{
if (!collector.second->CheckActive())
{
continue;
}
// キーの入力結果をセット
for (auto&& work : collector.second->GetAllButtonWork())
{
for (auto&& deviceToButtons : work.second)
{
for (auto&& button : deviceToButtons.second)
{
m_umWorkToButtonResult[work.first] |= m_deviceCentor.GetDevice(deviceToButtons.first)->GetButtonState(button);
}
}
}
// 軸の入力結果をセット
for (auto&& work : collector.second->GetAllAxisWork())
{
for (auto&& deviceToAxises : work.second)
{
for (auto&& axis : deviceToAxises.second)
{
m_umWorkToAxisResult[work.first] += m_deviceCentor.GetDevice(deviceToAxises.first)->GetAxisState(axis);
}
}
}
}
}
private:
// デバイスを管理するクラスのインスタンス
InputDeviceCentor m_deviceCentor;
// コレクタの名前とインスタンスを紐づけるコンテナ
std::unordered_map<std::string, std::shared_ptr<InputCollector>> m_umNameToCollector;
// Workと結果を紐づけるコンテナ
std::unordered_map<std::string, short> m_umWorkToButtonResult;
std::unordered_map<std::string, AxisStatus> m_umWorkToAxisResult;
};
使用例
この入力検知システムの使用例です。
今回外部ファイル化はしていないため、Jsonなどを用いて自由に実装してください。
使用例:Windows
// キーボードのフォーマットを作成
class KeyboadInputFormat :public InputButtonFormatBase<char>
{
public:
KeyboadInputFormat()
{
// キーを登録
AddButton("A", 'A');
AddButton("B", 'B');
AddButton("C", 'C');
AddButton("D", 'D');
AddButton("E", 'E');
AddButton("F", 'F');
AddButton("G", 'G');
AddButton("H", 'H');
AddButton("I", 'I');
AddButton("J", 'J');
AddButton("K", 'K');
AddButton("L", 'L');
AddButton("M", 'M');
AddButton("N", 'N');
AddButton("O", 'O');
AddButton("P", 'P');
AddButton("Q", 'Q');
AddButton("R", 'R');
AddButton("S", 'S');
AddButton("T", 'T');
AddButton("U", 'U');
AddButton("V", 'V');
AddButton("W", 'W');
AddButton("X", 'X');
AddButton("Y", 'Y');
AddButton("Z", 'Z');
AddButton("Up", VK_UP);
AddButton("Down", VK_DOWN);
AddButton("Left", VK_LEFT);
AddButton("Right", VK_RIGHT);
AddButton("Space", VK_SPACE);
AddButton("Return", VK_RETURN);
AddButton("RClick", VK_RBUTTON);
AddButton("LClick", VK_LBUTTON);
}
const bool CheckButtonState(char a_keyCode)override
{
return GetAsyncKeyState(a_keyCode) & 0x8000;
}
};
// マウスカーソルのフォーマットを作成
class MouseInputFormat :public InputAxisFormatBase<char>
{
public:
MouseInputFormat()
{
// 前フレームのCursor座標に初期値をセット
POINT cursorPos;
GetCursorPos(&cursorPos);
m_fixCursorPos = cursorPos;
AddKey("Mouse");
}
const AxisStatus CheckAxisState(char) override
{
POINT cursorPos;
GetCursorPos(&cursorPos);
AxisStatus result = { static_cast<float>(cursorPos.x) - m_fixCursorPos.x, static_cast<float>(cursorPos.y) - m_fixCursorPos.y };
m_fixCursorPos = cursorPos;
return result;
}
private:
POINT m_fixCursorPos = {};
};
// Windowsデバイスの作成
class WindowsInputDevice :public InputDeviceBase<char>
{
public:
WindowsInputDevice()
{
// 入力フォーマットを登録
AddInputButtonFormat<KeyboadInputFormat>("keyboad");
AddInputAxisFormat<MouseInputFormat>("mouse");
}
};
#include <iostream>
int main()
{
Input input;
// Windowsデバイスの追加
input.AddDevice<WindowsInputDevice>("windows");
// コレクタを作成
input.AddCollector("game");
// コレクタにワークを登録
input.GetCollector("game")->AddButtonToButtonWork("attack", "windows", "LClick");
input.GetCollector("game")->AddButtonToButtonWork("attack", "windows", "Return");
// 入力を検知
input.Update();
// 入力状態を取得
if (input.IsButtonWorkHold("attack"))
{
std::cout << "攻撃!" << std::endl;
}
return 0;
}
おわりに
今回は汎用性と拡張性の高い入力検知クラスの作り方を私なりに解説しました。
間違いや改善点などあれば教えていただけると幸いです。
Discussion