Unity快速上手教程(三):《反应堆》实例开发

作者:四五二十 2019-01-17 4k



前文:
Unity快速上手教程(一):拉方块
Unity快速上手系列之2:2D物理弹球

《反应堆》(STACK)是手机上一款独特而又中毒性超强的游戏。

游戏的规则极其简单,就是将板子一层一层的往上堆,堆的过程中需要尽量与下层板重合,没重合的部分就会被削掉,随着失误越来越多,堆积的面积越来越小,最终game over。

如果能连续做到完美重合,音效会和谐地逐步升高音阶,让人从生理上欲罢不能。

今天我们就来实现一下这款游戏。开局一堆图,先看看效果:

方块在塔顶上方来回移动


通过玩家控制让方块在适当时机落在塔顶,出去的部分将被切割,塔的高度+1,顶面积会越来越小

中途可以随时按暂停,方块将变成一个重启按钮供玩家点击(文本写错了,请不要在意)

方块落在塔顶之外则游戏结束。大侠请重新来过

通过以上几张效果图,大概可以看出来这个游戏的主要功能:

--方块移动:方块会轮流从两边轮流来回移动;

--方块切割:方块落在底座上时,出去的部分会被切割掉,落上去的部分会成为新的塔顶;

--重置方块:每次落下后重置方块的尺寸位置,尺寸和塔顶相同,位置比塔顶高一个身位;

--镜头跟随:相机随时跟随最上层底座;

--颜色渐变:方块的颜色会随机往某个方向渐变;

--底座销毁:游戏结束时所有底座会从上往下被销毁;

大概知道了流程后就开动起来吧。

场景搭建

一、创建一个Cube作为底座,位置放在(0,0,0),缩放尺寸设为(5,0.5,5),取名为TopFloor,因为目前只有它一层,所以它就是顶层;


尺寸可以自定义

二、创建一个空物体Pedestals来管理运行后的所有底座:

将来Pedestals可以挂上底座管理的脚本

三、创建分数Text:


四、创建移动方块MoveCube,尺寸和位置无需设置,代码里面再来初始化,再创建一个3DText取名Texter,做方块的子物体,调整好位置:

3DText的创建: Hierarchy面板点击右键==>3DObject==>3DText


Texter创建后设为禁用状态

预制体导入:游戏中产生的各种粒子特效和方块切割下来的边角料都是做的预制体

撞击特效:AT_Field

爆炸特效:Boom

诞生特效:NaissanceEffect

落下特效:SpecialEffect

边角料预制体:Cffcut

粒子特效可以自定义

代码编写

一.初始化:

游戏开始的时候,需要初始化移动方块MoveCube的位置和大小,不仅如此,在效果图里也看到,每次方块底座往上叠加一层,移动方块位置都要重置一下,而每次重置后都比顶层底座高一层。

就是说,初始化移动方块是少不了顶层底座的信息的,想到这代码也就不难写了:

GameObject top; //声明顶层
void Initialization() //初始化移动方块
    {
        //获取顶层,及顶层的位置
        top = GameObject.Find("TopFloor");
        Vector3 pos = top.transform.position;
        //将顶层的大小赋予自身
        transform.localScale = top.transform.localScale;
        size = transform.localScale; //记录自身大小
        //初始化位置
        transform.position = new Vector3(pos.x, pos.y + size.y, pos.z - 10);
    }

上面的方法是用查找名字TopFloor的方法寻找顶层。在之后的代码中如果每次产生新的顶层,就把新顶层的名字改为TopFloor,而原先的顶层改为其它名字,以保证查找的准确性。

二.方块平移

方块初始化之后,接下来就是往前走。

在游戏中方块是以前后来回移动的方式运动,如果用脚本来实现,我们可以在每次初始化后记录一下当前位置为初始位置,判断当方块往前走到一定距离,则往回走,走到初始位置后,又往前走,如此循环。可以使用一个Bool值来决定方块的前后移动状态:

    Vector3 initialPos; //初始位置(诞生点)
    bool forward; //是否往前走
    public float speed; //平移速度
    void TranslationFun() //平移方法
    {
            if (forward) //如果确定往前走
            {
                transform.Translate(0, 0, Time.deltaTime * speed); //前进
                if (Mathf.Abs(transform.position.z - initialPos.z) >= 20) //前进超过20m
                    forward = !forward; //变为后退状态                    
            }
            else //如果往后退
            {
                transform.Translate(0, 0, -Time.deltaTime * speed); //后退
                if (transform.position.z <= initialPos.z)  //如果退回到初始位置
                    forward = !forward; //又往前走
            }
        }
    }

每次初始化时可以记录一次初始位置,将forward状态定为True。

三.方块叠加

本游戏的核心玩法就是当移动方块经过顶层底座上方时,玩家需要在适当时机让方块尽可能地与完整得落在顶层方块上,多出去的将会被削掉,而如果移动方块落在顶层之外,则游戏直接结束:


void FallingFun() //落下方法
    {
        //z轴偏差大于自身z轴尺寸,说明没落在顶层上
        if (Mathf.Abs(transform.position.z - top.transform.position.z) >= size.z )
         {
             //游戏结束
         }
        else //否则就视为成功落在顶层底座上
        {
            //生成新顶层和边角料
            //将旧顶层的名字转移给新顶层
            //移动方块会根据新顶层的位置初始化一次        
        }
    }

上面代码中的游戏结束方法之后再来做。

四.方块切割(重点)

1.在游戏中方块成功落下时,看上去的表现是被“切割”了,其实我在这里的实现中采用了一个曲线救国的方法。

我重新创建了两个方块。一个叠加在顶层成为新顶层,另一个则成为边角料被处理,生成这两个方块的位置和大小则根据移动方块落下时与顶层方块产生的偏移量来确定。

采用这种方法新方块的生成又分为两种情况,请看下面两张图:

移动方块z轴坐标大于顶层方块z轴坐标时

移动方块z轴坐标小于顶层方块z轴坐标时

2.我们就拿第一种情况用一张图来分析两个新方块生成时的位置与大小:

第二种情况也可以参考此图

3.通过画图分析,我们的代码可以这样来写:

//生成新顶层和边角料(方法参数为顶层和移动方块)
    public GameObject CreateNewFoundationAndCffcut(GameObject top, GameObject moveCube)
    {
        //获取顶层位置
        Vector3 tPos = top.transform.position;
        //获取顶层大小
        Vector3 tSize = top.transform.localScale;
        //获取移动方块位置
        Vector3 mPos = moveCube.transform.position;

        //创建一个Cube做新顶层
        GameObject newTop = GameObject.CreatePrimitive(PrimitiveType.Cube);
        //加载一个边角料预制件
        GameObject cffcut = Instantiate(Resources.Load("Prefab/Cffcut") as GameObject);
        //获取移动方块与原顶层z轴位置偏差
        float z_Offset = Mathf.Abs(mPos.z - tPos.z);
        //新顶层大小(原顶层-偏差)
        newTop.transform.localScale = new Vector3(Mathf.Abs(tSize.x - x_Offset), tSize.y, Mathf.Abs(tSize.z - z_Offset));
        //新顶层位置和边角料大小及位置(受位置状态影响)

        //边角料大小(原顶层x值,原顶层厚度,z轴偏差值)
        cffcut.transform.localScale = new Vector3(tSize.x, tSize.y, z_Offset);
        if (mPos.z < tPos.z) //当移动方块z轴值小于原顶层z轴时
        {
             //新顶层位置(原顶层x值, 原顶层y值 + 原顶层厚度, 原顶层z值 - 偏差值/2)
             newTop.transform.position = new Vector3(tPos.x, tPos.y + tSize.y, tPos.z - z_Offset / 2);
             //边角料位置(原顶层x值,原顶层y值 + 原顶层厚度,原顶层z值-(原顶层z值/2+偏差值/2))
             cffcut.transform.position = new Vector3(tPos.x, tPos.y + tSize.y, tPos.z - (tSize.z / 2 + z_Offset / 2));
         }
         else//否则,当移动方块z轴值大于等于原顶层z轴时
         {
             //新底座位置(原顶层x值, 原顶层y值 + 原顶层厚度, 原顶层z值 + 偏差值/2)
             newTop.transform.position = new Vector3(tPos.x, tPos.y + tSize.y, tPos.z + z_Offset / 2);
             //边角料位置(原顶层x值,原顶层y值 + 原顶层厚度,原顶层z值+(原顶层z值/2+偏差值/2))
             cffcut.transform.position = new Vector3(tPos.x, tPos.y + tSize.y, tPos.z + (tSize.z / 2 + z_Offset / 2));
          }
        }
        return newTop; //返回新顶层
    }

因为边角料创建后即刻就会被销毁(销毁方法我们在后面来解决),所以我们把新顶层作为返回值。返回后的新顶层可以重新取名为TopFloor,方便移动方块初始化,这个方法可以在方块成功落下时调用。

五.边角料销毁

上面我们讲到边角料创建后就会被销毁(真杯具),那么我们可以专门为它写一个脚本挂在边角料预制体上:

// 挂边角料预制件上
public class DeleteCffcut : MonoBehaviour
{
    void Update()
    {
        Color a = GetComponent<MeshRenderer>().material.color; //获取颜色
        a.a -= Time.deltaTime * 0.5f; //a值逐渐减小
        //将改变后的颜色赋值给自身(颜色逐渐透明)
        GetComponent<MeshRenderer>().material.color = a;
        if (a.a < 0) //当透明度小到一定时候
            Destroy(gameObject); //销毁自身
    }
}

六.计分

之前我们搭建场景是创建了分数Text,分数的统计很简单,只要方块成功叠加一次,分数加一即可:

    void AddScore(Color cubeColor) //加分方法
    {
        //根据名字获取分数对象转成整数类型,+1后再转成字符串类型赋予自身
        Text score = GameObject.Find("Scroe").GetComponent<Text>();
        score.text = (System.Convert.ToInt32(score.text) + 1).ToString();
    }

计分方法和新顶层的创建应该是同步的,我们可以把两个方法放到一起去执行。

七.摄像机跟随

1.看效果图,方块每次叠加摄像机会跟着往上走一层,其实摄像机要做的工作很简单,时刻关注名叫TopFloor(顶层底座)的物体即可,因为TopFloor随时在易主,即每次产生新的顶层,新顶层就会将TopFloor的名字赋予自身,感觉摄像机就像一个势利眼一样,摄像机我们也为他单独写一个脚本:

//挂摄像机上,时刻盯紧顶层
public class CameraFollow : MonoBehaviour
{
    public float height; //摄像机与顶层相对高度
    public float followSpeed; //摄像机跟随顶层速度  
    void Update()
    {
        if (GameObject.Find("TopFloor")) //如果场景中有该物体,始终跟随
        {
            //获取顶层
            Transform topFloor = GameObject.Find("TopFloor").transform;
            //摄像机始终看向顶层方向
            Quaternion dir = Quaternion.LookRotation(topFloor.position - transform.position);
            transform.rotation = Quaternion.Lerp(transform.rotation, dir, Time.deltaTime * 5);
            //摄像机跟随顶层高度
            Vector3 cameraPos = transform.position; //获取摄像机坐标
            //摄像机时刻比顶层高出一定高度(跟着顶层上升)
            cameraPos.y = topFloor.position.y + height;
            //摄像机渐变到指定高度           
            transform.position = Vector3.Lerp(transform.position, cameraPos, Time.deltaTime * followSpeed);         
        }
    }
}
做完上面的工作,游戏的核心逻辑部分基本就完成了,剩下的工作就看个人喜好了。

八.摄像机旋转缩放

由于本人觉得摄像机的功能不应该止于此,所以又顺便给它丰富一下,让它可以绕顶层Y轴旋转,多角度进行游戏,还增加了镜头缩放的功能:

            //按住鼠标右键移动进行绕顶层旋转
            if (Input.GetMouseButton(1))
            {
                float h = Input.GetAxis("Mouse X");//获取鼠标水平移动
                transform.RotateAround(topFloor.position, Vector3.up, h * Time.deltaTime * rotateSpeed);
            }
            //使用鼠标滑条拉近拉远镜头
            float slider = Input.GetAxis("Mouse ScrollWheel"); //获取滑条滚动信息
            GetComponent<Camera>().fieldOfView -= slider * Time.deltaTime * zoomSpeed;

把这几段代码放到摄像机脚本的Update的if条件下,毫无违和感。

九.结束游戏

当移动方块没有成功落在顶层底座上时,游戏结束。

游戏结束的方法其实有很多,比如直接显示游戏结束的字幕。我这里脑洞大开地采取了让所有底座爆炸的方式。


具体的思路,是创建一个集合来管理所有底座。游戏开始时,就将第一个底座添加进集合,在游戏过程中,每创建一个新顶层(新底座),就将新顶层添加进集合中,当游戏结束时,让这个集合内的底座从后向前(游戏场景中从上往下)依次原地爆炸,相关代码如下:

// 底座管理类,挂Pedestals上
public class PedestalManage : MonoBehaviour
{
    public List<Transform> pedestals = new List<Transform>();//创建一个集合用来管理所有底座
    float timer = 0; //创建计时器
    void Start()
    {
        pedestals.Add(GameObject.Find("TopFloor").transform);//首先将初始顶层底座添加进集合      
    }
    public void AllPedestalBoom() //所有底座爆炸
    {
        //开始计时
        timer += Time.deltaTime;
        if (timer > 0.05f) //每0.05秒炸掉一层
            if (pedestals.Count > 0) //如果集合内不为空
            {
                Transform top = pedestals[pedestals.Count - 1]; //获取顶层(集合最后一位)
                if (pedestals.Count > 1) //如果底座有两层以上
                    pedestals[pedestals.Count - 2].name = top.name;//把顶层名字给下一层
                //将顶层从集合中移除并销毁
                pedestals.Remove(top);
                Destroy(top.gameObject);
                timer = 0; //计时清零
            }
    }
}

之前创建初始底座时就有一个叫Pedestals的空物体来管理它们,此脚本就可以挂在Pedestals上,在游戏中每次创建了新的底座,别忘了把它添加进集合中。

十.颜色渐变

1.每次方块成功叠加后,移动方块的颜色会逐渐变化,本人在刚开始构思的时候也有点懵,经过一番推敲,最后决定用下面的方法来实现:

2.先说思路,初始状态时方块为白色,是因为初始化时移动方块的RGB值都为最大值:

初始化时移动方块的RGB值都为最大值,所以初始状态为白色


减少R值的结果

减少G值的结果

4.为了实现颜色转化的随机性,需要随机抽取一个值进行递减,当它减为0时,再在剩下两个值中随机挑一个递减,直到剩下的那个。我的实现方法是,把RGB三个值放进一个数组中,然后打乱数组索引顺序:

float[] rgb = { 1, 1, 1 }; //声明rgb颜色数组(在代码中RGB值在0~1之间)
int[] rgbIndex = { 0, 1, 2 }; //创建颜色数组的索引也放在一个数组中         
int[] RandomIndex() //将数组内元素顺序随机打乱
    {
        //创建一个集合1,将索引数组的值给它
        List<int> list1 = new List<int>();
        for (int i = 0; i < rgbIndex.Length; i++)
        {
            list1.Add(rgbIndex);
        }
        //再创建一个集合2,把集合1的元素顺序随机打乱赋给集合2
        List<int> list2 = new List<int>();
        for (int i = list1.Count; i > 0; i--)
        {
            int index;
            index = Random.Range(0, i);
            list2.Add(list1[index]);
            list1.Remove(list1[index]);
        }
        //将打乱后的索引成员重新赋值给索引数组
        return list2.ToArray();
    }

此方法需要在脚本开始时就打乱一次颜色索引数组的索引顺序,一开始就处于随机状态。

5.使用打乱后的索引在颜色数组rgb中找出相应的值,依次进行递减,当三个值都为0时,颜色为黑色:


6.当判断三个值都为0时,又依次递加,递加也采用同样的随机方式,递加满了再递减:

bool reduce = true; //颜色rgb递减
void ColorGradualChange() //颜色渐变
    {
        //逐渐减少r,g,b的值
        if (reduce)
        {
            if (rgb[rgbIndex[0]] > 0)
                rgb[rgbIndex[0]] -= 0.1f;
            else if (rgb[rgbIndex[1]] > 0)
                rgb[rgbIndex[1]] -= 0.1f;
            else if (rgb[rgbIndex[2]] > 0)
                rgb[rgbIndex[2]] -= 0.1f;
            else //当三个值都小于0时,变为逐渐增加
            {
                reduce = !reduce; //改为逐渐增加
                rgbIndex = RandomIndex(); //再次打乱索引顺序
            }
        }
        else //逐渐增加r,g,b的值
        {
            if (rgb[rgbIndex[0]] < 1)
                rgb[rgbIndex[0]] += 0.1f;
            else if (rgb[rgbIndex[1]] < 1)
                rgb[rgbIndex[1]] += 0.1f;
            else if (rgb[rgbIndex[2]] < 1)
                rgb[rgbIndex[2]] += 0.1f;
            else //当三个值都大于1时,变为逐渐减少
            {
                reduce = !reduce; //改为逐渐减少
                rgbIndex = RandomIndex(); //再次打乱索引顺序
            }
        }
    }

这个方法也是方块叠加一次调用一次,所以要放在恰当位置,比如和加分方法放一起也是可以的。

十一.移动方块飞向镜头

看起来很炫酷,实现起来却很简单,我们假想摄像机前有一个方块,颜色为白色,坐标为***,尺寸为***(可以通过后期自己调试决定实际坐标大小),有了这个方块之后只需要将移动方块MoveCube的坐标大小及颜色变换成该方块就行了:

//由于并没此方块,所以方法参数都靠自己编(参数有位置,尺寸,旋转,颜色)
void Transformation(Vector3 pos, Vector3 local, Vector3 euler, Color color) //变换
    {
        //移动方块往该处飞行转换
        transform.position = Vector3.Lerp(transform.position, pos, 0.1f);
        //渐变为该尺寸
        transform.localScale = Vector3.Lerp(transform.localScale, local, 0.1f);
        //渐变为该旋转
        transform.eulerAngles = Vector3.Lerp(transform.eulerAngles, euler, 0.1f);
        //渐变为该颜色
        GetComponent<MeshRenderer>().material.color = Color.Lerp(GetComponent<MeshRenderer>().material.color, color, 0.1f);
        //如果接近目的地
        if ((transform.position - pos).magnitude < 0.1f)
        {
            //确定位置尺寸旋转和颜色
            transform.position = pos;
            transform.localScale = local;
            transform.eulerAngles = euler;
            GetComponent<MeshRenderer>().material.color = color;
        }
    }

此方法在游戏进行时调用,通过状态机决定往摄像机处转换或转换回原处。如果想实现转换回原处的操作,一定要在飞向摄像机时,记录好初始信息,才能复原成原状态,在移动方块上的文字也随转换状态启用和禁用。

总结

游戏的主要逻辑已经写完,剩下还有一些完善工作,比如特效,特效的生成时机位置方式可以按照个人喜好进行。

方块的移动状态,我做了z轴和x轴两种移动状态,可通过状态机实现,不一定要将状态限制为两种。

还有就是背景,做天空盒是一种方式,我是用的另一个摄像机,放了一堆粒子特效在面前做成背景,只渲染背景不渲染游戏物体,主摄像机则相反,两个摄像机一起工作。

附上完整工程代码地址:

http://link.zhihu.com/?target=ht ... i/Rainbow-Tower.git

有想进一步系统地学习游戏开发的,欢迎到http://levelpp.com/强势围观。

OK,本篇文章就到这里,接下来我打算写一系列商业游戏中会用得较多的一些内容,敬请期待。

知乎专栏:https://zhuanlan.zhihu.com/p/38359628

最新评论
暂无评论
参与评论

商务合作 查看更多

独立游戏 查看更多