PhysX/[Github] Jenga 분석

1. WinMain - Jenga 게임 메인 로직

헛둘이 2023. 2. 26. 12:43

본 게시물은 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의 메인 로직이다.

- 다음 게시글부터는 각 클래스별로 디테일하게 들여다볼 예정이다.