Unity Foreground Service 플러그인 테스트
유니티로 Foreground Service를 구현하기가 이렇게 어려운 줄 몰랐다.
검색해도 제대로 된 자료가 없다.
내가 JAVA를 거의 모르니까 더욱 힘들다. 아주 간단한 플러그인밖에 못 만들어봤으니.. 다른 언어를 접하기에는 이젠 너무 어렵다.
실제로 잘하는 사람은 분명 다 만들었을 텐데... 새로 만드는 사람에겐 잘 안된다..
일단 그래서 주로 사용하는 제미나이와 코파일럿을 비교하면서 진행해 봤다.
사실 진행했어도 알려주긴 하지만 대부분 동작하지 않았다. 그리고 좀 더 전문적인 코드 안내는 코파일럿보다 제미나이가 더 상세하게 안내하였다. 물론 단편적인 것일 수도 있기 때문에 개인적인 생각이 그렇다는 것이다.
소개된 코드는 질문을 계속 달리해 가면서 했다.
정말 여러번 보고 했던 거 또 하고, 또 하고, 또 하고를 계속하니 설정은 외우다시피 했다.
그러다 결국 오류 없이 작동시키기는 했다.
다만 테스트 버전에서는 되는데, 원래 적용하고자 하는 앱에는 안될 것 같다.
테스트 버전의 카운터가 포그라운드 서비스에서 동작하지 않는다. 그냥 서비스만 실행되는 것 같다.
그것이 아니면 내가 이해를 잘 못하는 것일 것이다. 아마도 강력한 후자일 것이다.
그러면 일단 테스트용으로 성공했던 것이라 하나하나 단계별로 확인해 본다.
이렇게 정리해 놔야 나중에 혹시나 업그레이드나 개선해서 실제 앱에 적용할 수 있을지도 모르기 때문이다.
우선 나의 개발 버전은 다음과 같다. (개발 버전 특성을 너무 따진다...)
- Unity 2018.4.3f
- Visual Studio 2017
- Android Sutdio Bumblebee 2021.1.1
다시금 느끼는 것이지만 구시대 유물 같은 버전을 쓰고 있다.
자 그러면 일단 제미나이를 통해 얻은 내용은 아래와 같지만 추가적인 검색 및 보완을 통해 어느 정도 수정한 코드 내용이다.
질문의 경우 뭐 대략적으로 뭐로 했을지는 감이 올 것이다.
Unity 앱에서 Foreground Service를 구현하는 방법은 다음과 같습니다.
1. Android Studio에서 Android 라이브러리 프로젝트 생성: Unity에서 사용할 Android 플러그인을 만듭니다.
2. Foreground Service 구현 (Java/Kotlin): Android 라이브러리 프로젝트에서 Foreground Service를 생성하고 필요한 로직을 구현합니다.
3. Unity에서 Android 플러그인 호출: Unity C# 코드에서 Android 플러그인의 Foreground Service를 시작하고 중지하는 메서드를 호출합니다.
4. 권한 설정: AndroidManifest.xml에 필요한 권한을 추가합니다.
아래에 각 단계별 예시 코드를 제공합니다.
1단계: Android Studio에서 Android 라이브러리 프로젝트 생성
1. Android Studio를 열고 "New Project"를 선택합니다.
2. "Phone and Tablet" 탭에서 "No Activity"를 선택하고 "Next"를 클릭합니다.
3. "Configure Your Project"에서 다음을 설정합니다:
- Name: UnityForegroundServicePlugin (원하는 이름으로 지정)
- Package name: com.yourcompany.unityforegroundservice (고유한 패키지 이름으로 변경)
- Language: Java
- Minimum SDK level: 원하는 최소 SDK 레벨 선택 (예: API 21 또는 23)
4. "Finish"를 클릭하여 프로젝트를 생성합니다.
5. 프로젝트가 생성되면, app 모듈을 마우스 오른쪽 버튼으로 클릭하고 "Open Module Settings" (F4)를 선택합니다.
6. "Modules" 탭에서 app 모듈을 선택하고 "Properties" 탭으로 이동합니다.
7. "Library"를 선택하여 이 모듈을 Android 라이브러리로 만듭니다. "Apply" 및 "OK"를 클릭합니다.
2단계: Foreground Service 구현 (Android Java 코드)
app/src/main/java/com/yourcompany/unityforegroundservice 경로에 다음 파일을 생성하고 코드를 작성합니다.
MyForegroundService.java
package com.yourcompany.unityforegroundservice;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.unity3d.player.UnityPlayerActivity;
import org.jetbrains.annotations.Nullable;
public class MyForegroundService extends Service
{
private static final String CHANNEL_ID = "MyForegroundServiceChannel";
private static final String TAG = "UnityForegroundService";
private static final int NOTIFICATION_ID = 123;
@Override
public void onCreate()
{
super.onCreate();
Log.d(TAG, "MyForegroundService onCreate------------------------------------");
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
// 알림을 클릭했을 때 Unity 앱의 메인 액티비티를 다시 열기 위한 Intent
Intent notificationIntent = new Intent(this, UnityPlayerActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
Log.d(TAG, "MyForegroundService onStartCommand-------------------------------");
// 이 부분에 백그라운드에서 계속 실행될 로직을 추가합니다.
// 예를 들어, 오디오 재생, 위치 업데이트 등
// 현재는 단순히 로그만 출력합니다.
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Unity App Running in Background")
.setContentText("Your Unity application is actively running.(작동중~)")
.setSmallIcon(android.R.drawable.ic_lock_idle_charging) // 적절한 아이콘으로 변경
.setContentIntent(pendingIntent)
// .setPriority(NotificationCompat.PRIORITY_HIGH)
.build();
startForeground(NOTIFICATION_ID, notification);
// START_STICKY: 서비스가 시스템에 의해 종료되면 재시작됩니다. (null Intent로)
// START_NOT_STICKY: 서비스가 시스템에 의해 종료되면 재시작되지 않습니다.
// START_REDELIVER_INTENT: 서비스가 시스템에 의해 종료되면 마지막 Intent와 함께 재시작됩니다.
return START_STICKY;
}
@Override
public void onDestroy()
{
super.onDestroy();
Log.d(TAG, "MyForegroundService onDestroy");
// 서비스가 종료될 때 필요한 정리 작업을 수행합니다.
}
@Nullable
@Override
public IBinder onBind(Intent intent)
{
return null;
}
private void createNotificationChannel()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
NotificationChannel serviceChannel = new NotificationChannel(
CHANNEL_ID,
"Unity Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
);
//serviceChannel.setDescription("This channel is used by Unity Foreground Service");
//serviceChannel.enableLights(true);
//serviceChannel.setLightColor(Color.RED);
//serviceChannel.enableVibration(false);
//serviceChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null)
{
manager.createNotificationChannel(serviceChannel);
}
Log.d(TAG, "MyForegroundService Create Notification------------------------------------");
}
}
}
UnityPlugin.java (Unity에서 호출할 인터페이스)
package com.yourcompany.unityforegroundservice;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.os.Build;
public class UnityPlugin {
private static final String TAG = "UnityForegroundService";
private static Context mContext;
public static void setContext(Context context)
{
mContext = context;
Log.d(TAG, "Context set in UnityPlugin--------------------------------");
}
public static void startMyForegroundService()
{
if (mContext == null)
{
Log.e(TAG, "Context is null. Cannot start service.-----------------");
return;
}
Log.d(TAG, "Attempting to start MyForegroundService---------------------");
Intent serviceIntent = new Intent(mContext, MyForegroundService.class);
// Android 8.0 (API 레벨 26) 이상에서는 startService() 대신 startForegroundService()를 사용해야 합니다.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mContext.startForegroundService(serviceIntent);
} else {
mContext.startService(serviceIntent);
}
Log.d(TAG, "MyForegroundService started (or attempted to start).");
}
public static void stopMyForegroundService()
{
if (mContext == null)
{
Log.e(TAG, "Context is null. Cannot stop service.");
return;
}
Log.d(TAG, "Attempting to stop MyForegroundService");
Intent serviceIntent = new Intent(mContext, MyForegroundService.class);
mContext.stopService(serviceIntent);
Log.d(TAG, "MyForegroundService stopped (or attempted to stop).");
}
}
AndroidManifest.xml (Android 라이브러리 프로젝트)
app/src/main 경로에 있는 AndroidManifest.xml 파일을 열고 <application> 태그 내부에 다음을 추가합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application>
<service
android:name="com.yourcompany.unityforegroundservice.MyForegroundService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
주의: FOREGROUND_SERVICE_CONTINUOUS 권한은 Android 14 (API 34) 이상에서 특정 유형의 foreground service에 대해 필요할 수 있습니다. 일반적인 게임의 경우 FOREGROUND_SERVICE 만으로 충분할 가능성이 높지만, 지속적인 오디오 재생이나 위치 업데이트와 같이 더욱 엄격한 백그라운드 작업이 필요한 경우 고려할 수 있습니다.
Gradle Build:
Android Studio에서 Build -> Make Project (또는 Build -> Rebuild Project)를 클릭하여 .aar 파일을 생성합니다. .aar 파일은 app/build/outputs/aar/app-debug.aar (또는 app-release.aar) 경로에 생성됩니다.
3단계: Unity에서 Android 플러그인 호출(Unity C# 코드)
1. .aar 파일 Unity 프로젝트로 가져오기: 생성된 app-debug.aar (또는 app-release.aar) 파일을 Unity 프로젝트의 Assets/Plugins/Android 폴더로 복사합니다. 이 폴더가 없다면 새로 생성합니다.
2. Unity C# 스크립트 작성: Unity 프로젝트에서 새로운 C# 스크립트 (예: ForegroundServiceManager.cs)를 생성하고 다음 코드를 작성합니다.
using UnityEngine;
using System.Collections;
public class ForegroundServiceManager : MonoBehaviour
{
private const string PLUGIN_PACKAGE_NAME = "com.yourcompany.unityforegroundservice.UnityPlugin";
private const string SERVICE_PACKAGE_NAME = "com.yourcompany.unityforegroundservice.MyForegroundService";
void Start()
{
// 앱이 시작될 때 Android Context를 플러그인에 전달합니다.
// AndroidJavaClass를 사용하여 Android UnityPlayer 클래스를 가져옵니다.
using (AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
// 현재 액티비티를 가져옵니다.
AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
// UnityPlugin의 setContext 메소드를 호출하여 컨텍스트를 전달합니다.
using (AndroidJavaClass pluginClass = new AndroidJavaClass(PLUGIN_PACKAGE_NAME))
{
pluginClass.CallStatic("setContext", currentActivity);
Debug.Log("Unity: Android Context set for plugin.");
}
}
}
public void StartForegroundService()
{
if (Application.platform == RuntimePlatform.Android)
{
using (AndroidJavaClass pluginClass = new AndroidJavaClass(PLUGIN_PACKAGE_NAME))
{
pluginClass.CallStatic("startMyForegroundService");
Debug.Log("Unity: Attempted to start foreground service.");
}
}
else
{
Debug.LogWarning("Unity: Foreground service only works on Android platform.");
}
}
public void StopForegroundService()
{
if (Application.platform == RuntimePlatform.Android)
{
using (AndroidJavaClass pluginClass = new AndroidJavaClass(PLUGIN_PACKAGE_NAME))
{
pluginClass.CallStatic("stopMyForegroundService");
Debug.Log("Unity: Attempted to stop foreground service.");
}
}
else
{
Debug.LogWarning("Unity: Foreground service only works on Android platform.");
}
}
// 앱이 일시 정지되거나 종료될 때 Foreground Service를 관리할 수 있습니다.
void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
// 앱이 백그라운드로 전환될 때 Foreground Service를 시작할 수 있습니다.
// StartForegroundService(); // 필요에 따라 주석 해제
Debug.Log("Unity: Application Paused. Consider starting Foreground Service.");
}
else
{
// 앱이 포그라운드로 돌아올 때 Foreground Service를 중지할 수 있습니다.
// StopForegroundService(); // 필요에 따라 주석 해제
Debug.Log("Unity: Application Resumed. Consider stopping Foreground Service.");
}
}
void OnApplicationQuit()
{
// 앱이 완전히 종료될 때 Foreground Service를 중지해야 합니다.
StopForegroundService();
Debug.Log("Unity: Application Quit. Foreground Service stopped.");
}
}
원래 4단계가 있기는 한데 AndroidManifest.xml을 설정하는 것이다. 위의 내용대로 해서 이 매니패스트까지 하면 오류가 나서 테스트용에서는 없어도 문제가 없다.
이후 Unity에 대한 몇 가지 설정이 더 있지만 기본적인 사항이라 생략한다.
아울러 중요한 사항은 처음 언급한 것처럼 제미나이를 통해 구현된 것을 이용하긴 했지만 저대로 하면 당연히 여러 가지 오류를 뿜어낸다. 어디까지나 제미나이는 내가 JAVA를 모르니까 초기에 코드를 잡아주는 역할을 했고 이후에는 보완할 사항들을 추가적인 제미나이의 답변이나 폭풍 검색으로 정정하며 정리하였다.
따라서 추가적으로 안드로이드 스튜디오에서 다음과 같은 설정을 추가로 해줘야 한다.
사용자가 만든 앱 패키지의 build.gradle을 다음과 같이 수정해 준다.
plugins {
id 'com.android.library'
}
android {
compileSdk 31
defaultConfig {
minSdk 24
targetSdk 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compileOnly files('libs/classes.jar')
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
//implementation 'com.google.android.material:material:1.12.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
여기서 살펴볼 것은 compileSdk / targetSdk의 최소 버전은 31부터로 설정한다.
JAVA에서 사용된 라이브러리가 최소 31부터 지원하기 때문이라고 한다.
다음으로 dependencies의 appcompat 버전과 material의 버전이다. 일단 내 개발 도구의 버전이 낮아서 위와 같이 해야만 오류가 나지 않는다.
gradle을 수정했다면 Sync를 한번 해주시고 Sync Project whith Gradle Files 한번 해주시고 우측의 Gradle 탭에서 만든 라이브러리명에서 Tasks > build > assemble를 하면 project의 build 폴더에서 aar의 정식 플러그인이 만들어진다.
이 플러그인을 유니티의 플러그인 폴더에 넣는다.
여기서 중요한 것이 있는데, 이대로 진행하면,
Failed resolution of: Landroidx/core/app/NotificationCompat$Builder;
라는 오류를 만나게 된다. 물론 이전에 다양한 오류들을 만날 수 있다.
이를 해결하기 위해서는 추가 플러그인이 필요하다.
Google's Maven Repository
Welcome to Google's Maven Repository Select an artifact in the left hand pane to view details Artifacts ({{selectedNode.subnode.length}}) Versions ({{selectedNode.subnode.length}}) folder_open {{child.text}} {{item.name}} {{item.value}} {{item.value.text}}
maven.google.com
이 사이트에서 androidx.core을 검색하고 aar 플러그인으로 다운로드하여서 유니티 플러그인 폴더에 넣어줘야 한다.
일단 성공한 버전은 core-1.2.0.aar 이다.
어느 정도 상위버전을 넣었더니 오류로 빌드가 실패한다.
정상적으로 빌드해서 설치하고 Start를 시작해 보면 띠리링 소리와 함께 동작하는 것을 볼 수 있다.
이제 필요한 동작을 안드로이드에서 추가해 주면 된다고 한다. 근데 나는 전체 동작이 필요했던 것인데 이건 안 되는 것 같다.
일부 로직으로만 적용해야 될 것 같다. OTL...
일단 되니까 테스트용에서 유니티 쪽으로 코드를 추가해서 실질적으로 전달돼서 작동하는지 확인해 봐야겠다.
아래는 작동 영상이다. Unity의 UI는 일부 변경되었습니다.
'개발 관련 > SW, App 관련' 카테고리의 다른 글
Unity 로우폴리곤 행성제작 변경 테스트 (3) | 2025.07.09 |
---|---|
Unity Asset Store 할인정보(7월3일 한정) (0) | 2025.07.03 |
Unity에서 Stream 사용시 Android 권한 문제 (0) | 2025.06.18 |
AssetStore 할인 정보 (1) | 2025.06.12 |
Unity 반올림, 내림, 소수점 간단 처리 관련 (0) | 2025.05.08 |
댓글