unity.png#1126px #313px

修改历史

时间版本
2024/2/261.0.0
2024/3/221.0.1
2024/5/111.0.2

接入流程(基于Unity 2020)

1. 添加minigame小游戏发布模版

Unity提供了2个默认模板(Default和Minimal),都在Unity的安装目录下

image.png
image.png

SDK使用的是Default模板,因为这个模板的功能更完整。把Default目录复制到项目的Assets/WebGLTemplates/<自己的模板> ,假设模板目录为minigameDefault,那么工程的Assets目录大概是这样子:

image.png

这样打包的时候,WebGL Template中会多出一个minigameDefault的选择,打包时选择minigameDefault。

image.png

2. 移除在移动设备上运行的警告

在minigameDefault模版工程的入口文件index.html中,把下面代码注释掉,不然Unity在手机上运行会弹出一个警告。

image.png

并将其改成下面这样:

// 改成unity-mobile,使之全屏显示
container.className = "unity-mobile";
config.devicePixelRatio = 1;

也可以直接去修改unity-container的class:

<div id="unity-container" class="unity-mobile">
...
</div>

unity打出来的包通常会在右边出现滚动条,如下:
image.png
需要给body添加css样式以隐藏滚动条

<body style="overflow: hidden;">

3. 添加SDK链接

在index.html的head标签中添加SDK链接:

<script src="https://sdk.minigame.vip/js/1.1/minigame.js"></script>

4.添加SDK启动代码

在index.html中封装unity启动方法startUnity,在进度条位置调用进度显示接口minigame.setLoadingProgress(100 * progress);,并将unityInstance赋值给全局对象。
window.unityInstance = unityInstance; ,用于后续和SDK进行交互,最后再启动SDK: minigame.startGameAsync()

function startUnity(){
  var script = document.createElement("script");
  script.src = loaderUrl;
  script.onload = () => {
    createUnityInstance(canvas, config, (progress) => {
      progressBarFull.style.width = 100 * progress + "%";
      minigame.setLoadingProgress(100 * progress);
    }).then((unityInstance) => {
      loadingBar.style.display = "none";
      fullscreenButton.onclick = () => {
        unityInstance.SetFullscreen(1);              
      };
      window.unityInstance = unityInstance;
	  // 延迟2秒,是为了跳过logo动画
	  setTimeout(() => {
		minigame.startGameAsync();
	  }, 2000);
    }).catch((message) => {
      alert(message);
    });
  };
  document.body.appendChild(script);
}

调用初始化minigame.initializeAsync,启动成功后调用startUnity

minigame.initializeAsync()
  .then(function () {
    console.info("minigame initializeAsync..");
	// 启动游戏
	startUnity();
  })
  .catch(function (e) {
	cc.error("minigame-sdk init error: ", e); 
  })

5. Unity和SDK的交互

Unity提供了.jslib扩展名文件来进行Unity和JavaScript的交互。不熟悉的同学可以看下官方链接:https://docs.unity3d.com/cn/2018.4/Manual/webgl-interactingwithbrowserscripting.html

js调用Unity:

这里假设 .jslib 文件名为JS.jslib,JS.jslib 文件放置在 Assets 文件夹中的“Plugins”子文件夹下。将广告接口调用添加到此文件里面,并通过unityInstance.SendMessage来从javaScript层发消息给Unity层,假设场景中挂载广告代码的对象名称为ScriptControl

mergeInto(LibraryManager.library, {
	// 播放插屏
	showInterstitial: function() {
		if (typeof MiniGameAds === "undefined") {
		  console.error("MiniGameAds is undefined");
		  return;
		}

		MiniGameAds.showInterstitial()
		.then(function() {
			// 播放成功,调用showInterstitialCallback,返回成功结果1
			unityInstance.SendMessage("ScriptControl", "showInterstitialCallback", 1);
		}).catch(function(e) {
		        // 播放成功,调用showInterstitialCallback,返回失败结果0
			unityInstance.SendMessage("ScriptControl", "showInterstitialCallback", 0);
		});
	},

  // 播放激励
	showRewardedVideo: function() {
		if (typeof MiniGameAds === "undefined") {
		  console.error("MiniGameAds is undefined");
		  return;
		}

		MiniGameAds.showRewardedVideo()
        .then(function() {
      			// 播放成功,调用showReWardedCallback,传入参数成功结果1
            unityInstance.SendMessage("ScriptControl", "showReWardedCallback", 1);
        }).catch(function(e) {
      			// 播放成功,调用showReWardedCallback,传入参数失败结果0
            unityInstance.SendMessage("ScriptControl", "showReWardedCallback", 0);
        });
	},

  // 播放banner
	showBanner: function() {
		if (typeof MiniGameAds === "undefined") {
		  console.error("MiniGameAds is undefined");
		  return;
		}

		MiniGameAds.showBanner().then(function() {
			      // 播放成功,调用showBannerCallback,传入参数成功结果1
            unityInstance.SendMessage("ScriptControl", "showBannerCallback", 1);
        }).catch(function(e){
       			// 播放成功,调用showBannerCallback,传入参数失败结果0
            unityInstance.SendMessage("ScriptControl", "showBannerCallback", 0);
        });
	},

  // 隐藏banner
	hideBanner: function() {
		if (typeof MiniGameAds === "undefined") {
		  console.error("MiniGameAds is undefined");
		  return;
		}

 		MiniGameAds.hideBanner().then(function() {
      			// 播放成功,调用hideBannerCallback,传入参数成功结果1
            unityInstance.SendMessage("ScriptControl", "hideBannerCallback", 1);
        }).catch(function() {
         		// 播放成功,调用hideBannerCallback,传入参数失败结果0
            unityInstance.SendMessage("ScriptControl", "hideBannerCallback", 0);
        });
	}
});

Unity调用js

public class AdsControl : MonoBehaviour {
  // 声明导入外部方法
  [DllImport("__Internal")]
  private static extern void showInterstitial();

  [DllImport("__Internal")]
  private static extern void showRewardedVideo();

  [DllImport("__Internal")]
  private static extern void showBanner();

  [DllImport("__Internal")]
  private static extern void hideBanner();                    
	
  // 激励广告回调,处理发放奖励
  public void showReWardedCallback(int isSuccess) {
    Debug.Log("show reward: " + (isSuccess == 1 ? "success" : "failed"));
  }

  // 插屏广告回调,由于不用发放奖励可不用处理 
  public void showInterstitialCallback(int isSuccess) {
    Debug.Log("show Interstitial: " + (isSuccess == 1 ? "success" : "failed"));
  }

  // banner广告播放回调,由于不用发放奖励可不用处理
  public void showBannerCallback(int isSuccess) {
    Debug.Log("show banner: " + (isSuccess == 1 ? "success" : "failed"));
  }

  // banner广告隐藏回调,由于不用发放奖励可不用处理
  public void hideBannerCallback(int isSuccess) {
    Debug.Log("hide banner: " + (isSuccess == 1 ? "success" : "failed"));
  }

  // 播放插屏,调用JS.jslib文件中声明的 showInterstitial
  public void showInterstitialAd()
  {
    showInterstitial();
  }

  // 播放插屏,调用JS.jslib文件中声明的 showRewardedVideo
  public void ShowRewardedVideoAd()
  {
    showRewardedVideo();
  }

  // 播放插屏,调用JS.jslib文件中声明的 showBanner
  public void showBannerAd()
  {
    showBanner();
  }

  // 播放插屏,调用JS.jslib文件中声明的 hideBanner
  public void hideBannerAd()
  {
    hideBanner();
  }
}

6. WebGL打包

WebGL打包时,默认会开启资源压缩,由于很多平台的服务器没有启用压缩格式的文件头,因此这里有两个选择:

1.关闭资源压缩: 这会导致导出的游戏包体过大。

image.png

2.开启解压回退: 默认情况下是关闭的,开启后会导致加载器大小增大和构建文件加载方案的效率降低,会增加游戏加载的时间。

image.png

具体选择哪一种,需要根据游戏项目的实际情况:

  • 游戏上微游平台: 使用方案2。

  • 游戏上Facebook平台: 使用方案1。

接口

广告相关接口

广告功能推荐使用微游SDK提供的广告接口(MiniGameAds)进行接入,对于熟悉FBIG接口的游戏开发者,也可以直接使用FBIG兼容的广告接口进行接入,推荐使用MiniGameAds。

MiniGameAds主要有5个接口:

  • showInterstitial(): Promise<void>

作用:展示插屏广告
示例:

// @ts-ignore
MiniGameAds.showInterstitial().then(()=>{
	console.info("====> show interstitial success");
}).catch(e => {
	console.error("====> show interstitial error: " + e.message);
});

注意:由于插屏广告默认第一次播放需要30秒后,才能播放,后面播放每次时间间隔40秒,如果cp测试的时候觉得时间太长,可以把minigame.json里fb_ad_delay_for_first_interstitial和fb_interstitial_refresh_interval字段改小。

  • showRewardedVideo():Promise<void>

作用:展示激励广告
示例:

// @ts-ignore
MiniGameAds.showRewardedVideo().then(()=>{
	console.info("====> show RewardedVideo success");
}).catch(e => {
	console.error("====> show RewardedVideo error: " + e.message);
});
  • isAdRadical(): boolean

作用:激进版广告开关
返回:true为激进版激励广告,需要展示激进版UI,false为普通版激励广告,需要展示普通版UI。
激进版:包含误点击、诱导点击的广告策略,类型比如:

  • 默认勾选:默认勾选看激励视频双倍,且勾选项字眼过小。

  • 延迟提示:不看激励广告的字眼,让用户以为只有一个选项。

普通版:没有激进版的误点击和诱导点击的广告策略。

注意:倘若游戏中存在激进型激励广告,需要做两套处理:一套按照原本的激进版处理,还需做一套普通版的,
通过在minigame.json中添加字段"isAdRadical“字段可以控制isAdRadical()的返回值,
”isAdRadical“:true则isAdRadical() === true,”isAdRadical“:false则isAdRadical() === false

示例:

const isAdRadical = MiniGameAds.isAdRadical();
// 根据isAdRadical来决定是显示激进版UI还是普通版UI

minigame.json具体配置如下:

{
  "platform": "minigame",
  "sdk": "https://sdk.minigame.vip/js/1.0/minigame-sdk.debug.js",
  "instance": "FBInstant",
  "gameName": "minigame",
  "features": {
    "ads": {
      "enabled": false, // 广告开关
      "isBannerEnabled": true, // banner开关
      "isTest": true, // 是否是广告本地测试环境
      "isAdRadical": true, // 是否为激进版激励广告
      "config": {
        "interstitial": "4864743603539728_5070034729677280",
        "banner": "4864743603539728_5082905605056859",
        "rewarded_video": "4864743603539728_5070034119677341",
        "rewarded_interstitial": "4864743603539728_5070034119677341"
      },
      "options": {
        "fb_max_ad_instance": 3,
        "fb_init_ad_count": 3,

        "fb_banner_refresh_interval": 40,
        "fb_interstitial_refresh_interval": 40,
        "fb_rewarded_video_refresh_interval": 0,

        "fb_max_banner_error": 1,
        "fb_max_interstitial_error": 3,
        "fb_max_rewarded_video_error": 3,

        "fb_auto_load_on_play": true,
        "fb_auto_reload_delay": 1,

        "fb_ad_delay_for_first_banner": 0,
        "fb_ad_delay_for_first_interstitial": 30,
        "fb_ad_delay_for_first_rewarded_video": 0
      }
    },
    "leaderboard": {
      "enabled": true
    },
    "ga": {
		"enabled": true,
		"isDefault": true,
		"config": {
			"gid": "UA-226110216-21"
		}
     }
  }
}
  • showBanner(): Promise<void>

作用:展示横幅广告
示例:

// @ts-ignore
MiniGameAds.showBanner().then(()=>{
	console.info("====> show banner success");
}).catch(e => {
	console.error("====> show banner error: " + e.message);
});
  • hideBanner(): Promise<void>

作用:隐藏横幅广告
示例:

// @ts-ignore
MiniGameAds.hideBanner().then(()=>{
    console.info("====> hide banner success");
}).catch(e => {
    console.error("====> hide banner error: " + e.message);
});

游戏统计(有需要才接入)

  • onGameEvent

    /**
      * @param eventName 事件名称
      * @param label  事件标签
      */
    public onGameEvent (eventName: string, label: string)

    调用实例:
    // 账号登录
    // @ts-ignore
    MiniGameAnalytics.onGameEvent("login", "自定义标签");

内购支付(有需要才接入)

内购接口

  • onReady(callBack: Function): void
    设置一个回调,callback在支付可用时,会被触发,onReady一般在进入游戏后调用,callback里面可以先处理下补单的操作。

Examples:

minigame.payments.onReady(function () {
    // 先获取未消耗的订单
    minigame.payments.getPurchasesAsync().then(function(purchases) {
      console.log(purchases);
      // 遍历订单
      purchases.forEach((purchase) => {
          // 消耗订单
           minigame.payments.consumePurchaseAsync(purchase.purchaseToken)
                .then(function () {
                  // 消耗成功
                  // 游戏发送奖励给玩家
                });
          })
    });
});
  • getCatalogAsync(): Promise<Product[]>
    获取游戏的产品目录

    Examples:

    minigame.payments.getCatalogAsync().then(function (catalog) {   
      console.log(catalog); // [{productID: '12345', ...}, ...]  
    });

  • purchaseAsync(purchaseConfig: PurchaseConfig): Promise<Purchase>
    开始特定产品的购买流程, productID在接入的时候,我们会进行提供。

    Examples:

    minigame.payments.purchaseAsync(
    {  productID: '12345',  
       developerPayload: 'foobar'
    }).then(function (purchase) {
      console.log(purchase);// {productID: '12345', purchaseToken: '54321', developerPayload: 'foobar', ...}
    });

  • getPurchasesAsync(): Promise<Purchase[]>
    获取玩家未消费的所有购买商品

    Examples:

    minigame.payments.getPurchasesAsync().then(function (purchases) {
      console.log(purchase);
      // [{productID: '12345', ...}, ...]
    });

  • consumePurchaseAsync(purchaseToken: string): Promise<void>
    消费当前玩家拥有的特定购买商品

    Examples:

    minigame.payments.consumePurchaseAsync('54321').then(function () {
      // 消耗成功
      // 游戏发送奖励给玩家
    });

    注意purchaseAsync支付成功后,需要马上调用消耗接口才能发放奖励,例如:
    minigame.payments.purchaseAsync({
      productID: '12345',
      developerPayload: 'foobar',
    }).then(function (purchase) {         
        minigame.payments.consumePurchaseAsync(purchase.purchaseToken)
          .then(function () {
              // 消耗成功
              // 游戏发送奖励给玩家
            });
    });

2.类型定义

支付参数:

export interface PurchaseConfig {
  /**
   * 产品id
   */
  productID: string;
  /**
   * 可选参数,开发人员指定的内容,将包含在返回的购买签名请求里。
   */
  developerPayload?: string;
}

商品信息:

export interface Product {
  /**
   * 产品的名称
   */
  title: string;
  /**
   * 产品的游戏指定id
   */
  productID: string;
  /**
   * 产品的描述
   */
  description?: string;
  /**
   * 产品相关图片的链接
   */
  imageURI?: string;
  /**
   * 产品的价格
   */
  price: string;
  /**
   * 产品的货币代码,字符串类型
   */
  priceCurrencyCode: string;
  /**
   * 产品的价格,数字类型
   */
  priceAmount: number;
}

支付返回:

export interface Purchase {
  /**
   * 可选参数,开发人员指定的内容,将包含在返回的购买签名请求里。
   */
  developerPayload?: string;
  /**
   * 购买的当前状态,如“收费”或“退款”
   */
  paymentActionType: string;
  /**
   * 购买交易的标识符
   */
  paymentID: string;
  /**
   * 产品id
   */
  productID: string;
  /**
   * 包含与所购买项目关联的本地金额和货币
   */
  purchasePrice: string;
  /**
   * 发生购买时的Unix时间戳
   */
  purchaseTime: string;
  /**
   * 代表可用于消费者购买时的token
   */
  purchaseToken: string;
  /**
   * 购买请求的服务器签名编码
   */
  signedRequest: string;
}

添加桌面快捷方式(置顶)(有需要才接入)

  • canCreateShortcutAsync(): Promise<boolean>;
    是否可以准备可以创建快捷方式

  • createShortcutAsync( ): Promise<void>;
    创建快捷方式

Examples:

minigame.canCreateShortcutAsync()
  .then(function(canCreateShortcut) {
  	// 如果可以创建
    if (canCreateShortcut) {
      // 尝试创建快捷方式
      minigame.createShortcutAsync()
        .then(function() {
          // Shortcut created
        	console.info("call createShortcutAsync done");
        })
        .catch(function(error) {
          // Shortcut not created
	        console.info("call createShortcutAsync fail: ", error);
        });
    }
  })
  .catch(function(error) {
  	console.info("canCreateShortcut fail: ", error);
  });

注意: 必须先判断是否准备好,可以创建的情况才创建快捷方式。

分享(有需要才接入)

  • shareAsync(payload: SharePayload): Promise<void>

Examples:

minigame.shareAsync({
  image: base64Picture,
  text: 'X is asking for your help!',
  data: { myReplayData: '...' },
  switchContext: false,
}).then(function() {
  // success to share, continue with the game.
})
.catch(function(error) {
  // share fail
});

参数定义:

interface SharePayload {
  /**
   * 表示共享的目标, 7.1已经删除,用问号兼容老版本
   * Indicates the intent of the share.
   * "INVITE" | "REQUEST" | "CHALLENGE" | "SHARE"
   */
  intent?: string;
  /**
   * 要分享的图像,使用 base64 编码
   * A base64 encoded image to be shared.
   */
  image: string;

  /*
   * gif 或者 video
   */
  media?: MediaParams;

  /**
   * 要分享的文字
   * A text message to be shared.
   */
  text: string;
  /**
   * 一个附加到分享上的数据。
   * 所有从这个分享启动的游戏都可以通过  FBInstant.getEntryPointData() 方法获取到该数据。
   *  A blob of data to attach to the share.
   */
  data?: Object;
  /**
   * 在共享对话框中设置共享目的地的可选数组。如果没有指定,将显示所有可用的共享目的地。
   */
  shareDestination?: string[],
  /**
   * 指示是否将用户切换到共享时创建的新上下文的标志
   */
  switchContext?: boolean,
}

语言

  • getLocale(): string;

const lang = minigame.getLocale(); //  "假设当前是美国英语环境, 返回en_US"
// 判断语言是否为英语	
if (lang.includes("en")){
	console.info("current language is en");
}
// 或
if (lang.substr(0, 2) === "en"){
	console.info("current language is en");
}

邀请

  • inviteAsync(payload: InvitePayload ): Promise<void>;

minigame.inviteAsync({
  image: base64Picture,
  text: {
    default: 'X just invaded Y\'s village!',
    localizations: {
      ar_AR: 'X \u0641\u0642\u0637 \u063A\u0632\u062A ' +
        '\u0642\u0631\u064A\u0629 Y!',
      en_US: 'X just invaded Y\'s village!',
      es_LA: '\u00A1X acaba de invadir el pueblo de Y!'
    }
  },
  data: { myReplayData: '...' }
}).then(function() {
  // continue with the game.
}).catch(function(error){
	console.error("invite error: ", error);
})

参数定义:

interface InvitePayload {
  /* 一个以base64编码的图像,用于分享。*/
  image: string;
  /* 文本消息,或者一个包含默认文本作为'default'值以及将语言区域键映射到翻译的另一个对象作为'localizations'值的对象。*/
  text: string | LocalizableContent;
  /*
   * 可选的呼叫动作按钮文本。默认情况下,我们将使用本地化的“Play”作为按钮文本。如果要提供自己的呼叫动作的本地化版本,请传递一个对象,将默认cta作为'default'值,并将语言区域键映射到翻译作为'localizations'值。
   */
  cta?: string | LocalizableContent;
  /* 要附加到分享的数据块。从分享启动的所有游戏会话都可以通FBInstant.getEntryPointData()访问此数据块。*/
  data?: Object;
  /* 要应用于建议的一组筛选器。可以应用多个筛选器。如果应用筛选器时没有返回结果,将生成没有筛选器的结果*/
  filters?: Array<InviteSection>;
  /*
   * 包含在对话框中的一组部分。每个部分可以分配一个要返回的最大结果数(最多为10个)。如果未包含最大值,将应用默认最大值。部分将按照它们在数组中列出的顺序包括。最后一个部分将包括较大的最大结果数,如果提供了maxResults,它将被忽略。如果此数组为空,将使用默认部分。
   */
  sections?: Array<InviteSection>;
  /* 可选的标题,显示在邀请对话框的顶部,而不是通用标题。此参数不作为消息的一部分发送,但仅在对话框标题中显示。标题可以是字符串,也可以是一个对象,将默认文本作为'default'值,并将语言区域键映射到翻译作为'localizations'值。 */
  dialogTitle?: string | LocalizableContent;
}

interface InviteSectionType {
    /** 这包含组上下文,例如来自组线程的上下文。 */
    GROUPS: string;
    /** 这包含个人用户,例如朋友或 1:1 线程。 */
    USERS: string;
}

interface LocalizationsDict {
    [key: string]: string;
}

/** 表示具有本地化和可依赖的默认值的字符串。 */
interface LocalizableContent {
    /** 当查看者的区域设置不是本地化对象中的键时要使用的字符串的默认值。 */
    default: string;
    /** 指定每个区域设置中查看器使用的字符串。 有关支持的区域设置值的完整列表 */
    localizations: LocalizationsDict;
}

interface InviteSection {
    /** 要包含在 informAsync 对话框中的部分的类型 */
    sectionType: InviteSectionType;
    /** 该部分中包含的可选最大结果数。该值不能高于 10。最后一部分将忽略该值,该部分将包含尽可能多的结果。如果未包含,则将使用该部分类型的默认最大结果数。 */
    maxResults: number;
}

本地测试环境

需要在游戏根目录启动一个web服务器,创建web服务器的方式有很多,比如node.js的http-server,Serve等模块,python的SimpleHTTPServer等等,具体使用什么库,由开发者自行决定。

这里以Node.js的http-server为例:
1.将minigame.json的isTest设置为true。
2.在游戏根目录启动web服务器,执行指令: http-server -o -c-l,这样会自动打开浏览器,并打开 http://127.0.0.1:8080/ ,接下来就可以直接测试广告了。

模拟线上环境

步骤:
1.打开https://minigamecloud.com/testgame
2.将minigame.json中的isTest设置为false
3.将接入SDK后的本地游戏链接,放进页面的地址栏中。

image.png

4.点击开始测试后,就可以模拟线上环境测试。


------------------------------------------------------ END ---------------------------------------------------------------------------

商务对接
商务对接
微信公众号
公众号