유니티

Mobile Notifications (로컬 푸시 기능)

잼잼재미 2025. 11. 8. 17:01

로컬 푸시


 

유니티에서 제공하는 로컬 알림을 띄우기 위한 공식 패키지. 앱이 꺼져있어도 지정한 시간에 기기로 알림을 보낼 수 있다. 안드로이드, IOS 모두 지원한다.

 

 

구현 방법


1. Mobile Notifications 설치

 

 

2. 아이콘 이미지 준비

 

기존 앱 아이콘 이미지를 그대로 사용하면 오류가 나기 때문에 위 처럼 Read/Write 체크를하고 Apply를 해준다. Large, Small 총 2개의 이미지가 필요하다.

 

Large Size : 앱 아이콘에 사용하는 이미지 그대로 사용

 

Small Size  : 1. 알파 채널이 0 또는 255만 존재

                     2. RGB 채널은 전부 흰색

                     3. PNG 포맷 (24bit + 알파)

                     4. 48 x 48 픽셀

해당 조건에 맞게 변환하여 사용

 

 

3. Mobile Notifications 설정

 

Android

Schedule at exact time : Exact When Available 만 체크 (그 외 불필요한 기능을 체크하면 구글 심사에서 걸릴 수가 있음)

이미지 추가 : 준비한 아이콘 이미지를 추가하고 Large, Small 설정

 

IOS

* 추후 추가 작성 예정

 

 

4. LocalPushManager 

using System;
using UnityEngine;
using System.Collections;
using UnityEngine.Android;



#if UNITY_IOS
using Unity.Notifications.iOS;
#endif

#if UNITY_ANDROID
using Unity.Notifications.Android;
#endif

public class LocalPushManager : Singleton<LocalPushManager>
{
    #region Instance
    public static LocalPushManager Instance
    {
        get
        {
            if (m_Instance == null && Application.isPlaying)
            {
                GameObject Obj = GameObject.Find("[Managers]");
                if (Obj == null)
                {
                    Obj = new GameObject("[Managers]");
                    DontDestroyOnLoad(Obj);
                }

                GameObject ManagerObj = GameObject.Find("[Managers]/LocalPushManager");
                if (ManagerObj == null)
                {
                    ManagerObj = new GameObject("LocalPushManager");
                    ManagerObj.transform.SetParent(Obj.transform);
                }

                m_Instance = ManagerObj.GetComponent<LocalPushManager>();
                if (m_Instance == null)
                {
                    m_Instance = ManagerObj.AddComponent<LocalPushManager>();
                }

                m_Instance.CreateInstance();
            }

            return m_Instance;
        }
    }
    #endregion

    #region Member Property
    #endregion

    #region Override Method
    protected override void CreateInstance()
    {
        Init();
    }

    public override void DestroyInstance() { }
    #endregion

    #region Public Method
    // 지정한 시간에 푸시 알람을 예약합니다.
    public void SchedulePushNotification(LocalPushType pushType, string title, string message, DateTime scheduleTime)
    {
        // 예약 시간이 현재보다 미래인지 확인
        if (scheduleTime <= Util.DateTimeNow)
        {
            Debug.LogWarning("The time is earlier or equal to the current time. Please enter a valid future time.");
            return;
        }

        try
        {
#if UNITY_ANDROID
           
            // Android: 알림 객체 생성 및 설정
            var notification = new AndroidNotification();
            notification.Title = title;
            notification.Text = message;
            notification.FireTime = scheduleTime;
            notification.LargeIcon = "icon_0";
            notification.SmallIcon = "icon_1";
            notification.ShowInForeground = true;
            string channelId = "my_channel_id";

            int pushCode = AndroidNotificationCenter.SendNotification(notification, channelId);

            switch (pushType)
            {
                case LocalPushType.FreeGold:
                    PlayerPrefs.SetInt(ClientDef.LOCALKEY_Push_FreeGold, pushCode);
                    break;

                default:
                    break;
            }

#elif UNITY_IOS
            // iOS: 예약 시간과 현재 시간 간의 간격(TimeInterval) 계산
            TimeSpan interval = scheduleTime - GameManager.Instance.DateTimeNow;

            if (interval.TotalSeconds < 1)
            interval = TimeSpan.FromSeconds(1);

            var timeTrigger = new iOSNotificationTimeIntervalTrigger()
            {
                TimeInterval = interval,
                Repeats = false
            };

            string notificationId = Guid.NewGuid().ToString();  // 중복되지 않는 고유 ID 생성

            var notification = new iOSNotification()
            {
                Identifier = notificationId,
                Title = title,
                Body = message,
                ShowInForeground = true,
                ForegroundPresentationOption = (PresentationOption.Alert | PresentationOption.Sound | PresentationOption.Badge),
                CategoryIdentifier = "custom_category",
                ThreadIdentifier = "custom_thread",
                Trigger = timeTrigger,
            };

            iOSNotificationCenter.ScheduleNotification(notification);

            switch (pushType)
            {
                case ClientDef.LocalPushType.TreasureTrove:
                    PlayerPrefs.SetString(ClientDef.LOCALKEY_Push_TreasureTrove, notificationId);
                    break;

                default:
                    break;
            }

#endif
        }
        catch (Exception e)
        {
            Debug.LogWarning("푸시알람 예약 중 오류 발생: " + e.ToString());
        }
    }

    public void CancelPushNotification(LocalPushType pushType)
    {
#if UNITY_ANDROID

        int pushCode = 0;

        switch (pushType)
        {
            case LocalPushType.FreeGold:
                pushCode = PlayerPrefs.GetInt(ClientDef.LOCALKEY_Push_FreeGold, 0);
                break;

            default:
                break;
        }

        if (pushCode == 0)
            return;

        AndroidNotificationCenter.CancelScheduledNotification(pushCode);

#elif UNITY_IOS

        string pushCode = string.Empty;

        switch (pushType)
        {
            case ClientDef.LocalPushType.TreasureTrove:
                pushCode = PlayerPrefs.GetString(ClientDef.LOCALKEY_Push_TreasureTrove, string.Empty);
                break;

            default:
                break;
        }

        if (pushCode == string.Empty)
            return;

        iOSNotificationCenter.RemoveScheduledNotification(pushCode);

#endif

        Debug.LogWarning("Complete Cancel to Push Notification.");
    }

    // 모든 예약된 알림을 취소합니다.
    public void CancelAllPushNotifications()
    {
#if UNITY_ANDROID
        AndroidNotificationCenter.CancelAllNotifications();
#elif UNITY_IOS
        iOSNotificationCenter.RemoveAllScheduledNotifications();
#endif

        Debug.LogWarning("Cancel All Push Notifications.");
    }
    #endregion

    #region Private Method
    // 앱 시작 시 권한 요청 및 채널 등록, 그리고 예약 알림 호출
    private void Init()
    {
#if UNITY_ANDROID
        // 디바이스의 안드로이드 api level 얻기
        if (!GameManager.Instance.IsEditor)
        {
            int apiLevel = GetAndroidAPILevel();
            Debug.LogWarning("ApiLevel: " + apiLevel);

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

        RegisterAndroidChannel();
#elif UNITY_IOS

#endif
    }

#if UNITY_ANDROID
    // Android 알림 채널 등록 (앱 실행 시 한 번만 호출)
    private void RegisterAndroidChannel()
    {
        var channel = new AndroidNotificationChannel()
        {
            Id = "my_channel_id",
            Name = "Real Fighter",
            Importance = Importance.High,
            Description = "Generic notifications",
        };
        AndroidNotificationCenter.RegisterNotificationChannel(channel);

        Debug.LogWarning("Register Android Channel");
    }

    private int GetAndroidAPILevel()
    {
        using (var versionClass = new AndroidJavaClass("android.os.Build$VERSION"))
        {
            return versionClass.GetStatic<int>("SDK_INT");
        }
    }
#endif

#if UNITY_IOS
    public IEnumerator RequestNotificationPermission(Action onGranted, Action onDenied)
    {
        // 어떤 권한들을 요청할지 지정 (Alert, Badge, Sound 등)
        var options = AuthorizationOption.Alert | AuthorizationOption.Badge | AuthorizationOption.Sound;

        // registerForRemoteNotifications = false 이면 원격 푸시 토큰 요청 안 함
        bool registerForRemote = false;

        using (var req = new AuthorizationRequest(options, registerForRemote))
        {
            // 요청 완료될 때까지 대기
            while (!req.IsFinished)
            {
                yield return null;
            }

            Debug.LogWarning("IOS Notification Permission Request finished");
            Debug.LogWarning("Granted: " + req.Granted);
            Debug.LogWarning("Error: " + req.Error);
            Debug.LogWarning("Device Token: " + req.DeviceToken);
        }
    }
#endif
    #endregion
}

 

* IOS 코드 부분은 더욱 보안이 필요하다.

 

 

5. AndroidManifest 설정

 

AndroidManifest.xml 이 없다면 해당 경로에서 체크해서 만들어 준다.

 

 

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

 

해당 Permission을 추가한다.

 

 

* 예시

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

	<!-- 알림 권한 추가 (Android 13 이상용) -->
	<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

	<application>
        <!--Used when Application Entry is set to Activity, otherwise remove this activity block-->
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:theme="@style/UnityThemeSelector">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
</manifest>

 

 

6. 사용 예시

public enum PopupType
{
    None,

    OkOnly,
    OkCancel,

    Max
}
// 기존에 예약된 로컬 푸시 제거
LocalPushManager.Instance.CancelPushNotification(LocalPushType.FreeGold);

// 푸시를 보낼 시간 계산
DateTime RewardTime = Util.DateTimeNow.Date.AddDays(1);
//RewardTime = RewardTime.AddSeconds(30);  // Test 용

LocalPushManager.Instance.SchedulePushNotification(
    LocalPushType.FreeGold,
    "Free Gold!",
    "무료 Gold를 받을 수 있습니다! 받으러 오세요~",
    RewardTime);

Debug.LogWarning($"Local Push Sucess! Send Push on {RewardTime}");

 

우선, 이미 예약된 로컬 푸시가 있다면 제거 해준다. 그리고 푸시를 보낼 시간을 현재 시간으로 부터 계산을 해서 새롭게 푸시를 예약해 준다.

 

 

※ 만약, 지정된 시간에 푸시가 오지 않는다면?!

현재 나의 테스트 안드로이드 휴대폰의 API는 30이다. 테스트로 몇분 뒤에 알림이 오도록 하였지만, 거의 하루가 지난 뒤에 푸시가 오는 현상이 있었다. 여러 서치 결과, 오래된 휴대폰 (낮은 API)는 정확한 시간에 알림이 오도록 하는 것이 불가능 했다. 이 부분은 추후, 다른 휴대폰으로 테스트가 필요하다. 

그리고 이전에 파이어베이스와 함께 사용하였을 때, 동일한 설정으로 정확한 시간에 알림이 오도록 하는 것이 가능했다. 이 부분도 추후에 파이어베이스와 함께 사용할 때, 업데이트 하도록 하겠다.

즉, API 버전이 낮다면, 정확한 시간에 알림이 오도록 하는 것이 불가능!

'유니티' 카테고리의 다른 글

튜토리얼 구현  (0) 2025.11.17
구글 플레이 aab 파일 용량 줄이기  (0) 2025.11.11
금칙어 적용  (0) 2025.11.03
SoundManager  (0) 2025.10.19
Cinemachine3 카메라 연출  (0) 2025.08.19