"Hello, world!" "Hello, 小游戏宿主!" 本文简要记录了 Unity Connect 作为小游戏宿主的第一块试验田,从 SDK 接入、体验优化,到平台能力接入的全过程。如果文中有任何错误,欢迎指出与交流,期待与你一起共建更加繁荣的小游戏宿主生态!
一、小游戏宿主的第一块试验田
有没有小伙伴已经发现了?最近 Unity Connect 首页底部的导航栏悄悄多了一个小手柄按钮,点进去就能进入全新的「Unity 小游戏中心」啦 !
在这里,你可以用十来分钟来一局紧张刺激的卡牌肉鸽,也可以当个岛主悠哉地建设自己的专属岛屿,甚至还能施展魔法,培养一位未来的龙骑法师。不管是午休时间,还是 上班摸鱼 ,都多了一个放松的好去处!
当然,我们在 Connect 上加入小游戏中心,不只是为了让大家在忙碌工作之余多一个“快乐角落”。更重要的是——Unity Connect 是我们接入「小游戏宿主」的第一块试验田。
通过这次接入,我们希望向开发者们展示:小游戏宿主不仅兼容多种游戏引擎,还能为移动 App 提供流畅、轻量的游戏运行环境。未来,我们也期待更多移动应用和小游戏开发者关注并使用这一能力,一起共建 Unity 小游戏生态!
这篇文章就简单介绍和分享小游戏宿主的前世今生以及 Connect 接入小游戏宿主的过程吧,如果有谬误请帮忙指出!
二、走进小游戏与宿主的世界
在小游戏的世界里,「宿主」其实就是小游戏的“家”——它是一个平台或容器,负责加载、运行和管理小游戏,还会提供各种运行时能力和平台服务,确保小游戏能顺利启动、顺畅运行。而运行在这个“家”里的小游戏,我们通常叫它「宿主小游戏」,也常被称为「Instant Game」。
小游戏因为“轻量化、即点即玩”的特性,自 2017 年问世以来就一路高歌猛进:2019 年初,开发者数量突破 10 万;同年 5 月,用户规模破亿;到了 2021 年,年流水破千万的小游戏产品已经超过 50 款!游戏类型也从最初的超休闲,逐渐拓展到了中重度玩法,比如 MMO、策略类等,越来越丰富。
各大平台自然也不甘落后,纷纷打造自己的小游戏生态,推出小程序/小游戏运行环境,力求在自家 App 里集成功能、留住用户、提升体验。
不过,并不是所有平台型公司都已经有了现成的宿主解决方案。有些是还没来得及做,有些是想更高效地把其他平台的小游戏“搬”到自己的 App 上。这时,一个灵活、易用的宿主方案就显得尤为重要——于是,Unity 小游戏宿主应运而生!
事实上,Unity 小游戏宿主是融合了客户端 SDK、服务端 API 以及管理后台的一体化综合性小游戏运行平台,平台开发者能够在管理后台创建宿主应用,上传小游戏,建立关联关系,通过服务端 API 获取游戏列表,小游戏开发者则能通过集成了小游戏宿主的应用调试小游戏。
三、简单但重要的从 0 到 1
Connect 接入小游戏宿主的流程可参考官方文档:
https://minihost.tuanjie.cn/help/docs/welcome
小游戏运行环境可参考:
https://developers.weixin.qq.com/minigame/dev/guide/best-practice/game-engine.html
这里我们以 Android 平台为例,和大家一起快速过一遍整个接入过程。整体可以分为两个步骤:
在宿主管理平台创建应用并上传小游戏
客户端集成宿主小游戏 SDK
感兴趣的小伙伴也可以跟着我们一步步操作,创建一个属于自己的宿主应用哦!
3.1 创建宿主应用 & 小游戏
参考文档:https://minihost.tuanjie.cn/help/docs/quickstart/create-host-app
在正式接入客户端 SDK 之前,我们需要先去宿主管理控制台新建一个“宿主应用”。这一步主要是为了配置 Bundle ID,并获取对应的 App Key 和 App Secret,这两个参数后面在初始化 SDK 时会用到。
接下来,我们需要为这个宿主应用创建一些小游戏。在管理控制台的「小游戏」页面中,我们可以先创建一个最简单的示例游戏,比如 Spinning Cube,并将它关联到刚才创建的宿主应用上。
小游戏创建好之后,我们还需要上传一个对应的游戏包。这个游戏包可以直接通过 Unity 提供的新增的 Build Target 导出。为了方便大家调试,我们也提供了一个现成的 Spinning Cube 游戏包 。
Spinning Cube 游戏包:
https://minihost.cdn.tuanjie.cn/release/game-pkg/spinning-cube-20250418.zip
上传时,控制台会自动检查游戏包是否符合规范。上传成功后,我们需要发布并上线这个游戏包作为其最新的线上版本,别忘了回到小游戏列表中确认这个小游戏已经成功启用,并且记住它的 Game ID (注意是 Game ID 不是游戏包 ID),在后续启动游戏时会用到!
到这里,在控制台上的准备工作就完成啦 !
3.2 客户端集成宿主 SDK
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/android/quick-integrate
客户端接入小游戏的步骤也非常简单,首先,从小游戏宿主官方下载页获取最新版的 Android SDK 压缩包。
小游戏宿主 SDK 下载:https://minihost.tuanjie.cn/download
里面包含两个 AAR 文件:
host-runtime-dynamic.aar(动态加载包)
host-runtime-static.aar(静态加载包)
这两个包的主要区别在于:动态包支持按需下载小游戏运行环境,更加灵活;而静态包则是将运行环境直接集成进 App,适合快速接入,所以我们这里先使用静态包。
接下来,按照官方文档的说明,把宿主 SDK 和它依赖的其他 SDK 一起添加到项目的 build.gradle 文件中的 dependencies 里。
dependencies{
//Host SDK
implementationfiles('host-runtime-static.aar')
//dependencies for host SDK
implementationlibs.okhttp
implementationlibs.mmkv
implementationlibs.gson
implementationlibs.java.websocket
implementationlibs.camera.core
implementationlibs.camera.camera2
implementationlibs.camera.lifecycle
implementationlibs.camera.video
implementationlibs.camera.view
implementationlibs.camera.extensions
implementationlibs.tbssdk
}# libs.versions.toml
[versions]
okhttp = "3.12.13"
mmkv = "1.3.2"
gson = "2.10.1"
javaWebsocket = "1.5.1"
cameraCamera2 = "1.2.3"
tbssdk = "44286"
[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
java-websocket = { module = "org.java-websocket:Java-WebSocket", version.ref = "javaWebsocket" }
camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCamera2" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCamera2" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCamera2" }
camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraCamera2" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraCamera2" }
camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCamera2" }
tbssdk = { module = "com.tencent.tbs:tbssdk", version.ref = "tbssdk" }配置好依赖之后,我们就可以开始写代码啦。首先,新建一个简单的布局和对应的 Activity。在布局中,暂时只需要一个用于承载游戏的容器视图就够了。并且我们可以给 Connect 中的某个按钮绑定点击事件跳转到这个 Activity 用于调试。
"1.0" encoding="utf-8"?>
android:id="@+id/gameContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
FrameLayout>在宿主 SDK 中,启动一个小游戏大致包括以下五个步骤:
initialize
createGameHandle
setGameStartOptions
start
play
每个步骤都有其特定的作用,我们可以在每个步骤的成功回调中触发下一个步骤,这样整个流程就能顺利串起来。
第一步,在 Activity 的 onCreate 中调用 TJHostHandle.initialize 来初始化宿主 SDK。在这一步中,我们需要传入控制台上创建宿主应用时生成的 App Key 和 App Secret,用于验证接入身份。
同时,我们还可以传入一个游戏容器,这样宿主 SDK 的加载页、弹窗等 UI 元素就会被限制在这个容器内,避免覆盖整个界面。
初始化成功后,我们可以通过 TJHostHandle.getInstance() 获取 SDK 的单例,后续的所有操作都通过它来完成。
初始化成功之后,我们能够获取 TJHostHandle 单例,后续的控制 API 都通过这个单例调用。
FrameLayout gameContainer;
privatestatic final String _TAG _= "[GameActivity]";
private final Handler handler = new Handler(Looper._getMainLooper_());
private TJHostHandle hostHandle;
@Override
publicvoidonCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout._activity_game_);
gameContainer = findViewById(R.id._gameContainer_);
TJHostHandle._initialize_(this, "**APP Key**", "**APP Secret**", gameContainer, new OnTJHostHandleInitializeListener() {
@Override
publicvoidonSuccess(TJHostHandle hostHandle) {
GameActivity.this.hostHandle = hostHandle;
handler.post(() -> createGameHandle());
}
@Override
publicvoidonFailure(Throwable throwable) {
Log._e_(_TAG_, "TJHostHandle.initialize failed: " + throwable.getMessage());
}
});
}接着,调用 createGameHandle 创建游戏句柄。这个过程支持通过 Bundle 传入一些初始化参数,比如 HTTP 缓存目录等,具体参数可以参考文档。
宿主管理文档:
https://minihost.tuanjie.cn/help/docs/api/android/management#%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
private void createGameHandle(){
hostHandle.createGameHandle(this, new Bundle(), new OnTJHostCreateHandleListener() {
@Override
public void onSuccess(TJHostHandle result){
setGameStartOptions();
}
@Override
public void onFailure(Throwable throwable){
Log._e_(_TAG_, "Host createGameHandle failed", throwable);
}
});
} 然后,我们需要设置游戏的启动参数。启动一个小游戏至少需要两个关键参数:
LAUNCH_KEY:小游戏的启动参数(可以是小游戏的 Game ID,具体定义参考文档)
USER_ID:用户 ID,用于区分不同用户的数据,以及记录用户启动游戏的历史
我们把前面创建的 Spinning Cube 的 Game ID 填入 LAUNCH_KEY,宿主 SDK 就会根据它去获取对应的游戏包下载地址及类型。USER_ID 可以先随便填一个测试用的值,后续接入正式用户体系时再替换。
除此之外,setGameStartOptions 还支持一些可选参数,比如是否展示展示更多按钮、是否开启调试模式等,具体可参考 文档 。
小游戏管理:
https://minihost.tuanjie.cn/help/docs/api/android/minigame-management#%E6%B8%B8%E6%88%8F%E5%90%AF%E5%8A%A8%E5%8F%82%E6%95%B0-launch_key-%E8%AF%B4%E6%98%8E
privatevoidsetGameStartOptions(){
Bundle bundle = new Bundle();
bundle.putString(TJRuntimeConstants._LAUNCH_KEY_, "Game ID");
bundle.putString(TJRuntimeConstants._USER_ID_, "User ID");
hostHandle.setGameStartOptions(bundle, new OnTJHostHandleListener() {
@Override
publicvoid onSuccess() {
start();
}
@Override
publicvoid onFailure(Throwable throwable) {
Log._e_(_TAG_, "Host set game start options failure", throwable);
}
});
}参数设置好之后,调用 start 开始加载游戏资源。加载成功后,我们就可以将游戏界面添加到之前准备好的容器视图中,并请求焦点。
private void start(){
hostHandle.start(_TAG_, new OnTJHostHandleListener() {
@Override
public void onSuccess(){
View gameView = hostHandle.getGameView();
if (gameContainer.indexOfChild(gameView) == -1) {
gameContainer.addView(gameView);
}
gameView.requestFocus();
play();
}
@Override
public void onFailure(Throwable throwable){
Log._e_(_TAG_, "Host start game failure", throwable);
}
});
}最后,调用 play 启动游戏,小游戏就正式跑起来啦 !
privatevoidplay(){
// Resume game
hostHandle.play(new OnTJHostHandleListener() {
@Override
public void onSuccess(){
Log._i_(_TAG_, "Host play game success");
}
@Override
public void onFailure(Throwable throwable){
Log._e_(_TAG_, "Host play game failure", throwable);
}
});
}整个流程虽然看起来有些步骤,但实际上操作起来还是比较顺畅的,只要前期配置好,后续想接入更多小游戏也很简单,只需要在控制台创建新的小游戏并且上传对应的游戏包,启动游戏时设置不同的 LAUNCH_KEY 即可轻松地实现横向拓展!
四、让宿主更聪明的一点点魔法 ✨
在小游戏成功跑起来之后,我们并没有就此收工。毕竟,“能跑”只是第一步,“跑得好”才是我们真正的目标。于是,我们开始通过宿主 SDK 提供的能力给宿主应用施加一些“魔法加成”,让它变得更聪明更贴心。
这一阶段的优化,主要围绕小游戏宿主生态中的三类角色展开:
宿主应用的使用者
小游戏的开发者
宿主应用的开发者
4.1宿主应用使用者
对于宿主应用使用者(最终用户)来说,我们的目标是:让感兴趣的用户可以方便地发现并启动想玩的游戏,同时确保小游戏的引入不会影响到 APP 原有的稳定性和功能;而对于不感兴趣的用户,我们希望尽可能降低宿主 SDK 的存在感,做到“无感接入”。
4.1.1 获取游戏列表:让用户看到能玩什么
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/server-integrate
在接入小游戏宿主之后,我们要面对的第一个问题就是:怎么把可玩的小游戏清晰地展示给用户?毕竟,游戏再好玩,用户找不到也是白搭。
好消息是,宿主平台已经为我们准备好了相关的服务端 API,专门用来获取当前宿主应用下已关联的小游戏列表:
curl --location 'https://minihost.tuanjie.cn/api/game/list?appId=**AppID**' \
--header 'Authorization: Bearer **ServiceToken**'这个接口需要两个参数:
AppID:宿主应用的 ID
ServiceToken:服务端访问令牌
它们和前面提到的App Key/App Secret类似,都可以在宿主控制台中获取。
接口返回的内容包含了所有已发布小游戏的元信息,其中的 launchKey 字段就是启动游戏时要用到的关键参数LAUNCH_KEY。
[
{
"id": "67fe09f272ef630b77c67089",
"appId": "67fe05f972ef630b77c663b3",
"name": "SpinningCube",
"gameType": "weixinminigame",
"tags": [
"Cube"
],
"iconUrl":
"https://minihost.cdn.tuanjie.cn/game_app/67bab0495795e8aac375737e/app_icon/cube.9c353a166f724813b0b2f5742bc1a19f.png",
"briefIntro": "A Spinning Cube",
"launchKey": "67fe09f272ef630b77c67089"
}
]虽然我们可以直接在客户端请求这个 API 并展示游戏列表,但这样做灵活性有限,比如想要实现:
搜索 / 关键词过滤
自定义排序(按热度、更新时间等)
分页加载
添加自定义字段(如“推荐标签”、“开发者信息”等)
这些功能在客户端处理起来就比较麻烦了。
所以,我们更推荐的做法是:
由 Connect 的服务端定期或按需调用宿主平台的 API,拉取并缓存游戏列表
然后由 Connect 自己的服务端封装一个更适合客户端使用的 API(结构更简洁、字段更定制)
客户端只需请求这个自定义 API,即可获取游戏列表并展示在 UI 中
当用户点击“启动”按钮时,只需要将对应游戏的 launchKey 作为参数传入宿主 SDK,即可启动该小游戏。通过这一套流程,我们不仅让用户能快速看到可玩的游戏,也为后续的个性化推荐、运营配置等功能打下了基础。毕竟,游戏列表是入口,入口体验好,用户才有可能留下来玩更多
4.1.2 游玩历史:让用户不错过每一场精彩
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/android/advanced-usage-%E6%9C%80%E8%BF%91%E6%B8%B8%E7%8E%A9%E5%88%97%E8%A1%A8
随着宿主应用中接入的小游戏越来越多,我们也意识到:用户可能需要快速找回之前玩过的游戏。于是,我们加入了「游玩历史」功能。宿主平台也提供了相关 API,支持通过 userId(与设置游戏启动参数时的 USER_ID 一致)获取用户的历史记录。例如:
publicstatic List
getUserHistory(Context context, String userId) // e.g. List historyList = TJHostHandle.getUserHistory(context, "user_id");
返回的历史记录是一个 HistoryModel 列表,每个 HistoryModel 包含了启动游戏的详细信息,字段说明如下:
publicclass HistoryModel {
String id; // Game ID
String versionId; // Game Version ID
String gameType; // Game Type
String name; // Game Name
List
tags; // Game Tags String iconUrl; // Game Icon Url String briefIntro; // Game Brief Introduction String launchKey; // Game Launch Key, **Primary Key** boolean dev; // Is Development Version String userId; // User ID long lastLaunchTime; // Last Launched Time }
需要注意的是,同一个小游戏可能会出现多个版本(比如用户通过扫码体验了某个测试包),这时返回的 launchKey 会包含具体的版本信息。通过该 launchKey 启动游戏时,宿主 SDK 会自动选择对应版本的游戏包进行加载,确保用户体验一致。
✨ 更进一步:自定义存储游玩历史
当然,如果你希望自行管理这些历史记录,宿主 SDK 还支持通过注入自定义的 HistorySaver 接口来自定义存储逻辑。
public void setHistorySaver(HistorySaver saver)
public interface HistorySaver {
void save(HistoryModel history);
}我们可以在宿主应用里实现一个 HistorySaver,在 save 方法中将用户的游玩记录上传到自己的服务端数据库中。这样,不仅可以实现跨设备同步,也能为后续的个性化推荐打好基础。
4.1.3 多进程 & 多任务栈:小游戏也能“开多个窗口”
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/android/quick-integrate#31-%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%90%AF%E5%8A%A8%E5%99%A8
有时候,用户希望能在宿主应用主界面和小游戏之间自由切换——毕竟上班摸鱼的时候,反应要快,可不能被发现了。另外,如果某个游戏包出现了 bug 导致崩溃或卡死,我们也不希望影响到整个宿主 App 的稳定性。
为了解决这些问题,我们采用了多进程 + 多任务栈的方案,让每个小游戏都能在独立的“窗口”中运行,互不干扰、互不拖累,资源也能各自独立管理。
最简单的实现方式是我们可以在 AndroidManifest.xml 中为游戏 Activity 指定一个独立的进程名和任务栈名,让它运行在自己的空间里:
".host.GameActivity"
android:screenOrientation="portrait"
android:configChanges="keyboard|orientation|screenSize|keyboardHidden|navigation|screenLayout"
android:launchMode="singleTask"
android:process=":hostProc"
android:taskAffinity="com.unity3d.unityconnect.host.affinity">如果我们希望支持多个小游戏同时运行,只需要多声明几个继承自 GameActivity 的子类,并在 Manifest 中为它们配置不同的进程名和任务栈名:
public class GemeActivity1 extends GameActivity {}
public class GemeActivity2 extends GameActivity {}这样,在运行时我们就可以通过 startActivity 启动不同的游戏 Activity,实现多个小游戏并行运行,各自独立互不干扰。
如果游戏已经在后台,再次点击还能“唤醒”吗?
当然可以!为了更方便地管理这些多进程的小游戏,宿主 SDK 提供了一个实用的工具类:MultiProcessLauncher。它专门用来解决以下几个问题:
启动新的小游戏时,自动顶掉最早启动的旧游戏
如果游戏已经在后台,点击时自动拉到前台
管理多个游戏容器的生命周期和状态
它主要包含两个接口:
configContainer:注册所有可用的游戏容器(Activity + 进程)
launch:通过 launchKey 启动或唤醒游戏
还包含一个默认的生命周期监视器实现 MultiProcessLauncher.MiniGameLifeCycle 用于监测小游戏状态。通过它们我们能直接实现各小游戏进程的管理。
我们先需要在游戏 Activity onCreate 中添加生命周期监视器,从而使得多进程监视器能够动态检测到游戏 Activity 的各项生命周期回调。
privatevoidinitLifeCycleObserver() {
String launchKey = getIntent().getStringExtra("launchKey");
gameLifeCycle = new MultiProcessLauncher.MiniGameLifeCycle(this, launchKey);
getLifecycle().addObserver(gameLifeCycle);
}假设我们希望最多支持两个小游戏同时运行,那么可以在 AndroidManifest 中声明两个容器,并使用 configContainer 将它们注册进多进程启动器:
privatevoidinitMultiProcessLauncher(){
Map
map = new HashMap<>(); map.put(":hostProc1", GameActivity1.class); map.put(":hostProc2", GameActivity2.class); MultiProcessLauncher.configContainer(map); }
在用户点击某个游戏时,调用 launch 方法并传入 launchKey,启动器会自动判断:
如果游戏已经在后台,会将其拉到前台
如果没有在后台,但容器已满,则顶掉最早启动的那个游戏
MultiProcessLauncher.launch(context, "launchKey", intent -> {
intent.putExtra("launchKey", launchkey);
// And other params if needed
});此外,Android 10 后 WebView 在多进程中的使用做了限制,每个进程只能访问自己的网络数据,因此需要为每个进程制定数据存储目录。我们可以在游戏 Activity 初始化时,简单使用进程名作为数据目录名称以避免报错。
WebView.setDataDirectorySuffix("processName");通过这种方式,我们不仅实现了小游戏之间的并行运行,也大大提升了宿主应用的稳定性和灵活性。用户可以像在浏览器中切换标签页一样,在多个小游戏间自由切换,真正实现“多开不是梦”。
4.1.4 支持动态加载:精简 APP 体积的魔法
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/android/quick-integrate#32-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-so-%E5%BA%93
前文提到,宿主 SDK 提供了两种版本:静态版和动态版。
静态版的优势在于接入简单、运行稳定,并且能一定程度上保证 SDK 的完整性和安全性。但它也有一个明显的短板——体积大。打包后 AAR 文件将近 40MB,对于宿主 App 来说,这可能会显著增加安装包大小,尤其对新用户或对小游戏不感兴趣的用户来说,这部分体积是“沉重”的负担 。
为了解决这个问题,Connect 选择使用了宿主 SDK 的动态版本。它的核心优势是轻量,AAR 包体不到 1MB。小游戏运行环境不再一开始就集成进 App,而是在真正需要的时候“按需加载”。
比如,当用户点击进入「游戏中心」时,我们再触发动态加载逻辑,下载小游戏运行所需的依赖。
因此宿主 SDK 提供了 prepare 方法,用于动态加载运行环境:
TJHost._prepare_(this, new OnLoadSharedLibraryListener() {
@Override
publicvoidonSuccess(String s){
l_ibPath _= s;
_isTJHostHandleInitialized _= true;
}
@Override
public void onDownloading(int progress){
// update UI
}
@Override
public void onFailure(Throwable throwable){
// retry or toast
}
});这个接口支持加载进度回调,可以配合 UI 展示加载动画或进度条,提升用户体验。
加载成功后,我们会拿到一个 libPath,也就是运行环境的本地路径。接下来,在启动游戏的 Activity 中,我们只需要在游戏 Activity onCreate 的时候调用以下接口,将这个路径注入到运行环境中即可:
boolean result = TJHost._installNativeLibraryPath_(getApplicationContext(), l_ibPath_);一旦注入成功,小游戏就可以像静态版一样正常运行,用户几乎感知不到差异。通过动态加载机制,我们成功地将小游戏运行环境“从包里搬到了云上”,不仅减轻了宿主 App 的初始体积负担,也为后续更灵活的按需加载、分模块更新打下了基础。
4.2 小游戏开发者
对于小游戏开发者来说,完整走一遍“创建宿主应用 → 创建宿主小游戏 → 上传游戏包 → 发布”这一整套流程,虽然是正式上线所必需的,但如果只是想快速验证一个小游戏包的运行效果,流程就显得有些繁琐了。
尤其是在开发阶段,我们更希望有一种便捷的方式,能快速启动某个小游戏包进行调试,甚至还能将这个方式分享给其他开发者或测试人员一起使用。同时,如果能配合一些调试工具,那开发效率自然也能大大提升。
4.2.1 扫码启动小游戏:一键体验更高效
参考文档:https://minihost.tuanjie.cn/help/docs/quickstart/create-game#3-%E6%89%AB%E7%A0%81%E4%BD%93%E9%AA%8C%E6%B8%B8%E6%88%8F
为了解决这个问题,我们首先想到的就是——扫码启动。
在宿主控制台中,每一个小游戏的线上版本,以及每一个上传的游戏包版本,都会自动生成一个二维码,支持扫码体验:
宿主应用通过这个二维码启动游戏的方式也非常简单:
扫描二维码,获取其中的字符串内容(这是一个特殊格式的 launchKey)
将该字符串作为游戏启动参数中的 LAUNCH_KEY 传入宿主 SDK
宿主 SDK 会自动识别该 launchKey 对应的游戏包信息,下载对应版本并启动游戏
宿主应用通过这个二维码启动游戏的方式也非常简单,只需要扫描获取二维码的对应的字符串,并且设置为游戏启动参数中的 LAUNCH_KEY 即可。
这个二维码启动方式的最大优势在于——没有组织权限限制。也就是说,即使你的宿主应用与小游戏不属于同一个组织,也可以通过扫码启动游戏,非常适合在开发、测试阶段跨团队协作使用。
此外,通过扫码启动的游戏,其游玩历史记录中的 dev 字段会被标记为 true,表示这是一个开发版本。同时,如果你多次扫码体验同一个小游戏的不同版本(比如测试包、灰度包等),系统也会为每个版本记录一条独立的历史记录,因为此时的 launchKey 会携带具体的版本信息。
扫码启动不仅让小游戏开发者调试更高效,也为测试人员、产品经理等非技术角色提供了一个无需打包、无需配置就能「一键体验」的便捷入口。再配合宿主 SDK 提供的调试工具,整个开发流程也就更加顺畅了。
4.2.2 小游戏 C# 代码调试支持:让问题排查更高效
在开发过程中,遇到小游戏中出现一些不符合预期的行为是常有的事。虽然我们可以通过游戏内的日志来初步判断问题的大致范围,但要精准定位问题根源,很多开发者还是希望能够通过调试工具直接查看代码运行逻辑。
不过,与在 Unity 编辑器中直接运行游戏不同,在宿主环境中运行小游戏时,我们无法像平常那样使用 IDE(如 Rider 或 Visual Studio)直接启动调试模式。这就对调试过程提出了新的挑战。
为了帮助开发者更高效地排查问题,宿主 SDK 提供了一个便捷的调试入口,允许宿主应用在合适的时机(比如用户点击某个按钮时)打开调试面板:
hostHandle.showDebugMenu();调试面板中包含了查看小游戏运行过程中的性能数据、启用 C# 代码等多个实用功能,
当点击「C# 调试」按钮后,宿主 SDK 会自动启动一个远程调试服务。此时,我们可以在 Rider 中通过 Attach to Process 的方式,连接到正在运行的小游戏进程,从而实现在项目代码中设置断点、逐步调试等功能。
通过这个调试面板,开发者不仅可以快速了解小游戏的运行状态,还能像在本地调试一样深入排查问题逻辑,大大提升了开发和测试效率。
4.3宿主应用开发者
对于宿主应用的开发者来说,无论是为了打造“超级 App”而引入宿主 SDK,还是计划用宿主方案替代原有的小游戏平台,最关键的问题始终是:小游戏接入之后,实际表现到底如何?
这里的“表现”不仅仅是访问量、启动次数这些表层数据,更重要的是小游戏在运行过程中的性能表现、稳定性、用户留存等核心指标。这些数据直接关系到:
小游戏是否值得继续推广
哪些内容更受用户欢迎
如何有针对性地进行优化和迭代
通过对这些数据的深入分析,我们可以更科学地评估宿主小游戏的整体质量和用户接受度,从而做出更明智的产品决策。
4.3.1 小游戏数据看板:一目了然的表现评估
为了解决上述问题,宿主控制台提供了专门的数据分析页面,帮助我们快速掌握小游戏的关键表现指标。
在这个页面中,我们可以按时间维度筛选数据,查看每个小游戏的:
性能表现(如加载耗时、帧率等)
用户访问情况(如启动次数、活跃用户数)
留存情况(次日、7 日留存等)
这些数据不仅帮助我们与小游戏开发者一起定位性能瓶颈、优化用户体验,也为后续的推广策略和买量投放提供了重要参考。
数据是最好的反馈机制。通过宿主控制台的数据看板,我们可以从“感觉不错”转向“数据说话”,让小游戏生态的优化更加精准、可持续。
上述这一系列优化改动,虽然看起来只是“锦上添花”,但它们让宿主应用的体验更加完整,也让我们在产品层面有了更多可发挥的空间!
五、乐高积木式的平台能力接入
小游戏跑起来之后,我们的工作并没有结束。下一步,就是思考如何把宿主应用的“平台能力”开放给小游戏使用。
在运行过程中,小游戏可能会调用一些平台能力的 JS 接口,比如登录、广告、支付等。由于宿主 SDK 本身并不内置这些功能(例如账号体系或广告服务),所以这些接口的具体实现就需要由宿主应用来完成。
那问题来了:如何像搭乐高积木一样,按需接入这些模块化的能力,并通过统一的方式暴露给小游戏使用呢?
5.1接入平台登录能力
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/android/platform-integrate#1-%E7%99%BB%E5%BD%95-sdk
首先,我们来看看宿主小游戏的登录流程:
当小游戏启动时,通常会调用平台的登录接口(如微信中的 wx.login,在宿主环境下对应的是 tj.login)。这个调用会提示宿主应用拉起登录浮层,引导用户完成登录。
登录成功后,宿主应用会返回一个临时凭证 code,小游戏客户端将该 code 传给自己的服务端,服务端再通过宿主提供的 Code2Session 接口换取用户的登录态信息(Session)。这个 Session 中包含了用户在当前小游戏中的唯一标识 OpenID,游戏可以通过这个 OpenID 来读取玩家的云存档或本地数据。
所以,宿主应用如果想支持登录能力,需要完成两件事:
在客户端实现 tj.login 这个 JS API,让小游戏能拿到 code
在服务端实现 Code2Session 接口,供小游戏服务端换取 Session
5.1.1 宿主应用实现 JS API:把 Native 能力暴露给 JS
有些小伙伴可能会疑惑:宿主是用 Java/Kotlin 开发的,小游戏是运行在 JS 环境中的,怎么把 Native 能力暴露给 JS 呢?
别担心,宿主 SDK 已经为我们准备好了 桥接接口 ,支持从 JS 调用 Native 方法 :
tj.customCommand(Object object, string methodName, ...);
tj.customCommandSync(Object object, String methodName, ...);桥接接口:https://minihost.tuanjie.cn/help/docs/api/android/custom-command#js-%E5%B1%82%E8%B0%83%E7%94%A8-java-%E5%B1%82%E6%8E%A5%E5%8F%A3
那么下一个想到的问题是,在宿主应用内如何介入小游戏所运行在的 JS 环境呢?我们可以通过启动参数 BUILTIN_CUSTOM_SCRIPTS_PATH 向小游戏环境中注入自定义 JS 文件。例如:
bundle.putString(TJConstants._BUILTIN_CUSTOM_SCRIPTS_PATH_, "customScripts");这样,宿主应用就会加载 assets/customScripts 目录下的 JS 文件,并注入到小游戏的运行环境中。
那么有了上面这些信息,我们知道在宿主应用内实现一个 JS API 可以通过在启动游戏时注入一些 JS 文件,文件中定义了小游戏需要调用的 JS API 如 tj.login 的方式实现,而这些 JS API 的最终实现则可以通过 customCommand 接口将其从 JS 层转移到 Native 层。
那么让我们来大展身手吧!我们可以在 assets/customScripts 目录下创建一个 auth.js 文件,定义 tj.login 方法。它的核心逻辑是调用 customCommand 接口,转发到 Native 层的 loginUnity 方法:
tj.login = function (obj) {
tj.customCommand({
success: function(res) {
let resObj = JSON.parse(res);
obj.success(resObj);
},
fail: function(res) {
let apiRes = {
errMsg: res.errorMsg,
errno: res.errorCode
};
obj.fail(apiRes);
},
complete: function() {
obj.complete();
}
}, "loginUnity");
};在 Java 端,我们需要在 setGameStartOptions 之后,设置一个自定义命令监听,处理来自 JS 的 loginUnity 调用:
hostHandle.setCustomCommandListener("loginUnity", (customCommandHandle, bundle) -> {
try {
String code = LoginSDK.getInstance().getUserCode();
JSONObject loginRes = new JSONObject();
loginRes.put("code", code);
customCommandHandle.pushResult(loginRes.toString());
customCommandHandle.success();
} catch (RemoteException e) {
customCommandHandle.fail(1001, "Failed to connect Login Service: " + e.getMessage());
} catch (Exception e) {
customCommandHandle.fail(1002, "Failed to serialize login result: " + e.getMessage());
}
});完成以上配置后,小游戏就可以通过调用 tj.login 获取宿主应用返回的 code,完成登录流程。整个过程就像拼装乐高一样:JS 和 Native 各自实现一块,通过宿主 SDK 提供的桥梁拼接在一起,既灵活又高效。
5.1.2 宿主应用服务端配置:Code 换 Session 的“最后一公里”
参考文档:https://minihost.tuanjie.cn/help/docs/sdk/server-integrate#jscode2session
前面我们已经完成了客户端的登录流程:小游戏通过调用 tj.login 拿到了登录凭证 code。接下来,就轮到服务端登场啦!
小游戏客户端会把这个 code 传给自己的游戏服务器,而游戏服务器则需要通过宿主应用的服务端接口,将 code 换成包含用户身份信息的 Session(比如 openId、session_key 等)。这一步就相当于登录流程的“最后一公里”。
为了让这个流程顺利跑通,我们需要做两件小事:
在宿主应用的服务端实现一个 Code2Session 接口,负责接收 code 并返回用户的 Session 信息。
在宿主控制台中,配置这个接口的地址,也就是“平台方接入 URL”。
配置完成后,宿主平台会自动将小游戏传来的 code 转发给你配置的接口,并将返回的 Session 信息传回给小游戏服务端。
通过这一步,小游戏的登录流程就真正打通了:从宿主应用拉起登录 → 拿到 Code → 宿主应用服务端换取 Session → 获取用户身份,是不是感觉已经可以愉快地登录了?
登录能力的接入,主要是为了确保每个需要用户身份的小游戏都能顺利运行。而除了登录之外,另一个我们非常关注的平台能力就是——广告。
作为小游戏变现的重要手段,广告能力的接入自然是优先级很高的一项工作。无论是激励视频、插屏广告,还是 Banner 展示,合理的广告接入不仅能带来收入,也能为小游戏生态带来更强的可持续性。
具体的广告能力实现方式可以参考宿主 SDK 的官方文档,里面对接入流程、回调处理等内容都有详细说明。这里我们就不展开赘述啦,感兴趣的小伙伴可以自行查阅。
六、我们的下一站
从 Connect 成为小游戏宿主的第一块“试验田”开始,我们一步步搭起了这片数字田地的基础设施:小游戏能跑起来了,宿主能变得更聪明了,平台能力也像乐高一样拼装上了。
但这只是开始。
未来,我们还将持续优化宿主 SDK 的接入体验,降低平台能力扩展的门槛,提供更丰富的调试工具、更灵活的运营能力、更智能的数据分析支持。我们也在探索更多引擎的兼容性、更广泛的平台适配,以及更轻量的运行时架构,力求让每一个 App 都能轻松拥有自己的小游戏生态。
当然,一个生态的繁荣,永远不是靠一个宿主应用就能完成的。我们希望有更多开发者、内容创作者、平台伙伴加入我们,一起耕耘、一起试验、一起收获——把这片“宿主田地”种得更大、更好、更有生命力。
毕竟,在这个赛博世界里,我们都是数字田地里的“农夫”——用代码种下希望,用创意收获乐趣。未来的宿主生态,就等我们一起开垦、一起灌溉、一起收成。
广阔天地,大有可为。我们的下一站,就在前方。
Unity 官方微信
第一时间了解Unity引擎动向,学习进阶开发技能
每一个“点赞”、“在看”,都是我们前进的动力
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.