查看原文
其他

最佳实践 |对象放置和物理效果

2018-01-22 Unity Unity官方平台

在Spotlight团队中,我们和优秀的Unity开发人员一起合作,深度挖掘Unity在游戏开发中的潜力。针对复杂图形、性能和设计方面的问题,我们看到了各种具有创新性的优秀解决方案。我们也看到了同样的问题与解决方案不断的反复出现。


最佳实践系列文章将探讨我们在与客户合作时遇到的一些常见的问题。这些都是我们的合作团队辛苦得出的经验和教训,我们很自豪能够和大家分享他们的智慧。我们已经为大家分享过以下内容:


这其中的很多问题只有在真正制作主机游戏、手机游戏、或者处理巨量游戏内容时才会出现。如果能在开发早期就将这些问题考虑进去,那么开发过程就会更轻松,而游戏也会更炫酷。

 

将对象放置到理想位置

尽管摆放对象是在Unity中最为常见的任务之一,我们还是看到不少团队为了如何能更好地完成这项工作而苦恼。要把对象排列完美,不让角色跑出世界,不让寻路出现障碍,并让各个物理对象能够放置在理想的位置,这的确不是件轻松的事。

 

 

Unity提供了一些工具来方便处理这个问题,这些工具各自扮演着不同的角色。尤其是在我们想要制作大量重复使用的内容,或是制作一张放置了大量动态物理对象的场景,了解这些工具的使用方法则尤为重要。

 

使用正确的辅助工具

Unity中提供了变换、旋转和缩放工具,在Unity 2017.3中还加入了能同时拥有这三个功能的通用工具。这些小工具能在3D环境下修改游戏对象的变换属性。除此之外,还有2D环境下处理对象的矩形变换工具(Rect Transform)。

 

所有这些辅助工具的运作方式都与其所处的工作模式直接相关。如果将模式设为中心(Center),辅助工具将使用根据对象边界计算得出的对象中心点。这个计算结果是个近似值,所以在处理复杂对象时或许会感觉该中心点偏移了真正的中心点。

 

如果将辅助工具模式设为轴心(Pivot),辅助工具将使用对象自身引用框的原点(0,0,0)。这个轴心点是由创建该资源的作者在制作时设定的。通常需要将对象按照彼此相对的位置进行摆放时,我们推荐使用轴心模式,而且在制作资源时要留意轴心点的位置。

 

第二个切换按钮,全局/局部(Global / Local)切换按钮,确定了操作对象时所处的空间。设为全局(Global)意味着辅助工具将会对齐世界(x,y,z)坐标轴,而设为局部(Local)将会对齐对象自身的(x,y,z)坐标轴。通常我们需要在局部空间下处理对象。但如果我们想要让多个对象互相对齐,则会切换到全局坐标系,并使用网格对齐工具来排列对象。


吸附对象

顶点吸附

在使用变换工具时按住V,变换辅助工具会切换为顶点吸附模式,显示为一个小方框。然后点击所选对象网格上的一个顶点,将其拖到光标下的任意顶点上。在处理任意网格的对齐时,尤其是要处理那些复杂的或是不存在碰撞的网格对齐时,这个方法很实用。

 

 

在上图中,不管是电线还是台灯都没有紧凑的碰撞网格,而且它们也都不是规则的立方体,所以它们不能用网格吸附或碰撞吸附处理。

 

如果需要这样放置多个游戏对象,我们也可以将顶点吸附模式保持在打开状态。按下Ctrl+Shift+V打开,按下Ctrl+V关闭。这样便不需要一直按住V就能顶点吸附多个对象了。


碰撞吸附

 按下CTRL + SHIFT可以启用碰撞吸附。启用后,必须拖拽变换辅助工具中心的黄色小方框,而不是使用坐标轴控制工具。这样会让所选对象与光标下对象的碰撞体相对齐。

 


用顶点吸附或网格吸附来对齐网格在处理静态几何体时十分出色,但如果要处理动态对象,比如说互相穿透并在触碰后发射到空中的对象时,应该吸附到碰撞体而不是可见网格。

 

碰撞安置

如果有大量动态物理对象,或是十分复杂的关节系统,利用碰撞吸附来放置对象也许就不太合适。幸运的是,现在Unity编辑器中可以运行PhysX。它能模拟几秒的游戏时间,完成物理效果,然后保存完成时对象的位置信息。

 

 

下方是示例代码,代码中不仅有大量注释,还包含了调试信息绘制和一个菜单项。只要将其放入项目中的任何一个Editor文件夹里,便能看到一个新的GameMenu->Settle Physics菜单项。


示例代码:PhysicsSettler.cs

using UnityEngine;

using UnityEditor;


[InitializeOnLoad]

class PhysicsSettler

{

    static bool registered = false;

    static bool active = false;

    static Rigidbody[] workList;

    static bool cachedAutoSimulation;

    const float timeToSettle = 10f;

    static float activeTime = 0f;


    static PhysicsSettler()

    {

        if (!registered)

        {

            EditorApplication.update += Update;

            SceneView.onSceneGUIDelegate += OnSceneGUI;

            registered = true;

        }

    }


    [MenuItem("GameMenu/Settle Physics")]

    static void Activate()

    {

        if( !active )

        {

            active = true;

            workList = Object.FindObjectsOfType<Rigidbody>();

            cachedAutoSimulation = Physics.autoSimulation;

            activeTime = 0f;


            foreach( Rigidbody body in workList )

            {

                body.WakeUp();

            }

        }

    }


    [MenuItem("GameMenu/Settle Physics", true)]

    static bool checkMenu()

    {

        return !active;

    }


    static void Update()

    {

        if( active )

        {

            activeTime += Time.deltaTime; 

            Physics.autoSimulation = false;

            bool allSleeping = true;

            foreach( Rigidbody body in workList )

            {

                if( body != null )

                {

                    allSleeping &= body.IsSleeping();

                }

            }


            if( allSleeping || activeTime >= timeToSettle)

            {

                Physics.autoSimulation = cachedAutoSimulation;

                active = false;

            }

            else

            {

                Physics.Simulate(Time.deltaTime);

            }

        }

    }


    static void OnSceneGUI(SceneView sceneView) 

    {

        if( active )

        {

            Handles.BeginGUI();

            Color cacheColor = GUI.color;

            GUI.color = Color.red;

            GUILayout.Label("Simulating Physics.", GUI.skin.box, GUILayout.Width(200));

            GUILayout.Label(string.Format("Time Remaining: {0:F2}",(timeToSettle - activeTime)), GUI.skin.box, GUILayout.Width(200));

            Handles.EndGUI();


            foreach( Rigidbody body in workList )

            {

                if( body != null )

                {

                    bool isSleeping = body.IsSleeping();

                    if( !isSleeping )

                    {

                        GUI.color = Color.green;

                        Handles.Label(body.transform.position, "SIMULATING");

                    }

                }

            }

            GUI.color = cacheColor;

        }

    }

}


 

当了解了何时和为何使用网格、顶点吸附、碰撞吸附时,我们将在关卡设计工作流程获得更好的开发体验。推荐开发者们多多尝试这些工具,例如:对比轴心(Pivot)和中心(Center)模式的区别,或是比较全局(Global)和局部(Local)模式的不同,这样就可以知道什么模式更为适合。花费在布置虚拟对象的时间越少,那么在开发时得到的乐趣就越多。

 

小结

感谢Campo Santo提供的资源作为示例场景。我们还将为大家分享游戏开发更多实践经验与在Unity官方中文社区(unitychina.cn),请保持关注!



推荐阅读


Unite 2018 Beijing

Unite 2018 Beijing

作为全球规模最大的Unity开发者聚会,2018年的Unite大会将于5月11-13日在北京国家会议中心举行。新年特惠技术通票正在限量发售中,原价1500元,限时特惠价格599元。  

购票地址:http://unite2018beijing.bagevent.com



点击“阅读原文”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存