저번 까지는 화면에 우주선을 움직이게 까지 했다.
이번에는 게임의 하이라이트 충돌이다.

물체간에 부딛히는 것을 인식하게 하는 것이다.
여기에는 실제로 많은 물리 지식과 수학적 지식이 필요하다.
나도 책을 여러번 읽고 이해를 할 수 있었다.
여기에 충돌에 관한 물리적 내용을 다루지는 않을 것이다.
실제 자세한 구현과 코드의 내용을 보려면 직접 책을 참고하는 것이 좋을 것이다!
(2d 게임 프로그래밍, 찰스 켈리)

이 글의 목적은 게임 상의 우주선이 게임 화면 안에서 갇혀있는 것이다.
우주선이 왔다리 갔다리 하는데 화면 끝으로 이동했을 때 넘어가는 것이 아니라
공이 벽에 튕기듯이 튕겨서 게임 화면 안에서 계속 돌아댕기는 프로그램을 만들 것이다.

충돌을 다루기 위해 벡터를 사용해야한다.
DirectX에서 제공하는 벡터 타입이다.
벡터 연산들도 제공한다.

그러나 이름이 길고 쓰기 불편하므로 우리가 이해하기 쉽게 간단한 이름으로

캡슐화 시켰다.


(벡터 재정의)



(벡터 연산)


위와 같이 벡터 타입과, 벡터 연산들을 우리가 사용하기 편하게 함수안에 넣어두었다.

그 다음 충돌을 다루게 하기 위해 Entity 라는 이름의 클래스를 만들어 볼 것이다.
Entity는 해석하면 객체이지만, 여기서는 고유명사처럼 Entity라고 말할 것이다.

Entity는 우리가 충돌을 인식하게 하기 위해 우리가 원하는 객체들을 Entity 클래스로 선언할 것이다.

그리고 각종 충돌에 관한 함수들을 집어 넣을 것이다.



(entity 이름 영역)


Entity의 이름 영역에는 충돌타입과 중력상수가 적혀있다.
충돌 타입은
원형 충돌
사각형 충돌
회전 사각형 충돌
복합 충돌
으로 구분할 수 있다.

그러한 것들을 이름영역에 정의해두었다.

다음은 Entity 클래스의 속성 변수들을 볼 것이다.



(속성 변수)


가지고 있는 속성 변수에는 위와 같다.

그리고 protected 영역에 선언된 함수들이다.



(protected 함수)


위 함수들은 다른 클래스 밖에서 호출되지 않고 내부에서만 호출되게 하기 위해 protected 안에서 선언되어있다.

이제 public 영역의 함수들을 볼 것이다.



(생성자)


생성자이다.
초기값들을 설정하는 부분이다.

다음으로는 초기화 함수이다.



(초기화 함수)


초기화 함수가 조금 특이하다.
첫번째 인자가 게임포인터라는 것만 빼면 Image의 초기화 함수랑 똑같다.
심지어 내부에서 Image의 초기화 함수를 호출한다.

게임 포인터에서 입력을 input으로 저장해둔다.
게임 중 입력 받아놓은 지역변수 값들을 가져와서 저장해두는 역할을 한다.

(activate() & update())


activate() 함수는 Entity 객체가 충돌관련 연산이 일어나게 할지 활성화 여부를 활성화 시키는 것이다.

update 함수는 계산된 속도 변화를 적용시킨다.
이전에 충돌과 관련된 계산이 끝난 후 변화 속도를 deltaV에 저장해 두고 여기서 적용시킨다.

그 후 Image의 update를 호출한다.


(ai 함수)


AI 함수는 인공지능에 관련된 함수이다.
지금 현재 우리의 객체에는 인공지능이 들어가지 않는다.
그렇지만 Entity 클래스의 객체를 인스턴스화 할 수 있게 하기 위해 빈 함수로 제공해둔다.

이렇게 하면 Entity 클래스에서 객체를 직접 생성할 수 있다.



(충돌 함수)


충돌에 관련된 함수이다.
위 함수는 각 객체의 충돌 타입을 확인 한 후 각 객체의 충돌 타입에 맞게 다시 충돌탐지 함수들을 호출한다.
각 충돌 탐지에 관한 함수들, 내용들은
시간이 된다면 따로 정리해서 올릴 예정이다.

그 후 이 전까지와 달라진 점이 있다.
이젠 Entity를 사용해 각 객체를 호출 할 것이다.
그러기 전에 행성과 우주선이라는 클래스를 따로 만들 것이다.
그리고 이 우주선과 행성 클래스는 Entity 클래스를 상속한다.
우주선과 행성 클래스를 따로 만드는 이유는 각 객체의 값들을 따로 관리하는게 좋기 때문이다.
여기저기 섞여 있는 것보다는!

planet 부터 보자!


(planet 헤더)


헤더 파일에 planet 의 속성들이 들어있다.



(planet 생성자)


생성자는 간단히 이름영역에서 설정한 값들을
변수에 초기값으로 설정하는 역할을 한다.

다음은 ship 클래스이다.


(ship 헤더)


planet 과 같이 속성 설정값들이 있다. ( 이 값들은 애니메이션과 관련된 설정들도 있고 지난 글들에서 다루어왔던 값들이므로 설명은 생략하겠다. )

planet과 다른점은 update 함수가 있다.

이 update 함수는 우주선이 벽에 부딛히고 튕겨져 나오는 것을 작성하기 위해 여기서 상속받아서 작성하였다.


(ship의 생성자)


planet과 마찬가지로 ship의 생성자 또한 이름영역에서 설정된 값들을 가지고 초기값을 설정하며 만들어진다.



(update 함수)


update 함수이다.
이 함수에서는 맨 먼저 Entity 의 update가 실행된다. Entity의 update에서 충돌 결과의 움직임이 적용이 되고 Image의 update가 실행이 된 후이다. 마지막으로 화면에 그려지기 전에 이루어지는 update이다.

Entity의 update에서 적용된 속도 값들을 이용하여 해당 우주선의 화면상 위치를 잡는 모습이 보이고
추가적으로 우주선이 천천히 뱅글뱅글 돌아가도록 spriteData의 angle 값을 바꾸고 있는 모습도 보인다.

그 뒤로 이제 화면에 부딛히는 과정을 체크한 후 부딛히면 날라온 방향에서 부딛힌 축에 뒤집히게 -1을 곱하여 반대로 뒤집어준다.

이제 실제 spacewar 파일에서 우주선과 행성 등을 다시 선언해주어야한다.
이 전까지는 Image로 선언했기 때문이다.



(재선언)


이 때 우주선은 Ship으로 행성은 Planet으로 선언했다.
둘 모두 Entity를 상속했고 Entity이고 Entity 는 Image를 상속했다.

실행해보면 우주선이 게임화면에서 계속 튕기면서 돌아댕기는 모습을 볼 수 있다.



(우주선 표류)



(우주선 표류)


다음에는 두 객체가 충돌하는 것에 관련해서 알아볼 것이다.
아까 Entity에서 충돌에 관련해 만들어두었지만 아직 안썼었다.

(* 참조 - 2d 게임 프로그래밍, 찰스 켈리)


지난 포스팅에서 우주선 자체가 깜빡 거리는 것 까지 했다.

이번에는 우주선이 움직이는 것을 표현해 볼 것이다.

오른쪽으로 이동하면서 우주선이 빙글빙글 돌면서 날라간다. 날라가면서 크기도 작아진다. 그리고 화면 밖으로 벗어나게 되면 그 반대에서 다시 나타나서 계속 움직이는 것을 만들 것이다.


(update)


update 함수를 수정해주면 된다.

우주선을 회전하게 하는 것을 각도 설정, 크기 설정에서 크기만 넣어주면 된다. 어떻게 작아지는지 어떻게 이미지 생성 위치가 바뀌는지에 대한 것은 애니메이션 그리는 것에서 우리가 다 만들어 놓은 것이다. 우리는 단지 회전각도와 위치만 설정해주면 된다.



(상수)


위 코드에 대한 상수값들이다.
회전 상수는 180으로 정의 되있어서 초당 180도를 회전한다는 의미이다.
SCALE_RATE 상수는 0.2로 정의 되어있어 초당 100픽셀만큼 움직인다는 의미이다.

SHIP_SCALE 상수는 1.5로 정의되어있는데 우주선이 원래 크기에서 1.5배로 커진다는 의미이다.


(실행 화면)


실행 시 깜빡이는 우주선이 오른쪽으로 한없이 날라다닌다.

이제는 화살표 키 입력을 받아 우주선의 움직임을 통제 해 볼것이다.



(update)


위에서 한것과 같이 단순히 키 입력만 확인하여 그에 따라 위치만 바꾸어 주면된다.

(실행화면)


실행시 키 입력에 따라 우주선이 움직인다. ( 사진으로 해놔서 의미가 있는지 모르겠다... )

그런데 키를 때면 바로 우주선이 멈춘다.
키를 때도 그 방향으로 계속 움직이게 수정해 줄 것이다.
반대 키를 누르면 그 방향에대한 속도가 줄게 할 것이다. 그러기 위해서는 속도라는 값이 들어가야한다.

키 입력에 따라 속도 값을 수정해주고, 그 속도만큼 움직이게 하면 되는 것이다.



(update)


위 처럼 해두니 키를 때었을 때 계속 날라다니는 현상이 있었다.

키를 때었을 때 점차 속도가 줄어들게 하고 싶었다. 그래서 update 아래에 아래 코드를 추가했다.



(추가한 코드)


GRAVITY는 실제 중력값은 아니고 속도가 감소한다는 의미의 상수를 넣었다.



(추가 상수)



(실행 화면)


실행 해보니 우주선이 잘 움직였고, 키를 때었을 때 점차 속도가 감소하였다.

마지막으로
속도가 좀 느리다는 느낌이 있었다.

그래서 스페이스바를 누르면 부스트 모드로 기존 속도의 2배로 날라가게 만들었다.


(추가한 코드)


스페이스바 입력을 받게 하기 위해 상수로 추가했다.



(스페이스바 추가)


그리고 boost 여부에 따라 2배로 움직이게 만들었다.



(boost 모드)


실행시 아주 아주 잘 동작하였다.
키 입력을 받아 우주선이 움직이게 하는 것까지 마무리했다.!!

(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)



저번에 그렸던 이미지에서 간단한 애니메이션을 추가해 볼 것이다.

우주선을 추가할 것인데, 우주선 주변에 불이 깜빡깜빡하는 아주 간단한 애니메이션을 추가해 볼 것이다.
가장 먼저 해야할 일은 우주선을 추가해주어야하는 일이다.

저번에 이미지를 그렸을 때와 같이 우주선 textureManager와 Image 클래스를 만들어준다.

(우주선 추가)


그 후 초기화 함수에도 추가해준다.


(텍스처 초기화)



(이미지 초기화)


이미지 클래스 초기화 후

우주선의 화면 위치를 설정해준다. 여기서는 좌측상단에 나타나도록 설정해두었다.
그 애래의 코드들은 우주선 애니메이션과 각도에 관한 설정들이다.
뒤에서 설명하겠다.!

애니메이션을 동작시키기 위해 애니메이션이 있는 이지미를 계속 업데이트해줘야한다.

이 과정은 게임 클래스 안의 update 함수 안에서 해당 이미지의 업데이트를 추가해주므로써 구현한다.



(이미지 update)


우주선의 업데이트 함수를 호출했다. 여기에 인자는 frameTime을 넣었다.
예전에 게임엔진 틀을 만들 때 frameTime을 계산해주기 위해 내부 타이머를 이용해 시간을 계산해주는 작업을 한 것이 기억날 것이다. 이런 곳에 사용하기 위해 계산을 해둔 것이다.

이미지클래스의 update를 확인해보자


(update 함수)


넘어온 frameTime은 한 루프가 돌면서 지난 시간이다. (이전 게임엔진 틀을 만들 때 만들었었다.)
먼저 체크하는 것은 애니메이션 동작이 있는 스프라이트인지 체크한다.
애니메이션 동작이 있는 것이라면 먼저 경과 시간을 계산해준다.
우리가 원하는 frameDelay마다 동작하게 하기 위해서이다. 그래서 그 다음으로 frameDelay보다 큰지 확인하고 크면 update를 하고 크지 않으면 다음 루프에서 update를 하게하기 위해서이다.
그 후 애니메이션 타이머에서 frameDelay를 빼준다. (다음 번 update에서 사용하기 위해서)
그리고 현재 프레임수를 1증가시킨다.
그리고 동작하는 것이 setRect() 함수이다.

이제 이 setRect() 함수를 보면 애니메이션이 이해가 갈 것이다.



(setRect() 함수)


이 함수에서는 이미지에서 특정 영역을 설정하는 함수인데, 현재frame을 기준으로 바뀌는 것을 볼 수 있다. 우리가 올린 이미지는 아래와 같다.


(우주선 이미지)


위의 이미지에서 하나씩 가져다 쓰는데 4개를 번갈아가면서 화면에 출력해주는 것이다.
그렇기에 우주선 주위에 불빛이 빤짝거리게 되는 원리이다.

즉, 움직이게 하고 싶은 이미지 마다 위와 같은 과정으로 프레임을 설정해 주는 것이다.

맨 처음에 봤던 우주선 애니메이션활성화 설정을 다시 볼 것이다.


(애니메이션 설정)


setFrames는 처음 프레임과, 마지막 프레임을 설정하는 것이다. 변수의 값은 아래 사진과 같다.


(상수)


프레임은 0에서 3까지 총 4개의 프레임이고 위의 사진에서 보았듯이 4개의 우주선 모양이 있는 것을 확인 할 수 있다. 그리고 setCurrentFrame 함수로 현재 프레임을 우주선의 시작 프레임으로 설정해주는 것이다. (초기화 함수이기 때문에)



(setCurrentFrame 함수)


currentFrame에 넘어온 인자를 넘겨준다. 그리고 영역을 설정한다.(현재 프레임에 맞는 그림)

자! 이제 실행해 볼 것이다!

그 전에! 추가해줘야할 것이 있다. render() 함수에 우주선 이미지 그리는 것을 추가해줘야한다.!


(우주선 그리기)


이제 빌드 후 실행??
나는 여기서 빌드 후 실행하고 잘 되는 줄 알았다.
그리고 곧 문제점을 깨달았다.
전체화면으로 바꾸려고 했을 때 되지 않았다.!! 화면이 까매지기만 했다. 디바이스 로스트 상황이었다.

코드를 다시 보다가 이해했다. 디바이스 로스트 때 돌아가는 것 중 우리가 우주선 이미지를 추가하면서 우주선 textureManager를 추가해줬는데 이것도 핸들해줘야하기 때문이다.


(로스트 처리)


이렇게 로스트 처리하게 되면 창전환도 괜찮았다.



(실행 화면)



우주선 주변의 불빛이 깜빡거리는 간단한 애니메이션을 추가해보았다.

(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)

게임 프로그램에 이미지를 그리는 것을 해볼 것이다.

우리가 사용할 것은 스프라이트이다.
스프라이트는 2D 이미지를 그리기 위해 설계된 DirectX 그래픽스 API의 일부이다.
(꼭 2D 이미지만을 위한 것은 아니지만 거의 주로 2D이미지에 사용된다.)

그렇다면 스프라이트를 이용하기 위해 스프라이트 포인터를 자주 사용하므로 간단하게 재정의한다.


(스프라이트 재정의)


앞으로 LP_SPRITE로 사용할 것이다.

그리고 스프라이트 포인터를 담고 있을 변수도 추가해준다.

스프라이트는 그래픽을 그리는 일을 하므로 그래픽 클래스에 변수를 만들것이다.


(스프라이트 포인터 변수)


그렇다면 이제 할 일은 스프라이트를 생성하고 그 스프라이트 주소를 우리가 만든 스프라이트 포인터 변수에 담아야 한다. (만들어야 사용할 수 있으므로 당연한 사고의 흐름이다!)



(스프라이트 생성)


스프라이트 생성함수는 위와 같다.
D3DXCreateSprite를 사용하고 첫번째 인자에 디바이스 인터페이스를 넘겨주고, 두 번째 인자는 생성될 스프라이트 객체의 주소를 담을 포인터 변수이다.

게임 엔진을 통해 수행되는 모든 스프라이트 그리기 작업은 하나의 DirectX 스프라이트로 처리된다. 앞으로 지금 만든 스프라이트를 이용해 그리기 작업을 수행할 것이다.

그 다음으로 이미지 파일로 부터 이미지를 가져와 텍스처를 만들것이다.
텍스처?
텍스처는 그래픽스에서 사용하는 프리미티브 타입에 적용할 수 있는 그림이다. 이 경우 텍스처는 스프라이트이다. ( 텍스처에 관해 궁금하신 분들은 구굴링! )
텍스처는 여기서 쉽게 말해 그릴 그림이라고 생각하고 넘어가도 된다. ( 2D 이미지를 그리는 경우 )

쉽게 정리해서
파일로부터 이미지를 가져와 그릴 수 있는 텍스쳐로 만든 후
스프라이트를 이용해 그 텍스쳐를 화면에 그린다!

그렇다면 파일로부터 이미지를 가져와 텍스처를 만드는 것을 계속해보자!



(텍스처 가져오기)


D3DXCreateTextureFromFileEx 함수를 이용해 파일로 부터 이미지를 가져오고 그 이미지로 텍스처를 만드는 작업을 한다. 맨 마지막 인자에 포인터 변수가 들어가는데 그 변수에 만들어진 텍스처가 저장된다.

중간 인자 중 텍스처의 폭과 높이가 있는데 이 정보를 파일로 부터 가져올 수 있다.
D3DXGetImageInfoFromFile을 사용하면 되는데 이 것을 포함해 텍스처를 로드하는 함수인

loadTexture의 완성 코드는 아래와 같다.


(loadTexture)


loadTexture함수는
파일로 부터 폭과 높이를 가져오고
그 후 텍스처를 만드는 함수이다.

이제 스프라이트를 그리는 작업을 살펴볼 것이다.
스프라이트는 신 사이에서 그려야한다.
신 시작 - (그리기) - 신 끝
위 처럼 신의 시작과 끝 사이에서 그려져야한다.

위의 원칙을 적용해 renderGame 함수를 적어보면 아래와 같다.


(renderGame)


renderGame함수는 게임 아이템을 그리는 함수이다.
렌더링 시작할 때 그래픽 함수에서 신을 시작하는 함수를 호출하고
그 후에 render() 를 호출한다. (여기서 그리기 작업이 들어가는 것이다. )
그리고 다 그렸으면 신을 종료하는 함수를 호출하여 그리는 작업을 마무리한다.
(지금까지 그리는 것들은 백버퍼에 그려지고 있는 것이다.)

그 후 백 버퍼를 화면에 표시해준다.

신을 시작하고 종료하는 함수는 아래와 같다.



(신 시작과 끝함수)


beginScene 함수는 신을 시작하는 함수이다.
신을 시작하기 전에 백버퍼를 지워 준 후 신을 시작한다.
endScene 함수는 신을 종료하는 함수이다.
내부적으로 디바이스 인터페이스에서 BeginScene과 EndScene을 호출한다.

신 시작 함수 호출과 스프라이트 그리기 함수 사이에서 한가지 작업이 더 들어가야한다.
그리기 전에 그릴 텍스처에 대한 설정이다. 크기 조정, 회전 및 위치를 지정하는 작업이다.

이 작업은 행렬 수학을 이용해 계산이 되는데 우리는 직접 계산해서 하는 것이 아니라 DirectX의 함수를 이용할 것이다.


(행렬 적용 및 그리기)


D3DXMatrixTransformation2D 함수를 사용하면 크기 조정, 회전 및 위치를 지정할 수 있는 변환 행렬을 구할 수 있다. 첫번째 인자로 만들어진 행렬이 저장된다.

만들어진 행렬을 SetTransform 함수에 넣어 호출해주면 해당 행렬을 스프라이트에 적용하게 되는 것이다.

그 후 Draw 함수를 이용해 그리기 작업이 들어가는 것이다.

이렇게 그리는 텍스처에 대한 속성값들이 있다. 이것들을 위한 구조체를 만들어서 정리하면 편하다.


(스프라이트 속성)


스프라이트 속성들을 모아서 구조체로 만들었다.

아래는 스프라이트 속성들을 설정하는 코드이다.


(스프라이트 속성 설정)


위 코드는 drawSprite 함수의 윗부분에 해당하고 이 코드들이 실행이 된 후 속성이 다 설정이 되면 위에서 봤던 행렬을 만들어준 후, 적용하고 그리는 단계로 drawSprite 함수가 이루어져 있다.

정리하면!
1. 그래픽 클래스에서 신을 시작한다.
2. 스프라이트의 Begin 함수를 호출
3. 스프라이트 속성 설정
4. 스프라이트 행렬 만들기
5. 스프라이트 행렬 적용
6. 스프라이트 그리기
7. 스프라이트의 End 함수 호출
8. 그래픽 클래스에서 신을 종료한다.


아래는 색상을 앞으로 이용하기 편하게 만들어 놓은 이름 공간이다.


(색상 namespace)


예를 들어 오렌지색을 사용하고 싶다면 코드에
graphicsNS::ORANGE라고만 입력하면 된다.

그래픽 클래스에 추가한 loadTexture 함수는 텍스처 데이터를 D3DPOOL_DEFAULT 메모리로 불러온다. 일반적으로 이 곳은 비디오 메모리인데, 다른 프로그램이 그래픽 디바이스를 사용하고 있다면 사용할 수 없거나, 로스트 상태가 된다. 그렇기에 로스트 상태가 되면 디바이스를 되찾는 과정이 필요하다.

게임에서 텍스처를 사용할 때 각 텍스처를 불러오고 관리하기 위해 TextureManager 객체를 만들 것이다.


(초기화함수)


초기화 함수에는 loadTexture를 이용해 해당 이미지를 가져와 텍스처를 생성하는 작업을 한다.

로스트 상태를 관리하기 위한 함수는 아래와 같다.


(로스트 관리)


이전 다른 디바이스와 마찬가지로 로스트 상태라면 해당 리소스를 해제해주어야하고 다시 얻어와야한다.

그 외 TextureManager 객체에 대한 데이터를 가져올 수 있는 함수들도 만들어준다.


(TextureManger 함수들)


이 함수들을 이용해 해당 텍스처에 대한 정보를 가져올 수 있다.
마지막으로 스프라이트를 그리고 관리하는데 필요한 모든 코드들을 Image 클래스로 통합할 것이다.

Image 클래스를 이용해 그리기는 작업을 할 것이다. (지금까지 만든것들을 Image 클래스에 통합하는 과정이다.)


(이미지 클래스 초기화함수)


초기화 함수에서는 필요한 객체를 가져오고, 해당 이미지의 텍스처 정보를 가져오는 과정이다.

이미지 클래스의 draw 함수이다.


(그리기 함수)


이 함수에서 그래픽의 drawSprite 함수를 호출한다. 그러면 우리가 지금까지 확인한 일련의 과정들이 실행되는 것이다.

draw 함수를 두개 만들었다. 하나는 위와 같고 다른 하나는 특정 sprite를 지정하여 그리는 함수로 아래와 같다.


(특정 스프라이트를 지정하여 그리는 함수)


그 외에 이미지 클래스의 데이터를 가져오고 설정하는 get과 set 함수들을 추가해준다.

그러면 지금까지 만든 내용들로 게임의 이미지를 그려볼 것이다.
성운 이미지를 배경으로 쓰고 행성 이미지를 그 위에 그리는 것이다.

Spacewar.h 함수에 게임 아이템 2개에 대한 텍스처와 이미지를 만들어준다.


(텍스처와 이미지)


그리고 초기화 과정에 텍스처 초기화과정과 이미지 초기화 과정을 넣어준다.


(게임 초기화 함수)


처음에 텍스처 초기화 과정이 이루어지면 그 텍스처매니저를 이미지 초기화 함수에 사용한다.
그리고 행성을 화면 중앙에 배치하는 코드를 넣었다.

이렇게 되면 그리는 작업의 준비는 끝난 것이다.

그리는 함수인 render() 함수에 해당 이미지를 그리는 함수를 호출하면된다.


(그리기)


위 render 함수 이전에 실행되는 것이 그래픽 클래스의 신 시작이고 render()함수 가 종료되고 실행되는 것이 크래픽 클래스의 신 종료 함수인 것을 기억해야한다.

그리는 것은 이미지 클래스의 draw(우리가 만든것) 을 이용한다.


(로스트 처리)


로스트 처리에 관한 부분도 넣어준다. 각각의 아이템마다 처리해준다.

그리고 빌드 후 실행해보자!


(실행 화면)



프로그램에 우리가 원하는 그림이 그려지는 걸 확인 할 수 있다.

(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)


이번에는 입력을 담당하는 Input Class를 살펴볼 것이다.

Input Class를 만든다.


(Input의 private)


입력에 관한 변수들이다.

public의 함수들을 볼 것이다.



(생성자)


생성자에서는 기초적인 초기화 작업을 진행한다.
키가 눌렸는지에 대한 배열 초기화와 버퍼 초기화, 마우스 정보 변수 초기화를 진행한다.
(* 컨트롤러에 대한 것은 Xbox 컨트롤러를 위한 것이다. 책에 포함되어있는 내용이지만, 나는 Xbox에 관한 설명은 건너 뛸 것이다.)



(소멸자)


Input 클래스가 삭제 될 때 마우스가 캡쳐상태라면 캡쳐 상태를 해제 해준다.

다음은 Input 클래스의 초기화 함수이다.



(초기화 함수)


초기화 과정에서 마우스를 캡쳐 상태로 설정시켜준다.
그리고 그 아래의 코드들은 고정밀 마우스 데이터를 가져오는 작업이다.

이제부터 입력에 관한 함수들이 나온다.


(키 눌림 안눌림)


키가 눌렸는지 안눌렸는지 체크하는 함수이다.
keyDown에서는 키가 눌렸으면 keysDown과 keysPressed 배열의 해당 index에 가서 true로 만들어준다. keysDown은 현재 눌렸는지 확인하는 것이다. 그리고 keyPressed는 현재 게임 루프가 실행되는 동안 눌렸었는지를 체크하기 위한 배열이다.

  따라서 keyUp함수에서는 keysDown만 false로 해준다.



(키 입력)


키 입력에 관한 함수이다. 




(키 체크)


isKeyDown은 현재 키가 눌렸는지 확인하는 함수이고 keysDown 배열을 확인한다.
wasKeyPressed는 현재 루프동안 키가 눌린 경험이 있는지 체크하므로 keysPressed 배열을 확인한다.
그리고 어떤 키든 눌렸었는지 체크하기 위한 함수 anyKeyPressed 함수도 추가해주었다.



(입력 초기화)


입력에 대한 초기화 함수이다. 초기화가 위에서 만든 initialize 함수와 다르다. 단순히 입력에 대한 초기화를 하는 것이다. 

  다음은 입력 중 마우스입력에 관한 함수들이다.



(마우스 입력)


mouseIn 함수로 마우스의 좌표를 가져오고, mouseRawIn을 사용해 마우스의 변화량을 가져온다.
mouseWheelIn으로 마우스 휠 정보를 가져온다.


(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)


게임 엔진의 기초틀을 만들 것이다.

Game Class를 만들 것인데, 추후 다른 게임을 만들 때 이 Class를 상속받아서 만들 것이다.
이제 만들 Game Class는 그래픽을 담당하는 Graphics Class와 입력을 담당하는 Input Class를 포함하고 있다. Graphics Class는 이전 포스팅에서 만들었던 DirectX와 비슷하므로 다른 점을 설명하고 넘어갈 것이다.

Game Class를 만드는 법을 살펴보자!


(Game Class의 include)


Game Class는 graphics 클래스와 input 클래스를 포함한다.

Game Class를 만든다.



(Game Class의 protected)


Game 클래스의 변수들을 담아두었다. private 대신 protected를 사용한 이유는 Game 클래스로부터 상속받은 클래스에게 변수의 직접적인 접근을 허용하게 하기 위해서이다.


public의 메서드들을 만든다.


(생성자와 소멸자)


생성자와 소멸자이다.
생성자는 input 클래스의 객체를 만든다. 다음 포스팅글에서 얘기하겠지만 Input 클래스의 생성자에서 기초적인 입력에 관한 버퍼들을 초기화 한다.
생성자에서 기초적인 초기화를 하고 추가적인 초기화가 없으므로 initialized 라는 변수에 false를 담아둔다. 후에 이 변수를 보고 game 클래스의 초기화를 시켜줄 것이다.

소멸자는 Game 클래스가 삭제될 때 예약된 메모리를 해제하는 순서로 구성되어있다.



(윈도우 메시지 핸들러)


위에는 윈도우 메시지를 처리하는 핸들러 함수를 Game 클래스 안에서 만들었다.
각 이벤트에 대한 처리를 담당한다.

기존에 있던 winmain.cpp 에 있던 WinProc 의 메시지 핸들하는 부분이 Game 클래스로 옮겨 졌으므로 winmain.cpp의 WinProc 도 수정해주어야한다.



(WinProc 수정)


game클래스 안의 messageHandler를 호출해준다.

Game 클래스의 초기화를 담당하는 메서드이다.



(Game클래스의 초기화)



인자로는 윈도우 핸들러를 받는다.
여기서 Graphics 객체를 생성한다. 그 후 그래픽 객체를 초기화 하고, input 객체를 초기화한다.
마지막의 타이머 사용은 frametime을 계산하기 위한 것이다.

  화면에서 움직이는 그래픽 아이템들이 정지된 이미지를 적절한 시간에 연속적으로 화면에 보여주므로써 움직이는 것처럼 보이는 것이다. 이 프레임시간이 너무 빠르면 그래픽 아이템이 너무 빠르게 움직일 것이고, 너무 느리면 그래픽 아이템이 화면에서 너무 느리게 움직일 것이다. 그러므로 우리는 적절한 시간을 간격으로 화면에 출력해주어야한다. (우리가 원하는 속도로)

  그 계산을 위해 고성능의 타이머를 사용한다. 요즘 PC에는 기본적으로 고성능의 타이머를 갖추고 있으므로 이것을 사용할 것이다. QueryPerformanceFrequency 함수를 이용해 timerFreq에 고성능 타이머의 발생 빈도를 저장한다. 그리고 QueryPerformanceCounter함수를 이용해 시간을 가져오는데 이 때 timeStart 변수에 시작 시간을 가져온다. 
  그 후 초기화를 진행 하였으므로 initialized 변수에 true라고 저장한다.

  그 다음 처리해야할 문제는 디바이스 로스트 상태이다.
디바이스 로스트 상태란 Direct3D 그래픽 디바이스를 놓친 상태이다. 예를 들어 우리가 게임을 하다가 Alt-Tab을 눌러서 다른 화면을 보다가 다시 게임을 틀면 게임이 나와야한다. 이 때 게임이 화면에 그려지지 않는 상태가 그래픽 디바이스 로스트 상태이다.

  그렇기에 이러한 상황을 처리해주어야한다. 디바이스 로스트 상태가 되면 리셋하고 리소스를 다시 생성해야한다.



(디바이스 로스트 처리)


  처리 알고리즘은 위와 같다.
이 때 주의할 점은 리셋 함수를 호출하기 전 기존에 할당되었던 모든 리소스를 해제하지 않으면 reset 함수가 실패한다. 

  위에서 사용한 getDeviceState와 reset 함수는 아래와 같다.


(getDeviceState, reset)


getDeviceState는 로스트 상태를 확인하기 위한 함수이다. 함수 내부를 보면 디바이스객체의 TestCooperativeLevel 함수를 사용하는데 이 함수는 매개변수 없이 실행이 되고 성공하면 D3D_OK를 반환하고 로스트 상태를 3가지로 반환한다.
D3DERR_DEVICELOST : 디바이스가 로스트 상태가 되서 현재 복구할 수 없다.
D3DERR_DEVICEENOTRESET : 디바이스를 다시 작동할 수 있다.
D3DERR_DRIVERINTERNALERROR : 디바이스에 내부 에러가 있다. 사용자에게 보고하지만, 할 수 있는 방법이 없다.

또 releaseAll 함수와 resetAll 함수이다.



(releaseAll, resetAll)


비워둔채 메서드만 생성해둔다.

  그 다음 구현해야할 것이 게임 그래픽 렌더링이다. 그래픽을 그리는 과정을 렌더링이라고 부른다.
렌더링은 DirectX의 BeginScene 함수를 호출해 신을 시작하고, 모든 렌더링이 끝난 후 DirectX의 EndScene 함수를 호출해 신을 끝낸다. 이 Scene 밖에서 렌더링을 하면 실패한다.

그러므로 graphics 클래스 안에 beginScene함수와 endScene을 만들어준다.



(beginScene, endScene)


beginScene에서 백버퍼를 지워준다. 그리고 그래픽 디바이스 객체의 BeginScene() 함수를 호출하고 그 결과를 반환한다.

endScene에서는 디바이스 객체의 EndScene() 함수를 호출한다.

  이 두 함수 사이에서 그래픽을 렌더링해야한다. 실제 게임 아이템을 렌더링하는 renderGame() 함수는 아래와 같이 생겼다.



(renderGame 함수)


beginScene과 endScene 사이에서 렌더링을한다.
백버퍼를 지워 준 후 그 백버퍼에 그래픽을 그린다. 그리고 endScene으로 그리는 것을 마무리한다. 그 후 디바이스 로스트를 체크하고 (로스트상태면 처리해주고)
그래픽 객체의 showBackbuffer 함수를 이용해 화면에 그렸던 백버퍼를 출력해준다.

  이제 메인 메시지 루프에서 계속 호출될 게임클래스의 run 함수를 만들 것이다.



(메인 메시지 루프)


지금 만들 Game의 run 함수는 winmain.cpp의 메인 메시지 루프에서 계속 호출 될 것이다.


(Game 클래스의 run 함수)


  run 함수는 위와 같다. 위에서 아래로 코드를 설명하겠다.

처음 부분으로는 그래픽 객체 확인이다.



(그래픽 객체 확인)


  그래픽 객체를 확인하고 없다면 굳이 run 함수를 실행시킬 필요 없으므로 바로 함수를 종료한다.

  그 후 frametime을 계산하기 위한 부분이 나온다.


(frametime 계산)


  루프 시작전 끝 시간을 가져와서 시작시간의 차이를 구해 frametime을 구한다. 그렇다면 루프 시작할 때 시작시간을 설정하고 끝날 때 끝시간을 설정하여 frametime을 구하면 안될까?
-> 그런 방법으로 frametime을 구한다면 게임 루프 밖의 소요되는 시간을 고려하지 못하게 되므로 실제 렌더링 되는 시간이 정확하지 않게 된다.

  위 코드를 보면 frametime의 최소시간과 최대 시간을 정해둔다. 최소시간을 정해두고 그거보다 빠르다면 게임을 잠깐 재워둔다. 왜(?) -> 굳이 게임을 재워야할까 싶긴 하지만 이건 절전을 위한 것이다. 빠르게 처리 되었을 때는 잠깐 쉬게한다면 CPU 부하를 줄여주고 환경 친화적인 프로그램으로 만들어준다. 

이건 앞으로 어떤 프로그램을 만들든지 중요한 부분이다. 우리의 배터리는 소중하므로!

  그 후 frametime을 이용해 평균 fps를 계산해준다. 만약 frametime이 너무 느리다면, 최대 frame 시간으로 frametime을 설정해준다. 보통 이정도 까지 도달할 경우는 거의 없다.

  그 다음부분은 게임동작 관련부분이다.


(게임 함수)


  일시정지 상태가 아니라면 게임이 진행되어야한다.

Game 클래스의 순수 가상함수 update(), ai(), collision() 함수를 실행해준다. 앞으로 만들 게임의 클래스에서 Game 클래스를 상속받을 것인데 그 때 이 함수들을 우리가 채워주는 것이다. 지금은 Game 클래스를 만드는 것으로 게임엔진의 틀을 만드는 과정이다.


(마지막 과정)


다 이루어진 후 renderGame 함수를 이용하여 게임 아이템을 그리고, 컨트롤러의 상태를 읽고 지금까지 받은 키 눌림 입력을 초기화해주면서 끝난다.

(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)

DirectX는 고성능 그래픽, 사운드 등의 주변 장치와 상호 작용을 필요로 하는 프로그램(특히 게임)을 작성하는 데 사용하는 API이다.

API 모음이라고 생각하면된다.
그렇다면 DirectX를 이용하여 간단한 프로그램을 만들어 볼 것이다.

윈도우즈 프로그래밍 카테고리에서 만들었던 Hello World 창 만드는 코드에서 덧붙여서 작성할 것이다. 

추가할 것은 헤더파일이다.


(graphics.h 추가)


헤더파일을 추가해주고, graphics 포인터를 만들것이다.



(graphics 포인터)


Graphics라는 클래스를 우리는 아직 만들지 않았다. 이제 만들어 볼 것이다.

헤더 파일을 하나 생성해준다. graphics.h



(include, define 부분)


이 헤더를 중복하여 정의가 되면 에러가 난다. 그러므로 컴파일 할 때 이 파일이 포함되어 있다면 다시 포함시켜 중복정의가 일어나지 않도록 첫부분에서 해주었다.

Graphics 클래스를 만들어준다.



(Graphics_private)


private 영역이다. 사용할 변수들과 initD3Dpp 함수를 선언했다.




(Graphics_public)


public 영역이다.
기본적인 생성자와, 소멸자를 선언했고, releaseAll() 함수는 소멸자에서 사용할 것인데 할당 받은 것들을 전부 반환시키기 위한 함수이다.
initialize 함수는 Graphics 클래스를 만든 후 초기화할 때 사용할 함수이다.
그 다음 showBackbuffer 함수를 선언한다.
자세한 함수 내용은 아래서 코드 설명할 때 할 것이다!

기존 Hello World 프로그램의 WinMain 함수에서 추가한다.



(메모리 누수)


위의 두줄은 메모리 누수를 탐지하기 위한 것이다.
디버그 모드로 빌드 시 메모리 누수를 확인한다.

메모리 누수란 프로그램이 종료될 때 할당받았던 메모리를 잘 반납하지 않고 종료가 되어 메모리가 야금야금 줄어드는 현상이다. 그렇기에 생성한 객체들을 종료할 때는 잘 반납시켜주고 종료해야한다.

윈도우를 생성한 뒤 Graphics 객체를 생성한다.



(Graphics 객체 생성)


아까 만들었던 graphics 변수에 Graphics 객체를 생성한다.
그리고 initialize 함수로 초기화 해준다.

이제 그 함수들을 살펴볼 것이다.



(Graphics 생성자)


Graphics 생성자에서는 사용하는 변수들에 대한 초기 설정을 해준다.
direct3d 라는 변수에는 우리가 Direct3D 객체를 만들 것이다.

device3d 라는 변수는 Direct3D 객체의 함수를 이용하여 디바이스 인터페이스를 만들고 저장할 곳이다.



(initialize 함수)


Direct3DCreate9 함수는 Direct3D 객체를 만들고 객체에게 줄 인터페이스를 얻는 함수이다.
함수 인자로는 해당 SDK의 버전을 넘겨줘야한다.
D3D_SDK_VERSION 값은 DirectX 헤더 파일에 의해 설정되는 값이다. 컴파일 하는 동안 사용되는 DirectX의 버전이 대상 시스템에 설치된 런타임 DLL과 일치하는지 확인하는데 사용되는 값이다.

성공적으로 실행되면 IDirect3D9 인터페이스를 가리키는 포인터를 반환하고 그렇지 않으면 NULL을 반환한다.
그 후 디바이스를 생성하는 코드이다.
CreateDevice 함수는 디바이스 인터페이스를 반환한다.
아래 순서대로 인자의 정보이다.

Adapter : 사용할 디스플레이 어댑터 수. D3DADAPTER_DEFAULT는 기본 디스플레이 어댑터를 지정하는데 사용된다.

DeviceType : D3DDEVTYPE_HAL. 하드웨어 레스터 변환을 지정한다. 이 값으로 지정한 후 CreateDevice가 실패하면 어댑터가 하드웨어 그래픽 가속을 지원하지 않는다는 것을 의미한다.

hFocusWindow : 포커스 윈도우는 Direct3D에게 애플리케이션이 포그라운드모드에서 백그라운드 모드로 전환했다고 알려준다. 창화면만 사용하면 NULL로 설정한다. 일반적으로 윈도우에 대한 핸들을 넘겨준다.

BehaviorFlag : 정점 처리를 어떻게 하는지에 대한 설정이다. 일단 소프트웨어를 이용한 정점 처리로 설정한다. 뒷부분에서 하드웨어가 지원 가능한지 체크하는 코드부분을 추가하고 하드웨어 정점 처리 방식을 추가할 것이다.

*pPresentationParameters : 생성할 디바이스에 대한 속성을 지정하는 D3DPRESENT_PARAMETERS 구조체를 가리키는 포인터이다. 이 구조체를 채우기 위해 initD3Dpp 함수를 만들 것이다.

**ppReturnedDeviceInterface : 반환값이다. 이 주소에 생성된 디바이스의 주소가 적힐 것이다.

그렇다면 이제 D3DPRESENT_PARAMETERS 구조체를 채울 initD3Dpp 함수를 볼 것이다.




(initD3Dpp 함수)


d3dpp 구조체의 값을 설정하는 함수이다.

이제 이렇게 디바이스까지 생성을 했다면 WinMain 함수를 이어서 본다.



(메인 메시지 루프)


처리할 메시지가 없다면 화면을 그려주는 graphics의 showBackbuffer() 함수를 실행한다.




(showBackbuffer 함수)


버퍼를 표시하기 전에 버퍼를 비우는 함수가 나온다.
Clear 함수이다.
인자는 순서대로 다음과 같다.
Count : pRects가 가리키고 있는 배열에 나열돼 있는 지울 사각형의 개수이다. pRects가 NULL이라면 0으로 설정한다.

pRects : 지울 사각형을 기술하는 D3DRECT 구조체 배열을 가리키는 포인터이다. 여기서는 버퍼 전체를 지울것이기 때문에 NULL로 설정한 것이다.

Flags : 비울 표현의 형식을 지정하기 위해 D3DCLEAR에 정의된 하나 이상의 플래그다. 여기서는 D3DCLEAR_TARGET을 사용한다. 이는 버퍼를 비우길 원한다는 것이다.

Color : RGB색상의 값이다.

Z : 깊이 버퍼이다. 0에서 1사이의 부동소수점 숫자로 z값을 정한다. 여기서는 사용하지 않으므로 0.0f로 설정한다.

Stencil : 스텐실 버퍼는 깊이 버퍼와 관련이 있다. 여기서 사용하지 않을 것이므로 0으로 설정한다.

그 후 Present 함수로 화면에 백 버퍼를 표시한다.
아까 우리가 initD3Dpp 함수에서 SwapEffect 변수에 DISCARD라는 값으로 설정했다.
화면에 보이는 버퍼와 화면에 보이지 않는 버퍼(백 버퍼)가 존재한다.
Page Flipping 기술을 쓸 것인데 화면에 표시되고 있을 때 백 버퍼에 그 다음에 출력할 것들을 계산해서 넣어두는 것이다. 그리고 화면에 백버퍼에 있던 것을 보여준다. 그러면 그 전에 화면에 보이던 버퍼가 백버퍼가 된다. 이런식으로 화면을 넘기면서 움직이는 것이다.

DISCARD로 설정한 것은 이전에 보였던 버퍼의 내용을 삭제하라고 하는 것이다. 디스플레이 버퍼의 이전 내용을 삭제하면 페이지 전환에서 최고의 성능을 발휘할 수 있다.

그렇다면 이제 빌드 한 후 프로그램을 실행 시켜보겠다.



(프로그램 실행)


화면처럼 초록색화면이 뜨는 것을 볼 수 있다.

이제는 ESC 키를 추가해 볼 것이다. 전체 화면일 때는 엑스박스가 없으므로 종료 시킬 수가 없다.

윈도우즈 프로그래밍 카테고리에서 키보드 입력 포스팅글에 나온 대로 추가한다.



(ESC키 메시지)


ESC키가 눌리면 프로그램 종료 메세지를 메세지 큐에 보낸다.

다음은 FULLSCREEN 변수를 통해 전체화면으로도 동작 할 수 있도록 만들어 볼 것이다.



(전체화면)


이전에는 WS_OVERLAPPEDWINDOW 로만 설정했었는데, 이제 FULLSCREEN의 값에 따라 전체화면인지 창화면인지 선택 할 수 있다.

더 추가해 볼것은 창 화면의 크기이다.
윈도우를 생성할 때 CreateWindow 함수의 width와 height는 윈도우 타이틀 바와 테두리를 포함하기 때문에 우리가 원하는 정확한 클라이언트 영역의 크기가 아니게 된다. 우리는 게임이 표시되는 테두리 안쪽 부분의 크기를 정확하게 재조정할 필요가 있다.

클라이언트 영역의 크기를 구하기 위해 GetClientRect를 사용한다.
새로 정의될 너비와 높이이다.
새 너비 = 원하는 너비 + ( 원하는 너비 - 클라이언트 너비 )

새 높이 = 원하는 높이 + ( 원하는 높이 - 클라이언트 높이 )



(크기 재조정)


크기를 재조정하는 함수는 MoveWindow 함수를 이용할 것이다.

이제 마지막으로 아까 소프트웨어 정점 처리만 사용한 것을
만약 지원가능한 어댑터를 찾았다면 하드웨어 정점 처리를 사용할 수 있도록 처리할 것이다.

graphics.h 헤더 파일에서 다음을 추가해준다.



(추가)


그리고 isAdapterCompatible 함수를 작성해준다.



(isAdapterCompatible 함수)


이GetAdapterModeCount 함수는 지정된 어댑터에 대해 사용 가능한 디스플레이 모드의 수를 반환하는 함수이다.

그리고 하나씩 돌아보면서 지원가능한 모드를 만나면 true를 반환한다.
EnumAdapterModes 함수를 통해 pMode의 구조체에 하드웨어에 대한 자세한 정보를 얻을 수 있다.

initialize 함수에 추가해 볼 것이다.



(새 initialize 함수)


전체 화면이고 어댑터가 호환이 된다면 refresh 속도를 호환되는 것으로 설정한다.
그리고 하드웨어 정점 처리 기능을 수행할 수 있는 지 체크하기 위해 GetDeviceCaps 메소드를 사용한다. 가능하다면 behavior에 하드웨어 정점처리 플래그를 넣고 가능하지 않다면 소프트웨어 정점처리 플래그를 넣고 behavior를 디바이스 생성할 때 넘겨주면 된다.

(* 참조 - 2D 게임 프로그래밍, 찰스 켈리)


DirectX 개발시
프로젝트의 설정이다.

1. 포함 디렉터리
-> Visual Studio에게 DirectX 헤더 파일을 찾아볼 곳을 알려주는 역할을 한다.

(포함디렉터리1)


구성을 모든 구성으로 만든다.





(포함디렉터리2)


왼쪽 창에서 VC++ 디렉터리를 클릭하고 오른쪽 창의 포함 디렉터리를 클릭한다.





(포함디렉터리3)


오른쪽 드롭다운 화살표를 클릭하고 <편집...>을 클릭한다.





(포함디렉터리4)


위와 같이
$(DXSDK_DIR)\Include
라고 적고 확인을 클릭한다.



2. 라이브러리 디렉터리

-> 링커에게 DirectX 라이브러리 파일을 찾아볼 곳(경로)을 알려준다.



(라이브러리 디렉터리1)


1번 설정에 이어서 오른쪽 창에서 라이브러리 디렉터리를 클릭하고
드롭다운 화살표를 클릭하여 <편집...>을 클릭한다.





(라이브러리 디렉터리2)


위와 같이
$(DXSDK_DIR)\Lib\x86
이라고 적고 확인을 클릭한다.

3. 런타임 라이브러리
-> C 런타임 라이브러리를 정적으로 링크해 C 런타임 라이브러리 DLL이 설치되지 않은 시스템에서도 프로그램이 동작하게 설정할 것이다.


(런타임 라이브러리1)


왼쪽 상단에 구성을 Release로 해준다.




(런타임 라이브러리2)


C/C++ 을 클릭하고 하위 메뉴를 열어준다.
거기에서 코드 생성을 클릭해주고
오른쪽 창에서 런타임 라이브러리를 /MT 로 설정한다.

4. 추가 종속성

-> Visual Studio에게 어떤 DirectX 라이브러리를 추가해야 하는지 알려준다.


(추가 종속성1)


왼쪽 상단의 구성을 모든 구성으로 만든다.
그 후 링커를 클릭하고 하위 메뉴를 열어준다.
그리고 오른쪽 창의 추가 종속성을 클릭하고 드롭다운 화살표를 클릭하여 <편집...>을 클릭한다.




(추가 종속성2)


위와 같이
d3d9.lib
이라고 적어주고 확인을 클릭한다.

(*참조 - 2D 게임 프로그래밍, 찰스 켈리)

+ Recent posts