블로그 이미지
Every unexpected event is a path to learning for you.

카테고리

분류 전체보기 (2737)
Unity3D (817)
Programming (474)
Server (33)
Unreal (4)
Gamebryo (56)
Tip & Tech (228)
협업 (58)
3DS Max (3)
Game (12)
Utility (136)
Etc (96)
Link (32)
Portfolio (19)
Subject (90)
iOS,OSX (53)
Android (14)
Linux (5)
잉여 프로젝트 (2)
게임이야기 (3)
Memories (20)
Interest (38)
Thinking (38)
한글 (30)
PaperCraft (5)
Animation (408)
Wallpaper (2)
재테크 (18)
Exercise (3)
나만의 맛집 (3)
냥이 (10)
육아 (16)
Total
Today
Yesterday
04-21 00:01

Unity 2021.3.37f1

----

 

기존에 Custom Debug.cs를 만들어서 Plugins 폴더에 넣고 Debug.Log 사용 유무를 제어했었는데,

스토어 둘러보다보니 Disable Logs Easy라는게 보여서 살펴보니 좋아보인다.

게다가 Free..

기존의 Debug.cs를 제거하고 이걸로 교체 함.

 

 

[링크] https://assetstore.unity.com/packages/tools/utilities/disable-logs-easy-206473

 

Disable Logs Easy | 유틸리티 도구 | Unity Asset Store

Use the Disable Logs Easy from Dev Dunk Studio on your next project. Find this utility tool & more on the Unity Asset Store.

assetstore.unity.com

 

[참조] https://blueasa.tistory.com/1024

 

디바이스에서 Debug.Log 메시지 출력하지 않도록 하기

[추가] 2024-04-17 #define으로 Editor, Device등 원하는 곳에 띄울 수 있게 약간 변형함 #if UNITY_EDITOR #define MY_DEBUG #endif using UnityEngine; using System.Collections; using System; using UnityEngine.Internal; /// /// It overrides Uni

blueasa.tistory.com

 

반응형
Posted by blueasa
, |

Unity 2021.3.37f1

Lunar Mobile Console - Pro v1.8.5

----

Lunar Mobile Console - Pro에서 Actions and Variables를 사용할 수 있는데,

이번에 enum 넣으면서 참조 링크를 다시 봤는데 enum 관련 작성을 제대로 안해놔서 좀 헤메서..

변수 타입별 사용방법 정리 겸 샘플 작성해서 올려 둠.

 

 

using UnityEngine;
using System.Collections;
using LunarConsolePlugin;

public class AnLunarConsole_Actions_Variables_Manager : MonoBehaviour
{
    public enum eVariablesType
    {
        One,
        Two,
        Three
    }

    [CVarContainer]
    public static class Variables
    {
        public static readonly CVar myBool = new CVar("My boolean value", true);
        public static CVarChangedDelegate CVarChangedDelegate_myBool = null;
        public static readonly CVar myFloat = new CVar("My float value", 3.14f);
        public static CVarChangedDelegate CVarChangedDelegate_myFloat = null;
        public static readonly CVar myInt = new CVar("My integer value", 10);
        public static CVarChangedDelegate CVarChangedDelegate_myInt = null;
        public static readonly CVar myString = new CVar("My string value", "Test");
        public static CVarChangedDelegate CVarChangedDelegate_myString = null;
        public static readonly CEnumVar<eVariablesType> myEnum = new CEnumVar<eVariablesType>("My enum value", eVariablesType.Two);
        public static CVarChangedDelegate CVarChangedDelegate_myEnum = null;
    }

    IEnumerator Start()
    {
        yield return null;

        InitActions();
        InitVariables();

        AddAction();
        AddDelegate();
    }

    void OnDestroy()
    {
        RemoveAction();
        RemoveDelegate();
    }

    #region Actions
    void InitActions()
    {

    }

    void AddAction()
    {
        LunarConsole.RegisterAction("[Common] Action Common 1", Action_Common_1);
        LunarConsole.RegisterAction("[Common] Action Common 2_1", () => Action_Common_2(1));
        LunarConsole.RegisterAction("[Common] Action Common 2_2", () => Action_Common_2(2));
        LunarConsole.RegisterAction("[InGame] Action InGame", Action_InGame);
        LunarConsole.RegisterAction("[OutGame] Action OutGame", Action_OutGame);
    }

    void RemoveAction()
    {
        LunarConsole.UnregisterAllActions(this); // don't forget to unregister!
    }

    void Action_Common_1()
    {
        // do something..
    }

    void Action_Common_2(int _iValue)
    {
        // do something..
    }

    void Action_InGame()
    {
        // do something..
    }

    void Action_OutGame()
    {
        // do something..
    }
    #endregion

    #region Variables
    void InitVariables()
    {
        Variables.myBool.BoolValue = true;
        Variables.myFloat.FloatValue = 3.14f;
        Variables.myInt.IntValue = 10;
        Variables.myString.Value = "Test";
        //Variables.myEnum.EnumValue = eVariablesType.Two;  // Can't
    }

    void AddDelegate()
    {
        // myBool
        Variables.CVarChangedDelegate_myBool = (value) =>
        {
            UnityEngine.Debug.Log("[myBool is] " + value);
            OnCVarChangedDelegate_myBool(value);
        };
        Variables.myBool.AddDelegate(Variables.CVarChangedDelegate_myBool);

        // myFloat
        Variables.CVarChangedDelegate_myFloat = (value) =>
        {
            UnityEngine.Debug.Log("[myFloat is] " + value);
            OnCVarChangedDelegate_myFloat(value);
        };
        Variables.myFloat.AddDelegate(Variables.CVarChangedDelegate_myFloat);

        // myInt
        Variables.CVarChangedDelegate_myInt = (value) =>
        {
            UnityEngine.Debug.Log("[myInt is] " + value);
            OnCVarChangedDelegate_myInt(value);
        };
        Variables.myInt.AddDelegate(Variables.CVarChangedDelegate_myInt);

        // myString
        Variables.CVarChangedDelegate_myString = (value) =>
        {
            UnityEngine.Debug.Log("[myString is] " + value);
            OnCVarChangedDelegate_myString(value);
        };
        Variables.myString.AddDelegate(Variables.CVarChangedDelegate_myBool);

        // myEnum
        Variables.CVarChangedDelegate_myEnum = (value) =>
        {
            UnityEngine.Debug.Log("[myEnum is] " + value);
            CEnumVar<eVariablesType> cEnumVar = (CEnumVar<eVariablesType>)value;
            eVariablesType eEnumValue = cEnumVar.EnumValue;
            OnCVarChangedDelegate_myEnum(eEnumValue);
        };
        Variables.myEnum.AddDelegate(Variables.CVarChangedDelegate_myEnum);
    }

    void RemoveDelegate()
    {
        Variables.myBool.RemoveDelegate(Variables.CVarChangedDelegate_myBool);
        Variables.myFloat.RemoveDelegate(Variables.CVarChangedDelegate_myFloat);
        Variables.myInt.RemoveDelegate(Variables.CVarChangedDelegate_myInt);
        Variables.myString.RemoveDelegate(Variables.CVarChangedDelegate_myString);
        Variables.myEnum.RemoveDelegate(Variables.CVarChangedDelegate_myEnum);
    }

    void OnCVarChangedDelegate_myBool(bool _bValue)
    {
        // do something..
    }

    void OnCVarChangedDelegate_myFloat(float _fValue)
    {
        // do something..
    }

    void OnCVarChangedDelegate_myInt(int _iValue)
    {
        // do something..
    }

    void OnCVarChangedDelegate_myString(string _strValue)
    {
        // do something..
    }

    void OnCVarChangedDelegate_myEnum(eVariablesType _eVariablesType)
    {
        // do something..
    }
    #endregion
}

 

 

[참조] https://github.com/SpaceMadness/lunar-unity-console/wiki/Actions-and-Variables#listening-for-variable-changes

 

Actions and Variables

High-performance Unity iOS/Android logger built with native platform UI - SpaceMadness/lunar-unity-console

github.com

 

반응형
Posted by blueasa
, |

[링크]

https://psh10004okpro.com/entry/Unity-%EB%A1%9C%EC%BB%AC%EB%9D%BC%EC%9D%B4%EC%A7%95-TextMeshPro-TMPFont-%ED%95%9C%EA%B5%AD%EC%96%B4-%EC%9D%BC%EB%B3%B8%EC%96%B4-%EC%A4%91%EA%B5%AD%EC%96%B4-%EB%8C%80%ED%91%9C%EB%82%98%EB%9D%BC-%EC%9C%A0%EB%8B%88%EC%BD%94%EB%93%9C-%EB%B2%94%EC%9C%84

 

Unity 로컬라이징 TextMeshPro TMP_Font 유니코드 범위

Unity TextMeshPro Character Set : Unicode Range (Hex) 핑크색 배경 글이 필수 주요 문자입니다. 한글 구분 시작 끝 한글(자음, 모음) 1100 11FF 호환용 한글(자음, 모음) 3131 318F 한글 음절(가~힣) AC00 D7A3 한자 구분

psh10004okpro.com

 

반응형
Posted by blueasa
, |

AppIconChangerUnity
https://github.com/kyubuns/AppIconChangerUnity

Change the app icon dynamically in Unity
Support for new icon formats in Xcode13
-> This means that A/B Test on AppStore is also supported.


- iOS only, require iOS 10.3 or later



## Instructions
- Import by PackageManager `https://github.com/kyubuns/AppIconChangerUnity.git?path=Assets/AppIconChanger`
- (Optional) You can import a demo scene on PackageManager.




## Quickstart

- Create `AppIconChanger > AlternateIcon` from the context menu




- Set the name and icon





- The following methods are available




## Tips


### What is the best size for the app icon?

When the Type of `AlternateIcon` is set to Auto Generate, the icon will be automatically resized at build time, so there is nothing to worry about. (The maximum size is 1024px.)
If you want to control it in detail, you can change the Type to Manual.





## Requirements
- Unity 2020.3 or higher.
- Xcode 13 or higher.



## License
MIT License (see LICENSE)

 

 

[출처] https://forum.unity.com/threads/appiconchangerunity-change-the-app-icon-dynamically-in-unity-ios-only.1245787/

 

AppIconChangerUnity - Change the app icon dynamically in Unity (iOS only)

AppIconChangerUnity https://github.com/kyubuns/AppIconChangerUnity Change the app icon dynamically in Unity Support for new icon formats in Xcode13 ->...

forum.unity.com

 

반응형
Posted by blueasa
, |
using UnityEngine;

public class OcclusionCulling2D : MonoBehaviour
{
    [System.Serializable] public class ObjectSettings
    {
        [HideInInspector] public string title;
        public GameObject theGameObject;

        public Vector2 size = Vector2.one;
        public Vector2 offset = Vector2.zero;
        public bool multiplySizeByTransformScale = true;

        public Vector2 sized { get; set; }
        public Vector2 center { get; set; }
        public Vector2 TopRight { get; set; }
        public Vector2 TopLeft { get; set; }
        public Vector2 BottomLeft { get; set; }
        public Vector2 BottomRight { get; set; }
        public float right { get; set; }
        public float left { get; set; }
        public float top { get; set; }
        public float bottom { get; set; }

        public Color DrawColor = Color.white;
        public bool showBorders = true;
    }

    public ObjectSettings[] objectSettings = new ObjectSettings[1];

    private Camera camera;
    private float cameraHalfWidth;

    public float updateRateInSeconds = 0.1f;

    private float timer;

    void Awake(){ 
        camera = GetComponent<Camera>();
        cameraHalfWidth = camera.orthographicSize * ((float)Screen.width / (float)Screen.height);

        foreach(ObjectSettings o in objectSettings){
           o.sized = o.size * (o.multiplySizeByTransformScale ? new Vector2(Mathf.Abs(o.theGameObject.transform.localScale.x), Mathf.Abs(o.theGameObject.transform.localScale.y)) : Vector2.one);
            o.center = (Vector2)o.theGameObject.transform.position + o.offset;

            o.TopRight = new Vector2(o.center.x + o.sized.x, o.center.y + o.sized.y);
            o.TopLeft = new Vector2(o.center.x - o.sized.x, o.center.y + o.sized.y);
            o.BottomLeft = new Vector2(o.center.x - o.sized.x, o.center.y - o.sized.y);
            o.BottomRight = new Vector2(o.center.x + o.sized.x, o.center.y - o.sized.y);

            o.right = o.center.x + o.sized.x;
            o.left = o.center.x - o.sized.x;
            o.top = o.center.y + o.sized.y;
            o.bottom = o.center.y - o.sized.y;
        }
    }

    void OnDrawGizmosSelected()
    {
        foreach(ObjectSettings o in objectSettings)
        {
            if(o.theGameObject)
            {
                o.title = o.theGameObject.name;

                if(o.showBorders)
                {
                    o.TopRight = new Vector2(o.center.x + o.sized.x, o.center.y + o.sized.y);
                    o.TopLeft = new Vector2(o.center.x - o.sized.x, o.center.y + o.sized.y);
                    o.BottomLeft = new Vector2(o.center.x - o.sized.x, o.center.y - o.sized.y);
                    o.BottomRight = new Vector2(o.center.x + o.sized.x, o.center.y - o.sized.y);
                    Gizmos.color = o.DrawColor;
                    Gizmos.DrawLine(o.TopRight, o.TopLeft);
                    Gizmos.DrawLine(o.TopLeft, o.BottomLeft);
                    Gizmos.DrawLine(o.BottomLeft, o.BottomRight);
                    Gizmos.DrawLine(o.BottomRight, o.TopRight);
                }
            }
        }
    }
    
    void FixedUpdate()
    {
        timer += Time.deltaTime;
        if(timer > updateRateInSeconds) timer = 0;
        else return;

        float cameraRight = camera.transform.position.x + cameraHalfWidth;
        float cameraLeft = camera.transform.position.x - cameraHalfWidth;
        float cameraTop = camera.transform.position.y + camera.orthographicSize;
        float cameraBottom = camera.transform.position.y - camera.orthographicSize;

        foreach(ObjectSettings o in objectSettings)
        {
            if(o.theGameObject)
            {
                bool IsObjectVisibleInCastingCamera = o.right > cameraLeft & o.left < cameraRight & // check horizontal
                                                      o.top > cameraBottom & o.bottom < cameraTop; // check vertical
                o.theGameObject.SetActive(IsObjectVisibleInCastingCamera);
            }
        }
    }
}

[출처] https://www.youtube.com/watch?v=hbBDqdoHUpE

반응형
Posted by blueasa
, |

Unity 2021.3.36f1

Xcode 15.3

----

 

[결론] 

Privacy Manifest 관련 대응 버전은 Unity 2023.2.13, 2022.3.21, 2021.3.36 이라고 한다.(메이저 버전별 해당 버전이후로 업데이트 필요)

위에 적힌 버전 이상 설치 돼 있으면, iOS 빌드를 할 때 알아서 PrivacyInfo.xcprivacy를 생성해 준다고 한다.

Unity 2021.3.36f1으로 iOS 빌드했는데, PrivacyInfo.xcprivacy도 추가돼있고, 내용도 제대로 들어있는 걸 확인했다.(필요한 거 확인해서 알아서 넣어준다고 어디선가 적힌걸 본 것 같은데..)

Unity 2021.3.36f1으로 iOS 빌드하면 PrivacyInfo.xcprivacy가 자동 생성된다.

 

쉽게 해결하려면 엔진 버전업 하자.

 

SDK 등 Third Party 플러그인들은 해당 Third Party에서 업데이트 대응해줘야 되는거니 내가 뭔가 할 건 없는 것 같다.

 

P.s. 

수동으로 작업하려면 'PolicyInfo.xcprivacy' 파일을 생성해서 수정하고, '../Assets/Plugins' 폴더에 파일을 넣어두면 된다고 한다.

자동으로 추가되는 정보 외에 별도로 추가하고 싶으면 파일을 해당 위치에 넣어두자.

 

[참조] https://forum.unity.com/threads/apple-privacy-manifest-updates-for-unity-engine.1529026/

 

Official - Apple privacy manifest updates for Unity Engine

Introduction At WWDC 2023 Apple announced a number of additions to its privacy program, including the introduction of Privacy Manifests. Since then,...

forum.unity.com

 

----

[링크1] [Unity] Apple Privacy Manifest 대응

 

[Unity] Apple Privacy Manifest 대응

안녕하세요. Apple이 공개한 Privacy Manifest를 반드시 포함해야 하는 SDK 목록 중에 UnityFramework가 있었고 이번에 Unity에서 공식적인 입장과 가이드를 공개했어요. https://forum.unity.com/threads/apple-privacy-man

phillip5094.tistory.com

 

[링크2] 개인정보 보호 매니페스트 및 서명을 필요로 하는 SDK

 

개인정보 보호 매니페스트 및 서명을 필요로 하는 SDK

안녕하세요. 이전에 Privacy manifest에 대해서 공부했는데요. [WWDC23] Get started with privacy manifests 안녕하세요. 이번엔 WWDC23 'Get started with privacy manifests' 세션을 보고 내용 정리해 볼게요. #개요 앱 사용

phillip5094.tistory.com

 

 

[참조] 【Xcode/iOS】Privacy Manifests에 대응하는 방법! PrivacyInfo.xcprivacy란?

 

Webエンジニア学習部屋

駆け出しwebエンジニアのあめの学習記録webサイトです。WordPressなどのCMSを使わずに自分でHTMLファイルやphpファイルを作成しサーバーにアップしています。プログラミングで悩んだポイントや

appdev-room.com

반응형
Posted by blueasa
, |

Unity 2021.3.33f1

Xcode 15.3

AppsFlyer v6.13.10

----

 

[추가] 2024-04-17

AppsFlyer v6.14.0에서 Xcode 15.3 관련 버그가 수정됐다.

v6.14.0으로 버전업 하면 해결.

 

[AppsFlyer Releases] https://github.com/AppsFlyerSDK/appsflyer-unity-plugin/releases

AppsFlyer v6.14.0

 

 

----

[빌드 에러 로그]

----

[Error] Type specifier missing, defaults to 'int'; ISO C99 and later do not support implicit int
[Warning] Incompatible pointer to integer conversion passing 'void *' to parameter of type 'int'
----
BOOL __swizzled_didReceiveRemoteNotification(id self, SEL _cmd, UIApplication* application, NSDictionary* userInfo,void (^UIBackgroundFetchResult)(void) ) {
    NSLog(@"swizzled didReceiveRemoteNotification");
    
    [[AppsFlyerLib shared] handlePushNotification:userInfo];
    
    if(__original_didReceiveRemoteNotification_Imp){
        return ((BOOL(*)(id, SEL, UIApplication*, NSDictionary*, (UIBackgroundFetchResult)))__original_didReceiveRemoteNotification_Imp)(self, _cmd, application, userInfo, nil);
    }
    return YES;
}

----

 

[2024-03-13] Xcode 15.3이 나와서 업데이트 하고 iOS 빌드 하니 위와 같은 에러가 난다.

에러 위치는 AppsFlyer 인데..

검색(아래 링크 참조)해보니 Xcode 15.3 업데이트 하고나서 같은 에러를 겪고 있는 것 같다.

AppsFlyer에서 대응해줘야 될 것 같은데, 링크에서 임시방편으로 수정하는 방법을 알려주고 있다.

일단 빌드를 할 수 없으니 Xcode 15.2로 다운그레이드 했다.

 

[결론]

Xcode 15.3을 쓰려면 두가지 방법중 하나를 고르자.

1. [임시방편] 당장 써야겠다면, 해당 에러 위치를 아래 내용을 참조해서 수정하자.

    1.1. AppsFlyer+AppController.m 파일 위치

          1.1.1. 수동 설치 했으면 : ..\Assets\AppsFlyer\Plugins\iOS\AppsFlyer+AppController.m

          1.1.2. Package로 설치했으면 : /Library/PackageCache/appsflyer-unity-plugin@xxxxxxxxxx/AppsFlyer+AppController.m

    1.2. AppsFlyer+AppController.m line:144 위치의 return 하는 소스를 아래와 같이 수정하자.

          -> return ((BOOL(*)(id, SEL, UIApplication*, NSDictionary*, int(UIBackgroundFetchResult)))__original_didReceiveRemoteNotification_Imp)(self, _cmd, application, userInfo, nil);

[참조] https://github.com/AppsFlyerSDK/appsflyer-unity-plugin/issues/263

 

[추가]

Xcode 15.3부터 C99 이상 강제하는 것 같다.

AppsFlyer 현재 버전이 C89인지.. C99 지원을 안하는 것 같은데..

결국 AppsFlyer가 C99 대응 업데이트를 해줘야 Xcode 15.3에서 정상적으로 빌드가 되지 싶다.

 

 

[참조] https://github.com/AppsFlyerSDK/appsflyer-unity-plugin/issues/263

 

Cannot build with xCode 15.3 · Issue #263 · AppsFlyerSDK/appsflyer-unity-plugin

Hello! I just updated xCode to 15.3 and there seems to be an ISO C99 issue with the AppsFlyer+AppController.mfile. The attached image is a screenshot from xCode 15.2 where the issue Type specifier ...

github.com

반응형
Posted by blueasa
, |

Unity 2021.3.35f1

GoogleMobileAds 8.7.0

----

 

작년 10월쯤부터 ANR이 갑자기 많아져서 뭘까 했는데, 이제는 커트라인까지 넘어서 경고가 뜨고 있다.

그래서 ANR쪽 로그를 보니 아래와 같은 로그가 뜬다.

 

[ANR 로그]

com.google.android.ump:user-messaging-platform@@2.1.0 
- com.google.android.ump.ConsentInformation$OnConsentInfoUpdateSuccessListener.onConsentInfoUpdateSuccess
Input dispatching timed out

 

해당 ANR이 생긴 시점에 업데이트 된 GoogleMobileAds 버전을 보니 8.4.1이다.

GoogleMobileAds 8.4.1버전부터 현재 기준 최신인 8.7.0까지 같은 문제가 아직도 고쳐지지 않고 여전히 있는 것 같다.

 

UMP면 GoogleMoblieAds인데..

인터넷 검색해보니 아래 링크처럼 똑같은 문제를 겪는 사람들이 많다.

 

[ANRs Caused by UMP 2.1.0 Callbacks] https://groups.google.com/g/google-admob-ads-sdk/c/jrOwbA7lgdQ

 

ANRs Caused by UMP 2.1.0 Callbacks

Hi Nick, I am using  Unity 2021.3.34, Google Mobile Ads Unity Plugin v8.6.0, Gradle version 6.7.1, for resolve dependencies EDM4U. Regards, Alex. вторник, 16 января 2024 г. в 21:02:33 UTC+2, Mobile Ads SDK Forum Advisor:

groups.google.com

 

링크에는 최근까지 글이 올라오고 있는데, 작년부터 있던 문제가 현재도 고쳐지지 않고 있는 것 같다.

결국 GoogleMobileAds에서 직접 수정한 버전이 올라와야 이 문제는 끝날 것 같다.

 

요즘은 Unity도, Xcode(Apple)도, Google(Firebase/GoogleMobileAds)도 다들 뭔가..

나사가 빠져 있는 것 같다.

 

 

 

 
반응형
Posted by blueasa
, |

Unity 2021.3.33f1

----

 

[추가]

에디터 플랫폼(Android/iOS) 별로 체크되는 파일이 다른 것 같다.

Editor - iOS 에디터에서 AssetDatabase.ForceReserializeAssets();를 했는데,

Editor - Android 에디터에서 변경 안된 것들이 있어서 Android 플랫폼 에디터에서 한 번 더 Reserialize를 했다.

양 쪽 플랫폼에서 한 번씩 돌려야되나.. 싶다.

 

----

프로젝트 개발하면서 유니티 메이저 버전 업그레이드나 다운그레이드를 하게 될 경우가 있는데,

이번에는 Unity 2022의 누수 및 크래시 버그가 심각해서 Unity 2021로 내려온 후에 알 수 없는 버그가 종종 나오게 됐다.

(잘되던게 UI 하나 변경했더니 다른 팀원 유니티에서 이상하게 뜬다던지..)

 

확인해본 바로는 meta 파일은 실제 에셋을 사용하는 시점에만 갱신해서

이미 Unity 2021인데도 Unity 2022의 메타파일을 사용하고 있다가, 수정하게 되면서 Unity 2021 meta 파일로 변경되면서 라이브러리가 꼬이는 것 같다.

Unity 메이저 버전이 바뀌면 meta 파일을 재정리를 좀 해주면 좋을텐데 안하는 듯..

 

그래서 프로젝트 전체를 강제로 Reserialize를 진행했다.

AssetDatabase.ForceReserializeAssets();

 

 

[참조] https://docs.unity3d.com/ScriptReference/AssetDatabase.ForceReserializeAssets.html

 

Unity - Scripting API: AssetDatabase.ForceReserializeAssets

When Unity loads old data from an asset or Scene file, the data is dynamically upgraded in memory, but not written back to disk unless the user does something that explicitly dirties the object (like changing a value on it). This method allows you to proac

docs.unity3d.com

 

 

 

반응형
Posted by blueasa
, |

Unity 2021.3.36f1

GoogleMobileAds 9.0.0

AppsFlyer 6.13.10

----

 

[추가] 2024-04-05

GDPR 동의가 엮이면서 '사용자 동의'를 받은 후로 앱 측정을 지연 시켜야 돼서,

GoogleMobileAdsSettings.asset에서 'Delay app measurement'를 체크해 줘야 되는 것 같다.

[참조] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko

 

----

이번에 GDPR 동의 관련 작업을 하게 되면서 이런저런 엮이는 것들이 많았는데..

문제가 됐던 이슈 리스트 정리해 봄

 

1) GDPR 동의를 추가하고나니 iOS의 IDFA(AppTrackingTransparency) 동의와 내용이 겹친다고

    "앞에서 거부했는데 왜 뒤에 또 동의창을 뛰우냐~" 라는 내용으로 iOS 검수 리젝 됨.

     - 참고로 iOS ATT(IDFA)와 GDPR의 순서는 GDPR먼저 뛰우고 ATT(IDFA)를 판단하라고 함

https://developers.google.com/admob/android/privacy/gdpr?hl=ko

 

    그래서 확인해보니, Admob(GoogleMobileAds)에서 GDPR과 IDFA 둘 다 제공하고 Admob에서 진행하면 알아서 유기적으로 처리해준다.

    - [EEA( European Economic Area)인 경우] iOS에서 GDPR 동의 거부하면 IDFA 동의창 안 뜸, GDPR 동의하면 IDFA 동의 창 뜸.

    - [EEA( European Economic Area)가 아닌 경우] iOS에서 IDFA 안내 및 동의창만 뜸.

 

[참고] https://blueasa.tistory.com/2789

 

[검수리젝] GDPR/IDFA(ATT) 관련 검수 리젝

Unity 2021.3.35f1 GoogleMobileAds 8.7.0 ---- GDPR 동의 로직 추가하고 iOS 검수 넣었더니 리젝 됐다. 사유는 대충 정리하면, GDPR 팝업에서 '거부'를 했는데, 같은 이슈인 'IDFA(AppTrackingTransparency)' 동의 여부를

blueasa.tistory.com

 

2) AppsFlyer도 이번에 버전업(6.13.0) 해서 보니, GDPR 관련 설정 옵션(AppsFlyerConsent 클래스가 생김)이 생겼다.

    아래 예시와 같이 GDPR 정보 셋팅 후, AppsFlyer를 Initialize 하라고 한다.

// If the user is subject to GDPR - collect the consent data
    // or retrieve it from the storage
    ...
    // Set the consent data to the SDK:
    AppsFlyerConsent consent = AppsFlyerConsent.ForGDPRUser(true, true);
    AppsFlyer.setConsentData(consent);
        
    AppsFlyer.startSDK();

 

    자세한 내용은 아래 AppsFlyer 문서를 참고하자.

[참고] https://ko.dev.appsflyer.com/hc/docs/basicintegration

 

연동

AppsFlyerObject 프리팹을 사용하거나 수동으로 플러그인을 초기화할 수 있습니다. AppsFlyerObject.prefabManual의 integrationCollect IDFA를 ATTrackingManagerSending SKAN 포스트백과 함께 사용하여 AppsflyerMacOS initializat

ko.dev.appsflyer.com

 

3) 위 AppsFlyer 연동하려고 보니 아래와 같이 GDPR 일 때, 넘겨주는 매개변수 2개가 보인다.

    - hasConsentForDataUsage : GDPR 동의 상태

    - hasConsentForAdsPersonaliation : 광고 개인화 동의 상태

public static AppsFlyerConsent ForGDPRUser(bool hasConsentForDataUsage, bool hasConsentForAdsPersonalization)
{
    return new AppsFlyerConsent(true, hasConsentForDataUsage, hasConsentForAdsPersonalization);
}

public static AppsFlyerConsent ForNonGDPRUser()
{
    return new AppsFlyerConsent(false, false, false);
}

 

- hasConsentForDataUsage는 GDPR 동의 상태라서 Admob에도 정보가 있어서 받아오면 되겠는데,

- hasConsentForAdsPersonaliation은 GDPR 동의 창 세부 상태에서 광고 개인화 동의를 체크를 하는데 정보를 어떻게 받는지 몰라서 이리저리 찾아보다가 IABTCF_VendorConsents가 광고 개인화 관련 동의 값이란 걸 알게 됐다.

 

[참고] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform

 

Mandatory Consent for Admob User Messaging Platform

I switched from the deprecated GDPR Consent Library to the new User Messaging Platform, and used the code as stated in the documentation. I noticed that when the user clicks on Manage Options then

stackoverflow.com

 

관련해서 잘 정리해 놓은 글을 찾아서 블로그에도 올려뒀다.

[링크] https://blueasa.tistory.com/2791

 

[펌] GDPR에 대해 알아보자 (Feat 애드몹)

1 얘들아 안녕, 다들 개발 잘 하고 있어? 저번 달에 GDPR 구현하다가 막혀서 여기다 글을 올렸었는데, 다행히 이젠 해결한 것 같아. 그 당시 해결하면 팁 남겨달라던 친구의 댓글이 기억나서 다시

blueasa.tistory.com

 

 

위의 링크글에서 동의 정보 관련에 필요한 CurrentGDPR 클래스 설명이 잘 돼 있다.

원래 CurrentGDPR 클래스는 GoogleMobileAds 8.6.0 이전 버전 기준으로 만들어져서,

GoogleMobileAds 8.7.0에 추가된 ApplicationPreferences를 사용해서 OS에 따라 분기하지 않아도 되도록 수정해서 아래 올려둔다.

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

using System.Linq;
using GoogleMobileAds.Api;

public static class CurrentGDPR
{
    private static bool _isGdprOn;
    private static string _purposeConsent, _vendorConsent, _vendorLi, _purposeLi, _partnerConsent;

    static CurrentGDPR()
    {
        InitData();
    }

    public static void InitData()
    {
        int gdprNum = 0;

        // [GoogleMobileAds 8.7.0]에서 ApplicationPreferences 추가됨
        // [참고] https://stackoverflow.com/questions/77838024/admob-gdpr-ump-issue-empty-iab-tcf-strings-on-android-after-user-consent
        // [참고] https://developers.google.com/admob/android/privacy/gdpr?hl=ko
        gdprNum = ApplicationPreferences.GetInt("IABTCF_gdprApplies");
        _purposeConsent = ApplicationPreferences.GetString("IABTCF_PurposeConsents");
        _vendorConsent = ApplicationPreferences.GetString("IABTCF_VendorConsents");
        _vendorLi = ApplicationPreferences.GetString("IABTCF_VendorLegitimateInterests");
        _purposeLi = ApplicationPreferences.GetString("IABTCF_PurposeLegitimateInterests");
        _partnerConsent = ApplicationPreferences.GetString("IABTCF_AddtlConsent");

        #region [GoogleMobileAds 8.6.0 이전] 버전
//#if UNITY_EDITOR // 에디터에서는 자바 호출이 에러나서 에외처리
//        gdprNum = 1;
//        _purposeConsent = "0000000000";
//        _vendorConsent = "0000000000";
//        _vendorLi = "";
//        _purposeLi = "";
//        _partnerConsent = "";
//#elif UNITY_ANDROID
//        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
//        AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
//        AndroidJavaClass preferenceManagerClass = new AndroidJavaClass("android.preference.PreferenceManager");
//        AndroidJavaObject sharedPreferences = 
//                 preferenceManagerClass.CallStatic<AndroidJavaObject>("getDefaultSharedPreferences", currentActivity);

//        gdprNum = sharedPreferences.Call<int>("getInt", "IABTCF_gdprApplies", 0);
//        _purposeConsent = sharedPreferences.Call<string>("getString", "IABTCF_PurposeConsents", "");
//        _vendorConsent = sharedPreferences.Call<string>("getString", "IABTCF_VendorConsents", "");
//        _vendorLi = sharedPreferences.Call<string>("getString", "IABTCF_VendorLegitimateInterests", "");
//        _purposeLi = sharedPreferences.Call<string>("getString", "IABTCF_PurposeLegitimateInterests", "");
//        _partnerConsent = sharedPreferences.Call<string>("getString", "IABTCF_AddtlConsent", "");
//#elif UNITY_IOS
//        gdprNum = PlayerPrefs.GetInt("IABTCF_gdprApplies", 0);
//        _purposeConsent = PlayerPrefs.GetString("IABTCF_PurposeConsents", "");
//        _vendorConsent = PlayerPrefs.GetString("IABTCF_VendorConsents", "");
//        _vendorLi = PlayerPrefs.GetString("IABTCF_VendorLegitimateInterests", "");
//        _purposeLi = PlayerPrefs.GetString("IABTCF_PurposeLegitimateInterests", "");
//        _partnerConsent = PlayerPrefs.GetString("IABTCF_AddtlConsent", "");
//#endif
        #endregion

        // 0 이면 아예 GDPR 대상이 아님. 1이어야 GDPR
        if (gdprNum == 1)
            _isGdprOn = true;
        else
            _isGdprOn = false;

        Debug.Log("GDPR을 띄우는가? " + _isGdprOn);
        Debug.Log("광고에 필요한 권한 동의: " + _purposeConsent);
        Debug.Log("광고에 필요한 적법관심(광고 개인화?) 동의: " + _vendorConsent);
        Debug.Log("구글에 동의처리가 되어있는가?: " + _vendorLi);
        Debug.Log("구글에 적법관심(광고 개인화?) 처리 여부: " + _purposeLi);
        Debug.Log("파트너 네트워크 여부: " + _partnerConsent);
    }

    // GDPR을 띄워야 할 유저인지(= 유럽 + 영국) 리턴
    public static bool IsGDPR()
    {
        return _isGdprOn;
    }

    // 광고가 보여지는지 여부 리턴
    public static bool CanAdShow()
    {
        int googleId = 755;
        bool hasGoogleVendorConsent = HasAttribute(_vendorConsent, googleId);
        bool hasGoogleVendorLi = HasAttribute(_vendorLi, googleId);

        // 광고 가능 - 비개인화 광고
        // return HasConsentFor(new List<int> { 1 }, _purposeConsent, hasGoogleVendorConsent)
        //        && HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 }, 
        //            _purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);

        // 광고 가능 - 제한적인 광고 - 1에 대한 권한이 없어도 됨 ㅇㅇ
        return HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 },
                   _purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);
    }

    // 개인화 광고가 보여지는지 여부 리턴
    public static bool CanShowPersonalizedAds()
    {
        int googleId = 755;
        bool hasGoogleVendorConsent = HasAttribute(_vendorConsent, googleId);
        bool hasGoogleVendorLi = HasAttribute(_vendorLi, googleId);

        return HasConsentFor(new List<int> { 1, 3, 4 }, _purposeConsent, hasGoogleVendorConsent)
               && HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 },
                   _purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);
    }

    public static bool IsPartnerConsent(string partnerID) // 파트너 권한 있는지 확인
    {
        return _partnerConsent.Contains(partnerID);
    }

    // 이진 문자열의 "index" 위치에 "1"이 있는지 확인합니다(1 기반).
    private static bool HasAttribute(string input, int index)
    {
        return (index <= input.Length) && (input[index - 1] == '1');
    }

    // 목적 목록에 대한 동의가 주어졌는지 확인합니다.
    private static bool HasConsentFor(List<int> purposes, string purposeConsent, bool hasVendorConsent)
    {
        return purposes.All(p => HasAttribute(purposeConsent, p)) && hasVendorConsent;
    }

    // 목적 목록에 대한 공급자의 동의 또는 정당한 이익이 있는지 확인합니다.
    private static bool HasConsentOrLegitimateInterestFor(List<int> purposes, string purposeConsent, string purposeLI, bool hasVendorConsent, bool hasVendorLI)
    {
        return purposes.All(p =>
            (HasAttribute(purposeLI, p) && hasVendorLI) ||
            (HasAttribute(purposeConsent, p) && hasVendorConsent));
    }
}

 

 

[결론]

여기서 내가 필요했던

AppsFlyer에 넘기려던 정보는 CurrentGDPR.CanShowPersonalizedAds() 였다.

CurrentGDPR .IsGDPR()도 써도 될 것 같긴한데..

GoogleMobileAds에서 ConsentInformation.ConsentStatus를 사용중인데 뭘 써야될지는 좀 테스트 봐야 될 것 같다.

 

 

  [초기화 순서]

1. GoogleMobileAds -

    1.1. EEA 일 경우

        1.1.1. GDPR 동의 진행

            1.1.1.1. GDPR 동의 하면, (iOS만)IDFA 동의 진행(GDPR 동의 요청하면 IDFA도 필요하면 자동으로 진행 된다.)

            1.1.1.2. GDPR 동의 안하면, IDFA 동의 Skip(IDFA도 거부로 판단)

    1.2. EEA가 아닐 경우

        1.2.1. GDPR 동의 진행 안함

        1.2.2. (iOS만) IDFA 동의 진행

    1.3. GoogleMobileAds 초기화 진행

 

2. GDPR / (iOS만)IDFA 동의 진행 완료 후, AppsFlyer 초기화 진행

    2.1. AppsFlyerConsent.ForGDPRUser() || AppsFlyerConsent.ForNonGDPRUser() 셋팅

    2.2. AppsFlyer.startSDK() 실행

 

 

[참고] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko

[참고] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform

[참고] https://stackoverflow.com/questions/77838024/admob-gdpr-ump-issue-empty-iab-tcf-strings-on-android-after-user-consent

[참고] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform

[참고] https://gall.dcinside.com/mgallery/board/view/?id=game_dev&no=150987

[참고] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko

[참고] https://ko.dev.appsflyer.com/hc/docs/basicintegration

반응형
Posted by blueasa
, |