目录
前言
本文基于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、简单游戏数据本地记录(最高分、音量、音乐开关)
游戏制作规划
- 资源商店获取心仪模型
- 制作地图,即游戏区域。
- 制作(获取)蛇头的建模与便于实现游戏功能的身体形式
- 实现蛇头转向与移动
- 实现撞墙停止移动
- 实现目标食物生成
- 实现食物碰撞销毁、得分增加
- 实现道具生成与销毁
- 实现蛇身生成
- 实现蛇身跟随移动
- 实现道具效果
- 制作游戏时UI并显示
- 实现得分实时显示
- 制作游戏结束UI
- 实现场景跳转
- 制作游戏开始界面、设置界面
- 实现背景音乐及开关、音量调节和本地保存
- 实现退出功能
- 游戏过程优化与体验优化
- 打包发布
游戏实现
一、获取模型
先新建3D核心模版项目
购买
在右上角窗口栏下找到资产商店进入。(跳转失败可以自己搜索进入,也可以点击此处)
点进分类后,在右侧进行筛选
这里我们把价格拉到0。(当然也可以付费选择自己喜欢的资源)
笔者找到的一些模型:
购买是需要登录的,这里建议Unity绑定微信号,用微信扫码登录比较方便。
导入
在窗口栏资产商店下的包管理器内,进行资源导入。
选中要导入的资源,在右侧先下载,点击导入,选择要导入的部分,这里建议新手全部导入,以免没选好导致关键资源缺失。
二、制作地图
我们需要一个地面和一个围墙作为游戏区域。
制作地面
首先在样例场景(这里直接另存为主界面)中新建一个平面,
选中之后,在Transform组件处调整大小和位置(也可以直接在图形化界面上拖动更改大小和位置,当然建议用组件,因为比较精准,便于后续控制,平面倒无所谓,足够大就行)。
制作围墙
新建一个立方体,同样在组件处更改参数。效果如图中所示(当然你们是白色的)。
这里改变颜色需要新建材质,改变材质颜色,再拖放到立方体上。
重命名为Wall,拖动到项目栏,新建为预制体,方便后续更改。
拖动Wall预制体到场景栏,实例化三个一样的立方体(可以像我一样直接放在原Wall下,成为子物体),调整立方体的位置,形成如图的围墙。(这里用组件调整参数的优势就体现出来了,很容易对齐)
在Wall预制体界面,我们给它添加刚体组件,并把“是运动学的”打开,以防后续与身体碰撞时墙体移动。
美化
然后我们在导入的动物模型里找到蛇的预制体,拖入场景中,调整大小与位置作为装饰。
因为我们找到的是个有动画的模型,而且看起来比较鬼畜,我们直接把它的动画控制组件给关掉。
接着我们调整光源的位置,制造出适当的阴影以体现3D效果。
游戏视角
我们选中摄像机,调整摄像机的位置和俯仰角度,使游戏区域展现在视角中。
三、蛇头与身体简单建模
原本是打算直接找一个蛇头建模的,但是找不到免费的。所以还是自己简单在Unity里做一个。
蛇头
新建一个球体,调成椭球作为头部(Head)。(上图中橙色边框物体)
后新建一个球体作为子物体(子物体使后续能够统一移动),调整位置和大小,充当眼球。再新建一个球体作为眼球的子物体,充当瞳孔,简单调整。
选中眼球整体,复制粘贴一个,再调整位置就是另一个眼睛了。
可以再搞个嘴,但是没什么必要,游戏视角也看不见。
蛇头这样就差不多了。但是考虑到蛇头与蛇身衔接的问题,为使后续转向操作看起来不那么鬼畜,我们添加一个胶囊体作为衔接(也作为头部的子物体)。
位置与形状如图。
蛇身
为了省事,直接用立方体作为身体。
我们制作两节身体作为初始身体。为了省事,制作好一个后直接复制粘贴调整位置一个就行。分别重命名为body1,body2。
body1拖动到项目栏,生成预制体,改名为Body。
【以上的颜色需要用材质来控制】
身体与下一步无关,可以先不启用。
四、蛇头转向与移动
网上有利用水平轴与垂直轴控制角色移动的教程,但是不太适配我们这个只用调整移动方向的逻辑,经过尝试后选择放弃。我们自己写一个脚本控制移动。
脚本逻辑
我们设定四个移动方向,上下左右,分别用3、1、2、0表示,即右边为0,然后顺时针方向增加。
无任何操作时,蛇头向当前移动方向持续移动;当按下相应按键时,移动方向改变。
功能实现
【因为是本文第一个脚本,所以讲得比较详细】
新建一个脚本,命名为PlayerMove,挂载在Head上,双击进入。
方法寻找
查看官方文档,寻找我们需要的功能:
左侧筛选->脚本->重要的类->Transform->Transform脚本参考
点进去选择使用方式并查看参数,如:
我们当然是围绕竖直方向旋转的,即世界Y轴方向,直接用Vector.up(0,1,0)表示就行。
变量定义
我们缺少一个旋转度数,所以定义一个角度angle。
我们需要记录上一帧的移动状态与按键按下后的移动状态对比,于是定义一个laststate。
Translate方法需要一个三元数作为参数,定义dir。
我们也需要一个速度参数,speed。
至此,本脚本的所有变量定义完毕。
[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度转向。
由于旋转功能代码重复,我们把它写成函数:
此处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”。
接下来新建WallCollide脚本,我们利用触发函数实现状态量改变。
【不用碰撞函数的原因:碰撞双方身份识别比较困难】
碰撞与触发条件
碰撞条件需要两方均有碰撞器,且运动方有刚体组件(这里建议都加上刚体)。
触发条件需要一方在碰撞器内打开“是触发器”。
在这里我们确保Wall预制体加上Box 碰撞器,并调节好Head的碰撞器位置和大小(太大头部会翘起),将Head的刚体组件内 “是运动学” 和碰撞器内的 “是触发器” 都打开。
其他脚本变量的引用
定义格式如图,定义为公共变量,中间是类名,即要引用的脚本名,然后起一个名字。
保存后在Unity界面,把想要关联的带目标脚本的物体拖入组件框内。
触发函数
官方有三个触发函数,分别在进入触发、触发中、触发结束时执行。
我们使用进入触发的函数;
void OnTriggerEnter()
{
pla.laststate = -1;
}
六、目标物生成
初始生成
在游戏刚开始时,需要在固定位置有一个目标物。
原版贪吃蛇的目标物是苹果,不过笔者没找到满意的苹果模型,倒是找到了个不错的西瓜模型,所以我们用西瓜代替。
在项目栏中找到我们导入的西瓜预制体,拖动到场景内,调整大小和旋转角度(西瓜的位置可以适当调整Y轴位置,避免西瓜与地面穿模)。之后把数据对应修改到预制体上。这样我们之后实例化的西瓜就和现在这个西瓜的大小、角度相同了。最后把场景中的西瓜删除。
新建一个西瓜生成脚本WatermelonGenerate,我们选择挂载在平面上。
定义一个游戏物体,watermelon,保存后把西瓜预制体拖入脚本对应位置。
我们通过Instantiate方法实例化预制体,相关用法自行查阅文档。
该方法只需要开始执行一次,所以放在Start函数内。
在这里实例化的位置pos我们直接指定了,所以上文直说调整Y轴位置,实际上我们可以把pos.y改为watermelon.transform.position.y。因为西瓜的Y坐标为0.04f就不会导致穿模,所以笔者并没有在代码中进行修改。下文其他物体的实例化将用到这个方式。
随机生成
在西瓜被“吃”之后,同步生成一个新的西瓜,新西瓜位置随机。
新建一个重新生成脚本ReGenerate,挂载在西瓜预制体上。
同样使用进入触发的函数:
细心的读者已经发现了,这次的触发函数跟上次不一样。因为上次是手打的,这次是自动补全的,这里的才是完整版。参数是碰撞另一方的碰撞器组件。
随机数的生成使用Random类中的Range方法,还是自行查阅。
Instantiate函数的参数中,this指的是该脚本,this.gameObject才是挂载脚本的物体,this在这里等同于挂载脚本的物体。同样第三个参数等同于挂载该脚本的物体的Transform组件的旋转。
转到定义后,我们可以看到,函数内进行了递归与转换,直接传入脚本即为传入挂载脚本的物体。
实际上,我们可以再实例化新西瓜后直接调用Destroy方法销毁旧西瓜,代码为Destroy(this.gameObject),注意这里不能写为Destroy(this),Destroy方法不存在以上的转换,此时销毁的是该组件,模型还留在原地。
因为后续道具的实现不能套用该脚本,为了统一与方便修改,我们把西瓜的销毁与道具的销毁放在一起。
七、目标物销毁
新建一个物体销毁脚本ObjectDestroy,挂载在Head上。
还是利用触发函数进行处理。
上一部分,我们提到触发函数的参数Collider,是碰撞另一方的碰撞器组件。
if (other.tag == "Food")
{
Destroy(other.gameObject);
}
tag是标签,这里碰撞器的标签等同于挂载该碰撞器的物体的标签,但还是建议中间加一个gameObject。西瓜同时只会存在一个,我们可以直接用物体名字判断,然后进行销毁,但为了与后续道具销毁保持一致,我们选择用标签进行判断。
标签设置
标签的修改在物体或预制体的信息左上角:
我们可以自己添加想要的标签。
八、道具生成与销毁
道具设计
笔者设计了三种道具,六种效果:
香蕉:速度减慢
菠萝:长度减短
箱子:未知效果(减速、加速、减短、增长、加分、减分)
道具生成
我们首先要设定道具生成的条件:
我们引入了速度的概念,设定速度随得分自然增加而加快。随着速度加快我们就需要减速道具的出现来减速。所以道具应在一定分数时出现。
同时,生成道具的种类是随机的,我们仍需利用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的值即与道具本身参数保持一致。
这里道具的引用需要用到游戏物体数组:
在Unity界面逐个拖入预制体即可。
道具销毁
销毁逻辑与西瓜相同。因为道具能够同时存在多个,名字会与预制体不同,不能用名字判断,所以使用标签。
不同的的道具物体需要不同的标签。
九、蛇身生成
脚本逻辑
记录蛇尾,每次在蛇尾位置生成身体,并成为新的蛇尾。
功能实现
还是在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拖放关联物体。
但是游戏过程中用代码生成的新身体缺少关联物体。我们需要回过头修改生成函数:
等号左侧是先获取组件,然后访问变量;右侧中间两行是以名字在场景中搜索物体,然后获取组件。注意组件变量的获取不要有遗漏,不然会报错。(GetComponent方法属于GameObject类)
十一、道具效果
减速道具
控制speed的数值即可。需要注意速度下限。
减短道具
我们需要写一个与身体生成相反的函数BodyDestroy,少了组件的动态获取,更简单。
先找到跟随的物体,销毁蛇尾后,更改蛇尾就行。
需要注意的是,我们设置最短的身体长度,不能一直销毁。当跟随目标是body1时,蛇身就只有初始的两节了,不执行销毁。
else if (other.tag == "tool2")
{
Destroy(other.gameObject);
BodyDestroy();
BodyDestroy();
BodyDestroy();
BodyDestroy();
}
函数的执行次数自行确定。
我们考虑到游戏进程较快,执行次数少时效果不明显。
未知效果
结合上述减速、减短效果,利用Random.Range方法,再结合switch语句,实现很简单。
十二、局内UI
UI的显示需要依靠画布,所以需要先新建一个画布Canvas。
UI设计
我们需要操作提示、道具效果提示和得分显示。
双摄像机显示
要想把画布上的UI显示在现在的游戏视角上,我们需要调整画布的渲染模式。
这时我们需要关联一个摄像机。
我们把主摄像机关联上去后,我们会发现在3D空间内,画布在平面之下,完全看不见画布上的内容。
其实我们调整平面距离就可以拉近。
但这样的方式下,我们把3D物体作为道具图标会受到旁边光源的影响。我们换用双摄像机的方法。【原方法距离足够近时也是可行的,诸位自行尝试】
新建一个摄像机,调整到一个不碍事的位置(自行把控),拖入画布组件框。
我们需要保证画布的平面距离在摄像机的可见范围内。
我们把新摄像机的空白处的显示画面改为“仅深度”(主摄像机无所谓),即用深度大的画面代替空白:
把该摄像机的深度设置为“0”,主摄像机深度设置为“1”,画布就显示在了游戏画面之上。
字体导入
UI文本的制作需要一款心仪的字体,往往自带的我们看着不好看,而且没有中文字体,所以我们要自己导入。
先在电脑文件夹内找到字体资源(也可以自己去网上下载想要的),路径如下:
找到Fonts文件夹。里边找一款复制到桌面,然后拖入Unity项目栏。
正规导入
自行查找其他作者的文章。
简易导入
右键,按照下图的路径(旧版文本类似):
选择生成SDF。之后就生成了类似下图的资源:
这时在字体选择栏就能找到该字体了,也可以自行拖入到文本组件的字体栏。
UI制作
所有UI组件都需要是画布的子物体才能显示(游戏物体不需要)。
制作UI时可以直接切换视图到2D,方便操作:
操作提示UI仅需一个纯色背景图加上一个文本子物体。
我们利用UI组件自带的锚点设置,可以快速确定UI大体位置,之后再进行微调。
锚点功能自己多尝试一下就懂了,这里不多讲。
因为这一部分UI集中在左下角,所以我们把它们的锚点统一在左下角。
我们把道具的预制体实例化一个成为画布的子物体,只要道具模型在UI摄像机的视野范围内,我们就在UI上看见了它。
当我们不想让摄像机看到非UI物体时,可以把摄像机的剔除遮挡改为“UI”,同样,我们也可以把物体的图层设置为“UI”。
以下便是最终效果:
十三、得分实时显示
我们的分数变化在ObjectDestroy上,也可以直接在该脚本内实现分数实时显示。
新建一个文本类text1(后续我们会有text2):
注意这里,新文本类要定义为TextMeshProUGUI。
我们保存后把分数显示的文本UI拖入脚本,在分数改变时改变该组件的文本内容:
这里我们文本内容的初始化在Unity界面的文本组件内进行。
十四、游戏结束UI
结束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脚本。
使用activeSelf方法监测自身是否被启用,具体内容自行查阅。
十五、场景跳转
按照之前的逻辑,我们只要在脚本内定义一个场景Scene类,然后进行关联,之后直接加载该场景就行。实际上并不可行。
我们知道,场景中无论三维、二维物体或者UI实际上都是游戏物体,加上了不同的组件而已。但场景并不是游戏物体,无法被关联。当我们定义场景类之后,Unity界面并不会出现场景关联的框。
所以我们需要换一种方式。
我们在官方文档下可以看到,SceneManager类下的LoadScene方法可以通过场景名或者索引的方式进行场景跳转。我们选择索引的方式进行跳转。
场景索引
我们在Unity界面的左上侧的文件栏下找到“生成设置”,把项目栏中的拖入框中,右侧的数值即为索引:
【场景索引值最小的即为游戏开始时进入的界面】
我们新建一个场景StartScene,也拖入其中。【场景直接新建在Scene文件夹内】
功能实现
新建一个脚本SceneJump,挂载到结束UI的两个按钮上。
定义索引变量index,保存后对照生成设置框,输入想跳转场景的索引即可。
【加载当前场景即为重新开始】
转到定义我们可以看到LoadScene的逻辑:关闭所有已加载场景,之后加载新场景。
我们先写一个场景跳转函数;
注意这里要设置为公共函数。
保存后,我们找到相关按钮的组件界面,在Button下添加一个鼠标单击事件,之后把挂载场景跳转脚本的物体(这里即本身)拖入,再在右侧选择我们写的场景跳转脚本,选择场景跳转函数。
十六、开始与设置界面
功能设计
开始界面:开始按钮、设置按钮、退出按钮。
设置界面:背景音乐开关、滑动音量调节、返回按钮。
开始界面
我们需要一张背景图片,笔者没有合适的图片,直接百度了一张。
图片导入
我们将下载好的图片拖入项目栏,选中,检查器内即为图片导入设置。
我们将纹理类型改为精灵Sprite,模式改为单一,右下角点击应用,原图片下就生成了一张精灵图。【模式改为多个可以在下边的编辑器内切割出多个精灵图,诸位自行尝试】
后续我们在开始场景内新建画布。【这个场景纯UI,且没有其他摄像机,画布就不用选择渲染模式了】
之后,新建图像子物体,把该图片选为源图像。再利用锚点设置,图像撑满画布就行了。
按钮设置
和前文一样,我们可以直接用自带的按钮UI,也可以新建物体添加按钮组件。
新版的按钮UI是一个按钮物体带一个文本子物体,我们不如直接新建一个文本添加Button组件。而且在开始界面,我们为了美观,不要按钮的背景图,直接用文本也不用删除图像组件了。
新建一个文本UI,添加Button组件,挂载上SceneJump脚本。
更换字体、调整大小位置等系列操作后,按钮就做完了。
这里需要注意的是,文本UI的本体是下图黄色的框:
在Rect Transform组件内调整参数后变化的也是这个框。这个框就是按钮的实际触发范围。
场景跳转功能实现与前文相同。
最后,我们建一个文本作为游戏标题。
成品如下:
设置界面
Esc返回
返回按键同上。这里设计一个小功能:Esc键返回。
新建一个脚本挂载在文本上,在Update函数内监测Esc键,按下加载场景就行。
这里我们直接把它和返回键上的SceneJump脚本关联一下,这样就不用关心这个键是跳转到哪个场景了。
我们把游戏结束UI的返回键上也加上该功能。
音乐开关与音量
音乐的开关我们选择切换ToggleUI。
Toggle由三部分组成,具体诸位自行查看。我们可以发现Label物体挂载的组件是旧版的文本组件。旧版的清晰度感人,我们直接移除换成新版。
由于资源有限,笔者也不多对图片进行更换,诸位自行调整。
音量滑动调整我们需要滑动条SliderUI。
Slider是没有文本的,我们给它新建一个文本子物体。
十七、音乐开关、音量调节
简单数据储存
PlayerPrefs是一个用于简单数据储存的类。通过键值对的方式储存,只能储存一些简单的字符串、整型、浮点型数据。储存位置在游戏的注册表处,可以找到位置自行改动,不安全,一般用于储存用户偏好。
【深入了解Unity的PlayerPrefs类:一份详细的技术指南(五)】
音乐开关
音乐的动态开关可以由两种方式实现:
一、用数据控制播放器组件是否启用。
二、用空物体预制体挂载播放器组件,用数据控制是否实例化。
笔者采用的是第二种方式。这里其实采用第一种方式更简单。第二种方式可以用于贯穿多个场景的背景音乐,感兴趣自行查阅。
首先,我们在项目栏新建一个预制体Audio,改名后添加Audio Source组件。导入音乐后,拖入组件资源框。此时我们先打开唤醒时播放和循环。
笔者电脑上只有一首歌,就直接导入了,诸位自行更换。
新建一个开关脚本AudioSwitch,挂载到Toggle上。
定义Toggle类变量audiocontroller,关联自身。
这里Awake函数与Start函数效果相同,Awake执行在Start之前,且物体生命周期内只执行一次。
条件语句获取音乐状态,没有该键值的话默认为开。
之后对应改变Toggle组件的变量(下图所示)。
之后写函数在状态改变时进行储存:
更改后及时保存,以防程序异常关闭。
Toggle的触发执行设置与Botton相同。
之后我们进入主界面编写播放器生成逻辑。
直接在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游戏物体变量。
之后,我们转到WallCollide脚本,书写游戏结束后音乐停止的逻辑:
我们使用播放器组件的Stop()方法停止播放。
音量调节
大体书写与开关相同。
新建音量脚本Volume,关联自身,同时把文本也关联进来,我们要实现音量的动态数字显示。
这里因为每次进入文本不同,需要每次进入时初始化,避免错误。(事实上只有音量调为零时会显示出错)
之后写个音量改变函数:
在播放器实例化时设置音量。
历史最高分
现在我们可以回头去设计结束UI的历史最高分功能。
新建一个脚本。
这里使用HasKey方法判断键值是否存在。
我们这里的逻辑是一直更新,把出现过的最高分当做历史最高分,而不是最终得分的最高分。
要记录最终得分的最高分,我们就不用新建这个脚本,直接写在FinalScore里在最终得分出现时判断就行了。
十八、退出游戏
新建一个脚本挂载在退出按钮上。
写一个按钮事件函数:
这里利用条件编译,在Unity调试界面就编译第一项,停止执行,否则编译退出逻辑。
退出逻辑使用Application类下的方法。
十九、体验优化
到此为止,我们的游戏基本就完成了。但是在调试过程中,我们会发现依然存在一些问题影响我们的游戏体验。
首当其冲的是,游戏开始太突然。我们需要添加一个提示游戏开始的功能。
开始提示
设想是进入主界面后,出现一个倒计时,倒计时结束后游戏开始,蛇开始移动。
我们只需初始化移动状态为-1,在倒计时结束后,更改为现脚本的初始状态(0)就实现了。(所以前文的代码中并未进行初始化)
我们发现Plane挂载的WatermelonGenerate脚本中更新函数还是空的,我们正好写在这里。
新建一个文本UI用来显示倒计时。
计时器
我们需要一个计时器来控制文本的更换,并以此控制移动状态的改变。
前文我们提到Time类中的daltatime,每帧加上这个帧时间,计时器就完成了。
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,当然建议诸位采用更具标识性的命名)
我们在各个道具的生效处把文本改成对应的就行。
我们不希望效果提示长时间占据屏幕,也不希望一闪而过看不清,所以我们再设置一个计时器。
与上文不同的是,我们新增一个状态量sta,用来标记效果文本是否启用,并在启用时记录已启用的时间,之后在适当时间时关闭。
timer的重置我们放在道具触发时进行。
这样既能实现timer的重置,又能实现:在效果文本显示时第二个道具被触发,已显示时长重置。
记得在道具触发时把文本启用。
游戏过程优化
在测试过程内,我们观察到,在快速左右摆头进行转向时,Head前端的碰撞器(触发器),有时误触body1的碰撞器,导致游戏直接结束,如图:
实际上视觉上蛇头并未触碰到身体。这导致游戏经常莫名奇妙结束,非常影响游戏体验。
我们将body1上的WallCollide脚本移除或者禁用来实现优化。
其他的优化,如系统卡顿时或许会出现蛇身不连续的问题,我们可以通过减小跟随距离的方法来优化。至于更多的优化,需要诸位自行探索。
缩放模式修改
在游戏界面的上侧,我们可以选择屏幕比例和像素大小。
当我们在当前设置下做好UI,切换到其他比例或像素大小时,UI的位置会发生很大的变动。尤其是我们用模型做的假UI。
我们可以在打包时锁定比例和像素,但是在一些设备上可能会出现黑色边界。
这时我们要找到比例缩放的功能。
我们找到画布下的Canvas Scaler组件,第一个选项就是缩放模式,我们选择屏幕大小缩放,并把参考分辨率改为我们设计时使用的分辨率。【三个界面都改一下】
之后我们重新更换分辨率调试,发现我们用模型做的假UI位置还是会小范围改变。
我们在画布上新建一个面板Panel,把模型UI拖到下面做子物体,利用画布控制面板,再用面板限制模型UI的位置。
我们把它的Image组件中的颜色A改为0,就是图中的透明效果了。
二十、生成设置
游戏制作的最后一步便是打包生成可执行文件。
我们在Unity界面左上角找到文件,点开在靠下位置找到生成设置:
关于生成设置的详细介绍诸位可自行查阅官方文档(23年版),或者查看其他文章,我们这里只简单讲解:
选择目标平台首先我们要先在Unity Hub中安装相应的平台模块。
基本的就是windows,我们直接依托windows讲解。
右侧第二项是针对的处理器架构,对于不同选项,Unity会自动导入不同资源。
第三项简单来说就是选择生成在自己电脑上还是其他电脑上。选远程设备请参考官方文档。
其他默认就行。
之后我们点进左下角的玩家设置Player Settings:
最上边的三个填空处,很容易理解。公司名称不管就行;产品名称就是游戏名,是打开游戏后窗口左上角的名称;版本自己确定。
往下的默认图标即程序图标,找张合适的图片就行。默认光标是窗口内的鼠标样式。
根据自己的需求设置,其中可调整大小的窗口打开之后,窗口的比例可以被拉动改变,一般游戏为了保证UI界面的正常,都不会启用该功能。关闭后我们可以设置默认分辨率。
默认为原生分辨率是使用玩家的设备的分辨率(自适应),各人设备的屏幕比例和像素不同,如果未像上文一样更改缩放模式,不建议启用,可能会导致UI位置错乱。
我们可以自行添加游戏启动界面展示的logo,点击预览,我们就可以再Unity的游戏界面内看到预览效果。诸位可以自行尝试。
其他设置选项诸位自行探索。
设置完成后,我们返回生成设置界面,点击右下角的生成Build。之后选择合适的位置就可以了。
总结
贪吃蛇是个小游戏,虽然我们添加了一些功能,仍有许多游戏功能的实现未能探索到。更进一步的探索就靠诸位自行努力了,或者,期待我们的下集。
觉得本文写得还可以的话可以点赞支持一下哦!