1. WinMain - Jenga 게임 메인 로직
본 게시물은 PhysX와 DirectX11의 이해를 위해서,
아래 Github 주소에 있는 Jenga project 코드를 분석하는 글입니다.
https://github.com/JaninaAlthoefer/JengaGame
GitHub - JaninaAlthoefer/JengaGame: A digital implementation of the game Jenga. Written in C++ and using NVidia PhysX (physics e
A digital implementation of the game Jenga. Written in C++ and using NVidia PhysX (physics engine) and DirectX. - GitHub - JaninaAlthoefer/JengaGame: A digital implementation of the game Jenga. Wr...
github.com
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
if(!InitWindow(hInstance, nShowCmd, WIDTH, HEIGHT, true))
{
MessageBox(0, L"Window Initialization - Failed",
L"Error", MB_OK);
exit(1);
}
- 윈도우 데스크톱 마법사를 실행하면 생성되는 코드 중 일부
- 이 부분은 여타 프로젝트와 별 다른 점 없음
- 화면의 크기를 넘겨줘서 AdjustWindowRect를 호출해주는 것이 조금 다르다.
class Physics *phys;
class Direct3D *d3d;
phys = new Physics();
d3d = new Direct3D(hwnd);
- Physics 클래스와 Direct3D 클래스의 인스턴스를 생성하는 코드
- Physics 클래스는 PhysX를 이용해 물리 관련 처리를 하는 클래스 (다른 게시글에서 통째로 다룰 예정)
- Direct3D 클래스는 DirectX11을 이용해 화면에 렌더링되는 모든 부분을 다루고 있다.
if(!d3d->InitScene())
{
MessageBox(0, L"Scene Initialization - Failed",
L"Error", MB_OK);
exit(1);
}
- d3d->InitScene()는 Scene을 구성하는 기본적인 요소들을 생성해주는 말 그대로 Scene을 초기화하는 함수
if(!d3d->InitDirectInput(hInstance, hwnd))
{
MessageBox(0, L"Direct Input Initialization - Failed",
L"Error", MB_OK);
return 0;
}
- 입력에 따른 처리를 하기 위해 DirectInput을 초기화 하는 함수
class AI *ai;
if (playAI)
{
ai = new AI(phys, d8Sound);
hasAI = true;
}
- 나와 대결할 젠가AI 를 만든다.
MSG msg;
ZeroMemory(&msg, sizeof(MSG));
while(true)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
- PeekMessage는 메시지가 없을 때도 로직이 돌게끔 해준다.
- 앞으로 나올 else 문에서 게임 로직이 돌아간다
else
{
// run game code here
vector<int> leftOverBlocks = phys->onGround();
if((!phys->stillStanding()||!leftOverBlocks.empty()||ai->giveUp())&&standing)
{ //ai giveup overlay missing
standing = false;
selecter = -1;
float h = phys->getTowerHeight();
d3d->SetGameOverCamera();
phys->dropBlock(&selecter);
statePicking = true;
d8Sound->playFallingSF();
//MessageBox(0, L"Game Over", L"Error", MB_OK);
}
- stillStanding 함수는 Physics의 멤버 함수로,
- 현재 타워의 높이를 가져와서 최소 높이 * 0.9보다 크다면 서 있다는 것으로 간주하는 것으로 보인다.
bool Physics::stillStanding()
{
return (getTowerHeight() >= (0.9f * minHeight));
}
- onGround 함수의 구현은 아래와 같다.
vector<int> Physics::onGround()
{
vector<int> onGround;
for (int i = 3; i < numBlocks; i++)
{
PxVec3 temp = blockArray[i]->getGlobalPose().p;
if (temp.y <= (2.0f * halfBoxShortSide))
onGround.push_back(i);
}
return onGround;
}
- blockArray는 PxRigidDynamic 클래스 객체 block의 포인터를 담는 배열이다.
vector<PxRigidDynamic*> blockArray;
- PxRigidDynamic 클래스는 동적인 물체를 모델링하는 데 사용되며,
물체의 위치, 회전, 속도, 질량 등을 추적하고 시뮬레이션하는 데 사용된다.
- getGlobalPose() 멤버 함수는 현재 물체의 위치와 회전 값을 가져온다. (PxTransform을 반환한다)
- 그리고 PxTransform의 p 멤버는 position을 의미하므로,
PxVec3 형인 temp에는 현재 블록의 위치값이 담기게 된다.
if (temp.y <= (2.0f * halfBoxShortSide))
onGround.push_back(i);
- 이 부분은 temp(현재 블록의 위치)가 2.0f * halfBoxShortSide보다 작다면 바닥에 떨어진 것으로 간주하고,
- onGound 배열에 인덱스를 추가한다.
- 그리고 이 배열을 반환한다.
bool AI::giveUp()
{
return (wrongLayout > 7);
}
- 조건문 중 하나인 ai의 giveUp은 ai가 기권을 인정했는지 여부를 나타내는 함수인데,
- wrongLayout가 8 이상이면 해당 값이 true가 나오게 된다.
- wrongLayout은 AI의 pickBlock 코드에서 어떤 조건에 의해 값이 증가하는데,
//stop if picking specific block would DEFINITELY topple tower
if ((lay == LEFT)||(lay == RIGHT)||(lay == MIDDLE)||(lay == LEFTRIGHT))
{
wrongLayout++;
return;//*/
}
- 탑이 무너질 가능성이 높은 경우 wrongLayout 값을 1 증가시키고 pickBlock 함수를 빠져나오는 것으로 보인다.
- 즉, 이 횟수가 8회 이상이면 AI가 진 것으로 간주 한다는 것.
- 마지막으로 standing 변수는 SPACE를 누르게 되면 true로 바뀌는데,
case VK_SPACE: { phys->buildTower(); standing = true;
statePicking = true;
float h = phys->getTowerHeight();
d3d->SetPickingStateCamera(h/2);
initMP();
if (playAI)
ai->setCurrentState(PICK);
currPlayer = firstPlayer;
break; }
- 탑이 제대로 세워졌는지를 검사하는 코드로 보인다.
- ai가 기권패 됨과 동시에, 탑이 세워졌다면 if문 내부로 진입
if((!phys->stillStanding()||!leftOverBlocks.empty()||ai->giveUp())&&standing)
{ //ai giveup overlay missing
standing = false;
selecter = -1;
float h = phys->getTowerHeight();
d3d->SetGameOverCamera();
phys->dropBlock(&selecter);
statePicking = true;
d8Sound->playFallingSF();
//MessageBox(0, L"Game Over", L"Error", MB_OK);
}
- if문 내부에서는 탑이 제대로 세워졌는지를 나타내는 것으로 추정되는 standing 변수를 false로 세팅
- SetGameOverCamera 함수는 카메라의 위치를 게임 오버 위치로 세팅하는 것으로 추측된다.
- dropBlock 함수는 현재 선택된 블록을 지정된 위치에 놓고, 블록에 대한 물리 시뮬레이션을 시작한다.
void Physics::dropBlock(const int *i)
{
if (currBlock < 0) return;
gScene->addActor(*blockArray[currBlock]);
blockArray[currBlock]->setLinearVelocity(PxVec3(0.0f, 0.0f, 0.0f));
blockArray[currBlock]->setAngularVelocity(PxVec3(0.0f, 0.0f, 0.0f));
blockArray[currBlock]->setActorFlag(PxActorFlag::eDISABLE_GRAVITY, false);
//stop here if re-pickup - not needed anymore??
if (highestBlocks[4]==currBlock)
return;
if (highestBlocks[3]==currBlock)
return;
adjustNumHighestBlocks();
highestBlocks.push_back(currBlock);
highestBlocks.erase(highestBlocks.begin());
currBlock = -1;
}
- blockArray는 위에서 언급한 바와 같이, PxRigidDynamic 포인터의 배열이다.
- currBlock은 정수형으로, 현재 선택된 인덱스를 의미한다.
- 이 값이 0보다 크다면, currBlock을 인덱스로 blockArray에 접근해서 PxRigidDynamic을 Scene의 Actor로 추가한다.
- setLinearVelocity는 Vec3로 각 축의 초기 속도를 세팅하는 함수이며, 전부 0으로 밀어준다.
- setAngularVelocity는 각속도를 세팅하는 함수이다.
- setActorFlag(PxActorFlag::eDISABLE_GRAVITY, false)는 선택된 블록에 대해 중력을 활성화하는 코드이다.
if (highestBlocks[4]==currBlock)
return;
if (highestBlocks[3]==currBlock)
return;
- 이 부분은 현재 선택된 블록이 가장 위에 있는 블록인지 확인하는 코드이다.
- 왜 3번과 4번 인덱스인지는 확인되지 않는다.
void Physics::adjustNumHighestBlocks()
{
numBlocksHighest++;
numBlocksHighest = numBlocksHighest % 3;
}
- adjustNumHeightBlocks 함수는 블럭의 높이를 1 증가시키고, 그 값을 0, 1, 2 중 하나로 유지시킨다.
- 아직은 이해가 되지 않음
highestBlocks.push_back(currBlock);
highestBlocks.erase(highestBlocks.begin());
- highestBlocks는 아마 가장 위에 있는 블럭들을 저장하는 벡터로 보인다.
- 젠가 룰이 한 층당 3 개의 블럭으로 유지되므로, 하나가 추가되니 하나를 지워주는 것으로 보인다.
- statePicking은 bool 변수인데, 상태를 의미하는 변수 같지만 정확한 의미는 알 수 없다.
- 사운드는 다루지 않을 것
bool Physics::outOfTower(const int *num)
{
if (*num < 0) return false;
PxVec3 tempPose = blockArray[*num]->getGlobalPose().p;
if (tempPose.x >= 5.7f*halfBoxMiddleSide || tempPose.x <= -5.7f*halfBoxMiddleSide ||
tempPose.z >= 5.7f*halfBoxMiddleSide || tempPose.z <= -5.7f*halfBoxMiddleSide)
{
currBlock = *num;
return true;
}
else
return false;
}
- 주어진 인덱스의 블록이 탑의 바깥에 있는지 확인하는 함수
- blockArray의 인덱스로 접근해서 해당 블록의 Transform에서 위치 값을 가져온 후,
- 해당 x, z축과 상수값을 비교해서 더 멀리 나가 있다면 타워 밖에 있다고 간주한다.
if(phys->outOfTower(&selecter)&&statePicking)
{
pickedBlock = selecter;
selecter = -1;
phys->disableCollision(&pickedBlock);
//MessageBox(0, L"OutOfTower", L"Error", MB_OK);
statePicking = false;
}
- statePicking은 블록을 집은 상태를 bool변수로 나타내는 듯 하다.
- outOfTower에 현재 선택된 블록의 인덱스를 넣고, 그 블록이 탑의 바깥에 있으면,
- 집은 블록을 현재 선택된 블록으로 저장하고, selector에 -1을 넣어 아무 것도 선택되어 있지 않다는 것을 나타내는 듯 하다.
- 아마 selector는 선택된 블록을 의미하는 듯 하다(집은 블록(pickedBlock)과는 다른 의미)
- pickedBlock 인덱스를 인자로 phys 객체의 disableCollision 함수를 호출하면
void Physics::disableCollision(const int *num)
{
if (*num < 0)
return;
currBlock = *num;
gScene->removeActor(*blockArray[currBlock]);
blockArray[currBlock]->setActorFlag(PxActorFlag::eDISABLE_GRAVITY, true);
}
- Scene에서 해당 Actor를 삭제하고,
- 해당 인덱스의 Actor에 접근하여 중력을 해제한다
- PhysX에서는 각 객체마다 이런 플래그로 중력을 관리하는구나!
- statePicking 은 블록을 집었는가를 의미하므로, false로 세팅한다.
if (!statePicking)
{
float h = phys->getTowerHeight();
d3d->SetPlacingStateCamera(h);
}
- 바로 그 아래, 블록이 탑 외부에 있었다면~ if문 내부로 진입해서 카메라의 높이를 다시 세팅해준다.
float time = GetDeltaTime();
//manage AI
if (currPlayer == aiPlayer && standing)
{
states stat = ai->getCurrentState();
if (stat == FINISHED)
{
changePlayer();
ai->setCurrentState(PICK);
}
else
ai->play(time);
}
- 현재 플레이어가 ai이고, 탑이 잘 서있다면 if문으로 진입한다.
- 현재 ai의 상태가 FINISHED라면, changePlayer를 통해 턴을 돌린다.
void changePlayer()
{
if (currPlayer == aiPlayer || currPlayer == secondPlayer)
currPlayer = firstPlayer;
else if (currPlayer == firstPlayer && !playAI)
currPlayer = secondPlayer;
else if (currPlayer == firstPlayer && playAI)
currPlayer = aiPlayer;
}
- ai->setCurrentState(PICK); 이부분은
void AI::setCurrentState(states s)
{
currentState = s;
if (!possibleBlocks.empty() && s == PICK)
{
wrongLayout = 0;
possibleBlocks.clear();
}
}
- 가능한 블록들이 비어있지 않고, 상태가 PICK이면
- wrongLayout을 0으로 초기화해준다
(이 값은 8 이상이면 기권패하는 값인데, 누적되면 갑자기 우리가 이기게 되므로 당연히 한턴에 적용되어야 하는 것이다)
- possibleBlocks.clear(); 는 그냥 턴이 지나갔으니 가능한 블록들을 지워주는 것 같다.
- 한마디로 이 코드는 ai를 초기화해주는 코드인 듯 하다.
else
ai->play(time);
- 그 바로 아래 else문은 stat가 FINISHED가 아닌 경우이므로, 이 경우 ai가 계속 경기를 진행한다.
- time은 delta time을 의미한다.
phys->stepPhysX(time);
d3d->DetectCamInput(time, hwnd);
- stepPhysX는 시간값을 받고 물리 시뮬레이터를 계속 돌리는 함수
void Physics::stepPhysX(float time)
{
timeAccumulator += time;
if (timeAccumulator < (stepSize))
return;
timeAccumulator -= stepSize;
gScene->simulate(stepSize);
gScene->fetchResults(true);
}
- detectCamInput은 키보드 입력에 따라 카메라를 조작하는 함수
- 현재 엔진의 Script에 해당하는 부분
void Direct3D::DetectCamInput(double time, HWND hwnd)
{
DIMOUSESTATE mouseCurrState;
BYTE keyboardState[256];
DIKeyboard->Acquire();
DIMouse->Acquire();
DIMouse->GetDeviceState(sizeof(DIMOUSESTATE), &mouseCurrState);
DIKeyboard->GetDeviceState(sizeof(keyboardState),(LPVOID)&keyboardState);
if(keyboardState[DIK_A] & 0x80)
{
SetCamX(-37.0f*time);
}
if(keyboardState[DIK_D] & 0x80)
{
SetCamX(+37.0f*time);
}
if(keyboardState[DIK_W] & 0x80)
{
SetCamY(+1.5f*time);
}
if(keyboardState[DIK_S] & 0x80)
{
SetCamY(-1.5f*time);
}
if(keyboardState[DIK_Q] & 0x80)
{
SetCamZ(+2.5f*time);
}
if(keyboardState[DIK_E] & 0x80)
{
SetCamZ(-2.5f*time);
}
if(mouseCurrState.lZ < 0.0f)
{
SetCamZ(+2.5f*time);
}
if(mouseCurrState.lZ > 0.0f)
{
SetCamZ(-2.5f*time);
}
mouseLastState = mouseCurrState;
return;
}
- UpdateScene은 말 그대로 현재 씬을 업데이트해주는 함수
d3d->UpdateScene(phys);
- cube, ground, skybox의 좌표계 변환을 담당
void Direct3D::UpdateScene(Physics *phys)
{
for (int i=0; i < numBlocks; i++)
{
//Reset cube matrix
blockWorldArray[i] = XMMatrixIdentity();
//cube world space matrix
float physMat[16];
phys->getBoxPoseRender(&i, physMat);
blockWorldArray[i] = XMMATRIX(physMat);
}
//Reset ground matrix
groundWorld = XMMatrixIdentity();
Translation = XMMatrixTranslation( 0.0f, -(1.0f/*-halfBoxShortSide*/), 0.0f );
Scale = XMMatrixScaling( 150.0f, 1.0f, 150.0f );
groundWorld = Scale * Translation;
//Reset skyWorld
skyWorld = XMMatrixIdentity();
Scale = XMMatrixScaling( 5.0f, 5.0f, 5.0f );
Translation = XMMatrixTranslation( XMVectorGetX(camPosition), XMVectorGetY(camPosition), XMVectorGetZ(camPosition) );
skyWorld = Scale * Translation;
}
- DrawScene 함수는 현재 씬을 그려주는 함수
- ConstantBuffer를 세팅하고, 그려주기 위한 각종 세팅들을 한 후에,
- 최종적으로, DrawIndexed 함수를 통해 RenderTarget에 그리고, Present로 화면에 뿌려준다.
- 특이한 점은, 이 함수 내에서 standing 변수의 상태를 검사해서 GameOver인지, Turn Change를 결정한다.
- 그 다음 단계로 낙하중인 블록이 있는지 확인하고, 있다면 사운드를 출력한다.
for (int i = 0; i < numBlocks; i++)
{
bool result = phys->fallingBlock(&i);
if (standing&&result)
d8Sound->playCollisionSF(i);
}
- 여기까지가 Jenga game의 메인 로직이다.
- 다음 게시글부터는 각 클래스별로 디테일하게 들여다볼 예정이다.