966
20
0
438
218
720
676
562
738
875
Unity3D游戏开发Android平台篇
Unity3D游戏开发Unity Timeline篇
Unity3D游戏开发Shader Graph篇
Unity3D脚本Lua篇
Unity是实时3D互动内容创作和运营平台,包括游戏开发、美术、建筑、汽车设计、影视在内的所有创作者,借助Unity将创意变成现实。Unity平台提供一整套完善的软件解决方案,可用于创作、运营和变现任何实时互动的2D和3D内容,支持平台包括手机、平板电
FairyGUI是一个跨引擎的开源UI解决方案,它包含一个出色的的UI编辑器,以及多个流行游戏引擎的运行库。
AndroidStudio是谷歌推出的一个Android集成开发工具,基于IntelliJIDEA.类似EclipseADT,AndroidStudio提供了集成的Android开发工具用于开发和调试。
IntelliJIDEA主要用于支持Java、Scala、Groovy等语言的开发工具,同时具备支持目前主流的技术和框架,擅长于企业应用、移动应用和Web应用的开发。
Eclipse是一个开放源代码的、基于Java的可扩展开发平台。Eclipse是Java的集成开发环境(IDE),当然Eclipse也可以作为其他开发语言的集成开发环境,如C,C++,PHP,和Ruby等。
Vim是UNIX文本编辑器Vi的加强版本,加入了更多特性来帮助编辑源代码。Vim的部分增强功能包括文件比较(vimdiff),语法高亮,全面的帮助系统,本地脚本(Vimscript),和便于选择的可视化模式。
Atom是Github专门为程序员推出的一个跨平台文本编辑器。具有简洁和直观的图形用户界面,并有很多有趣的特点:支持CSS,HTML,JavaScript等网页编程语言。它支持宏,自动完成分屏功能,集成了文件管理器。
SublimeText是一个文本编辑器(收费软件,可以无限期试用),同时也是一个先进的代码编辑器。SublimeText是由程序员JonSkinner于2008年1月份所开发出来,它最初被设计为一个具有丰富扩展功能的Vim。
VSCode,全称是VisualStudioCode,但因为全称太长,微软旗下另一款产品visualstudio又经常被简称为VS,所以这款与vs有一定渊源的代码编辑器就被简称为VSCode。
Gradle是一个基于JVM的富有突破性构建工具,其正迅速成为许多开源项目和前沿企业构建系统的选择,同时也在挑战遗留的自动化构建项目。本教程主要讲解了如何使用Gradle构建系统和构建系统过程中涉及的插件。
Git是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。
ApacheSubversion通常被缩写成SVN,是一个开放源代码的版本控制系统,Subversion在2000年由CollabNetInc开发,现在发展成为Apache软件基金会的一个项目,同样是一个丰富的开发者和用户社区的一
现在主流的手机基本上都是刘海屏、挖孔屏、灵动岛等异形屏, 因此不可避免的我们需要对游戏中的界面进行相关的适配工作。
首先我们引入一个“安全区域”的概念, 这个概念在Android、iOS开发中很常见, Unity也在UnityEngine.Screen中提供了safeArea属性用于获取安全区域的范围, 大家可以在异形屏的手机上分别打印Screen.safeArea和Screen.width、Screen.height来观察它们值的差异. 关于该接口的说明可以参考下图或官方文档.(这个接口在2020和以下版本的Unity中, 对于灵动岛设备支持有问题, 可以自己通过设备信息或Objective-c的代码进行替代, 不在本文中过多描述该问题)
屏幕高度(即屏幕最高点):Screen.height,安全区域最高点:Screen.safeArea.yMax,屏幕顶部异形区域大小:Screen.height - Screen.safeArea.yMax
(安全区域最低点:Screen.safeArea.yMin,可用于屏幕底部适配(苹果Touch Bar))
//计算屏幕顶部异形大小 int safeAreaOffset = (int)(Screen.height - Screen.safeArea.yMax); //UI偏移 RectTransform rectTransform = transform.Find("SafeArea").GetComponent<RectTransform>(); rectTransform.offsetMax = new Vector2(0,-safeAreaOffset);
TestPanel:UI界面根节点,添加背景图片,可忽略异形屏的影响将图片填充满屏幕
SafeArea:安全区域根节点,在UI被加载后执行以上代码,可以根据异形屏大小动态设置顶部偏移量,避免被异形屏遮挡
未广泛测试机型,有待验证! 还可以根据设备类型(SystemInfo.deviceModel)、设备名称(SystemInfo.deviceName),手动设置每个机型的偏移量
根据游戏的类型不同、需求上的差异, 对于界面的适配通常会有所差异, 一般上可以大致分为以下三种类型:
1、界面尺寸始终保持设计分辨率不变, 但可能存在需要与屏幕保持水平位置或垂直位置方向上居中的需求(例如确认弹窗、限时礼包等弹出式界面)
2、界面尺寸和屏幕尺寸始终保持相同大小(例如背包等全屏界面)
3、界面和安全区域始终保持相同大小、相同位置(例如moba、fps游戏的战斗操作界面)
在上述三种适配规则的基础上, 一些额外的需求
界面中的背景图和屏幕尺寸始终保持相同大小, 但部分可操作的控件希望始终在安全区域范围内。
全屏、安全区域适配的尺寸希望有限制(例如只希望适配720x1280 - 720x1480范围内的屏幕), 超出适配范围的部分用纯色或静态图填充。
希望玩家能自行调整安全区域的尺寸
在下文中将以Unity+FairyGUI为例针对上述提到的几种界面适配需求进行一一说明如何进行制作。
在上文中提到,Unity为我们提供了现成的Screen.safeArea接口获取安全区域范围,但受限于各项目所用的Unity引擎版本, 可能对部分新设备的支持不够到位(例如上文提到的Unity2020版本获取灵动岛区域存在问题),以及为了方便开发, 我们希望在UnityEditor环境下进行方便的安全区域修改与调试, 来及时看到适配效果, 所以自行封装一个获取安全区域的屏幕适配服务接口是一件很有必要的事情。 以我项目中的代码为例,定义出接口IScreenAdaptorService:
/// <summary> /// 屏幕适配服务接口。 /// </summary> public interface IScreenAdaptorService { /// <summary> /// 获取安全区域范围 /// </summary> ObservableVariable<Rect> SafeArea { get; } }
并实现在UnityEditor上专用的实现类:
/// <summary> /// 编辑器屏幕适配服务。 /// </summary> internal sealed class EditorScreenAdaptorService : IScreenAdaptorService, IInitialize { public ObservableVariable<Rect> SafeArea { get; } = new ObservableVariable<Rect>(); public void Initialize() { _debuggerGameObject = new GameObject("AdaptorDebugger"); _debugger = _debuggerGameObject.AddComponent<AdaptorDebugger>(); _debugger.OnDataDirty += () => SafeArea.Value = _debugger.GetSafeArea(); SafeArea.Value = _debugger.GetSafeArea(); } private GameObject _debuggerGameObject; private AdaptorDebugger _debugger; private sealed class AdaptorDebugger : MonoBehaviour { public event Action OnDataDirty; public int offsetTop; public int offsetBottom; public int offsetLeft; public int offsetRight; private int m_LastOffsetTop; private int m_LastOffsetBottom; private int m_LastOffsetLeft; private int m_LastOffsetRight; private int m_LastWidth; private int m_LastHeight; private void Awake() { DontDestroyOnLoad(gameObject); m_LastWidth = Screen.width; m_LastHeight = Screen.height; } public Rect GetSafeArea() { var x = offsetLeft; var y = offsetTop; var w = Screen.width - offsetRight - x; var h = Screen.height - offsetBottom - y; return new Rect(x, y, w, h); } private void Update() { var modify1 = UpdateInputData(); var modify2 = UpdateScreenSize(); if (!modify1 && !modify2) return; OnDataDirty?.Invoke(); } private bool UpdateInputData() { if (offsetTop == m_LastOffsetTop && offsetBottom == m_LastOffsetBottom && offsetLeft == m_LastOffsetLeft && offsetRight == m_LastOffsetRight) return false; m_LastOffsetTop = offsetTop = Math.Max(0, offsetTop); m_LastOffsetBottom = offsetBottom = Math.Max(0, offsetBottom); m_LastOffsetLeft = offsetLeft = Math.Max(0, offsetLeft); m_LastOffsetRight = offsetRight = Math.Max(0, offsetRight); return true; } private bool UpdateScreenSize() { var width = Screen.width; var height = Screen.height; if (width == m_LastWidth && height == m_LastHeight) return false; m_LastWidth = width; m_LastHeight = height; return true; } } }
在这个类中构造了一个GameObject实例, 并为其添加上调试组件, 这样我们就可以很方便的在面板上进行设置安全区域的范围。
在屏幕适配服务接口的基础上, 我们还需要封装一个UI界面适配服务的接口, 因为屏幕尺寸、屏幕安全区域尺寸 和 UI全屏尺寸、UI安全区域尺寸并不是相等的关系, 以FairyGUI为例, 我们在GRoot的代码中可以看到它会根据我们传入的缩放模式、设计分辨率与实时屏幕尺寸进行计算, 得到一个合适的屏幕缩放比例, 然后根据该缩放比例与实时的屏幕尺寸计算出GRoot的尺寸(即UI的全屏尺寸):
/// <summary> /// Set content scale factor. /// </summary> /// <param name="designResolutionX">Design resolution of x axis.</param> /// <param name="designResolutionY">Design resolution of y axis.</param> /// <param name="screenMatchMode">Match mode.</param> public void SetContentScaleFactor(int designResolutionX, int designResolutionY, UIContentScaler.ScreenMatchMode screenMatchMode) { UIContentScaler scaler = Stage.inst.gameObject.GetComponent<UIContentScaler>(); scaler.designResolutionX = designResolutionX; scaler.designResolutionY = designResolutionY; scaler.scaleMode = UIContentScaler.ScaleMode.ScaleWithScreenSize; scaler.screenMatchMode = screenMatchMode; scaler.ApplyChange(); ApplyContentScaleFactor(); } /// <summary> /// This is called after screen size changed. /// </summary> public void ApplyContentScaleFactor() { this.SetSize(Mathf.CeilToInt(Stage.inst.width / UIContentScaler.scaleFactor), Mathf.CeilToInt(Stage.inst.height / UIContentScaler.scaleFactor)); this.SetScale(UIContentScaler.scaleFactor, UIContentScaler.scaleFactor); }
所以我们需要封装UI界面适配器接口, 提供实际的UI全屏尺寸与UI安全区域尺寸供外部使用:
/// <summary> /// UI 界面适配服务接口。 /// </summary> public interface IUIScreenAdaptorService { /// <summary> /// UI 屏幕大小。(逻辑尺寸) /// </summary> ObservableVariable<Vector2Int> UIScreenSize { get; } /// <summary> /// UI 安全区域。(逻辑尺寸) /// </summary> ObservableVariable<Rect> UISafeArea { get; } }
基于FairyGUI实现的UI界面适配服务类实现如下:
/// <summary> /// FairyGUI 界面适配服务。 /// </summary> internal sealed class FairyGUIScreenAdaptorService : IUIScreenAdaptorService, IInitialize, IDisposable { [Inject] public IScreenAdaptorService ScreenAdaptorService { get; set; } public ObservableVariable<Vector2Int> UIScreenSize { get; } = new ObservableVariable<Vector2Int>(); public ObservableVariable<Rect> UISafeArea { get; } = new ObservableVariable<Rect>(); public void Initialize() { ScreenAdaptorService.SafeArea.Subscribe(OnSafeAreaChanged); GRoot.inst.onSizeChanged.Add(OnSizeChanged); UpdateSize(); } public void Dispose() { GRoot.inst.onSizeChanged.Remove(OnSizeChanged); ScreenAdaptorService.SafeArea.Unsubscribe(OnSafeAreaChanged); } private void OnSafeAreaChanged(Rect _, Rect __) { UpdateSize(); } private void OnSizeChanged() { UpdateSize(); } private void UpdateSize() { var safeArea = ScreenAdaptorService.SafeArea.Value; var factor = 1f / GRoot.contentScaleFactor; safeArea.x = Mathf.CeilToInt(safeArea.x * factor); safeArea.y = Mathf.CeilToInt(safeArea.y * factor); safeArea.width = Mathf.CeilToInt(safeArea.width * factor); safeArea.height = Mathf.CeilToInt(safeArea.height * factor); UIScreenSize.Value = new Vector2Int((int)GRoot.inst.width, (int)GRoot.inst.height); UISafeArea.Value = safeArea; } }
可以看到我们在这里根据GRoot计算出的屏幕缩放比例将屏幕安全区域换算成了UI安全区域.
前文提到, 根据界面类型的不同与需求的不同, 存在多种屏幕适配的方案, 所以我们基于FairyGUI抽象出统一的适配器接口, 供界面的逻辑实现中自行指定适配器实例.
/// <summary> /// 界面屏幕适配器接口 /// </summary> public interface IUIFormScreenAdaptor : IDisposable { /// <summary> /// 初始化适配器 /// </summary> /// <param name="contentPane">UI界面对象</param> /// <param name="uiRoot">UI分组根结点对象</param> void Initialize(GComponent contentPane, GComponent uiRoot); } /// <summary> /// FairyGUI界面逻辑基类的一部分 /// </summary> public partial class AFairyGUIFormLogic { /// <summary> /// 屏幕适配器 由界面自行重载指定 /// </summary> protected abstract IUIFormScreenAdaptor ScreenAdaptor { get; } private void _OnInitScreenAdapter() { var uiGroupHelper = (FairyGUIGroupHelper)UIForm.UIGroup.Helper; var uiGroupRoot = uiGroupHelper.GroupRoot; ScreenAdaptor.Initialize(ContentPane, uiGroupRoot); } private void _OnRecycleScreenAdapter() { ScreenAdaptor.Dispose(); } }
固定尺寸界面的适配方案很简单, 只关心位置不关心尺寸, 直接贴上源码:
/// <summary> /// 固定尺寸界面适配器, 界面尺寸不随屏幕尺寸变化, 提供位置适配功能 /// </summary> public sealed class ConstantUIFormScreenAdaptor : IUIFormScreenAdaptor { private readonly bool m_IsHorizontalCenter; private readonly bool m_IsVerticalCenter; private GComponent m_ContentPane; private GComponent m_UIGroupRoot; public ConstantUIFormScreenAdaptor(bool isHorizontalCenter = true, bool isVerticalCenter = true) { m_IsHorizontalCenter = isHorizontalCenter; m_IsVerticalCenter = isVerticalCenter; } public void Initialize(GComponent contentPane, GComponent uiRoot) { m_ContentPane = contentPane; m_UIGroupRoot = uiRoot; var xy = Vector2.zero; if (m_IsHorizontalCenter) xy.x = (uiRoot.width - contentPane.width) / 2; if (m_IsVerticalCenter) xy.y = (uiRoot.height - contentPane.height) / 2; contentPane.SetXY(xy.x, xy.y, true); if (m_IsHorizontalCenter) contentPane.AddRelation(uiRoot, RelationType.Center_Center); if (m_IsVerticalCenter) contentPane.AddRelation(uiRoot, RelationType.Middle_Middle); } public void Dispose() { if (m_IsHorizontalCenter) m_ContentPane.RemoveRelation(m_UIGroupRoot, RelationType.Center_Center); if (m_IsVerticalCenter) m_ContentPane.RemoveRelation(m_UIGroupRoot, RelationType.Middle_Middle); } }
以确认弹窗为例, 可以看到运行时它的大小并不会发生变化, 且始终在屏幕中央:
FairyGUI编辑器中的设置
代码中指定使用固定尺寸适配器, 并保持水平居中、垂直居中
运行时效果图