DX라이브러리2014. 12. 31. 16:01

4.2 이미지 자유 변형


확대, 축소 이외에 자유변형이라고 하는 처리방법도 있다. 이미지의 꼭지점 4개를 지정해줌으로써 그에 맞추어 이미지를 변형 시킬 수 있다. DrawModiGraph함수를 이용하여 구현한다. 레퍼런스는 여기를 확인하자.


선언

 int DrawModiGraph( int x1, int y1, int x2, int y2,

                              int x3, int y3, int x4, int y4, int GrHandle , int TransFlag );

기능

 메모리에 로드된 그래픽을 자유변형

인수

 x1 , y1 , x2 , y2, x3 , y3 , x4 , y4 : x1부터 순서대로 묘화할 이미지의 좌상, 우상, 우하, 좌하의 꼭지점 좌표.

 GrHangle : 묘화할 그래픽 핸들

 TransFlag : 투명도 유효 여부 (TRUE 유효, FALSE 무효)

반환값

 0 : 성공

 -1 : 에러 발생


예제


#include "DxLib.h"


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK ); 


        int Image;

        Image = LoadGraph( "image/character_00.png" );


        while( !ScreenFlip() && !ProcessMessage() && !ClearDrawScreen() ){


                DrawModiGraph( 

                        200,100,        //좌상

                        500,50,         //우상

                        400,300,        //우하

                        150,350,        //좌하

                        Image, TRUE );


        }


        DxLib_End();

        return 0;

}



실행결과



이방법을 통하여 3D와 비슷한 화면 연출이 가능해진다. 

Posted by 캡슐리어
DX라이브러리2014. 12. 31. 15:24

3.14 특정 FPS로 동작하기


지금까지 60Hz의 모니터를 사용하는 환경에서는 1초에 60번 루프한다 라고 배워왔다. 60Hz의 모니터에선 아래와 같은 코딩으로 1초간 60번 루프가 이루어진다.


while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){

1더하기();

}


대부분의 모니터가 60Hz로 동작하지만 그보다 크거나 작을 경우도 생각해보아야 한다. 60Hz이외의 환경에서도 같은 처리속도를 보장하기 위한 제어가 필요하다. (단, 보통 60Hz보다 낮은 모니터는 사용하는 경우가 없기 때문에 이보다 큰 경우만 생각한다.) 이론적으론 어렵지 않다. 1프레임마다 FPS에 맞는 계산을 해주어서 대기 시키게 된다. 시작 시간 : 0프레임째를 0이라고 한다면 각 프레임의 시작시간은 아래와 같다.


1프레임 째 "1000[ms] / 60[frame] *1" ms

2프레임 째 "1000[ms] / 60[frame] *2" ms

3프레임 째 "1000[ms] / 60[frame] *3" ms

.

.

.



90Hz의 모니터를 예로 든다면 1프레임 째의 시간은 "1000/90*1"이 되므로 60Hz때보다 빠르게 흘러간다. 때문에 이를 1000/60*1이 될 때 까지 지연 시켜야 한다. 이 지연되야 하는 시간은 각 프레임에서 "int형 지연시간 = 진행되어야 하는 시간 - 실제로 진행된 시간"이 된다. 이를 실제 코딩하게 되면 아래와 같다.


C++의 경우


#include <math.h>

#include "DxLib.h"


class Fps{

int mStartTime;         //측정개시시각

int mCount;             //카운터

float mFps;             //fps

static const int N = 60;//평균을 얻기위한 샘플 값

static const int FPS = 60; //설정하려는 FPS


public:

Fps(){

mStartTime = 0;

mCount = 0;

mFps = 0;

}


bool Update(){

if( mCount == 0 ){ //1프레임이라면 시각을 기억

mStartTime = GetNowCount();

}

if( mCount == N ){ //60프레임이라면 평균을 계산

int t = GetNowCount();

mFps = 1000.f/((t-mStartTime)/(float)N);

mCount = 0;

mStartTime = t;

}

mCount++;

return true;

}


void Draw(){

DrawFormatString(0, 0, GetColor(255,255,255), "%.1f", mFps);

}


void Wait(){

int tookTime = GetNowCount() - mStartTime; //걸린시간

int waitTime = mCount*1000/FPS - tookTime; //지연시켜야 하는 시간

if( waitTime > 0 ){

Sleep(waitTime); //대기

}

}

};


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

ChangeWindowMode(TRUE),DxLib_Init(),SetDrawScreen( DX_SCREEN_BACK );


Fps fps;


while( ProcessMessage()==0 && ClearDrawScreen()==0 && CheckHitKey(KEY_INPUT_ESCAPE)==0 ){

fps.Update(); //갱신

fps.Draw(); //묘화

ScreenFlip();

fps.Wait(); //대기

}


DxLib_End();

return 0;

}




C의 경우


#include <math.h>

#include "DxLib.h"


static int mStartTime;      //측정개시시각

static int mCount;          //카운터

static float mFps;          //fps

static const int N = 60; //평균을 얻기위한 샘플 값

static const int FPS = 60; //설정하려는 FPS


bool Update(){

if( mCount == 0 ){ //1프레임이라면 시각을 기억

mStartTime = GetNowCount();

}

if( mCount == N ){ //60프레임이라면 평균을 계산

int t = GetNowCount();

mFps = 1000.f/((t-mStartTime)/(float)N);

mCount = 0;

mStartTime = t;

}

mCount++;

return true;

}


void Draw(){

DrawFormatString(0, 0, GetColor(255,255,255), "%.1f", mFps);

}


void Wait(){

int tookTime = GetNowCount() - mStartTime; //걸린시간

int waitTime = mCount*1000/FPS - tookTime; //지연시켜야 하는 시간

if( waitTime > 0 ){

Sleep(waitTime); //대기

}

}


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

ChangeWindowMode(TRUE),DxLib_Init(),SetDrawScreen( DX_SCREEN_BACK );


while( ProcessMessage()==0 && ClearDrawScreen()==0 && CheckHitKey(KEY_INPUT_ESCAPE)==0 ){

Update(); //갱신

Draw(); //묘화

ScreenFlip();

Wait(); //대기

}


DxLib_End();

return 0;

}



실행결과


위 스샷처럼 60프레임으로 동작하고 있다. mFps에 측정된 실제의 FPS수치가 들어가 있고 Draw에서 측정한 FPS값이 출력된다. FPS의 평균을 몇번이나 할까 하는것은 변수 N을 통하여 조정 가능하다. 또, 정의된 FPS값을 변경하면 원하는 FPS로 설정 가능하다. 얘를 들어 다음과 같이 30FPS로 설정 가능하다.


static const int FPS = 30;


실행결과



30프레임으로 동작중이다. PSP같은 경우는 30FPS이며 PSP와 같이 높은 프레임레이트를 필요로 하지 않는 경우에는 이렇게 조정하는것도 좋은 방법이다. 특히 스마트폰 게임과 같은경우는 프레임레이트를 높이게되면 베터리의 소모에도 영향을 줄 수 있기 때문이다.

Posted by 캡슐리어
DX라이브러리2014. 12. 30. 17:02

3.13 개조불가능한 세이브데이터 만드는법 - 2


구체적인 이야기에 들어가기 앞서 간단하게 정리를 해본다.


세이브데이터

경험치 = 1000


위와 같은 데이터가 있다고 할 때 수정은 매우 간단하다. 


세이브데이터

경험치 = 2000


이처럼 단순히 수정해주기만 해도 된다. 하지만 다음과 같은 데이터라면 이야기가 틀려진다.


세이브데이터

경험치 = 1000

해시값 = 25


해시값은 경험치의 값에따라 변경되는 값이다. 경험치를 변경하기 위해선 해시값역시 수정해주어야 한다. 수정을 시도하는 입장에선 이 해시값이 어떻게 도출됐는지 알 수 없다. 시험삼아 경험치항목만 2000으로 변경해본다.


세이브데이터

경험치 = 2000

해시값 = 25


유저와는 다르게 프로그래머 입장에서는 이게 잘못된 데이터라는것을 알 수 있다. 해시값의 공식이 "경험치를 39로 나눈 나머지"라는 규칙을 직접 적용했기 때문이다. 1000을 39로 나눈 나머지는 25이지만 2000을 39로 나눈 나머지는 11이다. 계산이 맞지 않기 때문에 임의 수정된 값이란것을 알 수 있다.


이처럼 세이브데이터에 의존하여 변경하는 값을 함께 기록하면 세이브데이터의 임의 수정을 발견 가능하다. 위의 예에서는 39로 나눈 나머지라는 규칙을 사용했기 때문에 39번 시험해보면 해시값을 발견 가능하지만 지금부터 이야기할 해시 알고리즘인 MD5를 사용하게 되면 340282366920938463463374607431768211456개의 패턴이 있기 때문에 현실적으로 해독하기가 불가능하다.


MD5는 일종의 난수라고 생각 할 수 있다. 최초에 시드가 되는 키를 입력하는 것으로 규칙을 알 수 없는 일억개의 초기값이 결정된다. 여기에 데이터를 하나씩 부여하는 것으로 해시값은 하나하나 알 수 없는 값으로 변화한다. 하지만 언뜻 보기에 난수처럼 보이는 이 값들도 같은 순서를 통해 계산하면 반드시 같은 값을 반환한다. 초기값을 39라고 가정하자. 그리고 경험치인 "1000"의 문자열을 한문자씩 MD5 계산함수를 사용하여 반환해보면 


"1"을 적용하면 해시값이 변한다.

"0"을 적용하면 해시값이 변한다.

"0"을 적용하면 해시값이 변한다.

"0"을 적용하면 해시값이 변한다.


최종적으로 반환된 값은 언뜻보기에 알 수 없는 엄청난 길이의 함수값이 된다. 임의수정을 확인하기 위해서는 위에서의 방법과 마찬가지로 39를 초기값으로 "1,0,0,0"순서대로 데이터를 적용해보면 방금 얻은 해시값과 동일한 값을 얻을 수 있다. 이것이 "2000"같이 경험치가 임의수정됐다고 한다면 계산되는 해시값 역시 달라지며 임의수정을 검출해 낼 수 있다.


여기서 MD5 알고리즘을 이해하고 또 실제로 사용하는것은 어려울 수 있지만 windows.h에는 해시값에 대한 계산 라이브러리가 포함되어있다. 여기를 확인해보자. 이렇게 계산된 해시값을 세이브데이터에 추가하여 임의수정체크를 해주면 되겠다.

Posted by 캡슐리어
DX라이브러리2014. 12. 30. 16:26

3.12 개조불가능한 세이브데이터 만드는법 - 1


지난 섹션에서 작성한 세이브파일은 바이너리에디터를 사용하면 간단하게 수정가능하다. 그렇다면 암호화를 통해서 수정하지 못하게 하면 어떨까 라는 생각이 들겠지만 사실 암호화는 필요하지 않다. 암호화 같은 복잡한 방법을 사용하지 않더라도 세이브파일이 수정되었는가 여부만 검사하면 되는 일이다. 일단 다음과 같이 세이브데이터를 텍스트로 보존한다고 가정하자.


HP = 200

MP = 100

소지금 = 1000

경험치 = 1000 


유저는 이 세이브파일을 열어 "경험치 = 2000" 이라고 수정했을 때 수정한 사실을 알고 있다면 게임에 적용하지 않으면 된다. 이는 해시값을 이용하여 구현 가능하다. 해쉬값은 해쉬함수를 통하여 구할 수 있으며 해시함수는 특정 계산식을 이용하여 특정값을 다른 값에 대응시키는 역할을 한다. 


HP = 200

MP = 100

소지금 = 1000

경험치 = 1000 


위에서 예를 들었던 데이터는 데이터 본체를 출력하고 있기 때문에 어딘가가 수정되어도 수정되었다는 것을 알 수 없다. 하지만 데이터의 마지막 부분에 아래와 같이 합계를 추가했다고 한다면 경험치 부분을 "경험치 = 2000"이라고 수정해도 합계와 맞지 않기 때문에 수정된 데이터라는 것을 알 수 있다.


HP = 200

MP = 100

소지금 = 1000

경험치 = 1000 

합계 = 2300


이 처럼 어딘가가 변경되면 함께 바뀌게되는 구조를 포함시키면 데이터 수정 여부를 검출 가능하다. 또한 아래와 같이 바이너리로 저장되어있다면 값이 변화할때마다 같이 변화하는 값이 별도로 존재한다고 상상하기 힘들것이다. 단, 데이터가 1:1대응이라면 이 역시 판독될 가능성이 매우 크다. 



최후에 기록되는 숫자를 합계가 아니라 합계에서 79로 나눈 수의 나머지라고 하고 이 값을 해시값이라고 가정해보자.


HP = 200

MP = 100

소지금 = 1000

경험치 = 2000 

해시값 = 61


위에서와 같은 데이터를 가지고 있을 때에 경험치를 2000에서 3000으로 변했다고 생각하면 합계 4300이며 79나누었을 때 나머지는 34가 된다.


HP = 200

MP = 100

소지금 = 1000

경험치 = 3000 

해시값 = 34


유저에게는 합계를 79로 나눈 나머지 라는 계산식을 알 수 없기 때문에 수정해보려고 해도 불가능 하게 된다. 어느 한부분이라도 값이 틀려지면 해시값과 맞지 않기 때문에 임의 수정되었는가 여부의 검출이 가능해진다. 또한 세이브 데이터를 이용하여 해시값의 계산은 가능해지지만 해시값을 이용한 세이브데이터의 역산도 불가능해진다.



이와 같이 역으로 계산 불가능한 해시 계산 방법을 단방향 해시라고 한다. 해시값의 계산 방법은 굉장히 많기 때문에 솔직히 말해 어떤 것을 사용하든 상관 없다. 탁 까놓고 말해 위에서 예로 들었던 79로 나눈 나머지값을 해시로 사용하는 것 만으로도 데이터의 임의 수정 가능성은 매우 낮아지게 된다. 하지만 유저들이 세이브데이터를 분석하여 마지막 4바이트의 값이 해시값이란것을 알게 된다면 1/79의 확률로 데이터 수정이 가능하게 된다. 때문에 보통 많이 사용하는 해시함수에는 "MD5", "SHA"등이 있다. 예를 들어 MD5는 128bit로 표현하는 함수이며 340282366920938463463374607431768211456종류의 해시값이 존재한다. 분석하려면 매우 큰 노력이 필요할 것이다. 다음 섹선에서는 MD5를 이용한 해시값 이용방법을 알아본다.

Posted by 캡슐리어
DX라이브러리2014. 12. 30. 14:35

3.11 세이브데이터 만드는법


* 원본에서도 3.8 ~ 3.10은 없습니다.


C언어 입문시에 파일출력하는 법을 배우게 된다. 


FILE *fp = fopen( "***.txt", "w" );


위 명령줄을 통해 파일을 열고


int money = 1000;

fprintf( fp, "현재 소지금 = %d\n", money );


과 같은 명령줄을 사용하게되면 실제 파일에는 아래 그림과 같이 저장된다.



하지만 이런 방식으로는 데이터가 너무 쉽게 개조가 가능해지며 데이터의 양이 몇백, 몇천이상이 되었을 때 매우 번거로워진다. 때문에 텍스트 파일이 아닌 바이너리 파일로 출력을 하는것이 좋다. 이  fopen함수의 역할을 확인해보자.


FILE *fp = fopen( "***.dat", "wb" );


이 명령줄처럼 "b"를 붙이게 되면 바이너리파일로 출력된다. 바이너리 파일에 데이터를 출력하는 함수는 fwrite함수이며 각각 인수는 아래와 같이 설명 가능하다.


제 1인수 : 세이브 데이터를 갖게되는 변수의 어드레스

제 2인수 : 출력할 bite수

제 3인수 : 출력 갯수

제 4인수 : 출력할 파일 포인터


위에서 살펴본 money변수에 저장된 int형 값을 출력하고 싶을 때에는 아래 명령줄과 같다.


fwrite( &money, sizeof(int), 1, fp );


실행 예제를 작성해보면 다음과 같다.


#include <stdio.h>


int main(){

        int money = 1000;

        FILE *fp = fopen( "savedata.dat", "wb" ); // 파일 열기

        if( fp == NULL ){ // NULL이 반환되면 종료

                return 0;

        }

        fwrite( &money, sizeof(int), 1, fp ); // 파일에 money의 값을 출력

        fclose( fp ); //파일 닫기

        return 0;

}


실행결과



이 예제는 DX라이브러리를 사용하지 않기 때문에 별도의 프로젝트를 생성해서 테스트해보자. 참고로 필자는 데브C++을 사용해서 테스트했다. 프로그램을 실행해보면 savedata.dat라는 파일이 생성되며 메모장을 통해 열어보아도 내용 확인이 되지 않는다. (다만, 바이너리 에디터로 열게되면 텍스트파일과 마찬가지로 편집 가능해지기는 하다.) 세이브데이터는 하나씩 출력하면 매우 번거롭다. 따라서 구조체를 이용하여 한번에 출력하는 것이 편하다. 예를들어 세이브하고싶은 데이터에 HP, MP, 소지금, 경험치의 4종류의 데이터가 있다고 가정하면 이 것을 한개의 구조체로 만들고 fwrite함수를 통해서 한번에 출력한다.


#include <stdio.h>


typedef struct{

        int HP;         //HP

        int MP;         //MP

        int Money;      //소지금

        int Exp;        //경험치

}SaveData_t;


int main(){

        SaveData_t Data = { 200, 100, 1000, 1000 };

        FILE *fp = fopen( "savedata.dat", "wb" );

        if( fp == NULL ){

                return 0;

        }

        fwrite( &Data, sizeof(Data), 1, fp ); // SaveData_t구조체 내용을 출력

        fclose( fp );

        return 0;

}


생성된 파일을 열어보면 다음과 같다.



int형의 값들이 네개 저장되어있는것을 확인 가능하다. 다음은 출력한 데이터를 읽어들이는 프로그램을 만들어보자. fwrite함수는 출력을 담당했었고 이에 대응하여 입력은 fread함수가 담당한다. 인수부분은 fwrite와 동일하다. 위 예제에서 출력된 세이브파일을 읽어들여보자.


#include <stdio.h>


typedef struct{

        int HP;         //HP

        int MP;         //MP

        int Money;      //소지금

        int Exp;        //경험치

}SaveData_t;


int main(){

        SaveData_t Data;

        FILE *fp = fopen( "savedata.dat", "rb" );

        if( fp == NULL ){

                return 0;

        }

        fread( &Data, sizeof(Data), 1, fp );

        fclose( fp );


        printf("HP=%d\nMP=%d\n소지금=%d\n경험치=%d\n",

                Data.HP, Data.MP, Data.Money, Data.Exp );


        return 0;

}


실행결과



세이브파일 작성시의 예제와 차이점은 fopen에서의 파일 오픈 방법을 쓰기 방식의 w에서 읽기 방식의 r로 변환한 것 뿐이다. 이후는 fwrite를 fread로 변경했다. 파일 출력시와 마찬가지로 정상적으로 값을 불러오는 것을 알 수 있다. 세이브파일 제작시에는 구조체를 사용하자.



Posted by 캡슐리어
DX라이브러리2014. 12. 26. 14:56

3.7 특정 각도로 탄을 날려보자


슈팅게임에서의 탄도는 특정각도를 통하여 궤도가 설정되고 이를 구현하기 위해서는 삼각함수의 지식이 필수적으로 필요하다. 간단하게 살펴보자면 다음과 같다.



직각삼각형의 밑변과 사변의 각도가 30도일 때는 각 변의 길이 비율이 1:2:√3 이된다. 하지만 게임제작에 있어서는 이런걸 기억하고 있어보았자 딱히 쓸모가 없다. 탄의 발사 각도는 정확히 30도가 아니다. 1도가 될수도 있고 65도가 될수도 있다.



이와 같이 각도가 θ일때 x와 y가 어떻게 되는가 정도는 계산이 가능해야 한다. 



학생시절 배운 것 처럼 알파뱃의 모양을 생각하여 표현했다.


sinθ= y/r

conθ=x/r


이것은 다음과 같은 부분에서 사용하게된다.


그림1. 탄을 보낼 방향과 좌표


위 그림처럼 좌하를 기준 좌표로 하고 우상방향으로 이동하는 탄이 있다고 가정해보자. 이 탄은 각도θ로 움직이며 r정도 움직였을 때의 좌표는 어디인가? 라는 질문의 답을 얻을 때 사용하게 된다. sinθ=y/r이므로 y=sinθ/r 이며 마찬가지로 x=cosθ/r 이 된다. 탄은 1프레임에 r의 거리만큼 움직이며 이 거리가 바로 탄의 속도가 된다. 


따라서 θ와 r을 고정하게 되면 x,y의 값은 그저 기존값에 늘어난 양 만큼 가산해주면 좌표가 갱신된다.


x += cosθ × r;

y += sinθ × r;


다음 예제에서 이를 확인해보자 θ를 변수 angle, r을 변수 speed로 선언한다. z키를 입력하면 좌표를 중앙으로 복귀시키고 각도와 속도를 랜덤으로 재설정한다. 생성된 변수값들은 화면의 좌상에 표시된다.


#include <math.h>

#include "DxLib.h"


#define PI 3.1415926f


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK );


        int Handle[3];

        LoadDivGraph( "image/bullet_01.png", 3, 3, 1, 14, 16, Handle );


        float x=320,y=240,angle=0,speed=1;


        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){


                x += cos( angle ) * speed; //x좌표를 갱신

                y += sin( angle ) * speed; //y좌표를 갱신


                if( CheckHitKey( KEY_INPUT_Z ) == 1 ){ //z키를 누르면

                        x = 320;        // x좌표를 초기화

                        y = 240;        // y좌표를 초기화

                        angle = GetRand(10000)/10000.f * (PI*2);        //0~PI*2의 난수 생성

                        speed = 0.5f + GetRand(10000)/10000.f * 2;      //0.5~2.5의 난수 생성

                }


                DrawRotaGraph( x, y, 1.0, angle+PI/2, Handle[0], TRUE );

                DrawFormatString( 0,0,GetColor(255,255,255), "angle=%.2f, speed=%.2f\n", angle, speed );


        }


        DxLib_End();

        return 0;

}


실행결과



참고로 탄 이미지의 최초 각도는 +PI/2로 되어있다. 기본이미지의 방향은 상단 방향인데 프로그램상 0도의 경우 화면의 오른쪽이 된다. 즉, 최초의 각도가 -PI/2로 되어있기 때문에 이를 보정해주기 위해 +PI/2를 추가한다. 만약 이미지의 방향이 최초부터 오른쪽 방향으로 되어있다면 +PI/2는 불필요 하다.


x += cosθ × r를 소스코드로 변환하면 x += cos( angle ) * speed;된다. y좌표도 동일하다. 이와같은 계산을 거치게 되면 각도 angle과 속도 speed로 탄을 움직일 수 있게 된다.

Posted by 캡슐리어
DX라이브러리2014. 12. 26. 13:49

3.6 sin함수를 이용한 매끄러운 이동


sin파의 특정 부분부터 특정 부분까지 이동시키는 것도 가능하다. 



붉은 부분 (x가 0~PI/2의 부분)에서의 y는 0에서 시작하여 PI/2에 가까워질수록 변화량이 작아진다. x가 PI/2가 되면 y값은 1이 된다. sin파의 경우 x의 값이 0일 때 y값의 변화가 가장 크다. 이 지점부터 점점 느려지면서 멈추는 이미지가 된다. 그에 반해서 다음 그래프의 경우는 천천히 시작해서 천천히 멈추는 sin파의 역시 존재한다.



x가 2PI-PI/2 ~ 2PI+PI/2 


이와 같은 두 구간을 이용하는 것으로 다음과 같은 움직임을 구현 가능하다.


- 갑자기 움직이기 시작해서 천천히 멈춘다.

- 첨천히 움직이기 시작해서 천천히 멈춘다.


단, 이런식의 사용은 y가 -1에서 1 사이의 값을 가지기 때문에 sin파에 1을 더한 후 2로 나누어 0에서 1의 값을 갖도록 하는 경우가 많다.



이 그래프가 (sin파+1)/2 를 통해서



이렇게 변한다.


다음 예제는 Z키를 입력하면 2종류의 sin파를 사용하여 움직이는 프로그램이다.


#include <math.h>

#include "DxLib.h"


#define PI 3.1415926f


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK );


        int Handle;

        int Count = 0;

        Handle = LoadGraph( "image/character_01.png" );


        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){


                DrawRotaGraph( 420, 400-sin(PI/2/120*Count)*300, 1.0, 0.0, Handle, TRUE );


                DrawRotaGraph( 220, 400-(sin(-PI/2+PI/120*Count)+1)/2*300, 1.0, 0.0, Handle, TRUE );


                if( Count < 120 ){

                        Count++;

                }

                if( CheckHitKey( KEY_INPUT_Z ) == 1 ){

                        Count = 0;

                }

        }


        DxLib_End();

        return 0;

}  


실행결과




왼쪽 : -PI/2~PI/2의 범위를 사용한 이동

오른쪽 : 0~PI/2의 범위를 사용한 이동


왼쪽의 y의 계산식은 "400-sin(PI/2/120*Count)*300"이다. 위 예제에서 Count는 120까지 상승하고 PI는 120으로 나누었기 때문에 변화하는 값은 0에서 PI/2까지가 되겠다. 여기에 300을 곱한 후 400에서 빼주기 때문에 y의 값은 400에서 100까지 변화한다. 


오른쪽 y의 계산식은 "400-(sin(-PI/2+PI/120*Count)+1)/2*300"이다. 우선 sin함수에서 내부에서 계산 값은 -PI/2~PI/2가 된다. 이에 sin함수가 반환하는 값은 -1~1이 된다. 이를 0~1로 변화시키기 위해 1을 더하고 다시 2로 나누었다. 이후 다시 300을 곱하게 되면 y의 값은 왼쪽 과 마찬가지로 400에서 100으로 변화하게 된다.


Posted by 캡슐리어
DX라이브러리2014. 12. 24. 15:58

3.5 sin 함수를 이용한 물체 이동 / 확대축소


부드러운 상하이동 또는 확대축소가 필요할 경우 sin함수를 이용하면 좋다. sin은 삼각함수의 그 sin 맞다. sin함수를 그래프로 그려보면 다음과 같다.



가로를 시간축, 세로가 y 값이라고 하면 이 곡선과 같은 부드러운 상하 이동이 가능해진다. 원주율을 PI라고 한다면 1초간 60이 증가하는 카운터(Count)를 사용한다면 아래와 같은 공식을 만들 수 있다.


sin(PI*2/60*Count)


이 식은 -1~1사이의 값을 얻을 수 있고 200을 곱하게 된다면 -200에서 200사이의 값을 구할 수 있게 된다.


예제


#include <math.h>

#include "DxLib.h"


#define PI 3.141592654f


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK ); 


        int Handle;

        int Count = 0;

        Handle = LoadGraph( "image/character_01.png" );



        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){


                DrawRotaGraph( 100, 240+sin(PI*2/240*Count)*200, 1.0, 0.0, Handle, TRUE ); //이미지 이동

                DrawRotaGraph( 500, 240, 1.0+sin(PI*2/120*Count)*0.5, 0.0, Handle, TRUE ); //이미지 확대 축소

                                Count++;


        }

        

        DxLib_End();

        return 0;

}


실행결과



첫번째 DrawRotaGraph는 y좌표에 sin함수를 적용하였고 두번째 DrawRotaGraph에서는 이미지의 크기에 sin함수를 적용한 결과이다. 60카운트에 1초의 계산이기 때문에 각각 4초 2초 주기로 변화한다. 또한 첫번째는 y좌표가 40~440사이에서 변화하고 있으며 두번째에서는 크기 배율에 0.5를 곱하기 때문에 0.5배에서 1.5배사이의 크기 변화를 볼 수 있다. 다음 공식을 활용하도록 하자.


sin( PI*2 / 주기 * Count ) * 진폭

Posted by 캡슐리어
DX라이브러리2014. 12. 24. 14:43

3.4 간단한 선택화면 (역방향 루프)


전 섹션에서는 아래방향으로의 루프를 재현해보았다. 그렇다면 반대방향으로 루프는 어떻게 해야 할까. 언뜻 생각하기에 SelectNum변수에 +1을 해서 아래방향으로 진행했으니 그 반대로 -1을 하면 되지 않을까 하는 생각이 들게 된다. 하지만 -1을 하게 되면 수열의 진행은 다음과 같아 진다.


0, -1, -2, -3, -4, 0, -1, -2, -3, -4........


우리가 원하는 루프는


0, 4, 3, 2, 1, 0, 4, 3, 2, 1.......


이다. 빼는 계산으로는 불가능하다는 것을 알았으니 더하는 계산으로 동일하게 구현해야 한다. 이 때에는 -1이 아니라 +4를 해주면 원하는 값을 얻을 수 있다. 나누는 수에서 -1한 수를 더하는 것으로 역루프가 성립된다. 전 섹션에서 사용했던 예제에 역루프를 추가해 보자


#include <DxLib.h>


int Key[256];


int gpUpdateKey(){

        char tmpKey[256];

        GetHitKeyStateAll( tmpKey );

        for( int i=0; i<256; i++ ){ 

                if( tmpKey[i] != 0 ){

                        Key[i]++;

                } else {

                        Key[i] = 0;

                }

        }

        return 0;

}


// 메뉴항목에 필요한 구조체 선언

typedef struct{

        int x, y;       // 좌표 변수

        char name[128]; // 항목 이름 변수

} MenuElement_t;


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK );


        // 메뉴항목은 5개를 만든다.

        MenuElement_t MenuElement[5]={

                {  80, 100, "새 게임" }, // 구조체 내부에 선언된 순서대로 각각의 값이 저장된다. (x,y,name 순서)

                { 100, 150, "보너스" },

                { 100, 200, "도움말" },

                { 100, 250, "설정" },

                { 100, 300, "게임종료" },

        };

        int SelectNum = 0; // 현재 선택 번호



        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 && gpUpdateKey()==0 ){


                // 계산 구문

if( Key[ KEY_INPUT_DOWN ] == 1 ){ // 아래 방향키 처리


                        SelectNum = ( SelectNum + 1 ) % 5;

                }


if( Key[ KEY_INPUT_UP ] == 1 ){ // 위 방향키 처리


SelectNum = ( SelectNum + 4 ) % 5; // 역 루프 계산식

}


if( Key[ KEY_INPUT_DOWN ] == 1 || Key[ KEY_INPUT_UP ] == 1 ){ // 위 또는 아래 방향키가 입력

for( int i=0; i<5; i++ ){

if( i == SelectNum ){

MenuElement[i].x = 80;

} else {                      

MenuElement[i].x = 100;

}

}

}



                // 묘화 구문


                for( int i=0; i<5; i++ ){ // 메뉴 항목 묘화

                        DrawFormatString( MenuElement[i].x, MenuElement[i].y, GetColor(255,255,255), MenuElement[i].name );

                }


        }


        DxLib_End();

        return 0;

}


실행결과





Posted by 캡슐리어
DX라이브러리2014. 12. 24. 14:18

3.3 간단한 선택화면


이번 섹션에선 구조체를 이용하여 진행한다. 자세한 사항은 이곳을 참고하자.


메뉴항목에서는 X좌표, Y좌표, 메뉴이름이 한 묶음이 될 수 있으며 다음과 같은 방식으로 선언 가능하다.


typedef struct{

  int x, y좌표;

  char* 이름;

} 메뉴항목태그;


이를통하여 메뉴항목을 작성하면 매우 편리하다. 또한 만약 메뉴 항목이 5개라면 메뉴항목태그[5]같은 방식으로 배열을 선언하여 계산 루프를 진행하면 효율적이다. 다음 예제는 방향키 "하"를 누를 때 마다 메뉴에서 한칸씩 아래로 내려가는 모습을 구현하였다.


#include <DxLib.h>


int Key[256];


int gpUpdateKey(){

        char tmpKey[256];

        GetHitKeyStateAll( tmpKey );

        for( int i=0; i<256; i++ ){ 

                if( tmpKey[i] != 0 ){

                        Key[i]++;

                } else {

                        Key[i] = 0;

                }

        }

        return 0;

}


// 메뉴항목에 필요한 구조체 선언

typedef struct{

        int x, y;       // 좌표 변수

        char name[128]; // 항목 이름 변수

} MenuElement_t;


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK );


        // 메뉴항목은 5개를 만든다.

        MenuElement_t MenuElement[5]={

                {  80, 100, "새 게임" }, // 구조체 내부에 선언된 순서대로 각각의 값이 저장된다. (x,y,name 순서)

                { 100, 150, "보너스" },

                { 100, 200, "도움말" },

                { 100, 250, "설정" },

                { 100, 300, "게임종료" },

        };

        int SelectNum = 0; // 현재 선택 번호


        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 && gpUpdateKey()==0 ){


                // 계산 구문


                if( Key[ KEY_INPUT_DOWN ] == 1 ){ // 방항키"하"를 입력했을 때에만 처리


                        SelectNum = ( SelectNum + 1 ) % 5; // 현재 항목에서 한칸 밑으로 내림 (루프)


                        for( int i=0; i<5; i++ ){              // 메뉴 항목의 갯수인 5로 루프 처리

                                if( i == SelectNum ){          // 지금 처리중인 항목이 선택번호와 같으면

                                        MenuElement[i].x = 80; // 좌표를 x좌표를 80으로

                                } else {                       // 지금 처리중인 항목이 선택번호와 다르면

                                        MenuElement[i].x = 100;// 좌표를 x좌표를 100으로

                                }

                        }

                }


                // 묘화 구문


                for( int i=0; i<5; i++ ){ // 메뉴 항목 묘화

                        DrawFormatString( MenuElement[i].x, MenuElement[i].y, GetColor(255,255,255), MenuElement[i].name );

                }


        }


        DxLib_End();

        return 0;

}


실행결과



이번에도 역시 계산구문과 묘화구문이 나누어져있으며 항상 이를 의식하고 코딩해나가자. 아래 예제는 이 구분을 의식하지 않고 작성한 예이다.


#include <DxLib.h>


int Key[256]; 


int gpUpdateKey(){

char tmpKey[256];

GetHitKeyStateAll( tmpKey ); 

for( int i=0; i<256; i++ ){ 

if( tmpKey[i] != 0 ){ 

Key[i]++;     

} else {              

Key[i] = 0;  

}

}

return 0;

}


typedef struct{

        int x, y;               

        char name[128];

} MenuElement_t;


int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){

        ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK );


        MenuElement_t MenuElement[5]={

                {  80, 100, "새 게임" },

                { 100, 150, "보너스" },

                { 100, 200, "도움말" },

                { 100, 250, "설정" },

                { 100, 300, "게임종료" },

        };

        int SelectNum = 0;


        while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 && gpUpdateKey()==0 ){


                // 계산 구문


                if( Key[ KEY_INPUT_DOWN ] == 1 ){ 


                        SelectNum = ( SelectNum + 1 ) % 5; 


                }


                // 묘화 구문


                for( int i=0; i<5; i++ ){

                        if( i == SelectNum ){

                                DrawFormatString( MenuElement[i].x-20, MenuElement[i].y, GetColor(255,255,255), MenuElement[i].name );

} else {

DrawFormatString( MenuElement[i].x,    MenuElement[i].y, GetColor(255,255,255), MenuElement[i].name );

}

}


}


DxLib_End(); 

return 0;

}


실행결과는 위의 예제와 완전히 동일하다. 예제와 같이 짧은 코드에서는 크게 문제가 되지 않을 수 있지만 프로그램이 커지게 되면 이런 방식은 문제가 발생할 확률이 매우 높다. 되도록이면 계산, 묘화구문은 구별하여 작성하도록 한다.

Posted by 캡슐리어