이번에 버전업 중에 사용한 안드로이드 스튜디오 버전.
최신 버전은 아니지만, 일단 회사 데스크톱에 설치되어 있는 에디터 버전이 이렇기 때문에 얘 기준으로 세팅을 맞췄습니다.
Android beacon library 를 사용한 유니티 플러그인을 만든건 2년이지만, 그나마 최근이라고 해야하나?
당시 기기의 OS 레벨에 맞춰야 하는 상황과 스토어에서 요구하는 앱의 sdk 최저 레벨에 관련하여 대응해야하는 상황이 있어서 어느정도 손을 본 버전입니다.

올바르게 사용했다고는 생각들지는 않지만, 동작시켜서 원하는 기능 수행에 주목적을 두고 만들었습니다.

앞선 글들을 통해서 간단하게 AAR 파일을 만들어서 유니티에 동작을 시켜보셨기 때문에, 그 기준으로 진행을 해보겠습니다.

아래 스샷들은 해당 프로젝트를 하면서 추가해줘야하는 권한 및 필요한 gradle 작성 관련입니다.
없어져도 되는 부분도 있긴 할텐데, 쓸데없는 노파심에 예전에 넣어둔걸 그대로 냅둔 부분도 있긴 합니다.

 

- 코드 -

package com.example.beaconplugin;

import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Debug;
import android.util.Log;

import com.unity3d.player.UnityPlayer;

import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.InternalBeaconConsumer;
import org.altbeacon.beacon.MonitorNotifier;
import org.altbeacon.beacon.RangeNotifier;
import org.altbeacon.beacon.Region;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AndroidNativeBeacon implements InternalBeaconConsumer
{
    private static final String LOG_TAG = "Android";

    private BeaconManager beaconManager;
    private Beacon beacon;
    private  Map<String, Double> bDistances = new HashMap<String, Double>();
    private float catchBeaconDistance = 1.2f;

    private String gameObject;
    private String nitfyCallback;
    private String errorCallback;
    //    private String enterCallback;
    //    private String exitCallback;
    public AndroidNativeBeacon()
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon");
        beaconManager = null;
    }

    public void Init(Context context, String gameObject, String nitfyCallback, String errorCallback)//, String enterCallback, String exitCallback)
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon Init");
        // beaconManager
        beaconManager = BeaconManager.getInstanceForApplication(context);
        beaconManager.getBeaconParsers().add(new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"));

        this.gameObject = gameObject;
        this.nitfyCallback = nitfyCallback;
        this.errorCallback = errorCallback;
//        this.enterCallback = enterCallback;
//        this.exitCallback = exitCallback;
    }

    public void SetCatchDistance(float _value)
    {
        catchBeaconDistance = _value;
        Log.d(LOG_TAG, "AndroidNativeBeacon.SetCatchDistance : " + catchBeaconDistance);
    }

    public void OnNotify(String data)
    {
        Log.i(LOG_TAG, "AndroidNativeBeacon.OnNotify to gameObject=" + gameObject);
        UnityPlayer.UnitySendMessage(gameObject, nitfyCallback, data);
    }

    public void OnError(String errorMessage)
    {
        Log.i(LOG_TAG, "AndroidNativeBeacon.OnError message:" + errorMessage);
        UnityPlayer.UnitySendMessage(gameObject, errorCallback, errorMessage);
    }

//    public void OnEnterBeacon()
//    {
//        Log.i(LOG_TAG, "AltBeaconClient.OnEnterBeacon to gameObject=" + gameObject);
//        UnityPlayer.UnitySendMessage(gameObject, enterCallback, "");
//    }
//
//    public void OnExitBeacon()
//    {
//        Log.i(LOG_TAG, "AltBeaconClient.OnExitBeacon to gameObject=" + gameObject);
//        UnityPlayer.UnitySendMessage(gameObject, exitCallback, "");
//    }

    @Override
    public void onBeaconServiceConnect()
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon.onBeaconServiceConnect");

        beaconManager.addMonitorNotifier(new MonitorNotifier() {
            @Override
            public void didEnterRegion(Region region) {
                Log.i(LOG_TAG, "didEnterRegion - 비콘 연결됨");
                beacon = null;
                //EnterIBeacon();
            }

            @Override
            public void didExitRegion(Region region) {
                Log.i(LOG_TAG, "didExitRegion - 비콘 끊김");
                beacon = null;
                NotiyIBeacon(null);
                //ExitIBeacon();
            }

            @Override
            public void didDetermineStateForRegion(int state, Region region) {
                Log.i(LOG_TAG,  "I have just switched from seeing/not seeing beacons: "+state);
            }
        });

        beaconManager.addRangeNotifier(new RangeNotifier() {
            @Override
            public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {
                // 검출한 비콘의 정보를 전부 로그에 써내다
                if(beacons.size() > 0)
                {
                    Log.i(LOG_TAG, "didRangeBeaconsInRegion - 비콘들 검출함." + beacons.size());
                    NotiyIBeacon(beacons);
                }
            }
        });

        try {
            beaconManager.startMonitoring(new Region("unique-id-001", null, null, null));
            // 비콘 정보 감시를 개시
            beaconManager.startRangingBeacons(new Region("unique-id-001", null, null, null));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void Bind()
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon.Bind");
        AltBeaconManagerBind(BindType.Bind);
    }

    public void Unbind()
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon.UnBind");
        AltBeaconManagerBind(BindType.Unbind);
    }

//    private boolean AltBeaconBindStatus = false;
    private void AltBeaconManagerBind(BindType type)
    {
        Log.d(LOG_TAG, "AndroidNativeBeacon.AltBeaconManagerBind type:" + type.toString());

        switch (type)
        {
            case Bind:
                Log.d(LOG_TAG, "AndroidNativeBeacon.AltBeaconManagerBind bind");
                beaconManager.bindInternal(this);
                break;
//                AltBeaconBindStatus = true;
//            case Auto:
//                if(AltBeaconBindStatus)
//                {
//                    if (clients.size() == 0) {
//                        Log.d(LOG_TAG, "AndroidNativeBeacon.AltBeaconManagerBind unbind");
//                        beaconManager.unbindInternal(this);
//                    }
//                    else {
//                        Log.d(LOG_TAG, "AndroidNativeBeacon.AltBeaconManagerBind bind");
//                        beaconManager.bindInternal(this);
//                    }
//                }
//                break;
            case Unbind:
                Log.d(LOG_TAG, "AndroidNativeBeacon.AltBeaconManagerBind unbind");
                beaconManager.unbindInternal(this);
//                AltBeaconBindStatus = false;
                break;
        }
    }

    private void NotiyIBeacon(Collection<Beacon> beacons)
    {
        if(beacons != null)
        {
            //발열이 심해서 거리 계산 하는 반복문을 따로 해보자.
            for(Beacon b : beacons)
            {
                Double _Value = new Double(calculateDistance_double(b));

                bDistances.put(b.getId1().toString(), _Value);
            }
            //발열이 심해서 거리 계산 하는 반복문을 따로 해보자.
            Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon beacons is null.");
            Log.d(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon catchBeaconDistance : " + catchBeaconDistance);
            for (Beacon b : beacons)
            {
                Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon inside forech");

                if (beacon == null)
                {
                    Double beaconDistance = null;

                    if(bDistances.get(b.getId1().toString()) != null)
                    {
                        beaconDistance = (Double) bDistances.get(b.getId1().toString());//calculateDistance(b);
                    }

                    if(beaconDistance != null)
                    {
                        if(beaconDistance.doubleValue() <= catchBeaconDistance)
                        {
                            Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon beacon is null. put in b data");
                            beacon = b;
                        }
                        else
                        {
                            beacon = null;
                        }
                    }
                    else if(beaconDistance == null)
                    {
                        Log.i(LOG_TAG, "beaconDistance is NULL");
                    }
                }
                else if (beacon != null)
                {
                    Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon beacon is not null.");

                    if (!beacon.getId1().toString().equals(b.getId1().toString()))
                    {
                        Double beaconDistance = null;
                        Double bDistance = null;

                        if(bDistances.get(beacon.getId1().toString()) != null)
                        {
                            beaconDistance = (Double) bDistances.get(beacon.getId1().toString());//calculateDistance(b);
                        }

                        if(bDistances.get(b.getId1().toString()) != null)
                        {
                            bDistance = (Double) bDistances.get(b.getId1().toString());//calculateDistance(b);
                        }

                        Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon UUID is not same");
                        Log.i(LOG_TAG, "beacon.getId1() : " + beacon.getId1() + " , b.getId1() : " + b.getId1());

                        if(beaconDistance != null && bDistance != null)
                        {
                            Log.i(LOG_TAG, "beacon.getId1().Dis : " + beaconDistance.doubleValue() + " , b.getId1().Dis : " + bDistance.doubleValue());

                            if(beaconDistance.doubleValue() > bDistance.doubleValue())//if (beacon.getDistance() > b.getDistance())
                            {
                                if(bDistance.doubleValue() <= catchBeaconDistance)
                                {
                                    Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon b.Distance is short. put in b data");
                                    beacon = b;
                                }
                                else
                                {
                                    beacon = null;
                                }
                            }
                        }
                        else if(beaconDistance != null && bDistance == null)
                        {
                            Log.i(LOG_TAG, "beacon.getId1().Dis : " + beaconDistance.doubleValue());

                            if(beaconDistance.doubleValue() <= catchBeaconDistance)
                            {
                                Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon beacon.Distance is short.");
                                Log.i(LOG_TAG, "bDistance is NULL");
                            }
                            else
                            {
                                beacon = null;
                            }
                        }
                        else if(beaconDistance == null && bDistance != null)
                        {
                            Log.i(LOG_TAG, "b.getId1().Dis : " + bDistance.doubleValue());

                            if(bDistance.doubleValue() <= catchBeaconDistance)
                            {
                                Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon b.Distance is short. put in b data");
                                Log.i(LOG_TAG, "beaconDistance is NULL");
                                beacon = b;
                            }
                            else
                            {
                                beacon = null;
                            }
                        }
                    }
                    else if (beacon.getId1().toString().equals(b.getId1().toString()))
                    {
                        Double beaconDistance = (Double) bDistances.get(beacon.getId1().toString());//calculateDistance_double(beacon);

                        if(beaconDistance.doubleValue() <= catchBeaconDistance)
                        {
                            Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyIBeacon UUID is same");
                            beacon = b;
                        }
                        else
                        {
                            beacon = null;
                        }
                    }
                }
            }
        }

        bDistances.clear();
        NotiyInform(beacon);
    }

    protected static double calculateDistance_double(Beacon b)
    {
        int measuredPower = b.getTxPower();
        double rssi = b.getRssi();

        if (rssi == 0)
        {
            Log.i(LOG_TAG, "beacon.getId1() : " + b.getId1().toString() + " , rssi : 0");

            return -1.0;
            // if we cannot determine distance, return -1.
        }
        double ratio = rssi*1.0/measuredPower;

        if (ratio < 1.0)
        {
            double other = Math.pow(ratio,10);

            Log.i(LOG_TAG, "beacon.getId1() : " + b.getId1().toString() + " , other : " + other);

            return Math.pow(ratio,10);
        }
        else
        {
            double distance = (0.89976) * Math.pow(ratio, 7.7095) + 0.111;

            Log.i(LOG_TAG, "beacon.getId1() : " + b.getId1().toString() + " , distance : " + distance);

            return distance;
        }
    }

    protected static Double calculateDistance_Double(Beacon b)
    {
        int measuredPower = b.getTxPower();
        double rssi = b.getRssi();

        if (rssi == 0)
        {
            return -1.0;
            // if we cannot determine distance, return -1.
        }
        double ratio = rssi*1.0/measuredPower;

        if (ratio < 1.0)
        {
            return Math.pow(ratio,10);
        }
        else
        {
            Double distance = (0.89976) * Math.pow(ratio, 7.7095) + 0.111;

            return distance;
        }
    }

    private void NotiyInform(Beacon b)
    {
        String data = "";
        // "UUID:" + b.getId1()
        // "major:" + b.getId2()
        // "minor:" + b.getId3()
        // "RSSI" + b.getRssi()
        // "Name" + b.getBluetoothName();
        // "Address" + b.getBluetoothAddress();

        if(b != null)
        {
            data = String.format("%s,%s,%s,%s,%s,%s,%s,%s", System.currentTimeMillis(), b.getId1(), b.getId2(), b.getId3(), b.getTxPower(), b.getRssi(), b.getBluetoothName(), b.getBluetoothAddress());
        }

        // 로그 출력
        Log.i(LOG_TAG, "AndroidNativeBeacon.NotiyInform " + data);

        // 각 클라이언트에 진찰한 비콘 정보를 브로드캐스트
//        for (int i = 0; i < clients.size(); i++)
//            clients.get(i).
        OnNotify(data);
    }

    private enum BindType
    {
        Auto,
        Bind,
        Unbind,
    };

    @Override
    public Context getApplicationContext() {
        return null;
    }

    @Override
    public void unbindService(ServiceConnection connection) {

    }

    @Override
    public boolean bindService(Intent intent, ServiceConnection connection, int mode) {
        return false;
    }
}

 

맨위의 package com.example.beaconplugin; 의 example 은 다들 아시다싶이, 회사명이 들어가는 자리 혹은 관련 명칭이 들어가는 곳이라서 example 으로 대체해놓았습니다.

 

코드는 예전에 참고로 찾아서 본 투케이2K 님의 https://kkh0977.tistory.com/3314 글을 보고 작업한 것을 이리저리 다른 글도 참고하면서 만든겁니다.

 

구문을 보시다보면, 본래는 없던 메서드들이 자리하고 있는 것으로 보이는게 있을겁니다.

제가 관련 프로젝트에서 필요한 기능을 추가하려고 구글링을 통해서 이리저리 찾은 메서드들을 집어넣은거라 가볍게 넘어가주시면 됩니다. 그리고 사용하지 않는 메서드들은 왜 남아있는지 의문이 드실수도 있는데, 제가 순수한 안드로이드 프로그래머가 아니라서 노파심에 사용한 예시 코드의 모습이 그대로 남아 있는 상태입니다.

 

준비가 다되었다면, 빌드탭에서 저 망치를 한번 눌러주시면 빌드를 진행하게 됩니다.

 

빌드를 진행하면 아래와 같은 진행상태가 나올겁니다.

 

 

빌드가 정상적으로 된다면 이전글에서 확인한 해당 경로에 aar 파일이 존재할텐데, 디버그 빌드로 나온 것을 확인하셨을 겁니다.

 

이것을 릴리즈로 빌드하려면 아래와 같이 우측의 탭을 눌러서 창을 나오게 한 다음에 재생 버튼 같은 것을 눌러주시면 됩니다.

 

 

그러면 아래와 같은 창이 하나가 나올텐데, 스샷에 나온것처럼 해당 부분을 더블클릭을 해주면 릴리즈 빌드가 실시 됩니다.

 

 

빌드가 끝나고 경로를 가보면 아래와 같이 aar 파일들이 나온 것을 확인 할 수 있습니다.

 

다음글은 이 aar 파일을 이용해서 유니티에서 비콘을 동작시켜보는 코드들과 사용법을 알려드리겠습니다.

 

아래 링크는 최근에 발견한 안드로이드 비콘 라이브러리 사용관련 깃허브 글입니다.
제 글보다는 이 글이 더 나은 것 같아서, 첨부했습니다.

https://github.com/happymoment-s/TIL/blob/master/Android/%5BAndroid%5D%5BBeacon%5D%20Beacon%20Library%20%EC%82%AC%EC%9A%A9%EB%B2%95(AltBeacon%2C%20Nordic%2C%20Estimote).md

올해도 너무 일에 치여서 살다보니까, 올 1월에 첫글 썼다가 말이 되어서야 두번째 글을 쓰게 되었습니다.

 

이게 직장인의 비애인가 보다 하네요.

 

이번 글은 딱히 복잡할 일이 없습니다.

 

이전에 만든 AAR을 이용해서, 토스 메시지를 띄워보는 것이 끝이죠.

 

이번의 목표는 만든 AAR을 유니티에서 어떻게 쓸 수 있는지 방법은 터득하는 것입니다.

 

유니티 에디터에 어느정도 익숙하다는 전제로 설명하도록 하겠습니다.

 

일단 유니티에 안드로이드 플렛폼 프로젝트 하나를 만들도록 합니다.

 

그리고 원하는 이름의 스크립트를 하나 만들고 아래와 같이 작성하도록 합니다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class EmptyClass : MonoBehaviour
{
    private AndroidJavaObject aar;
    private AndroidJavaClass unityPlayer;
    private AndroidJavaObject context;

    private void Awake()
    {
        // Context(Activity) 객체를 취득
        unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
        aar = new AndroidJavaObject("com.test.myappliaction.플러그인 만들 당시에 쓴 자바 클래스명");
        
        //aar에 Context 전달.
        aar.Call("setContext", context);
    }
    
    public void ShowToast(string message) 
    {
    	//aar에 만든 해당 토스트 메시지 창 호출 함수에 문자열 값을 전달.
    	aar.Call("ShowToast", message);
    }
}

 

Canvas에 버튼 하나를 생성해서 버튼 클릭시, 해당 함수가 호출되도록 작업을 해줍니다.

 

그리고 테스트용 APK를 하나 빌드하여 테스트 기기에 설치하여, 테스트를 해봅니다.

 

버튼 클릭을 하면, 아래와 같이 팝업 메시지가 뜨면 성공입니다.

 

 

 

다음은 Android beacon library 를 이용해서 비콘 신호를 받는 앱을 만드는 것을 진행해보려고 합니다.

 

유니티에서 쉽게 비콘 통신을 할려면, 유니티 에셋 스토어에서 돈 주고 에셋 하나 사서 하는게 가장 좋습니다.

 

거기서 파는 에셋 중에 좋은건 IOS 에서도 쓸 수 있기 때문에 범용성 아주 좋습니다.

 

정말 추후에 IOS 비콘 플러그인도 만들었으면 하긴 하는데... 제가 IOS는 단 1도 몰라서 어떻게 만들어야 할지 걱정입니다.

 

심지어 맥(MAC)도 있어야 가능하니..... 아무튼 다음 글에서 뵙겠습니다.

비콘 신호를 감지하여, 특정 동작을 하기 위한 앱을 만들기 위해 Android beacon library 를 이용해, 유니티에서 쓰려고 한다. 

 

일단 AAR 파일을 빌드해야 하기 때문에, AAR 파일을 만들기 위해서 어떻게 해야하는지부터 정리를 하려고 한다.

 

본문보다 아래 참고로 링크를 붙은 일본쪽 블로그가 더 잘 정리가 되어 있으니, 해당 블로그를 보는 것을 더 추천한다.

Android Studio에서 프로젝트를 만듭니다.

No Activity로 선택하여 진행합니다.

프로젝트에서 새로운 모듈을 하나 추가한다.

추가할 모듈은 안드로이드 라이브러리로 선택합니다.

app 을 선택 후 삭제하고 나서 Apply 버튼을 누릅니다.

Unity 클래스 라이브러리를 추가하여 Unity 관련 API를 사용할 수 있습니다.

classes.jar 파일을 libs 폴더에 추가합니다.

해당 파일은 설치한 유니티 경로에서 아래의 경로에 위치해 있습니다.

/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes

 

이제 Unity 관련 API를 Java에서 사용할 수있게되었지만 classes.jar플러그인에 포함 된 상태로 두면 Unity 측 빌드에서

오류가 발생하므로 classes.jarJava 코드 컴파일로 만 사용하도록합니다.

 

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        minSdkVersion 29
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    // implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

위와 같이 수정을 합니다. 수정할 파일은 build.gradle(module:xxxxxxxx) 이다.

지금까지 완료되면 오른쪽 상단의 Sync Now을 클릭하여 Gradle 변경 사항을 적용합니다.

package com.xxxx.xxxxxx;

import android.content.Context;
import android.widget.Toast;

public class xxxxxx
{
	private Context context;
    
    public void setContext(Context _context)
    {
    	context = _context;
    }
    
    private void ShowToast(String msg)
    {
    	Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }
}

빌드를 하게 되면 아래와 같은 경로에 aar 파일이 생성됩니다.

/build/outputs/aar

 

다음에는 이 aar 파일을 사용하여, 유니티 프로젝트에서 사용하는 것을 진행해보도록 하겠습니다.

 

- 참고 -

https://gaprot.jp/2020/03/30/unity-android-native-plugin/

 

Unity向けAndroidネイティブプラグインの作り方 | ギャップロ

はじめに UnityでMLKitを使う記事を書いていたら、Androidプラグインの準備の段階だけで1つ記事になりそうなくらいだったので分割しました。UnityでMLKitを使う記事は次回にしたいと思います。

gaprot.jp

+ Recent posts