您如何使用Unity 2D处理宽高比差异?


67

对于这个问题,我已经得到了很多答案,但是它们都是通用的,通常不是很有用。这些教程都没有涉及长宽比和移动设备的处理,并且有无数种方法可以实现,似乎都存在缺陷。

我真的很想知道成功的游戏在没有大量不同尺寸资产的情况下,如何在iOS和Android上处理不同的宽高比。

我严格来说是移动设备,而不是台式机,特别是Unity设备,我不在乎UI,我只在乎游戏画布。

我想到的问题是,某些关键的东西必须放在某些地方并且不能掉到屏幕上。如今,在顶部或底部使用黑条是不可接受的。


3
这个问题非常广泛,因为正确的方法几乎取决于一切。你尝试了什么?为什么不起作用?
Anko 2014年

3
我尝试了各种方法,尝试调整了正交摄影机的尺寸,尝试将所有子画面附加到列表中并通过纵横比的差异对其进行缩放,将正交尺寸设置为screen.height / 2/100,还有许多其他想法。一些工作,但所有的人都有问题。我知道不同的游戏会处理不同的问题,但是绝对没有任何地方讨论这个话题,而且它不像许多人声称的那样“让团结来处理”那么容易。
2014年

1
那么,为什么它们不起作用?一个好的解决方案是什么样的?(顺便说一句,您也可以编辑问题以明确说明。)
Anko 2014年

7
一些扭曲了图像,有些没有正确排列。许多不同的问题,但是使用Unity开发的游戏中有65%是2D的,并且使2D可以正常工作。我只想知道人们正在使用什么,而不必重新发明轮子。没有人谈论它,也没有有关如何处理它的指南或文档。但是,如果没有合适的系统来完成移动项目,您将无法走得远。
2014年

1
“我要记住的问题是,某些关键的东西必须放在某些地方并且不能从屏幕上掉下来。如今,在顶部或底部使用黑条是不可接受的。” 保证元素不会从屏幕上掉落,零失真,但不会出现字母/毛坯堆积(黑条等)。这些要求是不可调和的。最后一个要求可能是最不重要的,或者可以通过将画布填充超出屏幕上必须显示的范围来隐藏。我所见过的大多数游戏都具有如此严格的要求,这些游戏都会带有装饰性的柱箱/边框。
Jibb Smart 2015年

Answers:


36

您想要的是通过计算camera.orthographicSize属性将照相机视口限制在纵向或横向(取决于您的需求)上,以便无论宽高比和分辨率如何,都可以构建2d场景:

// Attach this script on your main ortohgraphic camera:

/* The MIT License (MIT)

Copyright (c) 2014, Marcel Căşvan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */

using System;
using System.Collections;
using UnityEngine;

[ExecuteInEditMode]
[RequireComponent (typeof (Camera))]
public class ViewportHandler : MonoBehaviour
{
    #region FIELDS
    public Color wireColor = Color.white;
    public float UnitsSize = 1; // size of your scene in unity units
    public Constraint constraint = Constraint.Portrait;
    public static ViewportHandler Instance;
    public new Camera camera;

    private float _width;
    private float _height;
    //*** bottom screen
    private Vector3 _bl;
    private Vector3 _bc;
    private Vector3 _br;
    //*** middle screen
    private Vector3 _ml;
    private Vector3 _mc;
    private Vector3 _mr;
    //*** top screen
    private Vector3 _tl;
    private Vector3 _tc;
    private Vector3 _tr;
    #endregion

    #region PROPERTIES
    public float Width {
        get {
            return _width;
        }
    }
    public float Height {
        get {
            return _height;
        }
    }

    // helper points:
    public Vector3 BottomLeft {
        get {
            return _bl;
        }
    }
    public Vector3 BottomCenter {
        get {
            return _bc;
        }
    }
    public Vector3 BottomRight {
        get {
            return _br;
        }
    }
    public Vector3 MiddleLeft {
        get {
            return _ml;
        }
    }
    public Vector3 MiddleCenter {
        get {
            return _mc;
        }
    }
    public Vector3 MiddleRight {
        get {
            return _mr;
        }
    }
    public Vector3 TopLeft {
        get {
            return _tl;
        }
    }
    public Vector3 TopCenter {
        get {
            return _tc;
        }
    }
    public Vector3 TopRight {
        get {
            return _tr;
        }
    }
    #endregion

    #region METHODS
    private void Awake()
    {
        camera = GetComponent<Camera>();
        Instance = this;
        ComputeResolution();
    }

    private void ComputeResolution()
    {
        float leftX, rightX, topY, bottomY;

        if(constraint == Constraint.Landscape){
            camera.orthographicSize = 1f / camera.aspect * UnitsSize / 2f;    
        }else{
            camera.orthographicSize = UnitsSize / 2f;
        }

        _height = 2f * camera.orthographicSize;
        _width = _height * camera.aspect;

        float cameraX, cameraY;
        cameraX = camera.transform.position.x;
        cameraY = camera.transform.position.y;

        leftX = cameraX - _width / 2;
        rightX = cameraX + _width / 2;
        topY = cameraY + _height / 2;
        bottomY = cameraY - _height / 2;

        //*** bottom
        _bl = new Vector3(leftX, bottomY, 0);
        _bc = new Vector3(cameraX, bottomY, 0);
        _br = new Vector3(rightX, bottomY, 0);
        //*** middle
        _ml = new Vector3(leftX, cameraY, 0);
        _mc = new Vector3(cameraX, cameraY, 0);
        _mr = new Vector3(rightX, cameraY, 0);
        //*** top
        _tl = new Vector3(leftX, topY, 0);
        _tc = new Vector3(cameraX, topY , 0);
        _tr = new Vector3(rightX, topY, 0);           
    }

    private void Update()
    {
        #if UNITY_EDITOR
        ComputeResolution();
        #endif
    }

    void OnDrawGizmos() {
        Gizmos.color = wireColor;

        Matrix4x4 temp = Gizmos.matrix;
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
        if (camera.orthographic) {
            float spread = camera.farClipPlane - camera.nearClipPlane;
            float center = (camera.farClipPlane + camera.nearClipPlane)*0.5f;
            Gizmos.DrawWireCube(new Vector3(0,0,center), new Vector3(camera.orthographicSize*2*camera.aspect, camera.orthographicSize*2, spread));
        } else {
            Gizmos.DrawFrustum(Vector3.zero, camera.fieldOfView, camera.farClipPlane, camera.nearClipPlane, camera.aspect);
        }
        Gizmos.matrix = temp;
    }
    #endregion

    public enum Constraint { Landscape, Portrait }
}

如果您需要更多相关信息,请询问,我会回复。;)问候和欢呼。

更新:Eliot Lash的对象锚定脚本与此脚本一起使用,以便在需要时将对象放置在屏幕上的关键位置(相对于屏幕角/边框)。如果这样做,则将“ CameraFit”重命名为“ ViewportHandler”。

预览模拟各种纵横比的屏幕: 在此处输入图片说明


4
@Eliot在上面添加了MIT许可证。祝您项目顺利!
androidu 2015年

7
太好了,谢谢!一个好转
Eliot

1
MarcelCăşvan非常好的脚本!我添加了一个枚举,用于选择是否要为景观定义potrait的单位大小(高度/宽度)。只需在脚本中添加几行,然后我就可以GetComponent<Camera>().orthographicSize = UnitSize / 2f;用于定义/定义高度单位
am_ 2015年

2
@MarcelCăşvan很棒的东西!谢谢。我看到ComputeFunction()的deviceWidth和deviceHeight变量未使用。也许考虑删除这些。
user2313267 '16

1
CameraAnchor引发错误:“在此上下文中未定义CameraFit” –如果以后有人找到此答案,则似乎“ CameraFit”刚刚重命名为“ ViewportHandler”,因为它最初是发布的。如果只是将类从ViewportHandler重命名回CameraFit。
Lenny

5

通常,您不需要使用其他大小的资源-具有自动生成的mip贴图的导入纹理和精灵在以小于或等于图像原始像素大小的任何大小进行渲染时都将看起来不错。

场景布局是挑战。一种很好的方法如下(并且仅供参考,我使用3D相机查看位于z = 0处的2D内容):

  1. 任意决定最小“逻辑”显示尺寸(以像素或图块为单位)。这无需与任何实际分辨率相对应,但反映您要支持的最窄/最短纵横比。例如,对于横向游戏,我不会选择480x320,因为它的宽高比比iPad大。因此,我可能选择1024x768-甚至是480x360,这使我可以使用原始的iPhone大小的坐标系,并且纵横比与每个iPad(包括iPad Air 2等)相同。另请注意,您可以轻松地在图块坐标而不是像素坐标中工作-例如15x11.25。
  2. 对游戏逻辑进行编程,以便将所有重要内容(或可以放置在最小显示尺寸之内),但要准备在侧面增加额外的空间,即使只是装饰性填充物。
  3. 确定您需要缩放内容的大小,以使宽度或高度与最小值匹配,并且另一个轴大于或等于所需的最小值。若要进行“缩放以适合”,请将屏幕像素尺寸除以最小显示尺寸,然后将较小的结果比例值用作您的整体视图比例。
  4. 使用视图比例来计算有效(实际)显示尺寸,以达到游戏逻辑目的。
  5. 通过沿Z轴移动相机来实际缩放内容。

以代码形式:

  // Adjust the camera to show world position 'centeredAt' - (0,0,0) or other - with
  // the display being at least 480 units wide and 360 units high.

  Vector3 minimumDisplaySize = new Vector3( 480, 360, 0 );

  float pixelsWide = camera.pixelWidth;
  float pixelsHigh = camera.pixelHeight;

  // Calculate the per-axis scaling factor necessary to fill the view with
  // the desired minimum size (in arbitrary units).
  float scaleX = pixelsWide / minimumDisplaySize.x;
  float scaleY = pixelsHigh / minimumDisplaySize.y;

  // Select the smaller of the two scale factors to use.
  // The corresponding axis will have the exact size specified and the other 
  // will be *at least* the required size and probably larger.
  float scale = (scaleX < scaleY) ? scaleX : scaleY;

  Vector3 displaySize = new Vector3( pixelsWide/scale, pixelsHigh/scale, 0 );

  // Use some magic code to get the required distance 'z' from the camera to the content to display
  // at the correct size.
  float z = displaySize.y /
            (2 * Mathf.Tan((float)camera.fieldOfView / 2 * Mathf.Deg2Rad));

  // Set the camera back 'z' from the content.  This assumes that the camera
  // is already oriented towards the content.
  camera.transform.position = centeredAt + new Vector3(0,0,-z);

  // The display is showing the region between coordinates 
  // "centeredAt - displaySize/2" and "centeredAt + displaySize/2".

  // After running this code with minimumDisplaySize 480x360, displaySize will
  // have the following values on different devices (and content will be full-screen
  // on all of them):
  //    iPad Air 2 - 480x360
  //    iPhone 1 - 540x360
  //    iPhone 5 - 639x360
  //    Nexus 6 - 640x360

  // As another example, after running this code with minimumDisplaySize 15x11
  // (tile dimensions for a tile-based game), displaySize will end up with the following 
  // actual tile dimensions on different devices (every device will have a display
  // 11 tiles high and 15+ tiles wide):
  //    iPad Air 2 - 14.667x11
  //    iPhone 1 - 16.5x11
  //    iPhone 5 - 19.525x11
  //    Nexus 6 - 19.556x11

2

如果您开始使用条形图,它实际上很容易实现(尽管OP指出它是不可接受的,但我还是在发布它,因为它的好处是在移动性上不那么差,这是一个简单的解决方案,不需要任何代码)

Camera.orthographicSize是正交摄影机(大多数2D游戏使用的)中的变量,该变量适合屏幕上垂直测量的游戏单元数量(除以2)()。因此,选择适合大多数设备的宽高比(我选择16:9,因为我研究的大多数屏幕都是16:9、16:10、3:2),并添加以一定比例覆盖该屏幕的蒙版。

范例

在我的游戏中(此处未列出,因为这不是广告,如果需要,可以在评论中提问),我们使用纵向模式。为了做一个简单的16:9,我将Ortho相机的尺寸设置为16。这意味着相机会将32个游戏高度单位(在我的情况下为y:16到-16)调整到设备屏幕的垂直方向。

然后,我将黑色口罩放置在-9至+9之间进行游戏。瞧,游戏的屏幕在所有设备上看起来完全一样,而在稍宽一些的设备上看起来更窄。我绝对没有关于口罩的负面反馈。要制作风景,只需将这些值翻转即可,然后使相机的大小为9。更改这些值以匹配您决定的游戏单位比例。

我们观察到的黑条显示的唯一位置是在iPad上3:2。即使那样,我也没有任何抱怨。


1

我正在当前正在开发的游戏中进行此操作。我的背景图片是1140x720。最重要的位(永不被裁剪的位)包含在960x640中间区域中。我在相机的启动功能上运行以下代码:

    float aspect = (float)Screen.width / (float)Screen.height;

    if (aspect < 1.5f)
        Camera.main.orthographicSize = 3.6f;
    else
        Camera.main.orthographicSize = 3.2f;

    float vertRatio = Screen.height / 320.0f;
    fontSize = (int)(12 * vertRatio);

除了按钮等的字体大小外,我还定义了其他大小。它在我测试过的每个纵横比上都可以正常工作。自设置以来已经有一段时间了,所以我可能错过了一步。让我知道它是否无法正常工作,我会看看是否遗漏了任何东西。


1

@Marcel的答案和代码很棒,可以帮助我了解正在发生的事情。这是绝对的答案。只是以为某人可能还会发现我最终针对我的特定案例所做的事情是有用的:因为我想要一个非常简单的东西,一个精灵始终显示在屏幕上,所以我想到了以下几行:

public class CameraFit : MonoBehaviour {

    public SpriteRenderer spriteToFitTo;

    void Start () { // change to Update to test by resizing the Unity editor window
        var bounds = spriteToFitTo.bounds.extents;
        var height = bounds.x / camera.aspect;
        if (height < bounds.y)
            height = bounds.y;
        camera.orthographicSize = height;
    }
}

我将其添加到相机,并将精灵(这是我的背景)拖到脚本的唯一属性。如果您不想要任何黑条(水平或垂直),可以在此黑条后面放一个更大的背景...


0

有几种方法可以解决此问题,并且还没有一个完美的解决方案(至少我还没有找到一种解决方案),您选择的解决方案类型将在很大程度上取决于您所使用的游戏类型发展。

无论您做什么,都应从选择想要支持的最低分辨率开始,然后将Sprite构建为该分辨率。因此,如果您有兴趣为iOS开发,请访问http://www.iosres.com/,最低的iOS设备分辨率为480x320。

从那里,您可以开始按比例放大精灵,以满足更高的分辨率。需要注意的是,您最终会开始注意到精灵在您扩展得越远时开始变得模糊,在这种情况下,您可以切换到为更高分辨率而构建的另一组精灵。

或者,您可以完全忽略缩放比例,而只是决定以更高的分辨率显示更多的游戏屏幕(例如,我相信这就是Terraria的工作方式)。但是,对于许多游戏来说,这是不合适的。例如竞技游戏。

如今,在顶部或底部使用黑条是不可接受的。

是吗?许多想要强制4:3宽高比的游戏都这样做。由于使用的是Unity,因此可以使用AspectRatioEnforcer脚本来帮助实现这一点。


我的目标是移动设备,因此无法更改宽高比或分辨率。使用低分辨率资产不是可接受的解决方案。某些游戏(尤其是3D游戏)不是问题,您只显示或多或少即可。但是,无论设备如何,像Kingdom Rush,塔防等游戏和Alex这样的其他游戏都希望看到相同的东西。如果未在正确的位置显示内容,则游戏将无法正常运行。
2014年


0

有多种方法可以解决此问题,这取决于您的游戏以及最有效的方法。例如,在我们的游戏Tyrant Unleashed中,我们只是制作了宽幅地图,侧面没有重要的细节,因此可以在较窄的设备上剪掉侧面。但是,通过实际移动按钮或更好地适合屏幕的方法,其他游戏可能会更好。

(这也是我们方法的一部分,即在所有设备上保持一致的高度,只是改变宽度。这当然使我们的生活更轻松,但这再次对您的特定游戏可能有好处,也可能没有好处。我们的艺术风格(如果图像在不同的屏幕上稍微缩放),而这对于像素艺术来说可能很重要。基本上,这是“让Unity处理”的方法)


您是否曾做过其他无法选择的项目?我见过的几乎所有塔防风格的游戏以及许多其他风格的游戏,无需滚动即可在设备上看到整个游戏视图,并且这在设备之间是一致的。在IOS上,您可以制作资产并交换资产,但是在Android上这是不可能的(无论如何实际上只是一个很大的PITA)。
2014年

不,我没有。顺便说一句,我刚刚添加了一些有关屏幕大小的更多详细信息
晃动

0

我正在使用以下脚本,该脚本targetAspect向摄像机添加了一个参数,并根据orthographicSize屏幕比例对其进行了调整(此博客文章中有更多详细信息):

using UnityEngine;
using System.Collections;

public class AspectRatioScript : MonoBehaviour {

    public float targetAspect;

    void Start () 
    {
        float windowAspect = (float)Screen.width / (float)Screen.height;
        float scaleHeight = windowAspect / targetAspect;
        Camera camera = GetComponent<Camera>();

        if (scaleHeight < 1.0f)
        {  
            camera.orthographicSize = camera.orthographicSize / scaleHeight;
        }
    }
}

0

我的方法与其他人提供的解决方案基本相似:)我将尝试详细说明使游戏与屏幕大小无关的方法。

屏幕方向

根据屏幕方向(横向或纵向),您需要考虑相机将以固定高度还是固定宽度缩放。通常,对于横向游戏,我选择固定宽度;对于纵向游戏,我选择固定高度。

相机缩放

如所讨论的,这可以是固定高度或固定宽度。

固定高度:游戏的垂直区域将始终适合屏幕高度。并且随着屏幕长宽比的变化,屏幕的左右两侧将增加额外的空间。为此,您无需编写任何代码,这是unity camera的默认行为。

固定宽度:游戏的水平区域将始终适合屏幕宽度。随着屏幕长宽比的变化,顶部和底部将添加额外的空间。要实现这一点,您需要编写一小段代码。稍后请确保删除代码形式更新功能,并将其置于唤醒状态。

using UnityEngine;

[ExecuteInEditMode]
public class ScaleWidthCamera : MonoBehaviour {

    public int targetWidth = 640;
    public float pixelsToUnits = 100;

    void Update() {

        int height = Mathf.RoundToInt(targetWidth / (float)Screen.width * Screen.height);

        camera.orthographicSize = height / pixelsToUnits / 2;
    }
}

在编辑器中,您可以更改targetWidth来定义要显示的世界空间区域。以下视频以及2D游戏的许多其他用法说明了此代码:)

Unite 2014-Unity中的2D最佳做法

长宽比

遵循从最宽到最窄的宽高比,涵盖了Android和iOS的几乎所有屏幕尺寸

  • 5:4
  • 4:3
  • 3:2
  • 16:10
  • 16:9

我通常会在游戏窗口下按给定的顺序设置所有这些宽高比,因为在测试不同屏幕尺寸时很方便:)

消耗面积

这是根据所选的摄像机缩放比例而添加到屏幕的侧面或顶部/底部的区域。

对于固定高度,所有游戏元素最好以最窄的16:9比例适合。并且背景应该扩展到覆盖5:4的比例。这样可以确保您的游戏两侧都不会出现黑条。

对于固定宽度,它几乎相同,但是此处元素应以5:4的比例放置,而BG应延伸到16:9。

界线

有时我们无法使用消耗性区域方法,因为我们需要利用整个可用屏幕进行游戏。

例如,考虑一个固定高度的肖像游戏,捕捉从天上掉下来的硬币。在这种情况下,我们需要使播放器能够在可用的屏幕宽度上水平移动。

因此,我们需要根据世界坐标来确定摄影机的边界,以知道摄影机剪辑在世界位置的确切位置是在左侧,右侧,顶部还是底部。
我们还可以使用这些边界将游戏元素或UI锚定到相机的所需侧面。

使用Camera.ViewportToWorldPoint我们可以得到边界。视口空间是标准化的并且相对于相机。相机的左下角是(0,0); 右上角是(1,1)。z位置以相机的世界单位为单位。对于2D /正交,z无关紧要。

Vector3 leftBottom = camera.ViewportToWorldPoint(new Vector3(0, 0, camera.nearClipPlane));
Vector3 rightTop = camera.ViewportToWorldPoint(new Vector3(1, 1, camera.nearClipPlane));

float left = leftBottom.x;
float bottom = leftBottom.y;
float right = rightTop.x;
float top = rightTop.y;

用户界面

对于UI,我们可以应用与游戏元素相同的概念。引入Unity5 UI并提供NGUI之类的插件之后,这将不再是问题:)


0

我创建了Unity Asset'Resolution Magic 2D'来解决这个问题(广告:您可以从Unity Asset Store中获得它,或者在grogansoft.com上查看更多详细信息)。

我解决问题的方式如下...

定义屏幕区域,无论宽高比/分辨率如何,该区域都必须始终可见,并使用简单的矩形变换来“模版”出该区域。这是您理想的屏幕形状。然后,我使用一种简单的算法来缩放摄影机,直到被矩形遮挡的区域尽可能大,而摄影机仍可以100%看到它。

然后,您的主游戏区域总是占据尽可能多的屏幕。只要在您之前定义的矩形区域之外有足够的“额外”内容(例如您的背景),屏幕与您的“理想”矩形不同的长宽比的播放器将看到多余的内容,否则黑条会走。

我的资产还包括一些用于放置UI的逻辑,但是由于Unity的新UI系统而已过时。

我的资产通过最少的设置即可提供开箱即用的功能,并且在所有平台上都能很好地工作。

如果您使用这种技术(或我的资产),只需确保将游戏设计为在四周具有“可选”空间,以容纳比理想屏幕宽或高的屏幕(以避免出现黑条)。

我个人没有制作3D游戏,所以我不知道这是否可以在3D中工作(或者是否有必要)。

没有图片真的很难解释,因此请访问我的网站(Resolution Magic 2D


0

对我而言,最好的解决方案是使用相交线定理,以使侧面不存在任何割缝,也不会扭曲游戏画面。这意味着您必须根据不同的宽高比前进或后退。


-3

我创建了一个AssetStore扩展,它使纵横切换更加容易,称为AspectSwitcher。它提供了一个系统,使您可以轻松地为不同方面指定不同的属性。大多数人通常使用两种方法来切换方面。一种是为每个方面提供不同的游戏对象。另一种方法是创建自定义代码,该代码根据当前方面修改单个游戏对象的属性。这通常需要大量的自定义编码。我的扩展程序试图减轻很多痛苦。


2
如果您详细说明了可能用于解决OP的问题的实际技术,则此答案会更好,并且看起来不会那么垃圾。
乔什
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.