前言

在工作中存在于安卓手机适配与异形屏适配问题:

Unity 如何实现UI适配?水滴屏、异形屏?Canvas如何配置

Unity实现屏幕黑边

Unity How to Lock Camera View to an Aspect Ratio and Add Black Bars

Unity 的UI适配原理 - 知乎

怎么解决适配

要想知道适配什么首先得搞清美术的出图流程,我们一般来说的适配指的是UI适配,那美术在出UI资源的时候肯定是要根据设计分辨率来进行出图的,而不能美术A同学是以2480x2200 出图的,而美术B同学是以1920x1080出图的,这样的话两个UI切图放到一个界面就不协调和美观,所以这个设计分辨率肯定是有的。

所以到目前为止涉及到了3种分辨率

1、图片分辨率

2、设计分辨率

3、屏幕分辨率

适配的过程就是根据设计分辨率出图,然后将设计分辨率整体映射到屏幕上。

那映射的过程是我们要思考的问题。

目前常见有四种映射方式

假设 scaleX = 屏幕分辨率宽/设计分辨率宽,scaleY = 屏幕分辨率高/设计分辨率高,

1、铺满屏幕

这种方式是将设计分辨率宽拉伸scaleX倍,设计分辨率高拉伸scaleY倍,这样设计分辨率就铺满了整个屏幕,带来的问题就是宽高非等比缩放所造成的图片变形问题。

一般游戏不会采用这种方式。

2、ShowAll模式

这种方式是将设计分辨率宽和高拉伸min(scaleX,scaleY) 倍,这种方式图片不会变形,但是手机屏幕边缘会出现黑边,然后需要特殊处理。

这种方式我之前做的游戏采用过。

3、NoBorder模式

这种方式是将设计分辨率宽和高拉伸max(scaleX,scaleY) 倍,这种方式图片不会变形,但是游戏内容会部分超出屏幕。

这种方式用的也不多。

4、FixedWidth和 FixedHeight模式

这种方式比较特殊,它会改变设计分辨率的大小,我们以FixedHeight模式举例:

新设计分辨率的宽 = 屏幕分辨率宽 / scaleY

然后新设计分辨率再以scaleX,scaleY的倍数拉伸铺满屏幕,这样就不存在图片的变形问题以及ShowAll模式和NoBorder模式存在的问题。

这种方式是用的最多的。

unity怎么适配的

Anchors与Pivot

  • UI元素的适配

在设计UI时,常常需要考虑屏幕分辨率和设备尺寸等因素。为了让UI在不同的设备上呈现出较好的效果,需要对UI元素进行适配。

Unity提供了一种自适应UI的解决方案,即Anchors和Pivot(锚点和中心点)。Anchors决定了UI元素的位置和大小随着屏幕尺寸的变化而变化,Pivot决定了UI元素的旋转和缩放的中心。

VerticalLayoutGroup和HorizontalLayoutGroup

  • UI布局的适配

在UI的布局中,不仅需要考虑UI元素的适配,还需要考虑UI布局的适配。通常需要针对不同屏幕分辨率和设备尺寸进行适配。

Unity提供了一种灵活的UI布局系统,即UI Layout系统。它提供了多种布局组件,例如VerticalLayoutGroup和HorizontalLayoutGroup等,可以在不同分辨率和尺寸的屏幕上实现适配。

例如,可以在UI中使用VerticalLayoutGroup组件,以在垂直方向上布局UI元素。通过设置LayoutElement组件的minHeight和preferredHeight属性,可以确保UI元素在不同的屏幕尺寸上具有相同的高度。

Canvas Scaler

除此之外,还可以使用Canvas Scaler来对UI进行缩放,以适配不同分辨率的设备。

  • Canvas Scaler提供了三种缩放模式:

    • Constant Pixel Size(固定像素大小)

    • Scale with Screen Size(根据屏幕尺寸进行缩放)

    • Constant PhysicalSize(固定物理大小)

Constant Pixel Size(固定像素大小):这种方式下,UI元素的尺寸将按照在设计时所设置的像素大小来显示,不会发生缩放。如果在不同尺寸的屏幕上运行游戏,那么UI元素的大小将会在不同屏幕上显示不同。

Scale with Screen Size(根据屏幕尺寸进行缩放):这种方式下,UI元素的尺寸将会根据屏幕的大小进行缩放。UI元素的尺寸和位置都将随着屏幕的大小而发生改变,以确保UI在不同尺寸的屏幕上都能够适当地缩放,保持比例不失真。


在这种方式下,Screen Match Mode 会出现Match Width Or Height , Expand 和 Shrink三个选项,和我们上文讲到的适配方式不谋而合。区别就是这里调整的是画布的大小,可以理解为Unity是在调整设计分辨率,调整后的设计分辨率大小=画布的大小。

img

也就是说新的设计分辨率 = screenSize /scaleFactor。 这是等比缩放的,UI不会变形。

Constant Physical Size(固定物理大小):这种方式下,UI元素的尺寸将会根据物理尺寸来显示。这意味着,UI元素的物理大小将始终保持不变,而不是根据屏幕尺寸进行缩放。这种方式主要适用于需要在屏幕上显示实际大小的元素,例如地图、计时器等。

选择哪种适配方式取决于你的游戏需要什么样的UI效果。如果你想要UI在不同尺寸的屏幕上都保持相同的比例和大小,那么可以选择Scale with Screen Size;如果你需要在游戏中显示实际大小的元素,那么可以选择Constant Physical Size;如果你需要UI在不同尺寸的屏幕上以固定的像素大小显示,那么可以选择Constant Pixel Size。

总的来说,Unity提供了丰富的UI适配解决方案,包括UI元素的适配和UI布局的适配。通过合理运用这些解决方案,可以在不同的设备上实现良好的UI效果。

其他问题

有的时候由于我们要上的平台的多样性,很可能我们的适配方式不能在所有平台的效果都达到完美状态,比如你的项目一开始是竖屏设计的,突然有一天项目发展起来了,要上windows平台,那你的竖屏的设计分辨率就行不通了,此时怎么办呢?

再弄一套新的设计分辨率吗?还是说还用现在的设计分辨率,但是在平台上特殊处理呢?

个人觉得,特殊处理可能是个最优且折中的方案。这样代价最小,不影响开发效率,不需要增加测试成本。

除此以外,假如要特殊处理时候需要考虑下面几个问题:

1、最好不要业务层决定适配,业务层越简单越好

2、那底层哪里处理呢?

3、处理的逻辑是什么呢?

4、改了之后肯定会对个别界面有影响,是否能接受?

以上几个问题不同项目情况也不一样,没有统一答案,但考虑问题的方式是一样的。

异形屏适配

UI适配

主要是水滴屏,刘海屏适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public override void Adapt()
{
if (scaler == null)
{
return;
}
// 安全区域
var safeArea = Screen.safeArea;

//referenceResolution参考分辨率
//matchWidthOrHeight:屏幕适配权重(0-1,0=基于宽度适配,1=基于高度适配
//Screen.width/height:当前屏幕分辨率
int width = (int)(scaler.referenceResolution.x * (1 - scaler.matchWidthOrHeight)
* scaler.referenceResolution.y * Screen.width / Screen.height * scaler.matchWidthOrHeight);

int height = (int)(scaler.referenceResolution.y * scaler.matchWidthOrHeight
* scaler.referenceResolution.x * Screen.height / Screen.width * (scaler.matchWidthOrHeight - 1));

//比例因子:混合了高度和宽度的适配比例
float ratio = (scaler.referenceResolution.y * scaler.matchWidthOrHeight / Screen.height)
- (scaler.referenceResolution.x * (scaler.matchWidthOrHeight - 1) / Screen.width);

//设置锚点
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;

//设置偏移量
rectTransform.offsetMin = new Vector2(
safeArea.position.x * ratio,
safeArea.position.y * ratio
);
rectTransform.offsetMax = new Vector2(
safeArea.position.x * ratio + safeArea.width * ratio - width,
-(height - safeArea.position.y * ratio - safeArea.height * ratio)
);


}

黑边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

using UnityEngine;


// 处理游戏画面的安全区渲染, 挂在主相机上即可
public class M_Cam_SafeArea : MonoBehaviour
{
// 用于产生黑边固定色的临时canvas
public GameObject blackCanvas_pref;
public int frame_num = 0;
private Rect lastSafeArea = new Rect(0, 0, 0, 0);
private Camera mainCamera;
private GameObject blackCanvas;


void Awake()
{
mainCamera = Camera.main;
ApplySafeArea();
// 专门生成初始黑边的canvas
blackCanvas = Instantiate(blackCanvas_pref);
}


void Update()
{
/*
// 也可以每帧检测,如果安全区发生变化则更新布局
if (lastSafeArea != Screen.safeArea)
{
ApplySafeArea();
}
*/
frame_num += 1;
// 得是第2帧以后, 删除专用黑边canvas才行。以清除第一帧渲染的残留
if (frame_num == 2)
{
Destroy(blackCanvas);
}
}


void ApplySafeArea()
{
Rect safeArea = Screen.safeArea;
// 计算安全区的归一化坐标
float xMin = safeArea.x / Screen.width;
float yMin = safeArea.y / Screen.height;
float xMax = (safeArea.x + safeArea.width) / Screen.width;
float yMax = (safeArea.y + safeArea.height) / Screen.height;
// 设置相机的视口,使内容位于安全区内
// width, height, x(距离屏幕左边缘), y()
mainCamera.rect = new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
lastSafeArea = safeArea;
}
}

黑边适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
using UnityEngine;

/// <summary>
/// 屏幕宽高比适配工具
/// 功能:根据目标宽高比自动调整相机视口,在不同分辨率设备上保持画面比例
/// 实现方式:通过添加黑边(letterbox/pillarbox)避免画面拉伸变形
/// </summary>
public class AspectRatioUtility : MonoBehaviour
{
[Tooltip("目标宽高比(宽度/高度)\n示例:16:9=1.77, 4:3=1.33, 21:9=2.33")]
[SerializeField] private float targetAspect = 16.0f / 9.0f;

private Camera _camera; // 需要调整的相机组件缓存

void Start()
{
// 初始化时立即执行适配
_camera = GetComponent<Camera>();
AdjustAspectRatio();
}

/// <summary>
/// 核心适配逻辑:
/// 1. 计算当前屏幕实际宽高比
/// 2. 与目标宽高比较,确定需要添加黑边的方向
/// 3. 调整相机视口矩形(rect)实现适配
/// </summary>
public void AdjustAspectRatio()
{
if (_camera == null) return;

// 计算当前设备实际宽高比
float windowAspect = (float)Screen.width / Screen.height;

// 计算比例因子(当前宽高比相对于目标宽高比的变化率)
// scaleHeight < 1:屏幕比目标更宽,需要垂直方向压缩(上下加黑边)
// scaleHeight > 1:屏幕比目标更高,需要水平方向压缩(左右加黑边)
float scaleHeight = windowAspect / targetAspect;

Rect rect = _camera.rect;

if (scaleHeight < 1.0f)
{
/* 上下黑边模式:
* - 保持宽度占满屏幕(width=1.0)
* - 按比例缩小视口高度
* - 垂直居中显示(通过y偏移量实现)
*/
rect.width = 1.0f;
rect.height = scaleHeight;
rect.x = 0;
rect.y = (1.0f - scaleHeight) / 2.0f; // 计算上下黑边的总高度并平分
}
else
{
/* 左右黑边模式:
* - 保持高度占满屏幕(height=1.0)
* - 按比例缩小视口宽度(1/scaleHeight)
* - 水平居中显示(通过x偏移量实现)
*/
float scaleWidth = 1.0f / scaleHeight;
rect.width = scaleWidth;
rect.height = 1.0f;
rect.x = (1.0f - scaleWidth) / 2.0f; // 计算左右黑边的总宽度并平分
rect.y = 0;
}

_camera.rect = rect; // 应用新的视口设置

/* 视觉效果示例:
* 当运行在21:9屏幕(scaleHeight=0.75)时:
* - 视口高度变为屏幕的75%
* - 上下各留12.5%的黑边区域
*
* 当运行在4:3屏幕(scaleHeight=1.33)时:
* - 视口宽度变为屏幕的75%
* - 左右各留12.5%的黑边区域
*/
}
}