지금까지 OF실행 방법 및 간단한 개념과 파이프 게임의 설계, 구현 과정을 살펴보았다.

이번 포스팅에서는 구현한 기능들이 모두 잘 작동 하는지 확인하고, 게임을 플레이 해 보자.


1. 커맨드 키 (S/Q/R/C) 작동

S-Q-R 키를 순서대로 누른 경우
S-Q-C 키를 순서대로 누른 경우
 
2. 파이프 배치 및 현재 위치 업데이트, 물 흐르기, 점수 표시 기능
파이프는 스페이스 키를 눌러 배치 가능
 
플레이어가 배치한 파이프 모두 화면에 정상적으로 잘 표시되며

새로운 위치에 현재 파이프(핑크색 파이프)의 위치도 잘 업데이트 되고,

물줄기도 시간이 지날수록 점점 파이프를 따라 흐르는 등 모든 모션이 정상적으로 잘 작동한다.

화면 우측 상단에 점수 표시 기능도 정상적으로 작동한다.

 

3. 파이프 회전 기능

위, 아래 방향키로 파이프 회전 가능
 
4. 파이프 종류 변경 기능
좌, 우 방향키로 파이프 종류 변경 가능
 
5. 게임오버 상황 (총 4가지)
파이프를 미처 배치하기 전에 물줄기가 흘러내린 경우
파이프를 끊어지게 배치한 경우
파이프가 게임 화면범위 바깥을 튀어나가는 경우
파이프가 이미 놓여져 있는 자리에 또 배치하는 경우

6. 게임 성공

게임을 success 하면 최종 score를 확인할 수 있다.

이렇게 좀 허접하긴 해도, 구현한 기능 모두 에러 없이 잘 돌아간다.


6. 게임 플레이

간단한 경로로 파이프를 배치하여 게임을 클리어했다.

파이프 라인을 복잡하게 배치하여 물줄기가 오래 흐르게 하면 더 높은 점수를 획득할 수 있다.

 


이렇게 총 5개의 포스팅에 걸친 파이프게임 프로젝트 리뷰가 끝났다!

샘플 코드 없이 100% 직접 기획하고 구현한 자유 프로젝트라 뜻깊은 과제였던 것 같다.

 

지난 포스팅에 이어짐

이제 user defined 함수들을 살펴보자.

 

5) void ofApp::drawPipe( )

//----------- Function for drawing pipe -----------
// consider : type of pipe, rotation, location
// pink pipe = a pipe which is waiting to be placed by user
// grey pipe = pipes already placed on the screen
void ofApp::drawPipe() {
	if (currentP->pipe_type == 1) {
		float x, y;
		switch (currentP->rotation) {
		case 1:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 120, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x + 105, y - 10, 15, 60);
			break;
		case 2:
			x = currentP->x + 40;
			y = currentP->y;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 120);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x - 10, y + 105, 60, 15);
			break;
		case 3:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 120, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x, y - 10, 15, 60);
			break;
		case 4:
			x = currentP->x + 40;
			y = currentP->y;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 120);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x - 10, y, 60, 15);
			break;
		}
	}

	else if (currentP->pipe_type == 2) {
		float x, y;
		switch (currentP->rotation) {
		case 1:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x + 40, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x + 30, y + 65, 60, 15);
			break;
		case 2:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x + 40, y - 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x, y - 10, 15, 60);
			break;
		case 3:
			x = currentP->x + 40;
			y = currentP->y;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x, y + 40, 40, 40);
			ofDrawRectangle(x + 40, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x - 10, y, 60, 15);
			break;
		case 4:
			x = currentP->x + 40;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x + 65, y - 10, 15, 60);
			break;
		}
	}

	else if (currentP->pipe_type == 3) {
		float x, y;
		switch (currentP->rotation) {
		case 1:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x + 40, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x, y - 10, 15, 60);
			break;
		case 2:
			x = currentP->x;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x + 40, y - 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x + 30, y - 40, 60, 15);
			break;
		case 3:
			x = currentP->x + 40;
			y = currentP->y;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x, y + 40, 40, 40);
			ofDrawRectangle(x + 40, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x + 65, y + 30, 15, 60);
			break;
		case 4:
			x = currentP->x + 40;
			y = currentP->y + 40;
			ofSetColor(colorA, colorB, colorC);
			ofDrawRectangle(x, y, 40, 40);
			ofDrawRectangle(x + 40, y, 40, 40);
			ofDrawRectangle(x, y + 40, 40, 40);
			ofSetColor(colorA, colorD, colorE);
			ofDrawRectangle(x - 10, y + 65, 60, 15);
			break;
		}
	}
}
 

이번 포스팅의 user defined 코드들을 살펴보면 비로소 파이프 게임이 굴러가는 방식을 확인할 수 있다.

일단 drawPipe 함수는 파이프의 모양을 읽어 화면에 그리는 역할의 함수이다.

 

drawPipe 함수는 크게 3가지 파트로 나뉘어져 있으며, 각 파트는 다시 4가지 파트로 나뉜다.

파이프의 회전 각도나 파이프의 종류는 rotation과 pipe_type 변수 값을 통해 구별할 수 있다.

 

파이프의 종류는 총 3가지, 회전 각도는 총 4가지 이므로 drawPipe 함수는 3가지의 파이프의 종류에 대해 각각 4가지 의 roation 경우를 다룬다. 이는 코드를 보면 쉽게 이해할 수 있는데, 결과적으로 rotation 과 pipe_type 변수 값에 따라 다른 모양의 파이프가 화면에 표시되는 것이다.

 

현재 파이프는 핑크색으로, 이미 배치된 파이프는 회색으로 그려진다.

현재 파이프가 표시되는 위치는 파이프를 새롭게 배치할 때 마다 업데이트 되고,

이미 배치된 파이프의 위치 정보는 파이프 연결리스트에 저장되어 있어 draw 함수에서 해당 정보를 읽고 배치된 파이프를 모두 그린다.

 

7) void ofApp::addPath( )

//----------- Function for addning water nodes in water linked list -----------
// save every location water will go by : according to the pipes placed on the screen
// on every location the small blue circle will be drawn (water path)
void ofApp::addPath() {
	if (currentP->pipe_type == 1) {
		struct water *w;
		w = (struct water*)malloc(sizeof(struct water));
		switch (currentP->rotation) {
		case 1:
			w->x = currentP->x + 1; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 120; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x + 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x + 120;
			pyNew = currentP->y;
			break;
		case 2:
			w->x = currentP->x + 60; w->y = currentP->y + 1;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 120; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y + 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y + 120;
			break;
		case 3:
			w->x = currentP->x + 119; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 120; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x - 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x - 120;
			pyNew = currentP->y;
			break;
		case 4:
			w->x = currentP->x + 60; w->y = currentP->y + 119;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 120; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y - 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y - 120;
			break;
		}
	}

	else if (currentP->pipe_type == 2) {
		struct water *w;
		w = (struct water*)malloc(sizeof(struct water));
		switch (currentP->rotation) {
		case 1:
			w->x = currentP->x + 1; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x + 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y + 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y + 120;
			break;
		case 2:
			w->x = currentP->x + 60; w->y = currentP->y + 1;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y + 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x - 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x - 120;
			pyNew = currentP->y;
			break;
		case 3:
			w->x = currentP->x + 119; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x - 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y - 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y - 120;
			break;
		case 4:
			w->x = currentP->x + 60; w->y = currentP->y + 119;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y - 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x + 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x + 120;
			pyNew = currentP->y;
			break;
		}
	}
// 이어서
 
// 이어서
    else if (currentP->pipe_type == 3) {
		struct water *w;
		w = (struct water*)malloc(sizeof(struct water));
		switch (currentP->rotation) {
		case 1:
			w->x = currentP->x + 60; w->y = currentP->y + 119;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y - 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x - 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x - 120;
			pyNew = currentP->y;
			break;
		case 2:
			w->x = currentP->x + 1; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x + 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y - 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y - 120;
			break;
		case 3:
			w->x = currentP->x + 60; w->y = currentP->y + 1;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y + 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x + 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x + 120;
			pyNew = currentP->y;
			break;
		case 4:
			w->x = currentP->x + 119; w->y = currentP->y + 60;
			w->next = NULL;
			endW->next = w; endW = w;
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x - 1; w->y = endW->y;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			for (int i = 1; i < 60; i++) {
				struct water *w;
				w = (struct water*)malloc(sizeof(struct water));
				w->x = endW->x; w->y = endW->y + 1;
				w->next = NULL;
				endW->next = w; endW = w;
			}
			pxNew = currentP->x;
			pyNew = currentP->y + 120;
			break;
		}
	}
}
 

addPath 함수는 물줄기가 흘러가야 할 경로를 저장하는 역할을 수행한다. 새롭게 파이프를 배치할 때 마다 물줄기 연결리스트에 계속 경로가 추가되는데, 이때 '경로'라는 것은 파이프의 가운데 위치 경로를 의미한다.

물이 흐르는 경로가 저장되는 과정
위 그림과 같이 파이프 하나를 배치하면, 파이프의 가운데 위치 좌표가 하나씩 물줄기 연결리스트에 추가되는 것이다.

연결리스트에 저장된 경로를 어디까지 읽어 화면에 표시할 것 인지를 시간이 지날수록 점점 늘려 나가면,

마치 물줄기가 파이프를 따라 흘러가는 듯한 모양새로 표현된다.

 

위 코드가 긴 이유는 현재 배치하는 파이프의 모양과 각도에 따라 모양 3가지, 각도 4가지 : 총 12가지의 경우의 수에 대한 경로를 저장하도록 코드를 작성했기 때문이다...

 

코드 자체는 단순한 switch/case문의 반복이므로, 코드에 대한 세세한 설명은 생략하도록 하겠다.

 


이렇게 게임의 모든 구현이 끝났다!

아쉬운 점은 함수를 적극적으로 이용했다면 코드의 길이를 지금보다 훨씬 단축시킬 수 있었을 것 같다는 점...

 

그럼 다음 포스팅에서 이 간단한 게임이 어떻게 실행되는지를 확인해보자.

 

지난 포스팅에서 파이프 게임을 설계하는 단계까지 살펴보았다.

게임이 어떤 원리로 작동하며, 어떻게 설계되어야 하는지를 살펴보았기 때문에

이제 본격적으로 코드를 작성해보자.

 

일단 Openframeworks 프로젝트를 생성하면 안에 주석으로

여긴 뭘 쓰는 곳이다 여기 뭘 쓰면 뭐가 작동한다 등이 엄청 솰라솰라 달려있을 것이다.

나는 남이 작성한 주석은 영 거슬려 하는 편이라 필요한 것들 제외 모두 제거해 주었다.

 

1. main.cpp

#include "ofMain.h"
#include "ofApp.h"

int main( ){
	ofSetupOpenGL(1640,840,OF_WINDOW);
	ofRunApp(new ofApp());
}

main.cpp 파일은 프로젝트의 전체적인 셋업을 담당하는 파일이다.

나는 이 파일에서 window 크기만 잡아 주었다.

ofApp.cpp 파일을 컴파일하면 게임창이 팝업창처럼 뜨는데,

해당 창(window)의 크기를 잡아주는 것이다. 적당히 1640*840 정도로 잡아주었다.

 

2. ofApp.h

 

ofApp.h 파일은 ofApp.cpp 파일과 병행하여 작성해주면 되며, 일반적인 헤더파일의 용도대로

ofApp.cpp 파일에서 사용되는 자료구조 및 변수들을 선언해주면 된다.

#pragma once
#include "ofMain.h"
#include "ofTrueTypeFont.h"

먼저 필요한 표준 라이브러리를 선언해 준다.

그 다음 ofApp 클래스 안에 변수들을 선언해줄 것이다.

class ofApp : public ofBaseApp {
    public:
        //write code here
};

지금부터 살펴볼 코드는 모두 ofApp 클래스의 public 지정자 아래에 작성된다.

먼저 필요한 함수들을 선언해 준다. 함수 정의는 ofApp.cpp 파일에서 살펴보도록 하자.

void setup();
void draw();
void keyPressed(int key);
void keyReleased(int key);
void drawPipe();
void placePipe();
void addPath();

아래는 필요한 각종 변수 및 변수들의 초기값이다.

const int OF_KEY_SPACE = 32;
ofTrueTypeFont myfont;
ofImage screen;
ofPixels pix;
ofColor c1;
ofColor c2;
ofColor c3;

float limit = 40;
int start_flag = 0;
int game_over = 0;
int pipe_flag = 0;
int complete = 0;
int end_game = 0;
int score = 0;
float pxNew, pyNew;
		
int rotation = 1;
int pipe_type = 1;
float init_x = 80;
float init_y = 0;
int colorA,colorB,colorC;
int colorD,colorE;

아래는 구조체 정의와 구조체 변수들을 선언하는 부분이다.

typedef struct pipe {
	int pipe_type;
	int rotation;
	float x; float y;
	struct pipe *next;
} pipe;
struct pipe *currentP;
struct pipe *startP;
struct pipe *tempP1;
struct pipe *tempP2;

typedef struct water {
	float x; float y;
	struct water *next;
} water;
struct water *startW;
struct water *currentW;
struct water *tempW;
struct water *endW;

pipe 구조체는 파이프 게임에서 배치되는 파이프에 관련된 정보를 저장하는데 사용되며

water 구조체는 파이프 게임에서 계속 흘러가는 물줄기와 관련된 정보를 저장하는데 사용될 것이다.

 

위의 요소들을 하나의 class로 작성한 원본 코드는 아래와 같다.

class ofApp : public ofBaseApp{
    public:
        void setup();
        void draw();
        void keyPressed(int key);
        void keyReleased(int key);
        void drawPipe();
        void placePipe();
        void addPath();
        
        const int OF_KEY_SPACE = 32;
        ofTrueTypeFont myfont;
        ofImage screen;
        ofPixels pix;
        ofColor c1;
        ofColor c2;
        ofColor c3;
        
        float limit = 40;
        int start_flag = 0;
        int game_over = 0;
        int pipe_flag = 0;
        int complete = 0;
        int end_game = 0;
        int score = 0;
        float pxNew, pyNew;
        
        int rotation = 1;
        int pipe_type = 1;
        float init_x = 80;
        float init_y = 0;
        int colorA,colorB,colorC;
        int colorD,colorE;
        
        typedef struct pipe {
            int pipe_type;
            int rotation;
            float x; float y;
            truct pipe *next;
        } pipe;
        struct pipe *currentP;
        struct pipe *startP;
        struct pipe *tempP1;
        struct pipe *tempP2;
        
        typedef struct water {
            float x; float y;
            struct water *next;
        } water;
        struct water *startW;
        struct water *currentW;
        struct water *tempW;
        struct water *endW;		
};

3. ofApp.cpp

이제 본격적으로 프로그램이 구현된 ofApp.cpp 파일을 살펴보자.

 

ofApp.cpp 파일은 다양한 Openframeworks 함수들로 구성된다.

내가 게임 작성에 사용한 함수 구조들만 간단하게 정리하면 다음과 같다.

 

void ofApp::setup( ) : 프로그램 컴파일 시 가장 먼저 실행되는 함수. 변수 초기화, FPS설정 등에 사용된다.

void ofApp::draw( ) : 프로그램을 한번 컴파일 하면 일정 주기마다 반복적으로 실행되는 함수.

이 함수를 이용해서 화면에 나타나는 그림을 주기적으로 업데이트 해 움직이는 듯한 효과를 나타낼 수 있다.

void ofApp::keyPressed(int key) : 스페이스/방향키 이외의 키보드 키를 눌렀을 때 대응되는 작업을 관리한다.

void ofApp::keyReleased(int key) : 스페이스/방향키를 눌렀을 때 대응되는 작업을 관리한다.

 

아래 세개는 Openframeworks에서 제공하는 함수가 아닌, user defined function 이다.

 

void ofApp::drawPipe( ) : 파이프를 화면에 표시하는데 사용되는 함수이다. 이미 배치된 파이프는 파이프 연결리스트에서 그 정보를 읽어 화면에 모두 표시하고, 현재 배치하려는 파이프는 핑크색으로 화면에 표시하며 사용자가 모양을 바꾸거나 각도를 회전시킬 수 있다.

void ofApp::addPath( ) : 흘러가는 물줄기를 표현하는데 사용되는 함수이다. 시간이 지날수록 물줄기는 배치된 파이프를 따라 흐르기 때문에, 물줄기가 흘러가야 할 길(path)를 저장하고, 그 길을 따라 물줄기 그림을 화면에 표시해주는 역할을 한다.

 

그럼 함수 정의를 세부적으로 살펴보자.

 

1) void ofApp::setup( )

//----------- Set up preconditions for the game -----------
void ofApp::setup(){
	// setFrameRate and Background color, LineWidth
	ofSetFrameRate(1000);
	ofBackground(255, 255, 255);
	ofSetLineWidth(1);

	// initialize flags
	start_flag = 0;
	game_over = 0;
	pipe_flag = 0;
	complete = 0;
	score = 0;
	end_game = 0;

	// make header node for pipe nodes
	struct pipe *p;
	p = (struct pipe*)malloc(sizeof(struct pipe));
	p->pipe_type = 1;
	p->rotation = 1;
	p->x = 160; p->y = 0;
	p->next = NULL;
	startP = p;
	currentP = p;
	
	// make header node for water nodes
	struct water *w;
	w = (struct water*)malloc(sizeof(struct water));
	w->x = 40; w->y = 60;
	w->next = NULL;
	startW = w;
	endW = w;
	tempW = w;

	// save path of water along the initial pipe
	for (int i = 1; i <= 120; i++) {
		struct water *w;
		w = (struct water*)malloc(sizeof(struct water));
		w->x = (endW->x) + 1; w->y = endW->y;
		w->next = NULL;
		endW->next = w;
		endW = w;
	}
}

setup 함수는 아래에서 이야기할 함수들에서 사용되는 전역변수들을 초기화하는 것이 대부분이다.

컴파일 시 가장 먼저 실행되는 함수이므로, 변수들의 초기값을 설정해주는데 용이하다.

 

앞에 of 가 붙는 함수는 대부분 openframeworks에서 기본적으로 제공해주는 함수이다.

Openframeworks를 가지고 작업할 때는 직접 코드를 짜는 것도 중요하지만 기본적으로 제공되는 다양한 함수들이 필요한 경우가 많기 때문에, 아래 공식 페이지에서 기본적으로 제공해주는 함수들을 살펴보고 필요한 것을 가져다가 쓰면 쉽게 코드를 짤 수 있다.

 

이 외에도 필요한 기능은 구글링 해보면 거의 다 만들어져 있는 기본 제공 함수를 발견할 수 있을 것이다!

 

ofGraphics | openFrameworks

 

openframeworks.cc

 

ofPolyline | openFrameworks

 

openframeworks.cc

ofSetFrameRate 는 FrameRate를 설정해주는 함수로, 화면의 프레임, 즉 화면이 얼마나 부드럽게, 잘 흘러가는지를 setting 할 수 있다. 이것 저것 숫자 넣어보면서 가장 적합한 숫자를 찾아주면 된다. 숫자가 너무 작으면 화면이 뚝뚝 끊기고, 너무 크면 프로그램 실행 도중 과부하가 걸려 죽어버릴 수 있다.

ofBackground 는 배경 색을 지정해주는 함수이다. 나는 게임 내내 배경색은 일정하게 유지했기 때문에, setup 함수에서 처음에 한번 흰색으로 지정해 주었다.

 

그 다음 줄지어 놓이는 파이프와, 줄지어 흐르는 물줄기에 대한 정보는 연결리스트를 이용하여 구현할 것이기 때문에, 파이프 연결리스트의 헤더노드와 물줄기 연결리스트의 헤더노드를 ofApp.h에서 선언한 구조체를 이용해 구조체 포인터로 만들어 주었다(코드 주석 참고).

 

헤더노드를 각각 선언해 준 다음 물줄기는 언제나 파이프를 따라서 흐르기 때문에, 초기에 기본적으로 놓여있는 파이프를 따라 먼저 초기 물줄기를 셋팅해 준다. 초기에 기본적으로 놓여있는 파이프란 아래의 것을 의미한다.

빨간색 박스로 표시된 것이 초기 파이프. 저 위치에서부터 물이 흐르기 시작한다.

2) void ofApp::draw( )

본격적으로 draw 함수를 살펴보기 전에, 사용된 OF 기본 제공 함수들을 먼저 살펴보자

 

ofSetColor : 앞으로 그릴 도형이든, 선이든 뭐가 되었든의 색깔을 지정해주는 함수이다.

이 함수를 여러번 호출하는 경우, 그려지는 도형은 가장 최근에 호출된 함수의 색을 따른다.

ofDrawLine(x,y,x,y) : 선을 그리는 함수이다. 좌표점 두개(x,y),(x,y)를 입력받으며 두 점을 연결한 선을 그린다.

ofDrawCircle(x, y, r) : 원을 그리는 함수이다. 원의 중심 좌표 (x,y)와 반지름 r 을 입력하면 해당 위치에 입력한 크기의 원을 그려준다.

ofDrawRectangle(x,y,width,height) : 사각형을 그리는 함수이다. 사각형의 좌측상단 꼭지점의 좌표를 입력하고, width와 height를 입력하면 사각형을 좌측상단에서부터 그려주는 함수이다.

ofDrawRectangle(x,y,width,height)
myfont.load("Arial.ttf", 20);
myfont.drawString("current score : ", 1290, 30);
#include "ofTrueTypeFont.h"
...
ofTrueTypeFont myfont;

myfont.load / myfont.drawString : Openframeworks에서 화면에 글자를 입력하는데 사용되는 함수이다.

이거 찾느라 애먹었던 기억이... ofApp.h 파일에 보면 ofTrueTypeFont.h 라는 OF 제공 라이브러리와 myfont라는 변수를 선언한 부분이 있을 것이다. 해당 부분을 이용해서 화면에 글자를 입력할 수 있다.

myfont.load 함수는 글꼴을 호출하고, myfont.drawString 함수는 문자열과 좌표점 (x,y)를 입력받아 주어진 좌표점에 문자열을 표시하는 역할을 한다.

 

주의사항 : 글꼴을 사용하려면 본인이 만든 OF 프로젝트 폴더에서 bin>data 폴더 안에 반드시 글꼴 파일(예:Arial.ttf 등)을 추가해 주어야 한다!

screen.grabScreen(0, 0, 1640, 840);
pix = screen.getPixels();
c1 = pix.getColor(41, 41);
c2 = pix.getColor(0, 41);
ofImage screen;
ofPixels pix;
ofColor c1;
ofColor c2;
ofColor c3;

ofApp.h 파일을 보면 OF 제공 자료형의 screenpixc1c2c3 변수를 선언한 것을 확인할 수 있다.

 

이 자료형은 현재 화면에서 특정 좌표점의 색을 확인하는데 사용된다.

이 기능은 파이프를 화면상에 배치할 때, 파이프가 이미 놓인 자리에 또 파이프를 놓는 경우 게임오버를 발생시켜야 하므로 특정 위치에 파이프가 놓여 있는지를 확인하기 위해 사용된다(흰색이면 빈 자리, 회색이면 이미 파이프 있음).

 

물론 연결리스트를 뒤져서 파이프가 놓인적이 있는 자리인지를 확인할수도 있겠지만, 연결리스트는 random access가 불가능 하므로 search 시간이 너무 오래 걸려 프로그램이 느려질 수 있기 때문에 쉬운 방법을 선택했다.

 

screen.grabScreen(x,y,x,y) : 프로그램 실행 창에서 범위를 선택해 screen 변수에 할당하는 역할의 함수이다. 예를들어 프로그램 실행 창의 크기가 100*50 이라면 screen.grabScreen(0,0,50,40)를 입력하는 경우 아래와 같은 부분을 선택하게 되는 것이다.

screen.grabScreen(0,0,50,40)

pix = screen.getPixels( ) : screen 변수에 저장된 선택 범위를 픽셀화 하여 pix 변수에 저장한다는 의미이다.

c1 = pix.getColor(x, y) : 픽셀화 하여 저장한 pix 변수에서 (x,y) 좌표점의 색상을 c1 변수에 저장한다는 뜻이다.

색상은 RGB 형식으로 저장된다.

 

이런 식으로 자동화가 매우 잘 되어있다! 라이브러리 만세!

 

내가 사용한 OF 제공 함수는 이 정도이다.

그 외 C++ 언어에 대한 이해도만 있다면, 나머지 함수에 대한 코드를 이해하는 것은 어렵지 않을 것이다.

//----------- Draw game elements on the screen -----------
void ofApp::draw() {
	// background grid
	ofSetColor(230, 230, 230);
	ofDrawLine(0, 100, 0, 140);
	for (int i = 0; i <= 840; i += 40) { ofDrawLine(0, i, 1640, i); }
	for (int i = 0; i <= 1640; i += 40) { ofDrawLine(i, 0, i, 840); }

	// start and finish point
	ofSetColor(0, 0, 255);
	ofDrawRectangle(0, 40, 40, 40);
	ofDrawRectangle(1600, 760, 40, 40);

	// current score
	ofSetColor(0, 0, 255);
	myfont.load("Arial.ttf", 20);
	myfont.drawString("current score : ", 1290, 30);
	std::stringstream ssInt;
	ssInt << score;
	myfont.drawString(ssInt.str(), 1490, 31);

	// initial pipe
	ofSetColor(190, 190, 190);
	ofDrawRectangle(40, 40, 120, 40);
	ofDrawRectangle(40, 30, 15, 60);
	ofDrawRectangle(145, 30, 15, 60);

	// getColor : used for checking overlapped pipe
	screen.grabScreen(0, 0, 1640, 840);
	pix = screen.getPixels();
	c1 = pix.getColor(41, 41);
	c2 = pix.getColor(0, 41);

	// game ready
	if (start_flag == 0) {
		ofSetColor(255, 0, 0);
		myfont.load("Arial.ttf", 80);
		myfont.drawString("Press S to start", 445, 290);
		myfont.load("Arial.ttf", 30);
		myfont.drawString("- left/right key : change pipe", 560, 370);
		myfont.drawString("- up/down key : rotate pipe", 560, 450);
		myfont.drawString("- space key : place pipe", 560, 530);
		myfont.drawString("- Q key : quit game", 560, 610);
	}

	// S pressed : game start
	if (start_flag == 1) {
		// draw placed pipe (grey)
		if (pipe_flag == 1) {
			currentP = startP;
            colorA = 190; colorB = 190; colorC = 190;
		    colorD = 190; colorE = 190;
			while (currentP->next != NULL) {
				drawPipe();
				currentP = currentP->next;
			}
		}

		// draw next pipe (pink)
		if (complete == 0) {
			colorA = 252; colorB = 172; colorC = 175;
			colorD = 0; colorE = 0;
			drawPipe();
		}

		// draw water and add score
		// draw every wather path saved in water linked list
		currentW = startW;
		ofSetColor(0, 0, 255);
		while (1) {
			ofDrawCircle(currentW->x, currentW->y, 11);
			if (currentW == tempW) break;
			currentW = currentW->next;
		}

		int check = 0;
		for (int i = 0; i < 8; i++) {
			if (tempW->next != NULL && game_over != 1) {
				// check rather the water path is cut off
				if (abs((tempW->next->x) - (tempW->x)) > 2 || abs((tempW->next->y) - (tempW->y)) > 2) {
					game_over = 1;
					check = 1;
				}
				if (check == 0) {
					tempW = tempW->next;
					score++;
				}
			}
		}

		// water arrived destination : game ends
		if (tempW->next == NULL) {
			if (currentP->x != 1600 || currentP->y != 720) game_over = 1;
			else end_game = 1;
		}
	}

	// game over
	if (game_over == 1) {
		ofSetColor(255, 0, 0);
		myfont.load("Arial.ttf", 80);
		myfont.drawString("Game Over", 530, 340);
		myfont.load("Arial.ttf", 30);
		myfont.drawString("- Press R to restart", 630, 420);
		myfont.drawString("- Press C to close", 630, 500);
	}

	// game succeeded
	if (end_game == 1) {
		ofSetColor(255, 0, 0);
		myfont.load("Arial.ttf", 80);
		myfont.drawString("Your score : ", 390, 370);
		myfont.drawString(ssInt.str(), 985, 380);
		myfont.load("Arial.ttf", 30);
		myfont.drawString("- Press R to restart", 630, 480);
		myfont.drawString("- Press C to close", 630, 560);
	}
}
 

전체적인 구조는 코드에 기입해둔 주석을 보면 이해하기 쉬울 것이다.

일단 앞에서 설명했듯이 openframeworks의 재미있는 특성은, 이 draw 함수가 주기적으로 반복 실행된다는 것이다. 이 특성을 기억하면서 코드를 보면 동작 원리를 이해하기 쉽다.

 

일단 배경 색을 제외하고, 화면에 뭔가가 그려지길 바란다면 모조리 이 함수 안에 들어가야 한다.

다른 함수 또는 직접 define한 함수에서는 그리기 동작을 수행할 수 없다.

 

가장 먼저 배경 그리드(회색 모눈)와 시작/도착점, 현재 score, 초기 파이프, 게임 키에 대한 설명을 화면에 표시한다.

➔ 시작/도착점은 파란색 네모, 초기 파이프가 회색 I--I 자 모양의 파이프이다.

이 상태에서 키보드의 's' 키를 누르면 게임이 실행된다. 게임 시작 여부는 start_flag 변수를 이용하여 제어할 수 있다.

앞서 말했듯이, 프로그램이 실행되는 동안 draw 함수는 일정한 주기마다 반복적으로 호출된다.

이 때 start_flag가 0 이라면 게임은 위와 같은 시작화면을 표시하고, start_flag가 1 이라면 게임 진행 상황을 표시하게 되는 것이다.

 

게임이 한번 시작되면, 게임이 무사히 끝나거나 게임오버가 되기 전까지 아래의 사항들이 반복적으로 실행된다.

draw 함수가 반복적으로 호출되기 때문에 전체를 묶는 while문이 없어도 된다.

➔ 파이프 연결리스트를 읽어 배치된 파이프를 화면에 그린다.

➔ 사용자가 현재 배치할 수 있는 파이프를 핑크색으로 표시한다.

➔ 물줄기 연결리스트를 읽어 흘러내린 물줄기를 화면에 그린다.

➔ 물줄기를 그리는 도중 경로가 끊기면 사용자가 파이프를 배치하기 전에 물줄기가 먼저 앞서나갔거나, 파이프를 잘못(끊어지게) 배치했다는 의미이므로 게임오버를 발생시킨다.

➔ 물줄기가 흘러갈 때 마다 score 값을 증가시켜 화면에 표시한다.

➔ 물줄기가 도착지점에 도착하면 게임을 끝낸다.

+) 파이프를 겹치게 배치하여 게임오버가 나는 경우는 뒤에서 살펴본다.

 

게임오버 또는 게임이 success 되면 더 이상의 화면 전환 없이, 게임오버 또는 게임이 완료되었다는 화면을 표시한다. 게임이 success 된 경우 게임 오버 문구 대신 최종 score 값이 화면에 표시된다.

게임오버는 game_over 변수로, 게임 success는 end_game 변수로 제어할 수 있다.

게임 오버 화면 & 게임 sucess 화면

3) void ofApp::keyPressed(int key)

//----------- Execute an order allocated on each alphabet keys -----------
void ofApp::keyPressed(int key){
	// start the game
	if (key == 's' || key == 'S') start_flag = 1;

	// quit the game
	if (key == 'q' || key == 'Q') {
		if(game_over==0 && end_game==0) game_over = 1;
	}

	// restart the game
	if (key == 'r' || key == 'R') {
		// free allocated memories
		if (game_over == 1 || end_game == 1) {
			struct pipe *p;
			while (startP->next != NULL) {
				p = startP;
				startP = startP->next;
				free(p);
			} free(startP);

			struct water *w;
			while (startW->next != NULL) {
				w = startW;
				startW = startW->next;
				free(w);
			} free(startW);

			// get ready to start the game again
			setup();
			game_over = 0;
			end_game = 0;
			start_flag = 0;
		}
	}

	// exit the game
	if (key == 'c' || key == 'C') {
		// free allocated memories
		if (game_over == 1 || end_game == 1) {
			struct pipe *p;
			while (startP->next != NULL) {
				p = startP;
				startP = startP->next;
				free(p);
			} free(startP);

			struct water *w;
			while (startW->next != NULL) {
				w = startW;
				startW = startW->next;
				free(w);
			} free(startW);
			// close the game screen
			_Exit(0);
		}
	}
}
 

keyPressed 함수는 스페이스 키와 방향키를 제외한 키보드의 키가 눌렸을 때 이에 대응되는 작업을 관리하는 함수이다. 키가 눌리는 즉시 작업은 수행되어 기존의 데이터를 변화시키고, draw 함수는 주기적으로 반복되기 때문에 변화된 내용을 화면에서 바로 확인할 수 있다.

 

내가 활성화시킨 키는 네가지로 (S/Q/R/C) 이다. (대소문자 무관)

 

S : 게임을 시작하는 키. start_flag 값을 변경시킨다.

Q : 게임을 중간에 중단시키는 키. Q 가 눌리는 순간 game_over 플래그 값을 1로 setting 시킨다.

R : 게임을 Restart 하는 키. 게임이 끝난 경우에만 먹히는 키 이며 게임을 다시 시작할 수 있다. end_game 혹은 game_over 값이 1로 setting 되어 있는 경우 이를 0 으로 바꾸고, 파이프/물줄기 포인터 변수들을 free 시켜 메모리를 확보한 다음 setup 함수를 호출하여 게임 화면을 초기화 한다.

C : 게임을 종료하는 키. 게임이 끝난 경우에만 먹히는 키 이며 게임 화면을 끌 수 있다. 마찬가지로 파이프/물줄기 포인터 변수들을 free 한 다음 Exit 함수를 호출하여 게임 화면을 close 한다.

 

4) void ofApp::keyReleased(int key)

//----------- Execute an order allocated on each arrow keys and spacebar -----------
void ofApp::keyReleased(int key){
	// only operate when game is on the way
	if (start_flag == 1 && game_over != 1) {
		// change pipe type
		if (key == OF_KEY_RIGHT) {
			currentP->pipe_type++;
			if (currentP->pipe_type > 3) currentP->pipe_type = 1;
		}
		if (key == OF_KEY_LEFT) {
			currentP->pipe_type--;
			if (currentP->pipe_type < 1) currentP->pipe_type = 3;
		}

		// change pipe rotation
		if (key == OF_KEY_UP) {
			currentP->rotation++;
			if (currentP->rotation > 4) currentP->rotation = 1;
		}
		if (key == OF_KEY_DOWN) {
			currentP->rotation--;
			if (currentP->rotation < 1) currentP->rotation = 4;
		}

		// place pipe on current location
		if (key == OF_KEY_SPACE) {
			if (complete == 0) {
				addPath();

				// make new pipe node and link to the pipe linkedlist
				struct pipe *p;
				p = (struct pipe*)malloc(sizeof(struct pipe));
				p->pipe_type = currentP->pipe_type;
				p->rotation = currentP->rotation;
				p->x = pxNew; p->y = pyNew;
				p->next = NULL;
				currentP->next = p;
				currentP = p;
				pipe_flag = 1;
				if (pxNew == 1600 && pyNew == 720) complete = 1;

				// check rather the pipe is overlapped
				// check by confirming the color of specific location of the screen
				screen.grabScreen(0, 0, 1640, 840);
				pix = screen.getPixels();
				c3 = pix.getColor(pxNew + 60, pyNew + 60);
				if (c1 == c3) game_over = 1;
				if (c2 == c3) game_over = 1;
				if (pxNew > 1640 || pxNew < 0) game_over = 1;
				if (pyNew > 840 || pyNew < 0) game_over = 1;
			}
		}
	}
}
 

keyReleased 함수는 스페이스키 또는 방향키가 눌렸을 때 이에 대응되는 작업을 관리하는 함수이다.

마찬가지로 키가 눌리는 즉시 작업이 수행되며, draw 함수가 주기적으로 반복됨에 따라 화면에서 수행된 작업을 바로바로 확인할 수 있다.

 

좌/우 방향키 : 현재 배치하려고 하는 파이프를 회전시킨다.

위/아래 방향키 : 현재 배치하려고 하는 파이프의 종류를 바꾼다.

스페이스 키 : 파이프를 배치한다. 배치된 파이프는 결정한 자리에 고정되어 핑크색에서 회색으로 바뀐다.

+) 파이프를 배치하고 나면, 배치된 파이프 끝에서 다시 새로운 파이프를 배치할 수 있다.

+) 스페이스 키로 파이프를 배치할 때, 파이프가 이미 놓인 자리에 또 파이프를 배치하는 경우 게임오버가 된다.

파이프가 이미 놓인 자리에 또 파이프를 배치하여 게임오버가 된 경우
방향키는 단순히 현재 파이프의 모양을 변화시키는 역할만 한다.

스페이스 키로 파이프를 배치하는 경우 다양한 작업이 수행된다. 일단 파이프가 이미 배치된 자리에 또 파이프를 배치한것이 아닌지를 검사해 주어야 한다. 이는 앞에서 설명한 grabScreen 함수를 이용하면 된다. 만약 파이프가 있는 자리에 또 파이프를 배치한 경우 즉시 게임오버 처리를 해 준다.

게임오버가 아닌 경우에는 파이프를 배치했기 때문에 물줄기가 나아갈 수 있는 path가 추가된 것이다. 이를 물줄기 연결리스트에 추가해 주어야 한다.

 

그리고 새롭게 배치된 파이프가 화면에 표시될 수 있도록 파이프 연결리스트에도 파이프의 위치와 모양 정보를 추가한다. 마지막으로 다시 새로운 자리에 파이프 배치를 시작할 수 있도록 '현위치' 정보를 업데이트 시킨다.


다음 포스팅에서 이어짐!

 

||  null 대신 빈 배열이나 빈 컬렉션을 반환하라.
||  null 을 반환한다고 해서 성능이 좋아지는 것도 아니고,
||  오히려 작성해야 하는 오류 처리 코드만 늘어나기 때문이다.

메서드에서 null 이 반환되면 생기는 일

private final List<Cheese> cheesesInStock = ... ;
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
    System.out.println("good");

• getCheeses 메소드를 호출하는 쪽에서는 NullPointerException 을 피하기 위해 반환된 값이 null 인지의 여부를 매번

    체크해줘야 한다.

    ◦  이렇게 오류가 발생하는 것을 방지하기 위해 추가적으로 작성해주는 코드를 방어코드 라고 부른다.

    ◦  위 상황을 보면 불필요한 작업이 2개나 이루어지고 있다.

          -  getCheeses 메소드 : 반환하려는 대상의 Empty 여부를 체크하고 True 인 경우 null 반환

          -  getCheeses 메소드를 호출하는 쪽 : 메소드에서 반환된 값이 null 인지의 여부를 체크해 조건 처리

 

null 대신 빈 배열/컬렉션을 반환하는 경우

public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

• cheesesInStock 에 값이 들어 있으면 그 값으로 ArrayList 가 구성되어 반환되고, 비어있으면 Empty ArrayList 가 반환된다.

    ◦ cheesesInStock 이 Empty 인지 확인하는 절차를 생략할 수 있다.

    ◦  당연히 getCheeses 메소드를 호출하는 쪽에서도 null 이 반환되지 않으니 null 을 처리하는 과정을 생략할 수 있다.

 

null 대신 빈 배열/컬렉션을 반환하는 것이 더 좋은 이유

• 빈 배열/컨테이너를 굳이 만들어서 반환하는데에도 비용이 발생하기 때문에 null 을 반환하는 것이 낫다는 주장도 있다.

• 하지만 아래 두가지 이유 때문에 이는 틀린 주장이라고 할 수 있다.

1) null 대신 빈 배열/컬렉션을 반환하는 경우의 성능 차이는 아주 미미하다.

• 분석 결과, 이 정도의 할당으로 인해 발생하는 성능 차이는 대부분 신경 쓸 수준이 되지 못한다.

2) 빈 배열/컬렉션은 굳이 새로 할당하지 않고도 반환할 수 있다.

• 만에 하나 빈 배열/컬렉션을 반환하는 행위가 성능 차이의 주범이 된다고 해도, 매번 새로운 배열/컬렉션을 할당하여 반환하는 대신

    빈 불변 배열/컬렉션을 하나 만들어두고, 매번 이 똑같은 객체를 반환 시킴으로써 간단하게 해결할 수 있다.

public List<Cheese> getCheeses() {
    return cheeseInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheeseInStock);
}

||  Collections.emptyList : 빈 불변 리스트를 반환하는 메소드
||  Collections.emptySet : 빈 불변 집합(Set)을 반환하는 메소드
||  Collections.emptyMap : 빈 불변 맵(Map)을 반환하는 메소드

 

• 단 이 역시 최적화에 해당하니 꼭 필요할 때에만 사용하고,

  사용한 경우에는 수정 전과 후의 성능을 측정하여 실제로 성능이 개선 되었는지 여부를 꼭 확인하자.


public Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]);
}
// 또는 이렇게 작성할수도 있다.
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

• 컬렉션 말고 배열을 사용하는 경우에는 위 코드처럼 구현할 수 있다.

    ◦ toArray 메소드에 파라미터로 넣어준 Cheese[0] 배열은 반환 타입을 지정해주는 역할

    ◦ 이 방식이 성능을 떨어뜨릴 것 같다면 이 역시 길이가 0인 배열(불변)을 미리 선언해두고 매번 그 배열을 사용하면 된다.

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

• 단, toArray 메소드에 넘기는 배열을 미리 할당해 두는 것이 오히려 성능 저하의 원인이 된다는 연구 결과도 있으므로,

    정말 필요한 상황이 아닌 단순히 성능을 개선할 목적이라면 이 방식은 지양하도록 하자.

메서드와 매개변수

• 메서드의 입력 매개변수 값에는 제약 조건이 있을 수 있다.

    ◦  ex) 인덱스 값은 음수이면 안된다.

    ◦  ex) 객체 참조는 null 이 아니어야 한다.

• 매개변수가 이러한 제약 조건을 위배하지 않는지는 메서드 body가 시작되기 전에 검사해주는 것이 좋다.

    ◦  오류는 가능한 한 빨리 잡아야 하기 때문

    ◦  오류를 발생한 즉시 잡지 못하면(문제가 생긴채로 어딘가에 저장되어 버리면) 해당 오류를 감지하기 어려워지고,

         감지하더라도 오류의 발생 지점을 찾기 어려워진다.


매개변수 검사를 하지 않으면 생길 수 있는 문제

• 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.

• 메서드는 잘 수행되지만 잘못된 결과를 반환할 수 있다.

• 메서드는 잘 수행되지만 어떠한 객체의 상태를 변화시켜서 미래의 알 수 없는 시점에 이 메서드와 관련 없는 오류가 발생할 수 있다.


매개변수 관련 예외를 문서화 하는 방법

• 메서드의 매개변수 값이 잘못됐을 때 발생되는 예외를 문서화하면 개발자가 매개변수 관련 오류를 발생시킬 위험을 줄일 수 있다.

    ◦  @throws 자바독 태그를 사용할 수 있다.

    ◦  매개변수의 제약을 문서화 할 때는 제약을 어겼을 시 발생하는 예외도 함께 기술해주는 것이 좋다.

    ◦  일반적으로 발생하기 쉬운 예외

          -  IllegalArgumentException, IndexOutOfBoundsException, NullPointerException

/**
* (현재 값 mod m) 값을 반환한다. 
* 이 메서드는 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다.
*
* @param m (계수 : 양수여야 한다)
* @return mod m (현재 값)
* @throws ArithmeticException (m 이 0 이하이면 발생한다)
*/

public BigInteger mod(BigInteger m) {
    if (m.signum() < 0) // m이 양수면 1, 0이면 0, 음수면 -1 반환
        throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
    ...
}

• m == null 이면 m.signum 호출시 NullPointerException 예외가 발생한다.

    ◦  위 사항이 메서드 설명에서 언급되지 않는 이유

          -  이 설명은 메서드가 아닌, BigInteger 클래스 수준에 기술되어 있기 때문이다.

          -  모든 메서드에 일일이 주석을 작성해두는 것 보다, 클래스 단계에 주석을 한 번만 작성해두는 것이 훨씬 간편한 경우가 많다.


매개변수 검사에 사용할 수 있는 유용한 메소드

java.util.Objects.requireNonNull

this.value = Objects.requireNonNull(value, "예외메시지");

• java.util.Objects.requireNonNull 메서드를 활용하면 편리하게 null 검사를 수행할 수 있다.

    ◦  자바 7 에서 추가된 메서드이다.

    ◦  원하는 예외 메시지를 지정할 수도 있고, 입력을 그대로 반환하므로 값을 사용하는 동시에(언제든지) null 검사를 수행할 수 있다.

          -  반환되는 값을 사용하지 않고 오로지 null 검사만을 위해 사용해도 무방하다.

 

checkFromIndexSize , checkFromToIndex , checkIndex

public static int checkFromIndexSize(int fromIndex, int size, int length) {
  return Preconditions.checkFromIndexSize(fromIndex, size, length, null);
}

public static int checkFromToIndex(int fromIndex, int toIndex, int length) {
  return Preconditions.checkFromToIndex(fromIndex, toIndex, length, null);
}

public static int checkIndex(int index, int length) {
  return Preconditions.checkIndex(index, length, null);
}

• 자바 9 에서 추가된 메서드

• 리스트와 배열 전용으로 설계된 메소드이다.

• requireNonNull 과 달리 예외메시지를 지정할 수는 없다.

• 이상/이하(닫힌범위)는 다루지 못한다.

 

단언문(assert)

• public 이 아닌 메서드라면 단언문을 사용해 매개변수의 유효성을 검증할 수 있다.

    ◦  public 이 아닌 메서드의 경우, 직접 메서드가 호출되는 상황을 통제할 수 있다고 보는 것

    ◦  오직 유효한 값 만이 메서드에 넘겨지리라는 것을 보증할 수 있다.

private static void sort(long a[], int offset, int length) {
        assert a != null;
        assert offset >= 0 && offset <= a.length;
        assert length >= 0 && length <= a.length - offset;
        //계산 수행 ...
}

• 단언문은 자신이 단언한 조건이 무조건 참이라고 선언한다.

• 단언문과 일반적인 유효성 검사의 차이

    ◦  단언문은 실패하면 AssertionError 를 던진다.

    ◦  Runtime 효과/성능 저하를 전혀 발생시키지 않는다.


나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라

• 메서드가 직접 사용하지는 않지만, 이후 다른 연산에서 사용하기 위해 저장되는 매개변수는 특히 더 신경써서 검사하는것이 좋다.

    ◦  생성자가 이 원칙의 한 예시이다.

    ◦  생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체의 생성을 방지하기 위해 반드시 필요


메서드 매개변수 유효성 검사 규칙의 예외

• 다음과 같은 경우에는 메서드 body 실행 전 매개변수의 유효성을 검사하지 않아도 된다.

    ◦  유효성 검사 비용이 지나치게 높거나 실용적이지 않은 경우

    ◦  계산 과정에서 암묵적으로 검사가 수행되는 경우

          -  ex) Collections.sort(List) : 객체 리스트를 정렬하는 메서드

          -  리스트 안의 객체들은 모두 상호 비교될수 있어야 하며, 정렬 과정에서 이 비교가 이루어진다.

          -  만약 상호 비교할 수 없는 타입의 객체가 들어 있다면 그 객체와 비교할 때 ClassCastException 이 발생한다.

    ◦  때로는 계산 과정에서 필요한 유효성 검사가 이루어지지만, 계산에 실패했을 때 의도한것과 다른 예외가 발생할 수도 있다.

          -  잘못된 매개변수 값을 사용해서 발생된 예외와, 문서에서 던지기로 작성된 예외가 다를 수 있다.

          -  이 경우 예외번역(exception translate) 관용구를 사용해 문서에 작성된 예외로 번역해줘야 함

try {
    ... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) { // 추상화 수준에 맞게 번역한다.
    throw new HigherLevelException(...);
}

아이템을 마치며

• 이번 아이템을 '매개변수에 제약을 두는 것이 좋다' 라고 해석해서는 안 된다.

• 메서드는 최대한 범용적으로 설계되어야 하며, 매개변수 제약은 적을수록 좋다.