이번에 버전업 중에 사용한 안드로이드 스튜디오 버전.
최신 버전은 아니지만, 일단 회사 데스크톱에 설치되어 있는 에디터 버전이 이렇기 때문에 얘 기준으로 세팅을 맞췄습니다.
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

유니티에서 스크립트를 생성 후 비쥬얼 스튜디오로 열었을때 빨간 줄이 뜨면서 저런 에러 문구를 띄울 때가 있을겁니다.


이에 대한 원인은 컴파일러에 대한 오류가 가장 많은데 


대표적으로 전체 시스템 네임스페이스를 정의 하는 mscorlib.dll 를 프로젝트로 가져 오지 않은 경우에 발생한다고 합니다.


이걸 해결 하는 방법은 여러가지가 있으나 가장 쉬운 방법은 닷넷 프레임 워크를 다시 설치 하거나 비쥬얼 스튜디오 커뮤니티(설치 프로그램)


을 통해서 업데이트 혹은 재설치를 하는 것입니다.


다들 즐거운 코딩을 하시길 o//

작업 중에 "TypeLoadException: Could not load type 'UnityEngine.VR.VRSettings' from assembly 'Stores." 라는 메시지를 보게 되었다.(이 메시지는 logcat을 통해서 확인한 것이다.)


이 메시지가 끝 구간은 유니티 IAP를 통한 구글 인앱 결제의 해당 아이템 구매를 콜하는 부분에서 나온 것인데, 유니티 포럼을

이리저리 둘러보다보니 본인과 같은 일을 겪은 (서양)형들이 꽤나 많다는 것을 알게 되었다.


결론을 말하자면, 해당 유니티 프로젝트의 엔진 버전업을 하게 되면 로우 레벨단에서 처리하는 과정이 있는데, 그때 처리하는 과정에서

사용하는 플러그인(에셋)등에 문제가 있으면 그 작업이 제대로 안된다는 것이다. 그게 안되서 막상 IAP 플러그인을 넣고 실테스트를 하게 되면 IAP가 동작하는 도중에 저런 메시지를 띄운다는 것이다.


그러면 어떻게 해야하는가.


일단 프로젝트를 ReImport ALL을 해줘야한다.


참고로 이 작업을 하기 전에 프로젝트를 백업해두길 바란다.


자세하게 아는건 아니지만 메타데이터를 새로 작성하는 작업 같은데, 이 작업 도중에 잘못되서 프로젝트가 아작나면....


암튼 백업을 미리 해두고 Assets -> ReImport ALL 을 해주면 진행하다가 팝업이 하나 뜨게 될 것이다.


백업을 만들어 둔게 있냐 라는 식으로 경고 팝업이 뜰텐데, 우리는 이미 백업을 미리 해두었다. 진행하도록 하자.


싹 진행 후.


에러가 뜰 것이다.



이런 에러와 또 다른 추가 에러가 뜰 것인데, 이게 뜨는 이유는 위와 같이 불안정한 프로젝트 상태에서 IAP 플러그인을 임포트 후,

작업했다가 프로젝트 안정화 작업을 시행했을때 뜬다. 쉽게 말해서 네가 이전에 임포트 한 IAP 플러그인이 불안정한 상태이거나, 혹은 IAP 서비스가 오프라인이 되어 있거나 둘 중 하나이다. 물론 둘 다 일 수 있다.


본인은 일단 서비스를 재활성화 시킨 후, IAP 플러그인을 업데이트 하라는 것이 보여서 업데이트도 다 해주고, 혹시나 싶어서 ReImport 도 해주었다.


그리고 나서 APK를 만들어서 구글 콘솔을 통해 업로드 해주고, 구글 앱스토어에서 업뎃이 되길 기다렸다.


업데이트가 되고 나서 인앱 결제를 시도 해보니까. 문제없이 구글 결제 팝업이 뜨는걸 확인했다.


참고한 포럼 링크


https://forum.unity.com/threads/typeloadexception-could-not-load-type-unityengine-vr-vrsettings-from-assembly-stores.539915/

https://forum.unity.com/threads/could-not-load-type-unityengine-vr-vrsettings-from-assembly-stores.521484/

- https://answers.unity.com/questions/1320738/unity-iap-plugin-is-installed-but-unity-iap-is-dis.html

- https://answers.unity.com/questions/1131981/unhandled-exception-systemreflectionreflectiontype-1.html





게임 회사에서 구글 플레이 서비스와 애드몹을 같이 쓰는 경우가 일반적이다.


그런데 이 두 플러그인을 같이 넣고 apk 빌드를 돌리면 99퍼 확률로 터진다고 한다.


실제로 본인도 아무것도 모르고 돌렸다고 에러가 펑펑펑 터지는걸 보고 멘탈이 깨졌다.


- 터지면 나오는 에러 -




그래서 한 시간을 헤맨 결과. 해법을 찾았다.


해당 블로그(출처 : http://jinsdevlog.tistory.com/8) 에서 여러 원인 중 99퍼 확률로 한 가지 이유로 터진다고 언급 했다.


그것은 애드몹과 플레이 서비스의 gms 버전이 각기 다르기 때문이라는데, 이걸 해결하려면


아래의 파일들을 수정해주어야 한다고 한다.










본인의 당시 상황은 


유니티 2017.4.3

구글 플레이 서비스 플러그인 0.9.5

애드몹 3.15.0


이다.


위 두 파일을 열면 이렇게 보인다.


GooglePlayGamesPluginDependencise.xml



GoogleMobileAdsDependencise.xml



이전에는 두 파일에 공통되는 구문이 있었는 듯하다. 그러나 지금은 애드몹의 xml 파일에만 해당 구문이 있다.


그렇다면 문제는 해당 구문의 버전넘버에 문제가 있다는 것.


그래서 본인은 gms:play-services-ads: 뒤에 있는 숫자를 바꿔주었다. 위의 스샷의 넘버는 이미 바꿔준 상태이다.


바꾸기 전에는 15.0.1 이 였다.


바꾸고 저장한 후 asset->play service resolver -> android -> resolve를 클릭해주자.


그리고 play service resolver setting에서 auto resolve는 꺼주자.


혹시나 싶어서 아예 새로운 유니티 프로젝트를 하나 만들어서 이 두 플러그인을 넣고 apk를 만들어 봤는데,


위와 같은 에러는 나오지 않았다. 아마도 Plugins 폴더를 미리 생성해놨을시에 나타나는 문제 같은데,


만약에 이 두 플러그인을 사용할 계획이라면 미리 이 두 플러그인을 적용해놓고 나서 주 작업을 하는 것을 추천한다.

2012년도에 만들어진 프로젝트를 엔진 버전업 하는 작업을 받았는데, 


다행히 마지막 작업 날짜를 기준으로 작업한 엔진 버전으로 실행을 해서 프로젝트를 쉽게 열기는 했으나...


두 단계를 올리니까 이상한 버그가 발생 했다.


안드로이드 플랫폼으로는 이상없던 텍스쳐가 ios 플랫폼으로 바꾸니까 갈색으로 색이 깨져 버린 것.


  1. Failed to execute /Applications/Unity/Unity.app/Contents/Tools/PVRTexTool
  2. Failed to generate PVRT texture


라는 괴상한 에러가 떴는데 찾아보니까 ios에서 텍스쳐 압축 관련 기술 어쩌고 설명이 되어 있다.


그래서 이리저리 찾다가 유니티 이슈 팀이 3.5.6 단계에서 해당 이슈를 접수 받고나서

(https://forum.unity.com/threads/pvr-compression-failed.154597/


이후 3.5.7 버전에서 수정했다는 것을 확인 하게 되었다.


혹시 3.X 버전의 유니티 프로젝트를 엔진 버전업을 해야 하는 일을 맡게 되었을 때


이와 같은 에러가 발생하면 3.5.7 버전으로 엔진을 설치하여, 프로젝트를 열기를 바란다.

드디어 취직했다.


그동안 얼마나 삽질 했는지 참..


그리고 할 일거리가 굉장히 많다.


상당히 바쁠 것 같다...


열심히 해야지.


그래야 살아 남으니까.




+ Recent posts