JAVA横屏跑酷游戏,[转]unity实战之跑酷游戏

一、前言

这套教程涵盖了Unity Mesh编程、模拟水算法(water simulations)、方块移动算法(marching-cubes)等等。这是一套比较有深度的教程,可能需要你了解一些Unity和C#相关的知识。

二、效果图

b1cf6ebf2378d8b251ef731e739c7c41.png

三、正文

一、基础篇:生成数据块

预备开始

首先,我们先来创建一个空的项目,命名随意即可。

然后创建在Assets下创建一个文件夹,命名为“Scripts”,并创建三个C#脚本,如下图所示:

adc8a6d91de017bc1351e915bb77f4c7.png

Chunk用于存储方块数据和创建网格,并且对网格进行渲染和碰撞;Block用于存放方块需要的信息;MeshData用于存储网格数据。

Chunk.cs脚本:

76aab29f027f86902206c168830025d6.png

首先,我们要求该脚本必须包含上个组件:MeshFilter、MeshRenderer、MeshCollider。我们的数据块(就是由一堆方块组成的大方块~)需要这三个组件完成网格的创建和碰撞。

然后,我们有三个变量。我们有一个Block类型的三维数组变量 blocks,Block类用于存放方块需要的信息,因此我们这个blocks变量就是用于存放一个数据块的方块的信息。

chunkSize是一个静态变量,它用于表示我们的数据块各个方向的大小(就是长宽高的大小)。

最后我们有一个bool类型的变量,用于标志该数据块是否在每帧结束后更新。

其次有三个函数,分别为GetBlock、UpdateChunk、RenderMesh。GetBlock用于获取对应位置的方块;UpdateChunk用于更新数据块的网格数据,然后将更新的数据提供给RenderMesh去渲染。

MeshData.cs脚本:

dc275d045f77b41048ce2f7b59d95960.png

由于这个脚本只是为了存储数据,因此不必继承自MonoBehaviour。

前三个变量(vertices、triangles、uv)是用于渲染网格用的,后两个用于网格碰撞(colVetices、colTriangles)。

Block.cs脚本:

ae854cb7a8fb818dd6c73e82e2e5fd80.png

同样的,Block脚本也不需要继承自MonoBehaviour,并且它将会是所有方块的基类。主要用于存储方块所需信息。

BlockData函数用于生成该方块的网格信息。

我们来设想一下,假如我们的数据块是有25个方块组成的,那么在相邻的方块之间,有一些面就不必渲染出来,浪费系统资源。因此,接下来让我们进行剔除多余面数的处理。

首先,我们需要一个函数去判断两个方块是否相邻,如果相邻则对各个方向的面进行剔除处理。

在那之前,让我们先定一个表示方向的枚举(Block脚本中):

348c138b0fee9bcff751a7c21acdff29.png

然后让我们为Block脚本添加判断的函数:

e13a4889ff833fce691b0a7f6bbfce37.png

因为Block脚本是所有方块类的基类,所有我们在Block脚本中对IsSolid函数没有进行判断处理,全部返回true。

现在让我们开始写一些我们的BlockData函数,在这个函数中,我们会根据当前方块对应方向上相邻方块的面进行剔除处理。

6b2f1466de76931a3df9deebe3ca2236.png

上图中的注释也说得很清楚了。举个栗子:判断当前方块顶上相邻的方块是否有底面,如果有则当前方块就不制作顶面,如下图分析所示:

1a307181fec8b067643bef27e08d0523.png

接下来就是添加需要绘制对应的面的函数了。

上:

745931f0357f535105c4ad32f76fddc3.png

上面的注释也说得很清楚了,但是克森还是给你们秀一秀我的美术功底。

0f204423f45a1ca967e335881ae7023d.png

其它的面就不细讲,代码如下:

下:

086947d0bcb71659d098b5ae86260c7f.png

东:

6c1c2f2189212578a3bc8c2b620a917a.png

北:

e8281b495305cf4e7b11abf0f1d34197.png

南:

86fede8185be2f9af382a3f358e1c976.png

西:

61ca43ebe172df82adfb0fe79049da75.png

添加点之后,我们还要把这些点组合成三角形,因此在函数的最后调用了MeshData里的AddQuadTriangles(),由名字可知道,该函数用于添加面片,因此我们要用这四个顶点组合成一个面片,让我们回到MeshData中添加该函数:

d25053e9cb4c06b797c55b4b48fae2d8.png

再给大家上一次克森的美术作品,相信大家都能理解了吧。

e6a5d764f6fe99923ea9220d9507c2fc.png

接下来,让我们创建一个新的脚本,命名为“BlockAir”,让它继承值Block类,如下所示:

9d3f4b197d83efd1c796f4c68a0278af.png

Okey,现在让我们回到Chunk脚本中,开始添加方块进行测试咯。添加如下代码:

fe307df691c560e938250c70dcfec848.png

首先声明两个变量,一个为MeshFilter(网格过滤器)类型,另一个为MeshCollider(网格碰撞器)类型。分别用于存储和设置我们对应数据库上的组件属性。

首先通过GetComponent方法获取对应物体上的对应的组件,然后初始化了我们的数据块(blocks),我们的数据块是一个161616大小的正方体,里面由一堆小方块组成。当前数据库的小方块类型为 BlockAir。

然后修改了该数据库blocks[3, 5, 2]的方块数据,修改为Block类型方块。

最后调用UpdateChunk函数进行数据的更新。

好的,接下来让我们完善我们的UpdateChunk函数:

首先声明一个类型为MeshData的变量。然后循环遍历blocks进行数据的更新(就是调用每一个方块的BlockData函数,而BlockData函数则是用来处理方块的网格数据,例如剔除面等等)。

最后就是调用RenderMesh函数将更新好的网格数据传入,然后进行网格的渲染。

那么,接下来让我们完成我们的RenderMesh函数:

b0d499f8ec4c9baf3ead219fad3dd0ca.png

这个函数很简单,就是先调用Clear函数清除上一次网格的数据,然后重新设置即可。

这一篇只是简单的介绍怎么生成数据块,还没涉及到贴图和碰撞,所以在RenderMesh函数里只是更新了网格数据。下一篇则教大家如何添加贴图到数据块上。

说那么多,先看看效果。

首先创建一个空物体,然后为该物体添加Chunk脚本。你将会发现如下效果:

19ef42925431921d4c342901805b124a.png

f74f325209553d1e75cb5e3d7112580b.png

为什么方块会跑那去了,为什么会是紫色的呢?因为该方块没有材质,所以是紫色的。因为我们设置了该方块的位置为(3,5,2),那我们是在哪里设置的呢,其实是在这个地方设置了,如下图所示:

681233afa6e045189e1aa9cec4a3df83.png

这个时候你可以创建一个Cube物体去比对一下就知道了,下图演示:

15cc95705a7a18221b0fbb849b1d060a.png

看来是这样的没错。好,接下来克森带大家来走一走这个运行时候的步骤:

1.首先我们先进行实例化数据块,也就是Chunk类里面的blocks变量。

614e3ba83b2bfcf42b2b9a1855f48112.png

2.调用UpdateChunk更新网格数据,在UpdateChunk中又调用了各个方块的BlockData函数生成网格数据。

8ab5521b2a040f095273f8a424db9f9b.png

3.在BlockData中我们对当前方块根据检测相邻方块进行剔除面操作。

fac2c4bb149b9cb98921bf7c668ed203.png

(由于函数太大,所以只截一小部分)

4.最后调用RenderMesh对网格数据进行更新。

f23f1a147190adaeaae8632df95ad6ed.png

在这里,为什么我们只生成了一个方块呢。因为要想生成方块,就必须调用BlockData函数,而BlockAir的BlockData函数里我们只做了一个返回,并没有生成网格数据,

873c43de99257908f400068a37d63176.png

因此只生成了一个方块,也就是我们在后面修改的那个位置为(3,5,2)的方块,因为在Block类里BlockData函数已经生成了网格数据。

dd655976c54b105586390ac3449d2e0a.png

为什么克森不直接将所有的方块实例化为Block类呢,原因是这样做会造成数组下标越界。大家还记得下面这个函数吗?

e69f92319a42647fbcb58e9e5cc34909.png

假设当前方块的y为16,这y+1便会越界。对于这个处理,后续的文章中会有介绍。敬请关注吧。

忘记说最后一点了,对于为什么会生成一个方块呢。原因就是在下图判断中,如果返回为false则制作该方块对应的面,然后我们的BlockAir的IsSolid函数返回的就是false,因此我们的方块就出来了。

f2f7d0d80f68e9b53b70733f2b89fb9b.png

二、基础篇:生成贴图

上一次我们制作了一些函数去设置网格数据,在这篇文章中,我将教大家怎么为方块贴图。

贴图嘛,首先你得有图才行呀。因此请你将下面这张图右键保存到你的工程目录中:

cf7de56848d86941e104d869cc6d89cd.png

打开该图片的导入设置,设置图片类型(Texture Type)为 Advanced,然后将所有的勾都去掉,将过滤模式(Filter Mode)设置为 Point(no filter)。最后的设置如下所示:

93944692bd3b513955f5ccc5407216e0.png

好,让我们来看看为什么要这么设置呢?大家可从下面几张图中看出效果:

d892f608e14cb726684910b51ca5c201.png

(point模式)

8ca4772da4a57940469c65d6b6048024.png

(Bilinear模式)

为什么会这样呢?总之请大伙们记住的就是,如果做像素游戏那么就选择Point模式就对了。而Bilinear模式会对纹理进行插值计算,会先找出最接近像素的四个图素进行插值运算,这会使得纹理更为平滑;而Point模式只是对纹理进行简单的插值,会使用包含像素最多的部分的图素来贴图,容易出现所谓的“马赛克”想象。而“马赛克”想象正是我们想要的效果。

接下来拖拽该图片到上一篇创建好的Chunk物体上,这时你会看到工程目录中多出了一个Metarials文件夹,这是由Unity根据拖拽的纹理自动生成的,这边省去了手动创建纹理的时间。

接下来打开我们的Block脚本,我们需要使用一个结构体去保存纹理信息,以便对纹理坐标进行修改:

3b8706e0f130c136521debc86d58b0ad.png

接下来我们创建一个函数,该函数用于根据指定方面的面对纹理进行修改,简单的说就是给指定方向上的面贴指定的纹理:

dbf1f7d135c04e2529350491af0bc7ad.png

该函数很简单,就是返回一个修改好的Tile类型的结构体,也就是纹理上的坐标位置。

接下来我们需要一个float类型的产量,用于表示纹理位置的比例:

bb3ba5886508732f411c4b8d0bf43f1e.png

为什么会是0.25f呢,下图有解释:

925356c34bdad7c0b5008d69132f15d1.png

如果还不理解,没关系,我们继续往下走。

下面的函数用于生成对应方向面的UV位置:

0e3b0ed79942452f85e5ac53cda976d8.png

好,暂时不解释,接下来再在faceData*(*代表各个方向上的函数)函数中添加如下代码:

4cdcae67f50f76ec74e1160db377d724.png

最后在Chunk.cs脚本的RenderMesh函数中添加两行代码来渲染我们的UV贴图即可:

b8bcf106589e927f140b38f5b8cbaff1.png

先不做解释,先测试一番看看效果:

479c1a091e043005fd2c5fb43902997f.png

一个石块便完美的展示出来了。

好,接下来让我们来分析分析该石块制作的过程:

首先,用于生成UV位置的主要函数是如下函数:

4c9924699c02b51fa625c69a4c2f320f.png

然后该函数调用了TexturePosition函数来生成UV的 x 和 y 的位置:

41a9c7c91852f2cadbfcf682713a64e6.png

在该函数中将 x 和 y 都设置为0。然后让我们回到FaceUVs函数中来,计算计算最终生成UV的位置:

b6a2a927df9d5c9f1f34b8f052aaed5b.png

对应我们纹理中的位置如下图所示:

db32732eeb3ee91903656ec884915c4c.png

由于我们在各个方向的面都使用了同一个纹理坐标,因此该方块的每个都面都是上图中的纹理,接下来让我们生成文章前头看到的那个草块。

让我们新建一个C#脚本,命名为“BlockGrass.cs”,双击打开脚本,为其添加如下代码:

bcfc8b22888ef7e2c278867c96d358ec.png

其实这个脚本很简单,我们只是在TexturePosition函数中对某个方面上的面做了些特殊操作,仅此而已。

然后在初始化创建方块的地方为其添加如下代码:

780d0997881e6a99a7415cb569fad42f.png

这时候Play游戏,你将会看到如下图所示效果:

b1cf6ebf2378d8b251ef731e739c7c41.png

至于为什么会生成,前面已经有做过解释,这里就不再赘述。

三、基础篇:生成网格碰撞

上一次我们为方块添加了贴图,这一章我将为方块添加网格碰撞。

从下图可知,我们的方块已经有网格数据了,然而我们的Mesh Collider的Mesh属性还没有任何网格数据。

81b41b7ae1d3e017166de5a64758b1fd.png

接下来,让我们来为方块添加网格碰撞。

提示

更新网格碰撞数据是很耗性能的,因此我们应该把网格碰撞设计得越简洁越好。如果你不打算使用Unity自带的物理系统,那么你可以百度一下“AABB”碰撞检测算法,这是最简单的碰撞算法之一了。

因为网格碰撞会随着网格数据的变化而变化,为了方面网格碰撞的数据能与网格数据同步变化,首先在“MeshDta.cs”脚本中添加下面变量:

01d62fa0e77427b8a496c4844170069b.png

当该布尔变量为true的时候,我们在对网格添加面片数据的时候,也会对网格碰撞的面片数据进行更新,如下图所示:

ea9dd34baf3a2fbb36bd9b5a284adaa2.png

请大家注意一下,我们为网格碰撞添加面片的时候使用的是colVertices.Count,而不是vertices.Count。

那么接下来,我们为网格碰撞添加顶点,如下图所示:

e9256efcad3cf6dcd1058e7d332dad7f.png

好了,让我们回到“Block.cs”脚本中,修改一下网格顶点的添加方式:如下图所示:

601b2c0622cc4ac746296ef8c1df4a15.png

提示

一定要将“Block.cs”原来添加顶点的方式换成转换的方式!!!

这样便可在useRenderDataForCol为true的时候,添加网格的顶点数据的同时也添加了网格碰撞的顶点数据,是不是既方便又简单呀。

网格碰撞的顶点数据添加好了,接下来添加网格碰撞的三角形数据的步骤和添加顶点的步骤相似。

首先处理一下添加三角形的函数,如下图所示:

a1c67b1da7804f2c3523df2eb67f9a58.png

提示

目前我们只使用了AddQuadTriangies()函数,上图的函数也许会在后面的开发中用的,之所以提前写上是因为该函数和本篇文章相关联。

接下来回到我们的“Block.cs”脚本的添加下面一行代码就可以跑一跑测试了:

987cf5e173d8d1cd8de4dd78f71c920f.png

将这行代码添上之后,按下ctrl+s,之后回到Unity点击Play按钮,将会看到如下图所示效果:

b2a6145ae2ca0600f9cfcd99d9eccaca.png

OK,这一篇结束

四、基础篇:添加地形管理

上一次我们为方块添加了网格碰撞,这一章将会创建一个地形管理相关的类。

首先创建一个名为“WorldPos.cs”的脚本,双击打开,码入如下代码:

using System.Collections;

public struct WorldPos

{

public int x, y, z;

public WorldPos(int x, int y, int z)

{

this.x = x;

this.y = y;

this.z = z;

}

public override bool Equals(object obj)

{

if (!(obj is WorldPos))

return false;

WorldPos pos = (WorldPos)obj;

if (pos.x != x || pos.y != y || pos.z != z)

{

return false;

}

else

{

return true;

}

}

}

1234567891011121314151617181920212223242526272829

这个脚本里的代码很简单,也就是创建了一个带有三个 int 类型的结构体,然后重写了 Equals 方法,至于为什么这么做,接着往下看便知道了。

接下来创建一个名为“World.cs”的脚本,双击打开脚本,码入如下代码:

sing UnityEngine;

using System.Collections.Generic;

public class World : MonoBehaviour {

public Dictionary chunks = new Dictionary();

public GameObject chunkPrefab;

}

12345678910

咳咳咳,到这里就知道为什么了要创建“WorldPos.cs”脚本,和重写 Equals 方法了吧。

那就是因为我们使用了字典结构来管理我们的 Chunk,而 key 为 WorldPos,我们都知道字典的 key 是唯一的,如果想要得到某个 key 对应的值,那么我们就需要传入相等的 key 才能够得到对应的值,因此我们就需要重写 WorldPos 的 Equals 方法。如果不重写 Equals 方法的话,默认对比两个 WorldPos 则是通过它们各自的 Hash 值来对比的,而每个 new 出来的对象的 Hash 都不相同,所以这就是为什么要重写 Equals 方法的主要原因。

逼逼那么多,也不知道说得对不对,233。

Ok,让我们回到 Unity 编辑器中,然后按步骤执行如下操作:

9f063677e31c29621dfe84e5d09795e0.png

fa33039a42864de673095d5ebb3b27b3.png

Ok,接下来让我们运行 Unity,你会发现什么都没有。

让我们双击打开“Chunk.cs”脚本,修改如下代码:

1ac6a7b5fcf8fbe64cd977b3ecc15917.png

嚯嚯嚯,初始化代码都去掉了,我们要怎么创建 chunk 方块呀?

稍等骂爹,我们不是有一个用于管理 chunk 的类吗?对的,接下来让我们在 “World.cs”脚本里对“Chunk”进行初始化。回到“World.cs”脚本,添加如下代码:

void Start()

{

for (int x = 0; x < 1; x++)

{

for (int y = 0; y < 1; y++)

{

for (int z = 0; z < 1; z++)

{

CreateChunk(x * Chunk.chunkSize, y * Chunk.chunkSize, z * Chunk.chunkSize);

}

}

}

}

123456789101112131415

上图中的代码较为简单,就不细说了,接下来添加“CreateChunk”函数,如下图所示:

public void CreateChunk(int x, int y, int z)

{

WorldPos worldPos = new WorldPos(x, y, z);

GameObject newChunkObject = Instantiate(

chunkPrefab, new Vector3(x, y, z),

Quaternion.Euler(Vector3.zero)

) as GameObject;

Chunk newChunk = newChunkObject.GetComponent();

newChunk.pos = worldPos;

newChunk.world = this;

chunks.Add(worldPos, newChunk);

for (int xi = 0; xi < Chunk.chunkSize; xi++)

{

for (int yi = 0; yi < Chunk.chunkSize; yi++)

{

for (int zi = 0; zi < Chunk.chunkSize; zi++)

{

SetBlock(x + xi, y + yi, z + zi, new BlockGrass());

}

}

}

}

123456789101112131415161718192021222324252627282930313233

上图代码基本都上了注释,因此不再细说。接下来添加“SetBlock”函数,代码如下:

public void SetBlock(int x, int y, int z, Block block)

{

Chunk chunk = GetChunk(x, y, z);

if (chunk != null)

{

chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, block);

chunk.update = true;

}

}

12345678910111213

接下来先添加“GetChunk”函数,代码如下:

public Chunk GetChunk(int x, int y, int z)

{

WorldPos pos = new WorldPos();

float multiple = Chunk.chunkSize;

pos.x = Mathf.FloorToInt(x / multiple) * Chunk.chunkSize;

pos.y = Mathf.FloorToInt(y / multiple) * Chunk.chunkSize;

pos.z = Mathf.FloorToInt(z / multiple) * Chunk.chunkSize;

Chunk chunk = null;

chunks.TryGetValue(pos, out chunk);

return chunk;

}

1234567891011121314151617

接下来让我们回到“Chunk.cs”脚本中,添加如下关联函数,代码如下所示:

public static bool InRange(int index)

{

if (index < 0 || index >= chunkSize)

return false;

return true;

}

public void SetBlock(int x, int y, int z, Block block)

{

if (InRange(x) && InRange(y) && InRange(z))

{

blocks[x, y, z] = block;

}

}

123456789101112131415161718

Ok,码到这里基本功能就完工了,让我们运行 Unity,查看效果,此时会报如下错误:

b3a603c416c0df237a5ac46eaebf4fb5.png

双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“SetBlock”函数,这里为什么会报错,怎么看也没毛病啊。经过克森一系列的猜测,果然如此,组件的一些方法调用顺序出了问题。具体解决方案就是把“Chunk.cs”脚本你的“Start”函数该为“Awake”即可,如下所示:

void Awake()

{

filter = gameObject.GetComponent();

coll = gameObject.GetComponent();

blocks = new Block[chunkSize, chunkSize, chunkSize];

}

123456

好的,继续运行 Unity,居然还是报错了,错误如下:

b1926b7d3ec899257adfba386b7a84a1.png

双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“GetBlock”函数,报错信息为数组下标越界。

嘿,这个错误非常眼熟呀,之前的文章中也报了这个错误。其实就是因为我们在“Block.cs”脚本的“BlockData”函数中进行了如下判断:

a998c3d01ba890ea6e20c73b02623d6a.png

然后我们是在“Chunk.cs”脚本的“UpdateChunk”函数中调用了“BlockData”函数,如下所示:

7e81c25802c095230a92c0fa6d9928e0.png

在这个函数中,我们会传入的最大值为“chunkSize - 1” ,然而在“BlockData”函数中的判断中会进行“* + 1”(*表示 x、y、z 任意一个),最终会传入“GetBlock”函数中的最大参数值为“chunkSize”,因此便造成了数组下标越界。

Ok,逼逼了那么多,我们该怎么处理了,其实很简单,做个简单的判断即可,这个时候我们的“IsRange”函数便派上用场咯,修改的代码如下所示:

public Block GetBlock(int x, int y, int z)

{

if (InRange(x) && InRange(y) && InRange(z))

return blocks[x, y, z];

return new BlockAir();

}

12345678

Ok,这个时候再运行 Unity,便会看到如下图所示:

5908f2729a3354026357fa1189932be5.png

嚯嚯嚯,看来是成功了,倍儿棒。

Ok,让我们杂耍一下我们的成果,修改如下代码:

bade6ca61a73afe2145ab4e817a8d1df.png

05f28be58dfa89d0e52341587a33ceb8.png

运行 Unity,便看到如下图所示:

ebc47f3becd713eea73adb89bd2b9391.png

嚯嚯嚯,是我们想要的效果,不错,可以的,兄Dei。

接下来让我们看一下生成的 Chunk 网格是怎么样的,具体操作步骤如下所示:

2b6c4252c612ef54cb0c6c4d77c5bb0a.png

嚯嚯嚯,不错,也是我们想要的效果。chunk 里的多余的面被过滤掉了,这样便节省了贼多性能,哦耶!

Ok,文章至此基本结束了,最后让我们为“World.cs”脚本添加两个有用的函数,代码如下所示:

public Block GetBlock(int x, int y, int z)

{

Chunk chunk = GetChunk(x, y, z);

if (chunk != null)

{

Block block = chunk.GetBlock(

x - chunk.pos.x,

y - chunk.pos.y,

z - chunk.pos.z);

return block;

}

else

{

return new BlockAir();

}

}

public void DestroyChunk(int x, int y, int z)

{

Chunk chunk = null;

if (chunks.TryGetValue(new WorldPos(x, y, z), out chunk))

{

Object.Destroy(chunk.gameObject);

chunks.Remove(new WorldPos(x, y, z));

}

}

123456789101112131415161718192021222324252627282930

代码逻辑较为简单,因此就不逼逼了,好了,文章至此就结束吧。

标题:[转]unity实战之跑酷游戏

作者:shirln

地址:https://www.mmzsblog.cn/articles/2020/10/21/1603281638638.html

-----------------------------

如未加特殊说明,此网站文章均为原创。

网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。

公众号转载请联系网站首页的微信号申请白名单!

个人微信公众号 ↓↓↓

%E5%BE%AE%E4%BF%A1%E6%90%9C%E4%B8%80%E6%90%9C%E7%88%B1%E4%B8%8A%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91-e1eee390.png


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部