LoginSignup
3
1

More than 5 years have passed since last update.

Cocos2d-xでiBeacon発信機作ってみた

Posted at

iBeaconを使ってやることがあったので、Cocos2d-xを使ってiOS/Android向けのiBeacon発信機を作ってみました。

環境

  • Cocos2d-x 3.15.1
  • XCode Version 9.0 beta 2
  • Android Studio 2.3

プロジェクト作成

cocosコマンドを使ってプロジェクトを生成します。
Cocos2d-xの環境構築は他所のサイトを参考にしてください。
Macのターミナルを起動してコマンドを入力します。

cocos new BeaconTest -p jp.co.ienter.BeaconTest -l cpp

生成されたBeaconTest.xcodeprojを起動します。

s100.png

iOSライブラリ追加

iOSで必要なライブラリを追加します。

  • CoreBluetooth.framework
  • CoreLocation.framework

s101.png

AltBeacon導入

Androidで必要なライブラリを追加します。

  • AltBeacon

build.gradle(Module: BeaconTest)に以下を記述します。
jcenter()compile 'org.altbeacon:android-beacon-library:2+'を記述します。

repositories {
    jcenter()
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':libcocos2dx')
    compile 'org.altbeacon:android-beacon-library:2+'
}

s102.png

データ保持プログラムを実装する

UUIDを端末内に保存するためのプログラムを実装します。
下記ファイルを新規作成してください。

  • GameDataDM.cpp
  • GameDataDM.hpp
  • GameSettings.cpp
  • GameSettings.hpp

GameDataDMはplist形式で端末内にデータを保存します。

GameDataDM.hpp
#ifndef GameDataDM_hpp
#define GameDataDM_hpp

#include "cocos2d.h"

USING_NS_CC;

class GameDataDM;

using GameDataDMCallback = std::function<void (const bool &)>;

class GameDataDM : public cocos2d::Node
{
public:
    GameDataDM();
    virtual ~GameDataDM();
    virtual bool init();
    CREATE_FUNC(GameDataDM);

    ValueMap LoadBundlePlist(const char* pListName);
    void writeSaveFile(ValueMap data, GameDataDMCallback callback);

    CC_SYNTHESIZE(ValueMap, _dicItems, DicItem);    
};
GameDataDM.cpp
#include "GameDataDM.hpp"

#define FILE_NAME "GameData"
////////////////////////////////////////////////////////////////////////////////
GameDataDM::GameDataDM()
: _dicItems(NULL)
{
}

////////////////////////////////////////////////////////////////////////////////
GameDataDM::~GameDataDM()
{
}
////////////////////////////////////////////////////////////////////////////////
bool GameDataDM::init()
{
    std::string str = StringUtils::format("%s", FILE_NAME);
    _dicItems = this->LoadBundlePlist(str.c_str());
    return true;
}
////////////////////////////////////////////////////////////////////////////////
ValueMap GameDataDM::LoadBundlePlist(const char *pListName)
{
    std::string pWorkStr = StringUtils::format("%s.%s.svd", "jp.co.ienter.BeaconTest", pListName);
    std::string savepath = FileUtils::getInstance()->getWritablePath() + pWorkStr.c_str();
    ValueMap dic;
    dic.clear();
    if(FileUtils::getInstance()->isFileExist(savepath)) {
        dic = FileUtils::getInstance()->getValueMapFromFile(savepath.c_str());
    }
    return dic;
}
////////////////////////////////////////////////////////////////////////////////
void GameDataDM::writeSaveFile(ValueMap data, GameDataDMCallback callback)
{
    std::string pWorkStr = StringUtils::format("%s.%s.svd", "jp.co.ienter.BeaconTest", FILE_NAME);

    //保存箇所のフルパスを取得
    std::string savepath = FileUtils::getInstance()->getWritablePath() + pWorkStr.c_str();
    //保存
    if(!FileUtils::getInstance()->writeToFile(data, savepath.c_str())) {
        log("save Failed:%s" , savepath.c_str());
        callback(false);
    } else {
        callback(true);
    }
}

セーブデータを呼び出すためのプログラムを記述します。

GameSettings.hpp
#include "cocos2d.h"
#include "GameDataDM.hpp"
USING_NS_CC;
#define GS GameSettings::sharedSettings()

class GameSettings;

using GameSettingsCallback = std::function<void (const bool &)>;

class GameSettings {
public:
    ~GameSettings();
    GameSettings();
    static GameSettings* sharedSettings(void);

    void writeLocalData(GameSettingsCallback callback);
private:
    CC_SYNTHESIZE(GameDataDM*, _gameDataDM, GameDataDM);
    CC_SYNTHESIZE(ValueMap, _localData, LocalData);

    // UUID
    CC_PROPERTY(std::string, _uuID, UUID);
};
GameSettings.cpp
#include "GameSettings.hpp"

#define USER_DEFAULTS_KEY_UUID      "UUIDKey"

static GameSettings* g_gameSettings = NULL;

GameSettings* GameSettings::sharedSettings() {
    if (! g_gameSettings) {
        g_gameSettings = new GameSettings();

    }
    return g_gameSettings;
}

GameSettings::~GameSettings()
{
}

GameSettings::GameSettings()
{
    _gameDataDM = GameDataDM::create();
    _localData = _gameDataDM->getDicItem();
}

// 保存処理
void GameSettings::writeLocalData(GameSettingsCallback callback)
{
    _gameDataDM->writeSaveFile(_localData, [=](bool flg)
    {
        callback(flg);
    });
}

// 保存されているUUID取得
std::string GameSettings::getUUID() const
{
    std::string ret = "";

    if (_localData.count(USER_DEFAULTS_KEY_UUID)) {
        ret = _localData.at(USER_DEFAULTS_KEY_UUID).asString();
    }
    return ret;
}

// 取得したUUIDを保存する
void GameSettings::setUUID(std::string var)
{
    _localData[USER_DEFAULTS_KEY_UUID] = var.c_str();
    this->writeLocalData([=](bool flg){});
}

縦画面にする

AppDelegate.cppの21行目〜24行目をこのように修正します。
640x1136のサイズに固定しています。

AppDelegate.cpp
static cocos2d::Size designResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size smallResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size mediumResolutionSize = cocos2d::Size(640, 1136);
static cocos2d::Size largeResolutionSize = cocos2d::Size(640, 1136);

画面作成

スタート/ストップボタンを画面内に作成しましょう。

HelloWorldScene.h
#include "cocos2d.h"
USING_NS_CC;

class HelloWorld : public cocos2d::Scene
{
public:
    static cocos2d::Scene* createScene();

    virtual bool init();

    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
private:
    void setup();
    void startTapped(MenuItemSprite* sender);
    void stopTapped(MenuItemSprite* sender);
};
HelloWorldScene.cpp
#include "HelloWorldScene.h"
#include "BeaconBridge.hpp"
#include "GameSettings.hpp"

#define WINSIZE Director::getInstance()->getWinSize()
#define WINCENTER Point(WINSIZE.width*0.5, WINSIZE.height*0.5)

Scene* HelloWorld::createScene()
{
    return HelloWorld::create();
}

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Scene::init() )
    {
        return false;
    }

    this->setup();

    return true;
}

void HelloWorld::setup()
{
    // スタートボタン
    Sprite* btnStart_ON = Sprite::create();
    btnStart_ON->setTextureRect(Rect(0, 0, 200, 100));
    btnStart_ON->setColor(Color3B::GRAY);

    Sprite* btnStart_OFF = Sprite::create();
    btnStart_OFF->setTextureRect(Rect(0, 0, 200, 100));
    btnStart_OFF->setColor(Color3B(200, 50, 50));

    MenuItemSprite* btnStart = MenuItemSprite::create(btnStart_OFF,
                                                         btnStart_ON,
                                                         [this](Ref* sender) {this->startTapped((MenuItemSprite*)sender);});
    Label* lblStart = Label::createWithSystemFont("START", "Arial", 25);
    lblStart->setAlignment(TextHAlignment::CENTER);
    lblStart->setPosition(Vec2(btnStart->getContentSize().width*0.5, btnStart->getContentSize().height*0.5));
    btnStart->addChild(lblStart);

    // ストップボタン
    Sprite* btnStop_ON = Sprite::create();
    btnStop_ON->setTextureRect(Rect(0, 0, 200, 100));
    btnStop_ON->setColor(Color3B::GRAY);

    Sprite* btnStop_OFF = Sprite::create();
    btnStop_OFF->setTextureRect(Rect(0, 0, 200, 100));
    btnStop_OFF->setColor(Color3B(200, 200, 50));

    MenuItemSprite* btnStop = MenuItemSprite::create(btnStop_OFF,
                                                      btnStop_ON,
                                                      [this](Ref* sender) {this->stopTapped((MenuItemSprite*)sender);});
    Label* lblStop = Label::createWithSystemFont("STOP", "Arial", 25);
    lblStop->setAlignment(TextHAlignment::CENTER);
    lblStop->setPosition(Vec2(btnStop->getContentSize().width*0.5, btnStop->getContentSize().height*0.5));
    btnStop->addChild(lblStop);

    Menu* menu = Menu::create(btnStart, btnStop, NULL);
    menu->alignItemsVerticallyWithPadding(50);
    menu->setPosition(Vec2(WINCENTER.x, WINCENTER.y));
    this->addChild(menu);

    Label* lblUUID = Label::createWithSystemFont("", "Arial", 20);
    lblUUID->setAlignment(TextHAlignment::CENTER);
    lblUUID->setPosition(Vec2(WINCENTER.x, WINSIZE.height-20));
    this->addChild(lblUUID);

    // UUIDチェック
    if (GS->getUUID().length()) {
        lblUUID->setString(GS->getUUID());

        // Beacon初期化
        BeaconBridge::initBeacon(GS->getUUID());

    } else {
        // UUID取得する
        std::string uuid = BeaconBridge::initBeaconUUID();
        if (uuid.length()) {
            GS->setUUID(uuid);
            lblUUID->setString(GS->getUUID());

            // Beacon初期化
            BeaconBridge::initBeacon(GS->getUUID());

        }
    }

}

// ビーコン発信
void HelloWorld::startTapped(cocos2d::MenuItemSprite *sender)
{
    BeaconBridge::beaconAction(true, [=](bool flg)
    {
        log("start done!");

    });

}

// ビーコン停止
void HelloWorld::stopTapped(cocos2d::MenuItemSprite *sender)
{
    BeaconBridge::beaconAction(false, [=](bool flg)
    {
        log("stop done!");
    });

}

画面はこんな感じになります。

s103.PNG

ビーコン処理を記述する(Android用)

まずはiOSとAndroidに渡すためのブリッジ関数を記述しましょう

BeaconBridge.hpp
#include "cocos2d.h"

USING_NS_CC;

class BeaconBridge;

using BeaconCallback = std::function<void (const bool &)>;

class BeaconBridge
{
public :
    static void beaconAction(bool flg, BeaconCallback callback);
    static std::string initBeaconUUID();
    static void initBeacon(const std::string& uuid);
};

これはAndroid用の記述なので、XCodeには追加しないようにします。
jniの記述があるので、不明な場合はjniで検索してください。

BeaconBridge.cpp
#include "BeaconBridge.hpp"
#include "platform/android/jni/JniHelper.h"
#define  CLASS_NAME "org/cocos2dx/cpp/AppActivity"

static BeaconCallback _beaconCallback;

void BeaconBridge::beaconAction(bool flg, BeaconCallback callback)
{
    _beaconCallback = nullptr;
    _beaconCallback = callback;

    JniMethodInfo t;

    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "beaconAction", "(Z)V")) {
        t.env->CallStaticVoidMethod(t.classID, t.methodID, flg);

        // 解放
        t.env->DeleteLocalRef(t.classID);
    }
}

void BeaconBridge::initBeacon(const std::string& uuid)
{
    JniMethodInfo t;

    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "initBeacon", "(Ljava/lang/String;)V")) {
        jstring aUUID = t.env->NewStringUTF(uuid.c_str());
        t.env->CallStaticVoidMethod(t.classID, t.methodID, aUUID);

        // 解放
        t.env->DeleteLocalRef(aUUID);
        t.env->DeleteLocalRef(t.classID);
    }
}

std::string BeaconBridge::initBeaconUUID()
{
    std::string ret;
    JniMethodInfo t;

    if (JniHelper::getStaticMethodInfo(t, CLASS_NAME, "initBeaconUUID", "()Ljava/lang/String;")) {
        jstring jStr = (jstring)t.env->CallStaticObjectMethod(t.classID, t.methodID);
        const char* str = t.env->GetStringUTFChars(jStr, NULL);
        ret = str;

        t.env->ReleaseStringUTFChars(jStr,str);
        t.env->DeleteLocalRef(t.classID);
    }

    return ret;

}


extern "C"
{
    // Javaから呼び出される
    void Java_org_cocos2dx_cpp_AppActivity_beaconActionCallback(JNIEnv *env, jobject thiz, jboolean flg)
    {
        if (_beaconCallback) _beaconCallback(flg);
    }
}

Android Studioを起動してプログラムを追記します。

AppActivity.java

package org.cocos2dx.cpp;

import android.annotation.TargetApi;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseSettings;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.BeaconTransmitter;
import org.cocos2dx.lib.Cocos2dxActivity;

import java.util.UUID;

import static android.os.Build.ID;

public class AppActivity extends Cocos2dxActivity {
    private static Beacon beacon;
    private static BeaconTransmitter beaconTransmitter;
    private static AppActivity myref;
    private String wkUUID;

    public static synchronized AppActivity getInstance() {
        return myref;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        myref = this;

    }

    // UUID取得
    public static String initBeaconUUID()
    {
        return UUID.randomUUID().toString();
    }

    // Beacon初期化
    public static void initBeacon(String uuid)
    {
        beacon = new Beacon.Builder()
                .setId1(uuid)
                .setId2("1")  /*好きなIDを指定してください*/
                .setId3("80") /*好きなIDを指定してください*/
                .setManufacturer(0x004C)
                .build();

        BeaconParser beaconParser = new BeaconParser()
                .setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24");
        beaconTransmitter = new BeaconTransmitter(AppActivity.getInstance(), beaconParser);

    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void beaconAction(boolean flg)
    {
        if (flg) {
            //発信開始
            AppActivity.getInstance().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    beaconTransmitter.startAdvertising(beacon, new AdvertiseCallback() {
                        @Override
                        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                            super.onStartSuccess(settingsInEffect);
                            Log.d("BeaconTest", "Beacon Success!");
                            beaconActionCallback(true);
                        }

                        @Override
                        public void onStartFailure(int errorCode) {
                            super.onStartFailure(errorCode);
                            beaconActionCallback(false);
                            Log.d("BeaconTest", "Beacon Failure!");
                        }
                    });

                }
            });
        } else {
            // 発信停止
            if (beaconTransmitter.isStarted()) {
                beaconTransmitter.stopAdvertising();
            }

            beaconActionCallback(true);
        }

    }

    @Override
    protected void onStop() {
        super.onStop();

        if (beaconTransmitter.isStarted()) {
            beaconTransmitter.stopAdvertising();
        }
    }

    private static native void beaconActionCallback(boolean flg);
}

AndroidManifest.xmlに権限を追記します。

AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!--対応していない機種にインストールされないようにuser-futureを設定する-->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<!--Android 6.0でBLEを使うのに位置情報のパーミッションが必要-->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>

ビーコン処理を記述する(iOS用)

XCodeにファイルを新規追加します。
iOSフォルダに保存してください。
c++の記述をするので、ファイル名の拡張子は.mmを使用しています。

BeaconBridge.mm
#import "BeaconBridge.hpp"
#import "BeaconController.h"

// ビーコン発信・停止
void BeaconBridge::beaconAction(bool flg, BeaconCallback callback)
{
    [[BeaconController getInstance] beaconAction:flg callback:^(bool flg) {
        callback(flg);
    }];
}

// UUID生成
std::string BeaconBridge::initBeaconUUID()
{
    NSString* userID = [[UIDevice currentDevice].identifierForVendor UUIDString];
    return [userID UTF8String];
}

// ビーコン初期化
void BeaconBridge::initBeacon(const std::string& uuid)
{
    NSString *aUUID = [NSString stringWithCString:uuid.c_str()
                                           encoding:[NSString defaultCStringEncoding]];
    [[BeaconController getInstance] initBeacon:aUUID];

}

ビーコンの実装を記述します。

BeaconController.h
#import <CoreBluetooth/CoreBluetooth.h>
#import <CoreLocation/CoreLocation.h>

@class BeaconController;

typedef void (^BeaconHandler)(bool flg);

@interface BeaconController : NSObject<CBPeripheralManagerDelegate>
{

}

@property (nonatomic, readonly) CBPeripheralManager* peripheralManager;
@property (nonatomic, readonly) NSString *uuid;

+ (BeaconController*) getInstance;
- (void)initBeacon:(NSString*)uuid;
- (void)beaconAction:(bool)flg callback:(BeaconHandler)callback;

@end;
BeaconController.m
#import "BeaconController.h"

static BeaconController *instanceOfDialogHelper;

@implementation BeaconController
+ (BeaconController*) getInstance
{
    @synchronized(self)
    {
        if (instanceOfDialogHelper == nil)
        {
            instanceOfDialogHelper = [[BeaconController alloc] init];
        }

        return instanceOfDialogHelper;
    }

    // to avoid compiler warning
    return nil;
}

- (void) initBeacon:(NSString *)uuid
{
    _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self
                                                                     queue:dispatch_get_main_queue()];
    _uuid = [uuid copy];
}

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{

}

- (void) beaconAction:(bool)flg callback:(BeaconHandler)callback
{
    if (flg) {
        // ビーコン発信
        NSUUID* wkUUID = [[NSUUID alloc] initWithUUIDString:_uuid];
        CLBeaconRegion* region = [[CLBeaconRegion alloc] initWithProximityUUID:wkUUID
                                                                         major:1
                                                                         minor:80
                                                                    identifier:[wkUUID UUIDString]];

        NSDictionary* peripheralData = [region peripheralDataWithMeasuredPower:nil];
        [self.peripheralManager startAdvertising:peripheralData];
        callback(true);
    } else {
        // ビーコン停止
        [self.peripheralManager stopAdvertising];
        callback(true);
    }
}
@end

まとめ

とても短いコードでiBeaconの発信機を作成することが出来ました。
iOSの方が発信感度?が良かったです。

githubに今回のコードをアップしていますので、ご確認ください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1