블로그 이미지
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-19 00:04

Android 13에서 Runtime에 android.permission.POST_NOTIFICATIONS Permission 요청하기.

----

 

1. Android Target API 33으로 셋팅

 

2. AndroidManifest.xml에 POST_NOTIFICATIONS Permission 추가

<manifest ...>
    <application ...>
        ...
    </application>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

 

3. 권한 요청 원하는 시점에 아래 함수 호출(함수 호출하면 Android 13이상 폰에서는 알림 권한 허용 요청 팝업이 뜸)

void CheckPermission_PostNotifications_Android13()
{
    // 디바이스의 안드로이드 api level 얻기
    // ex) Prints "Android OS API-33" on Android 13.0
    // https://docs.unity3d.com/ScriptReference/SystemInfo-operatingSystem.html
    string androidInfo = SystemInfo.operatingSystem;
    Debug.Log("androidInfo: " + androidInfo);
    
    int apiLevel = int.Parse(androidInfo.Substring(androidInfo.IndexOf("-") + 1, 3), System.Globalization.CultureInfo.InvariantCulture);
    Debug.Log("apiLevel: " + apiLevel);

    // 디바이스의 api level이 33 이상이라면 퍼미션 요청
    if (33 <= apiLevel 
        && !UnityEngine.Android.Permission.HasUserAuthorizedPermission("android.permission.POST_NOTIFICATIONS"))
    {
        UnityEngine.Android.Permission.RequestUserPermission("android.permission.POST_NOTIFICATIONS");
    }
}

 

 

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

[참고]

안드로이드 알림은 api level 26 과 33 에서 변경점이 있다. 내 앱이 대상으로 삼은 Sdk Version 범위에 이 버전이 포함되어 있다면 버전에 따라 분기 처리를 해주는 편이 좋다.
API level 26 (Android 8.0 Oreo) 이상 변경점
API level 33 (Android 13 Tiramisu) 이상 변경점

public void InitializeAndroidLocalPush()
{
	// 디바이스의 안드로이드 api level 얻기
	string androidInfo = SystemInfo.operatingSystem;
	Debug.Log("androidInfo: " + androidInfo);
	apiLevel = int.Parse(androidInfo.Substring(androidInfo.IndexOf("-") + 1, 2));
	Debug.Log("apiLevel: " + apiLevel);

	// 디바이스의 api level이 33 이상이라면 퍼미션 요청
	if (apiLevel >= 33 &&
		!Permission.HasUserAuthorizedPermission("android.permission.POST_NOTIFICATIONS"))
	{
		Permission.RequestUserPermission("android.permission.POST_NOTIFICATIONS");
	}

	// 디바이스의 api level이 26 이상이라면 알림 채널 설정
	if (apiLevel >= 26)
	{
		var channel = new AndroidNotificationChannel()
		{
			Id = CHANNEL_ID,
			Name = "pubSdk",
			Importance = Importance.High,
			Description = "for test",
		};
		AndroidNotificationCenter.RegisterNotificationChannel(channel);
	}
}

api level 얻기 코드 출처

 

 

[참고 내용 출처]

https://velog.io/@maratangsoft/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1%EC%97%90%EC%84%9C-FCM-%EC%84%9C%EB%B2%84-%ED%91%B8%EC%8B%9C-%EB%B0%9B%EA%B8%B0

 

유니티 안드로이드 앱에서 FCM 서버 푸시 받기

유니티 모바일 앱 개발시 안드로이드 앱은 Firebase Cloud Messaging(이하 FCM) 서비스, iOS 앱은 Apple Push Notification 서비스를 이용해서 (FCM으로도 가능) 푸시를 수신할 수 있다. 그 중 FCM을 이용한 안드로

velog.io

 

[참고]

https://android-developer.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C13%EC%97%90%EC%84%9C-Notification-%ED%97%88%EA%B0%80-%EB%B0%9B%EA%B8%B0-%EB%B0%A9%EB%B2%95-%EB%B0%8F-%EB%B3%80%EA%B2%BD%EC%A0%90

 

안드로이드13에서 Notification 허가 받기 방법 및 변경점

안드로이드13에서 Notification에 대해 바뀐점 POST_NOTIFICATIONS (Notification Permission) 은 Target SDK API 33 이상부터 추가 가능 Target SDK API 32 이하의 앱이 Android 13 디바이스에 설치되면 Notification Channel을 등록

android-developer.tistory.com

반응형
Posted by blueasa
, |

어쩌다보니 유니티 어필리에이트 참여하게 돼서 가입하고 보니 레퍼럴 링크 개념인 것 같다.

아래 링크들로 구매 or 구독하면 나에게 일정의 수수료를 주는 듯..

 

손해볼 건 없으니 참여했는데.. 내가 내 레퍼럴 링크로 사도 되나..?? 라는 궁금증이 생김.

 

 

Promote Unity Subscription Products:
To promote Unity subscription products, use these links:

Unity Plus:
https://prf.hn/click/camref:1011lvKwd/destination:https://store.unity.com/products/unity-plus

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

Unity Pro:
https://prf.hn/click/camref:1011lvKwd/destination:https://store.unity.com/products/unity-pro

 

Unity Pro

The complete solutions for professionals to create and operate.

unity.com

Unity Learn Premium:
https://prf.hn/click/camref:1011lvKwd/destination:https://unity.com/learn-premium

 

Unity Learn

Advance your Unity skills with live sessions and over 750 hours of on-demand learning content designed for creators at every skill level.

unity.com

Unity Partner Courses:
https://prf.hn/click/camref:1011lvKwd/destination:https://unity.com/learn/partner-courses

 

Learn

Online and in-person Unity courses and training in 2D, 3D, AR, and VR development.

unity.com

 

 

반응형
Posted by blueasa
, |

Unity 2021.3.22f1

----

 

Unity Editor에서 Memory Profiler를  보다보니 아래와 같이 System.Byte[] 라는게 있는데 204MB라니..

생각보다 용량을 많이 차지한다.

그래서 이래저래 찾아봤는데 나온 내용은 Unity Editor에서 사용하는 건지, 버그인지..

아무튼 별다른 문제는 없는 것 같다..(하단 링크 참조)

 

해당 메모리는 없는걸로 생각하고 봐야 될 듯 하다.

 

[참조] https://teratail.com/questions/339976

 

teratail【テラテイル】|ITエンジニア特化型Q&Aサイト

teratail(テラテイル)はプログラミングに特化したQ&Aサイトです。実現したい機能や作業中に発生したエラーについて質問すると、他のエンジニアから回答を得られます。

teratail.com

 

반응형
Posted by blueasa
, |

Unity 2022.3.10f1

NGUI 2022.08.01

----

[추가2] 2023-10-25

NGUI v2023.07.26에서 아래와 같은 업데이트가 올라왔다.

- FIX: NGUI will now change all imported textures to be uncompressed when creating an atlas, matching how it used to work in older versions of Unity.

이 수정 사항 때문에 NGUI Atlas 압축을 유지해주려던 부분이 작동하지 않고 무조건 Uncompressed로 변경되고 있어서 해당 부분인 아래 소스 부분(NGUIEditorTools.cs : line 538)을 주석 처리 했다.

//-------------------------------------------------
//			  NGUI: Next-Gen UI kit
// Copyright © 2011-2023 Tasharen Entertainment Inc
//-------------------------------------------------

using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;

/// <summary>
/// Tools for the editor
/// </summary>

static public class NGUIEditorTools
{
    ...
    static public bool MakeTextureReadable (string path, bool force)
    {
        if (string.IsNullOrEmpty(path)) return false;
        var ti = AssetImporter.GetAtPath(path) as TextureImporter;
        if (ti == null) return false;

        var settings = new TextureImporterSettings();
        ti.ReadTextureSettings(settings);

        if (force || !settings.readable || settings.npotScale != TextureImporterNPOTScale.None || ti.textureCompression != TextureImporterCompression.Uncompressed)
        {
            settings.readable = true;

            if (NGUISettings.trueColorAtlas)
            {
                var platform = ti.GetDefaultPlatformTextureSettings();
                platform.format = TextureImporterFormat.RGBA32;
            }

            settings.npotScale = TextureImporterNPOTScale.None;
            
            #region NGUI Atlas 압축 관련 처리 [blueasa / 2023-10-25]
            /// NGUI v2023.07.26 에서 강제로 Uncompressed 하는 소스가 추가됨.
            /// 이 부분 때문에 아틀라스 압축이 유지되지 않고 풀리는 문제가 있어서 주석 처리 함
            /// trueColorAtlas 셋팅 될 때만 Uncompressed 강제 하도록 함
            if (NGUISettings.trueColorAtlas)
            {
                ti.textureCompression = TextureImporterCompression.Uncompressed;
            }
            #endregion
            
            ti.SetTextureSettings(settings);

#if UNITY_5_6
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
#else
            ti.SaveAndReimport();
#endif
        }
        return true;
    }
    ...
}

 

----

[추가]

아틀라스 압축을 사용하려면 Atlas Maker의 Truecolor를 끄자.

(켜놓으면 압축상태를 무시하고 무조건 Truecolor로 아틀라스를 묶어낸다.)

Truecolor 체크 해제

----

 

[수정] New Atlas 관련 예외처리 추가(2023-04-12)

 

----

게임 최적화를 위해 Andrio/iOS 둘다 설정에서 Texture compression format을 ASTC로 설정하고 사용하고 있다.

NGUI Atlas도 최적화를 위해 Compression을 High Quality로 사용중인데

여기서 문제가 NGUI의 Atlas를 묶을 때는 Compression이 None(비압축)이 아니면 에러가 발생한다.

 

그래서 Atlas 묶을 때는 Compression을 None으로 풀어서 묶은 다음 원래 Compression인 High Quality로 변경하고 있었는데

불편하기도하고 사용하다 실수가 나오기도 해서 이참에 NGUI Atlas Maker를 수정해서 자동으로 되도록 했다.

 

작동 원리는 아래와 같이 간단하다.

(처음 해보는거라 적용 지점이 어딘지 찾는다고 좀 헤멤)

 

  [작동 원리]

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

- Atlas가 압축 된 상태면

  1. Compression 별도로 저장

  2. Compression : None으로 변경

  3. Atlas Make(NGUI 원래 소스)

  4. 별도로 저장했던 Compression을 Atlas에 적용해서 원래 압축 상태로 되돌려 줌

- Atlas가 압축 안된 상태(None)면

  1. 원래 소스만 실행(위에서 3.만 실행)

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

 

추가된 소스는 NGUI의 UIAtlasMaker.cs - UpdateTexture() 함수에 두 곳 추가 되었다.

소스 위치를 알기 위해 UpdateTexutre() 함수 전체를 올렸고, blueasa를 찾으면 region 정리해 둔 두 곳이 있다.

 

//-------------------------------------------------
//            NGUI: Next-Gen UI kit
// Copyright © 2011-2020 Tasharen Entertainment Inc
//-------------------------------------------------

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

/// <summary>
/// Atlas maker lets you create atlases from a bunch of small textures. It`s an alternative to using the external Texture Packer.
/// </summary>
public class UIAtlasMaker : EditorWindow
{
	....
    
    /// <summary>
    /// Combine all sprites into a single texture and save it to disk.
    /// </summary>
    
    static public bool UpdateTexture (INGUIAtlas atlas, List<SpriteEntry> sprites)
    {
        // Get the texture for the atlas
        var tex = atlasTexture;
        var oldPath = (tex != null) ? AssetDatabase.GetAssetPath(tex.GetInstanceID()) : "";
        var newPath = NGUIEditorTools.GetSaveableTexturePath(atlas as UnityEngine.Object, atlasTexture);
    
        // Clear the read-only flag in texture file attributes
        if (System.IO.File.Exists(newPath))
        {
            System.IO.FileAttributes newPathAttrs = System.IO.File.GetAttributes(newPath);
            newPathAttrs &= ~System.IO.FileAttributes.ReadOnly;
            System.IO.File.SetAttributes(newPath, newPathAttrs);
        }
    
        bool newTexture = (tex == null || oldPath != newPath);
    
        if (newTexture)
        {
            // Create a new texture for the atlas
            tex = new Texture2D(1, 1, TextureFormat.ARGB32, false);
        }
        else
        {
            // Make the atlas readable so we can save it
            tex = NGUIEditorTools.ImportTexture(oldPath, true, false, false);
        }
    
        #region NGUI Atlas 압축 관련 처리 [blueasa / 2023-04-07]
        /// NGUI Atlas가 압축돼 있으면,
        /// 압축 해제한 다음 Atlas 만든 후 원래 압축으로 돌리도록 함
    
        // TextureImporter 가져오기
        string strPath = AssetDatabase.GetAssetPath(tex);
        TextureImporter tiAtlas = AssetImporter.GetAtPath(strPath) as TextureImporter;
        TextureImporterCompression ticTextureCompression_Original = TextureImporterCompression.Uncompressed;
        bool bTextureCompressed = false;
    
        // New Atlas 관련 예외처리(New Atlas는 로직 타지 않도록 수정)
        if (false == newTexture)
        {
            Debug.LogWarningFormat("[Atlas TextureCompression] {0}", tiAtlas.textureCompression);
    
            if (tiAtlas.textureCompression != TextureImporterCompression.Uncompressed)
            {
                // Atlas Texutre 압축 여부
                bTextureCompressed = true;
                // 압축 설정 백업
                ticTextureCompression_Original = tiAtlas.textureCompression;
                // 압축 설정 변경
                tiAtlas.textureCompression = TextureImporterCompression.Uncompressed;
                // 변경 내용 적용
                AssetDatabase.ImportAsset(strPath);
            }
        }
        #endregion
    
        // Pack the sprites into this texture
        if (PackTextures(tex, sprites))
        {
            var bytes = tex.EncodeToPNG();
            System.IO.File.WriteAllBytes(newPath, bytes);
            bytes = null;
        
            // Load the texture we just saved as a Texture2D
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
            tex = NGUIEditorTools.ImportTexture(newPath, false, true, !premultipliedAlpha);
        
            // Update the atlas texture
            if (newTexture)
            {
                if (tex == null)
                {
                    Debug.LogError("Failed to load the created atlas saved as " + newPath);
                    EditorUtility.ClearProgressBar();
                }
                else
                {
                    var mat = spriteMaterial;
        
                    if (mat == null)
                    {
                        var matPath = newPath.Replace(".png", ".mat");
                        var shader = Shader.Find(NGUISettings.atlasPMA ? "Unlit/Premultiplied Colored" : "Unlit/Transparent Colored");
                        mat = new Material(shader);
	    
                        // Save the material
                        AssetDatabase.CreateAsset(mat, matPath);
                        AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
        
                        // Load the material so it`s usable
                        mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
                        spriteMaterial = mat;
                    }
        
                    mat.mainTexture = tex;
                }
        
                ReleaseSprites(sprites);
        
                AssetDatabase.SaveAssets();
                AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
            }
        
            #region NGUI Atlas 압축 관련 처리 [blueasa / 2023-04-07]
            if (true == bTextureCompressed)
            {
                // 압축 설정 복원
                tiAtlas.textureCompression = ticTextureCompression_Original;
                // 변경 내용 적용
                AssetDatabase.ImportAsset(strPath);
            }
            #endregion
        
            return true;
        }
        else
        {
            if (!newTexture) NGUIEditorTools.ImportTexture(oldPath, false, true, !premultipliedAlpha);
    
            //Debug.LogError("Operation canceled: The selected sprites can`t fit into the atlas.\n" +
            //	"Keep large sprites outside the atlas (use UITexture), and/or use multiple atlases instead.");
    
            EditorUtility.DisplayDialog("Operation Canceled", "The selected sprites can`t fit into the atlas.\n" +
                    "Keep large sprites outside the atlas (use UITexture), and/or use multiple atlases instead", "OK");
            return false;
        }
    }
    
    ....
}

 

 

반응형
Posted by blueasa
, |

Audio Import 시, 원하는 최적화 관련 설정 자동 셋팅하도록 소스 정리 함.

(AudioCompressionFormat은 심플하게 Vobis / Quality 40으로 통일함.)

 

using System.IO;
using UnityEditor;
using UnityEngine;

public class AssetPostprocessor_Audio : AssetPostprocessor
{
    /*
     * 오디오클립 파일 경로
     * Resources - Sound ┬ BGM - MainBGM
     *                   ├ InGame - SFX
     *                   └ SoundEffect ┬ OutGame
     *                                 ├ UI
     *                                 └ Voice
     * Resources_AssetBundles ┬ sound ┬ sound_effect
     *                        │       ├ sound_main_bgm
     *                        │       ├ sound_sub_bgm
     *                        │       └ sound_voice
     *                        ├ sound_scenario
     *                        └ sound_scenario_StreamingAssets
     */
    void OnPreprocessAudio()
    {
        bool bDefaultSettingChanged = false;
        AudioImporter audioImporter = assetImporter as AudioImporter;
        AudioImporterSampleSettings defaultSampleSettings = audioImporter.defaultSampleSettings;
        AudioImporterSampleSettings iosSampleSettings = audioImporter.GetOverrideSampleSettings("iOS");
        bool biOSSettingChanged = false;

        UnityEditor.SerializedObject serializedObject = new UnityEditor.SerializedObject(audioImporter);
        UnityEditor.SerializedProperty normalize = serializedObject.FindProperty("m_Normalize");

        string strPath = audioImporter.assetPath;
        Debug.LogFormat("[audioImporter.assetPath] {0}", strPath);

        // Folder Name
        if (strPath.StartsWith("Assets/Resources/Sound/BGM"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = true;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.Streaming;
            audioImporter.preloadAudioData = false;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            //iosSampleSettings.loadType = AudioClipLoadType.Streaming;
            //iosSampleSettings.compressionFormat = AudioCompressionFormat.MP3;
            //iosSettingChanged = true;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources/Sound/InGame/SFX"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources/Sound/SoundEffect/OutGame"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources/Sound/SoundEffect/UI"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources/Sound/SoundEffect/Voice"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.CompressedInMemory;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound/sound_effect"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound/sound_main_bgm"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = true;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.Streaming;
            audioImporter.preloadAudioData = false;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound/sound_sub_bgm"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = true;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.Streaming;
            audioImporter.preloadAudioData = false;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound/sound_voice"))
        {
            audioImporter.forceToMono = true;
            normalize.boolValue = false;
            audioImporter.loadInBackground = false;
            //audioImporter.ambisonic = false;

            defaultSampleSettings.loadType = AudioClipLoadType.CompressedInMemory;
            audioImporter.preloadAudioData = true;
            defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            defaultSampleSettings.quality = 0.4f;   // 40
            defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

            bDefaultSettingChanged = true;
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound_scenario"))
        {
            // File Name
            if (strPath.Contains("/bg_"))  // BGM
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = true;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.Streaming;
                audioImporter.preloadAudioData = false;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
            else if(strPath.Contains("/se_"))  // SFX
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = false;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
                audioImporter.preloadAudioData = true;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
            else    // Etc(SFX?)
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = false;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
                audioImporter.preloadAudioData = true;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
        }
        else if (strPath.StartsWith("Assets/Resources_AssetBundles/sound_scenario_StreamingAssets"))
        {
            // File Name
            if (strPath.Contains("/bg_"))  // BGM(
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = true;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.Streaming;
                audioImporter.preloadAudioData = false;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
            else if (strPath.Contains("/se_"))  // SFX
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = false;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
                audioImporter.preloadAudioData = true;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
            else    // Etc(SFX?)
            {
                audioImporter.forceToMono = true;
                normalize.boolValue = false;
                audioImporter.loadInBackground = false;
                //audioImporter.ambisonic = false;

                defaultSampleSettings.loadType = AudioClipLoadType.DecompressOnLoad;
                audioImporter.preloadAudioData = true;
                defaultSampleSettings.compressionFormat = AudioCompressionFormat.Vorbis;
                defaultSampleSettings.quality = 0.4f;   // 40
                defaultSampleSettings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

                bDefaultSettingChanged = true;
            }
        }

        try
        {
            // 수정됐으면 Save
            if (true == bDefaultSettingChanged)
            {
                audioImporter.defaultSampleSettings = defaultSampleSettings;
                serializedObject.ApplyModifiedProperties();
                if (true == biOSSettingChanged)
                {
                    audioImporter.SetOverrideSampleSettings("iOS", iosSampleSettings);
                }
                audioImporter.SaveAndReimport();

                EditorUtility.SetDirty(audioImporter);

                Debug.LogFormat("[{0}] has been imported to [{1}]\n[Force To Mono] {2}\n[Normalize] {3}\n[Load In Background] {4}\n[Load Type] {5}\n[Compression Format] Default: {6}, iOS: {7}\n[Quality] {8}\n[Sample Rate Setting] {9}",
                                Path.GetFileName(strPath),
                                Path.GetDirectoryName(strPath),
                                audioImporter.forceToMono.ToString(),
                                normalize.boolValue,
                                audioImporter.loadInBackground,
                                defaultSampleSettings.loadType.ToString(),
                                defaultSampleSettings.compressionFormat.ToString(),
                                iosSampleSettings.compressionFormat.ToString(),
                                (defaultSampleSettings.quality * 100f),
                                defaultSampleSettings.sampleRateSetting.ToString()
                                );
            }

            // OnPreprocessAudio() 에서는 Refresh 안함
            //AssetDatabase.Refresh();
        }
        catch (System.Exception e)
        {
            Debug.LogError(e);
        }
    }

    // 파일이 이동한 경우 다시 임포트.
    static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        try
        {
            foreach (string movedAsset in movedAssets)
            {
                if (movedAsset.StartsWith("Assets/Resources/Sound")
                    || movedAsset.StartsWith("Assets/Resources_AssetBundles/sound")
                    || movedAsset.StartsWith("Assets/Resources_AssetBundles/sound_scenario")
                    || movedAsset.StartsWith("Assets/Resources_AssetBundles/sound_scenario_StreamingAssets")
                    )
                {
                    AssetDatabase.ImportAsset(movedAsset);
                }
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError(e);
        }
    }
}

 

[참조]

https://overworks.github.io/unity/2018/09/22/unity-audio-clip-import-guide.html

https://jwidaegi.blogspot.com/2019/07/unity-sound.html?sc=1680659862098#c4452839809969885205

https://www.jianshu.com/p/b389936ff1ad

https://blog.csdn.net/hururu20120502/article/details/121425411

https://drehzr.tistory.com/1016

 

 

반응형
Posted by blueasa
, |

Today we’ll be talking about the Preload Audio Data option in Unity’s Audio Import Settings and why understanding this small but critical checkbox is important for your game.

So many options… How does Unity choose which audio is loaded to a scene?

Editor’s Note: Compression Formats, Load Type and the Preload Audio Data/Load in Background settings go hand in hand. We recommend reading our blog posts on all three in the following order for a complete overview:

Blog 1: Compression Formats

Blog 2: Load Types

Blog 3: Preload Audio (this post)

Blog 4: Load in Background (coming soon)

This blog post has been made as a supplement to our video tutorial on this same topic.

 
 

Introduction

After figuring out how Unity compresses your audio with the different compression formats, and primes your audio for playing with the different Load Type settings, the next question to answer is: What does the Preload Audio Data option do?

The Unity Documentation provides important information on this subject, but it doesn’t explain exactly when and how audio files are selected to be loaded in an active scene.

 

Preload Audio Data

Here’s Unity’s definition of Preload Audio Data from their manual:

If enabled, the audio clip will be pre-loaded when the scene is loaded. This is on by default to reflect standard Unity behavior where all AudioClips have finished loading when the scene starts playing.

This information is pretty clear already. If you choose this option, you want the audio priming process with the respective Load Types to take place before the scene is activated and becomes playable.

This will result in mission-critical audio files being ready to be played at all times at the expense of making the scene load time a little bit longer.

So far, so good. But there is one question left unanswered:

How does Unity decide which Audio is going to be loaded in which Scene?

 

References

Let’s imagine this situation: you’re developing a large game that has 15 hours of playtime and around 30 scenes. You have something like 1000 SFX and 2000 dialogue files. You would not want every single audio asset marked with Preload Audio Data to be loaded in every scene, would you?

Unfortunately, the documentation doesn’t address this in detail, making the process of deciding whether or not to Preload your audio assets very difficult and full of uncertainty.

Luckily, Unity is a sophisticated engine. After many tests, we concluded that Unity will scan everything in our scene and load all audio files flagged with Preload Audio Data that are referenced in the scene in any way.

Let’s take a step back here because now we have another question to answer:

What is a reference?

Let’s open up Unity and find out.

 

Testing Testing

We’ve imported 10 sound effects, 10 dialogue, and 2 music files into a Unity project. They’re all marked with Preload Audio Data to simulate a larger game. There are also two different scenes to simulate loading and unloading audio in a normal scene transition.

Let’s start with the first scene. We have a GameObject here with an AudioSource that has an AudioClip.

We hope this looks familiar.

This is already a so-called reference and this audio file will be loaded when the scene is loaded. I turned off Play on Awake for this to make sure that the audio is not played. Let’s build this and confirm with the profiler.

Our AudioClip is loaded and ready for action.

Perfect. Even though the AudioClip is not played at all, it is loaded because it has been referenced.

Note that this music file, which has “Streaming” selected as its Load Type, is only 311 bytes in the memory, unlike the 200-ish kilobytes from the last tutorial. That will change if we actually play the sound file.

Let’s now add two scripts we’ve prepared for another GameObject.

These two scripts do nothing but define a public AudioClip variable and a public AudioClip array. Let’s add an AudioClip to that and profile this.

Voilá. We have the second audio clip loaded.

For the sake of this demonstration, I will add another two AudioClips to the array and check the profiler once again.

As you might have guessed, without Preload Audio Data selected, Unity will not load the AudioClip until it is called, even though it is referenced in the scene. We’ll be going more in-depth on this in our next tutorial.

So, a reference is as simple as that. As long as you have a reference of any kind to an AudioClip, be it directly in the AudioSource, as a variable in a script, or even in a ScriptableObject, it will be loaded when the scene is loaded if you have checked Preload Audio Data.

 

Scene Transitions

Now: what happens if we change to another scene? Will the audio data we loaded in this scene be automatically unloaded?

Let’s go to the second scene. We’ll keep one AudioClip constant across both scenes but change the rest. Now we can build and profile this once again.

And then we’ll transition to the next scene.

We see here that all but one of our AudioClips from the first scene are already unloaded from the memory, leaving only the one that is used again in the second scene.

The unloading process took place as the scene was changed; this is a normal routine that Unity does in the background called GarbageCollection.

 

Final Words

Let’s summarize what we’ve learned:

  • Selecting Preload Audio Data means the scene will not start until all sounds with this setting are loaded into memory. This is most important for audio content that is needed right away in the scene, such as footsteps, UI, or anything synced to visual content that plays immediately upon the scene’s activation.
  • Unity determines which files to load in a scene by searching for references. If an audio file is referenced unnecessarily, for example, an AudioClip in an AudioSource that you ended up not using but didn’t delete from the scene, it will be loaded as well, even if the entire GameObject is deactivated. In that respect, it’s important to keep your scenes clear of unused references.
 

That’s it! We hope this tutorial has helped you to understand the Preload Audio Data option in Unity’s Import Settings and why this information is important for your game.

If you want to learn more about how audio functions in Unity, be sure to check out our other tutorials (linked below). If you have a question, leave it in the comments or contact us directly through our website:

 

 

[출처] https://medium.com/@made-indrayana/preload-audio-data-how-unity-decides-which-audio-assets-to-load-to-a-scene-a440a654e7e2

 

Preload Audio Data & How Unity Decides Which Audio Assets to Load to a Scene

Today we’ll be talking about the Preload Audio Data option in Unity’s Audio Import Settings and why it’s important for your game.

medium.com

 

반응형
Posted by blueasa
, |

Force To Mono

Unity 측에서 음원을 모노로 변환하여 용량을 줄여주는 설정
모바일이라고 ON하고 싶다.

인스펙터상이라면 이 설정을 ON으로 하면 Normalize옵션 설정을 선택할 수 있게 된다.
이것은 음량을 평균화해 주는 기능입니다만, 음소재측에서 음량 조정되고 있는 케이스도 있어, 「작은 소리로 하고 싶었는데 어쨌든 음량 오르지 않았어?」같은 것이 됩니다
.

그래서, 모노럴 설정은 ON으로 하면서, 노멀라이즈는 OFF로 해 두고 싶은 기분이 되네요.

 

가져오기 설정을 자동화하고 싶습니다.

수동으로 하는 것은 힘들어지므로,
AudioImporter 를 스크립트로부터 괴롭히고 어떻게든 하고 싶다.

그러나 AudioImpoter에는 forceToMono매개 변수가 있지만
Normalize관련 매개 변수가 노출되지 않았습니다.

 

결과

아래와 같이 해 주면, 해당의 파라미터를 취득해 재기입할 수 있었습니다.

 

var audioImporter = assetImporter as AudioImporter;
var serializedObject = new UnityEditor.SerializedObject(audioImporter);
var normalize = serializedObject.FindProperty("m_Normalize");
normalize.boolValue = false;

serializedObject.ApplyModifiedProperties();

audioImporter.SaveAndReimport();
UnityEditor.EditorUtility.SetDirty(audioImporter);
UnityEditor.AssetDatabase.Refresh();

 

https://answers.unity.com/questions/1016473/how-to-disable-normalize-in-audioimporter.html
https://docs.unity3d.com/ScriptReference/EditorUtility.SetDirty.html

 

 

[출처] https://qiita.com/mrhdms/items/85bd2e524a9c76e1273c

 

AudioImporter で Force to mono は有効にしつつ、Normalize だけ無効にする - Qiita

Force to mono Unity側で音源をモノラルに変換して、容量を削減してくれる設定 モバイルだとONにしたい。 インスペクタ上だと、この設定をONにすると、 Normalize というオプション設定が選択でき

qiita.com

 

[참조] https://www.jianshu.com/p/b389936ff1ad

 

Unity插件-音频参数优化

需求:需要对音频做出修改,压缩音频质量,检测是否设置为Mono与Normalize, 超过10秒LoadType设为Streaming. 思路:遍历所有的音频文件,拿到所有的...

www.jianshu.com

 

반응형
Posted by blueasa
, |

[링크] https://overworks.github.io/unity/2018/09/22/unity-audio-clip-import-guide.html

 

유니티 오디오 클립 임포트 설정 가이드

옵션 설명

overworks.github.io

 

반응형
Posted by blueasa
, |

모바일 게임을 제작하다 보면, PC-에디터에서는 잘 나오던 게임 사운드가 핸드폰에서는 밀려 나오는 경우를 종종 겪을 수 있다.

이런 상황을 해결하기 위한 설정값을 기록해 둔다.
출처 : https://docs.unity3d.com/kr/current/Manual/class-AudioClip.html

프로젝트 세팅

Edit > Project Settings > Audio에서 DSP Buffer Size Best latency로 설정
- Best Performance : 성능이 우선 되어 출력 시 지연이 발생할 수 있음
- Best latency : 지연이 발생하지 않는 것을 우선시하여 출력 품질이 저하될 수 있음


리소스 타입별 세팅

배경음 1
- Force To Mono : 언 체크(퀄리티에 따라 가변)
- Load In Background : 체크
- Load Type : Streaming
- Preload Audio Data : 언 체크
- Compression Format : Vobis (100%)

배경음 2
- Force To Mono : 언 체크(퀄리티에 따라 가변)
- Load In Background : 체크
- Load Type : Compressed In Memory
- Preload Audio Data : 언 체크
- Compression Format : Vobis (70%)

효과음 : 작은 크기의 빈번한 출력 
- Force To Mono : 체크
- Load In Background : 언 체크
- Load Type : Decompress On Load
- Preload Audio Data : 체크
- Compression Format :  PCM

긴 효과음(보이스) : 중간 크기의 빈번한 출력
- Force To Mono : 체크
- Load In Background : 언 체크
- Load Type : Compressed In Memory
- Preload Audio Data : 체크
- Compression Format :  ADPCM

작은 크기의 가끔 발생하는 Sound
- Force To Mono : 체크
- Load In Background : 언 체크
- Load Type : Compressed In Memory
- Preload Audio Data : 언 체크
- Compression Format :  ADPCM

중간 크기의 가끔 발생하는 Sound
- Force To Mono : 체크
- Load In Background : 언체크
- Load Type : Compressed In Memory
- Preload Audio Data : 언 체크
- Compression Format :  Vobis (70%)


개별 리소스 세팅 정보

Force To Mono(모노 강제조정)
- 스테레오를 모노로 강제 조정
- 모바일이고 최적화를 중시할 경우 설정(체크) 함

Load In Background(지연된 로딩)
- 체크 시 출력 타이밍을 엄격히 지키지 않고, 느긋하게 백그라운드에서 로드
- 따라서 배경음악일 경우 사용 FX 사운드의 경우 체크 해제


Load Type
- Decompress On Load
 실행과 동시에 압축을 해제
 작은 사이즈의 FX 사운드에 유용
 많은 메모리 점유 CPU는 적게 사용

- Compressed In Memory
 메모리에 압축 상태로 저장, 실행 시 압축을 해제하여 재생
 약간의 성능상 오버헤드를 발생시킴
 보이스 사운드 등에 사용

- Streaming
 저장소에 위치한 오디오를 실시간으로 읽어냄.
 보통 배경음악에서 사용


Preload Audio Data
- 씬이 로딩될 때 씬에서 사용하는 모든 오디오 클립을 미리 로드
- 언체크시 플레이시 로드 하기에 랙 발생 됨


Compression Format
- PCM
 최고품질 / 용량 큼 / 작은 파일 크기에 적합 / FX 사운드
 Load Type은 Decompress On Load로 하자
 즉시 재생해야 하는 매우 짧은 효과음

- ADPCM
 중간 품질 / 용량 중간
 PCM대비 3.5배의 압축비, 노이즈가 포함됨
 총격 소리와 같이 무압축에 가까운 품질 까지는 필요없지만,
 지연시간 없이 자주 반복 재생 되야 하는 경우 적절

- Vobis
 최저품질 / 용량 적음 / 배경음에 적합
 압축률 설정이 가능(보통 70%로 설정)
 지연 재생 되어도 무방한 일반적인 배경음

 

 

[출처] https://jwidaegi.blogspot.com/2019/07/unity-sound.html

 

Unity Sound 설정

모바일 게임을 제작하다 보면, PC-에디터에서는 잘 나오던 게임 사운드가 핸드폰에서는 밀려 나오는 경우를 종종 겪을 수 있다. 이런 상황을 해결하기 위한 설정값을 기록해 둔다. 출처 :  https://

jwidaegi.blogspot.com

 

반응형
Posted by blueasa
, |

Unity 2021.3.16f1

Xcode 14.2

Firebase 10.4.0

----

 

[빌드 에러]

pod install output:



Analyzing dependencies

[!] CocoaPods could not find compatible versions for pod "GTMSessionFetcher/Core":

  In Podfile:

    Firebase/Auth (= 10.6.0) was resolved to 10.6.0, which depends on

      FirebaseAuth (~> 10.6.0) was resolved to 10.6.0, which depends on

        GTMSessionFetcher/Core (< 4.0, >= 2.1)



    GoogleSignIn (= 6.0.2) was resolved to 6.0.2, which depends on

      GTMAppAuth (~> 1.0) was resolved to 1.3.1, which depends on

        GTMSessionFetcher/Core (< 3.0, >= 1.5)



    GoogleSignIn (= 6.0.2) was resolved to 6.0.2, which depends on

      GTMSessionFetcher/Core (~> 1.1)

 

Firebase 10.6.0이 업데이트 돼서 다시 한 번 iOS 빌드를 시도해 봤는데 여전히 에러가 난다.

(참조 : https://blueasa.tistory.com/2706)

 

[XCode] [!] Cocoapods could not find compatible versions for pod "GTMSessionFetcher/Core"

Unity 2021.3.16f1 Xcode 14.2 Firebase 10.4.0 ---- [빌드에러] pod install output: Analyzing dependencies [!] CocoaPods could not find compatible versions for pod "GTMSessionFetcher/Core": In Podfile: Firebase/Auth (= 10.4.0) was resolved to 10.4.0, whic

blueasa.tistory.com

 

그래도 다시 이리저리 검색해보다보니 해결했다는 글이 아래와 같이 있다.

UnayOzan commented on Feb 5
Fixed this error with changing the version in "GoogleSignIn.podspec.json" file.

Firebase/Auth (= 10.4.0) was resolved to 10.4.0, which depends on
FirebaseAuth (~> 10.4.0) was resolved to 10.4.0, which depends on
GTMSessionFetcher/Core (< 4.0, >= 2.1)

GoogleSignIn (~> **5.0.2**) was resolved to **5.0.2**, which depends on
  GTMSessionFetcher/Core (~> **1.1**)
I found the file in {user}.cocoapods/repos/cocoapods/Specs and then just search for "GoogleSignIn.podspec.json".
Find the folder with the same version with the error, in my case 5.0.2.
And then change its dependencies with the version 7.0.0 or something that works with the other packages.
In my project the solution was this;

 "dependencies": {
    "AppAuth": [
      "~> 1.5"
    ],
    "GTMAppAuth": [
      ">= 1.3",
      "< 3.0"
    ],
    "GTMSessionFetcher/Core": [
      ">= 1.1",
      "< 4.0"
    ]
After that I exited Xcode, deintagrated pods, cleaned cache and installed it again.
Everything works fine now.

[링크] https://github.com/firebase/firebase-unity-sdk/issues/621

 

[Bug] FirebaseAuth and GoogleSignIn iOS Build failure · Issue #621 · firebase/firebase-unity-sdk

[REQUIRED] Please fill in the following fields: Unity editor version: 2021.3.17f1 Firebase Unity SDK version: 10.4.0 Source you installed the SDK: .unitypackage Problematic Firebase Component: Auth...

github.com

 

글을 보면 아래와 같은 내용이 있다.

And then change its dependencies with the version 7.0.0 or something that works with the other packages.
In my project the solution was this;

에러도 나와 거의 같아서 확인해보니 GoogleSignIn 버전이 현재 6.0.2여서 GTMSessionFetcher/Core (~> 1.1)를 지원하는데,

설치한 FirebaseAuth 10.6.0은 GTMSessionFetcher/Core (< 4.0, >= 2.1)을 사용하고 있어서

GTMSessionFetcher/Core 버전이 서로 호환이 안되는 것 같다.

 

위 링크의 내용대로

GoogleSignIn 버전을 7.0.0(2023-03-27 기준 최신) 버전으로 변경하고 빌드를 다시해보니 깔끔하게 빌드가 잘 된다.

 

[추가]

빌드는 잘 되는데 회사 플랫폼에서 사용하는 GoogleSignIn이 6.0.2여서 7.0.0으로 올리면서 API 호환이 안돼서 로그인 시도할 때 Crash가 나면서 앱이 강제종료 된다.

6.2.4
  2.x 버전을 허용하도록 GTMSessionFetcher 종속 항목을 업데이트했습니다. (#207)

GoogleSignIn에서 해당 빌드 오류 이슈를 해결한 버전은 6.2.4여서 6.2.4로 버전을 내렸다.

나처럼 다른 이슈로 7.0.0으로 못올리는 사람은 6.2.4로 한 번 시도해 보면 좋을 것 같다.

 

[GoogleSignIn 버전 참조] https://developers.google.com/identity/sign-in/ios/release?hl=ko

 

Google 로그인 iOS 및 macOS SDK 출시 노트  |  Authentication  |  Google Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 의견 보내기 Google 로그인 iOS 및 macOS SDK 출시 노트 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분

developers.google.com

 

반응형
Posted by blueasa
, |