本篇完成游戏菜单界面,场景中呈现所有的游戏选择界面,激活相应的游戏界面进入游戏场景,这里面使用到了VRStandardAssets.Menu中的一些脚本。下面用到的时候会写出注释,场景如下图:
一、实现功能
- 游戏选择界面的动画播放
当视线准心选择界面的时候,界面会向玩家弹出一段距离,并播放该游戏的介绍动画,视线移开时,停止播放,界面回到原来位置。 - 选择条的跟随,填充和场景的进入
选择条会动态的移动到当前准心的游戏界面上,并且在进入过程中会进行填充,填充完毕后开始相应的游戏。
二、步骤
1. UI的制作
游戏菜单的UI分为3块:BackGround,Menu,Selector。
下面开始一步一步制作,首先是BackGround,这里需要注意的是:3D UI的呈现是通过MeshFilter和MeshRenderer这两个组件,MeshFilter加载Mesh,MeshRenderer呈现Mesh;
1.1 BackGround
首先创建一个空对象,命名为BackGround,作为所有背景元素的父对象存在:
MenuBg和上一篇中一样(注意环境光的设置),这里就不在叙述直接进入MenuInfo步骤,创建一个空对象作为BackGround的子对象,同时加入MeshFilter和MeshRenderer这两个组件,同时添加上如下图所示的Mesh和Materials:
完成之后呈现如下图:
MenuLogo在前面,MenuElements在后面,旋转一下就可看到了。
1.2 Menu
同样的,在Menu中也包含了两个子元素:Fly和Shooter360,按照如下图的层级关系建立空对象:
完成后,在ItemFly和FlyDescription上分别添加MeshFilter和MeshRenderer组件,如下图:
这里需要注意的是,这个ItemFly是一个可交互项,所以添加一个Mesh Collider组件来响应射线的碰撞检测:
Shooter360中的设置如下图:
完成之后,呈现如下图:
1.3 Selector
Selector的设置类似,如下图:
完成设置后,游戏菜单的UI部分就完成,呈现如下图:
2.界面功能实现
在VR-Sample中所有的菜单功能在VRStandardAssets.Menu这个命名空间下。
2.1 界面的弹出功能
当准心移动到游戏界面时,界面会弹出一段距离,提醒玩家当前选择的是该游戏,使用到的是MenuItemPopout 脚本,代码的注释如下:
public class MenuItemPopout : MonoBehaviour
{
//控制弹出的Transform组件,用来改变位置
[SerializeField] private Transform m_Transform;
//弹出的交互项
[SerializeField] private VRInteractiveItem m_Item;
//弹出的速度
[SerializeField] private float m_PopSpeed = 8f;
//弹出的距离
[SerializeField] private float m_PopDistance = 0.5f;
//交互项的原始位置
private Vector3 m_StartPosition;
//交互项需要弹出到的目标位置
private Vector3 m_PoppedPosition;
//当前需要移动到的位置(被选中时为目标位置m_PoppedPosition,未被选中时为原始位置m_StartPosition)
private Vector3 m_TargetPosition;
private void Start ()
{
// 开始运行时保存交互项的初始位置
m_StartPosition = m_Transform.position;
// 保存目标位置
m_PoppedPosition = m_Transform.position - m_Transform.forward * m_PopDistance;
}
private void Update ()
{
// 确定当前需要移动到的位置,被选中为m_PoppedPosition,没有被选中为m_StartPosition
m_TargetPosition = m_Item.IsOver ? m_PoppedPosition : m_StartPosition;
// MoveTowards方法移动位置
m_Transform.position = Vector3.MoveTowards(m_Transform.position, m_TargetPosition, m_PopSpeed * Time.deltaTime);
}
}
完成后,在ItemFly上挂载VRInteractiveItem和MenuItemPopout 这两个脚本,设置如下:
同样,ItemShooter360设置和ItemFly一致,设置完成后,当准心移动到Fly界面时,界面会弹出一段距离。
2.2 界面动画播放
完成弹出功能后,继续进行动画播放的功能实现,大致思路为:将一组连续的图片纹理快速的替换到MeshRenderer的Material中,达到动画播放的效果:
使用到的脚本为MenuAnimator ,注释如下:
public class MenuAnimator : MonoBehaviour
{
//每秒图片纹理改变的速度
[SerializeField] private int m_FrameRate = 30;
//需要做呈现的MeshRenderer
[SerializeField] private MeshRenderer m_ScreenMesh;
//当前的交互项
[SerializeField] private VRInteractiveItem m_VRInteractiveItem;
//保存图片的数组
[SerializeField] private Texture[] m_AnimTextures;
//协程方法的等待间隔
private WaitForSeconds m_FrameRateWait;
//当前图片纹理的序号
private int m_CurrentTextureIndex;
//是否播放的标志
private bool m_Playing;
private void Awake ()
{
// 新建一个WaitForSeconds对象
m_FrameRateWait = new WaitForSeconds (1f / m_FrameRate);
}
private void OnEnable ()
{
//订阅交互项的OnOver和OnOut事件
m_VRInteractiveItem.OnOver += HandleOver;
m_VRInteractiveItem.OnOut += HandleOut;
}
//取消订阅交互项的OnOver和OnOut事件
private void OnDisable ()
{
m_VRInteractiveItem.OnOver -= HandleOver;
m_VRInteractiveItem.OnOut -= HandleOut;
}
private void HandleOver ()
{
m_Playing = true;
StartCoroutine (PlayTextures ());
}
private void HandleOut ()
{
m_Playing = false;
}
private IEnumerator PlayTextures ()
{
// 当被准心选中时,这是一个死循环,动画会一直播放
while (m_Playing)
{
// 修改m_ScreenMesh材质中的图片纹理为第m_CurrentTextureIndex个
m_ScreenMesh.material.mainTexture = m_AnimTextures[m_CurrentTextureIndex];
// m_CurrentTextureIndex自增,当到达最后一个时又从0开始
m_CurrentTextureIndex = (m_CurrentTextureIndex + 1) % m_AnimTextures.Length;
// 等待一个m_FrameRateWait在执行
yield return m_FrameRateWait;
}
}
}
完成后,将脚本挂载到ItemFly上,设置如下:
在ItemShooter360上也做同样的设置,完成后,当准心移动到Fly界面上时,播放相应的动画。
2.3选择条跟随移动和弹出
当准心移动到某个游戏界面时,界面下方的选择条会跟随移动,弹出提示当前的选择界面,这个功能使用到的是MenuSelectorMover脚本,值得一提的是,这里是通过父对象来控制旋转,子对象(即当前的交互项来控制移动),注释如下:
public class MenuSelectorMover : MonoBehaviour
{
//弹出的速度
[SerializeField] private float m_PopSpeed = 8f;
//弹出的距离
[SerializeField] private float m_PopDistance = 0.5f;
//跟随移动的速度
[SerializeField] private float m_MoveSpeed = 7f;
//父对象的Transform,用来控制Rotation移动
[SerializeField] private Transform m_ParentTransform;
//子对象的Transform,用来控制自身的弹出
[SerializeField] private Transform m_ChildTransform;
//场景中的所有界面交互项数组
[SerializeField] private VRInteractiveItem[] m_Items;
//旋转到的目标位置
private Quaternion m_TargetRotation;
//弹出的初始位置
private Vector3 m_StartPosition;
//弹出的目标位置
private Vector3 m_PoppedPosition;
//当前需要到达的位置
private Vector3 m_TargetPosition;
void Awake ()
{
// 保存初始位置
m_StartPosition = m_ChildTransform.localPosition;
// 保存需要弹出到的位置
m_PoppedPosition = m_ChildTransform.localPosition - Vector3.forward * m_PopDistance;
}
void Update ()
{
// 选择条默认位置为初始位置
m_TargetPosition = m_StartPosition;
// 遍历交互项数组,如果有交互项被选中,则将m_TargetPosition位置修改,没有则不变
for (int i = 0; i < m_Items.Length; i++)
{
//判断是否有交互项被选中
if (!m_Items[i].IsOver)
continue;
//设置目标位置的Rotation
m_TargetRotation = m_Items[i].transform.rotation;
//设置目标位置的Postion为m_PoppedPosition
m_TargetPosition = m_PoppedPosition;
break;
}
// 使用Vector3.MoveTowards方法控制跟随移动
m_ChildTransform.localPosition = Vector3.MoveTowards (m_ChildTransform.localPosition, m_TargetPosition,
m_PopSpeed * Time.deltaTime);
//使用Quaternion.Slerp方法控制旋转
m_ParentTransform.rotation = Quaternion.Slerp(m_ParentTransform.rotation, m_TargetRotation, m_MoveSpeed * Time.deltaTime);
}
完成后,挂载到Selector上,设置如下:
运行后,选择条就可以跟随当前的交互项而移动了。
2.4准心和选择条的填充以及进入相应场景功能
-
进度条的填充
这个功能使用到是SelectionSlider脚本,这个脚本用来控制Slider的填充,设置如下:
这里需要注意的是,这个脚本可以做2D或者3D的填充,2D使用Slider字段,3D使用Renderer字段:
- 准心填充以及进入下一个场景
这里使用到了MenuButton脚本,里面控制了准心背景的显示,以及进入下一个场景两个功能,脚本注释如下:
public class MenuButton : MonoBehaviour
{
//这个事件在当选中的 MenuButton进度条完成后执行
public event Action OnButtonSelected;
//下一个场景的名字
[SerializeField] private string m_SceneToLoad;
//摄像机的淡出脚本,进入下一个场景时需要进行摄像机的淡出动作
[SerializeField] private VRCameraFade m_CameraFade;
//准心的背景控制脚本,需要控制背景的显示和隐藏以及填充完毕时订阅相应的事件
[SerializeField] private SelectionRadial m_SelectionRadial;
//当前的交互项
[SerializeField] private VRInteractiveItem m_InteractiveItem;
//准心是否移入
private bool m_GazeOver;
//事件的订阅
private void OnEnable ()
{
m_InteractiveItem.OnOver += HandleOver;
m_InteractiveItem.OnOut += HandleOut;
m_SelectionRadial.OnSelectionComplete += HandleSelectionComplete;
}
//取消事件订阅
private void OnDisable ()
{
m_InteractiveItem.OnOver -= HandleOver;
m_InteractiveItem.OnOut -= HandleOut;
m_SelectionRadial.OnSelectionComplete -= HandleSelectionComplete;
}
//准心移入时执行的方法
private void HandleOver()
{
//准心背景显示
m_SelectionRadial.Show();
m_GazeOver = true;
}
//准心移出时执行的方法
private void HandleOut()
{
// 准心背景隐藏
m_SelectionRadial.Hide();
m_GazeOver = false;
}
//当准心背景填充完毕时执行的方法
private void HandleSelectionComplete()
{
//准心在当前交互项时
if(m_GazeOver)
StartCoroutine (ActivateButton());
}
private IEnumerator ActivateButton()
{
// 当摄像机正在渐入时,不执行
if (m_CameraFade.IsFading)
yield break;
//当事件被订阅时执行
if (OnButtonSelected != null)
OnButtonSelected(this);
// 开始摄像机渐出的动作.
yield return StartCoroutine(m_CameraFade.BeginFadeOut(true));
// 载入下一个场景
SceneManager.LoadScene(m_SceneToLoad, LoadSceneMode.Single);
}
}
完成后,挂载在ItemFly上,设置如下:
同样的,在Shoter360上配置一样,这里完成后,就可以进行运行测试啦。
三.注意事项
1.MenuButton脚本中OnButtonSelected事件使用:
这个是一个Action事件,添加了一个MenuButton参数,在使用的时候把当前脚本this作为参数传递:
这样当其他地方订阅这个事件的时候,在Reticle填充完毕后,可以对这个MenuButtion进行一些操作,当然也可以做其他的操作不管这个参数。