목차
퀴즈 게임 만들기
25.02.28
Google Play Games Service
: Google Play Console을 사용
※ 강사님의 Notion
https://spice-theory-152.notion.site/Google-Play-Games-Service-1a7d3cfdca3480609cffc4a1da9a9a74
Google Play Games Service 설정하기 | Notion
참고 자료
spice-theory-152.notion.site
--> 구글에 'google play games services 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
--> README의 Documentation에 나와있는 가이드 참고
https://developer.android.com/games/pgs/unity/unity-start?hl=ko
Unity용 Google Play 게임즈 설정 및 로그인 | Android game development | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Unity용 Google Play 게임즈 설정 및 로그인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 문서에서는
developer.android.com
>> GPGS를 사용하기 위해 Project 설정
--> Package Name을 'com.likelion.quizgame'으로 변경
>> Keystore 생성
: 우리가 만든 게임을 Store에 올리기 위해 보안적으로 안전하게 만들어주는 Key 파일
--> Key 파일이 Github에 올라가지 않도록 생성할 때 경로를 아예 다른 곳으로 지정하거나 '.gitignore'에 설정
--> 아래의 First and Last Name 등은 Optional 항목
--> Add Key 를 누르면 나오는 팝업에서는 Yes를 선택
※ Keystore의 Password와 Key의 Password를 각각 설정 가능하다. (여기선 Test로 likelion1234로 설정)
※ Key 파일은 담당자 1명이 담당해서 관리하는 것이 좋다.
※ 모바일 게임을 Build하여 Store에 올릴 때, 무조건 확인해봐야 하는 중요한 세팅들
: 63일차에서 이미 설정한 것들이 대부분
- Keystore 파일 등록
- Package Name
- IL2CPP
- ARM64
>> Build
: Store에 업로드할 때, 과거에는 'apk'확장자의 파일을 올렸지만, 이젠 개선되어 'aab' 확장자의 파일을 올리기 때문에 'aab' 확장자로 빌드하기 위해 Build App Bundle 체크
Google Play Console
: Google Play Console에 개발자 계정을 만들어서 앱 등록을 해야하는데, 개발자 계정을 만들 때 등록 수수료가 든다..
>> 앱 만들기
>> 앱 세부 정보 입력
: 앱인지 게임인지 정확하게 입력해야한다.
>> 내부 테스트 버전 만들기
: 테스트 및 출시 - 테스트 - 내부 테스트 - aab 파일 업로드 - 다음
>> 스토어 등록정보
: 사용자 늘리기 - 앱 정보 - 스토어 등록정보 - 기본 스토어 등록정보 만들기
--> 훑어보고 넘어감
Play 게임즈 서비스 설정
: 사용자 늘리기 - Play 게임즈 서비스 - 설정 및 관리 - 설정
--> 만들어둔게 없으니 새 클라우드 프로젝트 만들기
1. Google Cloud 콘솔 선택
2. 프로젝트 이름을 원하는 이름으로 설정 후 만들기
: 강사님은 사진과 같이 지어준 이름 그대로 사용
--> 조직이 있으면 선택
3. 다시 Google Play Console로 돌아와서 새로고침 후, 선택
: 이후 사용 클릭
└ Google Cloud Platform에서 OAuth 동의 화면 만들기
--> Google Cloud Platform을 눌러서 들어가기
- 앱 이름, 이메일 작성 후 다음
- 외부 선택 후 다음
- 연락처 정보로 이메일 주소 작성 후 다음
- 동의 후 계속, 그리고 만들기
--> 모든 과정을 마친 후, 구성 확인 클릭
└ 사용자 인증 정보 만들기
1. 사용자 인증 정보 추가 선택
2. OAuth 클라이언트 만들기 선택
3. OAuth 클라이언트 ID 만들기 선택
: 상단의 '+ CREATE CLIENT' 선택
└ 애플리케이션 유형은 'Android'
--> Project Settings - Player - Android - Other Settings - Package Name 과 패키지 이름이 동일하도록 작성
└ SHA-1 인증서 디지털 지문 생성하기
1. 생성을 위해 Keytool 설치
https://www.oracle.com/kr/java/technologies/downloads/#jdk21-windows
Download the Latest Java LTS Free
Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.
www.oracle.com
2. JDK 21로 설치
3. Windows Path에 JDK의 Bin 폴더 추가
: 내 PC 우클릭 - 속성 - 고급 시스템 설정 - 고급 - 환경 변수 - 시스템 변수 - Path 더블 클릭 - 새로 만들기 - 경로 복붙
--> cmd에서 'keytool' 명령어를 실행하여 설치가 되었는지 확인
4. cmd에서 keystore를 만든 경로로 이동하여 아래 명령어 실행
keytool -keystore path-to-debug-or-production-keystore -list -v
--> 'path-to-debug-or-production-keystore' 부분을 지우고 keystore 파일명과 확장명을 넣기 (현재는 likelion.keystore)
--> keystore의 password를 입력
5. SHA1 부분을 복사하여 'OAuth 클라이언트 만들기'로 돌아가 'SHA-1 인증서 디지털 지문'에 붙여넣기
>> 만든 OAuth 클라이언트를 선택하고 변경사항 저장을 클릭하여 사용자 인증 정보 만들기
└ 플러그인 설치
: 구글에 'google play games services 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
--> 해당 페이지에서 Source code.zip 파일을 다운로드 받아서 압축 해제
>> Unitypackage Import
--> 압축 해제한 폴더에서 'current-build' 폴더에 들어가 unitypackage 파일 선택
--> New가 아니라 주의표시와 함께 화살표로 나오는 것은 기존 파일이 바뀐다는 의미이므로 살펴보아야 한다.
--> Import 후 아래와 같은 Google Version Handler 팝업이 나타나면 Apply 버튼을 클릭
--> 텅 빈 Google Version Handler 창은 닫기
- 이후, Assets > External Dependency Manager > Android Resolver > Resolve 버튼을 클릭
--> 클릭 후 아래와 같은 팝업이 나타나는지 확인
└ 리더보드 설정
: Play 게임즈 서비스 > 설정 및 관리 화면에서 리더보드를 선택 후 “리더보드 생성” 버튼을 클릭
- 아래와 같이 리더보드를 설정 (수업에서는 Quiz Ranking으로 이름을 설정)
--> 수업 때는 Icon도 넣음
└ 업적 만들기
: 사용자 늘리기 > Play 게임즈 서비스 > 설정 및 관리 > 업적을 선택 후 “업적 만들기” 버튼을 클릭
- 이름을 'quiz1'로 변경하고 임시보관함에 저장
└ XML 입력
Play 게임즈 서비스 > 설정 및 관리 > 리더보드(or 업적) > 리소스 보기
--> 사진은 리더보드, 수업 때는 업적에서 '리소스 보기' 클릭 (둘다 만든 뒤라 둘다 추가되어 있으니 상관 없음)
--> 나온 Android(XML)을 복사
- 복사한 XML을 Unity에서 Window - Google Play Games - Setup - Android setup 에 붙여넣기
--> Client ID는 Optional
--> 이후 Setup 클릭
※ 세팅을 하고나면 빌드가 잘 안 되는 문제가 발생할 수 있음
: 현재는 이미 Minimum API Level과 Target API Level을 잘 설정해놔서 문제 없이 빌드가 된다.
--> 하지만 이거 외에도 많은 문제가 발생할 수 있음
└ 프로젝트 게시하기
: 사용자 늘리기 > Play 게임즈 서비스 > 설정 및 관리 > 설정 > 우측 상단의 '검토 및 게시' 클릭
--> '조치 필요'는 조치를 무조건 해줘야 한다.
>> 설명
: 게임의 표시 이름, 설명, 게임 카테고리 등을 입력합니다. (수업에선 이름은 그대로 둠)
--> 설명에 '테스트' 넣음
- 게임의 아이콘 이미지와 그래픽 이미지를 등록
--> 요구하는 사이즈에 맞게 준비
--> 이후 변경사항 저장
>> 업적
: 설명, 아이콘 추가 (수업에선 설명을 업적1, 업적2로 넣음)
--> 이후 '임시보관함에 저장' 클릭
>> 모든 조치를 마친 뒤, 우측 상단의 '출시' 클릭
└ Play 게임즈 서비스 SDK를 APK에 추가하여 API 사용
: GPGSManager.cs 생성
※ 아래 링크의 '로그인 서비스 확인' 이하 참고
https://developer.android.com/games/pgs/unity/unity-start?hl=ko
Unity용 Google Play 게임즈 설정 및 로그인 | Android game development | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Unity용 Google Play 게임즈 설정 및 로그인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 문서에서는
developer.android.com
>> 빈 게임 오브젝트로 'GPGS Manager'를 만들고 GPGSManager.cs 추가
>> Leaderboard Button의 OnClick()에 함수 추가 및 바인딩
>> 테스트용으로 점수를 부여해주는 Button 생성
: Reward Ad Test Button을 복붙하여 Score Add Test Button을 만들어서 위치 조정
--> Text는 "점수 추가"로 수정
--> Button의 OnClick()에 함수 추가 및 바인딩, int 값은 10으로 설정
>> Build하여 테스트
--> 현재 Google Play Console을 사용하지 못해서 테스트 못 해봄..
※ 테스트 이메일과 폰의 이메일이 다르다면 플레이 게임즈를 설치한 후, 계정 전환을 하면 된다.
인앱결제
: Unity Cloud 활용
>> In App Purchasing 설치
>> Project Settings > Services > In-App Purchasing
--> Unity Hub에서 Project를 만들 때, Unity Cloud에 연결했다면 'Use an existing cloud project'를 선택하고 연결하지 않았다면 'Create a new cloud project'를 선택 (현재 나는 연결되어있지 않기 때문에 'Create a new cloud project' 선택)
※ 'Create a new cloud project'를 선택한 경우
: 'Will this app be targeted to children as defined by applicable laws?' --> 아이들도 즐기는 앱인 지를 묻는 것이 아니라 아이들만을 위한 앱인 지를 묻는 것
>> 추가 설정
--> 상품 등록은 현재 계정 설정으로 인해 힘들기 때문에 넘어감
>> IAPManager.cs 생성 및 코드 작성
: IAP = In-App Purchasing
최종 코드
>> GPGSManager.cs
: leaderboardId는 Window - Google Play Games - Setup - Android setup에 입력한 XML 안에 있음
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using GooglePlayGames;
using GooglePlayGames.BasicApi;
public class GPGSManager : Singleton<GPGSManager>
{
#if UNITY_ANDROID
private const string leaderboardId = "CgkIw_uD_pgCEAIQAQ";
#elif UNITY_IOS
#endif
private void Start()
{
PlayGamesPlatform.Instance.Authenticate(ProcessAuthentication);
}
private void ProcessAuthentication(SignInStatus status)
{
if (status == SignInStatus.Success) // 로그인 성공
{
Debug.Log("GPGS Authentication Success");
string name = PlayGamesPlatform.Instance.GetUserDisplayName();
string id = PlayGamesPlatform.Instance.GetUserId();
string profileImage = PlayGamesPlatform.Instance.GetUserImageUrl();
}
else // 로그인 실패
{
Debug.Log("GPGS Authentication Failed");
}
}
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
// 스코어를 리더보드에 전달
public void ReportScore(int score)
{
Social.ReportScore(score, leaderboardId, success =>
{
if (success)
{
Debug.Log("Leaderboard report score success.");
}
else
{
Debug.Log("Leaderboard report score failed.");
}
});
}
public void ShowLeaderboard()
{
Social.ShowLeaderboardUI();
}
}
>> IAPManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
public class IAPManager : MonoBehaviour, IStoreListener
{
public enum Type
{
NOADS, // 광고 제거
NOADS_HEART_60, // 광고 제거 + 하트 60개
HEART_20,
HEART_60,
HEART_150,
HEART_320,
HEART_450
}
public const string kPID_Heart20 = "heart_20";
public const string kPID_Heart60 = "heart_60";
public const string kPID_Heart150 = "heart_150";
public const string kPID_Heart320 = "heart_320";
public const string kPID_Heart450 = "heart_450";
public const string kPID_NoAds = "noads";
public const string kPID_NoAds_Heart60 = "noads_heart_60";
private IStoreController _storeController;
private IExtensionProvider _storeExtensionProvider;
private void Start()
{
if (_storeController == null)
{
InitializePurchasing();
}
}
private void InitializePurchasing()
{
if (IsInitialized())
{
return;
}
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
builder.AddProduct(kPID_Heart20, ProductType.Consumable); // Consumable : 소모성 아이템
builder.AddProduct(kPID_Heart60, ProductType.Consumable);
builder.AddProduct(kPID_Heart150, ProductType.Consumable);
builder.AddProduct(kPID_Heart320, ProductType.Consumable);
builder.AddProduct(kPID_Heart450, ProductType.Consumable);
builder.AddProduct(kPID_NoAds, ProductType.NonConsumable); // NonConsumable : 비소모성 아이템
builder.AddProduct(kPID_NoAds_Heart60, ProductType.NonConsumable);
UnityPurchasing.Initialize(this, builder);
}
private bool IsInitialized()
{
return _storeController != null && _storeExtensionProvider != null;
}
public void BuyProduct(Type type) // 상품을 구매할 때 호출
{
switch (type)
{
case Type.HEART_20:
BuyProductID(kPID_Heart20);
break;
case Type.HEART_60:
BuyProductID(kPID_Heart60);
break;
case Type.HEART_150:
BuyProductID(kPID_Heart150);
break;
case Type.HEART_320:
BuyProductID(kPID_Heart320);
break;
case Type.HEART_450:
BuyProductID(kPID_Heart450);
break;
case Type.NOADS:
BuyProductID(kPID_NoAds);
break;
case Type.NOADS_HEART_60:
BuyProductID(kPID_NoAds_Heart60);
break;
}
}
private void BuyProductID(string productId)
{
if (IsInitialized())
{
Product product = _storeController.products.WithID(productId);
if (product != null && product.availableToPurchase)
{
Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
_storeController.InitiatePurchase(product);
}
else
{
Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
}
}
else
{
Debug.Log("BuyProductID FAIL. Not initialized.");
}
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
_storeController = controller;
_storeExtensionProvider = extensions;
}
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
}
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
Debug.Log("OnInitializeFailed InitializationFailureReason:" + error + " message:" + message);
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",
product.definition.storeSpecificId, failureReason));
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart20, StringComparison.Ordinal))
{
UserInformations.HeartCount += 20;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart60, StringComparison.Ordinal))
{
UserInformations.HeartCount += 60;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart150, StringComparison.Ordinal))
{
UserInformations.HeartCount += 150;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart320, StringComparison.Ordinal))
{
UserInformations.HeartCount += 320;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart450, StringComparison.Ordinal))
{
UserInformations.HeartCount += 450;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds, StringComparison.Ordinal))
{
UserInformations.IsNoAds = true;
}
else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds_Heart60, StringComparison.Ordinal))
{
UserInformations.HeartCount += 60;
UserInformations.IsNoAds = true;
}
else
{
Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", purchaseEvent.purchasedProduct.definition.id));
}
return PurchaseProcessingResult.Complete;
}
}
>> UserInformations.cs
using UnityEngine;
using UnityEditor;
public static class UserInformations
{
// 다른 수강생님이 만든 레지스트리 초기화 함수
// [MenuItem("Window/PlayerPrefs 초기화")]
// private static void ResetPrefs()
// {
// PlayerPrefs.DeleteAll();
// Debug.Log("PlayerPrefs has been reset.");
// }
private const string HEART_COUNT = "HeartCount"; // string key 값 저장
private const string LAST_STAGE_INDEX = "LastStageIndex";
// 하트 수
public static int HeartCount
{
get
{
// "HeartCount"라는 이름의 정보를 Int로 가져오는데, Default 값은 5 --> 최초로 게임이 시작되면 저장된 값이 없기 때문
return PlayerPrefs.GetInt(HEART_COUNT, 5);
}
set
{
// value 값으로 PlayerPrefs에 저장
PlayerPrefs.SetInt(HEART_COUNT, value);
}
}
// 스테이지 클리어 정보
public static int LastStageIndex
{
get { return PlayerPrefs.GetInt(LAST_STAGE_INDEX, 0); }
set { PlayerPrefs.GetInt(LAST_STAGE_INDEX, value); }
}
// 효과음 재생 여부
public static bool IsPlaySFX
{
get { return PlayerPrefs.GetInt("IsPlaySFX", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlaySFX", value ? 1 : 0); }
}
// 배경음악 재생 여부
public static bool IsPlayBGM
{
get { return PlayerPrefs.GetInt("IsPlayBGM", 1) == 1; }
set { PlayerPrefs.SetInt("IsPlayBGM", value ? 1 : 0); }
}
// 광고 제거 여부
public static bool IsNoAds
{
get { return PlayerPrefs.GetInt("IsNoAds", 0) == 1; }
set { PlayerPrefs.SetInt("IsNoAds", value ? 1 : 0); }
}
}
고민해야할 개선 사항
- Item을 구매할 때, IAPManager.cs의 BuyProduct()를 호출할텐데, 그럼 BuyProductID()로 들어가고 InitiatePuchase()함수를 통해 해당 product를 요청하고 끝난다. 즉, 동기적이지 않고 비동기적이다. --> 유저에게 팝업이 뜨고 유저가 구매를 승인하면 ProcessPurchase()가 호출된다. 따라서 구매 버튼을 눌렀을 때와 구매가 결정되고 끝났을 때가 순차적으로 진행되는 것이 아니므로 구매가 완료되면 호출되도록 Event 함수(Action 등)를 이용해야 한다.
- UserInfomation에서 데이터를 변경하면 GameManager랑 HeartPanel이 변경된 값을 참조하지 않고 있어서 나중에 고쳐야한다. (수강생님 의견)
'Development > C#' 카테고리의 다른 글
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 67일차 (0) | 2025.03.05 |
---|---|
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 66일차 (0) | 2025.03.05 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 64일차 (0) | 2025.02.27 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 63일차 (0) | 2025.02.26 |
멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 62일차 (0) | 2025.02.25 |