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
を起動します。
iOSライブラリ追加
iOSで必要なライブラリを追加します。
- CoreBluetooth.framework
- CoreLocation.framework
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+'
}
データ保持プログラムを実装する
UUIDを端末内に保存するためのプログラムを実装します。
下記ファイルを新規作成してください。
- GameDataDM.cpp
- GameDataDM.hpp
- GameSettings.cpp
- GameSettings.hpp
GameDataDMはplist形式で端末内にデータを保存します。
#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);
};
#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);
}
}
セーブデータを呼び出すためのプログラムを記述します。
#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);
};
#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のサイズに固定しています。
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);
画面作成
スタート/ストップボタンを画面内に作成しましょう。
#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);
};
#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!");
});
}
画面はこんな感じになります。
ビーコン処理を記述する(Android用)
まずはiOSとAndroidに渡すためのブリッジ関数を記述しましょう
#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で検索してください。
#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を起動してプログラムを追記します。
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に権限を追記します。
<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
を使用しています。
#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];
}
ビーコンの実装を記述します。
#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;
#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に今回のコードをアップしていますので、ご確認ください。