参考(admob官方文字教程)
注意导出的app包主文件已变,不要打开错了。
(即在xcode内打开的主文件由.xcodeproj变成.xcworkspace)
...
using GoogleMobileAds.Api;
...
public class GoogleMobileAdsDemoScript : MonoBehaviour
{
private RewardBasedVideoAd rewardBasedVideo;
...
public void Start()
{
#if UNITY_ANDROID//示例广告的appID
string appId = "ca-app-pub-3940256099942544~3347511713";
#elif UNITY_IPHONE
string appId = "ca-app-pub-3940256099942544~1458002511";
#else
string appId = "unexpected_platform";
#endif
// Initialize the Google Mobile Ads SDK.初始化
MobileAds.Initialize(appId);
// Get singleton reward based video ad reference.激励广告是单例模式,把脚本关联上单例
this.rewardBasedVideo = RewardBasedVideoAd.Instance;
this.RequestRewardBasedVideo();//尽早加载广告资源
}
private void RequestRewardBasedVideo()
{
#if UNITY_ANDROID//示例广告ID
string adUnitId = "ca-app-pub-3940256099942544/5224354917";
#elif UNITY_IPHONE
string adUnitId = "ca-app-pub-3940256099942544/1712485313";
#else
string adUnitId = "unexpected_platform";
#endif
// Create an empty ad request.创建一个新广告请求
AdRequest request = new AdRequest.Builder().Build();
// Load the rewarded video ad with the request.加载
this.rewardBasedVideo.LoadAd(request, adUnitId);
}
}
hello world示例里创建的广告需求
...
using GoogleMobileAds.Api;
...
public class GoogleMobileAdsDemoScript : MonoBehaviour
{
private RewardBasedVideoAd rewardBasedVideo;
...
public void Start()
{
// Get singleton reward based video ad reference.
this.rewardBasedVideo = RewardBasedVideoAd.Instance;
// Called when an ad request has successfully loaded.
rewardBasedVideo.OnAdLoaded += HandleRewardBasedVideoLoaded;
// Called when an ad request failed to load.
rewardBasedVideo.OnAdFailedToLoad += HandleRewardBasedVideoFailedToLoad;
// Called when an ad is shown.
rewardBasedVideo.OnAdOpening += HandleRewardBasedVideoOpened;
// Called when the ad starts to play.
rewardBasedVideo.OnAdStarted += HandleRewardBasedVideoStarted;
// Called when the user should be rewarded for watching a video.
rewardBasedVideo.OnAdRewarded += HandleRewardBasedVideoRewarded;
// Called when the ad is closed.
rewardBasedVideo.OnAdClosed += HandleRewardBasedVideoClosed;
// Called when the ad click caused the user to leave the application.
rewardBasedVideo.OnAdLeavingApplication += HandleRewardBasedVideoLeftApplication;
this.RequestRewardBasedVideo();
}
private void RequestRewardBasedVideo()
{
#if UNITY_ANDROID
string adUnitId = "ca-app-pub-3940256099942544/5224354917";
#elif UNITY_IPHONE
string adUnitId = "ca-app-pub-3940256099942544/1712485313";
#else
string adUnitId = "unexpected_platform";
#endif
// Create an empty ad request.
AdRequest request = new AdRequest.Builder().Build();
// Load the rewarded video ad with the request.
this.rewardBasedVideo.LoadAd(request, adUnitId);
}
public void HandleRewardBasedVideoLoaded(object sender, EventArgs args)
{
MonoBehaviour.print("HandleRewardBasedVideoLoaded event received");
}
public void HandleRewardBasedVideoFailedToLoad(object sender, AdFailedToLoadEventArgs args)
{
MonoBehaviour.print(
"HandleRewardBasedVideoFailedToLoad event received with message: "
+ args.Message);
}
public void HandleRewardBasedVideoOpened(object sender, EventArgs args)
{
MonoBehaviour.print("HandleRewardBasedVideoOpened event received");
}
public void HandleRewardBasedVideoStarted(object sender, EventArgs args)
{
MonoBehaviour.print("HandleRewardBasedVideoStarted event received");
}
public void HandleRewardBasedVideoClosed(object sender, EventArgs args)
{
MonoBehaviour.print("HandleRewardBasedVideoClosed event received");
}
public void HandleRewardBasedVideoRewarded(object sender, Reward args)
{
string type = args.Type;
double amount = args.Amount;
MonoBehaviour.print(
"HandleRewardBasedVideoRewarded event received for "
+ amount.ToString() + " " + type);
}
public void HandleRewardBasedVideoLeftApplication(object sender, EventArgs args)
{
MonoBehaviour.print("HandleRewardBasedVideoLeftApplication event received");
}
}
参考来源-admob测试广告
上面说的是用示例广告测试,下面我们说真实广告真机测试。
bug:framework not found iosurface for architecture arm64
bug:framework not found fileprovider for architecture arm64
解决方法:参考
点击上面的地址,下个9.0以上的版本,下完解压后不用安装,直接打开包内容。按照路径拷贝(拷贝到iPhoneOS.sdk而不是iPhonexx.x.sdk下面)。拷贝完在xcode打开的项目里清除,然后编译就OK了。
Start()里是该应用注册的admob id,Request()里是该应用的广告单元id。
激励广告常用两个事件:OnAdRewarded (发放奖励),OnAdClosed(关掉了视频就再请求加载新广告,暂停音效和游戏进程在这里恢复)。
之前看油管教程说注册和取消注册放在OnEnabled()和OnDisabled()里,但我试了没有反应,最后按照官方指导的把注册放在Start()里,取消还是放在Ondisabled()里(能否注销还未测试)。
在创建广告单元时要填写reward的类型(type)和数量(amount),所以发放奖励不要再自己另写了。
The Google Mobile Ads SDK was initialized incorrectly.
参考上面在unity内对admob adID的设置。也可以参考
是cocoapods的版本问题,大概流程是 ①卸载已有的cocoapods - ②安装新版cocoapods - ③拉取cocoapods依赖的库 - ④更新。参考来源
①打开终端,先卸载
sudo gem uninstall cocoapods -n/usr/local/bin
命令窗口中,遇到对话,输入y,并回车
②卸载完毕后,继续执行命令安装新版
sudo gem install cocoapods -n/usr/local/bin
如果成功安装就进行下一步,如果提示错误
ERROR: Could not find a valid gem ‘cocoapods’ (>= 0), here is why:
Unable to download data from https://rubygems.org/ — SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (https://api.rubygems.org/specs.4.8.gz)”
则输入下面命令
gem source -r https://rubygems.org/
gem source -a http://rubygems.org/
sudo gem install cocoapods
可成功安装。参考来源
如果提示ruby版本过低,则输入以下命令查看ruby源
gem sources -l //查看ruby源
默认情况下会显示
https://rubygems.org/
如果科学的使用了上网,则掠过下一步,如果没有:
//***********************//
ruby
源在墙内是访问不到的,需要置换为国内,原来一直用的是淘宝的,由于淘宝源不再更新,所以不在使用,现在用下面这个
//删除原始的源
gem sources --remove https://rubygems.org/
//添加新源(国内Ruby镜像)
gem source -a https://gems.ruby-china.com
只有在终端中出现下面文字才表明你上面的命令是成功的:
https://gems.ruby-china.com added to sources
为了验证你的Ruby
镜像是并且仅是gems.ruby-china
,可以用以下命令查看:
gem sources -l //检测是否成功
//*************************//
然后升级gem,gem是管理ruby的标准包
sudo gem update --system //升级gem
目前查看gem
版本为
gem -v //查看版本
正常的话就重新安装cocoapods
sudo gem install cocoapods -n/usr/local/bin
③拉取cocoapods依赖的库
打开访达Command + Shift + G,输入~/.cocoapods,到达该目录,删除该目录下所有内容,然后在该.cocoapods目录下创建repos文件夹。
在终端里一个一个执行下面三个命令(一个执行完了再输入下一个)
cd ~/.cocoapods/repos
git clone --depth 1 https://github.com/CocoaPods/Specs.git master
pod repo update
第二个命令会执行的久一些,如果出现拉取失败的各种提示,比如我的提示
error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 54
则可按参考来源尝试,我是没登GitHub,登陆了再次输入克隆命令后花了些时间就成功了。(所以可以尝试多克隆几次)
④最后一句命令完成后cocoapods就更新完毕,unity内重新build后就能看到xcworkspace文件了。
登陆苹果开发者账号,选择Certificates,Identifiers & Profiles
其实这些步骤如果完成上面的真机测试的话都已经使用过了。
即有证书(Certificates)才能证明自己的身份,App IDs提供app 的bundle ID,配置文件(pp)容许开发者把应用装到测试机器上。参考1 参考2
点击苹果开发者账号的appstore connect(如果不是苹果机的话直接浏览器里搜然后用苹果开发者账号登陆)。
点击 协议、税务和银行业务
进入后点击
浙商银行网站查询CNAPS
返回appstore connect首页,点击 我的APP,点击+号创建app。
如果提示“您没有适用于 iOS App的合格套装ID”,证明现在你没有还未管理的bundle ID了。
回到Appstore connect-我的App继续创建App
填写好基础信息
如果提示bundle ID不匹配无法创建的话,说明当前已有的bundle ID都已申请绑定好app了,再注册个新的bundle id就好了。
下图在要上线提交前再填写,只是沙箱真机测试的话不用管。
之后我们点击 功能 添加内购项目
苹果对上面内购类型的解释:
填写完毕后如下
上面 元数据丢失是因为我没有填写屏幕快照和审核信息,完整填写后就会显示准备提交。上面显示的“您的首个App内购买项目必须以新的App版本提交”不用管,意思是要在appstore内让玩家使用的话必须提交。沙箱真机测试就不用了。
先按照参考里在Unity的Services打开In-App Purchasing并导入官方包。
参考
之后新建camera模式的canvas并添加iap button
之后点击iap catalog填写信息
ID就填写之前在appstore connect里设置的id
Title和Description填写
然后点开Apple Configuration填写之前设置的该内购的标价。
Apple SKU和 Apple Team ID
添加完后关闭iap catalog选择刚添加的product ID
然后添加购买完成的代码
首先Unity内Window > Unity IAP > IAP Receipt Validation Obfuscator.打开验证混淆窗口。如果使用Google Play则复制进Google的公钥,如果只用Apple的话不用添加任何东西直接点红框按钮,Unity会在文件里生成Tangle文件。这个工具主要是加密收据并验证收据。
新建IAP脚本如下
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using System;
public class IAP : MonoBehaviour, IStoreListener
{
IStoreController m_StoreController; // The Unity Purchasing system.
IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
IAppleExtensions m_AppleExtensions;
ConfigurationBuilder builder;
public string coins100 = "com.mycompany.app.100coins";//消耗型
public string newMap = "com.mycompany.app.newMap";//非消耗型
public string subscription = "com.mycompany.app.subscription";//非续期型订阅
public string newableSub = "com.mycompany.app.newablesubscription";//可续期型订阅
//建议使用可续期型订阅通过SubscriptionManager读取是否到期等属性
void Start()
{
if (m_StoreController == null)
{
InitializePurchasing();
}
}
public void InitializePurchasing()//添加商品并初始化内购系统
{
if (IsInitialized())
return;
builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
builder.AddProduct(coins100, ProductType.Consumable);//在这里添加内购产品
builder.AddProduct(newMap, ProductType.NonConsumable);
builder.AddProduct(subscription, ProductType.Subscription);
builder.AddProduct(newableSub, ProductType.Subscription);
UnityPurchasing.Initialize(this, builder);
}
private bool IsInitialized()//判定是否初始化,初始化在OnInitialized()里
{
return m_StoreController != null && m_StoreExtensionProvider != null;
}
void BuyProductID(string productId)//充值内购按钮调用
{
if (IsInitialized())
{
// ... look up the Product reference with the general product identifier and the Purchasing
// system's products collection.
Product product = m_StoreController.products.WithID(productId);
// If the look up found a product for this device's store and that product is ready to be sold ...
if (product != null && product.availableToPurchase)
{
Console.WriteLine(string.Format("异步购买: '{0}'", product.definition.id));
m_StoreController.InitiatePurchase(product);//开始购买
}
else
{
Console.WriteLine("购买失败:无此产品或此产品无法购买");
}
}
else
{
Console.WriteLine("购买失败:IAP未完成初始化");
}
}
//恢复购买按钮调用,用于Non-Consumable(比如关卡等)和renewable Subscription(可续期订阅),消耗类和非续期订阅不可恢复
public void RestorePurchases()
{
if (!IsInitialized())
{
Debug.Log("购买失败:IAP还未完成初始化");
return;
}
//和以前不同的是:不要在这里判断平台,应该在UI里判断,如果不是ios则不要显示恢复按钮
if (m_StoreExtensionProvider != null)
{
IAppleExtensions apple = m_StoreExtensionProvider.GetExtension();
// Begin the asynchronous process of restoring purchases. Expect a confirmation response in
// the Action below, and ProcessPurchase if there are previously purchased products to restore.
apple.RestoreTransactions((result) => {
if (result)
{
Console.WriteLine("正在恢复: " + result + ". If no further messages, no purchases available to restore.");
//注意这里恢复的是购买的非消耗品。会自动调用一次非消耗品的脚本
//在此为:
//else if (string.equals(e.purchasedproduct.definition.id, newmap, stringcomparison.ordinal))
//{
// debug.log(string.format("processpurchase: pass. product: '{0}'", e.purchasedproduct.definition.id));
// // todo: the non-consumable item has been successfully purchased, grant this item to the player.
//}里的todo
}
else
{
Console.WriteLine("恢复购买失败");
}
});
}
}
public void CheckSub()//使用SubscriptionManager检查订阅是否到期
{
m_AppleExtensions = m_StoreExtensionProvider.GetExtension();
Dictionary introductory_info_dict = m_AppleExtensions.GetIntroductoryPriceDictionary();
foreach (var item in m_StoreController.products.all)
{
if (item.receipt != null)
{
if (item.definition.type == ProductType.Subscription)
{
string intro_json = (introductory_info_dict == null ||
!introductory_info_dict.ContainsKey(item.definition.storeSpecificId))
? null : introductory_info_dict[item.definition.storeSpecificId];
SubscriptionManager p = new SubscriptionManager(item, intro_json);
SubscriptionInfo info = p.getSubscriptionInfo();
Console.WriteLine("产品ID: " + info.getProductId());
Console.WriteLine("购买日期: " + info.getPurchaseDate());
Console.WriteLine("到期时间为: " + info.getExpireDate());//返回产品下次自动续订或到期的日期(对于已取消的自动续订订阅)
Console.WriteLine("是否已订阅 :" + info.isSubscribed().ToString());//非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("是否已过期 :" + info.isExpired().ToString());//非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("是否续期: " + info.isCancelled());//意味着已订阅,但不会续期,非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("产品是否为免费试用版 :" + info.isFreeTrial());//非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("是否自动续订: " + info.isAutoRenewing());//非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("下个结算日期前剩余时间: " + info.getRemainingTime());//下一个结算日期之前剩余的时间,返回“TimeSpan”
Console.WriteLine("是否在介绍期内: " + info.isIntroductoryPricePeriod());//非自动更新订阅返回“Result.Unsupported”
Console.WriteLine("介绍价格: " + info.getIntroductoryPrice());//该产品的介绍价格,返回格式为“0.99USD”的值
Console.WriteLine("介绍期剩余时间: " + info.getIntroductoryPricePeriod()); //没有介绍价格期的订阅产品返回“TimeSpan.Zero”
Console.WriteLine("介绍期内可应用此产品的数量: " + info.getIntroductoryPricePeriodCycles());//返回int
if (info.isSubscribed() == Result.True)
{
if (info.isExpired() == Result.False)
{
if (String.Equals(info.getProductId(), newableSub, StringComparison.Ordinal))
{
Console.WriteLine(string.Format("订阅有效:", info.getProductId()));
//这里写执行
}
}
else
Console.WriteLine("订阅过期");
}
}
}
}
}
public void CheckSubscriptionReceipt()//另一种检查订阅方法,通过receipt里的expiredate和当前时间对比来判断是否过期
{
var appleConfig = builder.Configure();//关于applestore的配置
var receiptData = Convert.FromBase64String(appleConfig.appReceipt);//将receipt转换为64位
AppleReceipt receipt = new AppleValidator(AppleTangle.Data()).Validate(receiptData);//AppleReceiptParser收据验证
//这里如果不使用unity的收据混淆则为:AppleReceipt receipt = new AppleReceiptParser().Parse(receiptData);
foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts)
{//这里如果有多个订阅的话,可以先匹配productID,再读取productType
if (productReceipt.productType == 3)//有三种类型:消耗/非消耗/订阅
{
Console.WriteLine("订阅productID = " + productReceipt.productID);
DateTime expirationDate = productReceipt.subscriptionExpirationDate;//订阅到期时间
Console.WriteLine("订阅到期时间 = " + expirationDate.ToString());
DateTime now = DateTime.Now.ToUniversalTime();//转换为世界时间
if (DateTime.Compare(now, expirationDate) < 0)//DateTime.Compare(t1,t2);t1早于t2小于0(等于0,大于0)
Console.WriteLine("订阅未过期");
//在这里写逻辑
else
//订阅无效,在这里写逻辑
Console.WriteLine("订阅已过期");
}
}
}
//--------IStoreListener接口
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
Debug.Log("初始化通过");
m_StoreController = controller;
m_StoreExtensionProvider = extensions;
}
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.Log("初始化失败,原因:" + error);
}
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
Debug.Log(string.Format("购买失败. Product: '{0}', 原因: {1}", i.definition.storeSpecificId, p));
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)//在这里写内购成功后的操作
{
bool validPurchase = true; // Presume valid for platforms with no R.V.
var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
AppleTangle.Data(), Application.identifier);
try
{//验证收据
var result = validator.Validate(e.purchasedProduct.receipt);
// For informational purposes, we list the receipt(s)
Console.WriteLine("收据已验证. Contents:");
foreach (IPurchaseReceipt productReceipt in result)
{
AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
if (null != apple)
{
Console.WriteLine(apple.productID);
Console.WriteLine(apple.purchaseDate);
Console.WriteLine(apple.originalTransactionIdentifier);
Console.WriteLine(apple.subscriptionExpirationDate);
Console.WriteLine(apple.cancellationDate);
Console.WriteLine(apple.quantity);
}
}
}
catch (IAPSecurityException)
{
Console.WriteLine("无效收据");
validPurchase = false;
}
if (validPurchase)
{
if (string.Equals(e.purchasedProduct.definition.id, coins100, StringComparison.Ordinal))
{
Debug.Log(string.Format("购买成功. ID: '{0}'", e.purchasedProduct.definition.id));
//这里写逻辑
}
else if (string.Equals(e.purchasedProduct.definition.id, newMap, StringComparison.Ordinal))
{
Debug.Log(string.Format("购买成功. ID: '{0}'", e.purchasedProduct.definition.id));
//这里写逻辑
}
else if (string.Equals(e.purchasedProduct.definition.id, subscription, StringComparison.Ordinal))
{
Debug.Log(string.Format("购买成功. ID: '{0}'", e.purchasedProduct.definition.id));
//这里写逻辑
}
else if (string.Equals(e.purchasedProduct.definition.id, newableSub, StringComparison.Ordinal))
{
Debug.Log(string.Format("购买成功. ID: '{0}'", e.purchasedProduct.definition.id));
//这里写逻辑
}
else
{
Debug.Log(string.Format("购买失败. Unrecognized product: '{0}'", e.purchasedProduct.definition.id));
}
}
// Return a flag indicating whether this product has completely been received, or if the application needs
// to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
// saving purchased products to the cloud, and when that save is delayed.
return PurchaseProcessingResult.Complete;
}
}
要说的是,里面有一个RestorePurchase方法和两个检查订阅是否过期的方法。经我测试保险起见在OnInitialized()里先调用RestorePurchases()再调用CheckSub()来恢复订阅逻辑。CheckSubscriptionReceipt()时见效时不见效。
注意如果要使用订阅的话最好用自动更新型订阅,方便Restore,同时SubscriptionManager里的各种属性也只对自动更新型有效。
如果使用服务器,则可以验证收据 参考
如果需要最新unity IAP的例子参考 和 Unity IAP 手册
有可能是因为没添加成功内购物品,日志里会显示UnityIAP:Received 0 products,且之后日志每项内购项目都会Unvailable product com.xxxxxx.xxxx 之类的。原因是开发者过期了,续费后不仅要在Developer Account里重新同意协议,还要在App connect的“协议,税务和银行业务”选项里重新统一一遍,直到下面你的app显示有效后就可以了。
xcode内Product→Archive打包。
然后验证
1.Xcode内Build,Archive,Debug,Release,Profile,Analyze的区别
参考来源
Build则是xcode内的那个播放键,就是生成真机测试版本,archive是生成打包appstore上传的文件。
2.提示:“Code signing is required for product type 'Application' in SDK 'iOS 11.2”
参考来源
我出现这条提示时因为我把pp文件里的release版(需要distribution identity绑定)填成了debug版(developer identity),换成对应的就OK了。
3.提示:“Invalid bitcode version (Producer: '802.0.42.0_0' Reader: '800.0.42.1_0')”
参考来源
4.valid时提示UIApplicationExitsOnSuspend key invaild
参考来源
5.提示The Google Mobile Ads SDK was initialized incorrectly
参考来源
6.提示all ios apps submitted to the app store must be built with the ios 13 sdk or later
是因为xcode版本太低了,先升级xcode。如果macOS版本太低升级不了Xcode的话先升级macOS
参考来源
建议更新下梯子,换个流畅的通道很快就好了。
ipa因为不合规定被打回了,登入注册developer账号的邮箱,苹果会发邮件并说明是什么原因。
修改后再次上传时记得版本号要升一阶。
因为使用了UIWebView API,在Unity和Admob的框架里都有使用。
虽然Unity和Admob都说已解决了问题。unity哪些版本修复了
此外,Unity引擎在所有受支持的版本中均删除了对UIWebview的引用:
2017.4.33f1
2018.4.10f1
2019.2.7f2
2019.3.0b4
2020.1.0a5
但我使用2019.3.8f1 2019.3.9f1 2018.4.22f1的中国特供版(是的,只能从unity.com下载)都还是有问题。
只有2020.1.0a25版本的Unity没有问题。(new:2019.3.13f1 中国特供版经我测试也没有问题)
查找当前版本有没有问题不用archive后上传后被拒后看邮件。直接在当前版本里新建空项目,转换到ios platform后build
然后在终端cd 你build好的文件夹 回车后 参考来源
grep -r UIWebView .
就会显示是否有UIWebview的引用。
admob说它修复了,但我更新了最新版本admob unity包 5.1.0 ,在使用unity 2020.1.0a25 版本下的空文件,还是提示有UIWebView的引用。所以目前我只能取消了游戏使用Admob,等之后版本出来会继续试看哪些能用。
查到的修改UIWebView 里面有修改libiPhone.a内的UIWebView的脚本,我试了还是不行。
在app connect 里查看 活动 选项卡,里面有你的版本,如果显示正在处理的话,20分钟以内会处理好,然后就可以在构建版本里选择你上传的版本了。20分钟还是正在处理的话 试试这个 。
没有问题不用管,这是移动端的调用,打包到真机后就不会有提醒了。
unity内项目build的时选simulator,然后在xcode内用模拟器针对appstore要求的各个尺寸截图。
直接mac打开截屏图片然后 文件-导出-取消勾选Alpha然后导出新图片就可以了。