源码学习网 首页 编程学园 Unity3d教程 查看内容

Unity快速上手系列之2:2D物理弹球

2019-7-14 01:32| 发布者: opiye| 查看: 83| 评论: 0

摘要: 大家好。以“跳一跳”为开端,微信小游戏从今年年初起以迅雷不及掩耳盗铃儿响叮当之势席卷了用户的手机。从创意小游戏,到页游遗风的挂机游戏,一时间百花齐放。当然,前者说是创意,其实绝大部分也就是直接把其他平 ...
腾讯云服务器秒杀

大家好。


以“跳一跳”为开端,微信小游戏从今年年初起以迅雷不及掩耳盗铃儿响叮当之势席卷了用户的手机。从创意小游戏,到页游遗风的挂机游戏,一时间百花齐放。
当然,前者说是创意,其实绝大部分也就是直接把其他平台上的游戏模式搬到H5上而已,例如经典的三维弹球。
而作为物理引擎的代表作品,实现一款三维弹球作品对初学者的锻炼还是挺大的。这也是我今天写这篇小文的主要目的。

制作此游戏分为两个大的步骤,一是场景搭建,二是脚本编写。下面我们就来一起逐步完成这款小游戏。
场景搭建:游戏属于2D游戏,所以场景我们用2D精灵(Sprite)来搭建


一.砌墙


首先搭建一圈2D碰撞器作围墙,限制小球活动范围:

所有的墙都要在2D碰撞器内添加拖入弹性物理材质,上下墙不添加


二.铺路


在小球外部活动范围内搭建触发器,之后将在触发器中获得寻路效果:

可创建一个空物体进行管理

三,枪口


枪口(Muzzle)用来定位小球发射的地方,枪口有个子物体阀门(Valve),作用为阻挡已发射的小球被弹回枪口:

枪口需要添加LineRenderer组件,用来绘制瞄准线,阀门需要添加2D碰撞器


四.分数显示

创建一个空物体Score,它的子物体CurrentScore才是用来显示分数的Text:

五.小球管理


创建一个空物体Balls来管理所有的小球,使用2D精灵创建一个小球

六.关卡设置


在场景内一共搭建9*6个小格子,每个小格子都用来随机生成敌人,实行分层管理,每层6个,共9层:

注意:绿色小方框只是为了教程更加直观特意加的,实际开发时请移除,小格子并未添加除Transform以外的任何组件,因为小格子的作用就是定位,游戏运行时需要在该位置随机生成敌人;

七.菜单栏


游戏结束时自动弹出,游戏运行中按”Esc”键也可调用:

八,摄像机定位


由于整个场景都处于固定状态,所以将Canvas设为世界模式:

将摄像机改为正交模式

将固定在整个场景前:

九.预制件制作


创建若干个敌人(Enemy)预制件,添加上2D碰撞器和物理弹性材质

创建道具(Stunt)预制件

变大道具BigStunt:碰到后球会变大
复制道具CopyStunt:碰到后增加一个小球


创建小球(Ball)预制件,当碰到复制特效后创建


脚本编写:脚本不多,加上2个状态机也就11个

我们下面来一一介绍它们:


一.LevelCreate

脚本说明:随机生成底层关卡的元素。

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;/// <summary>/// 挂LevelPanel上/// </summary>public class LevelCreate : MonoBehaviour{    //在编辑器中将当前分数ScoreText拖进去    public Text scoreText;     //所有种类几何体,在编辑器把所有几何体预制件拖进去    public Transform[] Enemys;    //所有种类道具,在编辑器把所有道具预制件拖进去    public Transform[] stunts;     //物体生成器,决定格子里是否生成东西(几率可自行设定)    public Transform PaneFactory()    {        int chance = Random.Range(0, 4);        if (chance < 3)          //75%不产生东西,            return null;        else                     //25%产生东西            return PaneManage();    }    //决定格子里该生产什么东西    Transform PaneManage()    {        int chance = Random.Range(0, 3);        if (chance < 2)           //66%产生几何体            return CreateEnemy();        else                      //33%产生道具            return CreateStunt();    }    //随机生成道具    Transform CreateStunt()    {        //随机产生一个道具数组索引        int index = Random.Range(0, stunts.Length);        //生成该索引处道具        return Instantiate(stunts[index]);    }    //随机生成敌人    public Transform CreateEnemy()    {        //随机产生一个几何体数组索引        int index = Random.Range(0, Enemys.Length);        //生成该索引处几何体        Transform enemy = Instantiate(Enemys[index]);        //给几何体赋一个随机颜色        enemy.GetComponent<Renderer>().material.color = new Color(Random.value, Random.value, Random.value);        //给几何体一个随机旋转角度        enemy.rotation = Quaternion.Euler(0, 0, Random.Range(0, 90));        //获取几何体子物体数字的Transform组件        Transform tf = enemy.GetComponentInChildren<Text>().transform;        //子物体不旋转        tf.rotation = Quaternion.Euler(0, 0, 0);               //获取当前分数        int score = System.Convert.ToInt32(scoreText.text);               if (score < 100) //如果当前分数不超过100分            //几何体数字在 1~9 之间随机生成            enemy.GetComponentInChildren<Text>().text = Random.Range(1, 10).ToString();         else //当前分数超过100分            //几何体数字在 1~当前分数/10 之间随机生成            enemy.GetComponentInChildren<Text>().text = Random.Range(1, score / 10).ToString();         return enemy;    }}


二.LevelState

脚本说明:关卡状态机,表示当前游戏运行状态。

using System.Collections;using System.Collections.Generic;using UnityEngine;/// <summary>/// 关卡运行状态/// </summary>public enum LevelState {     life,  //运行中    pause, //暂停    die, //游戏结束}

三.LevelMove

脚本说明:

1.关卡移动方式,底层往上走一层,顶层回到最底层;

2.对顶层的物体进行判断,如果敌人到达顶层,游戏结束;

3.在最底层生成新的物体;

关卡运动方向
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;/// <summary>/// 挂LevelPanel上/// </summary>public class LevelMove : MonoBehaviour{    //需要做一个菜单,游戏死亡时弹出,在编辑器把死亡菜单拖进来    public GameObject deathPanel;    //游戏初始状态为存活    public LevelState levelState = LevelState.life;    //声明一个关卡集合用来管理每层关卡    List<Transform> lineList = new List<Transform>();    LevelCreate levelcreate; //声明创建物体的类    private void Start()    {        levelcreate = GetComponent<LevelCreate>(); //获取创建关卡类        lineList = GetAllChild(transform); //获取第一层子物体(关卡)添加进关卡集合中           CreateLevel(); //游戏开始时创建一次底层的物体    }    private void Update()    {        //在游戏运行时按Esc        if (levelState == LevelState.life && Input.GetKeyDown(KeyCode.Escape))        {            levelState = LevelState.pause; //游戏状态变为暂停             deathPanel.SetActive(true); //启用菜单            Time.timeScale = 0; //游戏暂停        }        //在暂停状态时按Esc        else if (levelState == LevelState.pause && Input.GetKeyDown(KeyCode.Escape))        {            levelState = LevelState.life; //游戏状态变为运行            deathPanel.SetActive(false); //禁用菜单            Time.timeScale = 1; //游戏恢复        }    }    List<Transform> GetAllChild(Transform fatherObj) //获取所有第一层子物体    {        //声明一个集合放第一层所有子物体        List<Transform> sonList = new List<Transform>();        int number = fatherObj.childCount; //获取第一层子物体数量        for (int i = 0; i < number; i++)        {            //将所有第一层子物体添加进集合中            sonList.Add(fatherObj.GetChild(i));        }        return sonList; //返回第一层子物体集合    }    void CreateLevel() //创建底层关卡    {        Transform last = lineList[lineList.Count - 1]; //获取底层关卡,物体将从该层产生        List<Transform> sonList = GetAllChild(last); //获取底层所有小方格        //生成一个几何体(每次创建关卡至少有一个几何体)        Transform enemy = levelcreate.CreateEnemy();        int index = Random.Range(0, last.childCount); //随机定位一个格子        enemy.position = last.GetChild(index).position; //将几何体创建在该格子内        enemy.parent = last.GetChild(index); //几何体作为该格子的子物体可随关卡层移动        //然后在其它格子里随机生成物体        for (int i = 0; i < sonList.Count; i++)        {            if (i != index) //除了刚才已经有敌人的格子外            {                //声明一个变量接受生成的物体                Transform obj = levelcreate.PaneFactory();                if (obj != null) //如果成功生产出东西                {                    obj.position = sonList[i].position; //将该东西生产在此方格                    obj.parent = sonList[i]; //作为该方格的子物体随关卡层移动                }            }        }    }    //关卡往上走一层(第一层跳到最后)    public void LevelGetUp()    {        Vector3 tempPos = lineList[lineList.Count - 1].position; //获取最后层的坐标        //遍历所有关卡层         for (int i = lineList.Count - 1; i >= 0; i--)        {            if (i == 0) //如果是顶层                lineList[i].position = tempPos; //直接跳到底层            else //如果是其它层                lineList[i].position = lineList[i - 1].position; //移动到自己上一层        }        DestroyStunt(); //销毁顶层道具        lineList.Add(lineList[0]); //将第一层添加到集合最后        lineList.RemoveAt(0); //再移除第一层        CreateLevel(); //创建一次关卡关卡        if (Death()) //判断是否死亡        {            levelState = LevelState.die; //状态变为死亡            deathPanel.SetActive(true); //调用菜单        }    }    void DestroyStunt() //销毁顶层特技    {        //获取该层所有子物体        Transform[] lineSon = lineList[0].GetComponentsInChildren<Transform>();              for (int i = 0; i < lineSon.Length; i++) //遍历所有子物体的标签         {            if (lineSon[i].tag == "Stunt") //如果是道具            {                Destroy(lineSon[i].gameObject); //销毁该子物体            }        }    }    bool Death() //死亡判断    {        //获取顶层所有子物体        Transform[] lineSon = lineList[0].GetComponentsInChildren<Transform>();        for (int i = 0; i < lineSon.Length; i++) //遍历所有子物体的标签         {            if (lineSon[i].tag == "Enemy") //如果发现有几何体                return true; //直接游戏结束        }        return false; //如果一个都没有,游戏继续    }}


四.BallState

脚本说明:小球状态机,小球会随着游戏运行改变状态,不同状态的小球具有不同属性。

using System.Collections;using System.Collections.Generic;using UnityEngine;/// <summary>/// 小球状态机,不用挂在任何物体上/// </summary>public enum BallState {       Ready, //准备阶段    Battle, //战斗阶段    Bore, //上膛阶段}


五.BallMove

脚本说明:

1小球在创景中的交互;

2.发射前的小球处于准备状态;

3.发射后的小球进入转斗状态,碰到敌人会加分,敌人会减血;

4.小球需要一个防卡住的方法;

小球卡住示意图
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;/// <summary>/// 挂小球上/// </summary>public class BallMove : MonoBehaviour{    float timer; //计时用    //初始状态为准备状态    public BallState state = BallState.Ready;    //碰撞时调一次,用于打击几何体(敌人)    private void OnCollisionEnter2D(Collision2D collision)    {       if (state == BallState.Battle) //如果在战斗阶段       {          GetComponent<Rigidbody2D>().gravityScale = 1; //碰到东西后重力为1          if (collision.gameObject.tag == "Enemy") //如果碰到敌人              {             //获取敌人数字             Text enemyNumber = collision.transform.GetChild(0).GetComponent<Text>();             //获取当前分数             Text Score = GameObject.Find("ScoreText").GetComponent<Text>();              if (tag == "BigBall") //如果自己是大球             {                //敌人数字-2                enemyNumber.text = ((System.Convert.ToInt32(enemyNumber.text)) - 2).ToString();                //当前分数+2                Score.text = ((System.Convert.ToInt32(Score.text)) + 2).ToString();             }             else //如果自己是小球             {                //敌人数字-1                enemyNumber.text = ((System.Convert.ToInt32(enemyNumber.text)) - 1).ToString();                //当前分数+1                Score.text = ((System.Convert.ToInt32(Score.text)) + 1).ToString();             }          }       }    }    //碰撞时持续调用,防止小球被卡住    private void OnCollisionStay2D(Collision2D collision)     {       if (collision.gameObject.tag == "Enemy") //如果碰撞的是敌人       {          timer += Time.deltaTime; //开始计时          if (timer > 1) //一秒后还停留在那上面          {             switch (Random.Range(0, 4)) //随机方向弹开             {                case 0:                   GetComponent<Rigidbody2D>().AddForce(transform.up * 0.01f);                   break;                case 1:                   GetComponent<Rigidbody2D>().AddForce(-transform.up * 0.01f);                   break;                case 2:                   GetComponent<Rigidbody2D>().AddForce(transform.right * 0.01f);                   break;                case 3:                   GetComponent<Rigidbody2D>().AddForce(-transform.up * 0.01f);                   break;             }          }       }    }    //离开碰撞时调一次    private void OnCollisionExit2D(Collision2D collision)    {        timer = 0; //计时归零    }    private void Update()    {        transform.Rotate(0, 0, 0.0001f); //物体处于非完全静止状态,持续碰撞才会生效        switch (state)        {            case BallState.Bore: //上膛阶段                GetComponent<Rigidbody2D>().gravityScale = 0; //重力变为0                break;            case BallState.Ready: //准备阶段                GetComponent<CircleCollider2D>().isTrigger = false; //关闭触发                GetComponent<Rigidbody2D>().Sleep(); //小球停止不动                break;        }    }}


六.BallFindWay

脚本说明:

1.发射后的小球会寻路回到枪口,给每一个寻路碰撞器挂一个;

2.碰到顶上的碰撞器后.小球会进入上膛阶段,直接回到枪口;

using System.Collections;using System.Collections.Generic;using UnityEngine;/// <summary>/// FindTheWays下的所有寻路碰撞器都挂一个/// </summary>public class BallFindWay : MonoBehaviour{    public Transform muzzle; //在编辑器把枪口拖进去,我们需要枪口的坐标    public float boreSpeed=0.2f; //上膛速度    private void OnTriggerStay2D(Collider2D ball) //触发时持续调用    {        //获取小球的刚体        Rigidbody2D r2d = ball.GetComponent<Rigidbody2D>();         switch (name) //根据寻路碰撞器的名字决定施加力的方向        {            case "LeftDown":                r2d.AddForce(-transform.right * 0.002f);                break;            case "RightDown":                r2d.AddForce(transform.right * 0.003f);                break;            case "Left":            case "Right":                r2d.AddForce(transform.up * 0.002f);                break;            case "Up":                //启动协程寻路(上膛)                   StartCoroutine(MoveToMuzzle(ball.transform, muzzle));                 //打开小球触发器,使小球能越过枪口阀门                ball.GetComponent<CircleCollider2D>().isTrigger = true;                break;        }    }    //使用协程寻路让小球朝枪口处移动    public IEnumerator MoveToMuzzle(Transform ball, Transform muzzle)     {        ball.GetComponent<BallMove>().state = BallState.Bore; //小球状态改为上膛状态        while (ball.GetComponent<BallMove>().state == BallState.Bore) //如果是上膛状态        {                      //小球往枪口处寻路,完成上膛                       ball.position = Vector3.MoveTowards(ball.position, muzzle.position, boreSpeed * Time.deltaTime);            yield return new WaitForFixedUpdate(); //每次循环间隔1帧            //如果小球位置和枪口位置接近            if ((ball.position - muzzle.position).sqrMagnitude <= 0.001f)             {                ball.GetComponent<BallMove>().state = BallState.Ready; //小球进入准备阶段                ball.position = muzzle.position; //将小球定在枪口位置            }        }    }}


七.Aim

脚本说明:

1.把当前小球依次发射出去,并将发射的小球变为战斗状态;

2.小球发射方向是从枪口出发,往鼠标所在方向,显示瞄准线;

3.限制小球发射返回,不能往上发射,需要做两个限制点;

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class Aim : MonoBehaviour //挂枪口Muzzle上{    public GameObject balls; //把Balls拖进去       Rigidbody2D[] allBall; //声明一个数组用来管理所有小球    LineRenderer aimLine; //声明瞄准线    public Transform CriticalPointLeft; //把左边界拖进去    public Transform CriticalPointRight; //把右边界拖进去    public float shootingSpeed = 3.5f; //小球发射速度    public GameObject levelPanel; //把LevelPanel拖进去    bool levelStop; //判断关卡是否已上升    void Start()    {        Time.timeScale = 1; //游戏时间正常              allBall = balls.GetComponentsInChildren<Rigidbody2D>();//初始化(获取当前所有小球)        aimLine = GetComponent<LineRenderer>(); //获取枪口上的LineRenderer组件    }    void Update()    {        //当游戏状态为活着时        if (levelPanel.GetComponent<LevelMove>().levelState == LevelState.life)        {            if (Homing()) //所有小球都进入准备状态了            {                allBall = balls.GetComponentsInChildren<Rigidbody2D>(); //再次获取所有小球                if (levelStop) //关卡处于未上升状态                {                    levelPanel.GetComponent<LevelMove>().LevelGetUp(); //调用关卡上升方法                    levelStop = !levelStop; //关卡处于已上升状态                }                else                    AimLaunch(); //关卡上升完成后可进行瞄准发射            }        }    }    //判断所有小球是否都进入准备状态    public bool Homing()     {        //发现任何小球不在准备状态都返回False        for (int i = 0; i < allBall.Length; i++)        {            if (allBall[i].GetComponent<BallMove>().state != BallState.Ready)                return false;         }        return true; //未发现不在准备状态的小球,返回True    }    void AimLaunch() //瞄准发射    {        if (Input.GetMouseButtonDown(0)) //点击鼠标左键        {            aimLine.SetPosition(0, transform.position); //在枪口处生成瞄准线起点        }        if (Input.GetMouseButton(0)) //按住鼠标左键不放        {            //获取鼠标坐标            Vector3 v = Camera.main.ScreenToWorldPoint(Input.mousePosition);            //限制瞄准范围            v = DirectionRestriction(v, CriticalPointLeft, CriticalPointRight);            //将被限制过的鼠标坐标实时给瞄准线结束点            aimLine.SetPosition(1, new Vector2(v.x, v.y));         }        if (Input.GetMouseButtonUp(0)) //抬起鼠标左键        {            StartCoroutine(LineLaunch(transform.position)); //启动协程发射小球            aimLine.SetPosition(1, transform.position); //让结束点和起点重合(撤销瞄准线)             levelStop = !levelStop; //关卡标记为可上升状态        }    }       IEnumerator LineLaunch(Vector3 muzzlePos) //用协程排队发射小球    {        Vector3 pos1 = aimLine.GetPosition(1);//获取瞄准线结束点坐标        Vector3 directionAttack = (pos1 - muzzlePos).normalized;//获取瞄准结束点与枪口的方向向量        for (int i = 0; i < allBall.Length; i++) //挨个发射小球        {            //被发射的小球变为战斗状态            allBall[i].GetComponent<BallMove>().state = BallState.Battle;            //球往瞄准结束点方向寻路移动              allBall[i].AddForce(directionAttack * shootingSpeed * Time.deltaTime);                   yield return new WaitForSeconds(0.1f); //每隔0.1秒发射一个        }    }    //限定枪口瞄准方向    Vector3 DirectionRestriction(Vector3 v, Transform left, Transform right)    {        //最左不能左过左边界        if (v.x < left.position.x)             v.x = left.position.x;        //最右不能右过右边界        if (v.x > right.position.x)            v.x = right.position.x;        //高度不能超过边界        if (v.y > left.position.y)            v.y = left.position.y;        return v; //返回被限制后的坐标    }}


八.DeathBalance

脚本说明:

1.按暂停或游戏结束时会弹出的菜单面板;

2.菜单面板会显示本局分数和最高分数,有重启游戏和退出游戏的按钮;

using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.SceneManagement;using UnityEngine.UI;public class DeathBalance : MonoBehaviour //挂死亡面板DeathPanel上{    public Text score; //把当前分数ScoreText拖进去    public Text bureauScore; //把本局分数BureauScore拖进去    public Text topScore; //把最高分数TopScore拖进去    private void OnEnable()    {        bureauScore.text = score.text; //结算本局分数             if (PlayerPrefs.HasKey("分数")) //如果已经存储了分数            topScore.text = PlayerPrefs.GetString("分数");  //就获取上次存的最高分数        //让本局分数和最高分数比较,如果本局分数比最高分数大        if (Convert.ToInt32(bureauScore.text) > Convert.ToInt32(topScore.text))        {            topScore.text = bureauScore.text; //更新最高分数            PlayerPrefs.SetString("分数", bureauScore.text); //存储最高分数                }    }    public void RestartGame() //重新开始,挂按钮RestartGame上    {        SceneManager.LoadScene("ElasticBall"); //加载场景(提前保存一个场景)    }    public void QuitGame() //退出游戏,挂按钮QuitGame上    {        Application.Quit();    }}


九.Enemy

脚本说明:挂几何体(敌人)身上,实时监测自身血量,血量<=0时自动销毁。

using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;/// <summary>/// 挂每个几何体上/// </summary>public class Enemy : MonoBehaviour {    Text number; //声明数字    private void Start()    {        number = GetComponentInChildren<Text>(); //找到子物体(数字)    }    private void Update()    {        if (Convert.ToInt32(number.text) < 1) //如果数字小于1时            Destroy(gameObject); //销毁几何体自身    }}


十.BigBall

脚本说明:小球碰到后直径会变大20%,攻击力翻倍,标签也会更改,永久效果,且只能变大一次。

using System.Collections;using System.Collections.Generic;using UnityEngine;/// <summary>/// //挂变大道具上/// </summary>public class BigBall : MonoBehaviour{    private void OnCollisionEnter2D(Collision2D collision) //被碰撞是调用    {        if (collision.gameObject.tag != "BigBall") //当普通小球碰到时        {            collision.transform.localScale *= 1.2f;//小球变大20%成打球            collision.gameObject.tag = "BigBall"; //大球的标签设为“BigBall”        }        Destroy(gameObject); //销毁变大道具    }}


十一.CopyBall

复制道具:小球或打球碰到后会从预支件创建一个小球供玩家调配,

using System.Collections;using System.Collections.Generic;using UnityEngine;/// <summary>/// 挂复制道具上/// </summary>public class CopyBall : MonoBehaviour {    //小球需要做一个预制件拖进来    public Transform ball;     private void OnCollisionEnter2D(Collision2D collision)//被小球碰到时调用    {        //获取小球transform组件        Transform tf = collision.transform;        //在小球位置复制一个新小球(小球预制件)        Transform newBall = Instantiate(ball, tf.position,tf.rotation);        //新小球认小球的父物体“Balls”为自己的父物体        newBall.parent = tf.parent;        //新小球往右跳             newBall.GetComponent<Rigidbody2D>().AddForce(transform.right * 0.02f);        //旧小球往左跳        tf.GetComponent<Rigidbody2D>().AddForce(-transform.right * 0.02f);        //销毁复制道具        Destroy(gameObject);     }}


步骤全部完毕,代码中一些参数可随个人喜好随意设定,如预制件生成几率,瞄准发射范围,枪口位置,关卡格子布局等等。


游戏基本就做好啦。我们来看一下运行的效果:

下面是工程链接:

wushupei/PhysicalBallgithub.com图标

最后推荐一波,想学习游戏开发的同学,欢迎来levelpp.com/围观哦。




来源网址:https://zhuanlan.zhihu.com/p/37589814

鲜花

握手

雷人

路过

鸡蛋