18. Input 입력 구조 설계 1
https://hyrule.tistory.com/111
*** 공부 방법 ***
1. 코딩을 해야 하는 부분은 첫 부분에 변수나 함수, 메소드에 대한 선언이 코드블럭으로 표시되어 있다. //ex) MakeFunction(); 2. 코드블럭 하단에는 해당 선언에 대한 구현 로직이 작성되어 있다. 처
hyrule.tistory.com
[입력 시스템]
* 키 누름, 키 뗌 등 뿐만 아니라
* 키 조합, 마우스 클릭 등 입력에 관한 전반을 처리해주는 클래스
* + 격투게임 같은 경우 커맨드 처리까지 해 준다.
* 입력 통합 관리자
[간단한 구현 로직]
* 키 조합을 관리하는 구조체를 생성한다.
- 이 구조체에는 이름, 어떤 키를 누르는지, 키조합이 있는지, 이 키조합이 확인되었다면 어떤 함수를 실행시킬 것인지(함수 포인터)가 들어간다.
- CInput에서 이 구조체를 등록해주는 메소드를 만든다.
- 키 입력에 반응할 게임오브젝트는 자신이 키가 눌렸을때 어떤 반응을 할 것인지에 대한 로직을 메소드로 만들어 놓는다.
- 위 구조체 등록 메소드에 이름, 누를 키, 함수 포인터를 전달하면 CInput의 입력 배열에 삽입된다.
- 배열을 반복문을 통해서 키가 눌렸는지 확인한다.
* 정말 쉽게
- KeyState: '매 프레임마다' 방금 어떤 키가 눌렸는지 '상태'를 저장!
- BindKey: 키가 눌렸을때 뭘 할지, 어떤 기능 키와 동시에 눌러야 하는지 '조건'을 저장!
-> 조건 만족시 해당 작업을 처리하는 함수를 호출!
[사전 지식]
#include <functional>
- GameInfo.h에 등록해 놓는다.
- 함수 포인터를 등록해서 필요할 때 호출 가능한 객체를 제공해준다.
- 일반적인 함수 호출 시에는 자동으로 호출한 객체의 주소를 전달해주지만, function으로 호출 시에는 주소 전달이 되지 않으므로, 주소와 함께 전달해주어야 한다.
- 이 때 사용하는 것이 bind() 함수이다.
- 멤버함수를 등록할 때는 bind() 함수를 통해 함수 주소와 객체 주소를 묶어서 등록해 주어야 한다.
std::funtcion<반환타입(인자(있다면))> func;
func = std::bind(함수 주소, 호출 위치(주소), 인자(있다면));
이렇게 선언한다.
* 참고글: https://yhwanp.github.io/2019/09/15/std-function-and-std-bind/
C++ std::function 와 std::bind 사용법
std::function 란?C++11 부터 추가된 기능으로, C의 함수 포인터를 대체한다.아래와 같이 사용할 수 있다. 1234567891011void addAndPrint(int a, int b){ int sum = a + b; std::cout << sum << "\n";}std::functio
yhwanp.github.io
class CInput
- GameManager 필터의 하위 필터로 Input을 하나 생성한다.
- 싱글턴 패턴으로 생성해준다.
- bool타입을 반환하는 초기화 메소드 Init() 생성 -> 반환값을 true로 변경
- 반환값이 없고 DeltaTime을 인자로 받는 Update() 생성
- 일단 이 클래스를 CGameManager 클래스에서 생성시키고 초기화해준다.
- 클래스 제거도 꼼꼼히
//class CInput
struct KeyState
{
}
- 키 입력에 관한 상태를 저장해 줄 구조체를 생성한다.
- 어떤 키가 눌렸는지 확인할 변수를 하나 생성한다.
- 키보드 + 마우스 키 전부 합해도 150개가 안 된다.
- 입력 관련 정보를 저장하기 위한 변수는 unsigned char(0~255) 정도면 충분하고도 남는다는 뜻.
- 생성자를 하나 만들어서, 값을 초기화시켜준다.
struct KeyState
{
unsigned char key;
bool Down; //누르기 시작할 때
bool Push; //누르고 있을 때
bool Up; //누르고 있던 키가 올라올 때
KeyState() :
key(0),
Down(false),
Push(false),
Up(false)
{}
};
- 위의 구조체를 모아서 저장할 자료구조 m_mapKeyState를 생성한다.
- 일반적으로 키설정은 옵션에서 조정할때나 바뀌기 때문에 삽입/삭제가 빈번하지 않다.
- 눌린 키 값을 통해 빠르게 탐색할 수 있는 자료구조가 적합하다.
//키가 실시간으로 추가/제거 되는 경우는 많지 않다.
//탐색이 빠른 unordered map을 사용한다.
//unordered map은 같은 키를 중복해서 등록이 불가능하다.
//같은 키를 중복해서 등록하고 싶다면 multimap을 사용해야 한다.
//키 번호와 KeyState를 묶어놓기
private:
std::unordered_map<unsigned char, KeyState*> m_mapKeyState;
struct BindKey
{
}
- 위의 구조체를 묶어 키 조합 구조체를 하나 더 생성한다.
- 키조합의 이름은 무엇인지(문자열)
- 어떤 키를 쓰는지
- Ctrl, Alt, Shift와 조합되는 키인지
- 조건이 만족되면 어떤 함수를 실행할 것인지(functional)를 등록해야 되는데...
- 이 함수가 여러개가 될 수도 있으므로, 배열 형태로 들고 있어야 한다. -> VecFunction
- 위의 [사전 지식]에서 보았듯이, 클래스의 멤버 함수를 등록하려면 호출 위치를 bind해서 등록해주어야 한다.
- 그러므로 구조체를 또 하나 만들어주어야 한다.
struct BindFunction
{}
- 이 구조체는 두 개의 변수를 가진다.
1. void 포인터 타입 Obj
2. std::function에 인자가 없는 void 함수 변수 func
- Obj 변수는, 생성자를 통해 기본 nullptr로 초기화한다.
- 키 조합 추가 함수에서 호출 위치(Obj)와 함수 포인터를 전달받으면,
- 두 데이터를 bind() 함수를 통해 func 변수에 저장해서 가지고 있을 구조체.
- 위 BindFunction 구조체의 배열을, BindKey 구조체가 들고 있는 구조이다.
//<functional> 헤더의 기능
struct BindFunction
{
void* Obj;
std::function<void()> func;
BindFunction() :
Obj(nullptr)
{
}
};
//키조합(컨트롤, 쉬프트, 알트 등과 같이 조합되는 키)
struct BindKey
{
//이름
std::string Name;
//이름의 입력 조합은 어떤 키인지
KeyState* key;
//어떤 키와 조합될 것인지
bool Ctrl;
bool Alt;
bool Shift;
//키바인딩을 처리해 줄 함수 목록을 저장할 배열 생성.
//vecFunction[0] = Down에 대한 함수들
//vecFunction[1] = Push에 대한 함수들
//vecFunction[2] = Up에 대한 함수들
std::vector<BindFunction*> vecFunction;
//초기화
BindKey() :
key(nullptr),
Ctrl(false),
Alt(false),
Shift(false)
{
}
};
- 구조체 생성이 끝났으면, 해당 키조합 정보를 문자열(이름)을 통해 빠르게 찾을 수 있는 자료구조
m_mapBindKey를 생성한다.
- 여기에 등록되어 있는 키들만 입력되어있는지 확인한다. 나머지 키는 아예 반응하지 않음.
private:
std::unordered_map<std::string, BindKey*> m_mapBindKey;
CInput::FindKeyState()
- 인자로 Key를 받아서 KeyState의 주소를 리턴하는 메소드.
- 외부에서 사용할 일이 없다.
- m_mapKeyState 트리에서 Key를 탐색한다.
- 못 찾았을 경우 end()가 반환된다 -> 이 때는 nullptr을 반환한다.
- 찾았을 경우 트리의 Value를 반환한다.
//private로 선언할것
KeyState* CInput::FindKeyState(unsigned char Key)
{
std::unordered_map<unsigned char, KeyState*>::iterator iter = m_mapKeyState.find(Key);
//탐색을 했는데 못 찾았다면 end가 반환됨.
if (iter == m_mapKeyState.end())
return nullptr;
//iter->first == Key
//iter->second == Value
return iter->second;
}
CInput::FindBindKey()
- 위와 마찬가지로 키조합의 이름(문자열) 을 통해 키조합을 탐색하는 메소드를 생성한다.
//private
BindKey* CInput::FindBindKey(const std::string& Name)
{
std::unordered_map<std::string, BindKey*>::iterator iter = m_mapBindKey.find(Name);
//탐색을 했는데 못 찾았다면 end가 반환됨.
if (iter == m_mapBindKey.end())
return nullptr;
//iter->first == Key
//iter->secont == Value
return iter->second;
}
CInput::AddBindKey()
- 문자열 이름, 사용할 키값 Key를 받아서 키조합을 추가해주고 성공/실패 여부를 bool타입으로 반환해주는 메소드를 생성한다.
- 같은 이름을 가진 BindKey가 있는지 탐색하고, 등록되어 있을경우 true를 반환하고 빠져나온다.
- 중복 BindKey가 없을 경우, 해당 키로 이미 KeyState가 등록되어 있는지를 탐색한다.
- 해당 키가 등록되어있지 않을 경우, KeyState를 추가하고 m_mapKeyState에 새로 추가한다.
- 찾거나 새로 만든 KeyState를 BindKey 구조체에 등록하고, 나머지 정보를 모두 대입한 후에 m_mapBindKey에 새로 추가해준다.
//public
bool CInput::AddBindKey(const std::string& Name, unsigned char Key)
{
//같은 이름을 BindKey가 등록되어 있을 경우 그냥 리턴
if (FindBindKey(Name))
return false;
BindKey* NewKeyBind = new BindKey;
//해당 키로 이미 KeyState가 등록되어 있는지를 찾는다.
KeyState* State = FindKeyState(Key);
//등록이 안 되어 있을 경우 KeyState를 만들어서 등록한다.
if (!State)
{
State = new KeyState;
State->key = Key;
//둘이 묶어서 등록
m_mapKeyState.insert(std::make_pair(Key, State));
}
//새로운 키바인드
NewKeyBind->key = State;
NewKeyBind->Name = Name;
//키바인딩도 묶어서 등록한다.
m_mapBindKey.insert(std::make_pair(Name, NewKeyBind));
return true;
}
- m_mapKeyState와 m_mapBindKey는 모두 동적할당되어 삽입되어 있으므로, Input 클래스가 제거될 때 같이 제거해준다.
CInput::~CInput()
{
//코드블럭으로 감싼 이유: 코드블럭이 끝나면 iter과 iterEnd변수가 없어진다
//같은 이름으로 재사용 가능
{
//unordered_map의 value가 포인터이므로 동적할당을 먼저 제거해주어야 한다
std::unordered_map<unsigned char, KeyState*>::iterator iter = m_mapKeyState.begin();
std::unordered_map<unsigned char, KeyState*>::iterator iterEnd = m_mapKeyState.end();
while (iter != iterEnd)
{
SAFE_DELETE(iter->second);
++iter;
}
//이후 unordered_map을 초기화
m_mapKeyState.clear();
}
{
std::unordered_map<std::string, BindKey*>::iterator iter = m_mapBindKey.begin();
std::unordered_map<std::string, BindKey*>::iterator iterEnd = m_mapBindKey.end();
while (iter != iterEnd)
{
SAFE_DELETE(iter->second);
++iter;
}
m_mapBindKey.clear();
}
}
CInput::AddBindFunction()
- 지정한 입력 조건을 모두 만족했을 때, 호출할 메소드를 BindKey 구조체에 등록해주는 함수를 만든다.
-- 어떤 클래스에서 함수를 등록할 지 알 수 없으므로, 템플릿을 사용해야 한다.
-- 등록만 하는 함수이므로, 값을 반환하지 않는다.
-- 인자로 이름, 등록한 클래스의 종류, 함수 포인터를 받는다.
--- 인자로 받은 이름에 해당하는 BindKey를 찾는다. 만약 없다면 과정을 중단한다.
--- 찾았으면, BindFunction 구조체를 동적 할당하고,
---- void 포인터에 객체 주소를 등록하고
---- 함수 포인터 변수에 객체 주소와 함수를 bind()함수를 통해 등록한 뒤
---- BindKey 구조체의 vecFunction에 삽입한다.
public:
template <typename T>
void AddBindFunction(const std::string& KeyName,
Input_Type Type,
T* Object, void (T::* Func)())
{
BindKey* Key = FindBindKey(KeyName);
if (!Key)
return;
BindFunction* Function = new BindFunction;
Function->Obj = Object;
// 멤버함수를 등록할때 함수주소, 객체주소를 등록해야 한다.
Function->func = std::bind(Func, Object);
Key->vecFunction[(int)Type].push_back(Function);
}
- 위에서 동적할당을 해줬으므로 당연히 제거 과정도 필요하다.
- BindKey 구조체 안의 vecFunction을 순회를 돌며 제거해주어야 하는데,
BindKey 구조체는 list에서 모아서 관리하고, 소멸자에서 iterator 순회를 돌며 제거되고 있다.
그러므로 소멸자에서 순회를 돌며 각각의 BindKey가 제거되기 전에 먼저 vecFunction 순회를 돌아서 제거해주면 되는 것이다.
//기존 코드 수정: VecFunction 동적할당 해제
CInput::~CInput()
{
{
std::unordered_map<unsigned char, KeyState*>::iterator iter = m_mapKeyState.begin();
std::unordered_map<unsigned char, KeyState*>::iterator iterEnd = m_mapKeyState.end();
while (iter != iterEnd)
{
SAFE_DELETE(iter->second);
++iter;
}
m_mapKeyState.clear();
}
{
std::unordered_map<std::string, BindKey*>::iterator iter = m_mapBindKey.begin();
std::unordered_map<std::string, BindKey*>::iterator iterEnd = m_mapBindKey.end();
while (iter != iterEnd)
{
//BindKey를 제거하기 전 그 안의 VecFunction 배열의 동적할당을 먼저 제거한다.
size_t size = iter->second->VecFunction.size();
for (size_t i = 0; i < size; ++size)
{
SAFE_DELETE(iter->second->VecFunction[i]);
}
SAFE_DELETE(iter->second);
++iter;
}
m_mapBindKey.clear();
}
}
- 이제 키를 등록할 때, 이런 방식으로 등록하게 된다. 물론 아직 완성은 안 됐으므로 작동은 안 된다.
bool CInput::Init()
{
AddBindKey("MoveFront", 'W');
AddBindKey("MoveBack", 'S');
AddBindKey("GunRotationLeft", 'A');
AddBindKey("GunRotationRight", 'D');
return true;
}