是男人就下100层——Unity实现欢乐球球(下)链表对象池

作者:沈琰
本篇难度: ★★★
前言
大家好。
我,来填上期挖出来的坑了。摸鱼的日子过于舒服以至于差点就把更新给忘了。

进入正题,首先回顾下上期的内容。
上期传送门:是男人就下100层—Unity实现欢乐球球(上)Mesh生成
在上期的内容里我们只做了两件事:
1.用Mesh实现了自定义的3D物体作为接下来游戏内的素材。
2.用代码计算简易的模拟了小球的重力和反弹效果。
我们首先思考下还差哪些东西没做:
1.控制逻辑与游戏场景的生成复用。
2.分数的UI显示与计算。
3.一些提升游戏表现力的特效。
其中前两项都是归属于游戏逻辑的的范畴,并且也是游戏的核心,所以我们放到一起实现。
本篇同样会涉及到意想不到的知识点:用链表做对象池。

控制逻辑
原版游戏是用手指滑动手机屏幕让整个场景转动,同样的,我们可以用鼠标拖动屏幕来控制场
景转动,用代码去实现其实非常简单,每次Update获取鼠标这一帧和上一帧坐标的X值的变
化,用这个值计算旋转。
using System.Collections;
using UnityEngine;
public class InputLogic : MonoBehaviour
{
//记录上一帧鼠标的位置
Vector3 lastMousePos;
void TouchRotate()
{
//鼠标左键按下时转动
if (Input.GetMouseButton(0))
{
//计算这一帧鼠标位置与上一帧的差值,然后以Y轴转动
float moveX = (Input.mousePosition - lastMousePos).x;
transform.Rotate(0, -moveX, 0);
}
}
void Update ()
{
TouchRotate();
lastMousePos = Input.mousePosition;
}
}
由于我们只想让场景内物体横着转,所以计算出来的旋转幅度后用这个值沿着Y轴旋转。把脚本
挂在一个空节点上,场景内所有需要旋转的物体挂到空节点下,则其下所有子物体都会跟着空
节点一起旋转。

场景的生成与复用
上期内容中,我们已经能做到给定一个弧度,生成一个缺口大小与之对应的圆环了:

现在要做的就是在小球下落的过程中每隔一定高度就生成一个随机弧度的圆环。但是为性能考
虑我们肯定不能无限制的生成,得在已经生成的环移动到摄像机的视野之外时重新拿来复用。
应该已经有聪明的同学想到可以写一个对象池来实现这个功能,在小球与环的距离大于一个阈
值的时候把圆环加入对象池中,然后每次生成新的环时到对象池里面去取。
这个做法当没毛病,并且通常也是这么做的。但我们今天用个更简洁直观的方法,用一个
链表来实现这个功能。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameMode : MonoBehaviour
{
public Circle PrefabCircle;
public Transform Root;
LinkedList<Circle> CircleQueue;
LinkedListNode<Circle> curNode;
public float gap = 8.0f;
float lowestCircleY;
Transform cam;
Transform pillar;
Ball ball;
void Start ()
{
cam = Camera.main.transform;
pillar = GameObject.Find("Pillar").transform;
ball = GameObject.Find("ball").GetComponent<Ball>();
CircleQueue = new LinkedList<Circle>();
CircleQueue.AddLast(GetNewCircle());
curNode = CircleQueue.Last;
}
Circle GetNewCircle()
{
Circle circle = Instantiate(PrefabCircle, Root);
circle.Init();
return circle;
}
Circle GetNextCircle()
{
// 让链表循环使用的算法
LinkedListNode<Circle> next = curNode.Next;
if (next == null)
{
// 如果达到了结尾就回到开头
next = CircleQueue.First;
}
//如果圆环太高就隐藏等待复用
if (next.Value.transform.position.y > lowestCircleY + 2f * gap)
{
next.Value.gameObject.SetActive(false);
}
//如果链表的下一个环在场景中是隐藏的就返回这个环并在场景中显示
if (!next.Value.gameObject.activeInHierarchy)
{
curNode = next;
curNode.Value.gameObject.SetActive(true);
}
//如果下一个环在场景中显示,生成新的环并添加到链表next之前
else
{
curNode = CircleQueue.AddBefore(next, GetNewCircle());
}
return curNode.Value;
}
void Update ()
{
while (lowestCircleY + gap > ball.transform.position.y)
{
var circle = GetNextCircle();
//每次得到新的圆环时改变高度
circle.transform.position = new Vector3(0, lowestCircleY - gap);
circle.GenerateCircleByLevel();
lowestCircleY = circle.transform.position.y;
}
pillar.position = new Vector3(pillar.position.x, cam.position.y, pillar.position.z);
}
}
这段代码的作用就是使用一个链表来达到场景复用的目的,就像这样:

此处要注意的是链表并没有成为一个环形,我们只是在下一个节点为null的时候把返回值赋值为
链表的第一个元素而已。
然后我们让摄像机在球下落时跟着球一起移动,新建如下脚本挂载到主摄像机上。
public class ChaseBallCam : MonoBehaviour
{
Transform ball;
float baseY;
float camOffsetY;
void Start ()
{
ball = GameObject.FindGameObjectWithTag("Player").transform;
camOffsetY = ball.transform.position.y - transform.position.y;
}
void Update ()
{
//当球的位置低于偏移值时让摄像机和球一起动
float diffY = ball.transform.position.y - transform.position.y - camOffsetY;
if (diffY < 0)
{
// 比必要的位置再低一些,可以防止抖动
transform.position += new Vector3(0, diffY -0.15f, 0);
}
}
}
最后再把摄像机的Y轴坐标每一帧同步赋值给中间的圆柱,最后运行效果如下:
可以看到链表的总长度只有4个,但已足够实现场景内物体的循环,如此一来场景的基本结构
就搭好了。

分数的计算与UI显示
计算分数的算法最关键的问题,是如果连续通过多个缺口且没有触碰到圆环,那么获得的分数是
累加的。
这里的思路是用小球在圆环上反弹的时间来计算当次的下落获得的分数。在Unity里通过调用
Time.time能够获取从游戏开始到现在总共经过的时间。
我们在GameMode里每一次获取下一个圆环的时间,就是小球经过当前圆环的时间。所以我们
可以把计分函数放在生成圆环的循环里调用。在球的脚本里记录每一次反弹的时间,当最后一
次反弹的时间大于上一次计分的时间时,说明小球碰到了圆环,累加的计分需要重置。
void AddScore()
{
if (ball.lastBounceTime > lastAddScoreTime)
{
combo = 1;
}
else
{
combo++;
if (combo > 9) { combo = 9; }
}
lastAddScoreTime = Time.time;
var numObj = Instantiate(prefabNumber, canvas);
numObj.GetComponent<ScoreAnim>().SetNumber(combo);
totalScore += combo;
totalScoreText.text = string.Format("Score:{0}", totalScore);
}
然后在网上找一套数字的贴图资源,从1-9按数字修改一下图片的名字。

新建一个物体,在脚本里把图片存入一个list中。在子物体中添加Image组件,每次新建时通过
传入的分数来显示与之对应的图片,顺便加上一些透明渐变和移动的UI动画效果。别忘Image
组件只有在Canvas下才能正确的显示图片,所以场景中还需要新建一个Canvas。
sing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ScoreAnim : MonoBehaviour {
Image numImage;
public List<Sprite> nums;
int num;
void Start ()
{
numImage = transform.Find("Image").GetComponent<Image>();
numImage.sprite = nums[num % 10];
numImage.CrossFadeAlpha(0, 0.5f, false);
Destroy(gameObject, 1.0f);
}
public void SetNumber(int n)
{
this.num = n;
}
void Update ()
{
transform.Translate(new Vector3(0, 50*Time.deltaTime, 0));
}
}
最后在Canvas下新建一个Text组件显示总共获得的分数,在每次计分时更新分数。

游戏表现力的提升
到这里游戏的主体基本就完成了,但是我们还有活干,适当添加一些特效或者扩展功能让我们
的游戏显的更有趣一些。
比如可以在小球上加上一些特效:添加TrailRenderer组件实现尾迹,小球在圆环上反弹时
添加一张痕迹贴图,改变小球的大小显示反弹动画等等。

也可以仿照原版加入小球经过圆环时掉落的效果,添加障碍物的圆环让小球碰到就结束游戏,
顺便另外做个UI实现游戏的重新开始。这些都可以在前面的基础上稍微修改下实现,限于篇幅
就不详细展开说明,可在后面的工程链接中下载下来研究。
大家完全可以按照自己的喜好来做一些自己认为更有趣的改动。

结束
这个小游戏到这里就做完了,这个工程难度总体来说是略微高于入门工程的,但是就算是初学
者慢慢来也能完整的做出来。希望能通过这个工程帮助大家减少一些初学者常有的畏难情绪,
即便缺少素材,我们也能用代码去实现我们想要的功能。
本期文章工程地址:https://github.com/tank1018702/unity_002/tree/master/JumpBall
想系统学习游戏开发的童鞋,欢迎访问 http://levelpp.com/
游戏开发搅基QQ群:869551769
微信公众号:皮皮关