本文中Flappy Bird基于Unity2019.4.7f1完成,源工程已部分代码改为适配安卓
flappy bird:一夜爆红的胖鸟
这是一款简单又困难的手机游戏,游戏中玩家必须控制一只胖乎乎的小鸟,跨越由各种不同长度水管所组成的障碍。上手容易,但是想通关可不简单。Flappy bird 于2013年5月在苹果App Store上线,2014年2月份在100多个国家/地区的榜单一跃登顶,尽管没有精细的动画效果,没有有趣的游戏规则,没有众多的关卡,却突然大火了一把,下载量突破5000万次。
一、工程创建和素材导入
在Assets中创建不同的文件夹,存储声音、图片、脚本、材质素材。
二、添加背景
创建Quad,创建Material,bg,back,pipe,bird,整个场景设置图1所示。其中材质的创建得选择 Unlit Shader(无光照着色器):它是一个不包含光照(但包含雾效)的基本顶点/片元着色器
三、脚本使小鸟飞起来
关键技术: 小鸟飞行动画由三幅图组成,连续播放三幅图可以看出小鸟的翅膀在动。需要修改offset参数实现。 0为第一个图,0.333为第二个图,0.666为第三个图。因此,每隔0.1s播放一个图片。 比如:0.1s播放offset为0,0.2s播放offset为0.333,0.3s播放offset为0.666 所以调用SetTextureOffset函数 this.GetComponent().velocity 意思是给小鸟一个初始速度,方向为x方向。所以前提是给小鸟一个rigidbody组件。
public float timer=0;
public int frameNumber=10;
public int frameCount=0;
void Start()
{
this.GetComponent<Rigidbody>().velocity=new Vector3(1,0,0)
}
void Update(){
timer+=Time.deltaTime;
if(timer>=1.0f/frameNumber){
frameCount++;
timer-=Time.deltaTime;
int frameIndex=frameCount%3;
this.GetComponent<Renderer>().material.SetTextureOffset("_MainTex",Vector2(0.333f*frameIndex,0));
}
}
"_MainTex"是主要的漫反射纹理; "_BumpMap"是法线贴图 "_Cube"是反射cubemap.(立方体贴图)
四、随机生成管道的位置
修改管道的y值 随机值范围 Random.Range(a,b);//也就是说保持x和z不变,y是一个上下变化的值
public class pipe : MonoBehaviour{
void Start(){
RandomGeneratePosition();
}
public void RandomGeneratePosition(){
float pos_y=Random.Range(-0.2f,0.2f);
this.transform.localPosition=new Vector3(this.transform.localPosition.x,pos_y,this.tranform.localPosition.z);
}
}
五、添加小鸟、管道碰撞器
小鸟的碰撞器 锁定x,y方向的旋转,z方向的移动 管道的碰撞器
地面碰撞器
3个背景循环展示
当小鸟走到bg1时,修改bg的位置,放置在bg2前面,依次循环。创建bg为一个预制体,根据这个预制体创建bg1,bg2.修改坐标。创建游戏管理器存放共享的变量、分值、ui等信息。新建一个脚本GameManage,挂在摄像头上。在这个脚本中,定义了一个共享的第一个bg位置,也就是最前面的bg的位置,同时,创建了一个共享单例。 创建一个MoveTrigger触发器。 其中,MoveTrigger属于bg下属子物体,当小鸟走到差不多第2个背景时候,触发MoveTrigger,讲左侧的这个bg移动到右侧的bg2的右侧。设置的触发器的z值,保证小鸟可以触发到。 这里要存储2个背景的位置。也就是说。当小鸟触发到了触发器,将第一个背景移动到右侧的第三个背景的右侧。他们的坐标值在x方向上相差20.
public class MoveTrigger : MonoBehaviour{
public Transform currentBG;
public pipe p1;
public pipe p2;
void OnTriggerEnter(Collider other){
Transform firstBG = GameManage._instance.FirstBG;
currentBG.position=new Vector3(firstBG.position.x+10,currentBG.position.y,curremtBG.position.z);
p1.RamdomGeneratePosition();
p2.RamdomGeneratePosition();
}
}
代码的意思是:
currentBG为左侧的第一个bg的值。如果碰撞到是小鸟,firstBG为右侧的bg2的值,用bg2的x+10(当前的案例中应该是x+20),y和z不变。而后,currentBG赋值为FirstBG的值。
六、小鸟的跳跃
在小鸟的脚本中,鼠标左键按下后,有一个向上的速度。小鸟的重力必须加上。
七、相机的跟随
先试一下,将相机放在bird的子物体下。试一下看看,发现一个结果:bird飞后,相机会跟着,但是如果bird被撞飞后,相机会旋转。
解决如下:
创建一个相机跟随脚本。
手动给出 camera和bird的一个位置坐标差。
八、计分功能:
在每根管道中间添加一个触发器,选择一个pipe1,添加box collider
到pipe2上粘贴collider。 在GameManage中,定义一个变量存储分数的。 在pipe脚本中,添加以下代码:
九、游戏状态控制
在游戏开始时,小鸟没有速度,没有重力。是停止的,在鼠标右键点击后,给小鸟添加上速度和重力,在飞行过程中,若小鸟撞到管子,就掉落,鼠标左键按下去应该没有作用,游戏结束。所以,需要给游戏设定几种游戏状态。 初始情况下,取消小鸟的速度和重力。并在小鸟的飞行和鼠标左键按下的左右是在playing状态下
if(GameManage._instance.GameState==GameMabage.GAMESTATE_PLAYING){
timer+=Time.deltaTime;
if(timer>=1.0f/frameNumber){
frameCount++;
timer-=Time.deltaTime;
int frameIndex=frameCount%3;
this.GetComponent<Renderer>().material.SetTextureOffser("_MainTex",new Vector2(0.333f*frameIndex,0));
}
if(Input.GetMouseButton(0)){
Vector3 vel=this.GetComponent<Rigidbody>().velocity;
this.GetComponent<Rigidbody>().velocity=new Vector3(vel.x,2,vel.z);
}
}
设定函数,让其满足设置小鸟的速度和重力。 在GameMange中,从menu状态转变成Playing状态。在playing状态下,调用上述函数,使用方法是SendMessage。在一个脚本中向另一个脚本发送方法。 此时小鸟可以飞行了,可是,小鸟掉落后,再通过鼠标点击时,还可以点击后飞起。游戏状态没有改变。 于是,我们需要在小鸟撞击掉落后,修改游戏状态,同时,不让小鸟再次飞起。 注意: 不能在pipe中再写OnCollionEnter有关的函数,因为本代码又有触发器,两者是冲突的,要么触发器要么碰撞器。所以,我们在pipeup和pipedown中使用碰撞器。
十、添加声音
在Main Camera上,添加audio sourse 组件,添加声音文件sfx_swooshing,取消Loop 每次飞行都有翅膀煽动的声音,所以在bird上添加audiosourse,添加声音文件sfx_wing。取消play on awake。在bird的脚本中,播放声音。
if(input.GetMouseButton(0)){
audio.Play();
}
得分的声音。在MoveTrigger的时候,才会有声音。于是在Pipe1和pipe2上添加audio sourse声音文件sfx_point.同样在脚本中添加
audio.Play();
撞到柱子的时候,会有声音,于是在pipe_up,pipe_down都添加audio sourse,添加sfx_hit声音。取消play on awake。在pipeupordown脚本中,trigger过程中,audio.Play() 死亡的声音
public class PipeUpOrDown:MonoBehaviour{
public AudioSource hitMusic;
public AudioSource dieMusic;
void OnCollisionEnter(Collision other){
if(other.gameObject.tag=="Player"){
hitMusic.Play();
dieMusic.Play();
GameManager._instance.GameState=GameManager.GAMESTATE_END;
}
}
}
十一、失败后的界面
在GameManage脚本,对菜单显示及更新分数
更新最高分值
同时加上HighScoreText.text = preHighScore + " "; finalScoreText.text = Score + " "; //加上空串可以将int转换成字符串 在结束状态
游戏的退出
不过Application.Quit();的代码在Unity3D运行中是不好使的,此代码是在打包之后运行才能好使!
游戏的重新开始
额外的知识
1.PlayerPrefs
PlayerPrefs简单来说就是unity提供的一种本地存储数据的方式。
目前提供存储,int/float/string三种类型的数据。
存储结构类似于字典,一个key值对应一个value。
分别使用
PlayerPrefs.GetInt(key);
PlayerPrefs.GetFloat(key);
PlayerPrefs.GetString(key); 获取键值对应的数值。
PlayerPrefs.SetInt(key,value);
PlayerPrefs.SetFloat(key,value);
PlayerPrefs.SetString(key,value); 设置键值对应的数值。
PlayerPrefs.DeleteKey(key); 删除键值。
PlayerPrefs.HasKey(key); 判断键值是否存在。
值得注意的是,键值对应的数据的默认值为数据类型的默认值。
比如当我们尝试获取一个未曾使用的Key,其返回值可能是:0,0F 或者 "",而不是null。
但是unity却允许存入默认数值,所以在读取数值的时候,返回默认数值的情况有两种,一种是因为不存在该Key值,另一种是因为你存入了默认值。
PlayerPrefs的储存位置
在Mac OS X上PlayerPrefs存储在~/Library/PlayerPrefs文件夹,名为unity.[company name].[product name].plist,这里company和product名是在Project Setting中设置的,相同的plist用于在编辑器中运行的工程和独立模式 (打开Find,按住Option键,点击“前往 →“资源库”,就可以找到Preferences文件夹。). 在Windows独立模式下,PlayerPrefs被存储在注册表的 HKCU\Software[company name][product name]键下(打开“运行”输入regedit打开注册表),这里company和product名是在Project Setting中设置的. 是持久本地化存储
using UnityEngine;
using System.Colliections;
public class PlayerPerfsExample:MonoBehaviour
{
void Example()
{
PlayerPerfs.SetInt(“keyInt”,10);
PlayerPerfs.SetFloat(“keyFloat”,10.2f);
PlayerPerfs.SetString(“keyString”,”lmzqm”);
PlayerPerfs.GetInt("keyInt",0);
PlayerPerfs.GetFloat("keyFloat",0);
PlayerPerfs.GetString("keyString","0");
PlayerPerfs.DeleteAll();
PlayerPerfs.DeleteKey("keyInt");
bool exist = PlayerPerfs.HasKey("keyInt");
}
3.发布到安卓时需要修改的地方
将脚本中的:
if(Input.GetMouseButton(0))
改为:
if(Input.touchCount==1)
源工程
链接:https://pan.baidu.com/s/1ieh8vbuIfd9SObw7Y86smg 提取码:bird
|