환경
이름 | 버전 (2019-12-04 기준) |
Unity3d | 2018.4.13f1 (LTS) |
Unity3d IAP(In-App Purchasing) | 1.23.1 |
Firebase Unity SDK | 6.7.0 |
Node | 12.13.1 |
이 글은 Firebase의 기능과 Node.js 서버만을 이용합니다.
기본적 빌드 세팅
새로운 Unity3d 프로젝트를 만든다. 2018 LTS 버전으로 제작한다.
Build Settings -> Android 로 Switch Platform 해준다.
Player Settings... 누른 후 아래와 같이 세팅해준다.
중요 설정 값
PakageName - 기본 이름을 사용하면 안되고 임의의 값으로 수정해줘야한다. (Ex. com.TestCompany.TestMan)
Scripting Baakend - IL2CPP
Api Compatibility Level - .NET 4.x
Target Architectures - ARM64
Firebase Auth 세팅
로그인 방법 설정 추가설명
로그인 방법 설정
1. [개발] 메뉴에서 Authentication 을 선택한다.
2. [로그인 방법] 선택
이메일/비밀번호, Google, 익명 로그인 방법을 사용 설정 한다.
3. Unity3d 에 Firebase SDK 패키지 중 FirebaseAuth.unitypackage 를 추가한다.
자신의 유니티 프로젝트에 Firebase Unity SDK 들 중 dotnet4(자신의 프로젝트의 Scripting Runtime Version 에 따라 dotnet3 or dotnet4 를 선택해준다. .Net 4.x Equivalent 를 선택 시 dotnet4에 있는 패키지를 설치해주면 된다.) 폴더 안에 있는 FirebaseAuth.unitypackage 를 Import 해준다.
Unity3d에서 Google 로그인를 사용하기 위해 google-signnin-unity 를에 들어가 최신 버전의 unitypackage 를 다운로드 받은 후 Import 한다.
Import 후 3개의 오류가 발생할 수 있다.
Task 관련 오류가 발생 시 Assets - Parse - Plugins 폴더에 있는 dotNet45 폴더 내용을 뺀 나머지 중복 dll 파일을 삭제해줘야한다. .Net버전에 따라 지울 dll 파일을 선택한다. 필자는 .Net4.x 버전이므로 .Net3.5 버전인 Unity.Compat, Unity.Tasks 2개의 dll 파일을 지워줬다.
구글 로그인을 사용하기 위해서는 구글 개발자 계정을 등록해준다.
Play Console 사용 방법 을 참고하여 구글자 계정을 만든다. (결제를 해야한다.) 기존의 아이디를 보유하고 있다면 넘어가자.
https://play.google.com/apps/publish/signup
구글 개발자 계정을 생성한 후
File - Build Settings - Player Settings - Android 항목 - Publishing Settings - Keystore 설정을 해야한다.
Create a new keystore... 선택 후 Browse Keystore 버튼을 클릭하여 keystore 생성 위치를 정한다. 생성 위치는 프로젝트 폴더 내에 위치하도록 한다. 필자는 편하게 사용하기 위해 아래와 같은 위치에 생성했다.
keystore 비밀번호를 설정한 후 key - Alias - Create a new key 를 선택하여 새로운 키를 생성한다.
key 이름과 비밀번호를 설정한 후 Create Key 를 누른다.
방금 생성한 키를 선택한다.
인제 생성한 keystore 파일에서 Firebase 에 등록할 SHA 인증서 지문을 얻어야 한다. 터미널을 킨 후 keystore 파일이 있는 위치로 cd 명령어를 사용하여 이동한다.
keytool -list -v -keystore 자신의 키스토어 이름.keystore
keystore가 있는 위치에서 위 코드를 실행한다. 터미널에 다양한 정보가 나오는데 그 중 인증서 지문 : 아래에 있는 SHA1 지문을 복사한다.
복사한 SHA1 지문을 [자신의 Firebase Project - 프로젝트 설정 - 일반 탭]의 내 앱 중 Android 앱의 SHA 인증서 지문에 디지털 지문 추가
버튼을 눌러 아까 복사한 SHA1 지문을 넣어준다.
새로운 Firebase 앱 추가 설명
SHA 인증서 지문을 처음부터 추가하면서 설정하는 법. 아직 활성화를 하지 않았다면 아래 방법을 펼치기 하여 보고 따라하자.
새로운 Firebase 앱 추가 시 설정하기
새로운 앱을 만들때 모든 정보를 다 입력한 후 생성하면 추가로 SHA-1 지문을 차후에 추가를 안해도 된다.
Android 패키지 이름 - 자신의 Unity3d 프로젝트의 Package Name 과 똑같게 설정해줘야한다.
앱 닉네임 - 임의로 설정해도 된다.
SHA-1 - 위에 적어둔 방법으로 keystore에서 추출 후 입력해준다.
앱 등록 - 다음 - 다음 - 이 단계 건너띄기 로 슥슥 넘어가준다.
SHA 인증서 지문까지 추가한 후 google-services.json 버튼을 눌러 다운로드 받는다.
다운로드 받은 google-services.json json 파일을 자신의 Unity3d Project - Assets - Firebase 폴더에 넣어준다.
이 글은 Firebase 인증들 중 익명 로그인, 이메일 로그인, 구글 로그인을 기준으로 설명한다.
로그인 작업을 진행할 간단한 UI를 제작한다.
기본적인 UI 배치가 되어 있는 Scene 파일
위의 첨부된 압축 파일을 받은 후 압축해제하고 나온 파일을 Hierarchy 창에 드래그하면
와 같이 기본적인 UI 들이 배치된다.
GUI만 배치된 상태이고 각 Gameobject들에 들어가야하는 필수적인 cs 파일들은 비어있는 상태이다.
인제 Script들을 만들어서 버튼들을 채워줘야한다.
Firebase 인증을 담당하는 스크립트를 만들도록 한다.
LoginManager 에 넣어줄 Login.cs를 제작한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Google;
using Firebase.Auth;
using System.Threading.Tasks;
using UnityEngine.SceneManagement;
public class Login : MonoBehaviour
{
// Auth 용 instance
FirebaseAuth auth = null;
// 사용자 계정
FirebaseUser user = null;
// 로그인 선택 화면
public GameObject LoginPanel;
// 임시 로딩 패널
public GameObject LoadingPanel;
// 닉네임 설정 패널
public GameObject NicknamePanel;
// 이메일 생성 패널
public GameObject EmailCreatePanel;
// 연동 관련 패널
public GameObject DataInterlinkPanel;
// 기기 연동이 되어 있는 상태인지 체크한다.
private bool signedIn = false;
// 임시저장용 클래스
private User.userLoginData.LoginType tempLoginType = User.userLoginData.LoginType.None;
private string tempemail = string.Empty;
private string temppw = string.Empty;
private void Awake()
{
// 초기화
auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
// 유저의 로그인 정보에 어떠한 변경점이 생기면 실행되게 이벤트를 걸어준다.
auth.StateChanged += AuthStateChanged;
//AuthStateChanged(this, null);
LoadingPanel.SetActive(false);
LoadingPanel.SetActive(false);
NicknamePanel.SetActive(false);
}
private void Start()
{
//#if UNITY_EDITOR
//TestInit();
//#endif
}
// 테스트용 초기화 함수
public void TestInit()
{
if (auth.CurrentUser != null)
{
// 로그아웃 처리한다.
auth.SignOut();
//Server.Instance.TestInit(auth.CurrentUser.UserId);
//auth.CurrentUser.DeleteAsync();
}
}
// 계정 로그인에 어떠한 변경점이 발생시 진입.
void AuthStateChanged(object sender, System.EventArgs eventArgs)
{
if (auth.CurrentUser != user)
{
// 연동된 계정과 기기의 계정이 같다면 true를 리턴한다.
signedIn = user != auth.CurrentUser && auth.CurrentUser != null;
if (!signedIn && user != null)
{
UnityEngine.Debug.Log("Signed out " + user.UserId);
}
user = auth.CurrentUser;
if (signedIn)
{
UnityEngine.Debug.Log("Signed in " + user.UserId);
}
}
}
// 로그인 선택 패널을 열며 로그인한 user가 있는지 확인한다.
// 없으면 계정 생성 시작
public void LoginCheck()
{
// 연동 상태가 아니라면...
if (!signedIn)
{
LoginPanel.SetActive(true);
}
else
{
StartCoroutine(CurrentUserDataGet());
}
}
// 기존 유저 정보 서버에서 가져온다
public IEnumerator CurrentUserDataGet()
{
LoadingPanel.SetActive(true);
// 유저 정보
User.Instance.GetUserData(auth.CurrentUser.UserId, new System.Action(() => {
Debug.Log("유저 정보 로드 완료!");
// 유저 인벤 정보
User.Instance.GetUserInven(auth.CurrentUser.UserId, new System.Action(() => {
// 다음 씬으로 넘긴다.
NextSecne();
}));
}));
yield return null;
}
// 게임씬으로 넘어감
public void NextSecne()
{
Debug.Log("GameScene 으로...");
SceneManager.LoadSceneAsync(1);
}
// 익명 로그인
public async void AnonyLogin()
{
LoadingPanel.SetActive(true);
// 익명 로그인 진행
await auth.SignInAnonymouslyAsync().ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.LogError("SignInAnonymouslyAsync was canceled.");
return;
}
if (task.IsFaulted)
{
Debug.LogError("SignInAnonymouslyAsync encountered an error: " + task.Exception);
return;
}
// 익명 로그인 연동 결과
Firebase.Auth.FirebaseUser newUser = task.Result;
Debug.LogFormat("User signed in successfully: {0} ({1})",
newUser.DisplayName, newUser.UserId);
});
// 신규 유저 데이터 임시 저장
userDataInit();
userDataTempSave(User.userLoginData.LoginType.anony);
LoadingPanel.SetActive(false);
// 닉네임 생성 창 켜주기
NicknamePanel.SetActive(true);
}
// 구글 로그인
public void GoogleLogin()
{
LoadingPanel.SetActive(true);
try
{
// 구글 로그인 처리부분
// 구글 로그인 팝업창이 꺼지면 실행될 콜백함수를 선언한다.
GoogleLoginProcessing(new System.Action<bool>((bool chk) =>
{
if (chk)
{
// 신규 유저 데이터 임시 저장
userDataInit();
userDataTempSave(User.userLoginData.LoginType.google);
LoadingPanel.SetActive(false);
// 닉네임 생성 창 켜주기
NicknamePanel.SetActive(true);
}
else
{
LoadingPanel.SetActive(false);
}
}));
}
catch (System.Exception err)
{
Debug.LogError(err);
LoadingPanel.SetActive(false);
}
}
// 구글 로그인 구동 부분
private async void GoogleLoginProcessing(System.Action<bool> callback)
{
if (GoogleSignIn.Configuration == null)
{
// 설정
GoogleSignIn.Configuration = new GoogleSignInConfiguration
{
RequestIdToken = true,
RequestEmail = true,
// Copy this value from the google-service.json file.
// oauth_client with type == 3
WebClientId = ""
};
}
Task<GoogleSignInUser> signIn = GoogleSignIn.DefaultInstance.SignIn();
TaskCompletionSource<FirebaseUser> signInCompleted = new TaskCompletionSource<FirebaseUser>();
await signIn.ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.Log("Google Login task.IsCanceled");
callback(false);
}
else if (task.IsFaulted)
{
Debug.Log("Google Login task.IsFaulted");
callback(false);
}
else
{
Credential credential = Firebase.Auth.GoogleAuthProvider.GetCredential(((Task<GoogleSignInUser>)task).Result.IdToken, null);
auth.SignInWithCredentialAsync(credential).ContinueWith(authTask =>
{
if (authTask.IsCanceled)
{
signInCompleted.SetCanceled();
Debug.Log("Google Login authTask.IsCanceled");
callback(false);
return;
}
if (authTask.IsFaulted)
{
signInCompleted.SetException(authTask.Exception);
Debug.Log("Google Login authTask.IsFaulted");
callback(false);
return;
}
user = authTask.Result;
Debug.LogFormat("Google User signed in successfully: {0} ({1})", user.DisplayName, user.UserId);
callback(true);
return;
});
}
});
}
// 이메일 로그인
public async void EmailLogin()
{
var email = EmailCreatePanel.transform.Find("email").Find("Text").GetComponent<UnityEngine.UI.Text>().text;
var pw = EmailCreatePanel.transform.Find("pw").Find("Text").GetComponent<UnityEngine.UI.Text>().text;
if (email.Length < 1 || pw.Length < 1)
{
Debug.Log("이메일 ID 나 PW 가 비어있습니다.");
return;
}
EmailCreatePanel.SetActive(false);
LoadingPanel.SetActive(true);
await auth.CreateUserWithEmailAndPasswordAsync(email, pw).ContinueWith(task =>
{
if (task.IsCanceled)
{
UnityEngine.Debug.LogError("CreateUserWithEmailAndPasswordAsync was canceled.");
return;
}
if (task.IsFaulted)
{
UnityEngine.Debug.LogError("CreateUserWithEmailAndPasswordAsync encountered an error: " + task.Exception);
return;
}
// firebase email user create
Firebase.Auth.FirebaseUser newUser = task.Result;
UnityEngine.Debug.LogFormat("Firebase Email user created successfully: {0} ({1})", newUser.DisplayName, newUser.UserId);
return;
});
// 신규 유저 데이터 임시 저장
userDataInit();
userDataTempSave(User.userLoginData.LoginType.email, email, pw);
// 로딩 패널 꺼주ㄱ
LoadingPanel.SetActive(false);
// 닉네임 생성 창 켜주기
NicknamePanel.SetActive(true);
}
// 유저 닉네임 입력 시작
private void InsertNewUserData()
{
var nickname = NicknamePanel.transform.Find("InputField").Find("NickNameInput").GetComponent<UnityEngine.UI.Text>().text;
if (nickname.Length > 0)
{
// 신규 유저 데이터 입력
loginDataSave(tempLoginType, nickname, tempemail, temppw);
}
else
{
Debug.Log("별명을 입력해주세요.");
return;
}
}
// 유저 임시 데이터 초기화
private void userDataInit()
{
tempLoginType = User.userLoginData.LoginType.None;
tempemail = string.Empty;
temppw = string.Empty;
}
// 유저 데이터 임시 저장
private void userDataTempSave(User.userLoginData.LoginType loginType, string email = null, string pw = null)
{
tempLoginType = loginType;
tempemail = email;
temppw = pw;
}
// 신규 유저 데이터 입력
private void loginDataSave(User.userLoginData.LoginType loginType, string nickname = null, string email = null, string pw = null)
{
// 유저 데이터
var newUser = new User.userLoginData();
// DB에 저장될 유저데이터 초기화
newUser.loginType = tempLoginType;
newUser.nickname = nickname;
newUser.uid = user.UserId;
newUser.email = email;
newUser.pw = pw;
newUser.deviceModel = SystemInfo.deviceModel;
newUser.deviceName = SystemInfo.deviceName;
newUser.deviceType = SystemInfo.deviceType;
newUser.deviceOS = SystemInfo.operatingSystem;
newUser.createDate = auth.CurrentUser.Metadata.CreationTimestamp;
string json = JsonUtility.ToJson(newUser);
// 코인 저장용
var newUserInventory = new User.userGoodsData();
// DB에 저장될 유저데이터 초기화
newUserInventory.uid = user.UserId;
newUserInventory.coin = 0;
LoadingPanel.SetActive(true);
LoginPanel.SetActive(false);
NicknamePanel.SetActive(false);
// 위에 정리한 json을 서버에 보내 DB에 저장한다.
// 새로운 유저에 대한 데이터를 DB에 보낸다.
Server.Instance.NewUserJsonDBSave(json, () => {
// DB에 저장 후 디바이스 user정보에도 저장한다.
User.Instance.mainUser = newUser;
// 새로운 유저의 인벤토리 데이터를 DB에 보낸다.
Server.Instance.NewUserInventoryJsonDBSave(JsonUtility.ToJson(newUserInventory), () => {
// DB에 저장 후 디바이스 user정보에도 저장한다.
User.Instance.mainInventory = newUserInventory;
// 다음씬으로 이동
NextSecne();
});
});
}
// 연동 해제
public void SignOut()
{
if (auth.CurrentUser != null)
auth.SignOut();
DataInterlinkPanel.SetActive(false);
}
// 연동 계정 삭제
public void UserDelete()
{
if (auth.CurrentUser != null)
auth.CurrentUser.DeleteAsync();
DataInterlinkPanel.SetActive(false);
}
}
// Copy this value from the google-service.json file.
// oauth_client with type == 3
WebClientId = ""
webClientId 은 위에서 다운받았던 google-services.json 을 열고 client_type 3 인 client_id 를 복사하여 붙여넣기 한다.
유저 데이터를 처리할 User.cs 파일을 만든다.
using UnityEngine;
public class User : MonoBehaviour
{
// 스레드에 안전한 싱글톤 선언
private static bool shuttingDown = false;
private static object Lock = new object();
private static User instance;
public static User Instance
{
get
{
if (shuttingDown)
{
Debug.LogWarning("User Instance already destroyed. return null");
return null;
}
lock (Lock)
{
if (instance == null)
{
instance = (User)FindObjectOfType(typeof(User));
if (instance == null)
{
var userObject = new GameObject();
instance = userObject.AddComponent<User>();
userObject.name = "ServerManager";
DontDestroyOnLoad(userObject);
}
}
return instance;
}
}
}
private void Awake()
{
DontDestroyOnLoad(this);
}
// 게임에서 사용할 유저 정보를 저장해두는 곳
public userLoginData mainUser;
// 유저의 인벤토리
public userGoodsData mainInventory;
// 유저 정보 클래스
[System.Serializable]
public class userLoginData
{
public enum LoginType
{
None = 0,
anony = 1,
email = 2,
google = 3
}
public string nickname;
public LoginType loginType;
public string uid;
public string email;
public string pw;
public string deviceModel;
public string deviceName;
public DeviceType deviceType;
public string deviceOS;
public ulong createDate;
}
// 유저 재화 보유 클래스
[System.Serializable]
public class userGoodsData
{
public string uid;
public int coin;
}
// 유저 영수증 정보
[System.Serializable]
public class userReceipt
{
public string index;
public string uid;
public string transaction_id;
public string product_id;
public string platform;
public string price;
public string date;
public string givecheck;
}
// 서버에서 유저 데이터 받아오기
public void GetUserData(string uid, System.Action callback)
{
Server.Instance.GetUserDataDB(uid, callback);
}
// 서버에서 유저 인벤 정보 받아오기
public void GetUserInven(string uid, System.Action callback)
{
Server.Instance.GetUserInvenDB(uid, callback);
}
}
Node.js 서버와 통신을 할 서버를 만들어주자.
Firebase Hosting 에서 돌아갈 Node 서버를 제작을 해야하기 때문에 Firebase Hosting Project 를 먼저 활성화 시켜준다.
Mac에서 제작할 경우 모든 명령어 앞에 "sudo" 를 작성해야한다.
호스팅 활성화 설명
아직 Firebase Hosting을 Firebase Console에서 아직 활성화 하지 않았다면 아래 펼치기 를 참고하여 활성화 시키자.
설치, 초기화 세팅은 아래에서 자세하게 설명하니 넘어가자.
1. 개발 탭의 [Hosting] 을 선택한다.
2. [시작하기] 버튼을 눌러 시작한다.
3. 넘어간다.
4. 넘어간다.
5. [Appnickname] 을 설정 후 [연결 및 계속하기] 버튼을 눌러 다음으로 넘어간다.
6. 넘어간다.
Firebase Hosting 기본 가이드
아래를 참고하여 새로운 기본적인 Hosting 프로젝트를 만들어서 사용하자.
Firebase Hosting 프로젝트에 곧바로 Node.js 서버를 구축하는 법이다.
MAC에서는 아래에서 실행하는 명령어 앞에 sudo 를 붙여줘야한다.
참고
참고자료이다. 필요하지 않다면 참고하지 않아도 된다. 그냥 아래에 있는 기본 설정을 보면 된다.
https://firebase.google.com/docs/admin/setup?hl=ko
https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko
기본 설정
1. Node를 설치해해준다.
Node.js 를 설치하면 npm 도 같이 설치가 된다.
만약 node와 npm 가 설치가 되어있는지 확인 하려면 터미널에
node : node -v
npm : npm -v
을 실행하면 된다.
2. Visual Studio Code를 설치해준다.
https://code.visualstudio.com/
3. npm install -g express-generator 로 Express 를 설치한다.
4. npm install -g firebase-tools 로 firebase tools 를 설치한다.
firebase tools은 터미널에서 아무 장소에서나 실행시켜도 된다.
5. firebase tools 의 설치가 완료되면 Firebase hosting 프로젝트를 만들 위치로 이동한다.
만약 /Desktop/Firebase 에 Firebase Hosting 프로젝트를 만들고자 하면
cd desktop
cd Firebase
cd FirebaseHosting
로 Firebase Hosting 프로젝트을 설치할 폴더로 이동하면 된다.
자신이 만들고자하는 위치로 터미널에서 이동시킨다.
6. firebase login 으로 firebase 구글 로그인을 진행한다.
만약 로그인이 된 상태라면 위와 같이 과거에 로그인한 아이디가 나오고
아니면 새로운 로그인을 진행한다.
Firebase에서 CLI 사용량 및 오류 보고 정보를 수집하도록 허용하시겠습니까? Y or n 에서 선택하면 된다. 아무거나 선택해도 진행에 아무 영향 없다.
Y or N 를 선택 시 로그인 웹사이트가 뜬다.
Firebase를 설정해둔 구글 아이디로 로그인하면 된다.
아래와 같은 웹사이트 화면과 터미널 문구가 뜨면 로그인 완료가 된것이다.
7. 이 아래를 진행하기 전에 미리 자신의 Firebase console 에서 Firebase 기능들을 활성화 시켜줘야한다.
[Programming/Firebase] - [Firebase] 시작하기
[Programming/Firebase] - [Firebase] Web에서 Database 사용하기, [Programming/Firebase] - [Firebase] Web에서 Storage 사용하기 의 Database 설정 과 Storage 설정 을 참고하여 Database 와 Storage 를 활성화 시켜줘야한다.
활성화 단계까지만 진행하면 된다.
firebase init 을 실행한다.
Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
이 폴더에 대해 설정할 Firebase CLI 기능을 선택하십시오. Space를 눌러 피쳐를 선택한 후 Enter를 눌러 선택 사항을 확인하십시오.
Firebase Hosting 프로젝트가 설치될 폴더에서 사용할 Firebase 기능들을 활성화 후 Enter를 눌러 넘어간다. 이 기능들을 사용하려면 Firebase console 창에서 미리 기능활성화들을 해줘야한다.
위와 같이 선택 하였다.
본인이 사용할 기능들을 선택 후 Enter를 눌러주자.
8. Firebase project 를 선택해준다.
Use an existing project - 생성해둔 Firebase Project를 선택한다.
Create a new project - 새로운 Firebase Project를 생성한다.
미리 Firebase Project 를 세팅했다는 가정하에 Use an existing project 를 선택 후 Enter를 눌러준다.
Hosting을 진행할 Firebase Project를 선택 후 Enter를 누른다.
이 다음부터는 위에서 사용할 기능들을 설정해준다. 대부분 Firebase 에서 설정한대로 사용하면 된다.
Realtime Database 권한을 묻는 질문이다. Enter를 눌러 넘어간다.
Firestore 에 권한에 대한 질문이다. 2번 모두 기본 설정으로 넘어간다. Enter 눌러준다.
Firebase Functions 에 대한 질문이다.
node.js 를 호스팅할 예정이므로 JavaScript 를 선택한다.
차후 firebase deploy 를 할때 Eslint로 발생 가능한 버그를 파악하고 enforce style 를 적용할 것이냐는 질문이다 Yes 선택한다.
Firebase 기능을 사용하는데 필요한 node modules 를 설치할 것인지 물어본다. Yes 를 선택한다. firebase-functions 와 firebase-admin 이 설치된다.
Firebase Hosting 에 대한 질문이다.
Hosting 에 사용할 정적파일(HTML, CSS, JS) 소스 위치를 물어보는 질문이다. 기본 위치인 public 으로 사용할 예정이므로 Enter를 눌러 넘어간다.
Single-page app 으로 사용하기 위해 Hosting 으로 들어오는 모든 신호를 'index.html' 로 지정하겠냐는 질문이다. Single-page app 을 만들 예정이라면 Yes 를 선택한다. 아니라면 No 를 선택하면 된다.
Firebase Storage 에 대한 질문이다.
권한 파일에 대한 질문이다. 기본 설정을 사용하기 위해 Enter를 누른다.
밑과 같이 complete! 이 뜨면 설정과 Firebase Hosting 프로젝트 설치가 완료된것이다.
9. firebase serve 명령어로 제대로 설치가 되었는지 확인한다.
http://localhost:5000 로 접속한다.
위와 같이 Firebase Hosting Setup Complete 가 표시되면 제대로 설치된것이다.
Ctrl + C 로 서버를 종료한다.
10. Visual Studio Code 에서 Firebase Hosting 프로젝트가 설치된 폴더를 연다.
Firebase Hosting 프로젝트 폴더를 연 후 firebase.json 파일을 열어준다.
ignore 아래에
, "rewrites":[{
"source": "/**",
"function": "api1"
}
]
를 추가해준다.
추가 후 모습
{
"database": {
"rules": "database.rules.json"
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
// 여기부터
"rewrites":[{
"source": "/**",
"function": "api1"
}
]
// 여기까지
},
"storage": {
"rules": "storage.rules"
}
}
11. 터미널에서 Firebase Hosting 프로젝트 폴더 안에 있는 functions 폴더로 이동한다.
cd functions
functions 폴더 안에 있는 package.json 에 새로운 dependencies 를 추가해야한다.
functions 폴더로 이동시킨 터미널에서 아래 표에 적힌 node_module 들을 설치해준다.
설치 명령어 : sudo npm install node_module 이름 --save
node_module 이름 | 설명 |
cookie-parser | https://www.npmjs.com/package/cookie-parser |
debug | https://www.npmjs.com/package/debug |
ejs | https://www.npmjs.com/package/ejs |
express | https://www.npmjs.com/package/express |
http-errors | https://www.npmjs.com/package/http-errors |
morgan | https://www.npmjs.com/package/morgan |
ejs 3.0.1 버전에서 <% %> 가 작동안하는 버그가 있다. 버그가 없는 가장 최종버전인 2.7.4 버전을 사용하자.
sudo npm install ejs@2.7.4 --save
12. Firebase Hosting 프로젝트 폴더의 functions 폴더 안에 있는 index.js 파일의 내용을 변경해준다.
functions/index.js
////////
// 써드파티 미들웨어
////////
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const functions = require('firebase-functions');
var indexRouter = require('./main');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'view'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// 라우터 레벨 미들웨어
app.use('/main', indexRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
// firebase functions
const api1 = functions.https.onRequest(app);
module.exports = {
api1
};
Firebase Hosting 프로젝트 폴더의 functions 폴더 안에 main.js 파일도 새로 만든다.
functions/main.js
var express = require('express');
var cors = require('cors');
var router = express.Router();
// firebase Admin 써드파티 미들웨어
const admin = require('firebase-admin');
// firebase Admin 초기화
const firebaseAdmin = admin.initializeApp({
credential: admin.credential.cert({
// firebase admin sdk json 에 있는 키값들을 넣어준다.
}),
databaseURL:
storageBucket:
});
// firebase firestore 선언
let db = firebaseAdmin.firestore();
/* GET home page. 테스트용 코드 */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.get('/chat', (req, res, next) => {
console.log(req.query.test);
res.send("chatget send");
});
router.all('/chatall', (req, res, next) => {
console.log(req.body.test);
res.send("chatall send!");
//res.redirect('/');
});
router.post('/chatpost', (req, res, next) => {
console.log(req.body.test);
res.send("chatpost send!");
//res.redirect('/');
});
// 신규 유저 생성 후 DB 세팅 및 저장
router.post('/newUserCreate', (req, res, next) => {
const work = async () => {
var user = req.body;
// uid 가 없다면 오류 처리
if (!user.uid)
{
res.send(false);
return;
}
// cloud Firestore 에 새로운 유저 데이터 등록
db.collection('userData').doc(user.uid).set(user)
.then(() => {
// 성공시
console.log(user.uid + " 정보 생성완료!");
return res.send(true);
}, (err) => {
// 실패시
console.log(err);
return res.send(false);
});
}
work().catch((err) => {
console.log(err);
return res.send(err);
});
});
// 신규 유저 인벤토리 DB 세팅 및 저장
router.post('/newUserInvenCreate', (req, res, next) => {
const work = async () => {
var user = req.body;
// uid 가 없다면 오류 처리
if (!user.uid)
{
return res.send(false);
}
// cloud Firestore 에 새로운 유저 인벤토리 정보를 저장한다.
db.collection('userInventory').doc(user.uid).set(user)
.then(() => {
// 성공시
console.log(user.uid + " 인벤토리 생성완료!");
return res.send(true);
}, (err) => {
// 실패시
console.log(err);
return res.send(false);
});
}
work().catch((err) => {
console.log(err);
return res.send(err);
});
});
// 영수증 정보 저장
router.post('/userReceiptSave', (req, res) => {
const work = async () => {
try
{
var receiptData = req.body;
// 저장
// 중복의 영수증값이 있다면 덮어쓴다. (자동으로 덮어쓴다.)
db.collection('receipt').doc(receiptData.transaction_id).set(receiptData)
.then(() => {
console.log(req.body.transaction_id + " 결재 영수증 저장");
return res.send(true);
}, (err) => {
console.log(err);
return res.send(false);
});
} catch(err)
{
console.log(err);
return res.send(false);
}
}
work();
});
// 영수증 정보 갱신
router.post('/userReceiptUpdate', (req, res) => {
const work = async () => {
try
{
console.log(req.body.tid + " 갱신 시작");
// 특정 영수증의 transaction_id 를 기준으로 DB에서 검색 후 givecheck 를 0 -> 1로 수정한다.
doc = firebaseAdmin.firestore().collection('receipt').doc(req.body.tid).update({
givecheck: "1"
})
.then(() => {
return res.send(true);
})
.catch((err) =>
{
console.log(err);
return res.send(false);
});
}
catch(err)
{
console.log(err);
return res.send(false);
}
};
work();
});
// 유저 코인 정보를 갱신
router.post('/userRenewCoin', (req, res) => {
const work = async () => {
console.log(req.body.uid + " 코인 지급 완료");
var coinData = req.body;
coin = Number(coinData.coin);
// uid로 검색한 유저의 코인을 firebase 에서 지원하는 firestore.FieldValue.increment 함수를 이용하여
// 클라에서 보낸 코인 수 만큼 늘려준다.
db.collection('userInventory').doc(coinData.uid).update({
coin: admin.firestore.FieldValue.increment(coin)
}).then(() => {
// 성공시
console.log(coinData.uid + " 의 유저의 코인 증가 : " + coinData.coin);
return res.send(true);
}).catch((err) => {
console.log(err);
return res.send(false);
});
};
work();
});
// 특정 유저 데이터를 json으로 리턴해준다.
router.get('/userDataReturn', (req, res) => {
const work = async () => {
try
{
console.log(req.query.uid + " 유저 데이터 조회");
var uid = req.query.uid;
// uid를 이용하여 유저 정보를 db에서 검색한다.
db.collection('userData').doc(uid).get()
.then((snapshot) => {
var userData = snapshot.data();
if (!userData.uid)
{
return res.send(uid + " 유저 데이터 조회 실패");
}
return res.json(userData);
}).catch((err) => {
console.log(err);
return res.send(err);
});
}
catch(err)
{
console.log(err);
return res.send(err);
}
};
work();
});
// 특정 유저 인벤 정보를 json으로 리턴해준다.
router.get('/userInvenDataReturn', (req, res) => {
const work = async () => {
try
{
console.log(req.query.uid + " 유저 인벤 데이터 조회");
var uid = req.query.uid;
db.collection('userInventory').doc(uid).get()
.then((snapshot) => {
var userData = snapshot.data();
if (!userData.uid)
{
return res.send(uid + " 유저 데이터 조회 실패");
}
return res.json(userData);
}).catch((err) => {
console.log(err);
return res.send(err);
});
}
catch(err)
{
console.log(err);
return res.send(err);
}
};
work();
});
// 테스트용 DB 삭제
router.post('/testinit', (req, res) => {
console.log("Test Init Start...");
try
{
db.collection('userData').doc(req.body.uid).delete();
}
catch(err)
{
console.log(err);
res.send(false);
}
console.log(req.body.uid + " 삭제 완료");
res.send(true);
});
module.exports = router;
Firebase 키 값들 얻는 법 설명
비어져있는 키값 부분을 아래의 방법으로 키값을 구해 채워넣은 후 다음으로 진행하자.
firebase 키 값들 얻는 법
firebase overview 웹 화면에서 [프로젝트 설정] 을 눌러준다.
[일반] 탭을 누른 후 [내 앱] 항목의 자신의 웹 앱 프로젝트을 누른다.
그리고 Firebase SDK snippet 의 구성 값을 복사하여 main.js 의 "firebase 키 값" 부분에 넣어준다.
만약 자신의 웹 앱을 눌렀을때 Firebase SDK snippet 항목이 보이지 않는다면 잠시만 기다리면 등장한다.
[서비스 계정] 탭을 클릭 후 Firebase Admin SDK 의 [새 비공개 키 생성] 버튼을 클릭해준다.
json 파일이 하나 다운로드 받아지면 json 파일을 연 후 그 안의 내용을 복사하여 main.js 의 "firebase Admin 키 값" 부분에 붙여넣어준다.
위의 firebase 키 값에서 databaseURL: 과 storageBucket: 복사 한 후 firebase Admin 키 값 아래에 붙여넣어준다.
firebase 키 값들 얻는 법
firebase overview 웹 화면에서 [프로젝트 설정] 을 눌러준다.
[일반] 탭을 누른 후 [내 앱] 항목의 자신의 웹 앱 프로젝트을 누른다.
그리고 Firebase SDK snippet 의 구성 값을 복사하여 main.js 의 "firebase 키 값" 부분에 넣어준다.
만약 자신의 웹 앱을 눌렀을때 Firebase SDK snippet 항목이 보이지 않는다면 잠시만 기다리면 등장한다.
[서비스 계정] 탭을 클릭 후 Firebase Admin SDK 의 [새 비공개 키 생성] 버튼을 클릭해준다.
json 파일이 하나 다운로드 받아지면 json 파일을 연 후 그 안의 내용을 복사하여 main.js 의 "firebase Admin 키 값" 부분에 붙여넣어준다.
위의 firebase 키 값에서 databaseURL: 과 storageBucket: 복사 한 후 firebase Admin 키 값 아래에 붙여넣어준다.
13. firebase serve 를 통해 테스트한다.
터미널에서 자신의 firebase hosting 프로젝트 폴더/functions 폴더로 이동 후 sudo firebase serve 명령어를 실행시킨 후 브라우저 주소창에 "http://localhost:5000/main/chat" 로 들어가봅니다.
Test함수에 잘 접속이 된다면 현재 서버가 실행중인 터미널에서 Ctrl + C 를 눌러 서버 종료 후 sudo firebase deploy 를 실행하여 Firebase Hosting 에 배포한다.
(주소창으로 post 접속을 할 수 없기 때문에 post 접속은 오류가 발생한다.)
14. Firebase Hosting 에 firebase deploy 한다. firebase hosting project가 있는 위치에서 실행시켜야 한다.
제작한 서버가 Firebase Hosting에 deploy 된다.
Firebase Hosting 프로젝트를 만들때 Eslint 에 대한 설정을 Yes로 했다면 코드 스타일에 대한 오류가 발생할 수 있다. 당황하지말고 터미널에 적힌 오류에 대해서 코드 수정 후 다시 sudo firebase deploy 해준다.
Eslint 에 대한 스타일 오류일 경우 터미널에 수정 방법도 다 적혀있다.
Hosting URL : 에 뜬 URL 로 접속해보자. 차후에도 서버에 기능을 추가하고 firebase deploy 명령어를 사용해주면 된다.
Firebase Hosting 추가 설명
[Programming/Firebase] - [ Firebase] Hosting 사용하기
firebase cloud functions 에 대한 추가 설명
https://firebase.google.com/docs/functions/?hl=ko
Node.js 서버를 다 구축하고 Firebase Hosting 에 배포까지 완료했다면 Unity3d 로 다시 돌아간다.
서버와 통신 할 스크립트를 제작한다.
server.cs
server.cs 소스의 링크들에 적혀있는 firebase 도메인은 터미널에서 deploy 완료 후 나온 도메인 주소로 변경하거나 Firebase Hosting 메뉴에서 참고하여 변경하자. 변경 후 아래를 진행하자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System.Threading.Tasks;
using System;
public class Server : MonoBehaviour
{
// 스레드 작업에 안전한 싱글톤 패턴
private static bool shuttingDown = false;
private static object Lock = new object();
private static Server instance;
public static Server Instance
{
get
{
if (shuttingDown)
{
Debug.LogWarning("Server Instance already destroyed. return null");
return null;
}
lock (Lock)
{
if (instance == null)
{
instance = (Server)FindObjectOfType(typeof(Server));
if (instance == null)
{
var serverObject = new GameObject();
instance = serverObject.AddComponent<Server>();
serverObject.name = "ServerManager";
DontDestroyOnLoad(serverObject);
}
}
return instance;
}
}
}
// 영수증 저장이 될때까지 갱신을 대기 트리거
private static bool receiptSave = false;
// 어플리케이션이 꺼지면....
private void OnApplicationQuit()
{
shuttingDown = true;
}
// 파괴되면....
private void OnDestroy()
{
shuttingDown = true;
}
private void Awake()
{
DontDestroyOnLoad(this.gameObject);
}
/* 테스트 용 함수들 */
public void Get()
{
StartCoroutine(SendGet());
}
private IEnumerator SendGet()
{
#if UNITY_EDITOR
using (UnityWebRequest www = UnityWebRequest.Get("http://localhost:3000/chat?test=asd"))
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
// Firebase Hosting 후 발급받는 도메인을 입력한다.
using (UnityWebRequest www = UnityWebRequest.Get("firebase도메인/chat?test=asd"))
#endif
{
yield return www.SendWebRequest();
if (www.isNetworkError)
{
Debug.Log("Error While Sending: " + www.error);
}
else
{
Debug.Log("Received: " + www.downloadHandler.text);
}
}
}
public void Post()
{
StartCoroutine(SendPost());
}
private IEnumerator SendPost()
{
WWWForm form = new WWWForm();
form.AddField("test", "myPostTest");
#if UNITY_EDITOR
using (UnityWebRequest www = UnityWebRequest.Post("http://localhost:3000/chatpost", form))
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
// Firebase Hosting 후 발급받는 도메인을 입력한다.
using (UnityWebRequest www = UnityWebRequest.Post("firebase도메인/chatpost", form))
#endif
{
yield return www.SendWebRequest();
if (www.isNetworkError)
{
Debug.Log("Error While Sending: " + www.error);
}
else
{
Debug.Log("Received: " + www.downloadHandler.text);
}
}
}
/* 테스트 함수 끝 */
// 현재 클라이언트에 접속된 유저 UID 로 서버 DB에 저장된 유저 데이터를 가지고 온다.
public void GetUserDataDB(string uid, Action callback)
{
try
{
#if UNITY_EDITOR
StartCoroutine(UserDataGet("http://localhost:5000/main/userDataReturn?uid=", uid, callback));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(UserDataGet("firebase도메인/main/userDataReturn?uid=", uid, callback));
#endif
}
catch (Exception err)
{
Debug.Log(err);
}
}
private IEnumerator UserDataGet(string url, string uid, Action callback)
{
using (UnityWebRequest www = UnityWebRequest.Get(url + uid))
{
yield return www.SendWebRequest();
if (www.isNetworkError)
{
Debug.Log("Error While Sending: " + www.error);
}
else
{
Debug.LogFormat("Received: ({0}) {1}", url, www.downloadHandler.text);
var user = JsonUtility.FromJson<User.userLoginData>(www.downloadHandler.text);
Debug.Log(user);
User.Instance.mainUser = user;
callback();
}
}
}
// 현재 클라이언트에 접속된 유저 UID로 서버 DB에 저장된 유저 인벤 정보를 가지고 온다.
public void GetUserInvenDB(string uid, Action callback)
{
try
{
#if UNITY_EDITOR
StartCoroutine(UserInvenDataGet("http://localhost:5000/main/userInvenDataReturn?uid=", uid, callback));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(UserInvenDataGet("firebase도메인/main/userInvenDataReturn?uid=", uid, callback));
#endif
}
catch (Exception err)
{
Debug.Log(err);
}
}
private IEnumerator UserInvenDataGet(string url, string uid, Action callback)
{
using (UnityWebRequest www = UnityWebRequest.Get(url + uid))
{
yield return www.SendWebRequest();
if (www.isNetworkError)
{
Debug.Log("Error While Sending: " + www.error);
}
else
{
Debug.LogFormat("Received: ({0}) {1}", url, www.downloadHandler.text);
var user = JsonUtility.FromJson<User.userGoodsData>(www.downloadHandler.text);
Debug.Log(user);
User.Instance.mainInventory = user;
callback();
}
}
}
// 새로 생성된 유저 데이터(json)을 서버에 저장한다.
public void NewUserJsonDBSave(string json, Action callback = null)
{
try
{
#if UNITY_EDITOR
StartCoroutine(JsonDBSavePost("http://localhost:5000/main/newUserCreate", json, callback));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(JsonDBSavePost("firebase도메인/main/newUserCreate", json, callback));
#endif
}
catch (Exception err)
{
Debug.Log(err);
}
}
// 영수증 정보 저장
public void ReceiptSave(string json)
{
try
{
#if UNITY_EDITOR
StartCoroutine(JsonDBSavePost("http://localhost:5000/main/userReceiptSave", json));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(JsonDBSavePost("firebase도메인/main/userReceiptSave", json));
#endif
}
catch (Exception err)
{
Debug.LogError(err);
}
}
// 새로운 유저 인벤토리 생성
public void NewUserInventoryJsonDBSave(string json, Action callback = null)
{
try
{
#if UNITY_EDITOR
StartCoroutine(JsonDBSavePost("http://localhost:5000/main/newUserInvenCreate", json, callback));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(JsonDBSavePost("firebase도메인/main/newUserInvenCreate", json, callback));
#endif
}
catch (Exception err)
{
Debug.LogError(err);
}
}
private IEnumerator JsonDBSavePost(string url, string json, Action callback = null)
{
using (var uwr = new UnityWebRequest(url, "POST"))
{
Debug.Log(json);
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
uwr.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
uwr.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
uwr.SetRequestHeader("Content-Type", "application/json");
yield return uwr.SendWebRequest();
if (uwr.isNetworkError)
{
Debug.Log("Error While Sending: " + uwr.error);
}
else
{
Debug.LogFormat("Received: ({0}) {1}", url, uwr.downloadHandler.text);
receiptSave = bool.Parse(uwr.downloadHandler.text);
if (callback != null)
callback();
}
}
}
// 결제 아이템이 지급이 되면 영수증 id로 검색 후 지급명령 체크
private IEnumerator ReceiptGiveCheckDB(string url, string uid, string tid, string giveCheck)
{
WWWForm form = new WWWForm();
form.AddField("uid", uid);
form.AddField("tid", tid);
form.AddField("givecheck", giveCheck);
using (var uwr = UnityWebRequest.Post(url, form))
{
yield return uwr.SendWebRequest();
if (uwr.isNetworkError)
{
Debug.Log("Error While Sending: " + uwr.error);
}
else
{
Debug.LogFormat("Received: ({0}) {1}", url, uwr.downloadHandler.text);
}
}
}
// 유저 코인의 양을 증가시킨다.
public void userCoinMount(string json)
{
try
{
#if UNITY_EDITOR
StartCoroutine(JsonDBSavePost("http://localhost:5000/main/userRenewCoin", json));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(JsonDBSavePost("firebase도메인/main/userRenewCoin", json));
#endif
}
catch (Exception err)
{
Debug.LogError(err);
}
}
// 구매 후 서버 DB의 유저 코인 정보를 갱신한다.
public void RenewUserCoin(string uid, int coin, string tid)
{
try
{
#if UNITY_EDITOR
StartCoroutine(userRenewCoin("http://localhost:5000/main/userRenewCoin", uid, coin, tid));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(userRenewCoin("firebase도메인/main/userRenewCoin", uid, coin, tid));
#endif
}
catch (Exception err)
{
Debug.LogError(err);
}
}
private IEnumerator userRenewCoin(string url, string uid, int coin, string tid)
{
WWWForm form = new WWWForm();
form.AddField("uid", uid);
form.AddField("coin", coin);
using (var uwr = UnityWebRequest.Post(url, form))
{
yield return uwr.SendWebRequest();
if (uwr.isNetworkError)
{
Debug.Log("Error While Sending: " + uwr.error);
}
else
{
Debug.LogFormat("Received: ({0}) {1}", url, uwr.downloadHandler.text);
if (bool.Parse(uwr.downloadHandler.text))
{
if (!receiptSave)
{
StartCoroutine(waitSave(uid, tid));
}
if (receiptSave)
{
#if UNITY_EDITOR
StartCoroutine(ReceiptGiveCheckDB("http://localhost:5000/main/userReceiptUpdate", uid, tid, "1"));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(ReceiptGiveCheckDB("firebase도메인/main/userReceiptUpdate", uid, tid, "1"));
#endif
receiptSave = false;
}
else
{
Debug.Log("영수증 정보를 DB에 저장하지 못했습니다!");
}
}
}
}
}
private IEnumerator waitSave(string uid, string tid)
{
int count = 0;
while (!receiptSave)
{
yield return new WaitForSeconds(0.1f);
if (receiptSave)
break;
count++;
if (count > 10)
break;
}
if (receiptSave)
{
#if UNITY_EDITOR
StartCoroutine(ReceiptGiveCheckDB("http://localhost:5000/main/userReceiptUpdate", uid, tid, "1"));
#endif
#if UNITY_ANDROID && !UNITY_EDITOR
StartCoroutine(ReceiptGiveCheckDB("firebase도메인/main/userReceiptUpdate", uid, tid, "1"));
#endif
receiptSave = false;
}
else
{
Debug.Log("영수증 갱신 실패!");
}
}
public void TestInit(string uid)
{
StartCoroutine(instance.Init(uid));
}
// 테스트 초기화
private IEnumerator Init(string uid)
{
WWWForm form = new WWWForm();
form.AddField("uid", uid);
using (UnityWebRequest www = UnityWebRequest.Post("firebase도메인/main/testinit", form))
{
yield return www.SendWebRequest();
if (www.isNetworkError)
{
Debug.Log("Error While Sending: " + www.error);
}
else
{
Debug.Log("Received: " + www.downloadHandler.text);
}
}
}
}
Unity3d Scene(AuthWebServer Scene)에서 Missing Script 이 난 GameObject들에게 새로 생성한 스크립트들을 연결해준다.
LoginManager
User
ServerManager
데스크탑 에디터에서는 구글 로그인은 작동하지 않기 때문에 (Null 에러가 뜬다.) APK 에서만 구글 로그인 기능 테스트를 할 수 있다.
일단 play버튼을 눌러 실행 시키고 로그인 진행을 터치한 후 익명 로그인을 진행한다.
닉네임 입력창이 나온 후 Firebase Console 의Authentication 항목을 확인하면 새로운 계정이 하나 추가된 것을 알 수 있다.
아직 닉네임 만들기는 에러가 발생하므로 넘어가자.
마지막으로 인앱 결제를 처리해주는 Scene을 만드는 작업만 남았다.
인앱 결제 설정
Google Play Console 에 접속한 후 새로운 애플리케이션을 만든다.
다른 설정은 이따가 작성하도록 하고 서비스 및 API - 라이선스 및 인앱 결제 - 라이선스 키 복사를 한다.
다시 Unity3d 프로젝트로 돌아온 후 Window - General - Services 를 연다.
연결이 안된 경우 생성을 진행해서 연결해준다.
In-App Purchasing 기능을 켜준다.
Enable 버튼을 눌러 기능 사용 가능하게 합니다.
체크를 하지말고 Continue 를 눌러준다. 만약 13세 이하를 대상으로 하는 앱이면 체크 후 진행하면 된다.
import 버튼을 눌러준다. (import 후 사진을 찍어 Reimport로 나온다..)
전부 import 한다. 진행하던 도중 API Update Required 창이 뜨면 I Made a Backup. Go Ahead! 를 누른다.
window - Unity IAP - Receipt Validation Obfuscator 누른다.
파란색 상자 위치에 위에서 복사한 구글 라이선스 키를 넣어준다. (1.23.1 버전 기준 Project Secret Key을 입력하는 칸이 사라졌다. 입력하지 않아도 작동은 잘 된다.)
빨간색 상자 버튼을 눌러 키 난독화를 적용하고
초록색 상자 를 눌러 Analytics Dashboard에 접속한다.
Google License Key 상자 안에 위에서 붙여넣은 구글 라이센스 키를 붙여넣은 후 저장을 한다.
다시 Unity3d로 돌아와 아래의 칸에 다시 구글 라이센스 키를 붙여넣은 후 Verify 누른다.
만약 아래와 같이 Update 버튼이 안 나타나고 오류가 발생한다면 Receipt Validation Obfuscator 를 눌러 창을 열어서 다시 순서대로 세팅 해준다.
그런 후 인앱 결제를 진행할 Scene을 하나 만든다. 간단한 UI들이 필요하므로 아래 Scene을 다운받아 사용해도 된다.
IAPManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using UnityEngine.Purchasing.Security;
using UnityEngine.Analytics;
using UnityEngine.UI;
using System.Threading.Tasks;
public class IAPManager : MonoBehaviour, IStoreListener
{
static IStoreController storeController = null;
[SerializeField] Text txtCoin;
int nCoin;
[SerializeField] Text txtLog;
public bool isPurchaseUnderProcess = true;
// 결재 품목 목록
static string[] sProductlds;
private void Awake()
{
if (storeController == null)
{
// 인앱상품 - 관리되는 제품의 ID
sProductlds = new string[] { "coin_1000", "coin_5000" };
InitStore();
}
nCoin = 0;
}
private void Start()
{
// 결제 복구 로직
// 빠른 테스트를 위해 Start() 에서 실행한다.
#if UNITY_ANDROID && !UNITY_EDITOR
ReleaseAllUnfinishedUnityIAPTransactions();
#endif
}
// 결제 상품 초기화
void InitStore()
{
ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
builder.AddProduct(sProductlds[0], ProductType.Consumable, new IDs { { sProductlds[0], GooglePlay.Name } });
builder.AddProduct(sProductlds[1], ProductType.Consumable, new IDs { { sProductlds[1], GooglePlay.Name } });
UnityPurchasing.Initialize(this, builder);
}
// 초기화 완료시...
void IStoreListener.OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
storeController = controller;
txtLog.text = "결제 기능 초기화 완료";
Debug.Log(txtLog.text);
}
// 초기화 실패시...
void IStoreListener.OnInitializeFailed(InitializationFailureReason error)
{
txtLog.text = "OnlnitializeFailed " + error;
Debug.Log(txtLog.text);
}
// 인앱 상품 결제를 눌렀을 경우...
public void OnBtnPurchaseClicked(int index)
{
if (storeController == null)
{
txtLog.text = "구매 실패 : 결제 기능 초기화 실패";
Debug.Log(txtLog.text);
}
else
storeController.InitiatePurchase(sProductlds[index]);
}
// 결제 진행...
PurchaseProcessingResult IStoreListener.ProcessPurchase(PurchaseEventArgs e)
{
bool isSuccess = true;
string transactionId = string.Empty;
#if UNITY_ANDROID && !UNITY_EDITOR
CrossPlatformValidator validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
try
{
// 클라이언트 영수증 검산
IPurchaseReceipt[] result = validator.Validate(e.purchasedProduct.receipt);
// 클라이언트 검산 후 클라이언트 검산 데이터를 등록한다.
foreach(var productReceipt in result)
{
// https://docs.unity3d.com/kr/530/ScriptReference/Analytics.Analytics.Transaction.html
Analytics.Transaction(productReceipt.productID, e.purchasedProduct.metadata.localizedPrice, e.purchasedProduct.metadata.isoCurrencyCode, productReceipt.transactionID, null);
// 영수증을 서버에 저장한다.
// 차후 복구 로직을 테스트할때 편하게 하기 위해 givecheck 변수를 만들어 관리한다.
// 등록시 지급됨 체크 변수를 0으로 준다.
Server.Instance.ReceiptSave(JsonUtility.ToJson(CreateReceipt(productReceipt, e.purchasedProduct.metadata.localizedPrice.ToString())));
// id 백업
transactionId = productReceipt.transactionID;
// OS 별로 특정 세부 사항이 있다
// https://docs.huihoo.com/unity/5.4/Documentation/en/Manual/UnityIAPValidatingReceipts.html
//var google = productReceipt as GooglePlayReceipt;
//var apple = productReceipt as AppleInAppPurchaseReceipt;
}
}
catch (IAPSecurityException)
{
isSuccess = false;
}
#endif
// 복구 테스트
if (!isPurchaseUnderProcess)
{
Debug.Log("pending Test");
txtLog.text = "구매 복구 테스트 시작";
// 강제 결제 복수 테스트를 위해 무조건 결제가 완료가 안되게 한다.
return PurchaseProcessingResult.Pending;
}
else
{
// 정상 결제 서버로직
if (isSuccess)
{
if (e.purchasedProduct.definition.id.Equals(sProductlds[0]))
{
GiveLogic(1000, transactionId);
}
else if (e.purchasedProduct.definition.id.Equals(sProductlds[1]))
{
GiveLogic(5000, transactionId);
}
}
else
{
txtLog.text = "구매 실패 : 비정상 결제";
Debug.Log(txtLog.text);
}
// 결제 완료
return PurchaseProcessingResult.Complete;
}
}
void IStoreListener.OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
if (!p.Equals(PurchaseFailureReason.UserCancelled))
{
txtLog.text = "구매 실패 : " + p;
Debug.Log(txtLog.text);
}
}
// 서버에 보낼 영수증 데이터를 가공
public User.userReceipt CreateReceipt(IPurchaseReceipt rec, string price)
{
var userReceipt = new User.userReceipt();
userReceipt.uid = User.Instance.mainUser.uid;
userReceipt.transaction_id = rec.transactionID;
userReceipt.product_id = rec.productID;
userReceipt.platform = User.Instance.mainUser.deviceOS;
userReceipt.price = price;
userReceipt.date = rec.purchaseDate.ToString("yyyy/MM/dd HH:mm:ss");
userReceipt.givecheck = "0";
return userReceipt;
}
// 구매 복구
// 모든 인앱 상품들에게 너 지금 Pending 상태 인지 물어본다. 만약 pending 상태인 상품이 있다면 다시 결재를 진행한다.
// 편안한 테스트를 위해 버튼으로 작동하게 한다.
public void ReleaseAllUnfinishedUnityIAPTransactions()
{
foreach (string productId in sProductlds)
{
Product p = storeController.products.WithID(productId);
isPurchaseUnderProcess = true;
if (p != null)
storeController.ConfirmPendingPurchase(p);
}
}
// Peding 으로 가게 설정
public void Pending()
{
isPurchaseUnderProcess = !isPurchaseUnderProcess;
Debug.Log("Pending: " + !isPurchaseUnderProcess);
txtCoin.text = "Pending: " + !isPurchaseUnderProcess;
}
// 클라이언트 코인 지급을 처리한다.
void AddCoin(int value)
{
txtLog.text = "AddCoin : " + value;
Debug.Log(txtLog.text);
nCoin += value;
txtCoin.text = "Coin : " + nCoin.ToString("NO");
int result = User.Instance.mainInventory.coin + value;
User.Instance.mainInventory.coin = result;
}
// 코인 지급 로직
void GiveLogic(int value, string tid)
{
// 코인 지급
AddCoin(value);
// 지급한 코인 서버에 저장
// 서버에 저장 후 코인 갯수 갱신
// 코인지급이 끝난 후 givecheck를 1로 변경해준다.
Server.Instance.RenewUserCoin(User.Instance.mainUser.uid, value, tid);
}
}
IAPManager 스크립트를 만든 후 IAPManager GameObject 에 넣어준다.
Text 컴포넌트들을 채워준다.
실행을 하여 인앱 결제가 잘 초기화 되는지 확인한다. 에디터에서는 무조건 결제가 성공한다.
오류없이 잘된다면 /Assets/Plugins/Android/Firebase 에 위치한 AndroidManifest.xml 을 수정해줘야한다.
파일을 연 후 <uses-permission android:name="com.android.vending.BILLING"/> 을 추가하여 권한을 준다.
AndroidManifest 까지 수정 하였다면 Android APK 를 뽑아주자.
뽑은 APK 를 구글 개발자 콘솔에서 내부 테스트 트랙으로 출시하자.
앱 버전 - 내부 테스트 - 관리 클릭
아까 빌드한 APK 파일을 드래그하여 넣는다.
저장 클릭
스토어 등록정보를 대충 입력해준다.
스토어 이미지를 등록해준다. 첨부 파일을 이용하여 채워넣자. 게임 스크린샷의 경우 최소 2장을 넣어줘야한다. 같은 이미지를 넣어줘도 상관없다.
대충 선택 후 임시 저장을 눌러준다.
콘텐츠 등급을 설정해준다. 이메일을 넣고 쭈욱 아니오를 체크하고 등급을 받는다.
가격 및 배포는 배포 방법은 무료, 모든 국가를 사용가능으로 설정, 광고 미포함, 콘텐츠 가이드 체크, 미국 수출 법규 체크 후 저장한다.
앱 콘텐츠는 시작을 누른 후 대상 연령층 선택 후 아이들 관심 유도 아니오 선택 후 제출을 한다.
모든 체크 표시를 채우고 기다리면 출시가 된다. 출시대기에서 출시됨으로로 변하려면 하루정도 기다려야한다.
기다리는 동안 인앱 상품 세팅을 하자.
인앱 상품 - 관리되는 제품 으로 들어간다.
coin_1000 과 coin_5000 2개의 인앱 상품을 만들면 된다.
관리되는 제품 만들기 클릭
coin_1000 과 coin_5000 을 만들어준다.
가격을 입력 후 적용 후 저장까지 클릭한다.
추가하고자 하는 상품을 모두 추가하면 앱이 출시가 완료될때까지 기다리면된다.
출시가 되지 않은 상태에서는 인앱 상품 목록을 받지 못하기 때문에 출시가 완료된 후 테스트를 할 수 있다.
에디터에서 진행하는 인앱 결제 테스트는 페이크 결제로 항상 성공한다.
출시 후 처음부터 끝까지 테스트를 하면 아래와 같은 결과값이 나온다.
Google Login 시...
User 정보
User 결제 데이터
User 인벤토리 정보
'Programming > Firebase' 카테고리의 다른 글
[Firebase] Push (Cloud Messaging) 사용하기 (0) | 2019.12.13 |
---|---|
[Firebase] Firebase Hosting 기본 제작 (2) | 2019.12.05 |
[Firebase][과거] 목차 (0) | 2019.11.28 |
[Firebase] Unity3d에서 시작하기 (0) | 2019.11.26 |
[Firebase] Hosting 사용하기 (0) | 2019.11.26 |
최근댓글