https://hyrule.tistory.com/111
*** 공부 방법 ***
1. 코딩을 해야 하는 부분은 첫 부분에 변수나 함수, 메소드에 대한 선언이 코드블럭으로 표시되어 있다. //ex) MakeFunction(); 2. 코드블럭 하단에는 해당 선언에 대한 구현 로직이 작성되어 있다. 처
hyrule.tistory.com
SetKeyCtrl();
SetKeyAlt();
SetKeyShift();
- 편의를 위해 내가 지정한 키에 키조합을 추가하는 함수를 추가한다.
- 인자로 이름과, 메소드명에 있는 키를 활성화시킬건지 여부를 bool변수로 받는다. 이 때, bool변수는 따로 입력하지 않으면 기본값을 true로 전달한다.
- 인자로 받은 이름에 해당하는 키 조합이 있는지 찾고, 해당 키 조합에서 눌러야하는 키 조건을 변경해준다.
(SetKeyCtrl -> 컨트롤 키가 눌렸을 때(true))
void CInput::SetKeyCtrl(const std::string& Name, bool Ctrl)
{
BindKey* Key = FindBindKey(Name);
if (!Key)
return;
Key->Ctrl = Ctrl;
}
void CInput::SetKeyAlt(const std::string& Name, bool Alt)
{
BindKey* Key = FindBindKey(Name);
if (!Key)
return;
Key->Alt = Alt;
}
void CInput::SetKeyShift(const std::string& Name, bool Shift)
{
BindKey* Key = FindBindKey(Name);
if (!Key)
return;
Key->Shift = Shift;
}
- 키 상태를 매 프레임마다 업데이트하는 구조를 짠다.
- DeltaTime을 인자로 받아서 마우스의 상태, 키눌림 상태, 키조합을 확인하는 개별 메소드를 생성한다.
- UpdateMouse(), UpdateKeyState(), UpdateBindKey()
- 그리고 CInput::Update() 메소드 안에서 이 메소드들을 호출한다.
void CInput::Update(float DeltaTime)
{
UpdateMouse(DeltaTime);
UpdateKeyState(DeltaTime);
UpdateBindKey(DeltaTime);
}
우선 마우스는 나중에 하고 키보드 입력들부터 처리한다.
CInput::UpdateKeyState()
- 우선 Ctrl, Alt, Shift키를 확인한다.
cf) Ctrl = VK_CONTROL, Alt = VK_MENU, Shift = VK_SHIFT
-- 해당 키의 눌림 여부는 CInput 클래스에 변수를 생성하여 별도로 전달해준다.
- m_mapKeyState를 순회돌면서 GetAsyncKey 함수를 통해 어떤 키가 눌렸는지 확인한다.
-- 일단 이번 프레임에 키 입력이 들어왔는지를 확인하고,
논리 연산자를 통해 KeyState의 다음 사항들을 변경한다.
-- 확인해야할 사항은,
--- 키를 이번에 '처음 눌렀는지' -> Down
--- 키를 아까부터 '누르고 있는 상태인지' -> Push
--- 전 프레임까지만 키를 누르다가 '뗐는지 ' -> Up
//CInput.h
private:
bool m_Ctrl;
bool m_Alt;
bool m_Shift;
//CInput.cpp
void CInput::UpdateKeyState(float DeltaTime)
{
if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
m_Ctrl = true;
else
m_Ctrl = false;
if (GetAsyncKeyState(VK_MENU) & 0x8000)
m_Alt = true;
else
m_Alt = false;
if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
m_Shift = true;
else
m_Shift = false;
auto iter = m_mapKeyState.begin();
auto iterEnd = m_mapKeyState.end();
for (; iter != iterEnd; ++iter)
{
bool KeyPush = false;
if (GetAsyncKeyState(iter->second->key) & 0x8000)
{
KeyPush = true;
}
// 키를 눌렀을 경우
if (KeyPush)
{
// Down과 Push 모두 false라면 이 키를 지금 누른 것이다.
// 그러므로 둘다 true로 변경한다.
if (!iter->second->Down && !iter->second->Push)
{
iter->second->Down = true;
iter->second->Push = true;
}
// Down과 Push 둘중 하나라도 true라면 Down은 false가
// 되어야 한다. Push는 이미 위에서 true로 변경되었다.
else
iter->second->Down = false;
}
// 키가 안눌러졌을 경우 Push가 true라면
// 이전 프레임에 키를 누르고 있다가 지금 떨어졌다는 것이다.
else if (iter->second->Push)
{
iter->second->Up = true;
iter->second->Push = false;
iter->second->Down = false;
}
else if (iter->second->Up)
iter->second->Up = false;
}
}
- 위에서 Down, Push, Up 상태를 입력받긴 했는데,
어떻게 처리해봐야 할까?
- 같은 키를 누르더라도, 언제 작동할지는 달라진다.
- ex)리그오브레전드 사이온의 q스킬
Q -> Down: 스킬 시전 시작.
Q -> Push: 누르고 있는 동안 계속 차지
Q -> Up: 뗀 순간 타격
그렇기 때문에, BindKey 구조체 안에 있는 키 입력시 작동하는 함수들의 목록을 저장하고 있었던 배열
VecFunction을 열거체를 사용하여 이중 배열로 선언해준다.
enum class Input_Type
{
Down,
Push,
Up,
End
};
struct BindKey
{
// 이름
std::string Name;
// 어떤 키를 사용하는지.
KeyState* key;
bool Ctrl;
bool Alt;
bool Shift;
//이차원 배열로 선언!
std::vector<BindFunction*> vecFunction[(int)Input_Type::End];
BindKey() :
key(nullptr),
Ctrl(false),
Alt(false),
Shift(false)
{
}
};
- 이제 Down, Push, Up에 대한 함수를 각각 다른 위치에 삽입해야 한다.
-- 지난번에 만들었던 AddBindFunction() 함수도 이에 맞게 변경해준다. 소멸자도 적절히 변경해야 한다!
--> 추가로 방금 새로 만든 열거체 타입을 인자로 받고, 해당 인자를 배열의 인덱스로 사용하면 된다.
1. 기존 메소드 변경: CInput.h
public:
template <typename T>
void AddBindFunction(const std::string& KeyName,
Input_Type Type,
T* Object, void (T::* Func)())
{
//↑추가로 열거체 Input_Type을 받고 있다.
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);
}
DECLARE_SINGLE(CInput)
};
2. 기존 메소드 변경: CInput.cpp
CInput::~CInput()
{
{
auto iter = m_mapKeyState.begin();
auto iterEnd = m_mapKeyState.end();
for (; iter != iterEnd; ++iter)
{
SAFE_DELETE(iter->second);
}
m_mapKeyState.clear();
}
{
auto iter = m_mapBindKey.begin();
auto iterEnd = m_mapBindKey.end();
for (; iter != iterEnd; ++iter)
{
for (int i = 0; i < (int)Input_Type::End; ++i)
{
size_t Size = iter->second->vecFunction[i].size();
for (size_t j = 0; j < Size; ++j)
{
SAFE_DELETE(iter->second->vecFunction[i][j]);
}
}
SAFE_DELETE(iter->second);
}
m_mapBindKey.clear();
}
}
CInput::UpdateBindKey()
- m_mapBindKey 트리를 순회 돈다.
- 멤버 변수인 KeyState를 확인해서 만약 KeyState가 눌렸음을 확인하면,
추가로 해당 키가 Ctrl, Alt, Shift와 같이 눌려야 작동하는지 확인한다.
--1. Down인지 Push인지 Up인지 확인하고,
--2. Ctrl + Alt + Delete 처럼, 두개 이상의 기능 키와 결합이 될 수도 있으므로 전부 동시에 확인해야 한다!
void CInput::UpdateBindKey(float DeltaTime)
{
auto iter = m_mapBindKey.begin();
auto iterEnd = m_mapBindKey.end();
for (; iter != iterEnd; ++iter)
{
if (iter->second->key->Down &&
iter->second->Ctrl == m_Ctrl &&
iter->second->Alt == m_Alt &&
iter->second->Shift == m_Shift)
{
size_t Size = iter->second->vecFunction[(int)Input_Type::Down].size();
for (size_t i = 0; i < Size; ++i)
{
iter->second->vecFunction[(int)Input_Type::Down][i]->func();
}
}
if (iter->second->key->Push &&
iter->second->Ctrl == m_Ctrl &&
iter->second->Alt == m_Alt &&
iter->second->Shift == m_Shift)
{
size_t Size = iter->second->vecFunction[(int)Input_Type::Push].size();
for (size_t i = 0; i < Size; ++i)
{
iter->second->vecFunction[(int)Input_Type::Push][i]->func();
}
}
if (iter->second->key->Up &&
iter->second->Ctrl == m_Ctrl &&
iter->second->Alt == m_Alt &&
iter->second->Shift == m_Shift)
{
size_t Size = iter->second->vecFunction[(int)Input_Type::Up].size();
for (size_t i = 0; i < Size; ++i)
{
iter->second->vecFunction[(int)Input_Type::Up][i]->func();
}
}
}
}
입력 부분 구현이 완료되었다.
[ 사용해보기 ]
- 입력 기능을 사용하고자 하는 게임오브젝트에 Input 헤더를 포함시킨다.
-> 지금은 CPlayer에서 사용해볼 예정.
- 특정 입력을 했을 때, 처리해줄 함수를 생성한다.
지금 구현한 내용은 이동 뿐이므로 전/후 이동 함수와 총 좌우 회전 함수 총 4개를 생성하고,
Update() 메소드에서 기존의 입력 관련 코드들은 제거하고 관련 로직만 다 옮겨 준다.
void CPlayer::Update(float DeltaTime)
{
//각도를 통해 총구 방향 구하기
m_Dir.x = cosf(DegreeToRadian(m_GunAngle));
m_Dir.y = sinf(DegreeToRadian(m_GunAngle));
//총구 끝의 좌표 구하기
m_GunTipPos.x = m_Pos.x + m_Dir.x * m_GunLength;
m_GunTipPos.y = m_Pos.y + m_Dir.y * m_GunLength;
}
void CPlayer::MoveFront()
{
m_Pos += m_Dir * 400.f * DeltaTime;
}
void CPlayer::MoveBack()
{
m_Pos -= m_Dir * 400.f * DeltaTime;
}
void CPlayer::GunRotationLeft()
{
m_GunAngle -= 180.f * DeltaTime;
}
void CPlayer::GunRotationRight()
{
m_GunAngle += 180.f * DeltaTime;
}
- 문제가 발생했다. 따로 만든 메소드들에 DeltaTime을 전달해 줄 방법이 없어졌다.
-> DeltaTime을 관리하는 CGameManager에 GetDeltaTime() 메소드를 만들어서 가져오자
-> 좀더 편하게 만들고 싶다면 GameInfo 헤더에
#define 매크로를 등록하여 DELTA_TIME을 입력하면 위 함수가 실행되게 하자.
//GameManager.h
public:
float GetDeltaTime() const
{
return m_DeltaTime;
}
//GameInfo.h
#define DELTA_TIME CGameManager::GetInst()->GetDeltaTime()
- 입력 콜백 메소드에 DELTA_TIME을 넣어 에러를 없애 준다.
//Player.cpp
void CPlayer::MoveFront()
{
m_Pos += m_Dir * 400.f * DELTA_TIME;
}
void CPlayer::MoveBack()
{
m_Pos -= m_Dir * 400.f * DELTA_TIME;
}
void CPlayer::GunRotationLeft()
{
m_GunAngle -= 180.f * DELTA_TIME;
}
void CPlayer::GunRotationRight()
{
m_GunAngle += 180.f * DELTA_TIME;
}
- 우선 지난번에 CInput::Init() 메소드에 등록했던 키조합 등록 AddBindKey() 메소드들을 CPlayer::Init() 부분에서 옮겨서 실행해 준다.
문자 키의 경우 무조건 대문자로 등록해 주어야 함을 명심하자.
** AddBindKey를 통해 키조합을 등록하면, 먼저 등록한 순서대로 키 입력을 확인한다.
W 입력 처리 -> S 입력 처리 -> A 입력 처리 -> D 입력 처리 순서가 된다는 뜻이다.
그런데 이렇게 되면 현재 게임 로직에 약간 문제가 발생한다.
총의 회전이 이동 방향을 결정하기 때문에 총의 회전이 먼저 계산되고, 이후에 그 방향으로의 이동이 계산되어서 현재 입력 기준으로 캐릭터가 이동해야 하는데,
지금은 이전 프레임의 각도 기준으로 먼저 이동을 하고, 총구를 회전시키기 때문이다.
지금이야 체감이 안되지만 오버워치나 리그오브레전드 같은 게임에서 이랬다간 욕먹는다.
그러므로 우선순위가 높은 입력들을 먼저 배열에 추가해주어야 한다!
언젠가 때가 되면 우선순위 큐 등을 이용해서 먼저 처리하는 법도 배울 것이다...
- CInput::AddBindFunction() 메소드를 호출하여 해당 키가 눌렸을때 어떤 함수를 호출할 것인지도 추가한다.
bool CPlayer::Init()
{
//입력 처리 우선순위 순서대로 등록: 회전 먼저 -> 이후 전/후진
CInput::GetInst()->AddBindKey("GunRotationLeft", 'A');
CInput::GetInst()->AddBindKey("GunRotationRight", 'D');
CInput::GetInst()->AddBindKey("MoveFront", 'W');
CInput::GetInst()->AddBindKey("MoveBack", 'S');
//"MoveFront"라는 이름의 키 조합에 등록된 버튼(현재 'W')를 눌렀을 때,
//그 버튼을 계속 누르고 있는(Push) 상태라면,
//이 객체에서
//MoveFront 함수를 호출하겠다.
CInput::GetInst()->AddBindFunction("MoveFront", Input_Type::Push,
this, &CPlayer::MoveFront);
CInput::GetInst()->AddBindFunction("MoveBack", Input_Type::Push,
this, &CPlayer::MoveBack);
CInput::GetInst()->AddBindFunction("GunRotationLeft", Input_Type::Push,
this, &CPlayer::GunRotationLeft);
CInput::GetInst()->AddBindFunction("GunRotationRight", Input_Type::Push,
this, &CPlayer::GunRotationRight);
//
//...이외의 코드들...
//
return true;
}
이제 최종적으로 입력 시스템 구현은 끝났다.
실행시켜 보자...
물론 겉보기에는 여전히 전혀 차이가 없다...
하지만 이제 좀 더 빠르고 간결한 코드로 게임오브젝트를 구현할 수 있게 되었다.
이제 몬스터나 총알 발사등을 구현하여 겉모습을 바꿔보자...
**** 앞으로는 컴파일을 64비트로 하자. 64비트로 연습해야 된다! *****
원래 처음부터 64비트로 했어야 하는데 까먹었다...
Bin폴더와 BinObj 폴더를 제거하고, x64로 변경해준다.
'WIN32API FrameWork > 한단계씩 직접 구현' 카테고리의 다른 글
21. 총알 발사하기 / 목표물까지의 각도 구하기 (0) | 2022.05.20 |
---|---|
20. (활용)몬스터 만들기 (0) | 2022.05.20 |
18. Input 입력 구조 설계 1 (0) | 2022.05.19 |
17. Scene을 통한 최종 출력 (0) | 2022.05.18 |
16. (기타 설정)복사 생성자, TypeID 지정 (0) | 2022.05.18 |