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

카테고리

분류 전체보기 (2797)
Unity3D (853)
Programming (479)
Server (33)
Unreal (4)
Gamebryo (56)
Tip & Tech (185)
협업 (61)
3DS Max (3)
Game (12)
Utility (68)
Etc (98)
Link (32)
Portfolio (19)
Subject (90)
iOS,OSX (55)
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

[링크] https://rito15.github.io/posts/unity-mobile-optimization/

 

유니티 - 모바일 성능 최적화

프로젝트 설정

rito15.github.io

 

반응형
Posted by blueasa
, |

게임 출시를 앞두고, Android플랫폼의 심각한 발열과 낮은 프레임에 대한 최적화를 수행했다.

여러개의 프로젝트를 런칭하면서 쌓인 노하우 및 새로 공부한 지식으로 유의미한 최적화 성과를 거두었기에 최적화 방법을 공유한다.

 

먼저, 최적화 방법 공유에 앞서 최적화 대상 프로젝트의 스펙은 다음과 같다

-Unity 2021.3.8f로 개발

-URP 프로젝트

-NGUI사용

 

최적화 성과 (갤럭시 s8기준)

FPS :  20~30 -> 50~60

용량 : 400mb -> 300mb

발열 : S22에서 심각한 발열로 인해 30~40fps가 나옴 -> S22와 같은 심각한 발열 기기에서 거의 발열 없이 수십분 이상 구동 가능. 안정적인 60fps유지.

 

이와같은 성능을 유지하기 위해선 여러가지 복합적인 이유들이 있겠지만,

untiy editor의 stats기준으로 batch는 많아도 100~200수준을 잡아주는 것이 좋다

(스크립트 병목과 높은 vertex로 인한 성능 하락은 고려하지 않는다)

 

먼저 최적화에 앞서 본인 프로젝트에 대한 프로파일링을 우선 진행하길 바란다.

필자는 최적화에 Unity Profiler와 Memory Profiler(실험적) 을 사용했다

 

profiler를 통해 스크립트 병목의 원인을 추적하고, 메모리 스냅샷을 사용해 메모리 사용량을 최적화하라

 

그리고 최적화의 대상이 CPU처리인지 GPU처리인지도 파악해야 한다.

 

 

 

NGUI관련 최적화

1. UI Atlas의 depth최적화, panel을 최적화한다. 

ngui에서 가장 기본적인 것으로 동일한 panel을 사용하는 widget들의 depth를 정리하여 최적화 해주어야 한다.

무분별한 panel의 사용으로 동일한 atlas를 사용하는 widget이 쪼개져 중복적인 draw call을 야기하지 않도록 해야한다.

 

ngui에서 A와 B아틀라스 2개를 통해 UI를 구성한다면 흔이 2의 draw call을 예상할 것 이다.

하지만 만약 depth가 A A B A BB이런 식으로 ui를 배치한다면 draw call은 4가 된다

2의 draw call을 얻기 위해선 A A A B B B B와 같이 atlas에 따른 ui depth를 정리해주어야 한다.

 

하나의 atlas로 draw되다가 다른 atals를 만나면 중간에 batching이 끊기게 된다.

 

2. 동일한 atlas를 사용하는 widget은 단일 panel에서 최대한 순서를 정렬하여 처리해준다.

필자의 경우 ui atlas를 back / icon / overay / font 이렇게 4가지로 나누어, 정리했으며 

ui가 아무리 많아도 batch는 최대 4만 유지하도록 해주었다.

draw call의 병합 규칙은 두가지가 있는데, 하나는 material, texture, shader가 모두 같다는 것이고 다른 하나는 widget의 레이어가 같거나 인접해야 한다는 것이다.

 

3. NGUI에서 Transform의 변화, 생성, 삭제가 일어나는 객체를 조심해라

NGUI에서는 Panel에 속한 widget의 Transform이 변화(생성/삭제 포함) 하면 해당 panel에 있는 모든 widget을 다시 rendering한다. 

즉, Panel에 연출을 위한 widget이 tween position, scale등을 한다면 매 lateUpdate에 Panel은 drawcallfill을 다시 호출하는 것이다.

NGUI Widget의 Transform이 정적이지 않거나, 생성/삭제가 자주 일어나는 객체라면 완전히 다른 panel의 하위 객체로 두어 격리해두어야 한다.

즉, NGUI에서는 한 panel에 속한 widget의 상태에 변화가 생기면 해당 panel의 모든 atlas를 다시 렌더링해주게끔 되어있다.

 

3-1. 특히 NGUI의 UI Label을 조심해라

UI Label로 데미지 등을 처리할 경우 데미지 연출을 위해 UI Label이 매프레임 tweening하거나 animation을 재생할 수 있다. 이 경우 UI Label은 엄청난 성능 병목을 일으킨다.

매 프레임 움직이는 UILabel이 속한 panel의 draw fill/fill shadow/draw call fill을 야기시킨다.

특히 UI Label에 쉐도우나, 아웃라인 등이 들어가있다면 매프레임 Transform의 변화에 따라 Label을 다시 렌더링 해주게 된다.

이를 방지하기 위해 데미지 폰트 등은 이미지 폰트를 사용하는 것을 추천한다.

 

4. panel의 static상태 활성화

panel로 구성한 ui에 있는 모든 객체가 정적인 것이 보장된다면, panel의 static옵션을 활성화해주면 좋다.

ngui객체도 기본적으로 mono객체이며, static객체가 아니면 모든 ui객체에서 update/lateUpdate가 호출되기에 성능 병목이 발생한다.

정적인 객체의 경우 3번 방법을 참고하여 동적인 객체만 관리하는 panel등으로 격리해주자

 

 

그 외 Unity공통 최적화

1. GC를 줄이자

BinaryFormatter의 Serialize는 엄청난 양의 GC를 야기하며, 디바이스를 freeze시킨다. 이러한 동작은 매프레임 해주지 말고, 스케쥴러를 만들어 처리해주자

 

BigInteger.Parse(string)과 같은 value copy를 야기하는 처리를 피해주자

 

특정 타이밍에 GC.Collect를 수동으로 호출해주자

 

2. URP Setting의 Post Processing의 fast sRGB/Linear Conversions로 gamma공간을 사용하고 있다면 gpu의 부담을 덜어주자

 

3.  SustainedPerformanceMode를 활성화해 지속가능한 성능 옵션을 켜주어, 발열을 방지하자 (이로 인해 기본적인 fps의 하락이 있을 수 있음)

 

4. GPU의 병목/부하가 생기는 Project Setting의 옵션은 비활성화 하자 (Compute Skinning, Graphics Jobs...등)

 

5. Static Batching과 Dynamic batcing을 적극 활용하자

 

6. Texture compression format : ASTC, normal map encoding : DXT5nm-style

 

7. Screen.SetResoultion을 통해 화면 해상도를 지정해주자 (HD~FHD). 이를 해주지 않을 경우 기기의 해상도에 따라 렌더링되기 때문에 발열과 배터리소모의 원인이된다.

 

8. Target Framerate를 설정해주자 (60fps). 만약 프레임상한이 없다면 상위 기기에선 비정상적으로 높은 fps가 발생할 수 있으며 이는 발열/배터리소모의 원인이 될 수 있다.

 

9. realtime shadow는 최대한 피하고, 눈속임을 통해 그림자를 표시할 수 있는 방법을 찾자

quad를 통해 그림자를 표현하고, 이 quad를 dynamic batcing하는 등이 방법이다.

 

10. 가능한 많은 object pooling을 해주자. 

또한 NGUI객체의 경우 객체를 pool에서 회수/반환할 때 gameObject.SetActive보단 화면에 rendering되지 않는 먼 영역으로 좌표를 이동시켜 주는것이 좋은 선택지일 수 있다.

NGUI객체는 오브젝트가 enable/disable될 때 많은 병목을 일으키는 처리를 수행한다

 

11. 거리에 따른 최적화 방법을 숙지하라

Generate mipmap은 기본적을 texture의 용량을 1.3배 정도 부풀린다.

단 한 화면에 render되는 객체가 많을 경우 이는 한 화면의 메모리 부하를 줄여주므로 적극 활용해야한다.

 

또한 오브젝트에 LOD level을 세팅하면 거리에 따라 3D오브젝트의 렌더링 부하를 줄일 수 있지만 용량이 추가적으로 들어가는 것을 유의해야 한다.

 

Aniso Level을 주의해라. Aniso Level은 오브젝트를 그릴 때 단일 텍스쳐라도 멀어지면 흐리게 해주는 효과이다.

Aniso Level이 높을수록 선명해지겠지만 ,처리비용이 많이 든다.

프로젝트 세팅에서 확인하라 (기본값은 Forced On이다)

 

12. Asset의 Read/Write옵션을 주의하라

이 옵션은 기본적으로 비활성화지만 활성화 시 GPU메모리 뿐만 아니라 메인 메모리에도 복사되므로 소비량이 2배로 증가한다.

꼭 필요한 오브젝트의 경우만 식별하여 사용할 것

 

13. 파티클의 shader/material에 따라 render order를 조정해주어라.

NGUI와 같은 개념으로, 파티클에서 사용되는 shader/material도 render order를 조정해주어 batching되도록 해주자

 

14. Resources.Load를 매니징하자

디바이스로 부터 불러온 자원의 경우 사용 빈도에 따라 GC가 발생하지 않도록 상시 메모리에서 들고있는 등의 방법으로 최적화 해줄 수 있다.

 

15. Debug.Log를 주의해라

런타임 빌드 시 프로젝트 세팅에서 로깅 옵션을 완전히 비활성화 하거나, 스크립트에서 define으로 처리를 격리해주어라.

 

Unity Crash관련 팁

이 항목은 필자가 서비스중인 게임의 크래시 문제를 해결한 경험으로 작성되었기에, 지극히 주관적입니다.

 

1. Android환경에서 Vulkan API는 아직 불안정하다. Vulkan내부에서 native crash를 야기할 수 있다. (필자는 opengles 3을 권장)

 

2. Multithread rendering과 compute skinning도 native crash를 야기할 수 있다. 

 

3. 빈번한 Scene Load는 잦은 크래시를 야기할 수 있다.

맵의 전환 등으로 Scene이 load될 때 전 scene의 unload와 새로운 scene의 load중 스크립트가 unload된 scene의 객체 혹은 아직 preload상태의 scene에 접근할 경우 크래시가 발생한다.

 

추가적으로, unity의 scene 로드 구조는 순간적으로 폭발적인 메모리 사용량을 불러올 수 있다.

A장면에서 B장면으로 넘어갈 때 A장면과 B장면이 동시에 메모리에 올라가있는 순간이 존재하며 이 때 메모리 부족으로 크래시가 발생할 수 있다.

 

A장면에서 B장면으로 넘길 때 빈 장면(C)을 넣어 A장면의 메모리가 모두 해제되고, GC가 Collect되게 한 후 B장면을 로드해주는 방식은 아주 유용하다

 

4. Strip Engine Code옵션은 무조건 끄자

압축된 스크립트 dll을 참조하는 순간 native crash가 발생한다

 

 

[출처] https://gall.dcinside.com/mgallery/board/view/?id=game_dev&no=124306

 

최적화 노하우 공유 - 인디 게임 개발 마이너 갤러리

개인적으로 개발관련해서 메모용으로 사용하는 티스토리 블로그에 작성했다가,꽤 많은 분들에게 도움이 될 것 같아 내용만 똑같이 복붙하여 왔습니다.다들 고생하세요.-----게임 출시를 앞두고,

gall.dcinside.com

 

반응형
Posted by blueasa
, |

Unity 2021.3.33f1

NGUI 2023.08.01

----

 

NGUI-UILabel의 Effect에서 Shadow와 Outline을 같이 적용하고 싶어서 찾아보고 올려둠.

UILabel에 세 곳에 소스 추가

public class UILabel : UIWidget
{
    ....
    
    [DoNotObfuscateNGUI] public enum Effect
    {
        None,
        Shadow,
        Outline,
        Outline8,
        ShadowAndOutline,	// Add
    }   
    
    ....
    
    /// <summary>
    /// How many quads there are per printed character.
    /// </summary>

    public int quadsPerCharacter
    {
        get
        {
            if (mEffectStyle == Effect.Shadow) return 2;
            else if (mEffectStyle == Effect.Outline) return 5;
            else if (mEffectStyle == Effect.Outline8) return 9;
            else if (mEffectStyle == Effect.ShadowAndOutline) return 9;	// Add
            return 1;
        }
    }

	....
    
    public void Fill (List<Vector3> verts, List<Vector2> uvs, List<Color> cols, List<Vector3> symbolVerts, List<Vector2> symbolUVs, List<Color> symbolCols)
	{
        ...
        // Apply an effect if one was requested
		if (effectStyle != Effect.None)
		{
			int end = verts.Count;
			var symEnd = (symbolVerts != null) ? symbolVerts.Count : 0;

			pos.x = mEffectDistance.x;
			pos.y = mEffectDistance.y;

			ApplyShadow(verts, uvs, cols, offset, end, pos.x, -pos.y);
			if (symbolVerts != null) ApplyShadow(symbolVerts, symbolUVs, symbolCols, symOffset, symEnd, pos.x, -pos.y);

            #region Add ShadowAndOutline
            if (effectStyle == Effect.ShadowAndOutline)
            {
                pos.y /= 2;
                pos.x = pos.y;

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, -pos.x, pos.y);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, pos.x, pos.y);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, -pos.x, -pos.y);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, -pos.x, 0);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, pos.x, 0);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, 0, pos.y);

                offset = end;
                end = verts.Count;

                ApplyShadow(verts, uvs, cols, offset, end, 0, -pos.y);
            }
			#endregion

            if ((effectStyle == Effect.Outline) || (effectStyle == Effect.Outline8))
			{        
        ...
    }
	
}

 

 

[출처] https://gamedev.stackexchange.com/questions/151329/all-sides-shadow-outline-in-unity-ngui

 

All sides shadow outline in Unity NGUI

How can I make such exactly the same shadow using NGUI?

gamedev.stackexchange.com

 

반응형
Posted by blueasa
, |
// jave.lin 2022.03.17

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

// jave.lin : 输出系统的信息工具类
public class DumpSystemInfoUtil
{
    // jave.lin : 通过反射得方式获取不了
    public static string DumpSystemInfoByReflection()
    {
        var type = typeof(SystemInfo);
        // jave.lin : 下面发现反射不成功
        var fields = type.GetFields(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
        var fieldsToStrList = new List<string>();
        foreach (var field in fields)
        {
            // 过滤 过期得 API
            var obsoAttris = field.GetCustomAttributes(typeof(ObsoleteAttribute), true);
            if (obsoAttris != null && obsoAttris.Length > 0) continue;
            fieldsToStrList.Add(field.Name + ":" + field.GetValue(null).ToString());
        }
        return string.Join("\n", fieldsToStrList.ToArray());
    }

    // jave.lin : 所以只能通过一个个得去输出
    public static string DumpSystemInfoByManualyPrint()
    {
        var list = new List<string>(
            new string[]{
                "SystemInfo:\n",
                "\tbatteryLevel:" + SystemInfo.batteryLevel,
                "\tbatteryStatus:" + SystemInfo.batteryStatus,
                "\toperatingSystem:" + SystemInfo.operatingSystem,
                "\toperatingSystemFamily:" + SystemInfo.operatingSystemFamily,
                "\tprocessorType:" + SystemInfo.processorType,
                "\tprocessorFrequency:" + SystemInfo.processorFrequency,
                "\tprocessorCount:" + SystemInfo.processorCount,
                "\tsystemMemorySize:" + SystemInfo.systemMemorySize,
                "\tdeviceUniqueIdentifier:" + SystemInfo.deviceUniqueIdentifier,
                "\tdeviceName:" + SystemInfo.deviceName,
                "\tdeviceModel:" + SystemInfo.deviceModel,
                "\tsupportsAccelerometer:" + SystemInfo.supportsAccelerometer,
                "\tsupportsGyroscope:" + SystemInfo.supportsGyroscope,
                "\tsupportsLocationService:" + SystemInfo.supportsLocationService,
                "\tsupportsVibration:" + SystemInfo.supportsVibration,
                "\tsupportsAudio:" + SystemInfo.supportsAudio,
                "\tdeviceType:" + SystemInfo.deviceType,
                "\tgraphicsMemorySize:" + SystemInfo.graphicsMemorySize,
                "\tgraphicsDeviceName:" + SystemInfo.graphicsDeviceName,
                "\tgraphicsDeviceVendor:" + SystemInfo.graphicsDeviceVendor,
                "\tgraphicsDeviceID:" + SystemInfo.graphicsDeviceID,
                "\tgraphicsDeviceVendorID:" + SystemInfo.graphicsDeviceVendorID,
                "\tgraphicsDeviceType:" + SystemInfo.graphicsDeviceType,
                "\tgraphicsUVStartsAtTop:" + SystemInfo.graphicsUVStartsAtTop,
                "\tgraphicsDeviceVersion:" + SystemInfo.graphicsDeviceVersion,
                "\tgraphicsShaderLevel:" + SystemInfo.graphicsShaderLevel,
                "\tgraphicsMultiThreaded:" + SystemInfo.graphicsMultiThreaded,
                "\tsupportsShadows:" + SystemInfo.supportsShadows,
                "\tsupportsRawShadowDepthSampling:" + SystemInfo.supportsRawShadowDepthSampling,
                "\tsupportsMotionVectors:" + SystemInfo.supportsMotionVectors,
                "\tsupports3DTextures:" + SystemInfo.supports3DTextures,
                "\tsupports2DArrayTextures:" + SystemInfo.supports2DArrayTextures,
                "\tsupports3DRenderTextures:" + SystemInfo.supports3DRenderTextures,
                "\tsupportsCubemapArrayTextures:" + SystemInfo.supportsCubemapArrayTextures,
                "\tcopyTextureSupport:" + SystemInfo.copyTextureSupport,
                "\tsupportsComputeShaders:" + SystemInfo.supportsComputeShaders,
                "\tsupportsInstancing:" + SystemInfo.supportsInstancing,
                "\tsupportsHardwareQuadTopology:" + SystemInfo.supportsHardwareQuadTopology,
                "\tsupports32bitsIndexBuffer:" + SystemInfo.supports32bitsIndexBuffer,
                "\tsupportsSparseTextures:" + SystemInfo.supportsSparseTextures,
                "\tsupportedRenderTargetCount:" + SystemInfo.supportedRenderTargetCount,
                "\tsupportsMultisampledTextures:" + SystemInfo.supportsMultisampledTextures,
                "\tsupportsMultisampleAutoResolve:" + SystemInfo.supportsMultisampleAutoResolve,
                "\tsupportsTextureWrapMirrorOnce:" + SystemInfo.supportsTextureWrapMirrorOnce,
                "\tusesReversedZBuffer:" + SystemInfo.usesReversedZBuffer,
                "\tnpotSupport:" + SystemInfo.npotSupport,
                "\tmaxTextureSize:" + SystemInfo.maxTextureSize,
                "\tmaxCubemapSize:" + SystemInfo.maxCubemapSize,
                "\tsupportsAsyncCompute:" + SystemInfo.supportsAsyncCompute,
                "\tsupportsAsyncGPUReadback:" + SystemInfo.supportsAsyncGPUReadback,
                "\tsupportsMipStreaming:" + SystemInfo.supportsMipStreaming,
            });
        return string.Join("\n", list.ToArray());
    }
}

// jave.lin : 设备定档级别枚举
public enum eDeviceLevel
{
    Unknow = -1,
    VeryLow = 0,
    Low,
    Middle,
    High,
}

// jave.lin : 画质级别
public enum eQualityLevel
{
    Low = 1,
    Middle = 2,
    High = 3,
    Ultra = 4,
}

// jave.lin : shader lod
public enum eShaderLOD
{
    //High = 800,
    //Middle = 400,
    //Low = 200,
    //VeryLow = 100,
    //UnLimit = -1,
    // jave.lin : 太低的值对 built-in shader 的影响太大
    High = 800,
    Middle = 600,
    Low = 400,
    VeryLow = 200,
    UnLimit = -1,
}

// jave.lin : 游戏的质量设置类
public class GameQualitySettings
{
    private const string QS_POWER_SAVE_MODE_KEY = "graphics_setting.power_save_mode";
    private const string QS_QUALITY_LEVEL_KEY = "graphics_setting.quality_level";

    // 当 品质有调整事出发的事件函数
    public static Action<eQualityLevel> onLevelChanged;

    // 源来的 AA 和 阴影设置
    private static int srcAntiAliasing;
    private static ShadowQuality srcShadows;

    // 当前 品质等级
    private static eQualityLevel curLevel;

    // 获取 设备定档的质量级别
    public static eQualityLevel DeviceAdapterLevel
    {
        get; private set;
    }

    // 获取 或 设置 公开给外部的画质设置的属性
    public static eQualityLevel GraphicsLevel
    {
        get { return curLevel; }
        set
        {
            if (curLevel != value)
            {
                curLevel = value;
                _SetCurLevel(value);
                PlayerPrefs.SetInt(QS_QUALITY_LEVEL_KEY, (int)value);
                if (null != onLevelChanged)
                {
                    onLevelChanged.Invoke(value);
                }
            }
        }
    }

    // 获取 或 设置 省电模式, true: 30FPS, false: 60FPS
    public static bool PowerSaveMode
    {
        get
        {
            return Application.targetFrameRate < 40;
        }

        set
        {
            var src_v = PlayerPrefs.GetInt(QS_POWER_SAVE_MODE_KEY, -1);
            var tar_v = value ? 1 : 0;
            if (src_v != tar_v)
            {
                PlayerPrefs.SetInt(QS_POWER_SAVE_MODE_KEY, tar_v);
            }
            Application.targetFrameRate = value ? 30 : 60;
        }
    }

    // 静态构造函数
    static GameQualitySettings()
    {
        // 备份 原始 AA 和 阴影
        srcAntiAliasing = QualitySettings.antiAliasing;
        srcShadows = QualitySettings.shadows;

        // 初始化 品质 和 省电模式
        _SetDefaultPowerSaveMode();
        _SetDefaultLevel();
    }

    // 设置默认的品质等级
    private static void _SetDefaultLevel()
    {
        // 先 分析 并 设置 设备默认品质等级
        DeviceAdapterLevel = _AnalysicDeviceLevel();

        var src_v = PlayerPrefs.GetInt(QS_QUALITY_LEVEL_KEY, -1);
        // 如果品质等级没有设置过
        if (src_v == -1)
        {
            // 那么使用 设备默认品质
            PlayerPrefs.SetInt(QS_QUALITY_LEVEL_KEY, (int)DeviceAdapterLevel);
            curLevel = GraphicsLevel;
        }
        // 如果品质等级有设置过
        else
        {
            curLevel = (eQualityLevel)src_v;
        }
    }

    // 设置默认的省电模式
    private static void _SetDefaultPowerSaveMode()
    {
        var src_v = PlayerPrefs.GetInt(QS_POWER_SAVE_MODE_KEY, 0);
        if (src_v == 0)
        {
            PowerSaveMode = true;
            PlayerPrefs.SetInt(QS_POWER_SAVE_MODE_KEY, 1);
        }
        else
        {
            PowerSaveMode = src_v == 1;
        }
    }

    // 分析设备所属默认的品质等级
    private static eQualityLevel _AnalysicDeviceLevel()
    {
        if (SystemInfo.processorFrequency >= 2500 &&
            SystemInfo.processorCount >= 8 &&
            SystemInfo.systemMemorySize >= (6 * 1024) &&
            SystemInfo.graphicsMemorySize >= (2 * 1024) &&
            SystemInfo.graphicsShaderLevel >= 30 &&
            SystemInfo.graphicsMultiThreaded &&
            SystemInfo.supportsShadows &&
            SystemInfo.supportsInstancing &&
            SystemInfo.supports32bitsIndexBuffer
            )
        {
            return eQualityLevel.Ultra;
        }
        else if (SystemInfo.processorFrequency >= 2000 &&
            SystemInfo.processorCount >= 4 &&
            SystemInfo.systemMemorySize >= (4 * 1024) &&
            SystemInfo.graphicsMemorySize >= (1 * 1024) &&
            SystemInfo.graphicsShaderLevel >= 20
            )
        {
            return eQualityLevel.High;
        }
        else if (SystemInfo.processorFrequency >= 1500 &&
            SystemInfo.processorCount >= 2 &&
            SystemInfo.systemMemorySize >= (2 * 1024) &&
            SystemInfo.graphicsMemorySize >= (512) &&
            SystemInfo.graphicsShaderLevel >= 10
            )
        {
            return eQualityLevel.Middle;
        }
        else
        {
            return eQualityLevel.Low;
        }
    }

    // 设置 当前品质等级
    private static void _SetCurLevel(eQualityLevel level)
    {
        _SetAntiAliasing(level);
        _SetResolution(level);
        _SetTexMipmapOffset(level);
        _SetShadow(level);
        _SetLODBias(level);
        _SetGraphicsTier(level);
        _SetShaderLOD(level);
        _SetGlobalShaderKW(level);
    }

    // 设置 AA
    private static void _SetAntiAliasing(eQualityLevel level)
    {
        if (level >= eQualityLevel.High)
        {
            QualitySettings.antiAliasing = srcAntiAliasing;
        }
        else
        {
            QualitySettings.antiAliasing = 0;
        }
    }

    // 设置分辨率
    private static void _SetResolution(eQualityLevel level)
    {
        // jave.lin : BRP(Built-In Rendering Pipeline) 中
        // 需要对应的 Camera 开启 AllowDynamicResolution 后才能生效
        switch (level)
        {
            case eQualityLevel.Low:
                QualitySettings.resolutionScalingFixedDPIFactor = 0.75f;
                break;
            case eQualityLevel.Middle:
                QualitySettings.resolutionScalingFixedDPIFactor = 0.85f;
                break;
            case eQualityLevel.High:
                QualitySettings.resolutionScalingFixedDPIFactor = 0.85f;
                break;
            case eQualityLevel.Ultra:
                QualitySettings.resolutionScalingFixedDPIFactor = 1.00f;
                break;
        }
    }

    // 设置 Tex 纹理 mipmap offset
    private static void _SetTexMipmapOffset(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
                QualitySettings.masterTextureLimit = DeviceAdapterLevel < eQualityLevel.High ? 3 : 2;
                break;
            case eQualityLevel.Middle:
                QualitySettings.masterTextureLimit = DeviceAdapterLevel < eQualityLevel.High ? 2 : 1;
                break;
            case eQualityLevel.High:
                QualitySettings.masterTextureLimit = 0;
                break;
            case eQualityLevel.Ultra:
                QualitySettings.masterTextureLimit = 0;
                break;
        }
    }

    // 设置阴影
    private static void _SetShadow(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
            case eQualityLevel.Middle:
                //QualitySettings.shadows = ShadowQuality.Disable; // jave.lin : 有 BUG,会导致,Animator 组件中的 culling mode 不是 always animated 的对象超出屏幕的画,会被自动停止掉
                // 所以下面使用 shadowDistance 来替代关闭
                QualitySettings.shadowDistance = 0;
                break;
            case eQualityLevel.High:
                QualitySettings.shadows = srcShadows;
                QualitySettings.shadowResolution = ShadowResolution.Low;
                QualitySettings.shadowDistance = 70;
                break;
            case eQualityLevel.Ultra:
                QualitySettings.shadows = srcShadows;
                QualitySettings.shadowResolution = ShadowResolution.High;
                QualitySettings.shadowDistance = 100;
                break;
        }
    }

    // 设置 LOD 偏移
    private static void _SetLODBias(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
                QualitySettings.lodBias = 0.5f;
                break;
            case eQualityLevel.Middle:
                QualitySettings.lodBias = 0.75f;
                break;
            case eQualityLevel.High:
            case eQualityLevel.Ultra:
                QualitySettings.lodBias = 1.0f;
                break;
        }
    }

    // 设置 GraphicsTier 的层级
    private static void _SetGraphicsTier(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
            case eQualityLevel.Middle:
                Graphics.activeTier = GraphicsTier.Tier1;
                break;
            case eQualityLevel.High:
                Graphics.activeTier = GraphicsTier.Tier2;
                break;
            case eQualityLevel.Ultra:
                Graphics.activeTier = GraphicsTier.Tier3;
                break;
        }
    }

    // 设置 Shader LOD
    private static void _SetShaderLOD(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
                Shader.globalMaximumLOD = (int)eShaderLOD.VeryLow;
                break;
            case eQualityLevel.Middle:
                Shader.globalMaximumLOD = (int)eShaderLOD.Low;
                break;
            case eQualityLevel.High:
                Shader.globalMaximumLOD = (int)eShaderLOD.Middle;
                break;
            case eQualityLevel.Ultra:
                Shader.globalMaximumLOD = (int)eShaderLOD.High;
                break;
            default:
                Shader.globalMaximumLOD = (int)eShaderLOD.UnLimit;
                break;
        }
    }

    // 设置全局Shader Keyword
    private static void _SetGlobalShaderKW(eQualityLevel level)
    {
        switch (level)
        {
            case eQualityLevel.Low:
            case eQualityLevel.Middle:
                Shader.DisableKeyword("_SOFT_PARTICLE_ON");
                break;
            case eQualityLevel.High:
            case eQualityLevel.Ultra:
                Shader.EnableKeyword("_SOFT_PARTICLE_ON");
                break;
        }
    }
}

// jave.lin : 后效基类
public class PPBasic : MonoBehaviour { }
// jave.lin : Bloom 后效
public class BloomPP : PPBasic
{
    // jave.lin : start 时 先处理,处理当前品质
    // 然后监听 品质变化的时间
    private void Start()
    {
        OnQualityLevelChanged(GameQualitySettings.GraphicsLevel);
        GameQualitySettings.onLevelChanged -= OnQualityLevelChanged;
        GameQualitySettings.onLevelChanged += OnQualityLevelChanged;
    }
    // jave.lin : 销毁时记得删除回调
    private void OnDestroy()
    {
        GameQualitySettings.onLevelChanged -= OnQualityLevelChanged;
    }

    private void OnQualityLevelChanged(eQualityLevel ql)
    {
        // jave.lin : 当 品质等级大于或等于高时,才开启 Bloom 后效
        enabled = ql >= eQualityLevel.High;
    }
}

 

 

[출처] https://blog.csdn.net/linjf520/article/details/123546253

 

Unity - 画质设置_unity systeminfo.processorfrequency 值大于5000mhz-CSDN博客

Show me Your Code, Talk Is Cheap. 以前自己写的类,现在重新写一份 代码 便于日后直接搬运使用,代码都是相当简单,都是直接调用 unity 的 API 设置即可,可以理解为就是搬运而已 环境 Unity : 2018.2.11f1

blog.csdn.net

 

반응형
Posted by blueasa
, |

Unity 2021.3.33f1

----

 

앱 최초 실행 시, 최적화를 위해 그래픽 품질 관련 자동 설정하는 기능을 찾아보다 적용하고 올려 둠.

 

참조한 글을 보니 기본적으로 유니티 SystemInfo에서 지원하는 하드웨어 스펙을 보고 판단해서 Quality Level을 메기고 있다.

해당 레벨은 Unity의 UnityEngine.QualityLevel을 참조하고 있어서 대응 하는 enum을 추가(eQualityLevel)하고,

60프레임을 사용하기 위해 해당 부분 값만 수정했다.

 

필요할 때 AutoChooseQualityLevel()을 호출하고, 반환 받은 eQualityLevel 값에 따라 원하는 설정을 적용해주면 된다.

/// <summary>
/// UnityEngine.QualityLevel에 대응하는 enum
/// </summary>
public enum eQualityLevel : int 
{
    Fastest,
    Fast,
    Simple,
    Good,       // [참고] PC(i7-10700/64GB/RTX3700)에서 Good 나옴
    Beautiful,
    Fantastic
}

private eQualityLevel AutoChooseQualityLevel()
{
    Debug.Assert(Enum.GetNames(typeof(QualityLevel)).Length == Enum.GetNames(typeof(eQualityLevel)).Length, "Please update eQualityLevel to the new quality levels.");

	var shaderLevel = SystemInfo.graphicsShaderLevel;
    var cpus = SystemInfo.processorCount;
    var vram = SystemInfo.graphicsMemorySize;
    var fillrate = 0;

    // ShaderLevel
    if (shaderLevel < 10)
    	fillrate = 1000;
    else if (shaderLevel < 20)
    	fillrate = 1300;
    else if (shaderLevel < 30)
    	fillrate = 2000;
    else
    	fillrate = 3000;
    // CPU Count
    if (6 <= cpus)
    	fillrate *= 3;
    else if (3 <= cpus)
    	fillrate *= 2;
    // VRam
    if (512 <= vram)
    	fillrate *= 2;
    else if (vram <= 128)
    	fillrate /= 2;
        
    var resx = Screen.width;
    var resy = Screen.height;
    var target_fps = 60.0f;		// 현재 게임의 타겟 프레임에 맞게 설정
    var fillneed = (resx * resy + 400f * 300f) * (target_fps / 1000000.0f);
    // Change the values in levelmult to match the relative fill rate
    // requirements for each quality level.
    var levelmult = new float[] { 5.0f, 30.0f, 80.0f, 130.0f, 200.0f, 320.0f };

    const int max_quality = (int)eQualityLevel.Fantastic;
    var level = 0;
    while (level < max_quality && fillneed * levelmult[level + 1] < fillrate)
        ++level;

    var quality = (eQualityLevel)level;
    Debug.Log(string.Format("{0}x{1} need {2} has {3} = {4} level", resx, resy, fillneed, fillrate, quality.ToString()));

    return quality;
}

 

 

P.s. 참조 글 중에 에셋도 적혀있긴한데 사서 써보진 않음.

 

 

[참조] https://stackoverflow.com/questions/20978106/automatic-quality-settings/20978462#20978462

 

Automatic quality settings

a lot of apps that I see nowadays(Android) instead of having an options screen to select the graphical level, it just automatically does it, I know this is kind of a vast question, but how can I do...

stackoverflow.com

 

[참조] https://discussions.unity.com/t/auto-detect-quality-settings/24351

 

auto detect quality settings

is there a way that unity can auto detect client GPU/CPU capability and performance to set best quality settings?

discussions.unity.com

 

[에셋] https://assetstore.unity.com/packages/tools/integration/auto-quality-hardware-assessment-tool-aqhat-74134

 

Auto Quality Hardware Assessment Tool - AQHAT | 기능 통합 | Unity Asset Store

Use the Auto Quality Hardware Assessment Tool - AQHAT from Eager Amoeba® on your next project. Find this integration tool & more on the Unity Asset Store.

assetstore.unity.com

 

반응형
Posted by blueasa
, |

[링크]

유니티 오브젝트의 null 비교 시 유의사항 1

 

유니티 오브젝트의 null 비교 시 유의사항

다음글: 유니티 오브젝트의 null 비교 시 유의사항 2

overworks.github.io

 

유니티 오브젝트의 null 비교 시 유의사항 2

 

유니티 오브젝트의 null 비교 시 유의사항 2

이전글: 유니티 오브젝트의 null 비교 시 유의사항

overworks.github.io

 

반응형
Posted by blueasa
, |

Untiy 2021.3.42f1

GoogleMobileAds 9.1.0

----

 

[추가] 2024-08-22

GoogleMobileAds의 UMP로 GDPR과 IDFA(ATT)를 유기적으로 처리는 가능한데,

GoogleMobileAds의 IDFA(ATT)를 사용하면 안이쁜 '무료 이용'어쩌고 팝업이 한 번 뜬다.

이 부분을 없애기 위해서 ATT를 GoogleMobileAds의 IDFA를 Off 하고, Unity Package인 'iOS 14 Advertising Support'를 사용하도록 변경했다.

[참고] https://docs.unity3d.com/Packages/com.unity.ads.ios-support@1.0/manual/index.html

 

변경한 소스는 아래 소스에 갱신하고, 관련해서 CurrentGDPR.cs도 추가했다.

로직 순서는 대충 아래와 같다.

 

[순서] 애드몹(GoogleMobileAds) 초기화 -> CurrentGDPR 초기화 ->  ATT(IDFA) 초기화

 

// GoogleMobileAds
yield return new WaitUntil(() => true == blueasa.GoogleMobileAdsManagerSGT.Exists);
yield return null;
SGT.Ads.Initialize();
yield return new WaitUntil(() => true == SGT.Ads.Initialized);
Debug.LogWarning("[GoogleMobileAds] 초기화 완료");
yield return null;
// GDPR 관련 동의 상태 Load[blueasa / 2024-03-21]
CurrentGDPR.InitData();
Debug.LogWarning("[CurrentGDPR] 초기화 완료");
yield return null;
// ATT(IDFA) 동의 체크
SGT.Ads.RequestAuthorizationTracking();
Debug.LogWarning("[ATT] 동의 체크 완료");
yield return null;

 

순서만 참조하면 될 듯 한데, 간단히 설명하자면 아래와 같다.

 

1) GoogleMobileAds를 초기화하면서 GDPR 동의 내부에서 진행.

2) CurrentGDPR 초기화해서 GDPR 관련 데이터 Load

3) Load 한 CurrentGDPR 데이터 참조해서 ATT 요청할지 판단 및 실행(RequestAuthorizationTracking)

 

추가적으로, GDPR 동의했어도, ATT 거부를 했다면,

설정창 등에 보여주던 GDPR 동의상태 변경을 위한 버튼을 띄우면 안된다고 한다.

(Apple 검수 넣었다가 해당 이슈로 리젝 당함)

 

그래서 IsAuthorizationTracking() 함수를 이용해서 ATT 거부 상태면, GDPR 설정 버튼이 안뜨도록 해뒀다.

참고하자.

 

----------------------------------------------------------------

2024년 1월 16일까지(참조:https://support.google.com/admob/answer/14189727?hl=ko) Admob을 사용하려면 유저에게 GDPR 동의를 받으라고 하는 것 같다.

그래서 Google에서 관련 SDK를 내놓은게 UMP(User Messaging Platform) SDK다.

----

 

Unity GoogleMobileAds에 UMP 적용하기 위해 찾아보니 누군가가 github에 정리해 놨길래 가져와서 적당히(?) 개조해서 내 GoogleMobileAdsManager에 추가 했다.

 

[Unity-Google-UMP-Check] https://github.com/TheBossaaa/Unity-Google-UMP-Check

 

GitHub - TheBossaaa/Unity-Google-UMP-Check: Google Admob & UMP Working Script Example

Google Admob & UMP Working Script Example. Contribute to TheBossaaa/Unity-Google-UMP-Check development by creating an account on GitHub.

github.com

 

기본적으로 위 소스만 적용하고 폰에서 실행해보면 아래와 같은 에러가 나온다.

 

[Error Code] GoogleMobileAds.Ump.Api.FormError

 

해당 에러는 GoogleMobileAds Dashboard에서 GDPR 관련 설정이 안돼 있어서 나오는 에러이다.

아래 Admob UMP Docs에서는 딱히 GoogleMobileAds Dashboard 관련 설정을 설명 안해놔서 모르고 넘어갈 수 있다.

Dashboard에 설정을 하자.

애드몹 대쉬보드 - 개인 정보 보호 및 메시지 - GDPR

 

[FormError 관련 참조] https://github.com/googleads/googleads-mobile-unity/issues/2780

 

GoogleMobileAds.Ump.Api.FormError · Issue #2780 · googleads/googleads-mobile-unity

[REQUIRED] Step 1: Describe your environment Unity version: 2021.3.27f1 Google Mobile Ads Unity plugin version: 8.0.0 Platform: Android Platform OS version: Android 10 Any specific devices issue oc...

github.com

 

※ 추가적으로 Admob UMP Docs에 보면 Runtime에 GDPR 변경을 위한 버튼을 구현하는 샘플이 보인다.

    법적으로 Runtime에 GDPR 변경을 해줘야 되는거면 버튼도 별도로 추가해줘야 될 것 같다.

 

[참조] https://github.com/googleads/googleads-mobile-unity/issues/2780

 

GoogleMobileAds.Ump.Api.FormError · Issue #2780 · googleads/googleads-mobile-unity

[REQUIRED] Step 1: Describe your environment Unity version: 2021.3.27f1 Google Mobile Ads Unity plugin version: 8.0.0 Platform: Android Platform OS version: Android 10 Any specific devices issue oc...

github.com

 

[GDPR 테스트]

GDPR 테스트 관련 소스가 들어있는데, Test Device Hashed Id를 넣으라고 돼있고, 실행해보면 Logcat에 뜬다고 돼 있는데 안떠서 찾아보니 아래와 같은 내용이 있다.

(난 현재 테스트 폰이 API 30이 안돼서 Test Device Hashed Id는 안넣어도 되는 것 같다.)

 

I also struggled with the same problem. The reason NOT_REQUIRED is always returned is that the test device ID has not been registered. DebugGeography does not apply to non-test devices. In my case, even if it is a basic Android emulator, I had to register the test device ID to resolve this issue. Your emulator's device ID is displayed in logcat as follows when ConsentInformation.requestConsentInfoUpdate is called.

Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("xxxxxxxxx") to set this as a debug device.

EDIT: I did more tests. It requires a test device ID since Android API 30. Until API 29, it is not necessary to specify the test device ID. I'm not sure if this is only for my development environment.

[참조] https://stackoverflow.com/questions/68529268/ump-consentstatus-always-notrequired-when-testing

 

UMP ConsentStatus always "NotRequired" when testing

I have incorporated Google's UserMessagingPlatform into my Android project for user consent to be compliant with GDPR for AdMob. The problem is that the ConsentStatus always returns "NotRequired&

stackoverflow.com

 

[GoogleMobileAdsManager에 추가된 GDPR/IDFA(ATT) 관련 UMP 소스]

        using GoogleMobileAds.Ump.Api;


        #region UMP(User Messaging Platform)
        /// <summary>
        /// Privacy(GDPR) 요청
        /// Ensures that privacy and consent information is up to date.
        /// </summary>
        public void InitializeGoogleMobileAdsConsent()
        {
            // Test GDPR
            //GDPRDebugger();

            RequestConsentInfo();
        }

        ///Summary
        ///Request Consent Information
        ///Warning: Ads can be preloaded by the Google Mobile Ads SDK or mediation partner SDKs
        ///upon calling MobileAds.Initialize(). If you need to obtain consent from users in the European Economic Area (EEA), set any request-specific flags, such as tagForChildDirectedTreatment or tag_for_under_age_of_consent, or otherwise take action before loading ads.
        ///Ensure you do this before initializing the Google Mobile Ads SDK.
        ///Summary

        void RequestConsentInfo()
        {
            Debug.Log("[UMP] RequestConsentInfo()");

            // Set tag for under age of consent.
            // Here false means users are not under age of consent.
            ConsentRequestParameters request = new ConsentRequestParameters
            {
                TagForUnderAgeOfConsent = false,
                //ConsentDebugSettings = new ConsentDebugSettings
                //{
                //    // For debugging consent settings by geography.
                //    DebugGeography = DebugGeography.Disabled,
                //    // https://developers.google.com/admob/unity/test-ads
                //    TestDeviceHashedIds = TestDeviceIds,
                //}
            };

            // Check the current consent information status.
            ConsentInformation.Update(request, OnConsentInfoUpdated);
        }

        void OnConsentInfoUpdated(FormError consentError)
        {
            if (consentError != null)
            {
                // Handle the error.
                UnityEngine.Debug.LogErrorFormat("[UMP][OnConsentInfoUpdated] {0}", consentError);
                // UMP 동의 Error 시에도 초기화 진행[blueasa / 2024-03-15]
                InitializeGoogleMobileAds();
                return;
            }
            else
            {
                UnityEngine.Debug.LogFormat("Google Mobile Ads consent updated: {0}", ConsentInformation.ConsentStatus);
            }

            // If the error is null, the consent information state was updated.
            // You are now ready to check if a form is available.
            ConsentForm.LoadAndShowConsentFormIfRequired((FormError formError) =>
            {
                if (formError != null)
                {
                    // Consent gathering failed.
                    UnityEngine.Debug.LogErrorFormat("[UMP][LoadAndShowConsentFormIfRequired] {0}", consentError);
                    // UMP 동의 Error 시에도 초기화 진행[blueasa / 2024-03-15]
                    InitializeGoogleMobileAds();
                    return;
                }

                // Consent has been gathered.
                if (ConsentInformation.CanRequestAds())
                {
                    // UMP 동의 이후 GoogleMobileAds 초기화[blueasa / 2023-12-14]
                    InitializeGoogleMobileAds();
                }
            });
        }

        /// <summary>
        /// Privacy(GDPR) Button 활성화 여부 체크
        /// (설정 창에 버튼 추가함)
        /// </summary>
        public bool CheckActivePrivacyButton()
        {
            bool bActive = false;

            // EU인지 여부에 따라 버튼 활성화 체크 필요

            #region ConsentInformation.PrivacyOptionsRequirementStatus && AuthorizationTrackingStatus
            bActive = IsPrivacyOptionsRequirement() && IsAuthorizationTracking(); // Privacy && ATT 동의 체크
            //bActive = IsPrivacyOptionsRequirement();    // Privacy만 체크
            #endregion

            #region ConsentInformation.ConsentStatus
            //bActive = IsConsentForGDPR();
            #endregion

            return bActive;
        }

        /// <summary>
        /// 개인정보 보호 옵션을 표시해야 하는지 여부
        /// </summary>
        /// <returns></returns>
        public bool IsPrivacyOptionsRequirement()
        {
#if UNITY_EDITOR
            // [에디터] 무조건 비활성화 처리[blueasa / 2024-02-27]
            return false;
#endif
            // [PrivacyOptionsRequirementStatus enum 참조]
            // https://developers.google.com/admob/unity/reference/namespace/google-mobile-ads/ump/api#namespace_google_mobile_ads_1_1_ump_1_1_api_1a60f41ef4f7e14d5ae1fb5f23b7e0244b
            //public enum PrivacyOptionsRequirementStatus
            //{
            //    Unknown,
            //    NotRequired,
            //    Required
            //}

            bool bIsPrivacyOptionsRequirement = false;

            // 개인정보 보호 옵션을 표시해야 하는지 여부
            switch (ConsentInformation.PrivacyOptionsRequirementStatus)
            {
                // 개인정보 보호 옵션이 표시되어야 함.
                case PrivacyOptionsRequirementStatus.Required:
                    bIsPrivacyOptionsRequirement = true;
                    break;

                // 개인정보 보호 옵션은 표시할 필요가 없음.
                case PrivacyOptionsRequirementStatus.NotRequired:
                    bIsPrivacyOptionsRequirement = false;
                    break;

                // 개인 정보 보호 옵션 요구 사항 상태를 알 수 없음.
                case PrivacyOptionsRequirementStatus.Unknown:
                    bIsPrivacyOptionsRequirement = false;
                    break;

                default:
                    bIsPrivacyOptionsRequirement = false;
                    break;
            }
            Debug.LogWarningFormat("[PrivacyOptionsRequirementStatus] {0}", ConsentInformation.PrivacyOptionsRequirementStatus);

            return bIsPrivacyOptionsRequirement;
        }

        public bool IsConsentForGDPR()
        {
            // [ConsentStatus enum 참조]
            // https://developers.google.com/admob/unity/reference/namespace/google-mobile-ads/ump/api#namespace_google_mobile_ads_1_1_ump_1_1_api_1aa83ad2ecf6f2a08c584b60cef06f5133
            //ConsentStatus
            //{
            //    Unknown = 0,      // Unknown consent status.
            //    NotRequired = 1,  // Consent not required.
            //    Required = 2,     // User consent required but not yet obtained.
            //    Obtained = 3      // User consent obtained, personalized vs non-personalized undefined.
            //}

            bool bIsConsentForGDPR = false;

            switch (ConsentInformation.ConsentStatus)
            {
                case ConsentStatus.Unknown:     // 동의 상태를 알 수 없습니다.
                case ConsentStatus.Required:    // 사용자 동의가 필요하지만 아직 획득되지 않았습니다.
                case ConsentStatus.NotRequired: // 동의가 필요하지 않습니다.
                    bIsConsentForGDPR = false;
                    break;

                case ConsentStatus.Obtained:    // 사용자 동의 획득, 개인화 및 비개인화 정의되지 않음.
                    bIsConsentForGDPR = true;
                    break;
            }
            Debug.LogWarningFormat("[ConsentStatus] {0}", ConsentInformation.ConsentStatus);

            return bIsConsentForGDPR;
        }

        bool HasConsentFor(string strKey)
        {
            // Example value: "1111111111"
            string strPurpose = ApplicationPreferences.GetString(strKey);
            // Purposes are zero-indexed. Index 0 contains information about Purpose 1.
            if (!string.IsNullOrEmpty(strPurpose))
            {
                char cOneString = strPurpose[0];
                bool bHasConsentForOne = (cOneString == '1');

                return bHasConsentForOne;
            }

            return false;
        }

        /// <summary>
        /// Consent For GDPR
        /// </summary>
        /// <returns></returns>
        public bool HasConsentForPurpose()
        {
            return HasConsentFor("IABTCF_PurposeConsents");

            //// Example value: "1111111111"
            //string purposeConsents = ApplicationPreferences.GetString("IABTCF_PurposeConsents");
            //// Purposes are zero-indexed. Index 0 contains information about Purpose 1.
            //if (!string.IsNullOrEmpty(purposeConsents))
            //{
            //    char purposeOneString = purposeConsents[0];
            //    bool hasConsentForPurposeOne = (purposeOneString == '1');

            //    return hasConsentForPurposeOne;
            //}

            //return false;
        }

        /// <summary>
        /// Consent For Ads Personalization
        /// https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform
        /// </summary>
        /// <returns></returns>
        public bool HasConsentForVendor()
        {
            return HasConsentFor("IABTCF_VendorConsents");

            //// Example value: "1111111111"
            //string vendorConsents = ApplicationPreferences.GetString("IABTCF_VendorConsents");
            //// Purposes are zero-indexed. Index 0 contains information about Purpose 1.
            //if (!string.IsNullOrEmpty(vendorConsents))
            //{
            //    char purposeOneString = vendorConsents[0];
            //    bool hasConsentForVendorOne = (purposeOneString == '1');

            //    return hasConsentForVendorOne;
            //}

            //return false;
        }

        /// <summary>
        /// Privacy(GDPR) Button 클릭해서 GDPR 동의 창 띄우기
        /// </summary>
        public void OnClickPrivacyButton()
        {
            UpdatePrivacyButton();
        }

        void PrivacyButton()
        {
            //// Enable the privacy settings button.
            //if (_privacyButton != null)
            //{
            //    _privacyButton.onClick.AddListener(UpdatePrivacyButton);
            //    // Disable the privacy settings button by default.
            //    _privacyButton.interactable = false;
            //}
        }

        private void UpdatePrivacyButton()
        {
            // Logic for updating privacy options
            ShowPrivacyOptionsForm(); // You might want to call your method to show the privacy options form here
            Debug.LogFormat("[UMP][UpdatePrivacyButton] Privacy button clicked!");
        }

        /// <summary>
        /// Shows the privacy options form to the user.
        /// </summary>
        public void ShowPrivacyOptionsForm()
        {
            Debug.Log("[UMP] Showing privacy options form.");

            // PrivacyOptionsForm 무조건 팝업으로 변경[blueasa / 2023-12-14]
            ConsentForm.ShowPrivacyOptionsForm((FormError showError) =>
            //ConsentForm.LoadAndShowConsentFormIfRequired((FormError showError) =>
            {
                if (showError != null)
                {
                    Debug.LogErrorFormat("[UMP][LoadAndShowConsentFormIfRequired] Error showing privacy options form with error: {0}", showError.Message);
                }
                else
                {
                    // 버튼 상태 갱신(필요하면)

                    // Enable the privacy settings button.
                    //if (_privacyButton != null)
                    //{
                    //    _privacyButton.interactable =
                    //        ConsentInformation.PrivacyOptionsRequirementStatus ==
                    //        PrivacyOptionsRequirementStatus.Required;
                    //}
                }

            });
        }

        void GDPRDebugger()
        {
            Debug.Log("[UMP] GDPRDebugger()");

            ///Summary
            ///Use this for debugging
            ///

            // Define the test device ID for debugging
            string testDeviceHashedId = "0B030C0B27FA3A0A7FCF5766D3BBBA1A"; // Replace with your actual test device ID

            // Create debug settings for consent testing
            var debugSettings = new ConsentDebugSettings
            {
                TestDeviceHashedIds = new List<string>
                {
                    testDeviceHashedId
                }
            };

            // Set the debug geography for testing in the EEA
            debugSettings.DebugGeography = DebugGeography.EEA;
            Debug.Log("[UMP] GDPRDebugger Set : DebugGeography.EEA");

            // Set tag for under the age of consent.
            // Here false means users are not under the age of consent.
            ConsentRequestParameters request = new ConsentRequestParameters
            {
                TagForUnderAgeOfConsent = false,
                ConsentDebugSettings = debugSettings,
            };

            // Check the current consent information status.
            ConsentInformation.Update(request, OnConsentInfoUpdated);
        }

        /// <summary>
        /// Request ATT(IDFA)
        /// [Package] iOS 14 Advertising Support v1.0.0
        /// </summary>
        public void RequestAuthorizationTracking()
        {
#if UNITY_IOS
            Debug.Log("Unity iOS Support: Requesting iOS App Tracking Transparency native dialog.");
            
            var status = Unity.Advertisement.IosSupport.ATTrackingStatusBinding.GetAuthorizationTrackingStatus();
            Debug.LogWarningFormat("[RequestAuthorizationTracking-AuthorizationTrackingStatus] {0}", status);

            // 결정되지 않은 상태일 때만 체크 및 요청
            if (status == Unity.Advertisement.IosSupport.ATTrackingStatusBinding.AuthorizationTrackingStatus.NOT_DETERMINED)
            {
                bool bNeedRequest = false;
                if (false == CurrentGDPR.IsGDPR())
                {
                    // [Non EEA] ATT 동의 요청
                    bNeedRequest = true;
                }
                else
                {
                    // [EEA]
                    if (true == CurrentGDPR.IsPurposeConsents())
                    {
                        // [GDPR 동의] ATT 동의 요청
                        bNeedRequest = true;
                    }
                    else
                    {
                        // [GDPR 비동의] ATT 동의 체크가 필요없음
                        bNeedRequest = false;
                    }
                }

                // ATT 동의 요청 필요
                if (true == bNeedRequest)
                {
                    Unity.Advertisement.IosSupport.ATTrackingStatusBinding.RequestAuthorizationTracking();
                }
            }
#else
            Debug.LogWarning("Unity iOS Support: Tried to request iOS App Tracking Transparency native dialog, " +
                             "but the current platform is not iOS.");
#endif
        }

        /// <summary>
        /// ATT 동의 여부
        /// </summary>
        /// <returns>동의 여부</returns>
        public bool IsAuthorizationTracking()
        {
#if UNITY_IOS
            if (Application.platform == RuntimePlatform.IPhonePlayer)
            {
                var status = Unity.Advertisement.IosSupport.ATTrackingStatusBinding.GetAuthorizationTrackingStatus();
                Debug.LogWarningFormat("[AuthorizationTrackingStatus] {0}", status);

                switch(status)
                {
                    case Unity.Advertisement.IosSupport.ATTrackingStatusBinding.AuthorizationTrackingStatus.NOT_DETERMINED: // 결정되지 않음
                    case Unity.Advertisement.IosSupport.ATTrackingStatusBinding.AuthorizationTrackingStatus.RESTRICTED:     // 액세스 권한이 제한된 상태
                    case Unity.Advertisement.IosSupport.ATTrackingStatusBinding.AuthorizationTrackingStatus.DENIED:         // 거부됨
                        return false;

                    case Unity.Advertisement.IosSupport.ATTrackingStatusBinding.AuthorizationTrackingStatus.AUTHORIZED:     // 동의함
                        return true;
                }
            }
#endif
            return false;
        }
        #endregion

 

 

[CurrentGDPR.cs]

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()
    {
        // 원하는 시점에 Init하도록 변경[blueasa / 2024-03-21]
        //InitData();
    }

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

        // IABTCF_* 관련 항목 설명
        // [참조] https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details

        // 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);
    }

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

    /// <summary>
    /// 광고에 필요한 권한 동의 여부
    /// </summary>
    /// <returns>동의 여부</returns>
    public static bool IsPurposeConsents()
    {
        // Example value:
        // 동의   : "1111111111"
        // 비동의 : "0"

        if (true == string.IsNullOrEmpty(_purposeConsent))
        {
            return false;
        }

        Debug.LogFormat("[IsPurposeConsents] {0}", HasAttribute(_purposeConsent, 1));

        // Purposes are zero-indexed. Index 0 contains information about Purpose 1.
        // _purposeConsent[0]에 1(동의)이 있는지 체크
        return HasAttribute(_purposeConsent, 1);
    }

    /// <summary>
    /// 광고에 필요한 적법관심(광고 개인화?) 동의 여부
    /// </summary>
    /// <returns>동의 여부</returns>
    public static bool IsVendorConsents()
    {
        if (true == string.IsNullOrEmpty(_vendorConsent))
        {
            return false;
        }

        // _vendorConsent[0]에 1(동의)이 있는지 체크
        return HasAttribute(_vendorConsent, 1);
    }

    // 광고가 보여지는지 여부 리턴
    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)
    {
        // null 예외처리[blueasa / 2024-03-21]
        if(true == string.IsNullOrEmpty(input) || (input.Length <= index))
            return false;

        return (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));
    }
}

 

 

[참조] https://docs.adxcorp.kr/appendix/ump-user-messaging-platform

 

UMP (User Messaging Platform) - ADX Library

IDFA 메시지 작성은 선택사항이지만, GDPR 메시지 사용 설정을 할 경우, IDFA 메시지 작성도 같이 작성하십시오. 애드몹 UMP의 GDPR 동의 화면이 보이는 상태에서, 프로그래밍 방식으로 수동으로 ATT (AP

docs.adxcorp.kr

[사용자 메시지 플랫폼(UMP) Doc] https://developers.google.com/admob/unity/privacy?hl=ko

 

시작하기  |  Unity  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Google EU 사용자 동의 정책에 따

developers.google.com

[GoogleMobileAds.Ump.Api Doc] https://developers.google.com/admob/unity/reference/namespace/google-mobile-ads/ump/api

 

GoogleMobileAds.Ump.Api Namespace  |  Unity  |  Google for Developers

Stay organized with collections Save and categorize content based on your preferences. GoogleMobileAds.Ump.Api Summary Enumerations ConsentStatus ConsentStatus Consent status values. Properties NotRequired Consent not required. Obtained User consent obtain

developers.google.com

[GitHub/Sample] https://github.com/googleads/googleads-mobile-unity

 

GitHub - googleads/googleads-mobile-unity: Official Unity Plugin for the Google Mobile Ads SDK

Official Unity Plugin for the Google Mobile Ads SDK - googleads/googleads-mobile-unity

github.com

반응형
Posted by blueasa
, |

[추가2] 2024-04-08

Unity 2021.3.37f1

GoogleMobileAds 8.6.0

Firebase SDK 11.8.0

----

Unity 2021.3.37f1에서 기본 Gradle 버전이 Gradle 4.2.2로 버전업 되면서 별도로 Gradle 4.2.0을 다운받고 설정할 필요가 없어졌다.

그리고, Gradle 4.2.0에서 체크해야됐던 GoogleMobileAdsSettings 옵션을 더이상 체크 안해도 되게 됐다.

정리하자.

 

----

Unity 2021.3.32f1

GoogleMobileAds 8.6.0

Firebase SDK 11.6.0

----

 

[추가]

gradle을 Unity 2021 기본인 gradle plugin 4.0.1에서 gradle plugin 4.2.0으로 변경하고 나서

Unity Editor에서는 빌드가 잘되는데 이상하게 jenkins Android 에서만 빌드가 실패해서 삽질하면서 알아보니

jenkins를 Mac에 셋팅해뒀는데 gradle cache 폴더가 뭔가 꼬인 것 같다.

아래와 같은 Warning Log가 엄청나게 뜬다.

 

[Warning Log]

WARNING:/Users/{UserAccount}/.gradle/caches/transforms-2/files-2.1/ea30c3c071cd48c926311878c13eb08b/jetified-unity-classes.jar: D8: Expected stack map table for method with non-linear control flow.

 

그래서 아래 위치의 gradle cache 하위 있는 것들을 모두 삭제하고 새로 빌드를 실행해서 잘 돌아가는 것을 확인했다.

 

[Mac gradle cache 위치] /Users/{UserAccount}/.gradle/caches/

 

----

 

이번에 메모리 누수 이슈로 Unity 메이저 버전을 2022 -> 2021로 내렸는데,

GoogleMobileAds가 업데이트 돼서 기존 사용하던 8.4.1 -> 8.6.0으로 올리면서 Unity 버전관련해서 체크해야 될 사항들 간단히 정리해 둠.

 

일단 Unity 2022.3 이상은 상관 없었는데 2021.3으로 내려오면서 gradle 버전이 낮아지면서 처리할 문제가 생겼다.

아래 2가지 이슈를 수정하고 빌드 하자.

 

1) Unity 2021.3.32f1이 기본이 com.android.tools.build:gradle:4.0.1로 돼 있는데,

    GoogleMobileAds 8.6.0이 com.android.tools.build:gradle:4.2.0을 요구하고 있다.

    아래 링크의 내용을 참조해서 Base Gradle Templete에서 gradle plugin을 4.2.0으로 수정하고, gradle 6.7.1을 설치 및 연동하자.

 

   [설명 링크] https://developers.google.com/admob/unity/gradle?hl=ko#unity-integrated-builds

 

Android용 Gradle 업그레이드  |  Unity  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English Android용 Gradle 업그레이드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. The Google Mobile Ad

developers.google.com

 

['gradle 6.7.1 bin' Download Link] https://gradle.org/next-steps/?version=6.7.1&format=bin

 

Gradle | Thank you for downloading Gradle!

Next steps after downloading Gradle

gradle.org

 

2) 그리고, GoogleMobileAds 셋팅 파일에 Unity 2022.1 이하 프로젝트 관련 옵션이 2개 추가 되어 있는데,

     Unity 2021.3을 쓰고 있어서 둘 다 체크해야 된다.

    아래 내용과 링크를 참조하자.

GoogleMobileAds 8.6.0 Settings

 

[참조 링크] https://github.com/googleads/googleads-mobile-unity/issues/2930

 

play-services-ads:22.4.0 issue · Issue #2930 · googleads/googleads-mobile-unity

[REQUIRED] Step 1: Describe your environment Unity version: 2021.3.30f1 Google Mobile Ads Unity plugin version: 8.5.2 Platform: Android - Unity Editor Mediation ad networks used, and their versions...

github.com

 

[잡담]

Google도 이제 대충 하는건지..

이번에 새로 추가된 class 중에 Utils가 있는데 다른데서도 많이 쓰는 이름인데 namespace를 지정 안해놔서 클래스 명 충돌이 나서 namespace GoogleMobileAds로 처리 했다.

 

 

----

[참고]

Firebase도 당장은 아니지만(현재 Firebase SDK 11.6.0 기준 Firebase Android BoM 32.3.1) 추후 버전에서 곧 Android Gradle Plugin 버전을 4.2.0으로 강제하게 될 것 같다.

Firebase Android BoM Release Notes

[링크] https://firebase.google.com/support/release-notes/android

 

Firebase Android SDK Release Notes

 

firebase.google.com

반응형
Posted by blueasa
, |

[링크] https://learnandcreate.tistory.com/642

 

유니티에서 패키지를 기본값으로 재설정하기(reset pacakges to defaults)

유니티에서 패키지를 기본값으로 재설정하기(reset pacakges to defaults) 패키지를 기본값으로 재설정하면 프로젝트에서 사용자가 설치한 모든 패키지들을 제거하고 기본값으로 초기화합니다. 이 작

learnandcreate.tistory.com

 

반응형
Posted by blueasa
, |

[링크] https://j2su0218.tistory.com/503

 

[유니티Unity]유니티에서 인앱 업데이트 지원 하기

유니티에서 인앱 업데이트 지원 하기 구글 콘솔에 새로운 버전을 업데이트했을 때, 인앱 업데이트 기능을 넣어주지 않으면 사용자가 직접 앱을 업데이트시켜주어야 한다. 하지만 중요한 기능의

j2su0218.tistory.com

 

반응형
Posted by blueasa
, |