AppWarpとCubismSDKを使って応援アプリっぽいものを作ってみた

はじめまして、初Advent Calendar投稿の@mettoboshiです。
12日-13日にかけて、@blankblankさんがPusherとParseを使ったmBaaSをつかったアプリの分かりやすい解説がありましたが、私はAppWarpとCubismSDKを使って同じく通信系のアプリを作ってみようと思います。

## 何を作るの？
誰かが「応援」ボタンを押したらアプリを入れている人全員をミクさんが応援してくれるというアプリです。AppWarp(1.6.1)とCubismSDK(2.0.1)を使ってみました。
Cocos2d-xは3.3rc2です。(多分)

## AppWarpって？

[AppWarp](http://appwarp.shephertz.com/)は[PhotonCloud](http://photoncloud.jp/)と同じようなリアルタイムゲームを作るためのサービスです。AppWarpにはcocos2d-x対応のSDKがあり簡単に使用することが可能なので、これを使ってみようと思います。

## CubisumSDKって？

[CubismSDK](http://www.live2d.com/products/cubism_sdk)はLive2Dが出しているツールで、2Dの画像を使って、あたかも3Dっぽく動かせるツールです。コレさえあれば、3Dモデリングがなくても3Dっぽいゲームが作れる優れものです。小規模事業者であれば無料で使えます。私は2Dの画像も作れないのですが[サンプル](http://sites.cybernoids.jp/cubism_e/samples)があるので今回はコレを使います。

## 実装

### CubismSDKに関する準備

まずはCubismSDKから。公式サイトに[チュートリアル](http://sites.cybernoids.jp/cubism2/sdk_tutorial/platform-setting/cocos2d-x/ios/createproject)があるのでコレ通りやるとだいたい上手くいきます。ただ、12/10時点で、アニメーションさせる方法についてはLive2Dライブラリ - [モーションについて](http://sites.cybernoids.jp/cubism2/sdk_tutorial/live2d_library/load_to_play_motion/set_motion)を参考に実装を進めて行きます。(ダウンロードしたSDKの中の、sample_cocos2dx3.2 - SampleApp1のソースの方が詳しいかも)

とりあえず、ダウンロードしたSDK(2.0.04_1)を解凍しlibとincludeをプロジェクト直下にコピーし、Resoucesにmiku.moc, miku.model.json, miku.physics.json, motions/xx.mtn, texture_00.pngを追加しておきます。SDKについているframewarkフォルダもプロジェクトに追加しておきました。

次にビルドセッティングで、以下の4つを設定しておきます。


* Search Paths - User Header Search Paths | ../include
* Apple LLVM 5.1 - Preprocessing -  | L2D_TARGET_IPHONE_ES2
* Search Paths - Library Search Paths | "$(SRCROOT)/../lib/ios/$(CONFIGURATION)-$(PLATFORM_NAME)"
* Linking - Other Linker Flags | -lLive2D



### AppWarpに関する準備

AppWarpは公式サイトの[チュートリアル](http://blogs.shephertz.com/2013/08/19/cocos2d-x-realtime-fighting-game-tutorial/)がとても分かりやすいので、それを見ながらやってみましょう。
AppWarpSDK(1.6.1)のAppWarpX_3.0をプロジェクトにコピーしておきます。Build Settingに以下を追加しておきます。

* PROJECT - Build Settings - Header Search Paths | $(SRCROOT)/../cocos2d/external/curl/include/ios


#### AppWarp管理画面

とりあえず、AppWarpのユーザ登録を実施(カード不要！)し、管理画面でAppとRoomを作っておきます。

### cocos2d-x側の実装

今回はボタンを用意しておいて、ボタンを押したらAppWarp側にボタンを押したことを通知するようにします。
AppWarp側から通知があった場合は、CubisumSDKでアニメーションを実行するようにしていきます。


AppDelegate.cppはLive2d用の設定を追加

``` AppDelegate.cpp

#include "AppDelegate.h"
#include "HelloWorldScene.h"
#include "Live2D.h" //add

using namespace live2d; //add

USING_NS_CC;

AppDelegate::AppDelegate() {

}

AppDelegate::~AppDelegate() 
{
  Live2D::dispose();
}

bool AppDelegate::applicationDidFinishLaunching() {
  // initialize director
  auto director = Director::getInstance();
  auto glview = director->getOpenGLView();
  if(!glview) {
      glview = GLView::create("My Game");
      director->setOpenGLView(glview);
  }

  // turn on display FPS
  director->setDisplayStats(true);

  // set FPS. the default value is 1.0/60 if you don't call this
  director->setAnimationInterval(1.0 / 60);

  //Live2D
  Live2D::init();
  
  // create a scene. it's an autorelease object
  auto scene = HelloWorld::createScene();

  // run
  director->runWithScene(scene);

  return true;
}

// This function will be called when the app is inactive. When comes a phone call,it's be invoked too
void AppDelegate::applicationDidEnterBackground() {
  Director::getInstance()->stopAnimation();

  // if you use SimpleAudioEngine, it must be pause
  // SimpleAudioEngine::getInstance()->pauseBackgroundMusic();
}

// this function will be called when the app is active again
void AppDelegate::applicationWillEnterForeground() {
  Director::getInstance()->startAnimation();

  // if you use SimpleAudioEngine, it must resume here
  // SimpleAudioEngine::getInstance()->resumeBackgroundMusic();
}

```


SampleLive2DSprite.hとcppはCubismSDKについているサンプルを少しだけ変更してあります。

```SampleLive2DSprite.h
#ifndef __sampleCocos2dx__SampleLive2DSprite__
#define __sampleCocos2dx__SampleLive2DSprite__

#include "cocos2d.h"
#include "2d/CCSprite.h"
#include <vector>
#include "Live2DModelOpenGL.h"
#include "motion/Live2DMotion.h"
#include "motion/EyeBlinkMotion.h"
#include "L2DBaseModel.h"

//class SampleLive2DSprite :public cocos2d::Sprite, public live2d::framework::L2DBaseModel {
class SampleLive2DSprite :public cocos2d::Sprite {
  
  live2d::Live2DModelOpenGL* live2DModel ;
  live2d::MotionQueueManager* motionmanager;
  std::vector<cocos2d::Texture2D*> textures ;
  cocos2d::Texture2D* texture1;
  //  std::vector<live2d::Live2DMotion*> motions;
  std::map<std::string, live2d::Live2DMotion*> motions;
  live2d::EyeBlinkMotion *eyeBlink;
  
  
public:
  SampleLive2DSprite();
  virtual ~SampleLive2DSprite();
  void loadMotion(std::vector<std::string> paths);
  void startMotion(std::string motionName);
  virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t transformUpdated) override;
  void onDraw(const cocos2d::Mat4 &transform, bool transformUpdated);
  
protected:
  cocos2d::CustomCommand _customCommand;
};

#endif /* defined(__sampleCocos2dx__SampleLive2DSprite__) */
```

SampleLive2DSpriteはサンプルだと、表示して首を動かすような作りになっているのですが、首振りを削除して、loadMotionとstartMotionを追加しています。また、miku.mocを読み込んで表示するように変更してあります。

```SampleLive2DSprite.cpp

#include "SampleLive2DSprite.h"
#include "util/UtSystem.h"

#include "graphics/DrawProfileCocos2D.h"
#include "platform/CCFileUtils.h"
#include "L2DBaseModel.h"
#include "IPlatformManager.h"
#include "Live2DFramework.h"

#include <regex>

using namespace live2d;
using namespace live2d::framework;

USING_NS_CC;

SampleLive2DSprite::SampleLive2DSprite()
{
  //Live2D Sample
  const char* MODEL = "miku.moc" ;
  const char* TEXTURES[] ={
    "texture_00.png",
    NULL
  } ;
  unsigned char* buf;
  ssize_t bufSize;
  buf = FileUtils::getInstance()->getFileData(MODEL,"rb", &bufSize);
  
  live2DModel = Live2DModelOpenGL::loadModel( buf,bufSize ) ;
  
  for( int i = 0 ; TEXTURES[i] != NULL ; i++ ){
    Texture2D *texture = new Texture2D();
    Image *img = new Image();
    
    img->initWithImageFile(TEXTURES[i]);
    texture->initWithImage(img);
    
    
    Texture2D::TexParams texParams = { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE };
    texture->setTexParameters(texParams);
    texture->generateMipmap();
    
    int glTexNo = texture->getName() ;
    
    
    live2DModel->setTexture( i , glTexNo ) ;
    textures.push_back( texture ) ;
  }
  
  float w = Director::getInstance()->getWinSize().width;
  float h = Director::getInstance()->getWinSize().height;
  float scx = 2.0 / (live2DModel->getCanvasWidth());
  float scy = -2.0 / live2DModel->getCanvasWidth() * (w/h);
  float x = -1.0 ;
  float y = 0.5;
  float matrix []= {
    scx , 0 , 0 , 0 ,
    0 , scy , 0 , 0 ,
    0 , 0 , 1 , 0 ,
    x , y , 0 , 1
  } ;
  
  live2DModel->setMatrix(matrix) ;
  live2DModel->setPremultipliedAlpha( true );
  
  eyeBlink=new EyeBlinkMotion();
  
  std::vector<std::string> paths;
  paths.push_back("miku_idle_01.mtn");
  paths.push_back("miku_m_01.mtn");
  paths.push_back("miku_m_02.mtn");
  paths.push_back("miku_m_03.mtn");
  paths.push_back("miku_m_04.mtn");
  paths.push_back("miku_m_05.mtn");
  paths.push_back("miku_m_06.mtn");
  
  loadMotion(paths);
  
  motionmanager = new MotionQueueManager();
  motionmanager->startMotion(motions["miku_idle_01.mtn"], false);
  //  motionmanager->startMotion(motions["miku_m_01.mtn"], false);
  
}

SampleLive2DSprite::~SampleLive2DSprite()
{
  delete live2DModel;
  delete motionmanager;
  
  for (int i=0; i<textures.size(); i++)
  {
    textures[i]->release();
  }
}

void SampleLive2DSprite::draw(cocos2d::Renderer *renderer, const cocos2d::Mat4 &transform, uint32_t transformUpdated)
{
  Sprite::draw(renderer, transform, transformUpdated);
  
  _customCommand.init(_globalZOrder);
  _customCommand.func = CC_CALLBACK_0(SampleLive2DSprite::onDraw, this, transform, transformUpdated);
  renderer->addCommand(&_customCommand);
}

void SampleLive2DSprite::onDraw(const cocos2d::Mat4 &transform, bool transformUpdated)
{
  kmGLPushMatrix();
  kmGLLoadMatrix(&transform);
  
  live2d::DrawProfileCocos2D::preDraw();
  
  eyeBlink->setParam(live2DModel);
  motionmanager->updateParam(live2DModel);
  
  // trueだと終わってる
  if(motionmanager->isFinished()) {
    this->startMotion("miku_idle_01.mtn");
  }
  
  live2DModel->update() ;
  live2DModel->draw() ;
  
  live2d::DrawProfileCocos2D::postDraw() ;
  
  kmGLPopMatrix();
}

void SampleLive2DSprite::loadMotion(std::vector<std::string> paths) {
  
  FileUtils *fileUtils = FileUtils::getInstance();
  
  std::regex pattern(".*idle.*");
  std::smatch sm;
  
  //  paths.pop_back();
  for (auto data : paths) {
    std::string filePath = fileUtils->fullPathForFilename(data.c_str());
    ssize_t nSize = 0;
    unsigned char* pBuffer = fileUtils->getFileData(filePath.c_str(), "r", &nSize);
    log("%s",pBuffer);
    
    Live2DMotion *motion = Live2DMotion::loadMotion(pBuffer, nSize);
    delete pBuffer;
    
    motion->setFadeIn( 1000 );//フェードインの時間を1000msに設定
    motion->setFadeOut( 1000 );//フェードアウトの時間を1000msに設定
    
    if(std::regex_match(data, sm, pattern)) {
      motion->setLoop( true );//ループ再生を行う
    }
    motions[data.c_str()] = motion; 
  }  
}

void SampleLive2DSprite::startMotion(std::string motionName) {
  motionmanager->startMotion(motions[motionName], false);
}
```


HelloWorldSceneでは、AppWarpへの接続と、データの送受信を行います。
ヘッダファイルに、APPWARP_APP_KEY, APPWARP_SECRET_KEY, ROOM_IDを定義しておき、管理画面で取得した値を入れておきます。_talk, _labelは吹き出し、pLive2DSpriteはLive2d用モデルです。
genRandomはAppWarp用のユーザを適当に生成する関数(公式サイトのそのまま)、callButtonは「がんばれ」ボタン押下時に呼ばれます。あとの関数は、AppWarp用の関数です。

``` HelloWorldScene.h

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"
#include <extensions/cocos-ext.h>
#include "network/HttpClient.h"
#include "SampleLive2DSprite.h"
#include "ui/CocosGUI.h"

#include "appwarp.h"

// add
#define APPWARP_APP_KEY     "xxxxx"
#define APPWARP_SECRET_KEY  "xxxxx"
#define ROOM_ID             "xxxxx"

using namespace cocos2d;
USING_NS_CC_EXT;

class HelloWorld : public cocos2d::Layer, public AppWarp::ConnectionRequestListener, public AppWarp::RoomRequestListener, public AppWarp::NotificationListener, public AppWarp::ZoneRequestListener
{
public:
  // there's no 'id' in cpp, so we recommend returning the class instance pointer
  static cocos2d::Scene* createScene();

  // cubismSDK
  SampleLive2DSprite* pLive2DSprite;
  Scale9Sprite* _talk;
  Label* _label;
  
  // AppWarp
  std::string userName;
  std::string genRandom();
  void onConnectDone(int res);
  void onJoinRoomDone(AppWarp::room revent);
  void onSubscribeRoomDone(AppWarp::room revent);
  virtual void sendChat(std::string str);
  void onChatReceived(AppWarp::chat chatevent);
  
  
  // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
  virtual bool init();
   
  void callButton(Ref *pSender, ui::Widget::TouchEventType type);
  
  // implement the "static create()" method manually
  CREATE_FUNC(HelloWorld);
};

#endif // __HELLOWORLD_SCENE_H__
```


``` HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "SampleLive2DSprite.h" //追加
#include "ui/CocosGUI.h"

USING_NS_CC;

Scene* HelloWorld::createScene()
{
  // 'scene' is an autorelease object
  auto scene = Scene::create();
  
  // 'layer' is an autorelease object
  auto layer = HelloWorld::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 HelloWorld::init()
{
  //////////////////////////////
  // 1. super init first
  if ( !Layer::init() )
  {
      return false;
  }

  Size visibleSize = Director::getInstance()->getVisibleSize();
  Vec2 origin = Director::getInstance()->getVisibleOrigin();

  pLive2DSprite = new SampleLive2DSprite();
  this->addChild(pLive2DSprite);

  //ボタンの作成
  auto fightButton = ui::Button::create("fightButton.png");
  fightButton->setPosition(Vec2(visibleSize.width * 0.5, visibleSize.height * 0.2));
  fightButton->setTag(1);
  fightButton->setScale9Enabled(true);

  this->addChild(fightButton, 5);

  fightButton->addTouchEventListener(CC_CALLBACK_2(HelloWorld::callButton, this));

  
  //吹き出しの作成
  _talk = Scale9Sprite::create("sprite9.png");
  _talk->setPreferredSize(cocos2d::Size(Vec2(visibleSize.width, 200)));
  _talk->setPosition(visibleSize.width / 2, visibleSize.height - _talk->getContentSize().height / 2);
  _talk->setVisible(false);
  _talk->setTag(2);
  this->addChild(_talk);
  
  //talk label
  _label = Label::createWithSystemFont("", "ariel", 36);
  _label->setDimensions(visibleSize.width - 20, 200);
  _label->setPosition(Vec2(visibleSize.width / 2, visibleSize.height - _label->getDimensions().height / 2 - 20));
  _label->setColor(Color3B(100, 100, 100));
  _label->setTag(5);
  
  log("xefefeewfe %f, %f", _label->getPosition().x, _label->getPosition().y);
  this->addChild(_label);
  
  AppWarp::Client *warpClientRef;
  AppWarp::Client::initialize(APPWARP_APP_KEY,APPWARP_SECRET_KEY);
  warpClientRef = AppWarp::Client::getInstance();
  warpClientRef->setConnectionRequestListener(this);
  warpClientRef->setNotificationListener(this);
  warpClientRef->setRoomRequestListener(this);
  warpClientRef->setZoneRequestListener(this);
  
  userName = genRandom();
  warpClientRef->connect(userName);
  
  
  return true;
}

void HelloWorld::callButton(Ref *pSender, ui::Widget::TouchEventType type)
{
  if(type == ui::Widget::TouchEventType::BEGAN) {
    std::string sendData = "がんばれ！！";
    if (sendData != "") {
      AppWarp::Client *warpClientRef;
      warpClientRef = AppWarp::Client::getInstance();
      _talk->setVisible(false);
      _label->setString("");
      
      warpClientRef->sendChat(sendData);
    }
  }
  
  return;
}

std::string HelloWorld::genRandom()
{
  std::string charStr;
  srand (time(NULL));
  
  for (int i = 0; i < 10; ++i) {
    charStr += (char)(65+(rand() % (26)));
  }
  
  return charStr;
}


void HelloWorld::onConnectDone(int res)
{
  if (res==0)
  {
    printf("\nonConnectDone .. SUCCESS\n");
    AppWarp::Client *warpClientRef;
    warpClientRef = AppWarp::Client::getInstance();
    warpClientRef->joinRoom(ROOM_ID);
  }
  else
    printf("\nonConnectDone .. FAILED\n");
  
}

void HelloWorld::onJoinRoomDone(AppWarp::room revent)
{
  if (revent.result==0)
  {
    printf("\nonJoinRoomDone .. SUCCESS\n");
    AppWarp::Client *warpClientRef;
    warpClientRef = AppWarp::Client::getInstance();
    warpClientRef->subscribeRoom(ROOM_ID);
    
  }
  else
    printf("\nonJoinRoomDone .. FAILED\n");
}

void HelloWorld::onSubscribeRoomDone(AppWarp::room revent)
{
  if (revent.result==0)
  {
    printf("\nonSubscribeRoomDone .. SUCCESS\n");
  }
  else
    printf("\nonSubscribeRoomDone .. FAILED\n");
}

void HelloWorld::sendChat(std::string str)
{  
}

void HelloWorld::onChatReceived(AppWarp::chat chatevent)
{
  
  std::string chatData;
  chatData = chatevent.chat.c_str();
  _talk->setVisible(true);
  _label->setString(chatData);
  pLive2DSprite->startMotion("miku_m_04.mtn");
  printf("onChatReceived..");
  printf("%s : %s ", chatevent.sender.c_str(), chatevent.chat.c_str());
}
```

## 動作確認

![kansei.png](https://qiita-image-store.s3.amazonaws.com/0/42925/b34544b1-9efc-ecfc-cc69-13ce84d8b486.png)

「がんばれ」ボタンを押下すると、ミクさんが応援してくれます。
また、同じアプリが入っている別端末で誰かが「がんばれ」ボタンを押した場合も、
応援してもらえます。


## おわりに

諸々の都合でAndroidでの動作確認はできませんでした。。
Cocos2d-xに他のツールやサービスを組み合わせると簡単にアプリが作れてよいですね！

明日は、@ikemonnさんのlambdaに関する記事です。Cocos2d-xはC++11が使えるのでlambdaを颯爽とつかえるとかっこ良いですしね。楽しみです。