반응형

출처: https://ugames.tistory.com/entry/%EA%B5%AC%EA%B8%80%ED%94%8C%EB%A0%88%EC%9D%B4-%EB%82%B4%EB%B6%80%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%B0%ED%8F%AC-%EB%8B%A8%EA%B3%84%EB%B3%84-%EC%A0%95%EB%A6%AC

 


(1단계)

배포용 키 생성과 SHA1

 

유니티로 앱을 개발하고 구글 플레이 스토어에 개시하기 위해서는 앱에 서명을 해야 합니다.

유니티는 Project Settings의 Player 탭에서 설정할 수 있습니다. 메뉴의 Edite > Project Settings... 를 선택하면 다음과 같은 창이 열립니다.

Project Settings의 Player

 

우선 상단에 있는 Company Name을 설정해 줍니다. com.[CompanyName].[ProductName] 형식의 패키지 이름에 사용됩니다. 여기서 만약 Company Name 을 melon 으로 설정한다면 패키지 이름은 com.melon.Slime 이 됩니다.

 

배포용 서명은 "Publishing Settings" 에서 진행합니다. "Keystore Manager..." 버튼을 클릭합니다.

 

KeyStore Manager

 

위의 창에서 [Keystore...] 를 클릭하고 "Create New" 를 클릭하면 위의 화면과 같은 상태가 됩니다.

"Anywhere..." 와 "In Dedicated Location..." 모두 클릭하면 파일 브라우저 창이 열리는데 차이는 다음과 같습니다.

 

  • Anywhere :  프로젝트 폴더로 파일 탐색창이 열립니다.
  • In Dedicated Location :  현재 윈도우에 로그인한 계정의 사용자 폴더로 탐색창이 열립니다.

어디에 저장하든 상관없습니다.

 

새로운 Keystore 를 생성하면 Keystore Manager가 다음과 같이 변경됩니다.

 

Keystore Manager

 

키를 생성하기 위해 다음과 같이 진행합니다.

  1. 상단의 "Password", "Confirm password" 에서 Keystore 에 사용할 암호를 입력합니다.
  2. [New Key Values] 하단에 있는 Alias 에 Key의 이름을 입력하고 Password, Confirm Password 에서 Key 에 사용할 암호를 입력합니다. 
  3. 화면 하단의 [Add Key] 가 활성화되면 클릭합니다.

* 한 개의 Keystore 에 여러 개의 key를 등록할 수 있습니다.

 

 

다시 Project Settings 창의 Publishing Settings 에서 다음의 작업을 이어갑니다.

 

Project Settings 창

 

  1. Custom Keystore 가 체크되어 있지 않으면 체크합니다.
  2. 방금 생성한 keystore의 암호를 입력합니다. 암호를 올바르게 입력하면 [Project Key] 하단의 Alias 가 활성화됩니다. 
  3. Alias 에서 생성한 key를 선택합니다.
  4. key의 암호를 입력합니다.

이제 앱 서명의 준비가 완료되었습니다.

 

Google Play Games Service와 연동하기 위해선 key의 SHA1 지문이 필요하니 얻어보도록 하겠습니다.

 

1. keytool 이 필요합니다. keytool 은 jre 가 설치된 폴더에 있는 bin 폴더에 있습니다.

2. bin 폴더에서 shift + 우클릭을 하고 "여기에 PowerShell 창 열기" 를 클릭합니다.

 

 

PowerShell 창이 열리면 다음과 같이 입력합니다.

 

PowerShell - keytool

* [D:\Sources\Slime\user.keystore] 대신 각자 keystore 가 저장된 경로를 정확히 입력해 주셔야 합니다. 

 

우리가 필요한 것은 SHA1 입니다. SHA1의 값을 메모장 등에 복사해 두시면 됩니다.

 


(2단계)

 

이번엔 구글 플레이 게임즈 서비스를 연동하기 위한 설정에 대해 알아보겠습니다.

 

이번 내용은 구글 개발자 콘솔 Google Cloud Platform을 오가며 설정을 해야 하기 때문에 다소 복잡해 보일 수 있습니다. 전체적인 흐름은 다음과 같습니다.

 

  1. 구글 개발자 콘솔에서 앱 만들기
  2. Play 게임즈 서비스 설정
  3. Google Cloud Platform에서 OAuth 동의 화면 구성
  4. 구글 개발자 콘솔에서 사용자 인증 정보 추가로 이동
  5. Google Cloud Platform에서 OAuth 클라이언트 ID 생성
  6. 구글 개발자 콘솔에서 OAuth 클라이언트 ID를 이용해 사용자 인증 정보 추가 완료

 

1. 구글 개발자 콘솔에서 앱 만들기

 

구글 개발자 콘솔에 접속해서 새로운 앱을 만듭니다. 모든 앱에서 [앱 만들기] 를 클릭하고 다음과 같이 내용을 채워줍니다.

 

앱 이름은 게임의 이름입니다. 여기서는 예제로 Slime 으로 진행할 것입니다. 선언에 있는 개발자 프로그램 정책과 미국 수출법을 위와 같이 모두 체크해준 다음 화면 하단의 [앱 만들기] 버튼을 클릭합니다.

 

 

2. Play 게임즈 서비스 설정

 

좌측 메뉴에 보면 [Play 게임즈 서비스] 항목이 있습니다. 여기서 설정을 클릭하면 다음과 같은 페이지가 나타납니다. 아직 Google API 를 사용하지 않기 때문에 "아니요. 게임에서 Google API를 사용하지 않습니다." 를 선택한 다음 [만들기]를 클릭합니다.

Play 게임즈 서비스 설정

 

[만들기]를 클릭하면 나타나는 화면에서 아래와 같은 사용자 인증 정보 항목을 봅니다. 

 

Google Cloud Console 에서 OAuth 동의 화면을 구성해야 한다는 안내가 보입니다. [설정]을 클릭합니다.

 

OAuth 동의 화면 구성에 대한 안내가 나타납니다. 읽어 보시고 "Google Cloud Platform"을 클릭합니다.

 

 

3. Google Cloud Platform에서 OAuth 동의 화면 구성

 

Google Cloud Platform 으로 이동하면 다음과 같은 화면이 나타납니다.

 

Google Cloud

 

외부를 선택하고 [만들기]를 클릭합니다. 아래와 같은 페이지가 나타나면 * 표시된 항목을 모두 채워줍니다.

 

앱 등록 수정

 

 

정보를 입력했으면 [저장 후 계속]을 클릭합니다. 이 후에는 각 단계마다 별도의 입력 없이 [저장 후 계속]을 클릭하면 됩니다. 그러면 다음과 같은 화면이 나타납니다.

 

 

현재 게시 상태는 "테스트" 입니다. 테스트 밑에 [앱 게시] 를 클릭하고 게시 상태로 변경해 줍니다. 테스트 사용자가 필요하다면 [+ADD USERS] 를 클릭해서 추가해 주시면 됩니다. 이제 구글 개발자 콘솔로 이동합니다.

 

 

4. 구글 개발자 콘솔에서 사용자 인증 정보 추가로 이동

 

Play 게임즈 서비스 설정 페이지에서 사용자 인증 정보 항목에서 [사용자 인증 정보 추가전 OAuth 동의 화면 구성] 아내 문구에 있는 [새로 고침]을 클릭하면 다음과 같이 변경되면서 [사용자 인증 정보 추가]가 활성화됩니다.

 

 

[사용자 인증 정보 추가]를 클릭하면 다음과 같은 내용이 나오는 페이지로 이동됩니다. 유형에 Andoird 와 게임 서버가 있는데 이 Android 를 선택합니다.

 

 

[OAuth 클라이언트 만들기] 를 클릭하면 다음과 같이 생성하는 방법에 대한 안내 문구가 화면에 나타납니다. 

 

 

문구에서 [OAuth 클라이언트 ID 만들기] 를 클릭하면 Google Cloud Platform의 OAuth 클라이언트 ID 만들기 페이지로 이동됩니다.

 

 

5. Google Cloud Platform에서 OAuth 클라이언트 ID 생성

 

사용자 인증 정보

 

사용자 인증 정보를 만들기위한 페이지입니다. [+사용자 인증 정보 만들기] 를 클릭하고 [OAuth 클라이언트 ID] 를 선택하면 다음과 같은 OAuth 클라이언트 ID 만들기 페이지로 이동됩니다.

 

 

애플리케이션 유형을 Android 로 선택하고 정보를 입력해 줍니다.

 

  • 이름: OAuth 2.0 클라이언트의 이름입니다. 구글 개발자 콘솔에 등록한 게임 이름과 동일한 이름을 입력합니다.
  • 패키지 이름: 유니티에서 얻을 수 있습니다. com.[Company Name].[Product Name] 의 형식입니다.
  • SHA-1 인증서 디지털 지문: 배포용 키 생성과 SHA1 얻기 에서 생성한 지문을 넣으면 됩니다.

모두 올바르게 입력한 다음 [만들기] 를 클릭합니다.

 

 

6. 구글 개발자 콘솔에서 OAuth 클라이언트 ID를 이용해 사용자 인증 정보 추가 완료

 

구글 개발자 콘솔로 돌아옵니다. 안내 문구가 떠 있다면 [완료]를 클릭하시면 자동으로 새로 고침 되면서 OAuth 클라이언트 항목을 선택할 수 있게 됩니다. 안내문구가 떠 있지 않다면 F5를 눌러 새로 고침 해주세요.

 

 

위와 같이 OAuth 클라이언트를 선택한 다음 [변경사항 저장] 을 클릭합니다. 

 

 


3단계

 

이글에서는 다음과 같은 버전으로 테스트 설치를 진행하였습니다.

 

  • 유니티: 2021.3.11f1 LTS
  • Google Play Games Plugin for Unity: v10.14

 

Google Play Games Plugin for Unity 는 아래의 링크에서 받을 수 있습니다.

 

https://github.com/playgameservices/play-games-plugin-for-unity

 

GitHub - playgameservices/play-games-plugin-for-unity: Google Play Games plugin for Unity

Google Play Games plugin for Unity. Contribute to playgameservices/play-games-plugin-for-unity development by creating an account on GitHub.

github.com

 

 

위의 사이트로 이동하면 다음과 같은 페이지가 나타납니다. 여기서 Release 를 클릭하시면 됩니다.

 

 

Release 를 클릭하면 아래와 같은 화면이 나타나는 페이지로 이동됩니다.

 

 

여기서 v10.14의 빨간색 테두리 부분을 클릭하면 Source code를 다운로드 받을 수 있습니다. 파일은 현재 로그인한 윈도우 계정의 다운로드 폴더에 있습니다.

 

탐색기 다운로드 폴더에서 다운로드 받은 파일 확인

 

압축 해제 후 current-build 폴더

압축을 풀고 "current-build" 폴더에 있는 "GooglePlayGamesPlugin-0.10.14.unitypackage" 파일을 유니티 에디터의 Project 창으로 끌어 다 놓으면 (Drag & Drop) 됩니다.

 

 

위와 같은 "Import Unity Package" 창이 나타나면 [Import] 버튼을 클릭합니다.

 

설치가 완료될 때까지 잠시 대기합니다. 만약 Android Resolver 가 실행되지 않았다면 메뉴에서 [File > BuildSettings...] 를 선택합니다. 

 

 

Platform 에서 Android를 선택하고 [Switch Platform] 을 클릭합니다. 아래와 같은 창이 뜨면 [Enable] 을 클릭합니다.

 

 

 

작업이 완료되면 창이 자동으로 닫힙니다. Android Resolver 는 Android 와 관련된 여러 라이브러리를 사용할 때 발생하는 다양한 dependency 를 처리해 줍니다.

 

아래의 그림은 Android Resolver를 직접 실행시킬 수 있는 경로입니다.

 

 

현재 상태에서 위의 경로를 따라 직접 실행했을 때 다음과 같은 창이 나타나야 합니다.

 

 

 

이제 Google Play Games Service와 연동하기 위한 설정을 진행하도록 하겠습니다.

 

유니티 에디터의 메뉴에서 

[Window > Google Play Games > Setup > Android Setup...] 을 클릭하면 다음과 같은 창이 나타납니다.

 

 

빨간색 테두리와 같이 입력 칸이 두 개가 있는데 아래쪽은 아무것도 입력하지 않아도 됩니다.

 

다시 구글 플레이 콘솔 페이지로 이동합니다.

앱을 선택하고 좌측 메뉴에서 [Play 게임즈 서비스 > 설정]으로 이동합니다.

 

 

설정 페이지에서 [리소스 보기] 를 클릭하면 아래와 같은 창이 나옵니다.

 

여기서 빨간 테두리의 버튼을 클릭하면 내용이 클립보드에 복사됩니다. 이제 다시 유니티로 이동하여 다음과 같이 붙여 넣기 하시면 됩니다.

 

 

 

[Setup] 버튼을 클릭하면 자동으로 작업이 진행됩니다.

 

유니티 프로젝트에서 Google Play Games Service를 사용할 수 있는 기본 설정이 완료되었습니다.

 


(4단계)

Google 플레이 게임 서비스에 로그인을 위한 테스트 코드를 작성해 보도록 하겠습니다.

 

1. 로그인 UI 생성

 

로그인 테스트를 위해 버튼과 결과를 출력할 Text UI 를 만들어 보도록 하겠습니다.

다음과 같이 버튼 UI 를 추가합니다.

 

Button 추가

 

Hierarchy 창에 Canvas 가 생기고 그 아래에 Button 이 있습니다. 버튼의 Rect Transform 을 아래와 같이 설정해 줍니다.

유니티에서 모든 UI 오브젝트는 Canvas 컴포넌트를 갖는 오브젝트의 자식 오브젝트여야 합니다.

* 유니티에서 3D 월드 좌표계를 사용하는 오브젝트는 Transform 속성을 사용하고 UI는 기본적으로 스크린 좌표계를 사용하며 Rect Transform 속성을 사용합니다.

 

 

아래의 그림과 같이 Button UI 밑에 있는 Text(TMP) 오브젝트를 클릭하고 Inspector 창에서 붉은 테두리와 같이 Text와 Font Size를 변경해 줍니다.

 

 

 

다음은 로그인 결과를 출력할 Text - TextMeshPro UI 를 추가해 주세요. 이 UI 는 게임화면의 원하는 위치에 글자를 출력할 수 있게 해줍니다. 위의 버튼을 만들 때 사용했던 Text(TMP) 와 같은 것입니다.

 

TextMeshPro 추가

 

추가한 Text UI 의 Rect Transform을 아래와 같이 설정해 줍니다. 또, Text 의 Font Size 와 Alignment 를 붉은 테두리와 같이 변경해 줍니다.

 

 

 

위의 내용대로 설정하면 화면이 아래와 같이 됩니다.

 

 

2. 로그인 스크립트 작성 및 UI 연결

 

이제 로그인을 위한 새로운 스크립트를 만들도록 하겠습니다. 새 스크립트를 만드는 방법은 이전 글에서 진행했으니 여기서는 단계별 서술만 하도록 하겠습니다.

1. 빈 오브젝트를 만들고 이름을 GPGSHelper 로 변경합니다.
2. 생성한 오브젝트를 선택하고 Inspector 창에서 [Add Component] 를 클릭합니다.
3. New Script 를 클릭하고 이름을 GPGSHelper 로 설정한후 [Create and Add] 를 클릭합니다.

 

스크립트 코드를 다음과 같이 작성해 줍니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using TMPro;

public class GPGSHelper : MonoBehaviour
{
   public TextMeshProUGUI txtLoginResult;

   // Start is called before the first frame update
   void Start()
   {
      var config = new PlayGamesClientConfiguration.Builder().EnableSavedGames().Build();
      PlayGamesPlatform.InitializeInstance(config);
      PlayGamesPlatform.DebugLogEnabled = true;
      PlayGamesPlatform.Activate();
   }

   public void Login()
   {
      PlayGamesPlatform.Instance.Authenticate(SignInInteractivity.CanPromptAlways, (success) =>
      {         
         if (success == SignInStatus.Success)
            txtLoginResult.text = "Success";
         else
            txtLoginResult.text = "Failed";
      });
   }
}

 

using GooglePlayeGames 와 using GooglePlayGames.BasicApi 는 Google Play Game Service와 관련된 API를 사용하기 위해 추가해 주어야 합니다. using TMPro 는 UI 컴포넌트인 TextMeshProUGUI 를 사용하기 위해 추가된 것입니다.

 

위의 코드에서 실제 로그인을 요청하는 코드는 

PlayGamesPlatform.Instance.Authenticate(SignInInteractivity.CanPromptAlways, (success) =>
{         
    if (success == SignInStatus.Success)
    	txtLoginResult.text = "Success";
    else
    	txtLoginResult.text = "Failed";
});

 

이 코드입니다.

 

결과는 비동기 방식으로 전달됩니다. 성공할 경우 success 에 SiginInStatus.Sucess 가 넘어옵니다. 그렇지 않으면 로그인에 실패한 것입니다. 

 

이제 버튼을 클릭했을 때 실행할 Login() 함수를 버튼의 클릭 이벤트에 연결하는 작업과 결과를 출력할 Text UI 를 txtLoginResult에 연결하는 작업을 진행하도록 하겠습니다.

 

먼저 버튼 이벤트와 Login() 함수를 다음과 같이 연결합니다.

 

버튼 이벤트에 Login() 함수 연결

 

다음은 txtLoginResult 에 Text UI 를 다음과 같이 연결합니다.

 

 

이제 Play 버튼을 누르고 실행한 다음 Login 버튼을 클릭해 봅니다. 안타깝지만 Failed 가 출력될 것입니다. 구글 플레이 게임 서비스 로그인은 유니티 에디터에서는 사용할 수 없습니다. 실제 안드로이드 폰에 설치해야 로그인 할 수 있습니다.

 

구글 플레이 게임 서비스 로그인을 위한 긴 여정이 이제 마지막 관문만 남았습니다. 마지막은 다음 글에서 이어서 진행하도록 하겠습니다.

 


(5단계)

 

구글 플레이 서비스 로그인 대장정의 마지막 단계를 진행하도록 하겠습니다.

 

1. 유니티에서 aab 패키지 빌드

 

구글은 2018년 8월부터 앱의 새로운 형식인 aab를 도입했습니다. 구글 개발자 콘솔에 신규 등록하는 앱은 aab 형식이어야 합니다. 이제부터 유니티에서 aab 형식의 패키지를 생성해 보도록 하겠습니다.

 

우선 유니티 에디터 메뉴의 [File > Build Settings...] 를 선택하면 나타나는 창에서 다음과 같이 Build App Bundle (Google Play) 항목을 체크해 줍니다.

 

 

체크했으면 좌측 하단에 있는 [Player Settings...] 를 클릭합니다. Company Name 이 DefaultCompany 라면 적절한 이름을 넣어 변경해 주세요. (아무 이름이나 상관 없습니다.)

 

 

밑으로 조금 스크롤하면 다음과 같이 Target API Level을 선택하는 항목이 나타납니다. 이 글을 작성하는 시점에서 신규 앱을 등록하기 위해선 Target API Level 이 31 이상이어야 합니다. 다른 항목들도 다음의 그림과 같이 되도록 변경해 줍니다.

 

조금 더 밑으로 스크롤하면 [Publish Settings] 항목이 보입니다. 접혀 있다면 클릭해서 다음과 같은 화면이 나타나도록 해줍니다.

 

 

Keystore 와 Project Key 의 암호를 입력합니다. 만약 keystore 와 project key를 만들지 않았다면 여기를 참고해서 만들어 주세요.

 

이제 Project Settings 창을 닫고 Build Settings 창의 우측 하단에 있는 [Build] 버튼을 클릭해 줍니다. 창이 나타나면 다음과 같이 패키지 명으로 사용할 이름을 넣어 줍니다.

 

 

[저장] 버튼을 클릭하면 빌드가 시작됩니다.

* 만약 API Level 31 이 설치되어 있지 않다면 SDK 를 Update 할 것인지 물어보는 창이 나타날 것입니다. [Update Android SDK] 를 클릭해 주시면 됩니다.

 

빌드가 성공적으로 완료되면 aab 파일이 생성됩니다.

 

 

2. 구글 개발자 콘솔에서 내부 테스트 출시

 

구글 개발자 콘솔로 이동합니다. 

이전에 만든 앱을 선택한 다음 테스트 항목에 있는 내부 테스트를 클릭하면 다음과 같은 화면이 나타납니다.

 

 

[새 버전 만들기] 를 클릭합니다.

 

위에서 생성한 aab 패키지를 붉은 테두리 영역에 끌어다 놓으면 자동으로 업로드가 시작됩니다. 성공하면 파란색 테두리와 같은 내용이 나타납니다.

 

왼쪽 메뉴를 조금 내리고 [설정 > 앱 무결성] 페이지로 이동합니다. 이동 후 [앱 서명] 탭을 클릭하면 다음과 같이 앱 서명키 인증서를 얻을 수 있습니다.

 

 

SHA-1 인증서 지문을 이용해서 [Google Cloud Platform 에서 사용자 인증 정보 만들기] 에서 진행했던 방식대로 Android 유형의 OAuth 사용자 인증 정보를 추가로 만들어 주어야 합니다. 

 

이제 테스터를 등록해야 합니다. 내부 테스트 용도로 출시하는 현재 버전을 테스트할 수 있는 권한을 부여해 주는 작업입니다. 왼쪽 메뉴에서 [테스트 > 내부 테스트] 로 이동하고 [테스터] 탭을 클릭합니다.

 

 

이메일 목록 만들기를 클릭한 다음 목록 이름과 테스터 이메일을 추가해 줍니다. 본인 이메일과 테스트에 참여할 사람의 이메일을 입력해 주면 됩니다.

 

 

다 입력했으면 "이메일 주소 추가"의 입력란 아래에 설명이 나와 있듯이 Enter 키를 눌러야 합니다. [변경사항 저장] 버튼이 활성화되면 클릭해서 이메일 목록을 만들어 주시면 됩니다. 성공적으로 생성되면 다음과 같은 화면이 나타날 것입니다.

 

 

아직은 [링크 복사] 가 비활성화 되어 있을 것입니다. 이 링크를 복사해서 테스터들에게 전달해 주어야 테스터가 앱을 다운로드 받을 수 있게 됩니다.

 

다시 [출시] 탭을 클릭합니다. 

 

 

위의 빨간 테두리 부분인 [버전 검토 및 출시] 를 클릭합니다.

 

 

하단에 있는 [내부 테스트 트랙으로 출시 시작] 버튼을 클릭해 줍니다. 내부 테스트 트랙은 구글의 별도의 검토 과정 없이 바로 출시가 됩니다.

 

이제 [테스터] 탭을 클릭하면 아래와 같이 [링크 복사] 가 활성화되어 있습니다.

 

 

하단의 [링크 복사] 를 클릭하면 Google Play 를 통해 테스트 앱을 다운로드 받을 수 있는 링크가 복사됩니다. 이 링크를 테스터 목록에서 등록한 이메일 로 전송해 주시면 됩니다. 안타깝게도 구글 개발자 콘솔에서 자동으로 링크를 전송해 주는 기능은 없습니다. Gmail 등의 다른 메일 전송 프로그램을 이용해 직접 링크를 전달해 주셔야 합니다.

 

메일로 전송 받은 링크를 안드로이드 기기에서 열면 다음과 같은 페이지로 이동됩니다.

 

 

[ACCEPT INVITE] 를 클릭하면 아래와 같은 페이지로 이동됩니다.

 

 

 

[Download it on Google Play] 를 클릭하면 구글 플레이에서 앱을 설치할 때 볼 수 있던 화면이 보입니다.

* 아래의 화면이 보이지 않고 잘못된 링크라는 에러 페이지가 보일 수 있습니다. 이는 등록에 시간이 조금 걸리기 때문에 발생하는 문제로 시간을 조금 보낸 후 다시 [Download it on Google Play] 를 클릭하면 됩니다.

 

 

설치가 완료되면 실행을 해주세요. 실행한 다음 [Login] 버튼을 클릭하면 바로 로그인이 될 수도 있고 다음과 같은 로그인 계정을 선택하는 안내창이 나올 수도 있습니다. 안내창이 나왔다면 [계정 사용] 버튼을 클릭해 주면 됩니다.

 

 

 

로그인에 성공하였습니다.

 

 

 

5 단계에 걸쳐 설명한 구글 플레이 게임 서비스에 로그인하는 과정이 완료되었습니다. 다소 복잡하고 긴 과정이지만 이 글을 참고해서 진행하신 분들은 모두 성공하셨으면 좋겠습니다. 

반응형
반응형

출처: https://velog.io/@the_paper__/Unity%EC%97%90%EC%84%9C%EC%9D%98-Singleton-2%ED%8E%B8-MonoBehaviour-Singleton%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90

머리말

이번에는 MonoBehaviour Singleton을 사용하는 이유 및 문제점을 파악해서 해결 방법을 고민해 볼 겁니다.

사실 Singleton 관련 블로그 글을 써보려고 마음먹은 이유도 이러한 문제점들이 있는데 MonoBehaviour Singleton을 남발하는 경우가 많아서 쓰게 되었는데, 한 명이라도 이 글을 읽고 고민이라도 조금 했으면 해서 쓰게 되었습니다.


1. 사용하는 이유

설명

이전 글에서도 설명드렸듯이 몇 가지 꼽아보자면 이 정도가 될 것 같습니다.

  • MonoBehaviour의 기능을 이용하기 위함. (Update, LateUpdate.. 등등 함수들 및 Coroutine기능)
  • Native Plugin이용 시, SendMessage함수로 전달받아야 하는 경우.
  • 에디터에서 쉽게 확인 가능 (Hierarchy View, Inspector View)

이 부분들은 직접 사용해보시면 체감하시게 될 것이라 생각됩니다.

주로 MonoBehaviour의 기능을 사용하기 위함이 가장 큰데, 하나하나 다 설명하자면 너무 길어지고 글 주제인 "문제점"과도 맞지 않아 이 글을 읽는 여러분들이 체감하시길 바랍니다.

하지만, 정말 유용하고 많이 쓰이고 필수로 써야 할 곳들이 존재한 다는 것은 꼭 알아두시길 바랍니다.


2. 문제점

2.1 플레이 중이 아닐 때 호출 시 문제

설명

Unity로 개발하다 보면 너무나 당연하게 플레이 중일 때를 가정하고 코딩할 때가 많은데, 협업을 하다 보면 플레이 중이 아닐 경우에 해당 Singleton을 접근해야 하는 경우가 생깁니다. 보통은 저의 경우에는 기획파트 혹은 아트 파트 요청으로 개발 툴을 개발하면서 발생하게 됩니다.

예로 들자면 Map에 몬스터를 배치하는 Map Tool이라고 있다고 하면, 이 툴에서는 Map ID만으로 Map을 불러와야 하는데 그러기 위해서는 Map의 정보가 저장된 데이터를 로드해야 하는 상황이고, 해당 데이터를 로드하는 코드는 MonoBehaviour Singleton으로 짜여 있는 상황입니다.

만약에 플레이 중이 아닐 때 MonoBehaviour Singleton의 Instance로 접근 시에 우리가 짜두었던 코드대로 동작한다면 GameObject가 생성되고 해당 오브젝트에 Component가 붙게 됩니다.

테스트 코드

[UnityEditor.MenuItem("TestMenu/Singleton2/GetMapDat_10205")]
public static void GetMapData_10205()
{
    TableDataManager.Instance.GetMapData(10205);
}

간단하게 테스트 코드를 만들어보았습니다.

에디터에서 그대로 실행 시에 GameObject가 생성되었습니다.

당장에는 문제가 되지 않을 수 있습니다만, 이대로 불러들인 Scene을 저장하게 될 경우 다음에 소개해드릴 "중복으로 생성되었을 때의 문제"가 발생하게 됩니다.

2.2 중복으로 생성되었을 때의 문제

설명

게임을 플레이 중인데 똑같은 MonoBehaviour Singleton이 있다면 어떤 것을 참조할까 고민해보면, 이전 글에서 만들어둔 코드를 기준으로 한다면 기존에 만들어진 오브젝트가 있더라도 새롭게 만들어서 Instance에 등록해줄 것입니다.

여기서 문제가 되는 부분은 Update, LateUpdate와 같이 MonoBehaviour 함수로 Instance와 동일한 오브젝트가 아니더라도 동작하는 경우입니다.

테스트 코드

public class CameraManager : MonoBehaviourSingletonTemplate<CameraManager>
{
    private void Start()
    {
        Invoke("OnOneSec", 1f);
    }

    private void Update()
    {
        Camera.main.transform.Translate(Vector3.forward * Time.deltaTime);
    }

    private void OnOneSec()
    {
        Debug.Log(Camera.main.transform.position);
    }

    public Camera GetMainCamera() => Camera.main;
}

만약에 위 코드처럼 Update함수에서 Camera의 Transform을 Translate함수로 이동시킨다고 가정했을 때, 중복으로 생성되어있는 상태라 Translate함수 호출이 프레임당 두번씩 호출되게 됩니다.

임시수정

protected void Awake()
{
    if (m_Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    DontDestroyOnLoad(gameObject);
}

임시로 Awake함수를 수정했습니다. Instance와 비교하여 Instance와 다르면 제거하도록 작업해두었습니다.

물론, 제가 생각하기에 이 상황이 제대로 된 상황은 아니며 임시방편으로 사용하는 코드입니다.

하지만 이 코드 역시 다른 문제를 만들게 되는데, 아래 코드로 설명드리겠습니다.

private void OnDestroy()
{
    Destroy(Camera.main.gameObject);
}

Component가 파괴될 때 OnDestroy함수가 호출되는데, 파괴되면서 실행하는 코드들이 또 문제를 일으키게 됩니다.

private void OnDestroy()
{
    if (Instance != this) return;
    Destroy(Camera.main.gameObject);
}

결국 이번에도 Instance와 비교하여 실행할지 말지를 결정해주어야 합니다.
중복 오브젝트 덕분에 덕지덕지 덮어씌워 주는 느낌이라 별로 좋지 않습니다.

2.3 OnDestroy때 호출 시 문제

설명

Unity에서 게임 종료 시에 모든 오브젝트들을 제거하고 OnDestroy함수들이 호출되게 되는데, 여기서 MonoBehaviour Singleton오브젝트가 먼저 파괴되고 다른 오브젝트 OnDestroy함수에서 이미 파괴된 MonoBehaviour Singleton의 Instacne를 참조하는 경우 우리가 짜두었던 코드대로라면 새롭게 GameObject를 생성하게 됩니다.

Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)

위 상황일 때 위와 같은 에러가 나오게 되고, GameObject가 생성되지 못했기 때문에 이후에 Instance의 멤버 접근하는 코드들은 Null Reference Exception을 뱉어내게 됩니다.

어차피 게임이 종료되는 상황이라 이 에러가 나와도 문제가 없다고 생각하실 수 있지만, 실제로 빌드하여 확인해보면 게임 종료 시에 "비정상 종료"도 나오게 되어 저장되어야 하는 것이 저장되지 못하고 사용자에게 에러 창을 보여주는 것으로 부정적인 이미지를 줄 수 있습니다.

이 부분은 OnDestroy함수들 마다 Instance를 참조하는 부분을 다 제거해주거나 이미 파괴되었는지 체크해주는 코드를 넣어야 합니다.

이 또한 매우 번거롭고 임시방편으로 보입니다.

3. 해결 방법

설명

제가 생각하는 가장 쉬운 방법 위 문제점들을 인지하고 MonoBehaviour Singleton을 "꼭 필요할 때만 쓰는 것"입니다.

이 글 가장 처음에 어느 상황에서 사용하면 좋을지 모두 설명드렸습니다. 해당 상황에 잘 맞는 경우 사용하고, 그 외의 상황에서는 최대한 지양하는 것입니다.

하지만, 이 또한 예외적인 상황이 너무 많아 MonoBehaviour Singleton을 만들지 않고 Singleton과 MonoBehavior의 기능을 이용하는 스크립트를 추가하여 해결해 보는 것이 최선이지 않나 싶습니다.

Event Listener 구현

public class MonoBehaviourEventListener : MonoBehaviour
{
    static MonoBehaviourEventListener m_Instance = null;

    public static MonoBehaviourEventListener Instance
    {
        get
        {
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode == false)
                return null;
#endif

            if (m_Instance == null)
            {
                var obj = new GameObject("MonoBehaviourEventListener");
                m_Instance = obj.AddComponent<MonoBehaviourEventListener>();
            }
            return m_Instance;
        }
    }

    public System.Action OnUpdateEvent;
    public System.Action OnLateUpdateEvent;


    private void Update()
    {
        OnUpdateEvent?.Invoke();
    }

    private void LateUpdate()
    {
        OnLateUpdateEvent?.Invoke();
    }
}

우선은 간단하게MonoBehaviourEventListener를 만들어보았습니다.

간단하게 Singleton으로 만들어서 Update, LateUpdate를 콜백으로 호출하도록 짜두었으니, 이제 Singleton에서 해당 콜백 등록해주면 됩니다.

Application.isPlaying가 아니라 UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode로 체크했던 이유는, 플레이 시작할 때 첫 Awake함수를 호출 시 Application.isPlaying이 false로 반환하는 경우가 있어서입니다.

Singleton class 수정

public class CameraManager : SingletonTemplate<CameraManager>
{
    public CameraManager()
    {
        if (MonoBehaviourEventListener.Instance != null)
        {
            MonoBehaviourEventListener.Instance.OnUpdateEvent += OnMonoBehaviourUpdate;
        }
    }

    private void OnMonoBehaviourUpdate()
    {
        Camera.main.transform.Translate(Vector3.forward * Time.deltaTime);
        Debug.Log(Camera.main.transform.position);
    }

    public Camera GetMainCamera() => Camera.main;
}

이전에 만들었던 CameraManager를 MonoBehavior Singleton이 아니라 Singleton으로 만들고, MonoBehaviourEventLister를 참조하여 이벤트 등록하도록 만들어봤고, 로그로 정상작동도 확인했습니다.

앞으로도 MonoBehaviour의 기능들은 MonoBehaviourEventLister를 통해서 작업하면 됩니다.

Update, LateUpdate, FixedUpdate함수의 이벤트로 받거나 Coroutine 역시 MonoBehaviourEventLister.Instance.StartCoroutine 같은 식으로 접근해서 사용하면 됩니다.

대신 MonoBehaviour기능을 사용하지 않고 사용하던 로직을 동작시키려면 다른 방법으로 동작하도록 코드 수정을 해야 해서 이 부분만 추가로 구현을 해주면 됩니다.

하지만, 이렇게 해도 Native Plugin에서 SendMessage함수로 전달받아야 하는 경우, GameObejct가 없기 때문에 이런 식으로 해결하지는 못하고 어쩔 수 없이 GameObject를 써야 합니다.

public class MonoBehaviourSingletonTemplate<T> : MonoBehaviour where T : MonoBehaviour
{
    static T m_Instance = null;

    public static T Instance
    {
        get
        {
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode == false)
                return null;
#endif
            if (m_Instance == null)
            {
                var obj = new GameObject(typeof(T).Name);
                m_Instance = obj.AddComponent<T>();
            }
            return m_Instance;
        }
    }

    protected void Awake()
    {
        if (m_Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);
    }
}

어쩔 수 없이 써야 하는 부분은 플레이 중인 것을 체크하여 생성해주도록 구현해주시면 됩니다.
위 코드대로라면 플레이 중이 아닌 경우에는 Null로 리턴될 것이니, 이 부분은 꼭 예외처리가 필요하니 꼭 주의 바랍니다.


마무리

MonoBehavior Singleton의 문제를 체크해보고 저만의 해결 방법을 고민해보았습니다.
이 글을 읽는 여러분들에게 혹시라도 도움이 되셨다면 좋을 것 같네요.

제가 해결해보고자 한 방법이 최선이 아니라고 생각하고 프로젝트마다 해결 방법도 다르다고 생각합니다.

혹시라도 여러분들이 생각하는 방법이 있으시면 덧글로 남겨주시면 감사하겠습니다.

긴 글 읽어주셔서 감사합니다.

 

반응형
반응형

출처:https://velog.io/@the_paper__/Unity%EC%97%90%EC%84%9C%EC%9D%98-Singleton-1%ED%8E%B8-%EC%8B%B1%EA%B8%80%ED%84%B4-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

머리말

이 글과 이후 글에서는 싱글턴에 대한 설명보다는 Unity에서 구현해보는데 집중해보고, 문제점과 해결방법을 이야기해볼 예정입니다.
싱글턴에 대한 좀 더 상세하고 원론적인 내용이 필요하신 분은 다른 글을 보시면 될 것 같습니다.


1. Singleton

설명

싱글턴(Singleton)이라고 하면 디자인 패턴(Design Pattern) 중, 정말 많이 쓰이는 패턴 중 하나입니다.

싱글턴 패턴은 클래스를 글로벌로 접근 가능하도록하는 방법 중 하나인데, 만들기도 간단하고 만들어두면 정말 편하게 사용 가능해서 여기저기 많이 쓰이는 패턴입니다.

구현

public class ItemInventory
{
    static ItemInventory m_Instance = null;
    public static ItemInventory Instance
    {
        get
        {
            if (m_Instance == null) m_Instance = new ItemInventory();
            return m_Instance;
        }
    }

    public Item GetItem(int itemID) { /* ... */ }
    public bool AddItem(Item item) { /* ... */ }
}

간단한 예제로 ItemInventory라는 싱글턴 클래스를 만들어보았습니다.
static 변수로 클래스를 Instance로 접근하도록 구현하면 됩니다.

//프로퍼티로 구현
static ItemInventory m_Instance = null;
public static ItemInventory Instance
{
    get
    {
        if (m_Instance == null) m_Instance = new ItemInventory();
        return m_Instance;
    }
}

//readonly변수로 구현
public readonly static ItemInventory Instance = new ItemInventory();

//함수로 구현
static ItemInventory m_Instance = null;
public static ItemInventory GetInstance()
{
    if (m_Instance == null) m_Instance = new ItemInventory();
        return m_Instance;
}

저의 경우에는 프로퍼티를 좋아해서 프로퍼티로 구현했는데, 위 코드처럼 readonly변수로 Instance를 선언하거나 프로퍼티 대신 함수로 구현해도 무방합니다.

사용

var item = ItemInventory.Instance.GetItem(1000);

실제 호출되는 부분은 이렇게 Instance로 접근해서 싱글턴 클래스의 맴버를 호출하면 됩니다.

구현 (Template)

public class SingletonTemplate<T> where T : class, new()
{
    static T m_Instance = null;
    public static T Instance
    {
        get
        {
            if (m_Instance == null) m_Instance = new T();
            return m_Instance;
        }
    }
}

public class ItemInventory : SingletonTemplate<ItemInventory>
{
    public Item GetItem(int itemID) { /* ... */ }
    public bool AddItem(Item item) { /* ... */ }
}

추후에 재활용성을 고려하여 Template으로 구현한 예제입니다.
그렇게 긴 코드가 아니긴 하지만, 추후에 싱글턴에 기능이 더 추가되거나 싱글턴 클래스들의 관리가 필요할 경우 유용하게 쓰입니다.


2. MonoBehaviour Singleton

설명

Unity에서는 싱글턴 클래스가 MonoBehaviour의 기능을 이용해야할 경우들이 존재하는데, 이러한 문제를 해결하기 위해 Unity에서 사용하는 싱글턴입니다.

구현

public class MonoBehaviourSingletonTemplate<T> : MonoBehaviour where T : MonoBehaviour
{
    static T m_Instance = null;
    public static T Instance
    {
        get
        {
            if (m_Instance == null)
            {
                var obj = new GameObject(typeof(T).Name);
                m_Instance = obj.AddComponent<T>();
            }
            return m_Instance;
        }
    }
    
    protected void Awake()
    {
    	DontDestroyOnLoad(gameObject);
    }
}

MonoBehaviour는 Unity에서 사용하는 Component클래스이기 때문에, GameObject에 AddComponent함수로 추가하여 사용해야 합니다.

Instance 프로퍼티 내부 코드를 보시면 바로 아실 거라 생각됩니다.

사용 이유

아마도 다음 편에서 MonoBehaviour 싱글턴에 대해 좀 더 상세히 장단점을 다뤄볼 예정이긴 한데, 우선 이러한 싱글턴을 Unity에서 주로 사용되는 경우는 아래처럼 몇가지 상황을 위해 사용됩니다.

  1. Update, LateUpdate, FixedUpdate 등등 MonoBehaviour 함수 사용해야 할 경우
    1.1. MonoBehaviour: https://docs.unity3d.com/ScriptReference/MonoBehaviour.html
    1.2. 함수 호출 순서: https://docs.unity3d.com/Manual/ExecutionOrder.html
  2. Coroutine 사용해야 할 경우
  3. SendMessage로 전달받아야 할 경우

마무리

우선 이번 편에서 간단하게 싱글턴 클래스들을 만들어봤습니다.

사실 첫 글이라 어떻게 혹은 어떤 구성으로 쓰는 게 좋을지 고민하고 조사해보느라 시간이 너무 많이 소모되었던 것 같네요.

다음 편부터는 할 말이 정말 많아서 첫 편은 짧고 쉬운 소재로 골랐는데 글이 잘 쓰였을지 잘 모르겠네요.

길지 않은 글이였지만 봐주셔서 감사합니다.

반응형

+ Recent posts