【Unity入门】贪吃蛇3D道具版的实现

目录

前言

游戏压缩包下载: 

Unity包下载:

游戏思路

游戏目标功能

游戏制作规划

游戏实现

一、获取模型

购买

导入

二、制作地图

制作地面

制作围墙

​编辑

美化

游戏视角

三、蛇头与身体简单建模

蛇头

蛇身

​编辑

四、蛇头转向与移动

脚本逻辑

功能实现

方法寻找

变量定义

函数编写

五、停止移动

碰撞与触发条件

其他脚本变量的引用

触发函数

六、目标物生成

初始生成

随机生成

七、目标物销毁

标签设置

八、道具生成与销毁

道具设计

道具生成

道具销毁

九、蛇身生成

脚本逻辑

功能实现

十、蛇身跟随移动

脚本逻辑

功能实现

生成时获取组件变量

十一、道具效果

减速道具

减短道具

未知效果

十二、局内UI

UI设计

双摄像机显示

字体导入

正规导入

简易导入

UI制作

十三、得分实时显示

十四、游戏结束UI

十五、场景跳转

场景索引

功能实现

十六、开始与设置界面

功能设计

开始界面

图片导入

按钮设置

设置界面

Esc返回

音乐开关与音量

十七、音乐开关、音量调节

简单数据储存

音乐开关

音量调节

历史最高分

十八、退出游戏

十九、体验优化

开始提示

计时器

道具效果提示

游戏过程优化

缩放模式修改

二十、生成设置

总结


前言

本文基于Unity项目《贪吃蛇3D道具版》讲解游戏功能的具体实现方式,该项目对Unity基础知识进行了整合,仅适用于入门学习。

建议先在B站观看Unity基础知识的教学视频。

笔者所用Unity版本为 2023.2.20f1c1,语言为中文。

游戏压缩包下载: 

链接:https://pan.baidu.com/s/1tNpHRlx3IgKzFf_EYMOW4g?pwd=d5js

Unity包下载:

链接:https://pan.baidu.com/s/1SQSjxVyG65NtWDZjyo11NQ?pwd=ua4k

部分游戏资源来自网络,仅用于个人练习,不作它用。

游戏思路

与常见的贪吃蛇游戏基本逻辑相同,吃掉目标物增加长度与得分。

与其他不同之处在于添加了道具,因此移除了双击加速的逻辑,增加了吃掉目标物速度增加的逻辑。同时添加了一系列道具及效果,以此增加趣味性。

游戏目标功能

1、基本游戏逻辑(开始、移动、得分、结束)

2、道具效果

3、背景音乐(开关及音量调节)

4、UI界面

5、简单游戏数据本地记录(最高分、音量、音乐开关)

游戏制作规划

  1. 资源商店获取心仪模型
  2. 制作地图,即游戏区域。
  3. 制作(获取)蛇头的建模与便于实现游戏功能的身体形式
  4. 实现蛇头转向与移动 
  5. 实现撞墙停止移动
  6. 实现目标食物生成
  7. 实现食物碰撞销毁、得分增加
  8. 实现道具生成与销毁
  9. 实现蛇身生成
  10. 实现蛇身跟随移动
  11. 实现道具效果
  12. 制作游戏时UI并显示
  13. 实现得分实时显示
  14. 制作游戏结束UI
  15. 实现场景跳转
  16. 制作游戏开始界面、设置界面
  17. 实现背景音乐及开关、音量调节和本地保存
  18. 实现退出功能
  19. 游戏过程优化与体验优化
  20. 打包发布

游戏实现

一、获取模型

先新建3D核心模版项目

购买

在右上角窗口栏下找到资产商店进入。(跳转失败可以自己搜索进入,也可以点击此处

点进分类后,在右侧进行筛选

6486a6cc5d6b4fd08f81c6bedaf3dc56.png

这里我们把价格拉到0。(当然也可以付费选择自己喜欢的资源)

笔者找到的一些模型:

4fc620405c3a46ba8179bb6177067dba.png

95e774aed68a4d9fbb4d3e167b8467b5.png

购买是需要登录的,这里建议Unity绑定微信号,用微信扫码登录比较方便。

导入

在窗口栏资产商店下的包管理器内,进行资源导入。

8befa96864444da3a9780547918191cb.png

选中要导入的资源,在右侧先下载,点击导入,选择要导入的部分,这里建议新手全部导入,以免没选好导致关键资源缺失。

二、制作地图

我们需要一个地面和一个围墙作为游戏区域。

制作地面

首先在样例场景(这里直接另存为主界面)中新建一个平面,

5569cbaaca1b4f4488f584e6eb0351ea.png

选中之后,在Transform组件处调整大小和位置(也可以直接在图形化界面上拖动更改大小和位置,当然建议用组件,因为比较精准,便于后续控制,平面倒无所谓,足够大就行)。

制作围墙

新建一个立方体,同样在组件处更改参数。效果如图中所示(当然你们是白色的)。

280566c80f9047ec9265dc8c69de67e9.png

这里改变颜色需要新建材质,改变材质颜色,再拖放到立方体上。

重命名为Wall,拖动到项目栏,新建为预制体,方便后续更改。

拖动Wall预制体到场景栏,实例化三个一样的立方体(可以像我一样直接放在原Wall下,成为子物体),调整立方体的位置,形成如图的围墙。(这里用组件调整参数的优势就体现出来了,很容易对齐)

010dd0b4986d446d9dd85aedc4f561aa.png

在Wall预制体界面,我们给它添加刚体组件,并把“是运动学的”打开,以防后续与身体碰撞时墙体移动。

8a48361205cd4cf284af04302b1f427d.png

美化

然后我们在导入的动物模型里找到蛇的预制体,拖入场景中,调整大小与位置作为装饰。

因为我们找到的是个有动画的模型,而且看起来比较鬼畜,我们直接把它的动画控制组件给关掉。

44729a6c4a764c1dac1e13f840060ced.png

接着我们调整光源的位置,制造出适当的阴影以体现3D效果。

游戏视角

我们选中摄像机,调整摄像机的位置和俯仰角度,使游戏区域展现在视角中。

9880d6faf5ce4e9eb4c04c269d6d8656.png

三、蛇头与身体简单建模

原本是打算直接找一个蛇头建模的,但是找不到免费的。所以还是自己简单在Unity里做一个。

蛇头

702d82eb9ffe4d9a9fe867364374de0a.png

新建一个球体,调成椭球作为头部(Head)。(上图中橙色边框物体)

后新建一个球体作为子物体(子物体使后续能够统一移动),调整位置和大小,充当眼球。再新建一个球体作为眼球的子物体,充当瞳孔,简单调整。

选中眼球整体,复制粘贴一个,再调整位置就是另一个眼睛了。

5d16d97a69ad43bc94668cedd85ae809.png

可以再搞个嘴,但是没什么必要,游戏视角也看不见。

蛇头这样就差不多了。但是考虑到蛇头与蛇身衔接的问题,为使后续转向操作看起来不那么鬼畜,我们添加一个胶囊体作为衔接(也作为头部的子物体)。

a26eb9c0f6494e0b844f73e365a8710e.png

位置与形状如图。

蛇身

为了省事,直接用立方体作为身体。

697b5ad52b1f49ae91281c8603eb8257.png

我们制作两节身体作为初始身体。为了省事,制作好一个后直接复制粘贴调整位置一个就行。分别重命名为body1,body2。

body1拖动到项目栏,生成预制体,改名为Body。

【以上的颜色需要用材质来控制】

身体与下一步无关,可以先不启用。

e7f8973b6923404380b67062b4e55324.png

四、蛇头转向与移动

网上有利用水平轴与垂直轴控制角色移动的教程,但是不太适配我们这个只用调整移动方向的逻辑,经过尝试后选择放弃。我们自己写一个脚本控制移动。

脚本逻辑

我们设定四个移动方向,上下左右,分别用3、1、2、0表示,即右边为0,然后顺时针方向增加。

无任何操作时,蛇头向当前移动方向持续移动;当按下相应按键时,移动方向改变。

功能实现

【因为是本文第一个脚本,所以讲得比较详细】

新建一个脚本,命名为PlayerMove,挂载在Head上,双击进入。

方法寻找

查看官方文档,寻找我们需要的功能:

左侧筛选->脚本->重要的类->Transform->Transform脚本参考

4205b12ecff74af58802d638fb84faa1.png

f84f259cdfce401b8e96161aa66bf296.png

点进去选择使用方式并查看参数,如:

1b6370171c904848a858b444fee932df.png

我们当然是围绕竖直方向旋转的,即世界Y轴方向,直接用Vector.up(0,1,0)表示就行。

变量定义

我们缺少一个旋转度数,所以定义一个角度angle。

我们需要记录上一帧的移动状态与按键按下后的移动状态对比,于是定义一个laststate。

Translate方法需要一个三元数作为参数,定义dir。

我们也需要一个速度参数,speed。

至此,本脚本的所有变量定义完毕。

a72f8cd9c13b4377ae467afe3dc53d81.png

[HideInInspector] 的作用是使该公共变量不显示在Unity的界面。

此处laststate可以先初始化为 0,后期会对此处进行修改,所以图中没有进行初始化。

以上变量可以先定义为私有变量,后其他脚本需要引用时再行更改(speed可以直接定为公共,方便调试时更改参数)。

【多脚本引用的变量可以定义为静态变量,直接在整个项目中使用,但是变量名不能冲突。本项目采用静态变量可能会更简洁,诸位可以自行尝试】

函数编写

因为按键监测和移动每帧都需要执行,所以写在Update函数中。

用Input.GetKey()方法判断按键。

 if (Input.GetKey("w") && laststate != 1 )
 {
     ActRotate(3);
 }
 else if(Input.GetKey("a") && laststate != 0 )
 {
     ActRotate(2);
 }
 else if(Input.GetKey("d") && laststate != 2 )
 {
     ActRotate(0);
 }
 else if(Input.GetKey("s") && laststate != 3 )
 {
     ActRotate(1);
 }

我们只允许90度转向。

由于旋转功能代码重复,我们把它写成函数:

86560d4f4da94ccb9f2a8f0cdd5ab49a.png

 此处angle的计算方式使结果有正负值,代表顺逆时针。

至此我们实现了转向功能。

移动功能的实现:

switch (laststate)
{
    case 0: dir = Vector3.right * Time.deltaTime * speed; break; 
    case 1: dir = Vector3.back * Time.deltaTime * speed; break;
    case 2: dir = Vector3.left * Time.deltaTime * speed; break;
    case 3: dir = Vector3.forward * Time.deltaTime * speed; break;
}
transform.Translate(dir,Space.World);

Time.deltaTime为这一帧到上一帧的间隔时间,乘帧时间是为了使不同帧率环境下相同时间内移动距离相同,speed用于控制速度。

Translate方法请自行查阅文档。

代码保存后,诸位可以自行调试。

五、停止移动

蛇头的移动是通过方向和速度是通过PlayMove中的dir控制的,所以dir设置为零向量时,蛇头的移动停止。为此我们我们新增一种移动状态(laststate)“-1”。

3e4cd3fa7e954371904be0251ba0099a.png

接下来新建WallCollide脚本,我们利用触发函数实现状态量改变。

【不用碰撞函数的原因:碰撞双方身份识别比较困难】

碰撞与触发条件

碰撞条件需要两方均有碰撞器,且运动方有刚体组件(这里建议都加上刚体)。

触发条件需要一方在碰撞器内打开“是触发器”。

在这里我们确保Wall预制体加上Box 碰撞器,并调节好Head的碰撞器位置和大小(太大头部会翘起),将Head的刚体组件内 “是运动学” 和碰撞器内的 “是触发器” 都打开。

其他脚本变量的引用

6966c6d690f34a479e44ca58915611d5.png

定义格式如图,定义为公共变量,中间是类名,即要引用的脚本名,然后起一个名字。

保存后在Unity界面,把想要关联的带目标脚本的物体拖入组件框内。

96f2d596abcf400e8984a35de2c878a0.png

触发函数

官方有三个触发函数,分别在进入触发、触发中、触发结束时执行。

我们使用进入触发的函数;

164855d8966a4acea42c8e2ad32e8f52.png

void OnTriggerEnter()
{
    pla.laststate = -1;
}

六、目标物生成

初始生成

在游戏刚开始时,需要在固定位置有一个目标物。

原版贪吃蛇的目标物是苹果,不过笔者没找到满意的苹果模型,倒是找到了个不错的西瓜模型,所以我们用西瓜代替。

在项目栏中找到我们导入的西瓜预制体,拖动到场景内,调整大小和旋转角度(西瓜的位置可以适当调整Y轴位置,避免西瓜与地面穿模)。之后把数据对应修改到预制体上。这样我们之后实例化的西瓜就和现在这个西瓜的大小、角度相同了。最后把场景中的西瓜删除。

新建一个西瓜生成脚本WatermelonGenerate,我们选择挂载在平面上。

定义一个游戏物体,watermelon,保存后把西瓜预制体拖入脚本对应位置。

2e6091d7c57d4a25906289dd43f1f018.png

我们通过Instantiate方法实例化预制体,相关用法自行查阅文档。

该方法只需要开始执行一次,所以放在Start函数内。

在这里实例化的位置pos我们直接指定了,所以上文直说调整Y轴位置,实际上我们可以把pos.y改为watermelon.transform.position.y。因为西瓜的Y坐标为0.04f就不会导致穿模,所以笔者并没有在代码中进行修改。下文其他物体的实例化将用到这个方式。

随机生成

在西瓜被“吃”之后,同步生成一个新的西瓜,新西瓜位置随机。

新建一个重新生成脚本ReGenerate,挂载在西瓜预制体上。

同样使用进入触发的函数:

4bac429ea3da42088beb251327808da6.png

细心的读者已经发现了,这次的触发函数跟上次不一样。因为上次是手打的,这次是自动补全的,这里的才是完整版。参数是碰撞另一方的碰撞器组件。

随机数的生成使用Random类中的Range方法,还是自行查阅。

Instantiate函数的参数中,this指的是该脚本,this.gameObject才是挂载脚本的物体,this在这里等同于挂载脚本的物体。同样第三个参数等同于挂载该脚本的物体的Transform组件的旋转。

a3083b010c52412f88fd4afa6f54a427.png

转到定义后,我们可以看到,函数内进行了递归与转换,直接传入脚本即为传入挂载脚本的物体。

实际上,我们可以再实例化新西瓜后直接调用Destroy方法销毁旧西瓜,代码为Destroy(this.gameObject),注意这里不能写为Destroy(this),Destroy方法不存在以上的转换,此时销毁的是该组件,模型还留在原地。

因为后续道具的实现不能套用该脚本,为了统一与方便修改,我们把西瓜的销毁与道具的销毁放在一起。

七、目标物销毁

新建一个物体销毁脚本ObjectDestroy,挂载在Head上。

还是利用触发函数进行处理。

上一部分,我们提到触发函数的参数Collider,是碰撞另一方的碰撞器组件。

if (other.tag == "Food")
{
    Destroy(other.gameObject);
}

tag是标签,这里碰撞器的标签等同于挂载该碰撞器的物体的标签,但还是建议中间加一个gameObject。西瓜同时只会存在一个,我们可以直接用物体名字判断,然后进行销毁,但为了与后续道具销毁保持一致,我们选择用标签进行判断。

标签设置

标签的修改在物体或预制体的信息左上角:

726ff858b95540fd8280e1143672cec4.png

我们可以自己添加想要的标签。

八、道具生成与销毁

道具设计

笔者设计了三种道具,六种效果:

香蕉:速度减慢

菠萝:长度减短

箱子:未知效果(减速、加速、减短、增长、加分、减分)

道具生成

我们首先要设定道具生成的条件:

我们引入了速度的概念,设定速度随得分自然增加而加快。随着速度加快我们就需要减速道具的出现来减速。所以道具应在一定分数时出现。

同时,生成道具的种类是随机的,我们仍需利用Random.Range方法。【需要注意的是,Range方法生成区间左开右闭】

首先,要定义分数。我们把score定义在Plane挂载的WatermelonGenerate脚本内,并在Start函数内进行初始化。【定义在哪里不影响,只要是局内一直不销毁的物体,后续引用对就行】

先把脚本关联一下(因为速度同时变化,PlayerMove脚本也要关联),然后开始写逻辑:

if (other.tag == "Food")
{
    Destroy(other.gameObject);
    wat.score += 1;
    if (wat.score % 2 == 0)
    {
        pla.speed += 1f;
    }
    if (wat.score >= 6 && wat.score % 2 == 0)
    {
        int num = Random.Range(0, 3);
        Vector3 pos = new Vector3(Random.Range(-3.7f, 3.7f), tool[num].transform.position.y, Random.Range(-4.2f, 3.2f));
        Instantiate(tool[num], pos, tool[num].transform.rotation);
    }
}

速度的增加方式和速度以及道具出现的时机,诸位可以凭调试体验自行把控。

道具的位置、大小、角度的调试方法与西瓜一致。此处pos.y的值即与道具本身参数保持一致。

这里道具的引用需要用到游戏物体数组:

ee536b08f0ca484785417c756c61d996.png

在Unity界面逐个拖入预制体即可。

道具销毁

销毁逻辑与西瓜相同。因为道具能够同时存在多个,名字会与预制体不同,不能用名字判断,所以使用标签。

不同的的道具物体需要不同的标签。

fb281c4a15be4c2a9bf58af3c5b9a143.png

九、蛇身生成

脚本逻辑

记录蛇尾,每次在蛇尾位置生成身体,并成为新的蛇尾。

功能实现

还是在WatermelonGererate脚本内定义游戏物体nail,保存后把场景中的body2拖入组件作为蛇尾。

然后在ObjectDestroy定义body,保存并拖入Body预制体把生成逻辑写成函数(直接继承原蛇尾的参数):

void Bodygenerate()
{
    GameObject newnail = Instantiate(body, wat.nail.transform.position, wat.nail.transform.rotation);
    wat.nail = newnail;
}

之后,我们把该函数添加到西瓜销毁的代码后,就实现了吃到西瓜时身体增加。

十、蛇身跟随移动

脚本逻辑

使身体每一节的前方朝向前一节物体,然后在距离前一节身体超出一定范围时,向前移动,就实现了跟随前一节移动的效果。

功能实现

新建跟随移动脚本FollowMove。

定义前一节物体target,两个物体间的距离d,同时关联Head上的PlayerMove脚本。

void Update()
{
    d = (this.transform.position.x - target.transform.position.x) * (this.transform.position.x - target.transform.position.x) + (this.transform.position.z - target.transform.position.z) * (this.transform.position.z - target.transform.position.z);
    if (d >= 0.04f)
    {
        this.transform.LookAt(target.transform);
        this.transform.Translate(Vector3.forward * Time.deltaTime * pla.speed);
    }
}

这里都略去了gameObject。

使用了Transform下的LookAt方法来改变物体前方朝向,自行查阅。

生成时获取组件变量

我们把FollowMove脚本和WallCollide脚本都挂载到Body预制体上。【撞到身体和撞墙效果相同】

手动为body1,body2拖放关联物体。

但是游戏过程中用代码生成的新身体缺少关联物体。我们需要回过头修改生成函数:

997ea7a7235a4c20a1b17bbb252069fd.png

等号左侧是先获取组件,然后访问变量;右侧中间两行是以名字在场景中搜索物体,然后获取组件。注意组件变量的获取不要有遗漏,不然会报错。(GetComponent方法属于GameObject类)

十一、道具效果

减速道具

控制speed的数值即可。需要注意速度下限。

减短道具

我们需要写一个与身体生成相反的函数BodyDestroy,少了组件的动态获取,更简单。

bf2f74329c6348efada1594d21c2cfd7.png

先找到跟随的物体,销毁蛇尾后,更改蛇尾就行。

需要注意的是,我们设置最短的身体长度,不能一直销毁。当跟随目标是body1时,蛇身就只有初始的两节了,不执行销毁。

else if (other.tag == "tool2")
{
    Destroy(other.gameObject);
    BodyDestroy();
    BodyDestroy();
    BodyDestroy();
    BodyDestroy();
}

函数的执行次数自行确定。

我们考虑到游戏进程较快,执行次数少时效果不明显。

未知效果

结合上述减速、减短效果,利用Random.Range方法,再结合switch语句,实现很简单。

2b106a8d759f40f4b7ed90f7cc12716d.png

十二、局内UI

UI的显示需要依靠画布,所以需要先新建一个画布Canvas。

UI设计

我们需要操作提示、道具效果提示和得分显示。

双摄像机显示

要想把画布上的UI显示在现在的游戏视角上,我们需要调整画布的渲染模式。

a58ae4b89b55410f9225122dff4546ae.png

这时我们需要关联一个摄像机。

我们把主摄像机关联上去后,我们会发现在3D空间内,画布在平面之下,完全看不见画布上的内容。

6253a05958f148fc8858536047f751ab.png

其实我们调整平面距离就可以拉近。

7cee41b9b0104b7eb1d62891d8fe5c10.png

但这样的方式下,我们把3D物体作为道具图标会受到旁边光源的影响。我们换用双摄像机的方法。【原方法距离足够近时也是可行的,诸位自行尝试】

新建一个摄像机,调整到一个不碍事的位置(自行把控),拖入画布组件框。

我们需要保证画布的平面距离在摄像机的可见范围内。

d9fc8addad1746c39d2371510c3e4796.png

我们把新摄像机的空白处的显示画面改为“仅深度”(主摄像机无所谓),即用深度大的画面代替空白:

a96125d0e98c496b8505118cdd26af6b.png

把该摄像机的深度设置为“0”,主摄像机深度设置为“1”,画布就显示在了游戏画面之上。

字体导入

UI文本的制作需要一款心仪的字体,往往自带的我们看着不好看,而且没有中文字体,所以我们要自己导入。

先在电脑文件夹内找到字体资源(也可以自己去网上下载想要的),路径如下:

e8a696d206a44107961d1c24cc2b5780.png

找到Fonts文件夹。里边找一款复制到桌面,然后拖入Unity项目栏。

正规导入

自行查找其他作者的文章。

简易导入

右键,按照下图的路径(旧版文本类似):

9227c564793a4652b6fd5e59cd5d6028.png

选择生成SDF。之后就生成了类似下图的资源:

77c7109a1fbc47ea9bdb892cf8b2a323.png

这时在字体选择栏就能找到该字体了,也可以自行拖入到文本组件的字体栏。

UI制作

所有UI组件都需要是画布的子物体才能显示(游戏物体不需要)。

制作UI时可以直接切换视图到2D,方便操作:

0f5b9f391ea04255a5721ab6ebb13814.png

f6dfb24c21544880aa8d515a0f315d93.png

操作提示UI仅需一个纯色背景图加上一个文本子物体。

我们利用UI组件自带的锚点设置,可以快速确定UI大体位置,之后再进行微调。

854df4904aaa4e43ba3adb76dcf46db7.png

锚点功能自己多尝试一下就懂了,这里不多讲。

088319e0d48742f58c5d153471ae2aa5.png

因为这一部分UI集中在左下角,所以我们把它们的锚点统一在左下角。

我们把道具的预制体实例化一个成为画布的子物体,只要道具模型在UI摄像机的视野范围内,我们就在UI上看见了它。

当我们不想让摄像机看到非UI物体时,可以把摄像机的剔除遮挡改为“UI”,同样,我们也可以把物体的图层设置为“UI”。

5f5c640a55704b3ea02020adf383a5db.png

1833e56e19f14b838c0345e5e459c3b5.png

以下便是最终效果:

3f1a331a7721455086081560473a3785.png

十三、得分实时显示

我们的分数变化在ObjectDestroy上,也可以直接在该脚本内实现分数实时显示。

新建一个文本类text1(后续我们会有text2):

2a290f981a0d439397779d11cee0e821.png

注意这里,新文本类要定义为TextMeshProUGUI。

我们保存后把分数显示的文本UI拖入脚本,在分数改变时改变该组件的文本内容:

e7493519ab17432f822b13a2f756cbab.png

这里我们文本内容的初始化在Unity界面的文本组件内进行。

十四、游戏结束UI

4c38aa38fd0c40bb83e44ce959092ab5.png

结束UI大致就长这样。把其中所有可见UI都设为黑色图像的子物体,这样就能一同启用与关闭。

历史最高得分先不用管,先预留位置,后续我们再进行功能实现。

返回与重开键,可以直接用UI中的按钮(Button),也可以直接建一个图像,然后添加Button组件。本质是一样的。需要文本可以再加一个文本组件,也可以再建一个文本子物体。

我们设定在游戏结束时,也就是laststate== -1时,该界面出现。

只需要禁用该物体,在状态改变,即WallCollide脚本执行后启用该物体。

void OnTriggerEnter()
{
    pla.laststate = -1;
    GameObject.Find("Canvas").transform.Find("Gameover").gameObject.SetActive(true);
}

其中,transform.Find()是搜索子物体的方法,Gameover是黑色背景图像的名称。

先通过gameObject转为游戏物体,在调用GameObject中的方法SetActive启用该物体。

之后我们新建一个最终的分脚本FinalScore,挂载在最终得分文本上。

关联自身与WatermelonGenerate脚本。

0eeceb09023649f986df6434c22dad27.png

使用activeSelf方法监测自身是否被启用,具体内容自行查阅。

十五、场景跳转

按照之前的逻辑,我们只要在脚本内定义一个场景Scene类,然后进行关联,之后直接加载该场景就行。实际上并不可行。

我们知道,场景中无论三维、二维物体或者UI实际上都是游戏物体,加上了不同的组件而已。但场景并不是游戏物体,无法被关联。当我们定义场景类之后,Unity界面并不会出现场景关联的框。

所以我们需要换一种方式。

我们在官方文档下可以看到,SceneManager类下的LoadScene方法可以通过场景名或者索引的方式进行场景跳转。我们选择索引的方式进行跳转。

场景索引

我们在Unity界面的左上侧的文件栏下找到“生成设置”,把项目栏中的拖入框中,右侧的数值即为索引:

e0a4edac63974a1795ad6ce362ca0c11.png

【场景索引值最小的即为游戏开始时进入的界面】

我们新建一个场景StartScene,也拖入其中。【场景直接新建在Scene文件夹内】

功能实现

新建一个脚本SceneJump,挂载到结束UI的两个按钮上。

定义索引变量index,保存后对照生成设置框,输入想跳转场景的索引即可。

【加载当前场景即为重新开始】

78f1cb0618de41e9a6f62400e4af4632.png

转到定义我们可以看到LoadScene的逻辑:关闭所有已加载场景,之后加载新场景。

我们先写一个场景跳转函数;

be75a8846480454fbbcc8f3cd39517bc.png

注意这里要设置为公共函数。

保存后,我们找到相关按钮的组件界面,在Button下添加一个鼠标单击事件,之后把挂载场景跳转脚本的物体(这里即本身)拖入,再在右侧选择我们写的场景跳转脚本,选择场景跳转函数。

cac5b9a867f5419e9a7df196d771c4e2.png

31865b7f79584301ae80b1913148eae0.png

十六、开始与设置界面

功能设计

开始界面:开始按钮、设置按钮、退出按钮。

设置界面:背景音乐开关、滑动音量调节、返回按钮。

开始界面

我们需要一张背景图片,笔者没有合适的图片,直接百度了一张。

图片导入

我们将下载好的图片拖入项目栏,选中,检查器内即为图片导入设置。

67309cc1f91b4c249c889035d492db24.png

我们将纹理类型改为精灵Sprite,模式改为单一,右下角点击应用,原图片下就生成了一张精灵图。【模式改为多个可以在下边的编辑器内切割出多个精灵图,诸位自行尝试】

53fed196d41c43d7940753bccb2ff2a1.png

后续我们在开始场景内新建画布。【这个场景纯UI,且没有其他摄像机,画布就不用选择渲染模式了】

之后,新建图像子物体,把该图片选为源图像。再利用锚点设置,图像撑满画布就行了。

1fcf74a688074b46a39ef72fed92aabb.png

按钮设置

和前文一样,我们可以直接用自带的按钮UI,也可以新建物体添加按钮组件。

新版的按钮UI是一个按钮物体带一个文本子物体,我们不如直接新建一个文本添加Button组件。而且在开始界面,我们为了美观,不要按钮的背景图,直接用文本也不用删除图像组件了。

新建一个文本UI,添加Button组件,挂载上SceneJump脚本。

更换字体、调整大小位置等系列操作后,按钮就做完了。

这里需要注意的是,文本UI的本体是下图黄色的框:

6211a0c3dca14e0f9a9e10c61f497b2a.png

在Rect Transform组件内调整参数后变化的也是这个框。这个框就是按钮的实际触发范围。

场景跳转功能实现与前文相同。

最后,我们建一个文本作为游戏标题。

成品如下:

2923cbf6765a4bf2971008f7bb30d272.png

设置界面

Esc返回

返回按键同上。这里设计一个小功能:Esc键返回。

新建一个脚本挂载在文本上,在Update函数内监测Esc键,按下加载场景就行。

cb6af38607144868a1ce985644cd87f3.png

这里我们直接把它和返回键上的SceneJump脚本关联一下,这样就不用关心这个键是跳转到哪个场景了。

我们把游戏结束UI的返回键上也加上该功能。

音乐开关与音量

音乐的开关我们选择切换ToggleUI。

Toggle由三部分组成,具体诸位自行查看。我们可以发现Label物体挂载的组件是旧版的文本组件。旧版的清晰度感人,我们直接移除换成新版。

由于资源有限,笔者也不多对图片进行更换,诸位自行调整。

音量滑动调整我们需要滑动条SliderUI。

Slider是没有文本的,我们给它新建一个文本子物体。

十七、音乐开关、音量调节

简单数据储存

PlayerPrefs是一个用于简单数据储存的类。通过键值对的方式储存,只能储存一些简单的字符串、整型、浮点型数据。储存位置在游戏的注册表处,可以找到位置自行改动,不安全,一般用于储存用户偏好。

深入了解Unity的PlayerPrefs类:一份详细的技术指南(五)

音乐开关

音乐的动态开关可以由两种方式实现:

一、用数据控制播放器组件是否启用。

二、用空物体预制体挂载播放器组件,用数据控制是否实例化。

笔者采用的是第二种方式。这里其实采用第一种方式更简单。第二种方式可以用于贯穿多个场景的背景音乐,感兴趣自行查阅。

首先,我们在项目栏新建一个预制体Audio,改名后添加Audio Source组件。导入音乐后,拖入组件资源框。此时我们先打开唤醒时播放和循环。

笔者电脑上只有一首歌,就直接导入了,诸位自行更换。

新建一个开关脚本AudioSwitch,挂载到Toggle上。

定义Toggle类变量audiocontroller,关联自身。

78ee7592346841499e5664b9bfe210f1.png

这里Awake函数与Start函数效果相同,Awake执行在Start之前,且物体生命周期内只执行一次。

条件语句获取音乐状态,没有该键值的话默认为开。

之后对应改变Toggle组件的变量(下图所示)。

d32191094fd3466b8208b725261babde.png

之后写函数在状态改变时进行储存:

735845d0f7334022b355461dd0cf5c17.png

更改后及时保存,以防程序异常关闭。

Toggle的触发执行设置与Botton相同。

86e86843c08c40af923802c02df590e5.png

之后我们进入主界面编写播放器生成逻辑。

直接在WatermelonGenerate脚本内关联Audio预制体,变量名为Audio。

void Start()
{
    audiostate = 0;
    Vector3 pos = new Vector3(2, 0.04f, 0);
    Instantiate(watermelon, pos, watermelon.transform.rotation);
    if (PlayerPrefs.GetInt("Musicstate",1) == 1)
    {
        audio1 = Instantiate(Audio, Audio.transform.position, Audio.transform.rotation);
        audiostate = 1;
    }
}

为了方便其他脚本的书写和引用,我们定义了audiostate整型变量和audio1游戏物体变量。

fcd5b29d308b41b599eb1603492719a7.png

之后,我们转到WallCollide脚本,书写游戏结束后音乐停止的逻辑:

5624f088ad0348218eea8f1138965a0b.png

我们使用播放器组件的Stop()方法停止播放。

音量调节

大体书写与开关相同。

新建音量脚本Volume,关联自身,同时把文本也关联进来,我们要实现音量的动态数字显示。

dfc8f920e4f24e5e9ba95c4b038a7fa8.png

这里因为每次进入文本不同,需要每次进入时初始化,避免错误。(事实上只有音量调为零时会显示出错)

之后写个音量改变函数:

b04c214d57e44e1b854ed6c417c03463.png

ba678a3878734a888f5044c4e706c466.png

在播放器实例化时设置音量。

历史最高分

现在我们可以回头去设计结束UI的历史最高分功能。

新建一个脚本。

a50ff7b791584f198fbda88d7d3400d4.png

这里使用HasKey方法判断键值是否存在。

我们这里的逻辑是一直更新,把出现过的最高分当做历史最高分,而不是最终得分的最高分。

要记录最终得分的最高分,我们就不用新建这个脚本,直接写在FinalScore里在最终得分出现时判断就行了。

十八、退出游戏

新建一个脚本挂载在退出按钮上。

写一个按钮事件函数:

b1ee01bbbe0743b1a967eb7a54d09800.png

这里利用条件编译,在Unity调试界面就编译第一项,停止执行,否则编译退出逻辑。

退出逻辑使用Application类下的方法。

十九、体验优化

到此为止,我们的游戏基本就完成了。但是在调试过程中,我们会发现依然存在一些问题影响我们的游戏体验。

首当其冲的是,游戏开始太突然。我们需要添加一个提示游戏开始的功能。

开始提示

设想是进入主界面后,出现一个倒计时,倒计时结束后游戏开始,蛇开始移动。

我们只需初始化移动状态为-1,在倒计时结束后,更改为现脚本的初始状态(0)就实现了。(所以前文的代码中并未进行初始化)

我们发现Plane挂载的WatermelonGenerate脚本中更新函数还是空的,我们正好写在这里。

新建一个文本UI用来显示倒计时。

计时器

我们需要一个计时器来控制文本的更换,并以此控制移动状态的改变。

0d4278a67f0b45a58547ee09b19a6fff.png

前文我们提到Time类中的daltatime,每帧加上这个帧时间,计时器就完成了。

3d5787d2299a4c48a4de140b8c82656d.png

if(timer < 1)
{
    text.text = "3";
}
else if (timer >= 1 && timer < 2)
{
    text.text = "2";
}
else if (timer >= 2 && timer < 3)
{
    text.text = "1";
}
else if (timer >= 3 && timer < 4)
{
    text.text = "开始";
}
else
{
    if (text.gameObject.activeSelf)
    {
        text.gameObject.SetActive(false);
        pla.laststate = 0;
        if (audiostate == 1)
        {
            audio1.GetComponent<AudioSource>().Play();
        }
    }
}

避免状态重复执行,我们在最下面的部分添加条件,判断倒计时文本是否被启用。

同时,我们把可能有的音乐在倒计时结束后再打开。

更改后调试,我们发现倒计时蛇头不移动,但是身体发生了移动。我们回到FollowMove脚本添加条件:

void Update()
{
    if (pla.laststate != -1)
    {
        d = (this.transform.position.x - target.transform.position.x) * (this.transform.position.x - target.transform.position.x) + (this.transform.position.z - target.transform.position.z) * (this.transform.position.z - target.transform.position.z);
        if (d >= 0.04f)
        {
            this.transform.LookAt(target.transform);
            this.transform.Translate(Vector3.forward * Time.deltaTime * pla.speed);
        }
    }
}

道具效果提示

我们发现道具的效果除了减速道具外都不够直观,未知效果更是容易被忽略,我们需要添加上相应的效果提示。

新建效果文本,关联到ObjectDestroy脚本上。(这里就是前文所提到的text2,当然建议诸位采用更具标识性的命名)

a8e07575d248435587f008696a6c008d.png

我们在各个道具的生效处把文本改成对应的就行。

我们不希望效果提示长时间占据屏幕,也不希望一闪而过看不清,所以我们再设置一个计时器。

131914279ad946dca23706c31aa3467d.png

与上文不同的是,我们新增一个状态量sta,用来标记效果文本是否启用,并在启用时记录已启用的时间,之后在适当时间时关闭。

timer的重置我们放在道具触发时进行。

bcb2f8062fdb431e813efc163cab0590.png

这样既能实现timer的重置,又能实现:在效果文本显示时第二个道具被触发,已显示时长重置。

记得在道具触发时把文本启用。

游戏过程优化

在测试过程内,我们观察到,在快速左右摆头进行转向时,Head前端的碰撞器(触发器),有时误触body1的碰撞器,导致游戏直接结束,如图:

d9816199c2e745f09a48bb413e9b84ae.png

实际上视觉上蛇头并未触碰到身体。这导致游戏经常莫名奇妙结束,非常影响游戏体验。

我们将body1上的WallCollide脚本移除或者禁用来实现优化。

其他的优化,如系统卡顿时或许会出现蛇身不连续的问题,我们可以通过减小跟随距离的方法来优化。至于更多的优化,需要诸位自行探索。

缩放模式修改

7605496b686c47f8b62f9325d2a0a61e.png

在游戏界面的上侧,我们可以选择屏幕比例和像素大小。

当我们在当前设置下做好UI,切换到其他比例或像素大小时,UI的位置会发生很大的变动。尤其是我们用模型做的假UI。

我们可以在打包时锁定比例和像素,但是在一些设备上可能会出现黑色边界。

这时我们要找到比例缩放的功能。

我们找到画布下的Canvas Scaler组件,第一个选项就是缩放模式,我们选择屏幕大小缩放,并把参考分辨率改为我们设计时使用的分辨率。【三个界面都改一下】

e6a548daa8b545b897d04588677574f7.png

之后我们重新更换分辨率调试,发现我们用模型做的假UI位置还是会小范围改变。

我们在画布上新建一个面板Panel,把模型UI拖到下面做子物体,利用画布控制面板,再用面板限制模型UI的位置。

ebc7f4cee3e8435e931a7984c8528812.png

我们把它的Image组件中的颜色A改为0,就是图中的透明效果了。

二十、生成设置

游戏制作的最后一步便是打包生成可执行文件。

我们在Unity界面左上角找到文件,点开在靠下位置找到生成设置:

关于生成设置的详细介绍诸位可自行查阅官方文档(23年版),或者查看其他文章,我们这里只简单讲解:

0573fe37b0c8415bb400d14d60393ec7.png

选择目标平台首先我们要先在Unity Hub中安装相应的平台模块。

基本的就是windows,我们直接依托windows讲解。

右侧第二项是针对的处理器架构,对于不同选项,Unity会自动导入不同资源。

第三项简单来说就是选择生成在自己电脑上还是其他电脑上。选远程设备请参考官方文档。

其他默认就行。

之后我们点进左下角的玩家设置Player Settings:

64986657e4f34f41b0c1b091248c15f1.png

 最上边的三个填空处,很容易理解。公司名称不管就行;产品名称就是游戏名,是打开游戏后窗口左上角的名称;版本自己确定。

往下的默认图标即程序图标,找张合适的图片就行。默认光标是窗口内的鼠标样式。

33351332f32f460b876d433e6a30ef61.png

根据自己的需求设置,其中可调整大小的窗口打开之后,窗口的比例可以被拉动改变,一般游戏为了保证UI界面的正常,都不会启用该功能。关闭后我们可以设置默认分辨率。

默认为原生分辨率是使用玩家的设备的分辨率(自适应),各人设备的屏幕比例和像素不同,如果未像上文一样更改缩放模式,不建议启用,可能会导致UI位置错乱。

d39d1ffbb6c246db8a5e04fb7479b779.png

我们可以自行添加游戏启动界面展示的logo,点击预览,我们就可以再Unity的游戏界面内看到预览效果。诸位可以自行尝试。

其他设置选项诸位自行探索。

设置完成后,我们返回生成设置界面,点击右下角的生成Build。之后选择合适的位置就可以了。

总结

贪吃蛇是个小游戏,虽然我们添加了一些功能,仍有许多游戏功能的实现未能探索到。更进一步的探索就靠诸位自行努力了,或者,期待我们的下集。

觉得本文写得还可以的话可以点赞支持一下哦!

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值