开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

蛮牛译馆

关注:481

当前位置:游戏蛮牛 技术专区 蛮牛译馆

查看: 419|回复: 3

[Unity教程] C#教程-Swirly Pipe,构建无尽模式的赛道

[复制链接]  [移动端链接]
排名
3480
昨日变化
2

27

主题

121

帖子

686

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
47400
好友
6
蛮牛币
867
威望
0
注册时间
2014-9-30
在线时间
110 小时
最后登录
2017-1-4

蛮牛译员

QQ
发表于 2016-11-10 14:23:44 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 木木妖妖 于 2016-11-10 14:30 编辑

Swirly Pipe
Prototyping an Endless Racer
· Come up with a simple concept.
· Create a torus.
· Stitch endless random pipes together.
· Move the world around the player.
· Place obstacles inside pipes.
· Add a game menu and HUD.
· Deal with multiple target platforms.


在本教程中,我们将创建无尽游戏模式中的一个简单赛车原型。你可以尝试在浏览器中使用网络播放器或WebGL来查看或者是iOS设备上玩

我希望你能明白自己使用Unity编辑器的风格,并熟悉Unity脚本的基本知识。如果你已经学习了迷宫教程,那么你可以继续学习下去。查看Procedural Grid从而了解网格的基本知识。

本教程使用的是Unity 5.0.1


Race to infinity through dark pipes. 通过暗黑的管道来到无尽模式

From Concept to Prototype  从概念到原型
你怎么创建的游戏呢?你先提出一个概念,然后开始设计它,接着创建它,最后发布它,是不是这样的呢?

其实不全是这样的呢。你怎么知道你的想法是好的呢?你可以建立一个模型来测试一下。


关于模型的构想是指尽快把一个想法实现出来。把它放在人们的手上,看看你是否对某事很专注。如果反馈是有希望的,它是值得去多创建一点的,然后收集更多的反馈。接着你可以开始思考实际的设计,也许可以创建真正的游戏。如果有了扁平的模型概念,你可以避免白费很多功夫,这样可以开始一个新的设想。


蓝本不是一个完美的应用程序。它几乎没有什么功能,相当的丑陋以及混乱。它应该运行得足够好,使其对核心的体验是有效的,而这就是它。如果你的设想不是很好,那么所有优化和构建代码的时间都浪费了。不要故意搞的一团糟。如果有一些东西是一致的,以便任何人-包括自己-最终完成了真正的游戏,让模型变得有意义。


在本教程中,我们保持概念的简单性。3D无尽赛车游戏模式,穿过管道以及障碍。因为笔直的赛道很无聊,它应该是无休止的弯曲赛道。


赛道的形状是怎样影响了游戏的感觉。他们不应该太直,因为这很乏味。弯道应该也不能太急,那使它变得不能玩。我们可以使用什么技术来根据我们的爱好来创建赛道呢?


贝塞尔样条曲线看起来还不错。他们给了我们很多的控制权,所以我们可以做出任何我们想要的曲线。然而,他们不容易控制。我们如何限制随机放置的控制点以便于达到最终可以接受的形状,这是不好做到的。也没有简单而直接的方法来确定一个曲线段的长度。

Constructing a Bézier spline. 创建一个贝塞尔样条线


相反,我们可以构造附带双弧的管道系统。一个双圆弧由两段圆弧构成。你通过连接两段圆弧来创建。通过限制圆的半径和弧长来控制会很容易,计算每段弧的长度也很简单。


Constructing a biarc. 创建双弧


这是最重要的技术问题。现在是开始建立原型的时候了。当我们做的时候,我们会把一切都弄清楚。
Building a Pipe 创建一个管道

赛道是我们游戏中最重要的元素,所以让我们从这个开始吧。一个单一的赛道只是圆的一部分。如果我们是在3D场景中,那赛道就是圆环的一部分。


一个圆环是由两个圆的半径定义的。我们场景中的赛道,都是由管道的半径以及下图所示的曲线半径决定的。让我们把这些放在一个组件里。



一个圆环是一个圆环绕着另一个圆。
[AppleScript] 纯文本查看 复制代码
using UnityEngine;
public class Pipe : MonoBehaviour {
 
public float curveRadius, pipeRadius;}



我们将使用网格来创建管道的表面,所以我们需要决定如何将管道分割成矩形。沿曲线的线段数是独立于管道表面的部分。

        public int curveSegmentCount, pipeSegmentCount;


现在我们可以创建一个新的游戏对象,并把它变成一个管道。让我们把管道半径设置为1,曲线半径为四。我将管道表面划分为十个部分,并使用二十段的曲线。

A pipe ready for testing. 用于测试的管道

要放置顶点,我们需要能在圆环的表面上找到点。环面可以使用三维正弦函数描述。
x = (R + r cos v) cos u
y = (R + r cos v) sin u
z = r cos v


在上面的函数中,r是管道的半径。R是曲线的半径。U参数定义了沿曲线的弧度角,所以在02π范围之间。V参数定义了沿管道的角度。我们可以为它创建一个合适的函数
[AppleScript] 纯文本查看 复制代码
private Vector3 GetPointOnTorus (float u, float v) {
Vector3 p;
float r = (curveRadius + pipeRadius * Mathf.Cos(v));
p.x = r * Mathf.Sin(u);
p.y = r * Mathf.Cos(u);
p.z = pipeRadius * Mathf.Sin(v);
return p;
}



为了检验我们是否做得对,让我们在场景中画一些小玩意儿。首先只是一个围绕管道开始的单一环。
[AppleScript] 纯文本查看 复制代码
private void OnDrawGizmos () {
float vStep = (2f * Mathf.PI) / pipeSegmentCount;
for (int v = 0; v < pipeSegmentCount; v++) {
Vector3 point = GetPointOnTorus(0f, v * vStep);
Gizmos.DrawSphere(point, 0.1f);
}
}



The first ring. 第一个环

它工作了,现在展示整个带有颜色的圆环面
[AppleScript] 纯文本查看 复制代码
float uStep = (2f * Mathf.PI) / curveSegmentCount;
float vStep = (2f * Mathf.PI) / pipeSegmentCount;
 
for (int u = 0; u < curveSegmentCount; u++) {
for (int v = 0; v < pipeSegmentCount; v++) {
Vector3 point = GetPointOnTorus(u * uStep, v * vStep);
Gizmos.color = new Color(
1f,
(float)v / pipeSegmentCount,
(float)u / curveSegmentCount);
Gizmos.DrawSphere(point, 0.1f);
}
}


An entire torus.


我们可以看到,环面缠绕在Z轴上,顺时针沿着Z环绕。在顶部开始的,与当朝前看的时候,管道是沿着顺时针方向创建的。


现在来构建网格!我们需要给管道添加mesh filter 以及渲染器组件。还设置渲染材质为Unity的默认材质,所以我们可以看到正确的mesh

Mesh components.


当物体处于活动状态时,我们创建mesh。接着需要设置它的顶点以及triangles,为此我添加了一个独立的函数。
        private Mesh mesh;
        private Vector3[] vertices;
        private int[] triangles;

        private void Awake () {
                GetComponent<MeshFilter>().mesh = mesh = new Mesh();
                mesh.name = "Pipe";
                SetVertices();
                SetTriangles();
        }

        private void SetVertices () {}

        private void SetTriangles () {}


我们如何三角化圆环面?每一个矩形与它的neighbors共享顶点,或将每一个矩形的顶点给矩形自己。因为后者是最灵活的,让我们使用它吧。
        private void SetVertices () {
                vertices = new Vector3[pipeSegmentCount * curveSegmentCount * 4];
                mesh.vertices = vertices;
        }


让我们从矩形的第一个环开始。它们在第一步和第二步需要环的顶点。
                vertices = new Vector3[pipeSegmentCount * curveSegmentCount * 4];
                float uStep = (2f * Mathf.PI) / curveSegmentCount;
                CreateFirstQuadRing(uStep);
                mesh.vertices = vertices;


第一步是通过使用u获得两个顶点,AB。接着我们做一个步骤,使用v获得下一对,我们一直保持这样做,直到我们完成了整个圆。每一步,我们分配以前的点和新的点到当前的四变形顶点上。

        private void CreateFirstQuadRing (float u) {
                float vStep = (2f * Mathf.PI) / pipeSegmentCount;

                Vector3 vertexA = GetPointOnTorus(0f, 0f);
                Vector3 vertexB = GetPointOnTorus(u, 0f);
                for (int v = 1; v <= pipeSegmentCount; v++) {
                        vertexA = GetPointOnTorus(0f, v * vStep);
                        vertexB = GetPointOnTorus(u, v * vStep);
                }
        }


我们已经通过分段将这些顶点分配给四边形。

                Vector3 vertexA = GetPointOnTorus(0f, 0f);
                Vector3 vertexB = GetPointOnTorus(u, 0f);
                for (int v = 1, i = 0; v <= pipeSegmentCount; v++, i += 4) {
                        vertices = vertexA;
                        vertices[i + 1] = vertexA = GetPointOnTorus(0f, v * vStep);
                        vertices[i + 2] = vertexB;
                        vertices[i + 3] = vertexB = GetPointOnTorus(u, v * vStep);
                }



让我们用我们的第一个环初始化三角形。每一个四边形有两个三角形,所以有六个顶点参数。
        private void SetTriangles () {
                triangles = new int[pipeSegmentCount * curveSegmentCount * 6];
                mesh.triangles = triangles;
        }


当玩游戏的时候,我们会看到里面的管道,现在能很方便的从外面就能看到管道。因此,让我们给顶点排序,以便三角形显示在管道的外部。

                triangles = new int[pipeSegmentCount * curveSegmentCount * 6];
                for (int t = 0, i = 0; t < triangles.Length; t += 6, i += 4) {
                        triangles[t] = i;
                        triangles[t + 1] = triangles[t + 4] = i + 1;
                        triangles[t + 2] = triangles[t + 3] = i + 2;
                        triangles[t + 5] = i + 3;
                }
                mesh.triangles = triangles;

First ring of quads.



我们第一个环正确显示了。所有其他顶点都还停留在原点,所以我们没有看到其他的四边形。让我们改变这个。
        private void SetVertices () {
                vertices = new Vector3[pipeSegmentCount * curveSegmentCount * 4];
                float uStep = (2f * Mathf.PI) / curveSegmentCount;
                CreateFirstQuadRing(uStep);
                int iDelta = pipeSegmentCount * 4;
                for (int u = 2, i = iDelta; u <= curveSegmentCount; u++, i += iDelta) {
                        CreateQuadRing(u * uStep, i);
                }
                mesh.vertices = vertices;
        }


CreateQuadRingCreateFirstQuadRing函数功能一样,不同的是它每一步只需要添加一个顶点。它可以复制前面环的每个矩形的前两个顶点。

        private void CreateQuadRing (float u, int i) {
                float vStep = (2f * Mathf.PI) / pipeSegmentCount;
                int ringOffset = pipeSegmentCount * 4;

                Vector3 vertex = GetPointOnTorus(u, 0f);
                for (int v = 1; v <= pipeSegmentCount; v++, i += 4) {
                        vertices = vertices[i - ringOffset + 2];
                        vertices[i + 1] = vertices[i - ringOffset + 3];
                        vertices[i + 2] = vertex;
                        vertices[i + 3] = vertex = GetPointOnTorus(u, v * vStep);
                }
        }

Complete torus mesh.


我们现在看到整个圆环,但它看起来是平坦的。这是因为网格没有法线。让Unity计算法线,使它可以有一个更好的外观。圆环面会出现是因为我们使四边形变得独立。
        private void Awake () {
                GetComponent<MeshFilter>().mesh = mesh = new Mesh();
                mesh.name = "Pipe";
                SetVertices();
                SetTriangles();
                mesh.RecalculateNormals();
        }

Now with proper shading. unitypackage

Building a Pipe System 建立管道系统

现在我们正在创建一个完整的圆环,但每一个单独的管道只覆盖圆环的一小部分。我们可以通过将环之间的距离设置为固定值来完成。然后,曲线段的量决定了管道的弧长度。
        public float ringDistance;

        private void SetVertices () {
                vertices = new Vector3[pipeSegmentCount * curveSegmentCount * 4];

                float uStep = ringDistance / curveRadius;
                CreateFirstQuadRing(uStep);
                int iDelta = pipeSegmentCount * 4;
                for (int u = 2, i = iDelta; u <= curveSegmentCount; u++, i += iDelta) {
                        CreateQuadRing(u * uStep, i);
                }
                mesh.vertices = vertices;
        }


只需设置环距离为一个单位。也摆脱OnDrawGizmos,因为我们不再需要它了。

Only part of a torus.



我们需要东西将多个管道衔接在一起。因此,我们创建了一个新的管道系统组件以及对象。它需要一个管道预制体,这样可以创建固定数量的实例来作为本身的子物体。
using UnityEngine;
public class PipeSystem : MonoBehaviour {

        public Pipe pipePrefab;

        public int pipeCount;

        private Pipe[] pipes;

        private void Awake () {
                pipes = new Pipe[pipeCount];
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes = Instantiate<Pipe>(pipePrefab);
                        pipe.transform.SetParent(transform, false);
                }
        }}


我们的管道预制体和对象分配给系统。我让它现在创建四个管道。

A pipe system.


管道需要以某种方式相互对齐。第一个领导所有的管道,其他的都应该与前一个对齐。
                pipes = new Pipe[pipeCount];
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes = Instantiate<Pipe>(pipePrefab);
                        pipe.transform.SetParent(transform, false);
                        if (i > 0) {
                                pipe.AlignWith(pipes[i - 1]);
                        }
                }


现在我们需要找出管道如何调整使自己与实例对齐。为此,我们需要前一个管道的曲线弧度。
        private float curveAngle;
        
        private void SetVertices () {
                vertices = new Vector3[pipeSegmentCount * curveSegmentCount * 4];

                float uStep = ringDistance / curveRadius;
                curveAngle = uStep * curveSegmentCount * (360f / (2f * Mathf.PI));
                CreateFirstQuadRing(uStep);
                int iDelta = pipeSegmentCount * 4;
                for (int u = 2, i = iDelta; u <= curveSegmentCount; u++, i += iDelta) {
                        CreateQuadRing(u * uStep, i);
                }
                mesh.vertices = vertices;
        }


然后,我们可以通过相反方向的角度进行旋转。
        public void AlignWith (Pipe pipe) {
                transform.localRotation = Quaternion.Euler(0f, 0f, -pipe.curveAngle);
        }

Rotating pipes.


这个好了,但只有一步。现在,除了第一个管道最终重叠,其他都没问题。不知什么缘故,旋转需要被累积。有一个快速的方法可以完成这个,即暂时把管道变成它前一个的孩子。从该参考帧中设置旋转始终是正确的。然后,我们把它变成整个系统的子节点,同时保持其当前的世界位置。

                transform.SetParent(pipe.transform, false);
                transform.localRotation = Quaternion.Euler(0f, 0f, -pipe.curveAngle);
                transform.SetParent(pipe.transform.parent);

Cumulative rotation.


现在,管道完成整个循环,并再次迅速地以重叠结束。我们希望管道有一个随机的相对旋转值,所以它们可以想要旋转多少就可以旋转多少。我们可以做一些配置。


首先,我们要确保管道是以它父节点为起点。然后像以前一样和父节点对齐。下一步,我们可以移动了,以致于原点位于父节点的管道终点。在这里,我们可以执行随机的相对旋转。之后,我们向下移动,向下是从我们自己的视角来看的,对齐管道的结束和开始。

        public void AlignWith (Pipe pipe) {
                float relativeRotation = Random.Range(0f, 360f);
               
                transform.SetParent(pipe.transform, false);
                transform.localPosition = Vector3.zero;
                transform.localRotation = Quaternion.Euler(0f, 0f, -pipe.curveAngle);
                transform.Translate(0f, pipe.curveRadius, 0f);
                transform.Rotate(relativeRotation, 0f, 0f);
                transform.Translate(0f, -curveRadius, 0f);
                transform.SetParent(pipe.transform.parent);
        }

Relative rotations. 相对旋转


现在,管道是可以自由旋转的,同时仍能相互对齐。然而,网格不能完全连接在一起。我们必须限制相对旋转,以适应管道部分。
                float relativeRotation =
                        Random.Range(0, curveSegmentCount) * 360f / pipeSegmentCount;

Aligned rotations.


最后,允许大量的管道系统,在一定范围内允许随机的曲线半径和段数。

        public float pipeRadius;
        public int pipeSegmentCount;

        public float minCurveRadius, maxCurveRadius;
        public int minCurveSegmentCount, maxCurveSegmentCount;

        private float curveRadius;
        private int curveSegmentCount;
        
        private void Awake () {
                GetComponent<MeshFilter>().mesh = mesh = new Mesh();
                mesh.name = "Pipe";
               
                curveRadius = Random.Range(minCurveRadius, maxCurveRadius);
                curveSegmentCount =
                        Random.Range(minCurveSegmentCount, maxCurveSegmentCount + 1);
               
                SetVertices();
                SetTriangles();
                mesh.RecalculateNormals();
        }


这些范围取决于你要做什么。长而光滑的?短而弯曲的?你还想找到不会很快和本身相交的配置。我保持管道半径为1,让曲线半径保持在四到二十之间,以及管段数在十与四之间。


Parameterizing the randomness.参数的随机性unitypackage
Moving Through the Pipes通过管道移动

我们游戏需要一个角色,它应该有自己的组件和对象。角色通过系统移动,以一定的速度在一定时间内通过管道。
using UnityEngine;
public class Player : MonoBehaviour {

        public PipeSystem pipeSystem;

        public float velocity;

        private Pipe currentPipe;}

Player object.


事实上,让我们保持玩家一直待在原点,从而移动管道系统。你将无法说出这些区别,我们不需要担心离起点太远。无论如何,我们需要在系统的第一个管道开始。
        private void Start () {
                currentPipe = pipeSystem.SetupFirstPipe();
        }


现在PipeSystem已经很容易的提供了第一个管道。为了确保管道在原点处开始,整个系统必须相对于该管的曲线半径向下移动。
        public Pipe SetupFirstPipe () {
                transform.localPosition = new Vector3(0f, -pipes[0].CurveRadius);
                return pipes[0];
        }


当前的管道曲线半径是私有的。让我们保持不应该被改变的标识。转而给管道添加一个属性,这样其他物体可以访问它了。
        public float CurveRadius {
                get {
                        return curveRadius;
                }
        }
First pipe starts at the origin.


回到角色上,随着时间的推移,它移动的距离也会增加。我们想要记录下来,作为角色得分行为的典型方式。
        private float distanceTraveled;

        private void Update () {
                float delta = velocity * Time.deltaTime;
                distanceTraveled += delta;
        }


但是我们不可能直线移动。我们必须将delta应用到旋转中去。接着,我们可以使用它去更新系统的方向。
        private float deltaToRotation;
        private float systemRotation;

        private void Start () {
                currentPipe = pipeSystem.SetupFirstPipe();
                deltaToRotation = 360f / (2f * Mathf.PI * currentPipe.CurveRadius);
        }

        private void Update () {
                float delta = velocity * Time.deltaTime;
                distanceTraveled += delta;
                systemRotation += delta * deltaToRotation;
                pipeSystem.transform.localRotation =
                        Quaternion.Euler(0f, 0f, systemRotation);
        }


系统现在开始旋转。下一步就是去检测当前管道的终点。我们需要曲线角度,这样管道可以得到另外一个属性。
        public float CurveAngle {
                get {
                        return curveAngle;
                }
        }


一旦,角色移动穿过那个角,我们必须将所有额外的旋转转化成距离,接着跳到有delta的下一个管道。
        private void Update () {
                float delta = velocity * Time.deltaTime;
                distanceTraveled += delta;
                systemRotation += delta * deltaToRotation;

                if (systemRotation >= currentPipe.CurveAngle) {
                        delta = (systemRotation - currentPipe.CurveAngle) / deltaToRotation;
                        currentPipe = pipeSystem.SetupNextPipe();
                        deltaToRotation = 360f / (2f * Mathf.PI * currentPipe.CurveRadius);
                        systemRotation = delta * deltaToRotation;
                }

                pipeSystem.transform.localRotation =
                        Quaternion.Euler(0f, 0f, systemRotation);
        }


设置下一个管道意味着PipeSystem必须将管道转换成数组,将下一个管道与起点对齐,并重新定位。
        public Pipe SetupNextPipe () {
                ShiftPipes();
                AlignNextPipeWithOrigin();
                transform.localPosition = new Vector3(0f, -pipes[0].CurveRadius);
                return pipes[0];
        }


向前移动管道很容易。当前第一管道会成为新的最后一个管道。

        private void ShiftPipes () {
                Pipe temp = pipes[0];
                for (int i = 1; i < pipes.Length; i++) {
                        pipes[i - 1] = pipes;
                }
                pipes[pipes.Length - 1] = temp;
        }


可以通过简单地重置位置和旋转来调整新的第一个管道。为了确保所有其他管道能够一起移动,只暂时让他们成为管道的子节点。
        private void AlignNextPipeWithOrigin () {
                Transform transformToAlign = pipes[0].transform;
                for (int i = 1; i < pipes.Length; i++) {
                        pipes.transform.SetParent(transformToAlign);
                }
               
                transformToAlign.localPosition = Vector3.zero;
                transformToAlign.localRotation = Quaternion.identity;
               
                for (int i = 1; i < pipes.Length; i++) {
                        pipes.transform.SetParent(transform);
                }
        }


玩游戏时,游戏现在不断地循环穿过所有管道。但每个管道部分都是使用默认方向。我们可以使用一个世界级的旋转来改善这个不足。创建一个世界对象,并使管道系统成为它的一个孩子。

Adding the world. 添加世界对象


为了完成修复工作,角色需要对这个世界对象进行引用,它可以通过管道系统来获得引用。
        private Transform world;

        private void Start () {
                world = pipeSystem.transform.parent;
                currentPipe = pipeSystem.SetupFirstPipe();
                deltaToRotation = 360f / (2f * Mathf.PI * currentPipe.CurveRadius);
        }


现在,我们需要管道的相对旋转。所以让管道通过一个属性来记录它。
        private float relativeRotation;
        
        public float RelativeRotation {
                get {
                        return relativeRotation;
                }
        }
        
        public void AlignWith (Pipe pipe) {
                relativeRotation =
                        Random.Range(0, curveSegmentCount) * 360f / pipeSegmentCount;
                …
        }


玩家必须保持对世界对象旋转的记录,当进入新的管道时,就更新记录。它需要在Start和Update中都这样做,把代码放到自己的方法里是有意义的。
        private float worldRotation;

        private void Start () {
                world = pipeSystem.transform.parent;
                currentPipe = pipeSystem.SetupFirstPipe();
                SetupCurrentPipe();
        }

        private void Update () {
                …
                if (systemRotation >= currentPipe.CurveAngle) {
                        delta = (systemRotation - currentPipe.CurveAngle) / deltaToRotation;
                        currentPipe = pipeSystem.SetupNextPipe();
                        SetupCurrentPipe();
                        systemRotation = delta * deltaToRotation;
                }
                …
        }

        private void SetupCurrentPipe () {
                deltaToRotation = 360f / (2f * Mathf.PI * currentPipe.CurveRadius);
                worldRotation += currentPipe.RelativeRotation;
                if (worldRotation < 0f) {
                        worldRotation += 360f;
                }
                else if (worldRotation >= 360f) {
                        worldRotation -= 360f;
                }
                world.localRotation = Quaternion.Euler(worldRotation, 0f, 0f);
        }


有了这个方法,我们可以正确地穿过管道!一旦我们到达终点,我们还是循环回到第一个管道。当然,我们不希望这种情况发生,我们要继续前进。实现这一需求需要多少管道呢?

1000 pipes all at once.


从理论上讲,玩家可以永远前进,并能穿过无限量的管道。但我们不需要马上把所有的管道都展示出来。我们只需要那些玩家能看见的管道,这并不是很多,因为他们一直是处于视角之外的。


这意味着,我们可以使用刚刚穿过去的那个管道,并使用它来产生一个新的终点,希望是不可见的。为了完成这个,改变管道,以便于它在公共的Generate方法中与它的网格进行匹配。
        private void Awake () {
                GetComponent<MeshFilter>().mesh = mesh = new Mesh();
                mesh.name = "Pipe";
        }

        public void Generate () {
                curveRadius = Random.Range(minCurveRadius, maxCurveRadius);
                curveSegmentCount =
                        Random.Range(minCurveSegmentCount, maxCurveSegmentCount + 1);
                mesh.Clear();
                SetVertices();
                SetTriangles();
                mesh.RecalculateNormals();
        }


经过这样的变化,当它处于激活状态时,管道系统需要显式的生成管道。
        private void Awake () {
                pipes = new Pipe[pipeCount];
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes = Instantiate<Pipe>(pipePrefab);
                        pipe.transform.SetParent(transform, false);
                        pipe.Generate();
                        if (i > 0) {
                                pipe.AlignWith(pipes[i - 1]);
                        }
                }
        }


并且当到达下一个管道时也可以这样做。
        public Pipe SetupNextPipe () {
                ShiftPipes();
                AlignNextPipeWithOrigin();
                pipes[pipes.Length - 1].Generate();
                pipes[pipes.Length - 1].AlignWith(pipes[pipes.Length - 2]);
                transform.localPosition = new Vector3(0f, -pipes[0].CurveRadius);
                return pipes[0];
        }


它生效了。管道可以永远的走下去了。但让它运行一段时间后,你可能会看到有些东西出错了。

Unstable pipes.


事实证明,所有父节点的改变慢慢地降低了转换的质量。这不是位置和旋转的问题,因为我们不断地设置这些。然而,我们从来没有接触到变换的大小,这使它在一段时间后急剧收缩。幸运的是,每一次调整之后可以重置管道的大小来阻止收缩问题。

        public void AlignWith (Pipe pipe) {
                …
                transform.localScale = Vector3.one;
        }
unitypackage
Racing Through Pipes 比赛穿过管道

我们现在正穿过管道的中心。为了把它变成一个合适的赛车游戏,我们应该沿着管道的表面,所以我们可以操控去跨过障碍物。我们需要一些东西来编辑角色。


玩家对象本身实际上可以停留在原点。我们可以通过添加一个带有轻微垂直偏移的图片头像,并围绕X轴旋转来造成运动的错觉。要完成沿管道赛车的实验,把相机稍微调后,保持在头像上方,并使其成为一个子节点。


头像看起来应该是什么样子的?船?跑步的人或机器人?抽象的东西吗?头像的形状取决于你。我使用了两个简单的粒子系统,一个作为它的形状,一个用于创建轨迹。

Avatar hierarchy. 头像面板

旋转器是角色的直接子节点,被用来控制人物。角色本身是一个容器对象,我将其放置在(0, -0.75, 0)。我将相机的局部位置设置为(-0.75, 0.25, 0),旋转角度设置为(10, 90, 0)。


我配置粒子系统,让它形成一个形状,大致匹配一个半径为0.1单位的球体。轨迹的旋转为(0,270,0),因此,粒子向后喷射。我将它设置到模拟世界空间中去,粒子的轨迹将在角色背后旋转作为其引导。

A glowing avatar.



现在,我们正在查看管道内部,调整管道三角形的方式需要被倒置。

        private void SetTriangles () {
                triangles = new int[pipeSegmentCount * curveSegmentCount * 6];
                for (int t = 0, i = 0; t < triangles.Length; t += 6, i += 4) {
                        triangles[t] = i;
                        triangles[t + 1] = triangles[t + 4] = i + 2;
                        triangles[t + 2] = triangles[t + 3] = i + 1;
                        triangles[t + 5] = i + 3;
                }
                mesh.triangles = triangles;
        }


还删除了方向灯,因为它没有存在的意义。你仍然会看到一个定向的颜色渐变,这是由Unity的默认Skybox导致的。让我们保持现在的,因为它对于查看方向是有用的。

Inverted pipes, without a sun.



当移动穿过系统时,你会看到管道中的缝隙不断出现在我们的视野边缘。它发生的原因是我们立即删除了我们穿过的管道,尽管相机仍能看到他们的一部分。我们需要保留一个管道在我们身后,以防止这种情况发生。我们可以通过PipeSystem来返回第二个管道,而不是第一个。
        public Pipe SetupFirstPipe () {
                transform.localPosition = new Vector3(0f, -pipes[1].CurveRadius);
                return pipes[1];
        }

        public Pipe SetupNextPipe () {
                ShiftPipes();
                AlignNextPipeWithOrigin();
                pipes[pipes.Length - 1].Generate();
                pipes[pipes.Length - 1].AlignWith(pipes[pipes.Length - 2]);
                transform.localPosition = new Vector3(0f, -pipes[1].CurveRadius);
                return pipes[1];
        }


调整下一管道使其与起点对齐,现在应该是基于第二管道。
        private void AlignNextPipeWithOrigin () {
                Transform transformToAlign = pipes[1].transform;
                for (int i = 0; i < pipes.Length; i++) {
                        if (i != 1) {
                                pipes.transform.SetParent(transformToAlign);
                        }
                }
               
                transformToAlign.localPosition = Vector3.zero;
                transformToAlign.localRotation = Quaternion.identity;
               
                for (int i = 0; i < pipes.Length; i++) {
                        if (i != 1) {
                                pipes.transform.SetParent(transform);
                        }
                }
        }


只要我们一开始,我们就应该移动到第二个管道。
        private void Awake () {
                …
                AlignNextPipeWithOrigin();
        }


现在的差距是固定的,让我们继续致力于角色的转向。该玩家需要旋转速度,需要跟踪当前的旋转,需要旋转器的定义并作为它唯一的子节点。
        public float rotationVelocity;
        
        private float worldRotation, avatarRotation;

        private Transform world, rotater;
        
        private void Start () {
                world = pipeSystem.transform.parent;
                rotater = transform.GetChild(0);
                currentPipe = pipeSystem.SetupFirstPipe();
                SetupCurrentPipe();
        }


旋转速度是使用的第二角速度。它应该相当的快。我将它设置为180,这意味着它会在两秒钟以内环绕走完整个管道。

Configuring rotation.


我们需要玩家的输入能映射到实际旋转中,并可以使用Unity的默认水平输入配置。您可以在“Edit / Project Settings / Input.”中找到它。我将重力和灵敏度的设置增加到10,所以它会很快地响应输入。

Configuring the horizontal axis.  配置水平输入轴

更新角色的位置是通过获得轴的输入,分解速度和时间,并更新旋转来完成的。
        private void Update () {
                …
                UpdateAvatarRotation();
        }
        
        private void UpdateAvatarRotation () {
                avatarRotation +=
                        rotationVelocity * Time.deltaTime * Input.GetAxis("Horizontal");
                if (avatarRotation < 0f) {
                        avatarRotation += 360f;
                }
                else if (avatarRotation >= 360f) {
                        avatarRotation -= 360f;
                }
                rotater.localRotation = Quaternion.Euler(avatarRotation, 0f, 0f);
        }

Testing maneuverability.

现在,我们可以操控了,让我们确定管道表面的外观。我希望它看起来简约,同时还展示了管道的形状,增强了运动感。在黑色背景上有白色斑点图案的模式就可以达到这种效果。为此我们需要一个新的材质,可以采用亮Unlit / Texture shader。有一个点的纹理就足够了,因为它将被作为四边形进行重复。

Pipe surface texture and material.


改变管道预制体材质后,一切都是纯黑色。这是因为管道没有给它的网格设置UV坐标。每一个四边形需要在0 - 1UV范围之间覆盖这两个维度。
        private Vector2[] uv;
        
        public void Generate () {
                …
                mesh.Clear();
                SetVertices();
                SetUV();
                SetTriangles();
                mesh.RecalculateNormals();
        }
        
        private void SetUV () {
                uv = new Vector2[vertices.Length];
                for (int i = 0; i < vertices.Length; i+= 4) {
                        uv = Vector2.zero;
                        uv[i + 1] = Vector2.right;
                        uv[i + 2] = Vector2.up;
                        uv[i + 3] = Vector2.one;
                }
                mesh.uv = uv;
        }


创造光滑管道以及增加点数,我将预制体的段数增加为16,并设置其环距离为0.77。这会使点拉长,而不是完美的圆形,从而增强了速度感。

The new pipe surface, and an obvious hole.



当赛车穿过管道一段时间后,你会在终点处看到会有一个明显的洞出现。当在整个系统中使用六个管道,就会很少发生这样的情况,但是当它出现时,真的不和谐。我们可以增加管道数量,但除非我们将它设置的很高,否则洞可能仍然出现。一个更简单的解决方案是设置相机,清除原有的黑色。

Camera with a black background.


这个洞仍然出现,但它现在很难注意到,尤其是玩家也试图避免障碍物。为了更好地掩盖洞,我们可以用黑雾来淡出遥远的管道。这也增加了一个深度感的场景,即使我们不使用任何灯光。我用指数平方为模式的密度为0.1,所以它出现的很快。

Black fog adds a sense of depth. unitypackage
Placing Obstacles
放置障碍物


是时候将物体放入我们的管道内部。我们可以放各种各样的,尽管在本教程中我们只是放置简单不动的障碍物。


物体可以沿着管道的弧度放置,它所附带的旋转器就像头像一样控制他们在表面出现的地方。所以,定位一个物体需要管道,一个弧度角,以及一个环的旋转角度。让我们给它添加一个组件脚本。它应该被附加给有旋转器的根对象上,相对应的有一个3D模型作为其子物体。
using UnityEngine;
public class PipeItem : MonoBehaviour {

        private Transform rotater;

        private void Awake () {
                rotater = transform.GetChild(0);
        }

        public void Position (Pipe pipe, float curveRotation, float ringRotation) {
                transform.SetParent(pipe.transform, false);
                transform.localRotation = Quaternion.Euler(0f, 0f, -curveRotation);
                rotater.localPosition = new Vector3(0f, pipe.CurveRadius);
                rotater.localRotation = Quaternion.Euler(ringRotation, 0f, 0f);
        }}



我们可以使用一个简单的立方体作为一个3D模型。为了让它变得有趣一点,沿着Y轴旋转45度,这样我们将对着角的边缘而不是平坦的面。我给他们附加了白色固体材质,有一点点透明,所以玩家可以看见它后面的障碍物。我喜欢使用用灯光增加形体的深度,所以我会用到Legacy Shaders / Transparent / Diffuse shader。


首先创建一个小的障碍物,使用大小为(0.3, 0.2, 0.3)的立方体。这物体的本地坐标设置为(0, -0.9, 0),这样它就位于管道表面的顶部。接着再创建一个大的障碍物,大小为(0.2, 2.0, 0.2),没有位移,所以坐标为(0, 0, 0)。这给我们了一个中间桥梁,可以连接管道表面的两侧。接着把他们都转换成预制体。

Obstacle hierarchies and material.



我们怎样在管道里添加这些障碍物的实例?这有很多的方法可以完成添加。让我们先看看可以做这些工作的抽象管道生成器组件
using UnityEngine;
public abstract class PipeItemGenerator : MonoBehaviour {

        public abstract void GenerateItems (Pipe pipe);}



这种抽象概念是很有用的,但是我们也至少需要一个具体的方法。例如,创建一个随机的放置器。在管道的每一个环段都生成一个物体,在每一个环上用一个随机的四边形来调整它。它生产的物体也是随机的。
using UnityEngine;
public class RandomPlacer : PipeItemGenerator {

        public PipeItem[] itemPrefabs;

        public override void GenerateItems (Pipe pipe) {
                float angleStep = pipe.CurveAngle / pipe.CurveSegmentCount;
                for (int i = 0; i < pipe.CurveSegmentCount; i++) {
                        PipeItem item = Instantiate<PipeItem>(
                                itemPrefabs[Random.Range(0, itemPrefabs.Length)]);
                        float pipeRotation =
                                (Random.Range(0, pipe.pipeSegmentCount) + 0.5f) *
                                360f / pipe.pipeSegmentCount;
                        item.Position(pipe, i * angleStep, pipeRotation);
                }
        }}
This requires that Pipe exposes its curve segment count.
        public int CurveSegmentCount {
                get {
                        return curveSegmentCount;
                }
        }


现在我们可以使用这个组件来创建一个生成器的预制体。让我们使其中一个总是能放置小的障碍物,还有一个总是放置大的障碍物体,另一个能放相同数量的混合障碍物体,即大、小障碍物的数量一样多。

Random placers for either small, large, or both obstacle types.
我们也可以创建一个物体放置器组件,按着顺时针或逆时针方向将物体放进螺旋中。这与随机放置只有一点的不同。我在下面标记了差异。
using UnityEngine;
public class SpiralPlacer : PipeItemGenerator {

        public PipeItem[] itemPrefabs;

        public override void GenerateItems (Pipe pipe) {
                float start = (Random.Range(0, pipe.pipeSegmentCount) + 0.5f);
                float direction = Random.value < 0.5f ? 1f : -1f;

                float angleStep = pipe.CurveAngle / pipe.CurveSegmentCount;
                for (int i = 0; i < pipe.CurveSegmentCount; i++) {
                        PipeItem item = Instantiate<PipeItem>(
                                itemPrefabs[Random.Range(0, itemPrefabs.Length)]);
                        float pipeRotation =
                                (start + i * direction) * 360f / pipe.pipeSegmentCount;
                        item.Position(pipe, i * angleStep, pipeRotation);
                }
        }
}



我给螺旋式的放置器创建了三个预制体,并设置遇到小障碍和大障碍的概率相等。


为了使用这些生成器,让我们简单地给这些生成器分配一个数组,并在每个随机使用的管道中选取一个。
        public PipeItemGenerator[] generators;
        
        public void Generate () {
                …
                generators[Random.Range(0, generators.Length)].GenerateItems(this);
        }
        


我调整了他们在管道预制体的数组中选择生成器的概率。我不喜欢太多的障碍,在螺旋中简单的就好。

An arbitrary selection of generators.



障碍物在管道内部显示了。但是很难看到它们。为了改善可见度,你可以通过Window / Lighting调节环境灯光设置。将环境源设置为统一的灰色(99,99,99),并确保它处于最大的强度。

Flat gray ambient color.
这提高了很多的可见度,但带来了一个非常平坦的结果。让我们把头像变成一个光源,通过加入短程点光。一个三个单位的范围就足够了。这使得附近的物体看起来更有趣。

Adding a light.

Shedding light on obstacles.



我们添加物体后,我们也没有清理他们。这会导致管道系统里的垃圾堆积。解决这一问题的快速方法是在生成新的物体之前,销毁所有管道的子物体。
        public void Generate () {
                …

                for (int i = 0; i < transform.childCount; i++) {
                        Destroy(transform.GetChild(i).gameObject);
                }
                generators[Random.Range(0, generators.Length)].GenerateItems(this);
        }



现在我们有一个整洁的管道系统,填满了随机的障碍物。但角色头像物体不会承认他们的存在,而且会直接穿过他们。当然,它应该与障碍物体发生碰撞!要做到这个,我们必须在我们头像游戏对象上放更多的功能。



给角色头像物体添加一个球体碰撞器,并将其半径设置为0.1。然后给它添加一个刚体组件,这样物理系统会去检查碰撞。将它标记为kinematic,所以物理系统不会尝试移动它。然后添加一个新Avatar组件,并带有定义好的的形状以及轨迹粒子系统,再加上一个新的碰撞粒子系统。我们也需要一个对玩家的定义,它可以找到它层次结构的根。
using UnityEngine;
public class Avatar : MonoBehaviour {

        public ParticleSystem shape, trail, burst;

        private Player player;
        
        private void Awake () {
                player = transform.root.GetComponent<Player>();
        }}

Avatar configuration.


这碰撞性的粒子系统是在死亡的时候触发,它不会自己触发。

Burst when hit.



我们可以检测角色avatar是否碰撞到障碍物,将cube碰撞器转变为触发碰撞器并给avat添加ontriggerenter方法,在此方法中我们会判断玩家是否死亡。


然而,我们也可以开始一个死亡倒计时来代替立即就死亡,在我们执行一个死亡动画以及进一步传送的过程中,只有告诉玩家,它死了。


因此,当触发了死亡,我们就开始一个倒计时,停止轨迹粒子的发射,并触发一个爆炸性粒子。我们可以使用爆炸粒子的生命期来设置倒计时的持续时间。
        public float deathCountdown = -1f;

        private void OnTriggerEnter (Collider collider) {
                if (deathCountdown < 0f) {
                        shape.enableEmission = false;
                        trail.enableEmission = false;
                        burst.Emit(burst.maxParticles);
                        deathCountdown = burst.startLifetime;
                }
        }




每个update中,avatar必须检测生命倒计时是否被激活,如果是这样就用进度条表示。一旦时间用完了,重置粒子系统的设置并告诉玩家它应该死了。
        private void Update () {
                if (deathCountdown >= 0f) {
                        deathCountdown -= Time.deltaTime;
                        if (deathCountdown <= 0f) {
                                deathCountdown = -1f;
                                shape.enableEmission = true;
                                trail.enableEmission = true;
                                player.Die();
                        }
                }
        }



现在,让玩家的生命设置为不激活状态,这会导致相机被关闭,这很清楚的告诉大家,游戏已经结束了。
        public void Die () {
                gameObject.SetActive(false);
        }

A dying avatar.


现在障碍物体能够导致死亡,这是好的。不幸的是,游戏突然成为致命的,如果玩家在障碍物体的右上方开始,会立即死亡。为了防止这种死亡,我们必须确保的是,第一个管道系统没有任何障碍物体。我们可以通过将管道是否产生任何的障碍变成可选的来避免。
        public void Generate (bool withItems = true) {
                …
                if (withItems) {
                        generators[Random.Range(0, generators.Length)].GenerateItems(this);
                }
        }



        
我们可以接着将开始的管道系统设置为空的管道。我用了两个管道放在系统里,一个在玩家前面,一个在玩家后面。
public int emptyPipeCount;

        private void Awake () {
                pipes = new Pipe[pipeCount];
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes = Instantiate<Pipe>(pipePrefab);
                        pipe.transform.SetParent(transform, false);
                        pipe.Generate(i > emptyPipeCount);
                        if (i > 0) {
                                pipe.AlignWith(pipes[i - 1]);
                        }
                }
                AlignNextPipeWithOrigin();
        }

First, some empty pipes. unitypackage
Creating the User Interface
创建用户界面


Main menu configuration.

现在我们的游戏可以立即开始玩了。我们不需要首先显示一个主菜单来完成这个。通过GameObject / UI / Panel来创建一个新的UI面板,这将添加一个画布面板到场景中,以及一个事件系统对象来处理用户输入。将画布重命名为Main Menu,并将其设置为最高像素。然后删除面板的源图像,并将其颜色设置为黑色

UI现在在场景视图中是可见的,但是它在比3D内容大很多的地方才能显示。将场景转换成2D的,可以让其变得更美观。

Showing the menu.

接下来在主菜单里添加一个按钮。它不需要成为面板的子物体,但是它必须在面板的下方,以便它随后会被绘制。我将它的字体设置为40,将它的文字内容设置为>,使其粗略看起来就像一个播放按钮。

Button configuration.




将按钮放在面板的中心位置,并给它一个合适的尺寸。我设置为160和50.

Main menu with button.



我们需要一个主菜单组件,并带有一个方法来开始游戏。它需要告诉玩家开始玩了,接着将它自己设置为不激活状态,并让菜单消失。


using UnityEngine;
public class MainMenu : MonoBehaviour {

        public Player player;

        public void StartGame () {
                player.StartGame();
                gameObject.SetActive(false);
        }}



现在来修改玩家,以便它准备好,在自己的方法StartGame里抓取一个管道,而不是在start里面。抓取世界,旋转器可以移动到Awake中,在那里它不应该将自己激活,这样它就不会立即开始比赛。你可以接着添加MainMenu组件到UI中,并分配给玩家。
        public void StartGame () {
                distanceTraveled = 0f;
                avatarRotation = 0f;
                systemRotation = 0f;
                worldRotation = 0f;
                currentPipe = pipeSystem.SetupFirstPipe();
                SetupCurrentPipe();
                gameObject.SetActive(true);
        }

        private void Awake () {
                world = pipeSystem.transform.parent;
                rotater = transform.GetChild(0);
                gameObject.SetActive(false);
        }

Connecting menu to player.


我们现在必须配置按钮,所以它可以调用菜单的方法。通过给OnClick()区域添加一个入口来完成这个,选择菜单物体,接着从可选的选项里选择MainMenu.startGame。注意那个方法需要被设为公共的,这样才可以使用。

Hooking up the button.


按下play按钮就可以开始游戏了,当然只有在实际播放的模式中。玩家死后,为了回到菜单,玩家需要对main menu进行定义,所以它能调用EndGame方法。随着它能够将最后的分数显示给而玩家,以及最后通过的距离。

        public MainMenu mainMenu;

        public void Die () {
                mainMenu.EndGame(distanceTraveled);
                gameObject.SetActive(false);
        }


MainMenu需要一个分数标签来显示最后的分数,我们可以通过UnityEngine.UI.Text组件来完成这个。如果玩家分数很高,在显示之前,我们可以将其与距离相乘后除以10,这样我们就可以摆脱小数部分。当然,菜单必须将自己设置为不激活状态。
using UnityEngine;using UnityEngine.UI;
public class MainMenu : MonoBehaviour {

        public Player player;

        public Text scoreLabel;

        public void StartGame () {
                player.StartGame();
                gameObject.SetActive(false);
        }

        public void EndGame (float distanceTraveled) {
                scoreLabel.text = ((int)(distanceTraveled * 10f)).ToString();
                gameObject.SetActive(true);
        }
}

给菜单添加一个文本物体,确保它是白色的,这样他可以在黑色背景下显示,接着将它分配给菜单组件。

Adding a score label and connecting everything. Score!



一旦回到主菜单,游戏必须重新开始,当按钮按下过后。我们必须修改一下PipeSystem来支持这个功能。将管道物体的实例化放在Awake中,但是将它们的Generate函数以及SetupFirstPipe的分配调用都移走。
        private void Awake () {
                pipes = new Pipe[pipeCount];
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes = Instantiate<Pipe>(pipePrefab);
                        pipe.transform.SetParent(transform, false);
                }
        }

        public Pipe SetupFirstPipe () {
                for (int i = 0; i < pipes.Length; i++) {
                        Pipe pipe = pipes;
                        pipe.Generate(i > emptyPipeCount);
                        if (i > 0) {
                                pipe.AlignWith(pipes[i - 1]);
                        }
                }
                AlignNextPipeWithOrigin();
                transform.localPosition = new Vector3(0f, -pipes[1].CurveRadius);
                return pipes[1];
        }
现在我们的游戏可以很好的重新开始了,让我们添加一些更复杂的设置。我们可以这样做,通过将一个固定的速度改变成可以选择的加速度。这意味着玩家将走得越来越快,直到不可避免的撞上障碍物。遇到的困难就是是设置加速度的量,我们会在一个数组中配置。此外,我们希望有一个开始的速度,否则开始的游戏会太慢。调整玩家来提供这个新的功能。
        public float startVelocity;
        
        public float[] accelerations;

        private float acceleration, velocity;

        public void StartGame (int accelerationMode) {
                …
                acceleration = accelerations[accelerationMode];
                velocity = startVelocity;
                …
        }
        
        private void Update () {
                velocity += acceleration * Time.deltaTime;
                …
        }

This means that MainMenu needs to pass along mode, which it got from somewhere. Let's leave that up to the button.

        public void StartGame (int mode) {
                player.StartGame(mode);
                gameObject.SetActive(false);
        }



我将开始的速度设置到3,接着选择3个加速度,0.025, 0.05,以及0.1。尽管看起来不是很懂,但是0.1的加速度已经能让游戏在短时间里结束。


Three different acceleration modes.



播放按钮不再工作。这是因为我们移除了老的StartGame方法,它已经没有参数了,所以按钮不能再找到它。我们必须选择新的方法来代替。此时,它将允许我们输入一个方法的整数参数,默认为零的参数值。

Button with an integer argument.



创建两个重复的按钮,并将它们的方法参数设置为一个和两个。调整按钮的标签,以及让其表示不同的选择。将他们排列在一个整洁的队列里。

Three play choices.



现在,菜单可以使用了,让我们加入游戏中的平视显示器(HUD),玩家可以看到他们快速地移动,以及他们已经走了多远。为此,创建一个新的画布。在其右上角和右下角添加两个文本标签,并适当的设置锚点。这样他们会固定在角落里,无论显示的实际大小是多少。



Adding a HUD.


减少HUD排序,这样它将在主菜单的后面,默认隐藏。

HUD sorting order.


创建一个HUD组件,这样使其设置标签变得方便。要保持与主菜单的一致性,距离和速度应用相同的转换。

using UnityEngine;using UnityEngine.UI;
public class HUD : MonoBehaviour {

        public Text distanceLabel, velocityLabel;

        public void SetValues (float distanceTraveled, float velocity) {
                distanceLabel.text = ((int)(distanceTraveled * 10f)).ToString();
                velocityLabel.text = ((int)(velocity * 10f)).ToString();
        }}


接着给HUD一个玩家的定义,这样它就能保持数值的更新。
        
public HUD hud;
        
        public void StartGame (int accelerationMode) {
                …
                hud.SetValues(distanceTraveled, velocity);
        }
        
        private void Update () {
                …
                hud.SetValues(distanceTraveled, velocity);
        }


HUD and player configuration.

Now you know, how fast and far you go. unitypackage
Supporting Multiple Platforms

现在我们的原型有足够好的形状,让它跑到人们的手中。因此,建立一个平台,并尝试运行它!


你会遇到的一个烦恼是鼠标光标。当游戏正在播放时,它真的应该是隐藏的。幸运的是,这是通过主菜单轻松完成。
        public void StartGame (int mode) {
                …
                Cursor.visible = false;
        }

        public void EndGame (float distanceTraveled) {
                …
                Cursor.visible = true;
        }


构建一个平台是很好的呢,但是你可以上线从而让使用者更多。所以建一个Web播放器。它应该工作的很好,除了全屏分辨率会有一些小毛病。事实证明,默认的独立分辨率设置影响网络播放器的全屏分辨率。这有点奇怪,但有一个解决方案。禁用独立的原生分辨率,将数值设置的很大,然后再次启用原生分辨率。这将确保使用的是最高的分辨率。

Fullscreen resolution fix for web player.
建立的WebGL也能工作了,但记住,这仍然是一个预览功能。编写需要很长的时间,建设规模相当大,甚至压缩版的规模也很大。


也许让人们试试你的游戏的最好的方法是把它放在他们的手上。我们的游戏是手机的耐玩度,所以发布成或iOS版本是一个伟大的想法。这意味着我们应该支持触摸输入。

Unity的事件系统已经为UI提供了触摸支持。我们可以提供转向支持,就是通过简单地检测是左侧还是右侧的屏幕触摸。这个并需要知道设备的方向。由于输入处理比较复杂,让我们把它放在一个单独的Player方法中。

        private void Update () {
                …
                UpdateAvatarRotation();

                hud.SetValues(distanceTraveled, velocity);
        }
                                       
        private void UpdateAvatarRotation () {
                float rotationInput = 0f;
                if (Application.isMobilePlatform) {
                        if (Input.touchCount == 1) {
                                if (Input.GetTouch(0).position.x <Screen.width * 0.5f) {
                                        rotationInput = -1f;
                                }
                                else {
                                        rotationInput = 1f;
                                }
                        }
                }
                else {
                        rotationInput = Input.GetAxis("Horizontal");
                }
                avatarRotation += rotationVelocity * Time.deltaTime * rotationInput;
                …
        }


最后,帧率不是很好。这是因为Unity在移动台上使用默认的每秒30帧。我们可以通过将目标帧速率设置为一个更高的值来消除这一限制。这在任何地方都可以做,但要早做,例如当MainMenu唤醒的时候。




        private void Awake () {
                Application.targetFrameRate = 1000;
        }
Now go out and playtest!
现在可以发布,然后测试。




回复

使用道具 举报

排名
42833
昨日变化
27

0

主题

8

帖子

10

积分

Rank: 1

UID
182916
好友
0
蛮牛币
3
威望
0
注册时间
2016-11-11
在线时间
1 小时
最后登录
2016-11-12
QQ
发表于 2016-11-11 18:31:12 | 显示全部楼层
fasdfasdffads

回复

使用道具 举报

5熟悉之中
720/1000
排名
3237
昨日变化
2

14

主题

171

帖子

720

积分

Rank: 5Rank: 5

UID
109061
好友
0
蛮牛币
176
威望
0
注册时间
2015-6-17
在线时间
248 小时
最后登录
2017-1-19
QQ
发表于 2016-11-15 13:41:36 | 显示全部楼层
{:90:}{:90:}{:90:}{:90:}{:90:}看困了,啦啦啦啦啦啦啦啦阿里

回复 支持 反对

使用道具 举报

排名
11633
昨日变化
5

0

主题

20

帖子

71

积分

Rank: 2Rank: 2

UID
182815
好友
0
蛮牛币
155
威望
0
注册时间
2016-11-11
在线时间
11 小时
最后登录
2017-1-8
发表于 2016-11-16 12:15:25 | 显示全部楼层
不怎么明白呢  {:92:}{:92:}{:92:}

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表