目录---谷歌N部曲:
1.设置Splash Image
2.如何打安卓包才能上架google play?
3.Google Develop 后台创建APP
4.如何接入谷歌SDK,登录以及排行榜?
5.如何接入谷歌支付?
QA:
1.设置Splash Image
这个图片就是unity2019.3以后的Project Setting界面,且为安卓平台。
Virtual Reality Splash Image,这个应该是VR开机的画面,不用管
Splash Screen,开机画面设置选项
a.Splash Stype,一个是白色一个是黑色
b.Animation,设置动画模式,一个是没有动画,一个是慢慢放大
c.Show Unity Logo,顾名思义,不过个人版不能设置
(这里推荐不要取消啊,unity就是很牛逼的,就像你穿衣服前面有nike一样~)
d.Draw Mode,就可以设置显示的方式,一个是同屏上下,一个是依次显示
Background,就是设置背景图的,需要可以设置,我没用过觉得黑白背景就够了
Static Splash Image,静态开机图片,可以自己设置缩放模式,也不错
2.如何打安卓包并上架google play?
第一步
打安卓包需要先下载三个DK:JDK、SDK、NDK(打IL2CPP用的)
unity2019有个坑,就是指定ndk后,说版本对不上
这时候不要慌,只需要打开文件夹下的 source.properties
修改这里面的Revision的数值即可。
第二步,勾选IL2CPP
谷歌有规定,必须要支持目标架构64位的,那么唯一选择就只能是IL2CPP了
这样做也有好处,可以防破解
(不过现在下载的中国增强版都有混淆功能,不知道啥时候能给个热更)
如何配置呢?
注意:目标架构ARMv7和ARM64都要勾选。
第三步,签名
需要创建一个keystore,并且一直用这个keystore签名打包
(什么是keystore?怎么创建?可以百度一下)
上传到谷歌商店会对这个进行二次签名与验证,所以必须要保证每次上传的包都是这个签名才行
(注:这里需要勾选 Custom Gradle Template,接sdk需要配置gradle文件)
第四步,设置为aab
aab是谷歌推荐上传的文件,可以自动优化包体大小
(非要用apk也是可以的)
这样导出的包 就能上传到google play里面了。
3.Google Develop 后台创建APP
首先需要创建一个谷歌开发者账号,需要刷信用卡 25美刀,没有的可以TB代刷,不过有手续费~
后台界面如图:
所有应用,是用来看你的app的
游戏服务,是用来接比如成就排行榜啥的
首先需要创建应用:
输入默认语言和名称,没有ios那么坑爹,直接输入创建就可以
创建后界面如图:
若要发布,需要先填写左边四个灰色的对号那几个选项
都填写完了应该就变成绿色了
填表 大家应该都会的吧 就不给演示了
(坑:隐私权可以百度一下,有个五分钟搞定谷歌隐私权的文章)
(其他的都还好吧,有问题可以留言)
主要说一下应用版本:
正式版,就是对所有谷歌用户都能下载
开放式渠道,就是能在谷歌商店通过连接下载,可以设置最大测试人数,属于开放式测试
(推荐用这个测试啊)
封闭式渠道,就是只能对特定的测试id下载
内部测试渠道,估计也是特定的设备才能下载测试吧,没用过忘了
推荐使用开放式渠道啊,方便快捷,除非你项目这么叼,一般发布了也没人去下载你的
填写完版本后,然后填好测试用户的邮箱啥的,就点保存
然后就出现了查看按钮,点击就跳转个界面,就有个发布按钮,点击发布按钮就发布了
就是上面的两个界面,我都发布了不能截图了,不过很简单的啊~~
审核:
第一次发布需要3-5个工作日左右吧,不算周末
(不过现在疫情严重需要推迟一周,祝疫情早日过去)
之后每次小版本更新发布就快了~
谷歌不像苹果那么多事情,一般都会审核通过的~
4.如何接入谷歌SDK,登录以及排行榜?
首先需要下载谷歌SDK,unity这么牛逼,当然有支持unitypackage的包了,省了不少事情
下载地址:谷歌插件的GIT地址
GIT里面已经有很详细的教程了,在这里就记录几个坑就行了:
前提:必须要从谷歌商店下载的包才能正常登录,以及获取排行榜啥的,所以就需要你上传一个内测版本再测试
1.最新谷歌服务都采用了androidX了,和原来的android Suppoer库不能共存,假如你接了别的sdk,需要统一使用androidX的包才行
提供几个代码仅供参考:
gradle内:
([rootProject] + (rootProject.subprojects as List)).each {
ext {
it.setProperty("android.useAndroidX", true)
it.setProperty("android.enableJetifier", true)
}
}
或者
项目的Asset/Plugins/Editor下:
using System.IO;
using UnityEditor.Android;
using UnityEngine;
public class SupportAndroidXGradlePropertiesBuildProcessor : IPostGenerateGradleAndroidProject
{
public int callbackOrder
{
// 同种插件的优先级
get { return 999; }
}
public void OnPostGenerateGradleAndroidProject(string path)
{
Debug.Log("Bulid path : " + path);
string gradlePropertiesFile = path + "/../gradle.properties";
if (File.Exists(gradlePropertiesFile))
{
File.Delete(gradlePropertiesFile);
}
StreamWriter writer = File.CreateText(gradlePropertiesFile);
writer.WriteLine("org.gradle.jvmargs=-Xmx4096M");
writer.WriteLine("android.useAndroidX=true");
writer.WriteLine("android.enableJetifier=true");
writer.Flush();
writer.Close();
}
}
2.赠送一个排行榜的代码,之前接的时候写的
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ILeaderboardControl
{
bool authenticated
{
get;
}
void Authenticate();
void HandleAuthenticated(bool success);
void ShowLeaderboardUI();
void ReportScore(int score, int levelId);
void HandleScoreReported(bool success);
}
public class LeaderboardControl : Singleton<LeaderboardControl>, ILeaderboardControl
{
public LeaderboardControl()
{
}
public bool successed
{
get; private set;
}
public bool authenticated
{
get
{
return Social.localUser.authenticated;
}
}
public void Authenticate()
{
#if UNITY_EDITOR
return;
#endif
#if UNITY_ANDROID
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder().Build();
PlayGamesPlatform.InitializeInstance(config);
#if TEST
PlayGamesPlatform.DebugLogEnabled = true;
#endif
PlayGamesPlatform.Activate();
#endif
Social.localUser.Authenticate(this.HandleAuthenticated);
}
public void HandleAuthenticated(bool success)
{
if (success)
{
string userInfo = "Username: " + Social.localUser.userName + ", User ID: " + Social.localUser.id;
Logger.Log("*** HandleAuthenticated: success , userInfo : " + userInfo);
#if UNITY_ANDROID
((GooglePlayGames.PlayGamesPlatform)Social.Active).SetGravityForPopups(Gravity.TOP);
#endif
}
else
{
Logger.Log("*** HandleAuthenticated: failed !!!");
}
this.successed = success;
}
public void ShowLeaderboardUI()
{
#if UNITY_EDITOR
return;
#endif
if (!successed)
{
Logger.Log("*** Try to Authenticate...");
Social.localUser.Authenticate(this.HandleAuthenticated);
}
if (this.authenticated && this.successed)
{
#if UNITY_ANDROID
((PlayGamesPlatform)Social.Active).ShowLeaderboardUI();
#else
Social.ShowLeaderboardUI();
#endif
}
}
public void ReportScore(int score, int levelId)
{
#if UNITY_EDITOR
return;
#endif
string boardId = string.Empty;
#if UNITY_ANDROID
string[] googleBoardIds = { GPGSIds.leaderboard_challenge_room_1, GPGSIds.leaderboard_challenge_room_2, GPGSIds.leaderboard_challenge_room_3 };
boardId = googleBoardIds[levelId];
#elif UNITY_IOS
string appleBoardIds = "challenge";
boardId = appleBoardIds + levelId;
#endif
if (this.authenticated && this.successed)
{
Social.ReportScore(score, boardId, this.HandleScoreReported);
}
}
public void HandleScoreReported(bool success)
{
Logger.Log("*** HandleScoreReported: " + success);
}
}
5.如何接入谷歌支付?
谷歌和苹果支付只需要用Unity的IAP就可以
在PackangeManager里面下载,导入 In App Purchasing 插件
另外还需要点开Service窗口,需要开启Analytics和In-APP Purchasing
坑总结:
1.Service窗口打不开,可能是unity的版本不行,我之前2019.3.0就不行,2019.3.2就可以
2.Analytics需要先点一下运行开 传一下假数据,如果不行请多试几次
成功后就不是这个界面了,那就代表成功了
3.添加In-App Purchasing信息
注:必须要先开启Analytics,并且成功传输数据了,就是先满足条件2才可以,才能填写Google Public Key
然后点击Import即可。
给个代码参考一下:
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Purchasing;
using UnityEngine.UI;
public class IAPTools : MonoSingleton<IAPTools>, IStoreListener
{
private static IStoreController m_StoreController; // 存储商品信息;
private static IExtensionProvider m_StoreExtensionProvider; // IAP扩展工具;
private bool m_PurchaseInProgress = false; // 是否处于付费中;
public enum EProductType
{
xx,
ITEM_COUNT = 1
}
public void Init()
{
if (m_StoreController == null && m_StoreExtensionProvider == null)
InitUnityPurchase();
}
private bool IsInitialized()
{
return m_StoreController != null && m_StoreExtensionProvider != null;
}
// 初始化IAP;
public void InitUnityPurchase()
{
if (IsInitialized())
return;
// 标准采购模块;
StandardPurchasingModule module = StandardPurchasingModule.Instance();
// 配置模式;
ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);
// 注意ProductType的类型,Consumable是可以无限购买(比如水晶),NonConsumable是只能购买一次(比如关卡),Subscription是每月订阅(比如VIP);
// 这里初始化没有添加平台信息,因为平台信息有的时候还存在bug,如果必须添加,也可以添加,没有问题,确保平台信息添加正确就行了,如下:
// builder.AddProduct(C_ITEM_0, ProductType.Consumable, new IDs
// {
// {"yourProductName", AppleAppStore.Name},
// {"yourProductName", GooglePlay.Name},
// });
builder.AddProduct(你的id, ProductType.NonConsumable);
builder.AddProduct(你的id, ProductType.Consumable);
//初始化;
UnityPurchasing.Initialize(this, builder);
}
#region Public Func
//根据类型购买商品:
public void BuyProductByID(IAPTools.EProductType productType)
{
if (IsInitialized())
{
if (m_PurchaseInProgress == true)
return;
string productId = productType.ToString();
Product product = m_StoreController.products.WithID(productId);
if (product != null && product.availableToPurchase)
{
Logger.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
m_StoreController.InitiatePurchase(product);
m_PurchaseInProgress = true;
}
else
{
Logger.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
}
}
else
{
Logger.Log("BuyProductID FAIL. Not initialized.");
}
}
// 确认购买产品成功;
public void DoConfirmPendingPurchaseByID(string productId)
{
Product product = m_StoreController.products.WithID(productId);
if (product != null && product.availableToPurchase)
{
if (m_PurchaseInProgress)
{
m_StoreController.ConfirmPendingPurchase(product);
m_PurchaseInProgress = false;
}
}
}
// 恢复购买;
//此物品已经购买成功了,但是苹果没有接受到app的消息 这个时候需要执行
//他会把你没有解决的订单和一次性购买项目再次拉去一遍,他会执行 ProcessPurchase 这个回调函数
public void RestorePurchases()
{
if (!IsInitialized())
{
Logger.Log("RestorePurchases FAIL. Not initialized.");
return;
}
if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer)
{
Logger.Log("RestorePurchases started ...");
var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
apple.RestoreTransactions((result) =>
{
// 返回一个bool值,如果成功,则会多次调用支付回调,然后根据支付回调中的参数得到商品id,最后做处理(ProcessPurchase);
Logger.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
});
}
else
{
Logger.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
}
}
#endregion
#region IStoreListener Callback
// IAP初始化成功回掉函数;
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
Logger.Log("OnInitialized Succ !");
m_StoreController = controller;
m_StoreExtensionProvider = extensions;
// 这里可以获取您在AppStore和Google Play 上配置的商品;
//ProductCollection products = m_StoreController.products;
//Product[] all = products.all;
//for (int i = 0; i < all.Length; i++)
//{
// Logger.Log(all[i].metadata.localizedTitle + "|" + all[i].metadata.localizedPriceString + "|" + all[i].metadata.localizedDescription + "|" + all[i].metadata.isoCurrencyCode);
//}
#if UNITY_IOS
m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
#endif
}
// IAP初始化失败回掉函数(没有网络的情况下并不会调起,而是一直等到有网络连接再尝试初始化);
public void OnInitializeFailed(InitializationFailureReason error)
{
switch (error)
{
case InitializationFailureReason.AppNotKnown:
Logger.Log("Is your App correctly uploaded on the relevant publisher console?", Logger.LogLevel.Error);
break;
case InitializationFailureReason.PurchasingUnavailable:
Logger.Log("Billing disabled! Ask the user if billing is disabled in device settings.");
break;
case InitializationFailureReason.NoProductsAvailable:
Logger.Log("No products available for purchase! Developer configuration error; check product metadata!");
break;
}
}
//外部监听回调事件
public Action<IAPTools.EProductType> onPurchaseSuccess;
public Action onPurchaseFail;
// 支付成功处理函数;
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
Logger.Log("Purchase OK: " + e.purchasedProduct.definition.id);
// 消息结构 : Receipt: {"Store":"fake","TransactionID":"9c5c16a5-1ae4-468f-806d-bc709440448a","Payload":"{ \"this\" : \"is a fake receipt\" }"};
Logger.Log("Receipt: " + e.purchasedProduct.receipt);
IAPTools.EProductType productType;
if (Enum.TryParse<EProductType>(e.purchasedProduct.definition.id, out productType))
{
this.onPurchaseSuccess?.Invoke(productType);
}
// 我们自己后台完毕的话,通过代码设置成功(如果是不需要后台设置直接设置完毕,不要设置Pending);
return PurchaseProcessingResult.Complete;
}
// 支付失败回掉函数;
public void OnPurchaseFailed(Product item, PurchaseFailureReason r)
{
Logger.Log("Purchase OK: " + r.ToString());
this.m_PurchaseInProgress = false;
this.onPurchaseFail?.Invoke();
}
// 恢复购买功能执行回掉函数;
private void OnTransactionsRestored(bool success)
{
Logger.Log("Transactions restored.");
}
// 购买延迟提示(这个看自己项目情况是否处理);
private void OnDeferred(Product item)
{
Logger.Log("Purchase deferred: " + item.definition.id);
}
#endregion
}
(ps:某些SDK是其他线程返回的,如果需要在接入回调做些什么,是不能直接在其他线程里操作unity的一些东西的,比如操作gameobject,那就需要设置一个标识位,在判断)
商品信息是在谷歌后台配置的,如图:
商品按类型分为三种:受管制、订阅、奖励
我用到了受管制的物品,大概有两种类型:Consumable 和 NonConsumable
如果是 Consumable 就是可重复购买,消耗型的道具,比如+生命值
如果是 NonConsumable 就是不能重复买,买完就一直存在状态,比如无限生命
谷歌会自己做一步操作:
重装APP的时候,会自动根据账号去检测 NonConsumable类型的 是否购买过了,如果买过了,就会调用 Restore 保证道具能拉回来,重新走一遍你的获奖逻辑,需要自行判断一下~
如何测试呢?
如果是测试账号,就会变成测试订单,不会扣钱的,直接买就可以了