Unity下落式音游实现——(2)滑块移动及生成
前期准备
导入资源及布置UI,创建脚本SliderController.cs、SceneController.cs、GameInformation.cs
鼓盘属于UI还是游戏对象?
毫无疑问,分数条、进度条等属于UI(给玩家展示信息);滑块属于游戏对象(频繁生成和销毁)问题就在于鼓盘,从游戏逻辑的角度,滑块需要在鼓盘附近作位置判断(hit or miss),当成游戏对象处理更方便;从功能看,鼓盘需要频繁播放动画。最后还是决定将鼓盘放在UI层,用SceneController当滑块和鼓盘交互的中介
思路
滑块移动速度受轨道的长度和音乐节奏影响,由于轨道长度不变而音乐节奏却可能会随着难度改变,因此主要用时间(而不是速度)去控制滑块的移速
通过在轨道上选取部分点进行插值,实现滑块移动。
Q:为什么不直接取起点和终点,只在这两个点进行插值移动?
A:会更简单,但是引入目标点数组会增强扩展性(曲线路径)
实现过程
首先在scene中手动布置轨道,每个轨道上分别设置三个点(起点、中点和终点)以模拟路径
在Hierarchy创建空物体Orbit,将刚才创建的所有路径点作为它的子物体
滑块移动
// SliderController.cs
public class SliderController : MonoBehaviour
{
private Transform[] target; // 目标点
private float step; // 移动步长
private float movingTime; // 滑块移动总时间(手动设置)
private int targetIndex = 1; // 滑块在起点生成,丢弃第一个点
// 设置移动目标数组(被SC调用)
void setTarget(Transform[] t)
{
target = t;
startTime = Time.time;
for (int i = 1; i < target.Length; ++i)
distanceToEnd += Vector3.Distance(target[i - 1].position, target[i].position);
}
// 设置总路程时间
void setMovingTime(float MovingTime)
{
movingTime = MovingTime;
step = Time.fixedDeltaTime * (distanceToEnd / movingTime);
}
private void FixedUpdate()
{
if (targetIndex < target.Length)
{
transform.position = Vector3.MoveTowards(transform.position, target[targetIndex].position, step);
// 注意浮点数比较
if (Vector3.Distance(transform.position, target[targetIndex].position) < 0.001f)
{
++targetIndex;
}
}
}
}
Q:为什么移动用MoveTowards?为什么不用lerp?
A:MoveTowards是以step为步长,从起点往终点走,其实本质上也是lerp;用lerp也可以,只不过感觉没有MoveTowards顺手
float private t = .0f;
// 移动要放在FixedUpdate里
private void FixedUpdate()
{
if (targetIndex < target.Length)
{
transform.position = new Vector3(Mathf.Lerp(target[targetIndex-1].position, target[targetIndex].position, t), 0, 0);
// 改变大小和颜色,实现渐变
transform.localScale = Vector3.MoveTowards(transform.localScale, target[targetIndex].localScale, step);
GetComponent<SpriteRenderer>().color = Vector4.MoveTowards(GetComponent<SpriteRenderer>().color, target[targetIndex].gameObject.GetComponent<SpriteRenderer>().color, step);
// 需要实时更新t
t += Time.fixedDeltaTime / movingTime;
}
}
生成滑块
将滑块打包成prefab(记得绑刚写的脚本),创建空物体SceneController,编写以下脚本
// SceneController.cs
public class SceneController : MonoBehaviour
{
[SerializeField] private GameObject Orbits;
[SerializeField] private GameObject singleSlider;
private Transform[] OrbitsPosition;
void Start()
{
// GetComponentsInChildren也会返回自己,要记得丢弃首元素
OrbitsPosition = Orbits.GetComponentsInChildren<Transform>();
}
void genSlider(int OrbitNum)
{
// 生成滑块
GameObject slider = Instantiate(singleSlider) as GameObject;
// 设置对应轨道信息
// GameInformation见下文
Transform[] tmp = new Transform[GameInformation.EachOrbitsliderNum]; // 加1是丢弃首元素空物体
for (int i = OrbitNum * GameInformation.EachOrbitsliderNum + 1, n = 0; n < GameInformation.EachOrbitsliderNum; ++n)
tmp[n] = OrbitsPosition[i++];
// 设置slider初始条件
slider.transform.position = tmp[0].position;
slider.GetComponent<SpriteRenderer>().color = tmp[0].gameObject.GetComponent<SpriteRenderer>().color;
slider.transform.localScale = tmp[0].localScale;
// 将对应轨道信息传给slider
slider.SendMessage("setTarget", tmp);
}
}
GameInformation是一个用静态类记录信息的脚本,不用挂到游戏物体上,因此不继承MonoBehaviour。设置该脚本的目的是方便更改一些在多个脚本中同时会用到的全局变量(比如后续经常改变的滑块movingTime)
// GameInformation.cs
public class SceneController : MonoBehaviour
{
public static class GameInformation
{
public const int EachOrbitsliderNum = 3; // 每条轨道上的预置体数量
}
// 不同难度下滑块移动的总时间
public static class SlideTime
{
public const float standardTime = 0.7f;
public const float Easy = 0.4f;
public const float Normal = 0.7f;
public const float Hard = 1;
}
// 记录关卡状态
public enum MissionStatus
{
show = 0,
game = 1
}
}
注意到新增加两个新东西,MissionStatus用于切换关卡状态(需求中的演示和游戏两个状态)。需要在SliderController.cs和SenceController.cs两个脚本中添加成员变量
private MissionStatus status;
Q:为什么不直接用一个全局变量量记录MissionStatus,而是设置成枚举?
A:确实这样更好,省去了大多数游戏对象都要拥有一份MissionStatus,但之前已经写成这样了,没办法。
SlideTime不同难度下滑块移动的总时间,现在可以对movingTime赋值了
// SliderController.cs
private float movingTime = SlideTime.standardTime / SlideTime.Easy;
总结
目前为止我们能在对应位置生成滑块并指定滑块的移动路径和移动速度(由时间控制)