本文记录了将引力引擎 SDK 接入抖音小游戏(Unity)的完整过程,包含云函数搭建、openId 获取、SDK 初始化、注册事件上报等所有细节,新手按步骤操作即可完成接入。
文章目录
-
- [1. 背景与目标](#1. 背景与目标)
- [2. 准备工作](#2. 准备工作)
-
- [2.1 引力引擎后台参数](#2.1 引力引擎后台参数)
- [2.2 抖音开发者后台参数](#2.2 抖音开发者后台参数)
- [2.3 Unity 工程准备](#2.3 Unity 工程准备)
- [3. 关键概念澄清](#3. 关键概念澄清)
-
- [openId 与 anonymousCode 的区别](#openId 与 anonymousCode 的区别)
- 为什么需要云函数
- [4. 架构设计](#4. 架构设计)
- [5. 第一步:搭建腾讯云函数(为了获取抖音OpenID)](#5. 第一步:搭建腾讯云函数(为了获取抖音OpenID))
-
- [5.1 创建云函数](#5.1 创建云函数)
- [5.2 编写云函数代码](#5.2 编写云函数代码)
- [5.3 配置环境变量](#5.3 配置环境变量)
- [5.4 配置公网访问](#5.4 配置公网访问)
- [5.5 部署并发布版本](#5.5 部署并发布版本)
- [5.6 验证云函数](#5.6 验证云函数)
- [6. 第二步:Unity 接入引力引擎 SDK](#6. 第二步:Unity 接入引力引擎 SDK)
-
- [6.1 创建 GravityEngineManager.cs](#6.1 创建 GravityEngineManager.cs)
- [7. 第三步:修改游戏启动逻辑](#7. 第三步:修改游戏启动逻辑)
- [8. 第四步:配置抖音合法域名](#8. 第四步:配置抖音合法域名)
- [9. 验证与测试](#9. 验证与测试)
- 在这里插入图片描述
- [10. 常见问题排查](#10. 常见问题排查)
-
- [❌ `HTTP/1.1 443 Unknown HTTP status`](#❌
HTTP/1.1 443 Unknown HTTP status) - [❌ `error: 2, errmsg: "bad secret"`](#❌
error: 2, errmsg: "bad secret") - [❌ `error: 1, errmsg: "未注册的 appid"`](#❌
error: 1, errmsg: "未注册的 appid") - [❌ `[GravityEngine] 初始化失败: name must be required`](#❌
[GravityEngine] 初始化失败: name must be required) - [❌ `[GravityEngine] 初始化失败: 参数错误`](#❌
[GravityEngine] 初始化失败: 参数错误)
- [❌ `HTTP/1.1 443 Unknown HTTP status`](#❌
- 总结
1. 背景与目标
引力引擎(Gravity Engine)是字节跳动旗下的广告归因与用户分析平台,用于统计小游戏广告变现效果。
目标:
- 在抖音小游戏中接入引力引擎 SDK
- 使用抖音真实
openId作为用户标识(clientId) - 上报用户注册事件
$MPRegister(引力引擎要求的买量归因必要事件)
仅上报注册事件 ,广告展示($AdShow)由引力引擎后台从抖音自动拉取,无需客户端手动上报。
2. 准备工作
2.1 引力引擎后台参数
登录 引力引擎后台 获取以下参数:
| 参数 | 说明 | 示例 |
|---|---|---|
| Access Token | SDK 鉴权 Token | LZh7dWzhqb**** |
| App ID(包名) | 抖音小游戏 appid | tt0c5c7ae1923c3***** |
2.2 抖音开发者后台参数
登录 抖音开发者后台 → 开发管理 → 开发设置,获取:
| 参数 | 说明 |
|---|---|
| AppID | 小游戏唯一标识,如 tt0c5c7ae1923c3***** |
| AppSecret | 用于服务端换取 openId,不能暴露在客户端 |
2.3 Unity 工程准备
-
导入
GravityEngine-x.x.x.unitypackage(从引力引擎后台下载) -
确保已接入头条
StarkSDK/TTSDK插件 -
在 Unity 的 Player Settings → Scripting Define Symbols 中添加:
GRAVITY_BYTEDANCE_TT_GAME_MODE
3. 关键概念澄清
openId 与 anonymousCode 的区别
这是接入时最容易混淆的地方:
| 标识符 | 来源 | 长度 | 是否为真正 openId |
|---|---|---|---|
anonymousCode |
TT.Login 客户端回调 |
100+ 字符(开发者工具中尤其长) | 否,是匿名临时标识 |
| 真正的 openId | 服务端 code2Session 换取 |
约 36 字符 | 是 |
引力引擎要求 clientId 必须为抖音 openId 原值 ,因此必须通过 code2Session 获取真实 openId。
为什么需要云函数
调用 code2Session 接口需要传 AppSecret,而 AppSecret 不能放在客户端代码中(违反抖音安全规范,可能导致小游戏下架)。
解决方案: 用腾讯云函数做中转服务,AppSecret 存在云端环境变量中,客户端只传 appid 和 code。
4. 架构设计
Unity 客户端
│ POST { appid, code } + Header: x-api-key
▼
腾讯云函数(GetOpenID)
│ 根据 appid 查找 AppSecret
│ 调用抖音 code2Session API
▼
抖音服务端
│ 返回真实 openid
▼
云函数 → 只返回 openid(过滤敏感的 session_key)
▼
Unity 客户端
│ 缓存 openId 到 PlayerPrefs
│ 初始化引力引擎 SDK
▼
引力引擎后台
└ 上报 $MPRegister 注册事件
多项目复用: 云函数通过 JSON 映射表支持多个项目,添加新项目只需修改环境变量,无需改代码。
5. 第一步:搭建腾讯云函数(为了获取抖音OpenID)
如果你已经获取过了,可以跳过这一步
为什么一定要云函数或者服务器?因为获取OpenID需要向抖音传递AppSecret参数,AppSecret 放在客户端存在泄露风险,抖音文档明确建议仅在服务端使用。
5.1 创建云函数
- 登录 腾讯云函数控制台
- 点击 新建,配置如下:
| 配置项 | 值 |
|---|---|
| 函数类型 | Web 函数(重要!不要选事件函数) |
| 函数名称 | GetOpenID |
| 地域 | 广州(延迟低) |
| 运行环境 | Node.js 16.13 |
| 提交方法 | 在线编辑 |
| 执行方法 | index.main_handler |
5.2 编写云函数代码
在在线编辑器中,找到 app.js,全部替换为以下代码:
javascript
/**
* 腾讯云函数 Web 函数 - 抖音小游戏 code2Session 中转服务
* 零依赖,使用 Node.js 内置模块
*
* 环境变量:
* APP_SECRETS = {"tt你的appid1":"AppSecret1","tt你的appid2":"AppSecret2"}
* API_KEY = 自定义随机字符串(防止接口被滥用)
*/
const http = require('http');
const https = require('https');
let secretsMap = null;
function getSecretsMap() {
if (secretsMap) return secretsMap;
try {
secretsMap = JSON.parse(process.env.APP_SECRETS || '{}');
} catch (e) {
console.error('APP_SECRETS 解析失败:', e.message);
secretsMap = {};
}
return secretsMap;
}
function httpsGet(url) {
return new Promise((resolve, reject) => {
const req = https.get(url, { timeout: 5000 }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => resolve(body));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
});
}
function readBody(req) {
return new Promise((resolve) => {
let raw = '';
req.on('data', (chunk) => raw += chunk);
req.on('end', () => resolve(raw));
});
}
function send(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
return send(res, 405, { error: 1, errmsg: '仅支持 POST 请求' });
}
// API Key 鉴权
const expectedKey = process.env.API_KEY;
if (expectedKey) {
const clientKey = req.headers['x-api-key'] || '';
if (clientKey !== expectedKey) {
return send(res, 403, { error: 1, errmsg: '无效的 API Key' });
}
}
// 解析请求体
let body;
try {
const raw = await readBody(req);
body = JSON.parse(raw || '{}');
} catch (e) {
return send(res, 400, { error: 1, errmsg: '请求体 JSON 格式错误' });
}
const { appid, code, anonymous_code } = body;
if (!appid) return send(res, 400, { error: 1, errmsg: '缺少 appid' });
if (!code && !anonymous_code) return send(res, 400, { error: 1, errmsg: '缺少 code 或 anonymous_code' });
// 查找对应 AppSecret
const secret = getSecretsMap()[appid];
if (!secret) {
console.error(`未注册的 appid: ${appid}`);
return send(res, 400, { error: 1, errmsg: '未注册的 appid' });
}
// 调用抖音 code2Session
let url = `https://minigame.zijieapi.com/mgplatform/api/apps/jscode2session?appid=${appid}&secret=${secret}`;
if (code) url += `&code=${encodeURIComponent(code)}`;
if (anonymous_code) url += `&anonymous_code=${encodeURIComponent(anonymous_code)}`;
try {
const raw = await httpsGet(url);
const data = JSON.parse(raw);
console.log(`code2Session appid=${appid} error=${data.error} hasOpenid=${!!data.openid}`);
// 只返回必要字段,不暴露 session_key
return send(res, 200, {
error: data.error || 0,
openid: data.openid || '',
anonymous_openid: data.anonymous_openid || '',
errmsg: data.errmsg || data.message || ''
});
} catch (e) {
console.error('code2Session 异常:', e.message);
return send(res, 500, { error: 1, errmsg: '服务端请求异常' });
}
});
// Web 函数必须监听 9000 端口
server.listen(9000, () => {
console.log('GetOpenID 服务已启动,监听端口 9000');
});
5.3 配置环境变量
在创建页面展开 高级配置 → 环境配置,添加两条环境变量:
| 变量名 | 值 | 说明 |
|---|---|---|
APP_SECRETS |
{"tt你的appid":"你的AppSecret"} |
多项目用逗号分隔加在同一个 JSON 里 |
API_KEY |
任意随机字符串,如 MyKey2026abcXYZ |
客户端请求时携带,防止接口被滥用 |
多项目示例:
json
{
"tt0c5c7ae1923c3*****": "AppSecret_游戏1",
"tt511627abcf779*****": "AppSecret_游戏2"
}
⚠️ 注意:
APP_SECRETS的 key 必须是tt开头的抖音 appid,不是其他格式
其他配置:
| 配置项 | 值 |
|---|---|
| 执行超时时间 | 改为 10 秒(默认 3 秒不够) |
5.4 配置公网访问
展开 函数 URL 配置 ,勾选 公网访问 ,鉴权类型选 开放(我们自己用 API_KEY 鉴权)。
5.5 部署并发布版本
-
点击 完成 创建函数
-
进入函数后,点 版本管理 → 发布新版本
-
将新版本流量设为 100%
-
在 函数 URL 页面记录公网访问 URL,格式如:
https://1255346748-xxxxxxxx.ap-guangzhou.tencentscf.com
5.6 验证云函数
在命令行测试(PowerShell):
powershell
Invoke-RestMethod `
-Uri "https://你的云函数URL" `
-Method POST `
-ContentType "application/json" `
-Headers @{"x-api-key"="你的API_KEY"} `
-Body '{"appid":"tt你的appid","code":"test123"}'
预期返回(test123 是假 code,抖音会报错,但说明云函数本身跑通):
json
{"error": 2, "openid": "", "errmsg": "bad secret"}
如果返回
443 exit unexpected,说明云函数没有成功启动,检查代码是否完整粘贴并重新发布版本。
6. 第二步:Unity 接入引力引擎 SDK
6.1 创建 GravityEngineManager.cs
在 Assets/Scripts/Analytics/ 目录下创建 GravityEngineManager.cs:
csharp
using System.Collections;
using System.Collections.Generic;
using Com.Arcanemiracle;
using GravityEngine;
using UnityEngine;
using UnityEngine.Networking;
#if SOLARENGINE_BYTEDANCE
using TTSDK;
#endif
/// <summary>
/// 引力引擎SDK管理器(抖音小游戏 GRAVITY_BYTEDANCE_TT_GAME_MODE)
/// 接入说明:
/// - 仅上报 $MPRegister 用户注册事件
/// - clientId 必须使用抖音真实 openId(通过 code2Session 服务端接口换取)
/// - 启动流程:有缓存 openId 直接启动;否则 TT.Login → 云函数 → 获取真实 openId
/// </summary>
public class GravityEngineManager : UnitySingleton<GravityEngineManager>
{
// 引力引擎 Access Token(在引力引擎后台获取)
private const string ACCESS_TOKEN = "你的引力引擎AccessToken";
// 云函数配置(AppSecret 安全存储在云端,不在客户端暴露)
private const string DOUYIN_APPID = "tt你的抖音AppID";
private const string CODE2SESSION_URL = "https://你的云函数URL";
private const string CODE2SESSION_API_KEY = "你设置的API_KEY";
// PlayerPrefs 缓存键
private const string PREF_REAL_OPENID = "GravityEngine_RealOpenId";
private const string PREF_NICKNAME = "GravityEngine_NickName";
private const string PREF_REGISTERED = "GravityEngine_Registered";
private bool _isSdkStarted = false;
public override void Awake()
{
base.Awake();
}
/// <summary>
/// TT SDK 初始化完成后调用(在 TT.InitSDK 回调中触发)
/// 有缓存真实 openId 则直接启动;否则走 TT.Login + code2Session 流程
/// </summary>
public void InitAfterTTSDKReady()
{
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
string cachedOpenId = PlayerPrefs.GetString(PREF_REAL_OPENID, "");
string cachedNickName = PlayerPrefs.GetString(PREF_NICKNAME, "");
if (!string.IsNullOrEmpty(cachedOpenId))
{
Debug.Log($"[GravityEngine] 使用缓存的真实 openId: {cachedOpenId}");
StartAndInitSDK(cachedOpenId, cachedNickName);
}
else
{
FetchRealOpenId();
}
#endif
}
/// <summary>
/// TT.Login(forceLogin:true) 获取 code,再通过云函数换取真实 openId
/// 抖音宿主中用户已登录,forceLogin:true 静默完成,不弹窗
/// 若失败,降级到 anonymous_code → anonymous_openid
/// </summary>
private void FetchRealOpenId()
{
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
TT.Login(
successCallback: (code, anonymousCode, isLogin) =>
{
Debug.Log($"[GravityEngine] TT.Login 成功 - code长度: {code?.Length ?? 0}, isLogin: {isLogin}");
if (!string.IsNullOrEmpty(code))
{
StartCoroutine(RequestCode2Session(code));
}
else if (!string.IsNullOrEmpty(anonymousCode))
{
Debug.LogWarning("[GravityEngine] code 为空,降级使用 anonymousCode 换取 anonymous_openid");
StartCoroutine(RequestCode2SessionAnonymous(anonymousCode));
}
else
{
Debug.LogError("[GravityEngine] code 和 anonymousCode 均为空,无法初始化");
}
},
failedCallback: (errMsg) =>
{
Debug.LogError($"[GravityEngine] TT.Login(forceLogin:true) 失败: {errMsg},尝试匿名登录");
TT.Login(
successCallback: (code2, anonymousCode2, isLogin2) =>
{
if (!string.IsNullOrEmpty(anonymousCode2))
StartCoroutine(RequestCode2SessionAnonymous(anonymousCode2));
else
Debug.LogError("[GravityEngine] 匿名登录也未获取到有效标识");
},
failedCallback: (errMsg2) =>
{
Debug.LogError($"[GravityEngine] 匿名登录也失败: {errMsg2}");
},
forceLogin: false
);
},
forceLogin: true
);
#endif
}
private IEnumerator RequestCode2Session(string code)
{
string postData = JsonUtility.ToJson(new Code2SessionRequestWithCode
{
appid = DOUYIN_APPID,
code = code
});
yield return RequestCode2SessionInternal(postData, "openid");
}
private IEnumerator RequestCode2SessionAnonymous(string anonymousCode)
{
string postData = JsonUtility.ToJson(new Code2SessionRequestWithAnonymous
{
appid = DOUYIN_APPID,
anonymous_code = anonymousCode
});
yield return RequestCode2SessionInternal(postData, "anonymous_openid");
}
private IEnumerator RequestCode2SessionInternal(string postData, string openIdField)
{
using (var request = new UnityWebRequest(CODE2SESSION_URL, "POST"))
{
request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(postData));
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("x-api-key", CODE2SESSION_API_KEY);
request.timeout = 10;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var json = request.downloadHandler.text;
Debug.Log($"[GravityEngine] code2Session 响应: {json}");
var resp = JsonUtility.FromJson<Code2SessionResponse>(json);
string resultOpenId = openIdField == "anonymous_openid"
? resp.anonymous_openid
: resp.openid;
if (resp.error == 0 && !string.IsNullOrEmpty(resultOpenId))
{
Debug.Log($"[GravityEngine] 获取真实 openId 成功: {resultOpenId}");
CacheAndInitSDK(resultOpenId, "");
}
else
{
Debug.LogError($"[GravityEngine] code2Session 失败: error={resp.error}, errmsg={resp.errmsg}");
}
}
else
{
Debug.LogError($"[GravityEngine] code2Session 网络请求失败: {request.error}");
}
}
}
private void CacheAndInitSDK(string openId, string nickName)
{
PlayerPrefs.SetString(PREF_REAL_OPENID, openId);
if (!string.IsNullOrEmpty(nickName))
PlayerPrefs.SetString(PREF_NICKNAME, nickName);
PlayerPrefs.Save();
StartAndInitSDK(openId, nickName);
}
/// <summary>
/// 用户显式登录后调用,更新昵称(openId 不变)
/// </summary>
public void OnNickNameUpdated(string nickName)
{
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
if (string.IsNullOrEmpty(nickName)) return;
PlayerPrefs.SetString(PREF_NICKNAME, nickName);
PlayerPrefs.Save();
string cachedOpenId = PlayerPrefs.GetString(PREF_REAL_OPENID, "");
if (_isSdkStarted && !string.IsNullOrEmpty(cachedOpenId))
StartAndInitSDK(cachedOpenId, nickName);
#endif
}
private void StartAndInitSDK(string openId, string nickName)
{
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
if (!_isSdkStarted)
{
if (FindObjectOfType<GravityEngineAPI>() == null)
new GameObject("GravityEngine", typeof(GravityEngineAPI));
GravityEngineAPI.StartGravityEngine(ACCESS_TOKEN, openId, GravityEngineAPI.SDKRunMode.NORMAL);
_isSdkStarted = true;
Debug.Log($"[GravityEngine] SDK 已启动,clientId(openId): {openId}");
}
// nickname 为必填项,为空时用 openId 前8位兜底
string effectiveNickName = string.IsNullOrEmpty(nickName)
? openId.Substring(0, System.Math.Min(8, openId.Length))
: nickName;
int appVersion = 1;
if (int.TryParse(Application.version, out int parsedVersion))
appVersion = parsedVersion;
GravityEngineAPI.Initialize(
openId,
effectiveNickName,
appVersion,
openId,
false,
new GEInitializeCallback(
(_) =>
{
Debug.Log("[GravityEngine] 初始化成功");
GravityEngineAPI.Flush();
TrackRegisterEventOnce();
},
(errorMsg) =>
{
Debug.LogError($"[GravityEngine] 初始化失败: {errorMsg}");
}
)
);
#endif
}
/// <summary>
/// 首次运行上报 $MPRegister 注册事件(仅一次,本地标记去重)
/// </summary>
private void TrackRegisterEventOnce()
{
#if GRAVITY_BYTEDANCE_TT_GAME_MODE
if (PlayerPrefs.GetInt(PREF_REGISTERED, 0) == 0)
{
GravityEngineAPI.TrackMPRegister();
PlayerPrefs.SetInt(PREF_REGISTERED, 1);
PlayerPrefs.Save();
Debug.Log("[GravityEngine] 首次上报注册事件 $MPRegister");
}
else
{
Debug.Log("[GravityEngine] 注册事件已上报过,跳过");
}
#endif
}
// 分开两个请求类,避免 JsonUtility 把 null 字段序列化为空字符串导致云函数误判
[System.Serializable]
private class Code2SessionRequestWithCode
{
public string appid;
public string code;
}
[System.Serializable]
private class Code2SessionRequestWithAnonymous
{
public string appid;
public string anonymous_code;
}
[System.Serializable]
private class Code2SessionResponse
{
public int error;
public string openid;
public string anonymous_openid;
public string errmsg;
public string message;
}
}
/// <summary>
/// 引力引擎初始化回调
/// </summary>
public class GEInitializeCallback : IInitializeCallback
{
private readonly System.Action<Dictionary<string, object>> _onSuccess;
private readonly System.Action<string> _onFailed;
public GEInitializeCallback(
System.Action<Dictionary<string, object>> onSuccess,
System.Action<string> onFailed)
{
_onSuccess = onSuccess;
_onFailed = onFailed;
}
public void onSuccess(Dictionary<string, object> responseJson) => _onSuccess?.Invoke(responseJson);
public void onFailed(string errorMsg) => _onFailed?.Invoke(errorMsg);
}
7. 第三步:修改游戏启动逻辑
在游戏的 TT.InitSDK 成功回调 中调用 GravityEngineManager:
csharp
// GameLaunch.cs 中的 TT.InitSDK 调用处
TT.InitSDK(
successCallback: () =>
{
// TT SDK 就绪后初始化引力引擎
// 必须在 InitSDK 回调后调用,否则 TT API 尚未就绪
GravityEngineManager.Instance.InitAfterTTSDKReady();
// ...其他初始化逻辑
},
failCallback: (errMsg) =>
{
Debug.LogError($"TT.InitSDK 失败: {errMsg}");
}
);
如果用户有手动登录流程,在登录成功后调用(更新昵称):
csharp
// 用户登录成功后
GravityEngineManager.Instance.OnNickNameUpdated(nickName);
8. 第四步:配置抖音合法域名
登录 抖音小游戏开发者后台 → 开发管理 → 开发设置 → 服务器域名 → request 合法域名,添加:
https://你的云函数URL(如 https://1255346748-xxx.ap-guangzhou.tencentscf.com)
https://backend.gravity-engine.com
https://api.gravity-engine.com
9. 验证与测试
最后打包到模拟器或真机扫码运行,看到日志成功之后,过段时间可以看到后台
预期日志
首次启动:
[GravityEngine] TT.Login 成功 - code长度: 123, isLogin: True
[GravityEngine] code2Session 响应: {"error":0,"openid":"_000YbIAk...","errmsg":""}
[GravityEngine] 获取真实 openId 成功: _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngineSDK] 有接入 ByteDanceTT 小游戏
[GravityEngine] SDK 已启动,clientId(openId): _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] 初始化成功
[GravityEngine] 首次上报注册事件 $MPRegister
后续启动(使用缓存):
[GravityEngine] 使用缓存的真实 openId: _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] SDK 已启动,clientId(openId): _000YbIAkZvmOLxJAH9SfhUl4Bh7gIZ_QdoZ
[GravityEngine] 初始化成功
[GravityEngine] 注册事件已上报过,跳过
引力引擎后台验证
登录引力引擎后台 → 分析 → 用户组查,应能看到:
- 用户 openId 与日志一致
- 注册时间正确
- 媒体平台显示"自然量"或对应渠道
10. 常见问题排查
❌ HTTP/1.1 443 Unknown HTTP status
原因: 云函数进程启动失败(0 code exit unexpected)。
解决:
- 检查
app.js代码是否完整粘贴 - 确认
scf_bootstrap内容为node app.js(Web 函数默认) - 重新部署并发布新版本(编辑后必须点"部署"再"发布版本")
❌ error: 2, errmsg: "bad secret"
原因: APP_SECRETS 环境变量中的 AppSecret 不正确。
解决: 仔细核对 AppSecret,注意每一个字符(大小写、0 和 O 等容易混淆的字符)。
❌ error: 1, errmsg: "未注册的 appid"
原因: APP_SECRETS 的 key 与客户端传来的 appid 不匹配。
解决: 确认 APP_SECRETS 的 key 格式为 tt 开头的抖音 appid,如 tt0c5c7ae1923c3d4f07。
❌ [GravityEngine] 初始化失败: name must be required
原因: 引力引擎 Initialize 接口的 nickName 参数不能为空。
解决: 代码中已用 openId 前 8 位作为默认昵称兜底,确认 effectiveNickName 逻辑存在。
❌ [GravityEngine] 初始化失败: 参数错误
原因: clientId(openId)格式不正确,常见于使用了超长的 anonymousCode(开发者工具中可超过 100 字符)。
解决: 确保使用通过 code2Session 换取的真实 openId(约 36 字符),而不是直接使用 anonymousCode。
总结
| 步骤 | 关键点 |
|---|---|
| 云函数 | Web 函数类型,监听 9000 端口,AppSecret 存环境变量 |
| openId 获取 | TT.Login → code → 云函数 code2Session → 真实 openId |
| SDK 初始化 | 必须在 TT.InitSDK 回调后执行 |
| 事件上报 | 仅上报 $MPRegister,$AdShow 由后台自动拉取 |
| 缓存机制 | openId 存 PlayerPrefs,后续启动直接使用,无需重复调用云函数 |
| 多项目复用 | 一个云函数通过 APP_SECRETS JSON 支持多个 appid |

