单元1 搭建场景 本教程将会带大家实现一款经典的小游戏——像素鸟,英文名Flappy Bir
单元1 搭建场景
本教程将会带大家实现一款经典的小游戏——像素鸟,英文名Flappy Bird。这是一款休闲动作游戏,玩家通过点击屏幕控制小鸟的起飞降落,躲开沿途的障碍物。游戏没有胜利,玩家通过创建新的最高分而不断地挑战自我。
任务1.1 导入美术资源包
1. 创建工程
打开Unity的可执行程序(本案例使用的是Unity2017.2.0f3),选择创建2D工程,如图所示。

2. 导入资源包
找到教程提供的资源包,如图所示。直接拖入到Unity的Project视图中。

3. 设置图片格式
如果创建工程选择了2D模式,那么系统会自动将导入的图片设置为Sprite类型,此步骤可忽略不用操作。如果选择了3D模式,需要手动设置。在Project视图中选中所有的图片,如图所示。

在右侧Inspector视图中,将Texture Type设置为Sprite(2D and UI),最后不要忘记单击下方的“Apply”按钮,如图所示。

任务1.2 创建背景
1. 新建场景
按“Ctrl+S”键,或者选择File→Save Scene命令,将场景命名为Game,保存。此时场景里只保留Camera一个游戏对象,如图所示。

2. 调整相机
如果创建工程的时候选择了3D模式,则需要调整相机的设置。选中Camera,在Camera组件里设置Projection为Orthographic,Size设置为1.75。
在Game视图中设置默认分辨率为1440*900,如图所示。

3. 添加大地
在image文件夹里找到back图片,这是我们的地面。直接拖拽到场景中,然后再复制2个,分别命名为back0、back1、back2。如图所示。

将它们的坐标分别设置为(-0.25,0,0)、(4.8,0,0)、(9.85,0,0),此时的场景应该是这样的。

接着,我们需要创建一个空游戏对象,命名为Ground,修改坐标为(0,0,0),然后将3个back都放到Ground下,成为其子物体,如图所示。

这样一来,在实现无尽地图时会比较方便。
4. 添加天空
在image文件夹里找到bg,拖拽到场景中,同样再复制2个,命名为bg0、bg1、bg2。将他们的坐标分别设置为(-0.95,0,0)、(2.07,0,0)、(5.09,0,0)。同样新建一个空物体,命名为Sky,将3个bg对象都放到Sky下,成为其子物体。此时的场景应该是这样的。

天空遮住了地面,不用着急。选中3个bg对象,在Inspector的Sprite Renderer组件里修改Sorting Layer为-2。最终效果如图所示。

任务1.3 添加水管
1. 添加两根水管
从image文件夹中将pipe图片拖拽到场景中,命名为pipeUp,然后复制1个,命名为pipeDown,这就是我们的两个水管。将两个水管的Sprite Renderer中sorting layer修改为-1,让其介于地面和天空之间,如图所示。

将pipeUp的坐标修改为(0,-3,0),pipeDown的坐标修改为(0,3,0),效果如图所示。

2. 复制水管
为了表示这两个水管是一个整体,让PipeUp成为pipedown的子物体,如图所示。

将PipeDown复制出2个,命名为PipeDown1和PipeDown2。最后,设置PipeDown0的坐标为(3,-3,0),PipeDown1的坐标为(6,-3,0),PipeDown2的坐标为(9,-3,0),最后的效果如图所示。

3. 统一管理
同样的,为了方便后面生成无数个管子,我们需要创建一个空游戏对象,命名为GateGroup,修改坐标为(0,0,0),将三个PipeDown放到GateGroup下面,成为其子物体,如图所示。

最后,不要忘记按“Ctrl+S”键保存场景。
单元2 会飞的小鸟
场景已经搭建完成了,接下来该主角登场了。
任务2.1 创建小鸟
1. 添加小鸟游戏对象
将image文件夹里bird拖到场景中,重命名为Bird,可以看到效果如图。

这是因为bird图片的默认格式为Single。
2. 设置bird图片格式
在Project视图选中bird图片,在Inspector视图修改Sprite Mode为Multiple,然后点击Sprite Editor按钮,进入Sprite Editor窗口,对bird图片进行切割,如图所示。

设置三个方框的Position,分别如下图所示。



设置好后,单击Sprite Editor窗口中的“Apply”按钮,应用修改并关闭窗口。
最后,在Inspector视图下方,单击“Apply”按钮,应用对图片格式的修改。此时,Scene场景中的小鸟恢复了正常,如图所示。

任务2.2 起飞降落
1. 添加刚体
在Hierarchy视图中选中Bird,在Inspector视图中单击“Add Component”按钮添加Rigidbody 2D组件。设置Gravity Scale值为0.7,如图所示。

运行游戏,可以看到小鸟受重力影响而降落。
2. 添加SportCtrl.cs
创建SportCtrl.cs脚本,并添加到Bird游戏对象上。打开SportCtrl脚本,添加Rigidbody2D的字段并在Start方法中引用自身,如图所示。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SportCtrl : MonoBehaviour
{
Rigidbody2D rb;
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
// Update is called once per frame
void FixedUpdate()
{
Fly();
}
public void Fly()
{
float xSpeed = 1f;
Vector3 v = rb.velocity;
float ySpeed = v.y;
if (Input.GetMouseButton(0))
{
ySpeed = 2f;
}
rb.velocity = new Vector2(xSpeed, ySpeed);
}
}
再运行游戏试一试,已经可以控制小鸟起飞降落了。
任务2.3 碰撞
现在小鸟已经可以飞了,但是会穿过地面和管子,这一节将会解决这个问题。
1. 添加小鸟的碰撞体
在Bird游戏对象上添加Circle Collider 2D,半径设置为0.18,如图所示。

2. 添加大地的碰撞体
给Ground下的三个子物体back0、back1、back2添加Box Collider 2D组件,并设置成如图所示的配置。

运行游戏,可以看到小鸟已经可以碰到地面了,如图所示。

3. 添加管子的碰撞体
选中所有的PipeDown和PipUp,如图所示。

全部都添加上Box Collider 2D组件即可。
单元3 无尽模式
任务3.1 相机跟随
1. 创建CamCtrl.cs脚本
在Main Camera上添加新脚本CamCtrl.cs,并完成以下代码。
public class CamCtrl : MonoBehaviour {
public Transform target;
Vector3 offset;
// Use this for initialization
void Start () {
offset = transform.position-target.position;
}
// Update is called once per frame
void LateUpdate () {
transform.position = target.position + offset;
}
}
回到编辑器,对Target进行赋值,如图所示。

运行游戏,发现我们的相机已经可以跟着小鸟移动了。
2. 限制相机高度
相机有时会穿帮,需要限制高度。修改脚本的LateUpdate方法如下。
void LateUpdate () {
Vector3 pos= target.position + offset;
if (pos.y > 0.9)
pos.y = 0.9f;
else if (pos.y < -0.9)
pos.y = -0.9f;
transform.position = pos;
}
任务3.2 无限地图
我们的思路是,将有限的图片进行重复使用,从而实现无限地图的效果。当最左侧的图片不可见时,将其移动到最右侧。
1. 创建Endless.cs脚本
添加如下代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Endless : MonoBehaviour {
public float distance;
void OnBecameInvisible(){
transform.Translate(Vector3.right* distance * 3);
}
}
2. 实现无限地面
然后将脚本添加到back0、back1、back2,并在编辑器中设置distance为5.05。如图所示。

运行游戏进行测试,注意测试的时候需要关闭Scene视图。可以看到地面已经可以无限延长了,如图所示。

3. 实现无限天空
同理,给bg0、bg1、bg2添加Endless脚本,并设置distance为3.02。
运行游戏测试,可以看到如图效果。

4. 实现无限水管
同理,给pipeDown0、pipeDown1、pipeDown2添加Endless脚本,并设置distance为3。
运行游戏测试,可以看到水管也有无限个了。
任务3.3 水管随机高度
1. 创建RandomHeight.cs脚本
创建RandomHeight.cs脚本,添加如下代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RandomHeight : MonoBehaviour
{
void OnBecameVisible()
{
Vector3 pos = transform.position;
pos.y = Random.Range(-3f, -1.5f);
transform.position = pos;
}
}
2. 添加脚本
选中所有的pipeDown,全都添加RandomHeight脚本。
运行游戏,测试效果。可以看到管子已经可以产生随机高度了。

单元4 游戏逻辑
任务4.1 UI
1. 创建开始界面
在场景中创建画布Canvas,然后制作开始界面。
首先在Canvas下新建一个空物体,命名为StartWnd。然后在StartWnd下新建一个Image,命名为imgTitle,SourceImage使用main图片。然后在StartWnd下新建一个Button,命名为btnStart,SourceImage使用start图片。如图所示。

调整位置,最后效果如图所示。

2. 创建准备界面
隐藏StartWnd,在Canvas下新建一个空物体,命名为ReadyWnd。然后在ReadyWnd下新建一个Button,命名为page1,SourceImage使用tap图片,效果如图所示。

隐藏page1,然后在ReadyWnd下新建一个Image,命名为page2,SourceImage使用ready图片。如图所示。

调整位置,效果如图所示。

3. 创建结束界面
隐藏ReadyWnd,在Canvas下新建一个空物体,命名为EndWnd。然后在EndWnd下新建一个Button,命名为page1,SourceImage使用gameover图片,效果如图所示。

隐藏page1,然后在EndWnd下新建一个空游戏对象,命名为page2。在page2下新建一个Image,命名为bg,SourceImage使用score图片。在bg下新建两个Text,一个命名为txtScore,一个命名为txtBest。在page2下新建一个Button,命名为btnRestart,SourceImage使用start图片,如图所示。

调整样式和位置,最后效果如图所示。

4. 创建左上角得分显示
在Canvas下新建Text,命名为txtScore。调整参数,最后效果如图所示。

任务4.2 创建游戏管理器
1. 创建GameRoot.cs脚本单例
创建空游戏对象GameRoot,并添加GameRoot.cs脚本。代码如下。
public class GameRoot : MonoBehaviour
{
public static GameRoot Instance;
void Start()
{
Instance = this;
}
}
2. 创建游戏状态
public const int GAMESTART = 0;
public const int GAMEREADY = 1;
public const int GAMERUN = 2;
public const int GAMEEND = 3;
public int GAMESTATE = GAMESTART;
3. 引用UI窗口和主角
添加引用UI窗口和主角的字段,代码如下。
public Transform StartWnd, ReadyWnd, EndWnd, bird;
public Text txtScore;
public int score=0;
在外部对这些字段进行赋值,如图所示。

4. 创建游戏状态变更的方法
/// <summary>
/// 刚进入游戏时
/// </summary>
public void Enter() {
StartWnd.gameObject.SetActive(true);
GAMESTATE = GAMESTART;
}
/// <summary>
/// 进入准备状态时
/// </summary>
public void Ready() {
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}
/// <summary>
/// 进入娱乐状态时
/// </summary>
public void Run() {
ReadyWnd.gameObject.SetActive(false);
GAMESTATE = GAMERUN;
}
/// <summary>
/// 游戏结束时
/// </summary>
public void End()
{
EndWnd.gameObject.SetActive(true);
GAMESTATE = GAMEEND;
}
任务4.3 UI逻辑
1. 添加StartWnd脚本
首先创建StartWnd.cs脚本,代码如下:
using UnityEngine;
using UnityEngine.UI;
public class StartWnd : MonoBehaviour
{
public Button btnStart;
// Use this for initialization
void Start()
{
btnStart.onClick.AddListener(OnStartClick);
}
void OnStartClick()
{
GameRoot.Instance.Ready();
}
}
将脚本添加到StartWnd上,然后对UI字段进行赋值。

2. 添加ReadyWnd脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ReadyWnd : MonoBehaviour
{
public Button page1;
public Transform page2;
// Use this for initialization
void Start()
{
page1.onClick.AddListener(OnClick);
}
void OnEnable()
{
page1.gameObject.SetActive(true);
page2.gameObject.SetActive(false);
}
public void OnClick()
{
//显示page2
page1.gameObject.SetActive(false);
page2.gameObject.SetActive(true);
//1秒过后调用
StartCoroutine(Run());
}
IEnumerator Run() {
yield return new WaitForSeconds(1);
GameRoot.Instance.Run();
}
}
将脚本添加到ReadyWnd上,并对外部引用进行赋值。如图所示。

3. 添加EndWnd脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EndWnd : MonoBehaviour
{
public Button page1;
public Transform page2;
public Button btnRestart;
public Text txtScore, txtBest;
// Use this for initialization
void Start()
{
page1.onClick.AddListener(OnPage1Click);
btnRestart.onClick.AddListener(OnRestartClick);
}
void OnEnable() {
page1.gameObject.SetActive(true);
page2.gameObject.SetActive(false);
}
// Update is called once per frame
void Update()
{
}
void OnPage1Click()
{
page2.gameObject.SetActive(true);
page1.gameObject.SetActive(false);
txtBest.text = PlayerPrefs.GetInt("best").ToString();
txtScore.text = GameRoot.Instance.score.ToString();
}
void OnRestartClick()
{
gameObject.SetActive(false);
GameRoot.Instance.Ready();
}
}
将脚本添加到EndWnd上,并给外部引用赋值,如图所示。

任务4.4 得分
1. 添加得分触发器
在pipeDown0下创建空游戏对象,命名为scoreTrigger。

设置Position为(0,3,0),然后在scoreTrigger上添加Box Collider 2D组件,设置如图所示。

效果如图所示。

同理,在pipeDown1和pipeDown2下也创建scoreTrigger。最后效果如图所示。

2. 添加得分方法
在GameRoot中添加以下方法,代码如下。
public void GetPoint()
{
score++;
txtScore.text = "Score:" + score;
}
3. 添加得分触发方法
在GateGroup上创建ScoreCtrl脚本,代码如下。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreCtrl : MonoBehaviour {
public void OnTriggerEnter2D(Collider2D other) {
if (other.CompareTag("Player")) {
GameRoot.Instance.GetPoint();
}
}
}
为了让这个方法对所有Trigger有效,在GateGroup上添加RigidBody2D组件,并设置组件,如图所示。

最后,修改Bird对象的Tag为Player。
任务4.5 失败
添加失败触发方法
在SportCtrl脚本里添加碰撞检测方法,代码如下。
void OnCollisionEnter2D(Collision2D coll)
{
GameRoot.Instance.End();
}
单元5 逻辑优化
任务5.1 控制优化
1. 禁用控制
修改SportCtrl.cs的FixedUpdate方法,代码如下。
void FixedUpdate()
{
if (GameRoot.Instance.GAMESTATE != GameRoot.GAMERUN)
return;
Fly();
}
2. 禁用和启用重力
在SportCtrl.cs中添加两个方法:
public void EnableGravity() {
rb.gravityScale = 0.7f;
}
public void DisEnableGravity() {
rb.gravityScale = 0;
}
然后添加SportCtrl单例并在游戏开始消除重力:
public static SportCtrl Instance;
void Start()
{
Instance = this;
rb = GetComponent<Rigidbody2D>();
DisEnableGravity();
}
在GameRoot中调用:
public void Ready()
{
SportCtrl.Instance.DisEnableGravity();
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}public void Run()
{
SportCtrl.Instance.EnableGravity();
ReadyWnd.gameObject.SetActive(false);
GAMESTATE = GAMERUN;
}
3. 禁用和启用速度
在SportCtrl里添加方法:
public void Init() {
rb.velocity = Vector3.zero;
rb.angularVelocity = 0;
}
在GameRoot里调用:
public void Ready()
{
SportCtrl.Instance.Init();
SportCtrl.Instance.DisEnableGravity();
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}
任务5.2 场景重置
1. 在GameRoot游戏对象上添加MapManager脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapManager : MonoBehaviour {
public List<Transform> maps = new List<Transform>();
List<Vector3> pos = new List<Vector3>();
// Use this for initialization
void Start () {
for (var e in maps) {
pos.Add(e.position);
}
}
public void Init () {
for (int i = 0; i < maps.Count; i++)
{
maps[i].position = pos[i];
}
}
}
2. 在外部赋值

3. 在GameRoot里调用
public void Ready()
{
GetComponent<MapManager>(). Init ();
SportCtrl.Instance. Init ();
SportCtrl.Instance.DisEnableGravity();
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}
任务5.3 保存分数
1. 读取历史最高分
在GameRoot中添加最高分变量:
public int best;
在游戏结束时,判断是否更新最高分:
public void End()
{
if (GAMESTATE == GAMEEND)
return;
EndWnd.gameObject.SetActive(true);
GAMESTATE = GAMEEND;
int best=PlayerPrefs.GetInt("best");
if (score > best)
{
PlayerPrefs.SetInt("best", score);
}
}
2. 重置分数
在GameRoot里修改方法:
public void Ready()
{
score = 0;
GetComponent<MapManager>().Init();
SportCtrl.Instance. Init ();
SportCtrl.Instance.DisEnableGravity();
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}
单元6 声音和动画
任务6.1 添加声音
1. 在GameRoot上添加AudioManager脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance;
public AudioSource fgSource;
public AudioSource bgSource;
public AudioClip wing;
public AudioClip hit;
public AudioClip die;
public AudioClip point;
public AudioClip swooshing;
void Start() {
Instance = this;
}
public void PlayHit() {
fgSource .PlayOneShot(hit);
}
public void PlayWing()
{
if (!fgSource.isPlaying)
fgSource.PlayOneShot(wing);
}
public void PlayPoint()
{
bgSource.PlayOneShot(point);
}
public void PlaySwooshing()
{
fgSource .PlayOneShot(swooshing);
}
public void PlayDie()
{
fgSource .PlayOneShot(die);
}
}
在GameRoot下创建两个空物体fgsource和bgsource,均添加AudioSource组件。

2. 在外部对AudioManager进行赋值

3. 在GameRoot中调用
public void Ready()
{
AudioManager.Instance.PlaySwooshing();
score = 0;
GetComponent<MapManager>(). Init ();
SportCtrl.Instance. Init ();
SportCtrl.Instance.DisEnableGravity();
bird.position = Vector3.zero;
StartWnd.gameObject.SetActive(false);
ReadyWnd.gameObject.SetActive(true);
GAMESTATE = GAMEREADY;
bird.rotation = Quaternion.identity;
}
public void End()
{
AudioManager.Instance.PlayHit();
if (GAMESTATE == GAMEEND)
return;
AudioManager.Instance.PlayDie();
EndWnd.gameObject.SetActive(true);
GAMESTATE = GAMEEND;
int best=PlayerPrefs.GetInt("best");
if (score > best)
{
PlayerPrefs.SetInt("best", score);
}
}
public void GetPoint()
{
AudioManager.Instance.PlayPoint();
score++;
//AudioSvc.Instance.Playpoint();
txtScore.text = "Score:" + score;
}
4. 在SportCtrl中进行赋值
public void Fly()
{
float xSpeed = 1f;
Vector3 v = rb.velocity;
float ySpeed = v.y;
if (Input.GetMouseButton(0))
{
ySpeed = 2f;
AudioManager.Instance.PlayWing();
}
rb.velocity = new Vector3(xSpeed, ySpeed, 0);
}
任务6.2 添加动画
1. 添加图片
在SportCtrl脚本中添加图片列表:
public SpriteRenderer renderer;
public List<Sprite> sprites=new List<Sprite>();
int index=0;
float timer=0;
2. 在外部赋值

3. 切换图片
void SwitchSprite(float interval) {
timer += Time.fixedDeltaTime;
if (timer > interval) {
timer = 0;
index = (index + 1) % sprites.Count;
renderer.sprite = sprites[index];
}
}
// Update is called once per frame
void FixedUpdate()
{
if (GameRoot.Instance.GAMESTATE != GameRoot.GAMERUN)
return;
Fly();
SwitchSprite(0.1f);
}