提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
这个游戏算是本Unity菜鸡真正意义上从头到尾跟着教程,一步步踩坑到完成的游戏了。
写这篇博客主要是用于总结学习该游戏项目,嗯,下面开始吧。
(PS:这个游戏是跟着siki学院的愤怒的小鸟教程做的,b站地址:【SiKi学院Unity】Unity初级案例 - 愤怒的小鸟_哔哩哔哩_bilibili
? 官网的课程资料、源码及笔记下载地址:http:// http://www.sikiedu.com/course/134)
提示:以下是本篇文章正文内容,下面案例可供参考
一、游戏逻辑与设计
本款游戏作为一个入门级的Unity项目,它的实现逻辑并不复杂,首先把游戏场景分为三个:
其中,加载界面和关卡选择界面,可以选择Unity自带的UI功能进行实现,也就是用Image来显示背景图片和各个按键等组件布局;
关于游戏界面,例如小鸟、小猪、木块、背景和草地等对象,这里是通过新建一个空物体,然后添加图片的形式进行实现,然后例如暂停窗口、胜利窗口、失败窗口就可以使用UI进行实现,把所有窗口放在一个Canvas里,然后默认取消显示,当达成目标功能时(例如关卡胜利、失败,点击暂停按键等),就将它对应的组件设置为激活状态,这样就达成了界面显示的功能;
然后是关于游戏的逻辑了,由于愤怒的小鸟它主要的游戏核心,其实就是——碰撞。
所以在这里,绝大部分的游戏逻辑其实都是通过碰撞和触发进行实现,我们把小鸟、小猪、障碍物等组件加上刚体和碰撞器,然后根据需求针对特殊的个体组件添加触发器,然后我们可以通过判断碰撞和触发的状态,来决定是否发生了游戏对象的逻辑碰撞,然后选择执行对应代码。
二、游戏场景搭建
1.游戏背景
游戏背景就是由几个图片(背景图片、地面、草丛)拼接而成,并给地面添加一个碰撞器(BoxCollider2D),以便小鸟和敌方单位不会一直落下。
2.玩家模块
玩家模块由以下几个对象组成:
对于小鸟,需要添加其刚体与碰撞器组件,并根据不同小鸟的大小,调整碰撞范围;
关于小鸟的“弹弓模拟”操作,这里使用了Spring Joint2D组件进行实现,中心点为小鸟中心,左右两个点分别设置在左弹弓和右弹弓上的合适位置,然后可以根据情况调整距离和频率,这样就可以初步实现类似弹弓弹簧的效果(目前还无法飞出);
接下来关于拖拽小鸟,形成画线的功能,这里使用LineRender进行画线操作,在脚本中,当可以进行画线操作时(当前小鸟已经激活,但是还没有飞行),设置其每一段Line的两点并画线;
/// <summary>
/// 划线
/// </summary>
public void Line()
{
right.enabled = true;
left.enabled = true;
right.SetPosition(0, rightPos.position);
right.SetPosition(1, this.transform.position);
left.SetPosition(0, leftPos.position);
left.SetPosition(1, this.transform.position);
}
3.敌人模块
敌人模块就是靠自己进行发挥了,针对不同的物体对象(小猪、木块、木条、铁块、柱子...)设置其刚体和碰撞器(主要是碰撞体大小),然后设置其血量(最小和最大承受速度),受伤图片等等其它的变量,然后根据自己的想象和设计,设计出不同的关卡;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
三、游戏代码模块
1.小鸟(包括特殊小鸟)
通过控制canMove来区分当前小鸟和等待小鸟的状态,当该小鸟为激活状态时,canMove为true,可以执行Line(弹弓画线)和Fly(飞行过程)方法,进行弹弓的操控与飞行操作,当飞行状态时,重新置canMove为false(防止重复操作),其中控制小鸟的操作由鼠标来执行,在代码中即是,在OnMouseUp、OnMouseDown来进行监听;
当飞行过程中,可以通过点击鼠标调用ShowSkill方法执行特殊小鸟的技能操作,这个方法可以写成虚方法,在后面的特殊小鸟中,进行重写并调用;当该小鸟飞出后,通过延时调用Next方法,重新销毁当前小鸟,并激活下一个等待小鸟;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class Bird : MonoBehaviour
{
public bool isClick = false;
public float maxDis = 1.5f;
[HideInInspector]
public SpringJoint2D sp;
protected Rigidbody2D rg;
public LineRenderer right;
public LineRenderer left;
public Transform rightPos;
public Transform leftPos;
public GameObject boom;
protected TestMyTrail myTrail;
[HideInInspector]
public bool canMove = false;
public float amooth = 3;
public AudioClip select;
public AudioClip fly;
private bool isFlay;
public bool isReleased = false;
public Sprite hurt;
public SpriteRenderer render;
public void Awake()
{
sp = GetComponent<SpringJoint2D>();
rg = GetComponent<Rigidbody2D>();
render = GetComponent<SpriteRenderer>();
myTrail = GetComponent<TestMyTrail>();
}
private void OnMouseDown()
{
if (canMove)
{
AudioPlay(select);
isClick = true;
rg.isKinematic = true;
}
}
private void OnMouseUp()
{
if (canMove)
{
isClick = false;
rg.isKinematic = false;
Invoke("Fly", 0.1f);
right.enabled = false;
left.enabled = false;
canMove = false;
}
}
public void Update()
{
//如果点击的是UI界面,则直接返回
if (EventSystem.current.IsPointerOverGameObject())
{
return;
}
if (isClick)
{
this.transform.position = Camera.main.ScreenToWorldPoint(Input.mousePosition);
this.transform.position += new Vector3(0, 0, -Camera.main.transform.position.z);
if(Vector3.Distance(this.transform.position,rightPos.position) > maxDis)
{
Vector3 pos = (this.transform.position - rightPos.position).normalized;
pos *= maxDis;
this.transform.position = pos + rightPos.position;
}
Line();
}
//相机跟随
CamereMove();
//飞行时,点击左键
if (isFlay)
{
if (Input.GetMouseButtonDown(0))
{
ShowSkill();
}
}
}
public void CamereMove()
{
float posX = this.transform.position.x;
Camera.main.transform.position = Vector3.Lerp(Camera.main.transform.position, new Vector3(Mathf.Clamp(posX,0,17),Camera.main.transform.position.y,
Camera.main.transform.position.z), amooth*Time.deltaTime);
}
public void Fly()
{
isReleased = true;
isFlay = true;
AudioPlay(fly);
myTrail.StartTrail();
sp.enabled = false;
Invoke("Next", 3);
}
/// <summary>
/// 划线
/// </summary>
public void Line()
{
right.enabled = true;
left.enabled = true;
right.SetPosition(0, rightPos.position);
right.SetPosition(1, this.transform.position);
left.SetPosition(0, leftPos.position);
left.SetPosition(1, this.transform.position);
}
public virtual void Next()
{
Gamemanager._instance.birds.Remove(this);
Destroy(this.gameObject);
Instantiate(boom, this.transform.position, Quaternion.identity);
Gamemanager._instance.NextBird();
}
public void OnCollisionEnter2D(Collision2D collision)
{
isFlay = false;
myTrail.ClearTrail();
}
public void AudioPlay(AudioClip clip)
{
AudioSource.PlayClipAtPoint(clip,this.transform.position);
}
public virtual void ShowSkill()
{
isFlay = false;
}
public void Hurt()
{
render.sprite = hurt;
}
}
剩下的黄鸟(加速)、绿鸟(回旋)和黑鸟(爆炸),就是继承了redbird这个类,然后根据需求,重写ShowSkill方法即可;
public class YellowBird : Bird
{
public override void ShowSkill()
{
base.ShowSkill();
rg.velocity *= 2;
}
}
?
public class GreenBird : Bird
{
public override void ShowSkill()
{
base.ShowSkill();
Vector3 speed = rg.velocity;
speed.x *= -1;
rg.velocity = speed;
}
}
?其中,BlackBird因为要通过触发判断爆炸效果,并且爆炸后直接进行销毁,所以要额外多写几个方法进行实现
public class BlackBird : Bird
{
public List<Pig> blocks = new List<Pig>();
/// <summary>
/// 进入触发区域
/// </summary>
/// <param name="collision"></param>
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "Enemy")
{
blocks.Add(collision.gameObject.GetComponent<Pig>());
}
}
/// <summary>
/// 退出触发区域
/// </summary>
/// <param name="collision"></param>
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
blocks.Remove(collision.gameObject.GetComponent<Pig>());
}
}
public override void ShowSkill()
{
base.ShowSkill();
if( blocks!=null && blocks.Count > 0)
{
for(int i = 0; i < blocks.Count; i++)
{
blocks[i].Dead();
}
}
OnClear();
}
public void OnClear()
{
rg.velocity = Vector3.zero;
Instantiate(boom, this.transform.position, Quaternion.identity);
render.enabled = false;
GetComponent<CircleCollider2D>().enabled = false;
myTrail.ClearTrail();
}
public override void Next()
{
Gamemanager._instance.birds.Remove(this);
Destroy(this.gameObject);
Gamemanager._instance.NextBird();
}
}
?
2.敌人(包括小猪、木块等障碍物)
这里主要就是调整敌方的“血量”,但是这里的血量并不是一点一点扣除的,而是进行一个判断,当该物体碰撞的对象是Player(小鸟),并且当
- 碰撞速度>maxspeed时,敌方死亡
- minspeed<碰撞速度<maxspeed时,敌方调整为受伤状态
- 碰撞速度<minspeed时,敌方不受伤
然后写一个Dead方法,用于处理死亡后的操作(播放死亡音乐、爆炸特效、销毁物体等)
public class Pig : MonoBehaviour
{
public float maxSpeed = 10;
public float minSpeed = 4;
private SpriteRenderer render;
public Sprite hurt;
public GameObject boom;
public GameObject score;
public bool isPig = false;
public AudioClip hurtClip;
public AudioClip dead;
public AudioClip birdCollision;
private void Awake()
{
render = GetComponent<SpriteRenderer>();
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.gameObject.tag == "Player")
{
AudioPlay(birdCollision);
collision.transform.GetComponent<Bird>().Hurt();
}
if(collision.relativeVelocity.magnitude > maxSpeed)
{
Dead();
}
else if( collision.relativeVelocity.magnitude>minSpeed && collision.relativeVelocity.magnitude < maxSpeed)
{
AudioPlay(hurtClip);
render.sprite = hurt;
}
}
public void Dead()
{
if (isPig)
{
Gamemanager._instance.pigs.Remove(this);
}
AudioPlay(dead);
Destroy(this.gameObject);
Instantiate(boom, this.transform.position, Quaternion.identity);
GameObject go = Instantiate(score, this.transform.position + new Vector3(0,0.5f,0), Quaternion.identity);
Destroy(go, 1.5f);
}
public void AudioPlay(AudioClip clip)
{
AudioSource.PlayClipAtPoint(clip, this.transform.position);
}
}
3.游戏管理器
游戏管理器用于控制游戏逻辑,这里定义两个列表,用于存储所有小鸟和所有小猪,
在开始时,激活小鸟列表的第一个对象为当前小鸟,其它小鸟等待,当当前小鸟飞行完成后,调用Next方法,删除已发射小鸟,并激活下一个等待小鸟进行操纵;
当小鸟数量=0或是小猪数量=0时,进行逻辑判断
- 当小猪数量=0时,胜利!
- 当小猪数量<0,并且小鸟数量=0时,失败!
然后显示对应的UI界面,其中当胜利时,获得的星星个数为 当前剩余小鸟个数+1,以此逻辑进行得分判断,并通过Unity自带的PlayerPrefs类,以键值对的形式(key为当前的关卡名称),进行数据的存储;
剩下的就是定义一些UI按键的绑定方法,例如
- Next? ? ?下一关
- SaveData? ?当游戏胜利时,保存当前关卡得分(取最大)
- Home? ? 返回游戏首页
- RePlay? ?重新开始当前游戏关卡
public class Gamemanager : MonoBehaviour
{
public List<Bird> birds;
public List<Pig> pigs;
public static Gamemanager _instance;
public Vector3 originPos; //初始位置
public GameObject win;
public GameObject lose;
public GameObject[] starts;
public int starsNum = 0;
public int totalNum = 5;
public void Start()
{
Initialized();
}
public void Awake()
{
_instance = this;
originPos = birds[0].transform.position;
}
/// <summary>
/// 初始化小鸟
/// </summary>
private void Initialized()
{
for(int i = 0; i < birds.Count; i++)
{
if (i == 0)//第一只小鸟
{
birds[0].transform.position = originPos;
birds[i].enabled = true;
birds[i].sp.enabled = true;
birds[i].canMove = true;
}
else
{
birds[i].enabled = false;
birds[i].sp.enabled = false;
}
}
}
public void NextBird()
{
if (pigs.Count > 0)
{
if (birds.Count > 0)
{
//下一只小鸟
Initialized();
}
else
{
//输了
lose.SetActive(true);
}
}
else
{
//赢了
win.SetActive(true);
}
}
public void ShowStarts()
{
StartCoroutine("show");
//Debug.Log("胜利!!!" + birds.Count);
}
IEnumerator show()
{
for (; starsNum < birds.Count + 1; starsNum++)
{
if(starsNum >= starts.Length)
{
break;
}
yield return new WaitForSeconds(0.2f);
//Debug.Log(starts[i].name);
starts[starsNum].SetActive(true);
}
}
public void RePlay()
{
SaveData();
SceneManager.LoadScene(2);
}
public void Home()
{
SaveData();
SceneManager.LoadScene(1);
}
public void Next()
{
SaveData();
string currentLevel = PlayerPrefs.GetString("nowLevel");
Debug.Log(currentLevel);
int num = int.Parse(currentLevel.Substring(5,1)) + 1;
string nextLevel = currentLevel.Substring(0, currentLevel.Length - 1) + System.Convert.ToString(num);
Debug.Log("下一关为: " + nextLevel);
//加载下一关
PlayerPrefs.SetString("nowLevel", nextLevel);
SceneManager.LoadScene(2);
}
public void SaveData()
{
Debug.Log("当前关卡的星星数量为: " + starsNum);
//当前的星星数目大于已存储星星数目时,进行更新存储
if (starsNum > PlayerPrefs.GetInt(PlayerPrefs.GetString("nowLevel")))
{
PlayerPrefs.SetInt(PlayerPrefs.GetString("nowLevel"), starsNum);
}
//存储所有的星星个数
int sum = 0;
for(int i = 1; i <= totalNum; i++)
{
sum += PlayerPrefs.GetInt("level" + i.ToString());
//Debug.Log("第"+ i.ToString() +"关的星星为: " + PlayerPrefs.GetInt("level" + i.ToString()));
//Debug.Log("sum为: " + sum);
}
Debug.Log("将要存储的星星总数为: " + sum);
PlayerPrefs.SetInt("totalNum", sum);
}
}
4.地图选择
地图UI设计为以下四个部分,其中最后一个部分没有功能实现,所以真正的关卡其实只有前面三部分
?在开始时,我们先读取所有已通关关卡的星星数目总和,当星星综合大于设定的map星星数目时,该map才进行解锁,设置isSelect=true,否则锁定该关卡,设置isSelect=false;
当点击该map时,隐藏map视图,显示关卡视图level
public class MapSelect : MonoBehaviour
{
public int starsNum;
public bool isSelect = false;
public GameObject locks;
public GameObject starts;
public GameObject map;
public GameObject panel;
public Text startsText;
public int startNum = 1;
public int endNum = 5;
public void Start()
{
//清除所有游戏数据
//PlayerPrefs.DeleteAll();
if(PlayerPrefs.GetInt("totalNum",0) >= starsNum)
{
Debug.Log("星星总数为: " + PlayerPrefs.GetInt("totalNum"));
isSelect = true;
}
if (isSelect)
{
locks.SetActive(false);
starts.SetActive(true);
//TODO:Text显示
TextShow();
}
}
public void TextShow()
{
int count = 0;
for (int i = startNum; i <= endNum; i++)
{
count += PlayerPrefs.GetInt("level" + i.ToString(), 0);
}
startsText.text = count.ToString() + "/15";
}
public void Selected()
{
if (isSelect)
{
panel.SetActive(true);
map.SetActive(false);
}
}
public void PanelSelect()
{
panel.SetActive(false);
map.SetActive(true);
}
}
5.关卡选择
当激活关卡视图时,首先激活第一关,设置isSelect = true,然后遍历剩下的关卡,通过PlayerPrefs获取已存储的数据,当目标关卡的星星数目>0时,激活该关卡,否则进行锁定;
当激活某一关时,要通过PlayerPrefs得到该关卡的星星数目,然后通过控制star[i](星星列表)进行星星的显示;
然后定义一个Slect方法,用于选择关卡,当点击某个关卡时,通过PlayerPrefs设置当前关卡为点击关卡,并通过SceneManager读取场景
public class LevelSelect : MonoBehaviour
{
public bool isSelect = false;
public Sprite levelBG;
public Image img;
public GameObject[] stars;
public void Awake()
{
img = GetComponent<Image>();
}
public void Start()
{
//是第一关
if(this.transform.name == this.transform.parent.GetChild(0).name)
{
isSelect = true;
}
else
{
int beforeNum = int.Parse(this.gameObject.name) - 1;
if( PlayerPrefs.GetInt("level"+beforeNum.ToString()) > 0)
{
isSelect = true;
}
}
//激活关卡
if (isSelect)
{
img.overrideSprite = levelBG;
this.transform.Find("num").gameObject.SetActive(true);
//读取星星个数
int count = PlayerPrefs.GetInt("level" + this.gameObject.name);
if (count > 0)
{
for(int i = 0; i < count; i++)
{
stars[i].SetActive(true);
}
}
}
}
public void Selected()
{
if (isSelect)
{
PlayerPrefs.SetString("nowLevel", "level" + this.gameObject.name);
SceneManager.LoadScene(2);
//Debug.Log("选择成功");
}
}
}
6.暂停界面
通过UI设计一个暂停界面,当为激活时,它在游戏窗口外,当激活时,通过动画,将UI界面移入游戏窗口;
定义方法Pause,当点击暂停按钮时,调用该方法,然后调用暂停动画,将UI窗口移入游戏界面,
设置Time.timeScale = 0,以达到暂停游戏的效果,并隐藏该暂停按钮;
然后定义Resume方法,当点击还原按钮时,调用该方法,调用还原动画,重新将UI窗口移除游戏界面,并设置第一个小鸟为激活状态,并显示暂停按钮;
接下来定义Home方法和Retry方法,分别实现返回首页,和重新开始该关卡的操作,这里可以调用Gamemaneger中的Home和Retry方法,不过要注意提前设置Time.timeScale = 1,以免游戏继续暂停;
public class PausePanel : MonoBehaviour
{
private Animator anim;
public GameObject button;
public void Awake()
{
anim = GetComponent<Animator>();
}
/// <summary>
/// Home按键
/// </summary>
public void Home()
{
Time.timeScale = 1;
Gamemanager._instance.Home();
}
/// <summary>
/// Retry按键
/// </summary>
public void Retry()
{
Time.timeScale = 1;
Gamemanager._instance.RePlay();
}
/// <summary>
/// Pause按键
/// </summary>
public void Pause()
{
anim.SetBool("isPause", true);
button.SetActive(false);
//暂停
if (Gamemanager._instance.birds.Count > 0)
{
if(Gamemanager._instance.birds[0].isReleased == false)
{
Gamemanager._instance.birds[0].canMove = false;
}
}
}
/// <summary>
/// Resume按键
/// </summary>
public void Resume()
{
Time.timeScale = 1;
anim.SetBool("isPause", false);
//还原
if (Gamemanager._instance.birds.Count > 0)
{
if (Gamemanager._instance.birds[0].isReleased == false)
{
Gamemanager._instance.birds[0].canMove = true;
}
}
}
/// <summary>
/// pause动画结束后调用
/// </summary>
public void PauseAnimEnd()
{
Time.timeScale = 0;
}
/// <summary>
/// resume动画结束后调用
/// </summary>
public void ResumeAnimEnd()
{
button.SetActive(true);
}
}
?
四、场景间的组合搭配
一个游戏场景由以下几个模块构成:
- Main Camera? ? ? ? 主摄像机
- Player? ? ? ? ? 玩家模块,包括弹弓、当前小鸟、准备小鸟
- Enemy? ? ? ? ?敌人模块,包括所有敌方单位:小猪、木块等障碍物
- env? ? ? ? ? ? ? ?游戏背景:背景图片,地面,草丛
- Cavas? ? ? ? ? UI画布,所有的UI组件都放在这里,用于管理UI的所有操作
- UICamera? ? UI摄像机,查看UI镜头
- Gamemenager? ? 游戏管理器,挂载Gamemaneger脚本,管理几乎所有的游戏逻辑与功能模块
五、游戏的发布
关于游戏的发布,首先在项目设置里,设置你的游戏画面、游戏图标、鼠标图标等设置
然后进入Build Settings,设置需要发布的场景画面Scenes,根据需求,在不同的平台上发布游戏,在这里我选择发布的平台是Windows和Android(安卓发布,需要设置例如jdk、sdk,ndk等环境配置)
最后点击Build,铛铛,大功告成~
总结
以上就是一个Unity入门菜鸡开发的入门级2D游戏项目,本文主要用于自己学习总结,如有不对的地方望各位大佬指正。
下面是已发布成功的游戏本体:
PC和安卓:https://pan.baidu.com/s/1Q6SH-YWx7u4qF5DFDjOVqw? ? ? ?提取码:9xpx?
安卓(蓝奏云):愤怒的小鸟? ? 密码:2zp4
|