지난번 연재 6-1, 6-2, 6-3, 6-4와 이어집니다.
Save 기능을 구현하기 위해 데이터베이스에 테이블을 추가하고 테이블에 값을 추가하고 삭제하는 메서드를 만들어보도록 하겠습니다.
우선 데이터베이스에 테이블을 추가하도록 합니다.
----------DatabaseManager.cpp----------
…생략…
//테이블 생성
void DatabaseManager::createDB(){
…생략…
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK){
log("createDB() SUCCESS");
}
else
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
}
{
//캐릭터 저장 테이블
string query = "create table if not exists TB_USER_CHARACTER(";
query += "NO integer primary key autoincrement, ";
//얼굴형 번호
query += "HEAD_NO interger not null, ";
//얼굴 색상번호
query += "HEAD_COLOR_NO interger not null, ";
query += "HAIR1_NO interger not null, ";
query += "HAIR1_COLOR_NO interger not null, ";
query += "HAIR2_NO interger not null, ";
query += "HAIR2_COLOR_NO interger not null, ";
query += "EYE_NO interger not null, ";
query += "EYE_COLOR_NO interger not null, ";
query += "MOUTH_NO interger not null, ";
query += "MOUTH_COLOR_NO interger not null, ";
query += "ETC_NO interger not null, ";
query += "ETC_COLOR_NO interger not null, ";
query += "BG_NO interger not null, ";
query += "BG_COLOR_NO interger not null";
query += ")";
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK)
{
log("createDB() SUCCESS");
}
else
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
}
}
TB_USER_CHARACTER 라는 테이블을 생상하는 코드를 createDB()에 추가하였습니다.
각각 파트에 대한 번호와 색상 번호값을 갖는 테이블입니다.
그럼 테이블이 제대로 생성되는지 확인해보도록 하겠습니다.
----------DevConf.h----------
#define DB_INIT true
DevConf.h 의 DB_INIT를 true로 수정합니다. 이전에 DB_INIT이 true이면 데이터베이스 파일을 삭제하고 다시 생성하도록 하였습니다. DB_INIT이 false일 경우 테이블을 추가해도 createDB()메서드는 한번만 실행되도록 구현하였기 때문에 해당 코드가 실행되지 않습니다. 따라서 DB_INIT을 true로 수정한 뒤 디버거를 실행하여 강제로 createDB()를 실행하도록 합니다.
디버거 CharacterScene을 호출하고 로그를 살펴봅니다.
Figure 6‑55 Create 로그 확인
createDB() SUCCESS 8개 모두 출력된 것으로 보아 테이블이 제대로 생성된 것을 알 수 있습니다.
다음으로 insert와 delete의 기능을 수행할 메서드를 DatabaseManager.h파일에 선언하도록 합니다.
----------DatabaseManager.h----------
#include "sqlite3.h"
#include "cocos2d.h"
//USING_NS_CC;와 같습니다.
using namespace cocos2d;
//std의 네임스페이스를 사용하겠다고 정의합니다.
using namespace std;
struct head
{
public:
int no;
char *image;
//Point에는 x와 y값이 들어갑니다.
Point position;
//Color3B에는 r, g, b값이 들어갑니다.
Color3B color1;
Color3B color2;
Color3B color3;
Color3B color4;
//색상 변경 관련 체크 변수
bool isColor;
};
struct character
{
public:
int headNo;
int headColorNo;
int hair1No;
int hair1ColorNo;
int hair2No;
int hair2ColorNo;
int eyeNo;
int eyeColorNo;
int mouthNo;
int mouthColorNo;
int etcNo;
int etcColorNo;
int bgNo;
int bgColorNo;
};
class DatabaseManager
{
private:
bool openDB();
void closeDB();
sqlite3 *_sqlite;
public:
DatabaseManager();
~DatabaseManager();
//에러메시지를 담을 변수
char *_errorMSG;
//결과의 상태를 담을 변수
int _result;
static DatabaseManager *getInstance();
void createDB();
void insertDB();
void selectDB();
list<head*> selectDB(string table, int no);
void insertCharacterDB(character *characterInfo);
void deleteCharacterDB(int no);
};
character 라는 구조체를 정의해주었습니다.
insertCharacterDB()에 character구조체를 매개변수로 받습니다.
deleteCharacterDB()는 No를 입력받아 해당 No가 있는 row를 삭제하도록 구현할 것 입니다.
메서드를 선언했으므로 해당 메서드를 정의해보도록 합니다.
----------DatabaseManager.cpp----------
…생략…
void DatabaseManager::insertCharacterDB(character *characterInfo){
{
//캐릭터 하나 추가
string query = "insert into TB_USER_CHARACTER(HEAD_NO, HEAD_COLOR_NO, HAIR1_NO, HAIR1_COLOR_NO, HAIR2_NO, HAIR2_COLOR_NO, EYE_NO, EYE_COLOR_NO, MOUTH_NO, MOUTH_COLOR_NO, ETC_NO, ETC_COLOR_NO, BG_NO, BG_COLOR_NO) ";
char temp[255];
sprintf(temp, "values (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", characterInfo->headNo, characterInfo->headColorNo, characterInfo->hair1No, characterInfo->hair1ColorNo, characterInfo->hair2No, characterInfo->hair2ColorNo, characterInfo->eyeNo, characterInfo->eyeColorNo, characterInfo->mouthNo, characterInfo->mouthColorNo, characterInfo->etcNo, characterInfo->etcColorNo, characterInfo->bgNo, characterInfo->bgColorNo);
query += temp;
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK)
{
log("insertDB() SUCCESS");
}
else
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
}
}
void DatabaseManager::deleteCharacterDB(int no){
{
//해당 번호의 row삭제
//맨뒤에 공백있습니다. where절과 공백으로 구분
string query = "delete from TB_USER_CHARACTER ";
char temp[20];
sprintf(temp, "where NO = %d", no);
query += temp;
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK)
{
log("deleteDB() SUCCESS");
}
else
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
}
}
DatabaseManager.cpp 파일의 하단에 insertCharacterDB()와 deleteCharacterDB()를 정의 하였습니다.
특별한 내용은 없고 쿼리문만 신경써서 작성하면 별 어려움 없이 메서드를 추가할 수 있습니다.
insertCharacterDB()에서 컬럼을 보면 NO 라는 컬럼을 테이블에서 생성하였는데 insert구문에서 추가하지 않은 이유는 NO라는 컬럼이 autoincrement 속성으로 되어있어 자동으로 추가되어 생략하였습니다.
데이터베이스에 추가하고 삭제하는 메서드를 작성했으니 CharacterScene의 SAVE 버튼에 기능을 구현하겠습니다.
먼저 해당 값들을 저장할 변수를 선언하도록 합니다.
----------CharacterScene.h----------
#include "cocos2d.h"
#include "cocos-ext.h"
#include "DatabaseManager.h"
USING_NS_CC;
USING_NS_CC_EXT;
class CharacterScene : public cocos2d::Layer
{
public:
//생성자
CharacterScene();
//소멸자
~CharacterScene();
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();
// implement the "static create()" method manually
CREATE_FUNC(CharacterScene);
Sprite *_characterBg;
void onClickMenu(Ref *object);
void onClickHome(Ref *object);
void onClickRandom(Ref *object);
void onClickSave(Ref *object);
void onClickGallery(Ref *object);
void setImage(std::string tableName, int rowNo, int colorNo);
Sprite *_face;
Sprite *_hair1;
Sprite *_hair2;
Sprite *_eye;
Sprite *_mouth;
Sprite *_etc;
Sprite *_bgStyle;
//포지션에 따라 말풍선의 화살표의 위치를 변경한다.
void setBalloon(int position);
Sprite *_arrow;
Sprite *_balloon;
ScrollView *_scrollView;
void setSubMenuItem(int position);
std::string _currentTableName;
void showColorPopup(Ref *object);
character *_characterInfo;
};
DatabaseManager.h를 include하였고 character 구조체 변수를 선언하였습니다.
여기서 문제가 발생합니다.
CharacterScene.h와 CharacterScene.cpp 두곳에서 DatabaseManager.h가 include되어있기 때문입니다.
CharacterScene.cpp는 CharacterScene.h와 DatabaseManager.h이 include되어있습니다. CharacterScene.h에도 DatabaseManager.h가 include 되어있습니다. 이 DatabaseManager.h파일이 중복되어 include되어 있어 문제가 발생합니다. 중복되어 include되는것을 방지하는 방법을 알아보도록 하겠습니다.
----------DatabaseManager.h----------
#ifndef __DATABASEMANAGER_H__
#define __DATABASEMANAGER_H__
#include "sqlite3.h"
#include "cocos2d.h"
//USING_NS_CC;와 같습니다.
using namespace cocos2d;
//std의 네임스페이스를 사용하겠다고 정의합니다.
using namespace std;
struct head
{
public:
int no;
char *image;
//Point에는 x와 y값이 들어갑니다.
Point position;
//Color3B에는 r, g, b값이 들어갑니다.
Color3B color1;
Color3B color2;
Color3B color3;
Color3B color4;
//색상 변경 관련 체크 변수
bool isColor;
};
struct character
{
public:
int headNo;
int headColorNo;
int hair1No;
int hair1ColorNo;
int hair2No;
int hair2ColorNo;
int eyeNo;
int eyeColorNo;
int mouthNo;
int mouthColorNo;
int etcNo;
int etcColorNo;
int bgNo;
int bgColorNo;
};
class DatabaseManager
{
private:
bool openDB();
void closeDB();
sqlite3 *_sqlite;
public:
DatabaseManager();
~DatabaseManager();
//에러메시지를 담을 변수
char *_errorMSG;
//결과의 상태를 담을 변수
int _result;
static DatabaseManager *getInstance();
void createDB();
void insertDB();
void selectDB();
list<head*> selectDB(string table, int no);
void insertCharacterDB(character *characterInfo);
void deleteCharacterDB(int no);
};
#endif // __DATABASEMANAGER_H__
#ifndef, #define, #endif를 이용하여 중복으로 include되는것을 방지하였습니다.
위처럼 설정해두면 중복으로 추가하려 해도 이미 추가되어있어 추가가되지 않습니다.
----------CharacterScene.cpp----------
…생략…
CharacterScene::CharacterScene(){
//객체를 초기화
_face = NULL;
_hair1 = NULL;
_hair2 = NULL;
_eye = NULL;
_mouth = NULL;
_etc = NULL;
_bgStyle = NULL;
_balloon = NULL;
_arrow = NULL;
_scrollView = NULL;
//초기화
_characterInfo = new character;
}
CharacterScene::~CharacterScene(){
//생성된 객체를 해제한다.
delete(_characterInfo);
}
…생략…
void CharacterScene::setImage(std::string tableName, int rowNo, int colorNo){
//매개변수에 맞춰 headList를 가져온다.
auto headList = DatabaseManager::getInstance()->selectDB(tableName, rowNo);
//첫번째 구조체 가져오기, setImage()를 이용하면 하나의 row만 반환됩니다.
auto head = headList.front();
//zOrder 값
int zOrder = 0;
if (tableName == "TB_FACE"){
//_face가 NULL이 아니면
if (_face != NULL){
//부모로부터 자기를 없앰
_face->removeFromParentAndCleanup(true);
}
zOrder = 2;
//데이터베이스에서 이미지의 파일명과 해당 파일의 좌표를 가져와 위치시킨다.
_face = Sprite::create(head->image);
_face->setPosition(head->position);
_characterBg->addChild(_face, zOrder);
_characterInfo->headNo = head->no;
}
else if (tableName == "TB_HAIR1"){
if (_hair1 != NULL){
_hair1->removeFromParentAndCleanup(true);
}
zOrder = 4;
_hair1 = Sprite::create(head->image);
_hair1->setPosition(head->position);
_characterBg->addChild(_hair1, zOrder);
_characterInfo->hair1No = head->no;
}
else if (tableName == "TB_HAIR2"){
if (_hair2 != NULL){
_hair2->removeFromParentAndCleanup(true);
}
zOrder = 1;
_hair2 = Sprite::create(head->image);
_hair2->setPosition(head->position);
_characterBg->addChild(_hair2, zOrder);
_characterInfo->hair2No = head->no;
}
else if (tableName == "TB_EYE"){
if (_eye != NULL){
_eye->removeFromParentAndCleanup(true);
}
zOrder = 3;
_eye = Sprite::create(head->image);
_eye->setPosition(head->position);
_characterBg->addChild(_eye, zOrder);
_characterInfo->eyeNo = head->no;
}
else if (tableName == "TB_MOUTH"){
if (_mouth != NULL){
_mouth->removeFromParentAndCleanup(true);
}
zOrder = 3;
_mouth = Sprite::create(head->image);
_mouth->setPosition(head->position);
_characterBg->addChild(_mouth, zOrder);
_characterInfo->mouthNo = head->no;
}
else if (tableName == "TB_ETC"){
if (_etc != NULL){
_etc->removeFromParentAndCleanup(true);
}
zOrder = 5;
_etc = Sprite::create(head->image);
_etc->setPosition(head->position);
_characterBg->addChild(_etc, zOrder);
_characterInfo->etcNo = head->no;
}
else if (tableName == "TB_BG"){
if (_bgStyle != NULL){
_bgStyle->removeFromParentAndCleanup(true);
}
zOrder = 0;
_bgStyle = Sprite::create(head->image);
_bgStyle->setPosition(head->position);
_characterBg->addChild(_bgStyle, zOrder);
_characterInfo->bgNo = head->no;
}
if (head->isColor){
Color3B color;
if (colorNo < 0){
//srand로 rand의 기준되는 값을 변경하지 않으면 같은 패턴으로 반복된다.
srand(time(NULL));
//1~4사이의 값을 랜덤하게 발생시킴
colorNo = rand() % 4 + 1;
}
switch (colorNo)
{
case 1:
color = head->color1;
break;
case 2:
color = head->color2;
break;
case 3:
color = head->color3;
break;
case 4:
color = head->color4;
break;
}
if (tableName == "TB_FACE"){
_face->setColor(color);
_characterInfo->headColorNo = colorNo;
}
else if (tableName == "TB_HAIR1"){
_hair1->setColor(color);
_characterInfo->hair1ColorNo = colorNo;
}
else if (tableName == "TB_HAIR2"){
_hair2->setColor(color);
_characterInfo->hair2ColorNo = colorNo;
}
else if (tableName == "TB_EYE"){
_eye->setColor(color);
_characterInfo->eyeColorNo = colorNo;
}
else if (tableName == "TB_MOUTH"){
_mouth->setColor(color);
_characterInfo->mouthColorNo = colorNo;
}
else if (tableName == "TB_ETC"){
_etc->setColor(color);
_characterInfo->etcColorNo = colorNo;
}
else if (tableName == "TB_BG"){
_bgStyle->setColor(color);
_characterInfo->bgColorNo = colorNo;
}
}
}
해당 파트에 대한 번호와 색상값에 대한 정보를 setImage()에서 _characterInfo에 추가 해주었습니다.
색상값이 없으면 0번으로 추가됩니다..
다음으로 onClickSave() 메서드에서 데이터베이스를 추가하는 기능을 구현합니다.
----------CharacterScene.cpp----------
…생략…
void CharacterScene::onClickSave(Ref *object){
log("onClickSave");
DatabaseManager::getInstance()->insertCharacterDB(_characterInfo);
}
onClickSave()에서 insertCharacterDB()를 _characterInfo를 매개변수로 추가하여 호출합니다.
디버거를 실행하여 save버튼을 누르고 로그를 살펴보도록 합니다.
Figure 6‑56 insert 로그
onClickSave 다음에 insertDB() SUCCESS가 출력되는 것을 확인 할 수 있습니다.
하지만 유저는 로그를 볼 수 없으므로 유저가 알 수 있도록 팝업을 추가하도록 합니다.
이번에 만들 팝업 클래스는 텍스트와 콜백 메서드를 입력받아 동적으로 사용할 수 있도록 만들어보도록 하겠습니다.
TextPopup이라는 이름의 클래스를 Classes폴더에 생성합니다. [5.3] 참고.
TextPopup.h 파일과 TextPopup.cpp 파일을 생성하였으면 아래와 같이 클래스를 수정합니다.
----------TextPopup.h----------
#include "cocos2d.h"
USING_NS_CC;
class TextPopup :public Layer
{
public:
TextPopup();
~TextPopup();
static TextPopup * create(std::string text, Object *listner, SEL_CallFunc selector, bool isCancelBT);
bool init(std::string text, Object *listner, SEL_CallFunc selector, bool isCancelBT);
//메서드호출시 받을 listner,
Object *_listner;
//OK버튼 선택시 callback으로 호출할 메서드, listner의 selector를 호출한다.
SEL_CallFunc _selector;
void onEnter();
bool onTouchBegan(Touch* touch, Event* event);
void onClickOk(Ref *object);
void onClickCancel(Ref *object);
};
----------TextPopup.cpp----------
#include "TextPopup.h"
TextPopup::TextPopup()
{
}
TextPopup::~TextPopup()
{
}
void TextPopup::onEnter(){
Layer::onEnter();
setTouchEnabled(true);
setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
}
bool TextPopup::onTouchBegan(Touch* touch, Event* event){
return true;
}
TextPopup * TextPopup::create(std::string text, Object *listner, SEL_CallFunc selector, bool isCancelBT){
TextPopup *ret = new TextPopup();
if (ret && ret->init(text, listner, selector, isCancelBT))
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
bool TextPopup::init(std::string text, Object *listner, SEL_CallFunc selector, bool isCancelBT){
_listner = listner;
_selector = selector;
//여기에 팝업을 작성한다.
auto winSize = Director::getInstance()->getOpenGLView()->getDesignResolutionSize();
auto fadeBack = LayerColor::create(Color4B(0, 0, 0, 0), winSize.width, winSize.height);
this->addChild(fadeBack);
fadeBack->runAction(FadeTo::create(0.5f, 200));
auto back = Sprite::create("pop_up_bg.png");
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(back);
//라벨 추가
auto labelSize = Size(back->getContentSize().width, back->getContentSize().height - 50);
//텍스트, 폰트, 폰트크기, 라벨크기, 정렬
auto label = LabelTTF::create(text.c_str(), "Arial", 20, labelSize, TextHAlignment::CENTER);
label->setPosition(Point(back->getContentSize().width / 2, back->getContentSize().height / 2));
label->setColor(Color3B::BLACK);
back->addChild(label);
Vector<MenuItem*> itemArray;
auto okBT = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(TextPopup::onClickOk, this));
itemArray.pushBack(okBT);
if (!isCancelBT){
//cancelBT가 false이면 OK 버튼만 생성한다.
okBT->setPosition(Point(back->getContentSize().width / 2, 25));
}
else{
//CancelBT가 true면 OK버튼과 Cancel 버튼을 둘다 생성한다.
okBT->setPosition(Point(back->getContentSize().width / 2 - 50, 25));
auto cancelBT = MenuItemImage::create("btn_cancel.png", "btn_cancel_on.png", CC_CALLBACK_1(TextPopup::onClickCancel, this));
itemArray.pushBack(cancelBT);
cancelBT->setPosition(Point(back->getContentSize().width / 2 + 50, 25));
}
auto menu = Menu::createWithArray(itemArray);
menu->setPosition(Point::ZERO);
back->addChild(menu);
return true;
}
void TextPopup::onClickOk(Ref *object){
if (_listner != NULL && _selector != NULL)
(_listner->*_selector)(); //받아 넣은 selector를 호출한다.
onClickCancel(NULL);
}
void TextPopup::onClickCancel(Ref *object){
this->removeFromParentAndCleanup(true);
}
텍스트와 콜백 메서드 그리고 취소버튼의 여부를 받아 동적으로 팝업을 생성할 수 있는 코드를 작성하였습니다.
LableTTF를 이용하여 text를 출력하였습니다.
onClickOk()를 보면 (_listner->*_selector)();를 사용하였는데 이것은 listner의 selector를 실행시킨 것입니다.
그럼 SAVE버튼에 팝업 기능을 추가하겠습니다.
----------CharacterScene.cpp----------
#include "CharacterScene.h"
#include "DatabaseManager.h"
#include "DevConf.h"
#include "ColorPopup.h"
#include "TextPopup.h"
USING_NS_CC;
…생략…
void CharacterScene::onClickSave(Ref *object){
log("onClickSave");
DatabaseManager::getInstance()->insertCharacterDB(_characterInfo);
this->addChild(TextPopup::create("저장이 완료되었습니다.", this, NULL, false), 10);
}
onClickSave()가 실행되면 팝업을 추가해주었습니다.
디버거를 실행해보도록 합니다.
Figure 6‑57 실행화면
한글이 모두 깨져서 출력됩니다.
원인은 파일의 인코딩 때문입니다. 여기서 win32와 Android / iPhone을 따로 설정해야 합니다.
먼저 win32에서 인코딩을 진행하겠습니다. 윈도우에서 wild character를 사용하면 쉽게 인코딩을 할 수 있습니다. 하지만 리눅스기반인 Android와 / iPhone에서 사용이 불가능합니다. 따라서 컴파일시 win32일때만 인코딩을 진행하겠습니다.
TextPopup에서 win32일때 인코딩을 하겠습니다.
----------TextPopup.cpp----------
…생략…
bool TextPopup::init(std::string text, Object *listner, SEL_CallFunc selector, bool isCancelBT){
_listner = listner;
_selector = selector;
//여기에 팝업을 작성한다.
auto winSize = Director::getInstance()->getOpenGLView()->getDesignResolutionSize();
auto fadeBack = LayerColor::create(Color4B(0, 0, 0, 0), winSize.width, winSize.height);
this->addChild(fadeBack);
fadeBack->runAction(FadeTo::create(0.5f, 200));
auto back = Sprite::create("pop_up_bg.png");
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(back);
//라벨 추가
auto labelSize = Size(back->getContentSize().width, back->getContentSize().height - 50);
//문자열을 담을 변수
char utf8Text[255];
#if(CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
//win32 utf-8
wchar_t* wChar;
wChar = new WCHAR[255];
//wChar에 text를 담는다.
MultiByteToWideChar(CP_ACP, 0, text.c_str(), strlen(text.c_str()) + 1, wChar, 255);
//wChar를 인코딩하여 utf8Text에 담는다.
WideCharToMultiByte(CP_UTF8, 0, wChar, -1, utf8Text, 1024, NULL, NULL);
#endif
//Android와 iPhone는 파일의 인코딩을 변경하면 한글이 적용됩니다.
#if(CC_TARGET_PLATFORM != CC_PLATFORM_WIN32)
//win32가 아니면 utf8Text에 들어온 text를 넣는다.
sprintf(utf8Text, "%s", text.c_str());
#endif
//텍스트, 폰트, 폰트크기, 라벨크기, 정렬
auto label = LabelTTF::create(utf8Text, "Arial", 20, labelSize, TextHAlignment::CENTER);
label->setPosition(Point(back->getContentSize().width / 2, back->getContentSize().height / 2));
label->setColor(Color3B::BLACK);
back->addChild(label);
Vector<MenuItem*> itemArray;
auto okBT = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(TextPopup::onClickOk, this));
itemArray.pushBack(okBT);
if (!isCancelBT){
//cancelBT가 false이면 OK 버튼만 생성한다.
okBT->setPosition(Point(back->getContentSize().width / 2, 25));
}
else{
//CancelBT가 true면 OK버튼과 Cancel 버튼을 둘다 생성한다.
okBT->setPosition(Point(back->getContentSize().width / 2 - 50, 25));
auto cancelBT = MenuItemImage::create("btn_cancel.png", "btn_cancel_on.png", CC_CALLBACK_1(TextPopup::onClickCancel, this));
itemArray.pushBack(cancelBT);
cancelBT->setPosition(Point(back->getContentSize().width / 2 + 50, 25));
}
auto menu = Menu::createWithArray(itemArray);
menu->setPosition(Point::ZERO);
back->addChild(menu);
return true;
}
#if와 #endif를 이용하였습니다. 이것은 전처리기라는 것으로 컴파일시 해당 루틴을 컴파일합니다.
따라서 win32가 아닌 다른 플랫폼빌드시 아래쪽의 #if만 빌드됩니다.
디버거를 실행해 한글이 제대로 출력되는지 확인합니다.
Figure 6‑58 실행화면
한글이 제대로 출력됩니다.
하지만 이것은 윈도우 디버거만 적용이 된것이지 Android와 iPhone에서는 한글이 출력되지 않습니다.
전처리기를 사용하여 win32일때만 수정했기 때문입니다.
Android와 iPhone에서 한글이 제대로 나오지 않는경우는 비주얼스튜디오를 사용하여 클레스를 생성한 경우입니다. 맥에서 Xcode를 이용할시 파일이 자동으로 UTF-8로 저장되기 때문에 Android와 iPhone에서 한글이 출력됩니다.
.cpp 파일과 .h파일을 생성하는데 비주얼 스튜디오는 ANSI 형식으로 파일을 저장합니다. 이 파일 저장형식을 UTF-8로 수정하여야 합니다.
자세한 내용은 부록을 참고바랍니다.
한글 출력을 수정하였고 이번엔 게임에서 사용할 최대 이미지 개수가 20개므로 20개까지만 저장되도록 수정하겠습니다.
----------DatabaseManager.h----------
#ifndef __DATABASEMANAGER_H__
#define __DATABASEMANAGER_H__
#include "sqlite3.h"
#include "cocos2d.h"
//USING_NS_CC;와 같습니다.
using namespace cocos2d;
//std의 네임스페이스를 사용하겠다고 정의합니다.
using namespace std;
struct head
{
public:
int no;
char *image;
//Point에는 x와 y값이 들어갑니다.
Point position;
//Color3B에는 r, g, b값이 들어갑니다.
Color3B color1;
Color3B color2;
Color3B color3;
Color3B color4;
//색상 변경 관련 체크 변수
bool isColor;
};
struct character
{
public:
int headNo;
int headColorNo;
int hair1No;
int hair1ColorNo;
int hair2No;
int hair2ColorNo;
int eyeNo;
int eyeColorNo;
int mouthNo;
int mouthColorNo;
int etcNo;
int etcColorNo;
int bgNo;
int bgColorNo;
};
class DatabaseManager
{
private:
bool openDB();
void closeDB();
sqlite3 *_sqlite;
public:
DatabaseManager();
~DatabaseManager();
//에러메시지를 담을 변수
char *_errorMSG;
//결과의 상태를 담을 변수
int _result;
static DatabaseManager *getInstance();
void createDB();
void insertDB();
void selectDB();
list<head*> selectDB(string table, int no);
int insertCharacterDB(character *characterInfo);
void deleteCharacterDB(int no);
};
#endif // __DATABASEMANAGER_H__
insertCharacterDB()의 리턴값을 int로 수정합니다.
----------DatabaseManager.cpp----------
…생략…
int DatabaseManager::insertCharacterDB(character *characterInfo){
{
//개수 체크
sqlite3_stmt *pStmt = NULL;
string query = "select count(*) from TB_USER_CHARACTER";
_result = sqlite3_prepare_v2(_sqlite, query.c_str(), query.length(), &pStmt, NULL);
if (_result == SQLITE_OK)
{
log("selectDB() SUCCESS");
if (sqlite3_step(pStmt) == SQLITE_ROW)
{
int cnt = sqlite3_column_int(pStmt, 0);
if (cnt >= 20){
sqlite3_finalize(pStmt);
return 2;
}
}
}
sqlite3_finalize(pStmt);
}
{
//캐릭터 하나 추가
string query = "insert into TB_USER_CHARACTER(HEAD_NO, HEAD_COLOR_NO, HAIR1_NO, HAIR1_COLOR_NO, HAIR2_NO, HAIR2_COLOR_NO, EYE_NO, EYE_COLOR_NO, MOUTH_NO, MOUTH_COLOR_NO, ETC_NO, ETC_COLOR_NO, BG_NO, BG_COLOR_NO) ";
char temp[255];
sprintf(temp, "values (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", characterInfo->headNo, characterInfo->headColorNo, characterInfo->hair1No, characterInfo->hair1ColorNo, characterInfo->hair2No, characterInfo->hair2ColorNo, characterInfo->eyeNo, characterInfo->eyeColorNo, characterInfo->mouthNo, characterInfo->mouthColorNo, characterInfo->etcNo, characterInfo->etcColorNo, characterInfo->bgNo, characterInfo->bgColorNo);
query += temp;
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK)
{
log("insertDB() SUCCESS");
return 1;
}
else{
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
return 0;
}
}
}
insert 구문을 실행하기전에 select쿼리를 한번 실행하여 row의 개수를 체크한뒤 20개가 넘어가면 2를 return 하도록 해주었습니다. 성공은 1이고 데이터베이스 오류는 0을 return합니다.
이번엔 CharacterScene.cpp를 수정하도록 합니다.
----------CharacterScene.cpp----------
…생략…
void CharacterScene::onClickSave(Ref *object){
log("onClickSave");
int result = DatabaseManager::getInstance()->insertCharacterDB(_characterInfo);
if (result == 1)
this->addChild(TextPopup::create("저장이 완료되었습니다.", this, NULL, false), 10);
else if (result == 2)
this->addChild(TextPopup::create("저장이 실패하였습니다.\n저장 가능한 개수를 초과하였습니다.", this, NULL, false), 10);
else
this->addChild(TextPopup::create("저장이 실패하였습니다.", this, NULL, false), 10);
}
insertCharacterDB()의 return값에 따라 성공과 실패 팝업으로 나누었습니다.
Figure 6‑59 실행화면
20개까진 저장이 되고 21개째부턴 에러메시지가 나옵니다.
저장을 20개로 제한하였습니다.
이번에는 같은 이미지를 저장하지 못하게 해야합니다.
같은 이미지도 저장이 안되게 수정하겠습니다.
----------DatabaseManager.cpp----------
…생략…
int DatabaseManager::insertCharacterDB(character *characterInfo){
{
//개수 체크
sqlite3_stmt *pStmt = NULL;
string query = "select count(*) from TB_USER_CHARACTER";
_result = sqlite3_prepare_v2(_sqlite, query.c_str(), query.length(), &pStmt, NULL);
if (_result == SQLITE_OK)
{
log("selectDB() SUCCESS");
if (sqlite3_step(pStmt) == SQLITE_ROW)
{
int cnt = sqlite3_column_int(pStmt, 0);
if (cnt >= 20){
sqlite3_finalize(pStmt);
return 2;
}
}
}
sqlite3_finalize(pStmt);
}
{
//같은 이미지 체크
sqlite3_stmt *pStmt = NULL;
string query = "select count(*) from TB_USER_CHARACTER where ";
char temp[500];
sprintf(temp, "HEAD_NO = %d AND HEAD_COLOR_NO = %d AND HAIR1_NO = %d AND HAIR1_COLOR_NO = %d AND HAIR2_NO = %d AND HAIR2_COLOR_NO = %d AND EYE_NO = %d AND EYE_COLOR_NO = %d AND MOUTH_NO = %d AND MOUTH_COLOR_NO = %d AND ETC_NO = %d AND ETC_COLOR_NO = %d AND BG_NO = %d AND BG_COLOR_NO = %d", characterInfo->headNo, characterInfo->headColorNo, characterInfo->hair1No, characterInfo->hair1ColorNo, characterInfo->hair2No, characterInfo->hair2ColorNo, characterInfo->eyeNo, characterInfo->eyeColorNo, characterInfo->mouthNo, characterInfo->mouthColorNo, characterInfo->etcNo, characterInfo->etcColorNo, characterInfo->bgNo, characterInfo->bgColorNo);
query += temp;
_result = sqlite3_prepare_v2(_sqlite, query.c_str(), query.length(), &pStmt, NULL);
if (_result == SQLITE_OK)
{
log("selectDB() SUCCESS");
if (sqlite3_step(pStmt) == SQLITE_ROW)
{
int cnt = sqlite3_column_int(pStmt, 0);
if (cnt >= 1){
sqlite3_finalize(pStmt);
return 3;
}
}
}
sqlite3_finalize(pStmt);
}
{
//캐릭터 하나 추가
string query = "insert into TB_USER_CHARACTER(HEAD_NO, HEAD_COLOR_NO, HAIR1_NO, HAIR1_COLOR_NO, HAIR2_NO, HAIR2_COLOR_NO, EYE_NO, EYE_COLOR_NO, MOUTH_NO, MOUTH_COLOR_NO, ETC_NO, ETC_COLOR_NO, BG_NO, BG_COLOR_NO) ";
char temp[255];
sprintf(temp, "values (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)", characterInfo->headNo, characterInfo->headColorNo, characterInfo->hair1No, characterInfo->hair1ColorNo, characterInfo->hair2No, characterInfo->hair2ColorNo, characterInfo->eyeNo, characterInfo->eyeColorNo, characterInfo->mouthNo, characterInfo->mouthColorNo, characterInfo->etcNo, characterInfo->etcColorNo, characterInfo->bgNo, characterInfo->bgColorNo);
query += temp;
_result = sqlite3_exec(_sqlite, query.c_str(), NULL, NULL, &_errorMSG);
if (_result == SQLITE_OK)
{
log("insertDB() SUCCESS");
return 1;
}
else{
log("ERROR CODE : %d, ERROR MSG : %s", _result, _errorMSG);
return 0;
}
}
}
중간에 모든값이 똑같은 row가 1개이상 있는지 검사하는 로직을 추가하여 만약 같은 값이 있을경우 3을 return 하도록 하였습니다.
해당 컬럼들에 대해 같은 값이 추가되지 않도록 테이블을 생성할 때 컬럼들을 unique로 지정하는 방법도 있습니다.
3이 return 되었을때의 처리를 추가하도록 합니다.
----------CharacterScene.cpp----------
…생략…
void CharacterScene::onClickSave(Ref *object){
log("onClickSave");
int result = DatabaseManager::getInstance()->insertCharacterDB(_characterInfo);
if (result == 1)
this->addChild(TextPopup::create("저장이 완료되었습니다.", this, NULL, false), 10);
else if (result == 2)
this->addChild(TextPopup::create("저장이 실패하였습니다.\n저장 가능한 개수를 초과하였습니다.", this, NULL, false), 10);
else if (result == 3)
this->addChild(TextPopup::create("저장이 실패하였습니다.\n같은 이미지가 존재합니다.", this, NULL, false), 10);
else
this->addChild(TextPopup::create("저장이 실패하였습니다.", this, NULL, false), 10);
}
3이 넘어오면 에러메시지를 출력해주도록 하였습니다.
디버거를 실행하여 확인해봅니다.
Figure 6‑60 실행화면
제대로 동작하는 것을 확인할 수 있습니다.
데이터베이스에 대한 처리를 모두 완료 하였으므로 DevConf.h 파일의 DB_INIT을 false로 수정해놓습니다.
----------DevConf.h----------
#define DB_INIT false
이제부턴 데이터베이스를 초기화할 필요가 없기때문입니다.
갤러리 화면으로 전환되는 기능을 구현하겠습니다.
GalleryScene이라는 이름의 클래스를 Classes폴더에 생성합니다. [5.3] 참고.
Figure 6‑61 클래스 추가
GalleryScene.h 파일과 GalleryScene.cpp 파일이 생성되었습니다.
GalleryScene.h 파일과 GalleryScene.cpp 파일을 아래와 같이 수정합니다.
----------GalleryScene.h----------
#include "cocos2d.h"
class GalleryScene : public cocos2d::Layer
{
public:
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();
// implement the "static create()" method manually
CREATE_FUNC(GalleryScene);
void onClickHome(Ref *object);
void onClickMake(Ref *object);
};
----------GalleryScene.cpp----------
#include "GalleryScene.h"
#include "StartScene.h"
USING_NS_CC;
Scene* GalleryScene::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = GalleryScene::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool GalleryScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
//code here
//Device의 크기를 가져옵니다.
Size winSize = Director::getInstance()->getOpenGLView()->getDesignResolutionSize();
//Sprite를 생성하여 이미지를 불러옵니다.
auto back = Sprite::create("bg_gallery.png");
//back를 해당 포인트에 위치시킵니다. 화면의 정중앙에 위치하도록 했습니다.
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
//this에 back을 자식 노드로 추가하였습니다.
this->addChild(back);
auto home = MenuItemImage::create("menu_home2.png", "menu_home2_on.png", CC_CALLBACK_1(GalleryScene::onClickHome, this));
home->setAnchorPoint(Point(0, 0));
home->setPosition(Point(0, 0));
auto make = MenuItemImage::create("menu_make.png", "menu_make_on.png", CC_CALLBACK_1(GalleryScene::onClickMake, this));
make->setAnchorPoint(Point(1, 0));
make->setPosition(Point(winSize.width, 0));
auto menu2 = Menu::create(home, make, NULL);
menu2->setPosition(Point::ZERO);
this->addChild(menu2);
return true;
}
void GalleryScene::onClickHome(Ref *object){
log("onClickHome");
auto Scene = StartScene::createScene();
//StartScene으로 이동하는데 지금 Scene을 StartScene에서 돌아오지 않기에 replaceScene을 해줌
Director::getInstance()->replaceScene(Scene);
}
void GalleryScene::onClickMake(Ref *object){
log("onClickMake");
//이전화면이 캐릭터 만드는 화면이므로 지금 Scene을 Director에서 pop한다.
Director::getInstance()->popScene();
}
기존에 사용했던 Sprite, MenuItemImage와 Menu를 사용하여 화면을 구성하였습니다.
GalleryScene를 생성하였으니 CharacterScene에서 GalleryScene을 호출하도록 합니다.
----------CharacterScene.cpp----------
#include "CharacterScene.h"
#include "DatabaseManager.h"
#include "DevConf.h"
#include "ColorPopup.h"
#include "TextPopup.h"
#include "GalleryScene.h"
USING_NS_CC;
…생략…
void CharacterScene::onClickGallery(Ref *object){
log("onClickGallery");
auto Scene = GalleryScene::createScene();
Director::getInstance()->pushScene(Scene);
}
GalleryScene을 호출했으면 디버거를 실행하여 갤러리 버튼을 눌러봅니다.
Figure 6‑62 갤러리Scene 실행화면
갤러리 화면으로 문제없이 이동됩니다.
갤러리 화면에서 HOME과 MAKE A FACE 버튼도 동작하는지 확인하도록 합니다.
Figure 6‑63 홈버튼 실행화면
Figure 6‑64 캐릭터 생성 실행화면