지난번 연재 8-1, 8-2, 8-3, 8-4, 8-5, 8-6과 이어집니다.
*그림번호가 너무 많이 늘어나 1부터 리셋합니다.
사운드를 on / off 할 수 있는 설정과 종료기능을 구현해보도록 하겠습니다.
----------StartScene.h----------
#include "cocos2d.h"
class StartScene : 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(StartScene);
void onClickGame(Ref *object);
void onClickRank(Ref *object);
void onClickSound(Ref *object);
void onClickExit(Ref *object);
};
----------StartScene.cpp----------
…생략…
// on "init" you need to initialize your instance
bool StartScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
//code here
//Device의 크기를 가져옵니다.
auto winSize = Director::getInstance()->getOpenGLView()->getDesignResolutionSize();
this->addChild(BackgroundLayer::create());
auto title = Sprite::create("title_1.png");
title->setPosition(Point(winSize.width / 2, 230));
this->addChild(title);
auto action1 = Sequence::createWithTwoActions(RotateTo::create(0.05f, -1), RotateTo::create(0.05f, 1));
title->runAction(RepeatForever::create(action1));
auto deco = Sprite::create("intro_bear1.png");
deco->setAnchorPoint(Point(0, 1));
deco->setPosition(Point(winSize.width, 0));
this->addChild(deco);
auto easeAction = EaseInOut::create(MoveBy::create(0.5f, Point(-deco->getContentSize().width, deco->getContentSize().height)), 1);
deco->runAction(easeAction);
auto gameMenu = MenuItemImage::create("btn_play.png", "btn_play_on.png", CC_CALLBACK_1(StartScene::onClickGame, this));
gameMenu->setPosition(Point(winSize.width / 2, 130));
auto rankMenu = MenuItemImage::create("btn_ranking.png", "btn_ranking_on.png", CC_CALLBACK_1(StartScene::onClickRank, this));
rankMenu->setPosition(Point(winSize.width / 2, 70));
auto soundMenu = MenuItemImage::create("sound.png", "sound_on.png", CC_CALLBACK_1(StartScene::onClickSound, this));
soundMenu->setPosition(Point(winSize.width - 70, winSize.height - 20));
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (!isSound){
//sound off 설정이면 이미지를 변경해준다.
auto off_normal = Sprite::create("sound_off.png");
soundMenu->setNormalImage(off_normal);
auto off_select = Sprite::create("sound_off_on.png");
soundMenu->setSelectedImage(off_select);
}
auto exitMenu = MenuItemImage::create("btn_close.png", "btn_close_on.png", CC_CALLBACK_1(StartScene::onClickExit, this));
exitMenu->setPosition(Point(winSize.width - 20, winSize.height - 20));
auto menu = Menu::create(gameMenu, rankMenu, soundMenu, exitMenu, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
if (isSound){
CocosDenshion::SimpleAudioEngine::getInstance()->playBackgroundMusic("sound/bgm.mp3", true);
}
return true;
}
…생략…
void StartScene::onClickSound(Ref *object){
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
isSound = !isSound; //반대로 변경
UserDefault->setBoolForKey("isSound", isSound);
UserDefault->flush(); //flush() 해주어야 적용됩니다.
//넘어온 Object를 MenuItemImage로 캐스팅
auto menuItem = (MenuItemImage *)object;
if (!isSound){
CocosDenshion::SimpleAudioEngine::getInstance()->stopBackgroundMusic(true);
//sound off 설정이면 이미지를 변경해준다.
auto off_normal = Sprite::create("sound_off.png");
menuItem->setNormalImage(off_normal);
auto off_select = Sprite::create("sound_off_on.png");
menuItem->setSelectedImage(off_select);
}
else{
CocosDenshion::SimpleAudioEngine::getInstance()->playBackgroundMusic("sound/bgm.mp3", true);
//sound on 설정이면 이미지를 변경해준다.
auto off_normal = Sprite::create("sound.png");
menuItem->setNormalImage(off_normal);
auto off_select = Sprite::create("sound_on.png");
menuItem->setSelectedImage(off_select);
}
}
void StartScene::onClickExit(Ref *object){
Director::getInstance()->end();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
exit(0);
#endif
}
버튼 2개를 추가하였습니다. Sound 버튼에는 UserDefault를 사용해 isSound값을 저장하여 사용했습니다.
isSound가 true면 배경음악이 출력됩니다. isSound가 false이면 이미지를 변경하여 표시해주었습니다.
Sound버튼을 선택하면 UserDefault에서 isSound값을 가져와 반대로 설정하고 이미지를 변경하고 배경음악을 stop하거나 play해주었습니다.
디버거를 실행해 확인하도록 합니다.
Figure 1 실행화면
종료버튼 선택시 어플이 종료됩니다.
Figure 2 실행화면
그럼이제 게임내부에서 사용되는 효과음을 Play해주는 부분에서 변수값을 이용해 사운드를 체크해주도록 합니다.
사운드를 사용하는 부분이 많다면 따로 클래스를 만들어 관리해주어도 됩니다.
그럼이제 배경음악에 관한 처리는 다되었고, 효과음들을 처리하도록 합니다.
----------GameScene.cpp----------
…생략…
void GameScene::polarbearAnimationFinish(){
…생략…
case 3: //펭귄 구출
{
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//펭귄 구출시 효과음 추가
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/cage.wav");
}
_score = _score + RESCUE;
_rescueCount = _rescueCount + 1;
…생략…
----------Polarbear.cpp--------
…생략…
void Polarbear::callCallback(){
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//착지할때 효과음 추가
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/jump.wav");
}
//에니메이션이 완료되면 callback메소드 호출
if (_listener && _selector)
(_listener->*_selector)();
}
----------GameoverPopup.cpp----------
…생략…
bool GameoverPopup::init(){
//여기에 팝업을 작성한다.
auto UserDefault = CCUserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//게임오버 팝업이 출력되면 배경음악을 잠시 멈춘다.
CocosDenshion::SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/gameover.mp3");
}
//화면 크기를 가져옴
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 gameover = Sprite::create("gameover.png");
gameover->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(gameover);
return true;
}
…생략…
bool GameoverPopup::onTouchBegan(Touch* touch, Event* event){
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//배경음악을 다시 실행시킨다.
CocosDenshion::SimpleAudioEngine::getInstance()->resumeBackgroundMusic();
}
Director::getInstance()->popScene();
return true;
}
----------ResultPopup.cpp----------
…생략…
bool ResultPopup::init(int score, int penguin){
…생략…
this->schedule(schedule_selector(ResultPopup::updateScore), 3.0f / 60, 59, 0.05f);
auto UserDefault = CCUserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//배경음악을 잠시 멈추고 효과음을 실행시킨다.
CocosDenshion::SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/score.wav");
}
return true;
}
…생략…
void ResultPopup::onClickOK(Ref *object){
auto UserDefault = CCUserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//배경음악을 다시 실행시킨다.
CocosDenshion::SimpleAudioEngine::getInstance()->resumeBackgroundMusic();
}
Director::getInstance()->popScene();
}
…생략…
void ResultPopup::setParticle(Node *node){
//파티클 추가
auto UserDefault = UserDefault::getInstance();
bool isSound = UserDefault->getBoolForKey("isSound", true);
if (isSound){
//효과음을 약간 다르게 넣기위해서 tag를 받아 효과음을 출력
if (node->getTag() == 0)
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/firework1.mp3");
else if (node->getTag() == 1)
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/firework2.mp3");
else if (node->getTag() == 2)
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("sound/firework3.mp3");
}
//30개의 입자를 가진 ParticleExplosion 생성
auto particle = ParticleExplosion::createWithTotalParticles(30);
particle->setPosition(Point(node->getContentSize().width / 2, node->getContentSize().height / 2));
//텍스쳐를 변경
particle->setTexture(TextureCache::getInstance()->addImage("star.png"));
//파티클의 속도설정
particle->setSpeed(100);
//파티클시작 색상값 비율 투명도를 80%로 조절하였다.
particle->setStartColor(Color4F(1, 1, 1, 0.80f));
//자동으로 없앤다.
particle->setAutoRemoveOnFinish(true);
node->addChild(particle);
}
사운드를 출력하는 부분에 대해서 isSound값을 체크하여 사운드를 구분해주었습니다.
디버거를 실행하고 사운드를 off한 뒤 해당 사운드가 나오지 않는지 확인합니다.
Figure 3 실행화면
사운드를 종료하고 배경음악, 펭귄 구출시, 게임오버시, 게임완료시 사운드가 나오지 않는 것을 확인하였습니다.
게임방법설명에 대한 팝업을 추가하도록 하겠습니다.
HowToPlayPopup이라는 이름의 클래스를 Classes 폴더에 생성합니다. [5.2] 참고.
Figure 4 클래스 추가
HowToPlayPopup.cpp파일과 HowToPlayPopup.h파일이 생성되었습니다.
HowToPlayPopup.h파일과 HowToPlayPopup.cpp파일을 아래와 같이 수정하도록 합니다.
----------HowToPlayPopup.h----------
#include "cocos2d.h"
USING_NS_CC;
class HowToPlayPopup :public Layer
{
public:
static HowToPlayPopup * create();
bool init();
void onEnter();
bool onTouchBegan(Touch* touch, Event* event);
void onTouchEnded(Touch* touch, Event* event);
};
----------HowToPlayPopup.cpp----------
#include "HowToPlayPopup.h"
HowToPlayPopup * HowToPlayPopup::create(){
HowToPlayPopup *ret = new HowToPlayPopup();
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
bool HowToPlayPopup::init(){
//여기에 팝업을 작성한다.
//화면 크기를 가져옴
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 howtoplay = Sprite::create("how_to_play.png");
howtoplay->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(howtoplay);
return true;
}
void HowToPlayPopup::onEnter(){
Layer::onEnter();
setTouchEnabled(true);
setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
}
bool HowToPlayPopup::onTouchBegan(Touch* touch, Event* event){
return true;
}
void HowToPlayPopup::onTouchEnded(Touch* touch, Event* event){
this->removeFromParentAndCleanup(true);
}
HowToPlayPopup을 생성하였습니다. 이 팝업이 표시된상태에서 화면을 한번 터치하면 팝업이 사라지도록 구현하였습니다.
onTouchEnded()는 터치가 시작될때가 아니라 터치가 끝날 때 호출 됩니다.
게임방법설명팝업을 스테이지화면 우측상단에 버튼을 추가하고 버튼이 클릭되면 게임방법설명팝업이 추가되도록 만들겠습니다.
----------StageScene.h----------
#include "cocos2d.h"
class StageScene : 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(StageScene);
void onClickBack(Ref *object);
cocos2d::LayerColor *_stageLayer;
void setStage();
void onClickStage(Ref *object);
void onEnter();
void onClickHowToPlay(Ref *object);
};
----------StageScene.cpp----------
#include "StageScene.h"
#include "BackgroundLayer.h"
#include "GameScene.h"
#include "HowToPlayPopup.h"
USING_NS_CC;
…생략…
// on "init" you need to initialize your instance
bool StageScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
//code here
this->addChild(BackgroundLayer::create());
//화면 크기를 가져옴
auto winSize = Director::getInstance()->getOpenGLView()->getDesignResolutionSize();
auto deco = Sprite::create("stage_bear.png");
deco->setAnchorPoint(Point(1, 0));
deco->setPosition(Point(winSize.width + deco->getContentSize().width, -deco->getContentSize().height));
this->addChild(deco);
auto easeAction = EaseInOut::create(MoveBy::create(0.5f, Point(-deco->getContentSize().width, deco->getContentSize().height)), 1);
deco->runAction(Sequence::createWithTwoActions(DelayTime::create(0.5f), easeAction));
auto back = MenuItemImage::create("btn_back.png", "btn_back_on.png", CC_CALLBACK_1(StageScene::onClickBack, this));
back->setPosition(Point(30, winSize.height - 20));
auto howtoplay = MenuItemImage::create("m_help.png", "m_help_on.png", CC_CALLBACK_1(StageScene::onClickHowToPlay, this));
howtoplay->setPosition(Point(winSize.width - 20, winSize.height - 20));
auto menu = Menu::create(back, howtoplay, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
_stageLayer = NULL;
//scene이 생성되면 onEnter()가 자동으로 호출된다.
//따라서 setStage()를 주석처리하고 onEnter에서 실행해준다.
//setStage();
return true;
}
…생략…
void StageScene::onClickHowToPlay(Ref *object){
this->addChild(HowToPlayPopup::create(), 99);
}
스테이지 화면에 버튼을 추가하고 버튼이 선택되면 팝업이 출력됩니다.
디버거를 실행하여 팝업을 확인하도록 합니다.
Figure 5 실행화면
화면을 터치하면 다시 스테이지 화면이 나옵니다.
서버는 가상서버나 직접 서버를 유지하기 힘든 독자를 위해 직접 본인의 컴퓨터에 서버를 세팅하여 같은 네트워크 상에서 접속하여 테스트 해볼 수 있도록 해보겠습니다.
서비스를 목적이 아닌 테스트 목적이므로 내부 네트워크 상에서만 동작합니다.
서버는 보통 아파치, My-sql, PHP를 세팅하는데 앞글자를 따서 amp라고 부릅니다.
윈도우 환경에서 이 amp환경을 한번에 쉽게 설치할 수 있는 xampp라는 소프트웨어 패키지를 이용해 서버 환경을 세팅해보도록 하겠습니다.
http://sourceforge.net/projects/xampp/files/으로 접속합니다.
Figure 6 xampp 다운로드
xampp를 다운받을 수 있는 페이지가 나왔습니다.
Download를 선택합니다. 버전은 현재나와있는 최신버전을 다운로드 받으면 됩니다.
[그림 6-131 xmpp 다운로드2]
Figure 7 xampp 다운로드
위와 같은 화면이 나오면서 잠시 기다리면 다운로드 창이 나옵니다.
Figure 8 xampp 다운로드
저장하여 실행하거나 실행을 선택하여 설치를 진행합니다.
Figure 9 xampp 다운로드
경고 문구가 나옵니다. 권한때문에 설명한 경로는 피해서 설치하라는 경고입니다.
xampp를 사용하는데 바이러스 백신이 실행중이라면 종료하는 것이 좋습니다.
OK를 선택하여 인스톨을 진행합니다.
Figure 10 xampp 다운로드
Next를 선택합니다.
Figure 11 xampp 다운로드
어떤 프로그램들을 설치할것인지 묻는 창이 나옵니다. 필요하지 않은 것이 있다면 해제 하면 됩니다만 잘모르겠으면 다 설치해도 문제없습니다.
Next를 선택합니다.
Figure 12 xampp 다운로드
위치는 기본경로인 C:/xampp에 설치하도록 합니다.
Next를 선택합니다.
Figure 13 xampp 다운로드
이런화면이 나오는데 체크를 해제 하고 Next를 선택합니다. XAMPP에 대해 알아보겠냐는 체크입니다.
궁금하신분은 체크를 놔두시고 Next를 선택하셔도 됩니다.
Figure 14 xampp 다운로드
설치 준비가 끝났습니다. Next를 선택하면 설치가 진행됩니다.
Figure 15 xampp 다운로드
진행률을 확인하면서 설치가 완료되기를 기다립니다.
진행중 방화벽관련 경고가나온다면 허용을 선택하도록 합니다.
Figure 16 xampp 다운로드
설치가 완료되면 Finish를 선택합니다.
Figure 17 xampp 다운로드
Finish를 선택하면 이런 컨트롤 판넬이 나타납니다.
Figure 18 xampp 설정
Start를 눌러 모든 서비스를 켜줍니다.
방화벽 관련 경고가 나온다면 허용을 선택해줍니다.
Figure 19 xampp 포트 변경
Apache를 실행하려다보면 이런 에러가 발생하는 경우가 있습니다. 이미 다른 소프트웨어가 443포트를 이미 사용중이라는 것인데요.
Figure 20 xampp 포트 변경
Config-Apache(httpd-ssl.conf)를 선택합니다.
Figure 21 xampp 포트 변경
메모장으로 설정파일이 열리는데요 Listen 443을 다른포트로 바꾸어줍니다. 444로 바꾸도록 하겠습니다.
저장하고 메모장을 닫아준뒤 start를 실행하면 실행되는 것을 확인할 수 있습니다.
그럼이제 제대로 서버가 동작하고 있는지를 확인할 차례입니다.
Figure 22 xampp 확인
C:\xampp\htdocs로 이동합니다. 이곳이 바로 내 서버의 루트 폴더입니다.
Figure 23 xampp 확인
이 폴더에 gameapi 라는 폴더를 하나 생성합니다.
gameapi폴더 내부로 들어갑니다.
Figure 24 xampp 확인
index.txt라는 파일을 하나 생성합니다.(오른쪽 버튼을 눌른뒤 새로 만들기-텍스트 문서를 선택하면 파일이 생성됩니다.)
Figure 25 xampp 확인
이 TXT파일에 위와같이 <?php phpinfo(); ?>라고 입력한뒤 저장하고 파일을 닫아줍니다.
(위 명령어는 php의 정보를 알 수 있는 php메소드입니다.)
Figure 26 xampp 확인
index.txt파일을 index.php라고 변경해줍니다.
Figure 27 xampp 확인
브라우저에서 http://localhost/gameapi/index.php으로 접속합니다.
localhost라는 것은 지금서버가 동작하고 있는 네트워크라는 내용입니다.
내 네트워크 내에서 방금 생성한 index.php파일을 호출한 것이죠. index.php파일을 호출하면 서버에 설치된 php정보를 볼 수 있습니다.
위 화면까지 나왔다면 서버설치가 완료된 것입니다.
복잡한 서버세팅을 간단하게 끝냈습니다.
게임에서 XAMPP를 이용하여 네트워크가 동작하도록 하려면 이 XAMPP의 서비스를 start해주어야 네트워크가 동작합니다.
세팅된 서버에 데이터베이스를 구축하도록 하겠습니다.
필요한 테이블은 점수를 저장하고 랭킹을 확인할 수 있는 테이블이 필요합니다.
Figure 28 데이터베이스 생성
XAMPP Control Panel 에서 MySQL의 Admin을 선택합니다.
Figure 29 데이터베이스 생성
브라우저에 phpMyAdmin이란 페이지가 열립니다.
phpMyAdmin은 데이터베이스를 관리할 수 있는 페이지입니다. 여러가지 정보들을 메인화면에서 확인 할 수 있습니다.
Figure 30 데이터베이스 생성
홈화면의 상단 메뉴에서 데이터베이스를 선택합니다.
새 데이터베이스 만들기가 나옵니다.
Figure 31 데이터베이스 생성
데이터베이스 이름을 polarbear라고 입력한 뒤 만들기를 선택합니다.
Figure 32 데이터베이스 생성
알림 화면이 나타나면 데이터베이스 생성이 완료되었습니다.
Figure 33 데이터베이스 생성
좌측을 보면 polarbear라는 데이터베이스가 생성되었습니다.
데이터 베이스를 선택하여 해당 데이터베이스로 들어갑니다.
Figure 34 데이터베이스 생성
데이터베이스에 테이블이 없기 때문에 새 테이블 만들기가 나타납니다.
Figure 35 데이터베이스 생성
필요한 테이블은 tb_ranking 이라는 테이블을 생성하도록 하겠습니다.
해당 테이블은 3개의 컬럼을 갖도록 하겠습니다.
테이블 이름과 컬럼수를 입력했으면 실행을 선택합니다.
Figure 36 데이터베이스 생성
무언가 복잡한 화면이 나타났습니다.
여기서 중요한 부분은 이름, 종류, 길이/값, 기본값, Null, A_I, 설명 부분입니다.
이름은 말그대로 컬럼명입니다. 영어로 입력합니다.
종류는 해당 컬럼의 형식입니다. int는 숫자 varChar는 텍스트 이런식으로 선택합니다.
길이는 컬럼에 저장할 수 있는 크기입니다.
기본값은 해당 컬럼의 값이 Null일경우 설정된 기본값으로 저장이됩니다.
Null은 체크하면 Null 저장이 가능해집니다. 체크가 안되있는데 해당 컬럼에 Null이 들어올경우 기본값 설정이 안되어있음 에러가 발생합니다. 기본값을 설정해두었다면 이부분을 신경쓸 필요가 없습니다. Null이 들어올경우 기본값으로 변환되어 저장되기때문이죠.
A_I 부분은 AutoIncreament 옵션으로 자동으로 증가 하는 값을 저장합니다. 보통 해당 row의 고유값으로 많이 사용합니다. A_I를 사용하기 위해선 컬럼이 INT형 이어야 합니다.
설명은 해당 컬럼에 대해 설명을 달아놓을 수 있습니다.
그럼 아래와 같이 컬럼을 입력하도록 합니다.
Figure 37 데이터베이스 생성
1. 이름 : NO, 종류 : INT, 길이/값 : 100, A_I : 체크, 설명 : 고유값
2. 이름 : NAME, 종류 : VARCHAR, 길이/값 : 50, 설명 : 이름
3. 이름 : POINT, 종류 : INT, 길이/값 : 100, 설명 : 점수
위와같이 입력하였으면 우측 하단의 저장 버튼을 선택합니다.
Figure 38 데이터베이스 생성
테이블이 생성되었습니다.
Figure 39 데이터베이스 생성
좌측에서 polarbear 데이터베이스의 이름 앞에 + 를 선택하면 방금생성한 테이블이 나타납니다.
Figure 40 데이터베이스 생성
이 테이블을 선택하면 위와 같은 내용이 나옵니다.
테이블에 값이 있으면 여기에 보여집니다.
값을 하나 임의로 넣어보도록 하겠습니다.
테이블이 선택된 상태에서 상단의 메뉴중 삽입을 선택합니다.
Figure 41 데이터베이스 생성
NO는 자동으로 추가되는 값이므로 입력하지 않아도 됩니다.
NAME에는 test, POINT에는 100을 입력하고 실행을 누릅니다.
Figure 42 데이터베이스 생성
다음과 같은 쿼리가 실행榮募?내용이 나옵니다.
형식에 맞지않는 값이 들어가거나 하면 여기서 에러가 발생합니다.
값을 추가했으면 상단 메뉴중 보기를 선택합니다.
Figure 43 데이터베이스 생성
tb_ranking 테이블에 다음과 같은 값이 추가되었습니다.
위와 같은 값이 출력된다면 데이터베이스를 성공적으로 생성된 것입니다.
그럼 NAME에 한글을 한번 입력해보도록 하겠습니다.
Figure 44 데이터베이스 생성
NAME에 테스트 라고 입력하고 POINT에 100이라고 입력했습니다.
실행을 선택해봅니다.
Figure 45 데이터베이스 생성
불길한 빨간색이 나왔습니다. 무언가 정상적으로 처리되지 않았나봅니다.
상단 메뉴중 보기를 선택해 데이터베이스의 값들을 확인해보도록 합니다.
Figure 46 데이터베이스 생성
???라고 추가되었습니다.
이유는 인코딩 값이 맞지 않아서입니다.
그럼 한글을 추가할 수 있도록 수정해보도록 하겠습니다.
Figure 47 데이터베이스 생성
상단 메뉴중 구조를 선택하고 NAME을 봅니다. 데이터 정렬방식을 보면 NAME에 latin1_swedish_ci이라고 입력되어있습니다.
이것을 수정하면 됩니다.
NAME의 우측에 변경을 선택합니다.
Figure 48 데이터베이스 생성
그럼 NAME 컬럼의 값을 변경할 수 있습니다.
데이터정렬방식에서 콤보박스를 선택합니다.
Figure 49 데이터베이스 생성
utf8_general_ci로 수정하고 저장을 누릅니다.
Figure 50 데이터베이스 생성
NAME의 데이터 정렬방식이 utf8_general_ci로 수정되었습니다.
그럼 다시 테스트 라는 값을 입력해보도록 합니다.
Figure 51 데이터베이스 생성
Figure 52 데이터베이스 생성
테스트란 이름이 NAME에 추가되었습니다.
이것으로 데이터베이스 생성이 완료되었습니다. 테스트용으로 추가되어있는 값들을 삭제를 선택해 지워주도록 합니다.
서버 연동을 하는데 이름을 추가할 수 있도록 하겠습니다.
이름은 최초로 랭킹보기를 선택했을 때 추가하도록 하겠습니다.
NamePopup이라는 이름의 클래스를 Classes 폴더에 생성합니다. [5.2] 참고.
Figure 53 클래스 추가
NamePopup.cpp파일과 NamePopup.h파일이 생성되었습니다.
NamePopup.h파일과 NamePopup.cpp파일을 아래와 같이 수정하도록 합니다.
----------NamePopup.h----------
#include "cocos2d.h"
#include "cocos-ext.h"
USING_NS_CC;
USING_NS_CC_EXT;
class NamePopup :public Layer, public cocos2d::extension::EditBoxDelegate
{
public:
static NamePopup * create();
bool init();
virtual void onEnter();
bool onTouchBegan(Touch* touch, Event* event);
//editBox의 이벤트 콜백 메소드
virtual void editBoxEditingDidBegin(EditBox* editBox);
virtual void editBoxEditingDidEnd(EditBox* editBox);
virtual void editBoxTextChanged(EditBox* editBox, const std::string& text);
virtual void editBoxReturn(EditBox* editBox);
EditBox *_nameEdit;
void onClickOK(Ref *object);
};
----------NamePopup.cpp----------
#include "NamePopup.h"
NamePopup * NamePopup::create(){
NamePopup *ret = new NamePopup();
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
bool NamePopup::init(){
//여기에 팝업을 작성한다.
//화면 크기를 가져옴
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 noti = LabelTTF::create("이름을 입력해 주세요.", "Arial", 25);
noti->setPosition(Point(winSize.width / 2, winSize.height / 2 + 50));
this->addChild(noti);
//에디트 박스 추가
_nameEdit = EditBox::create(Size(200, 50), Scale9Sprite::create("yellow_edit.png"));
_nameEdit->setPosition(Point(winSize.width / 2, winSize.height / 2));
_nameEdit->setFontSize(25);
_nameEdit->setFontColor(Color3B::RED);
_nameEdit->setPlaceHolder("Name:");
_nameEdit->setPlaceholderFontColor(Color3B::WHITE);
_nameEdit->setMaxLength(8);
_nameEdit->setReturnType(EditBox::KeyboardReturnType::DONE);
_nameEdit->setDelegate(this);
this->addChild(_nameEdit);
//버튼 추가
auto okMenu = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(NamePopup::onClickOK, this));
okMenu->setPosition(Point(winSize.width / 2, winSize.height / 2 - 50));
auto menu = Menu::create(okMenu, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
return true;
}
void NamePopup::onEnter(){
Layer::onEnter();
setTouchEnabled(true);
setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
}
bool NamePopup::onTouchBegan(Touch* touch, Event* event){
return true;
}
void NamePopup::editBoxEditingDidBegin(cocos2d::extension::EditBox* editBox)
{
log("editBox %p DidBegin !", editBox);
}
void NamePopup::editBoxEditingDidEnd(cocos2d::extension::EditBox* editBox)
{
log("editBox %p DidEnd !", editBox);
}
void NamePopup::editBoxTextChanged(cocos2d::extension::EditBox* editBox, const std::string& text)
{
log("editBox %p TextChanged, text: %s ", editBox, text.c_str());
}
void NamePopup::editBoxReturn(EditBox* editBox)
{
log("editBox %p was returned !", editBox);
}
void NamePopup::onClickOK(Ref *object){
this->removeFromParentAndCleanup(true);
}
이름입력팝업 클래스를 생성하였습니다.
EditBox는 cocos2d-x의 extension에 추가되어있기 때문에 namePopup.h 파일에 cocos-ext.h파일을 include하였고 extension의 namespace를 사용하기위해 USING_NS_CC_EXT 을 추가해주었습니다.
비주얼 스튜디오에서 extension을 사용하기 위해 libExtensions를 추가하는 방법은 [5.3.7] 참고.
EditBox에 각종 옵션을 설정해주었습니다.
PlaceHolder는 기본적으로 보여줄 텍스트입니다.
그럼 이름이 등록되어있지 않을 경우 랭킹보기를 선택하면 이름입력팝업이 출력되도록 구현해보도록 하겠습니다.
----------StartScene.cpp----------
#include "StartScene.h"
#include "BackgroundLayer.h"
#include "StageScene.h"
#include "RankingPopup.h"
#include "SimpleAudioEngine.h"
#include "NamePopup.h"
USING_NS_CC;
…생략…
void StartScene::onClickRank(Ref *object){
log("onClickRank");
auto UserDefault = UserDefault::getInstance();
std::string name = UserDefault->getStringForKey("name", "");
if (name.compare("") == 0)
this->addChild(NamePopup::create(), 99);
else
this->addChild(RankingPopup::create(), 99);
}
UserDefault에서 name을 가져와 name이 “”(공백)이면 이름입력 팝업이 출력되도록 하였습니다.
디버거를 실행해 이름입력 팝업이 출력되는지 확인하도록 합니다.
Figure 54 실행화면
이름입력 팝업이 출력되었습니다. 한글이 제대로 출력되지 않습니다. 윈도우에서 사용할 경우에 대한 처리를 하고 넘어가겠습니다.
----------NamePopup.cpp----------
...생략...
bool NamePopup::init(){
//여기에 팝업을 작성한다.
//화면 크기를 가져옴
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));
//라벨 추가
std::string text = "이름을 입력해 주세요.";
//문자열을 담을 변수
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 noti = LabelTTF::create(utf8Text, "Arial", 25);
noti->setPosition(Point(winSize.width / 2, winSize.height / 2 + 50));
this->addChild(noti);
//에디트 박스 추가
_nameEdit = EditBox::create(Size(200, 50), Scale9Sprite::create("yellow_edit.png"));
_nameEdit->setPosition(Point(winSize.width / 2, winSize.height / 2));
_nameEdit->setFontSize(25);
_nameEdit->setFontColor(Color3B::RED);
_nameEdit->setPlaceHolder("Name:");
_nameEdit->setPlaceholderFontColor(Color3B::WHITE);
_nameEdit->setMaxLength(8);
_nameEdit->setReturnType(EditBox::KeyboardReturnType::DONE);
_nameEdit->setDelegate(this);
this->addChild(_nameEdit);
//버튼 추가
auto okMenu = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(NamePopup::onClickOK, this));
okMenu->setPosition(Point(winSize.width / 2, winSize.height / 2 - 50));
auto menu = Menu::create(okMenu, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
return true;
}
플랫폼에 맞추어 인코딩을 수정하는 코드를 추가하였습니다. 디버거를 다시 시작하도록 합니다.
Figure 55 실행화면
한글이 제대로 출력됩니다. editBox를 선택해보도록 합니다.
Android와 iPhone에서 한글이 제대로 나오지 않는경우는 비주얼스튜디오를 사용하여 클레스를 생성한 경우입니다. 맥에서 Xcode를 이용할시 파일이 자동으로 UTF-8로 저장되기 때문에 Android와 iPhone에서 한글이 출력됩니다.
.cpp파일과 .h파일을 생성하는데 비주얼 스튜디오는 ANSI 형식으로 파일을 저장합니다. 이 파일 저장형식을 UTF-8로 수정하여야 합니다.
자세한 내용은 부록을 참고바랍니다.
Figure 56 실행화면
editBox를 선택하면 Input윈도우가 출력됩니다.
이 윈도우에 이름을 추가하도록 합니다.
Figure 57 이름입력
북극곰 이란 이름을 입력하였습니다. 이름을 추가한뒤 OK를 선택합니다.
Figure 58 실행화면
editBox에 이름이 입력되었습니다.
그럼 이름을 입력받고 랭킹팝업을 호출하도록 하겠습니다.
----------NamePopup.cpp----------
#include "NamePopup.h"
#include "RankingPopup.h"
…생략…
void NamePopup::onClickOK(Ref *object){
auto UserDefault = UserDefault::getInstance();
UserDefault->setStringForKey("name", _nameEdit->getText());
//flush() 해주어야 적용됩니다.
UserDefault->flush();
//랭킹팝업을 호출한다.
this->getParent()->addChild(RankingPopup::create());
this->removeFromParentAndCleanup(true);
}
_nameEdit에서 getText()를 이용하여 입력된 이름을 가져와 UserDefault에 저장해주었습니다.
이름을 입력받으면 랭킹팝업을 호출하도록 하였습니다.
여기서 이름이 공백일경우에 대한 예외처리를 추가하도록 하겠습니다.
----------NamePopup.cpp----------
…생략…
void NamePopup::onClickOK(Ref *object){
if (strcmp(_nameEdit->getText(), "") == 0){
//이름을 공백으로 입력받으면
MessageBox("이름을 입력하시기 바랍니다.", "경고");
return;
}
auto UserDefault = UserDefault::getInstance();
UserDefault->setStringForKey("name", _nameEdit->getText());
//flush() 해주어야 적용됩니다.
UserDefault->flush();
//랭킹팝업을 호출한다.
this->getParent()->addChild(RankingPopup::create());
this->removeFromParentAndCleanup(true);
}
이름이 공백일 경우에 대한 예외처리를 해주고 경고 메시지를 출력하도록 하였습니다.
MessageBox는 os의 컴포넌트들을 사용하는 것 입니다.
그럼 다음으로 이름이 제대로 저장되어 있는지 확인하기 위해 로그를 추가하도록 하겠습니다.
----------StartScene.cpp----------
…생략…
void StartScene::onClickRank(Ref *object){
log("onClickRank");
auto UserDefault = UserDefault::getInstance();
std::string name = UserDefault->getStringForKey("name", "");
log("name : %s", name.c_str());
if (name.compare("") == 0)
this->addChild(NamePopup::create(), 99);
else
this->addChild(RankingPopup::create(), 99);
}
디버거를 실행해보도록 합니다.
Figure 59 실행화면
공백인 상태에서 확인을 선택하면 경고 팝업이 나타납니다.
Figure 60 실행화면
이름을 입력하고 확인을 선택합니다.
Figure 61 실행화면
랭킹 팝업이 출력되었습니다.
랭킹 팝업을 닫고 다시한번 랭킹 팝업을 선택해 로그를 확인합니다.
로그를 확인해보고 입력한 이름이 출력되는지 확인합니다.
Figure 62 로그
name에 입력한 북극곰이 출력된 것을 확인할 수 있습니다.
이것으로 이름입력이 완료되었습니다.
점수를 서버로 보내는 시점은 랭킹보기를 선택했을 때 점수를 서버로 보내고 리스트를 받아오도록 합니다.
Figure 63 api 작성
먼저 xampp/htdocs/gameapi폴더로 이동합니다.
앞에서 게임연동 api를 위치하기위해 만들어놓은 폴더입니다.
Figure 64 api 작성
이곳에다 텍스트 문서하나를 새로 만들어놓습니다.
Figure 65 api 작성
이름을 ranking.php라고 수정합니다.
이 방금 생성한 ranking.php파일을 메모장이나 비주얼스튜디오로 열어 아래와 같이 입력해줍니다.
----------ranking.php----------
<?php
$connect = mysql_connect("localhost", "root", "");
//db가 있는 주소
mysql_select_db("polarbear"); //db선택
mysql_query("set names utf8"); //Utf8 지원
$name = $_REQUEST['name'];
$point = $_REQUEST['point'];
//해당 이름으로 등록된 스코어가 있는지 체크
$query = "SELECT count(*) FROM `tb_ranking` WHERE `NAME` LIKE $name;";
$result = mysql_query($query);
if($row = mysql_fetch_array($result)){
$count = (int)$row["count(*)"];
}
if($count > 0){
//해당 이름으로 등록된 스코어가 있으므로 update
$query = "UPDATE `polarbear`.`tb_ranking` SET `POINT` = $point WHERE `NAME` LIKE $name;";
mysql_query($query);
}
else{
//이름으로 등록된 스코어가 없으므로 insert
$query = " INSERT INTO `polarbear`.`tb_ranking` (`NAME`, `POINT`) VALUES ($name, $point);";
mysql_query($query);
}
//select 예시
$query = "SELECT * FROM `tb_ranking` ORDER BY `POINT` desc limit 10;";
$result = mysql_query($query);
$data = array();
while($row = mysql_fetch_array($result)){
$item = array();
$item["NAME"] = $row["NAME"];
$item["POINT"] = (int)$row["POINT"]; //int로 형변환 하여 배열에 넣어주었다.
$data[] = $item;
}
$json_encode_value = json_encode($data); //json 형식으로 변환하기
echo $json_encode_value; //출력
mysql_free_result($result); //result free
mysql_close($connect);
?>
위 소스는 name과 point를 파라메터로 받아 name으로 데이터베이스를 검색하고 이미 있는 이름일 경우 point를 비교하여 높은 점수를 update하고 없는 이름이면 추가해주는 php 페이지 입니다.
두번째 줄의
$connect = mysql_connect("localhost", "root", "");는 mysql의 접근 계정과 비밀번호입니다. 계정과 비밀번호를 설정하였으면 설정한 계정과 비밀번호를 입력하면 됩니다.
데이터를 처리한뒤 point로 정렬하여 point가 높은 row 10개를 json형식으로 출력해주는 구문입니다.
그럼 입력을 완료 했으면 저장을 하고 닫아줍니다.
Figure 66 api 작성
윈도우 익스플로러를 실행한 뒤 http://localhost/gameapi/ranking.php?name="aaa"&point=102 를 입력해봅니다.
위 url은 방금생성한 ranking.php파일을 호출하는데 name과 point를 파라메터로 추가해 호출해준 것입니다.
Figure 67 api 작성
[{"NAME":"aaa","POINT":102}] 라고 출력되었습니다.
위 와 같은 형식을 Json 형식이라고 하며 곧이어 좀더 알아보도록 하겠습니다.
위 내용은 aaa가 데이터베이스에 존재하지 않아 데이터베이스에 추가되었고 추가된 데이터베이스의 값이 출력된 것입니다.
phpmyadmin으로 들어가 방금입력한 값이 추가되었는지 확인합니다.
Figure 68 api 작성
값이 추가되어있습니다.
이번엔 point의 값을 변경하여 호출해보도록 합니다.
Figure 69 api 작성
http://localhost/gameapi/ranking.php?name="aaa"&point=159 라고 호출해보도록 하겠습니다.
Figure 70 api 작성
aaa의 POINT의 값이 변경되어 출력되었습니다.
기능이 정상적으로 동작되는 것을 확인했습니다.
이것으로 서버연동 api작성이 완료되었습니다.
테스트에 사용했던 데이터를 phpmyadmin으로 접속해 삭제 해두도록 합니다.
테이블의 모든데이터를 지우는 방법은 아래와 같습니다.
Figure 71 테이블 비우기
phpmyadmin에서 테이블을 선택한뒤 상단메뉴에서 테이블 작업을 선택합니다.
Figure 72 테이블 비우기
하단에 나타나는 메뉴중 데이터나 테이블 삭제 항목중에서 테이블 내용 비우기(TRUNCATE)를 선택하면 테이블의 구조는 놔둔채 데이터만 모두 삭제할 수 있습니다.
테이블 삭제를 선택하면 테이블 자체가 통째로 삭제되므로 주의하기 바랍니다.
먼저 통신을 연동하기 위해선 xampp가 설치된 pc의 내부ip주소를 알아야합니다.
현재 테스트를 위해서 내부 네트워크에서만 동작하도록 xampp를 설정하였습니다.
따라서 localhost란 것이 서버 자신의 ip를 말하는 것이므로 내부 네트워크에서 다른 디바이스를 이용해 접속하려면 내부 localhost가 아닌 내부 서버ip를 알아야합니다.
윈도우에서 디버거로 실행하는덴 문제없지만 같은 네트워크 상에서 다른 디바이스로 접속하려면 localhost로 입력하면 안됩니다.
따라서 xampp가 설치된 컴퓨터에서 비주얼스튜디오 디버거를 이용하여 접속하면 localhost로 접속해도 가능합니다. 같은 네트워크에서 다른 디바이스로 접속하기 위해선 xampp가 설치된 컴퓨터의 ip가 필요합니다.
컴퓨터의 내부 IP를 알아내고 IP주소를 고정시키도록 하겠습니다.
우리나라에서 많이 쓰고있는 공유기인 iptime 공유기를 기준으로 설명하도록 하겠습니다. 대부분 공유기는 같은 기능이 있으므로 본인이 다른 모델을 사용한다고 해도 같은 기능이 있으므로 진행하시기 바랍니다.
Figure 73 내부 ip 설정
먼저 브라우저를 이용하여 192.168.0.1으로 접속합니다.
iptime 공유기의 환경설정 하는곳입니다.
관리도구를 선택합니다.
Figure 74 내부 ip 설정
이곳에서 자신의 네트워크 관련된 정보를 확인 할 수 있습니다.
여기서 참고할 부분은 내부 네트워크 정보쪽입니다.
동적 IP 할당 범위를 보면 해당 범위내 아이피를 자동으로 할당합니다.
pc도 ip를 할당받을경우 ip주소가 바뀌므로 내부 ip를 고정시켜 다른단말에서 ip로 접속할 수 있도록 하겠습니다.
좌측에서 고급 설정 – 네트워크 관리 – 내부 네트워크 설정으로 접속합니다.
Figure 75 내부 ip 설정
내부 네트워크에 관련된 정보가 나와있습니다. 하단으로 내려보면 수동 IP 할당 설정 부분이 있습니다.
검색된 IP/MAC 주소 쪽을보면 현재 접속된 PC의 IP주소와 맥어드레스가 나옵니다.
이 PC의 내부 IP주소를 수동으로 할당하게 해서 고정시켜버리도록 하겠습니다.
현재 접속된 PC좌측의 네모를 선택한뒤 상단에 추가를 선택합니다.
그럼 좌측의 설정된 IP/MAC 주소 쪽으로 이동됩니다.
이것으로 PC의 내부 IP주소를 수동으로 할당하게 해서 고정시켜버렸습니다.
이 PC의 주소를 잘기억하시기바랍니다. 저는 192.168.0.2로 되어있습니다. 네트워크에 연결된 기기에 따라 주소가 다를 수 있습니다.
그럼이제 이 IP주소로 클라이언트를 연결시켜보도록 하겠습니다.
RankingPopup.h와 를 아래와 같이 수정합니다.
----------RankignPopup.h----------
#include "cocos2d.h"
#include "network/HttpClient.h"
USING_NS_CC;
using namespace cocos2d::network;
class RankingPopup :public Layer
{
public:
static RankingPopup * create();
bool init();
virtual void onEnter();
bool onTouchBegan(Touch* touch, Event* event);
void onClickOk(Ref *object);
void onHttpRequestCompleted(HttpClient *sender, HttpResponse *response);
void jsonParse(std::string jsonText);
LabelTTF *_loading;
};
----------RankignPopup.cpp----------
#include "RankingPopup.h"
#include "json\document.h"
…생략…
bool RankingPopup::init(){
//여기에 팝업을 작성한다.
//Device의 크기를 가져옵니다.
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_rank.png");
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(back);
auto okMenu = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(RankingPopup::onClickOk, this));
okMenu->setPosition(Point(back->getContentSize().width / 2, 10));
auto menu = Menu::create(okMenu, NULL);
menu->setPosition(Point::ZERO);
back->addChild(menu);
//로딩중 텍스트 출력
_loading = LabelTTF::create("loading...", "Arial", 30);
_loading->setColor(Color3B::YELLOW);
_loading->setPosition(Point(back->getContentSize().width / 2, back->getContentSize().height / 2));
back->addChild(_loading);
//데이터 호출
auto UserDefault = UserDefault::sharedUserDefault();
std::string name = UserDefault->getStringForKey("name", "");
int point1 = UserDefault->getIntegerForKey("point1", 0); //스테이지 1의 점수
int point2 = UserDefault->getIntegerForKey("point2", 0); //스테이지 2의 점수
int point3 = UserDefault->getIntegerForKey("point3", 0); //스테이지 3의 점수
char url[255];
//ip주소는 xampp가 설치된 pc의 내부 ip 주소를 입력합니다.
sprintf(url, "http://192.168.0.2/gameapi/ranking.php?name=\"%s\"&point=%d", name.c_str(), point1 + point2 + point3);
HttpRequest* request = new HttpRequest();
// required fields
request->setUrl(url);
request->setRequestType(HttpRequest::Type::GET);
request->setResponseCallback(this, httpresponse_selector(RankingPopup::onHttpRequestCompleted));
// optional fields
request->setTag("sendData");
HttpClient::getInstance()->send(request);
// don't forget to release it, pair to new
request->release();
return true;
}
…생략…
void RankingPopup::onHttpRequestCompleted(HttpClient *sender, HttpResponse *response)
{
if (!response)
{
return;
}
// You can get original request type from: response->request->reqType
if (0 != strlen(response->getHttpRequest()->getTag()))
{
log("%s completed", response->getHttpRequest()->getTag());
}
int statusCode = response->getResponseCode();
char statusString[64] = {};
sprintf(statusString, "HTTP Status Code: %d, tag = %s", statusCode, response->getHttpRequest()->getTag());
log("response code: %d", statusCode);
if (!response->isSucceed())
{
log("response failed");
log("error buffer: %s", response->getErrorBuffer());
return;
}
// dump data
std::vector<char> *buffer = response->getResponseData();
std::string text(buffer->begin(), buffer->end());
log("%s", text.c_str());
jsonParse(text);
}
void RankingPopup::jsonParse(std::string jsonText){
_loading->removeFromParentAndCleanup(true); //로딩중 텍스트 제거
rapidjson::Document document;
//json 파싱
//에러 검사 파일의 끝이 있는지와 Json형식이 잘되어 있는지를 확인 합니다.
if (jsonText.c_str() != NULL && !document.Parse<2>(jsonText.c_str()).HasParseError())
{
//데이터 파싱에 성공
rapidjson::Value& root = document;
for (int i = 0; i < root.Size(); i++){
std::string Name = root[i]["NAME"].GetString();
int point = root[i]["POINT"].GetInt();
log("%s : %d", Name.c_str(), point);
}
}
else{
//데이터 파싱에 실패
MessageBox("network error", "error");
this->removeFromParentAndCleanup(true);
}
}
로딩중 라벨을 추가하였고 ip주소를 이용하여 ranking.php를 호출하도록 하였습니다.
이름과 점수를 파라메터로 추가하여 호출해주었습니다.
(IP주소는 본인의 서버PC의 내부 IP주소를 입력해야합니다.)
이전 장에서 서버연동 api를 작성하여 출력된 데이터를 Json형식으로 출력하였습니다.
Cocos2d-x 3.4 버전엔 rapidjson이 포함되어 있습니다. json형식을 클라이언트에서 사용하기 쉽게 가져올 수 있는 라이브러리 입니다.
json은 쉽게 예기해서 배열과 비슷합니다.
[ ]는 jsonArray라고 불리며 배열을 담습니다.
{ }는 JsonObject라고 불리며 하나의 객체를 담습니다.
반복되는 데이터는 JsonArray에 담아 리스트처럼 사용하고 하나하나의 객체는 JsonObject에 담아 가져와 사용합니다.
지금 사용하는 형식은 JsonArray안에 JsonObject가 들어있는 형태로 api를 작성하였으므로 참고하시기 바랍니다.
점수를 저장하는 부분은 아직 작성이 안되어있으므로 0으로 출력될 것입니다.
콜백으로 onHttpRequestCompleted()을 등록하였고 출력된 텍스트를 jsonParse()에서 파싱 하였습니다.
파싱된 데이터는 먼저 로그로 출력하도록 하였습니다.
디버거를 실행해 확인하도록 합니다.
Figure 76 api 연동
로딩이란 라벨이 출력됩니다.
Figure 77 api 연동
데이터 파싱이 완료되면 로딩이란 라벨이 사라집니다.
Figure 78 로그
이때 로그를 살펴보면 서버와 연동이 제대로 된 것을 확인할 수 있습니다.
서버에서 출력되는 텍스트와 이름과 점수를 분리해놓은 로그를 확인 할 수 있습니다.
phpmyadmin으로 접속해 데이터베이스에 해당값이 제대로 추가되어있는지 확인합니다.
Figure 79 api 연동
제대로 추가되어있습니다.
테스트를 위해 서로다른 값을 9개 더 추가해놓습니다.
Figure 80 api 연동
상단메뉴에서 삽입을 선택하고 이름과 점수를 다르게 입력하고 실행을 선택합니다.
Figure 81 api 연동
북극곰은 디버거에서 사용하는 이름이고 나머지 9개는 임시 데이터를 추가해놓았습니다.
이상태에서 디버거에서 랭킹보기를 다시 선택하고 로그를 살펴봅니다.
Figure 82 api 연동 로그
점수순으로 정렬되어 10개의 데이터 모두 제대로 연동된 것을 확인할 수 있습니다.
이제 그럼 점수를 연동하기 위해 각 스테이지별 점수를 저장해보도록 하겠습니다.
GameScene.cpp파일을 오픈하고 polarBearAnimationFinish()를 아래와 같이 수정합니다.
----------GameScene.cpp----------
…생략…
void GameScene::polarbearAnimationFinish(){
log("polarbearAnimationFinish");
//tag를 분해하여 col과 row를 구한다.
int col = (_polarbearCurrentTag - 1) % 5;
int row = (_polarbearCurrentTag - 1 - col) / 5;
log("col = %d, row = %d", col, row);
int type = getTileType(col, row);
log("type = %d", type);
switch (type)
{
case 0: //일반 빙하
_score = _score + TILE;
break;
case 2: //목적지 도착
_score = _score + TILE;
stopTimer();
//남은점수에 비례하여 GOAL_BONUS를 추가해주었습니다.
_score = _score + ((_countdownTimer / TIME) * GOAL_BONUS);
{
auto UserDefault = UserDefault::getInstance();
if (_stage == 1)
{
UserDefault->setBoolForKey("lock2", false);
UserDefault->setIntegerForKey("point1", _score);
}
else if (_stage == 2)
{
UserDefault->setBoolForKey("lock3", false);
UserDefault->setIntegerForKey("point2", _score);
}
else if (_stage == 3)
{
UserDefault->setIntegerForKey("point3", _score);
}
//호출해주어야 값이 바뀐다.
UserDefault->flush();
}
this->addChild(ResultPopup::create(_score, _rescueCount), 99);
break;
ResultPopup을 호출하는 부분에서 _score를 스테이지에 맞추어서 저장해주었습니다.
디버거를 실행하여 각 스테이지별로 게임을 클리어한 뒤 랭킹보기를 선택하고 점수가 연동되었는지 확인합니다.
Figure 83 실행화면
3개 스테이지를 클리어한 점수입니다. 3개 스테이지를 클리어한 뒤 랭킹보기를 선택하여 로그를 살펴봅니다.
Figure 84 로그
3개의 스테이지를 클리어한 점수가 북극곰이란 유저에게 적용된 것을 확인할 수 있습니다.
서버연동도 끝났고 점수 저장도 끝났으니 서버에서 출력된 값을 화면에 출력해주도록 하겠습니다.
RankingPopup.h와 RankingPopup.cpp를 아래와 같이 수정합니다.
----------RankingPopup.h----------
#include "cocos2d.h"
#include "network/HttpClient.h"
USING_NS_CC;
using namespace cocos2d::network;
class RankingPopup :public Layer
{
public:
static RankingPopup * create();
bool init();
virtual void onEnter();
bool onTouchBegan(Touch* touch, Event* event);
void onClickOk(Ref *object);
void onHttpRequestCompleted(HttpClient *sender, HttpResponse *response);
void jsonParse(std::string jsonText);
LabelTTF *_loading;
Sprite *_back;
};
----------RankingPopup.cpp----------
…생략…
bool RankingPopup::init(){
//여기에 팝업을 작성한다.
//Device의 크기를 가져옵니다.
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_rank.png");
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
this->addChild(back);
auto okMenu = MenuItemImage::create("btn_ok.png", "btn_ok_on.png", CC_CALLBACK_1(RankingPopup::onClickOk, this));
okMenu->setPosition(Point(back->getContentSize().width / 2, 10));
auto menu = Menu::create(okMenu, NULL);
menu->setPosition(Point::ZERO);
back->addChild(menu);
//로딩중 텍스트 출력
_loading = LabelTTF::create("loading...", "Arial", 30);
_loading->setColor(Color3B::YELLOW);
_loading->setPosition(Point(back->getContentSize().width / 2, back->getContentSize().height / 2));
back->addChild(_loading);
//데이터 호출
auto UserDefault = UserDefault::sharedUserDefault();
std::string name = UserDefault->getStringForKey("name", "");
int point1 = UserDefault->getIntegerForKey("point1", 0); //스테이지 1의 점수
int point2 = UserDefault->getIntegerForKey("point2", 0); //스테이지 2의 점수
int point3 = UserDefault->getIntegerForKey("point3", 0); //스테이지 3의 점수
char url[255];
//ip주소는 xampp가 설치된 pc의 내부 ip 주소를 입력합니다.
sprintf(url, "http://192.168.0.2/gameapi/ranking.php?name=\"%s\"&point=%d", name.c_str(), point1 + point2 + point3);
HttpRequest* request = new HttpRequest();
// required fields
request->setUrl(url);
request->setRequestType(HttpRequest::Type::GET);
request->setResponseCallback(this, httpresponse_selector(RankingPopup::onHttpRequestCompleted));
// optional fields
request->setTag("sendData");
HttpClient::getInstance()->send(request);
// don't forget to release it, pair to new
request->release();
//전역변수에 back를 받는다.
_back = back;
return true;
}
…생략…
void RankingPopup::jsonParse(std::string jsonText){
_loading->removeFromParentAndCleanup(true); //로딩중 텍스트 제거
rapidjson::Document document;
//json 파싱
//에러 검사 파일의 끝이 있는지와 Json형식이 잘되어 있는지를 확인 합니다.
if (jsonText.c_str() != NULL && !document.Parse<2>(jsonText.c_str()).HasParseError())
{
//데이터 파싱에 성공
rapidjson::Value& root = document;
for (int i = 0; i < root.Size(); i++){
std::string Name = root[i]["NAME"].GetString();
int point = root[i]["POINT"].GetInt();
log("%s : %d", Name.c_str(), point);
//랭킹 추가
char rankChar[255];
sprintf(rankChar, "%d.", i + 1);
auto labelRanking = LabelTTF::create(rankChar, "Arial", 12);
labelRanking->setColor(Color3B::BLACK);
labelRanking->setPosition(Point(22, (_back->getContentSize().height - 50) - i * 22));
_back->addChild(labelRanking);
//닉네임 추가
char nickChar[255];
sprintf(nickChar, "%s", Name.c_str());
auto labelNick = LabelTTF::create(nickChar, "arial", 14);
labelNick->setColor(Color3B::BLACK);
labelNick->setPosition(Point(90, (_back->getContentSize().height - 50) - i * 22));
_back->addChild(labelNick);
//점수 추가
char pointChar[255];
sprintf(pointChar, "%d", point);
auto labelPoint = LabelTTF::create(pointChar, "Arial", 10);
labelPoint->setColor(Color3B::BLACK);
labelPoint->setPosition(Point(180, (_back->getContentSize().height - 50) - i * 22));
_back->addChild(labelPoint);
}
}
else{
//데이터 파싱에 실패
MessageBox("network error", "error");
this->removeFromParentAndCleanup(true);
}
}
_back을 전역변수로 선언한 뒤 back을 받아넣었습니다. LabelTTF를 이용해 순위와 이름, 점수를 표시한뒤 _back에 추가해주었습니다.
순위는 출력되는 순서대로 1등부터 10등이기 때문에 i값에 +1하여 출력해주었습니다.
디버거를 실행하여 랭킹이 출력되는지 확인하도록 합니다.
Figure 85 실행화면
10명의 유저의 점수가 모두 출력되는 것을 확인하였습니다.
이것으로 게임의 모든 기능구현이 완료되었습니다.
이렇게 게임 하나를 완성하기 위한 여러가지 테크닉을 익혀보았습니다. 스프라이트와 액션을 사용하여 더욱 재미있는 게임을 만들어 볼 수 있습니다. 지금까지 배운 여러가지 클레스를 사용하여 자신만의 재미있는 게임을 만드는데 도움이 되었으면 좋겠습니다.