Unity 小游戏取消支付宝支付后画面变大并超出屏幕
结论
这类现象通常不是场景内容突然放大,而是取消支付并返回小游戏时,运行容器的窗口尺寸、设备像素比或安全区域发生了变化。Unity 没有及时获取正确的渲染尺寸,画布因此按照错误的宽高重新适配。
只看红米 K80 Ultra 上的表现,还不能确定这是支付宝、Unity 小游戏转换方案或特定系统版本的官方已知问题。可以先将当前项目使用的小游戏转换工具和运行容器升级到其支持的稳定版本,再根据窗口尺寸日志判断问题出在哪一层。若容器返回的尺寸有误,需要由平台或转换工具修复;若尺寸正确但 Unity 显示异常,可以在支付结束后主动刷新分辨率和 UI 布局。
可能原因
支付窗口会使小游戏进入后台或失去焦点。取消支付并返回后,小游戏可能依次执行窗口恢复、横竖屏确认、安全区域更新和 Unity 渲染恢复。
部分机型上的事件执行顺序可能不同,常见异常有:
- Unity 恢复渲染时读取到临时窗口尺寸。
- 容器混用了逻辑像素和物理像素,宽度被乘上错误的设备像素比。
- 游戏采用横屏,支付页却以竖屏显示,返回游戏时方向状态没有完全恢复。
- WebGL Canvas 或小游戏渲染画布的 CSS 尺寸与实际缓冲区尺寸不一致。
Canvas Scaler、相机视口或自定义适配脚本缓存了支付前的屏幕参数。- 支付取消分支没有执行与支付成功分支相同的界面恢复逻辑。
如果只有右侧超出屏幕,应重点检查宽高比是否发生变化。等比例放大一般会同时影响多个方向;只有右侧明显溢出,通常说明画布宽度、相机宽高比或 UI 锚点使用了错误值。
定位方法
1. 记录支付前后的屏幕状态
除了支付成功回调,还要在支付取消、支付失败和小游戏重新获得焦点时记录以下数据:
Screen.widthScreen.heightScreen.safeAreaScreen.orientation- 主相机的
aspect - 根节点
Canvas和RectTransform的尺寸 - 平台接口返回的窗口宽高、屏幕宽高及设备像素比
可以先在 Unity 侧加入以下诊断脚本:
using UnityEngine;
public sealed class DisplayDiagnostics : MonoBehaviour
{
[SerializeField] private Camera targetCamera;
[SerializeField] private RectTransform uiRoot;
public void Dump(string stage)
{
Rect safeArea = Screen.safeArea;
Debug.Log(
$"[Display] stage={stage}, " +
$"screen={Screen.width}x{Screen.height}, " +
$"orientation={Screen.orientation}, " +
$"safeArea=({safeArea.x},{safeArea.y},{safeArea.width},{safeArea.height}), " +
$"cameraAspect={(targetCamera != null ? targetCamera.aspect : -1f)}, " +
$"uiRoot={(uiRoot != null ? uiRoot.rect.size.ToString() : "null")}"
);
}
private void OnApplicationFocus(bool hasFocus)
{
Dump(hasFocus ? "focus" : "blur");
}
private void OnApplicationPause(bool paused)
{
Dump(paused ? "pause" : "resume");
}
}
至少比较以下四个时间点:
- 首次进入游戏。
- 拉起支付前。
- 收到取消支付回调时。
- 返回游戏后的连续几帧。
如果取消回调时尺寸异常,稍后又自动恢复,问题主要出在生命周期事件的执行时序。如果平台接口持续返回错误尺寸,Unity 侧反复刷新布局也无法彻底解决。
2. 检查 Canvas 配置
使用 UGUI 时,检查根 Canvas 上的 Canvas Scaler 配置:
UI Scale Mode使用Scale With Screen Size。Reference Resolution与设计分辨率一致。- 根据项目布局选择
Screen Match Mode,不要在运行时无条件修改。 - 右侧 UI 的锚点不要全部固定在左侧。
- 不要长期使用启动时保存的
Screen.width、Screen.height计算 UI 尺寸。
如果自定义适配代码直接修改了 Canvas.scaleFactor、根节点缩放或相机 orthographicSize,需要检查这段代码是否会在支付返回时重复执行。多次乘以缩放比例是画面整体变大的常见原因。
3. 检查相机和渲染目标
确认支付返回后,以下参数是否匹配:
Camera.main.rect
Camera.main.aspect
Screen.width
Screen.height
如果使用了 RenderTexture,还要检查它的宽高是否仍停留在进入游戏时的值。窗口变化后继续把旧尺寸的纹理铺满新画布,也可能造成拉伸或裁切。
4. 对比平台窗口参数
在小游戏平台侧记录系统信息和窗口变化事件。具体 API 名称应以当前使用的支付宝小游戏 SDK、Unity 转换插件或运行适配层文档为准,不要照搬其他小程序平台的接口。
重点检查以下参数之间的关系:
logicalWidth / logicalHeight
physicalWidth / physicalHeight
devicePixelRatio
canvas.width / canvas.height
通常,画布的显示尺寸使用逻辑像素,渲染缓冲区才可能按照 devicePixelRatio 放大。如果两处都乘了设备像素比,画面就可能被重复缩放。
临时修复方案
如果平台最终返回的尺寸正确,只是 Unity 恢复得太早,可以在支付结束及应用重新获得焦点后,延迟数帧再刷新显示参数和 UI。
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public sealed class DisplayRecovery : MonoBehaviour
{
[SerializeField] private CanvasScaler canvasScaler;
[SerializeField] private RectTransform rootLayout;
private Coroutine recoveryCoroutine;
public void OnPaymentFinished()
{
ScheduleRecovery();
}
private void OnApplicationFocus(bool hasFocus)
{
if (hasFocus)
{
ScheduleRecovery();
}
}
private void OnApplicationPause(bool paused)
{
if (!paused)
{
ScheduleRecovery();
}
}
private void ScheduleRecovery()
{
if (!isActiveAndEnabled)
{
return;
}
if (recoveryCoroutine != null)
{
StopCoroutine(recoveryCoroutine);
}
recoveryCoroutine = StartCoroutine(RecoverDisplay());
}
private IEnumerator RecoverDisplay()
{
// 等待容器完成窗口和方向恢复。
yield return null;
yield return null;
int width = Screen.width;
int height = Screen.height;
if (width <= 0 || height <= 0)
{
recoveryCoroutine = null;
yield break;
}
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
mainCamera.aspect = (float)width / height;
mainCamera.ResetProjectionMatrix();
}
if (rootLayout != null)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(rootLayout);
}
Canvas.ForceUpdateCanvases();
Debug.Log(
$"[DisplayRecovery] size={width}x{height}, " +
$"canvasScale={(canvasScaler != null ? canvasScaler.scaleFactor : -1f)}"
);
recoveryCoroutine = null;
}
}
这里等待“两帧”只是验证时序问题时采用的保守做法,不一定适合所有项目。如果容器的恢复时间不固定,更稳妥的办法是监听窗口变化,等宽高连续若干帧保持一致后再刷新布局。
不要为了恢复显示而无条件执行以下操作:
transform.localScale *= scale;
每次重新进入前台时,这种写法都会继续累乘缩放。应保存设计基准值,并使用绝对赋值:
rootTransform.localScale = baseScale * calculatedScale;
如果项目必须主动设置分辨率,应先确认目标宽高来自可靠的平台窗口参数,再调用:
Screen.SetResolution(targetWidth, targetHeight, false);
小游戏运行环境未必完整支持或需要 Screen.SetResolution。调用后没有效果,不代表代码没有执行,最终行为仍由平台容器和 Unity 适配层的实现决定。
建议的解决顺序
- 更新支付宝小游戏运行环境、Unity 小游戏转换插件及其适配库。不要在生产项目中直接跨大版本替换,先在分支中验证。
- 检查支付成功、取消和失败三个回调是否执行了相同的界面恢复流程。
- 移除或修正重复累乘 UI 缩放的代码。
- 记录支付前后 Unity 与平台两侧的窗口参数。
- 如果最终窗口参数正确,增加延迟刷新或稳定尺寸检测。
- 如果平台返回的窗口宽高、方向或设备像素比错误,制作最小复现项目,并提交给支付宝小游戏或所用 Unity 转换工具的维护方。
提交问题时需要的信息
为了判断问题是否与特定机型兼容性有关,提交时应提供:
- 红米 K80 Ultra 的系统版本和显示设置。
- 支付宝客户端版本。
- Unity 编辑器版本。
- 小游戏转换插件或适配方案的名称及版本。
- 游戏的横竖屏配置。
- 支付前、取消回调和重新获得焦点后的完整尺寸日志。
- 是否使用 UGUI、
RenderTexture、自定义分辨率适配或安全区域脚本。 - 最小复现工程及录屏。
- 同一安装包在其他机型上的对比结果。
如果异常只出现在红米 K80 Ultra 上,而且最小工程中的平台窗口参数在取消支付后已经出错,基本可以将问题范围缩小到支付宝运行容器、设备系统适配或转换层,而不是具体的游戏业务逻辑。在平台修复前,可以通过重新读取稳定的窗口尺寸并刷新 Unity 布局来规避。
备注:内容仅供参考。
