前言

   该文档为《Unity游戏开发文档(3):Dancing Line》的附属文档,亦可看作是单独的技术总结文档。

目录

  • 综述
  • 构建滚动菜单
  • 读取关卡信息
  • 填充菜单选项
  • 选项自动对齐
  • 选项自动调整大小
  • 随选项的变换切换示例音乐
  • 最终效果
  • 参考资料


综述

无论是在游戏中还是在其他应用程序中,我们都经常使用到 “滚动式菜单” 这一功能。它的泛用性很广,当我们有大量的UI组件,但又没办法一次性在屏幕里放下时,滚动式菜单的价值就体现出来了。

Dancing Line 中,我们希望用一个滚动式菜单来作为玩家选择关卡的载体。它需要具备以下几个功能:

  • 能够滚动地显示所有选项,用户能够通过鼠标滚轮,鼠标拖曳或是特定的UI交互件来滚动菜单。
  • 在任何时候都会有一个选项作为当前被选中的选项。当用户结束滚动后,菜单需要自动进行校准,使得离中心点最近的一个选项被设为选中的选项。
  • 被选中的选项的尺寸要比未被选中的选项的尺寸大。当一个新的选项被设为选中的选项后,它的尺寸需要被相应地放大,而被替代掉的选项则需要相应地缩小。
  • 用户点击被选中的选项后,将会跳转到对应的关卡中。

选项自动对齐

选项自动调整大小


构建滚动菜单

关于如何创建一个滚动菜单可参考下方的视频

Swipe Control with Touch for Menu Level: https://www.youtube.com/watch?v=GURPmGoAOoM


读取关卡信息

创建了滚动菜单后,我们需要在外部读取游戏所有的关卡,然后把关卡转换为滚动菜单中的选项,并替换掉原有的预设选项。

那么我们首先需要做的是读取外部的关卡信息。在 Dancing Line 中,我们在游戏文件的根目录下设置了一个 Maps 文件夹用以存放关卡,每个关卡会各自存放在以关卡名为文件夹名的文件夹下。每个关卡的文件夹下需要包含一个与关卡同名的 .csv 和一个与关卡同名的 .mp3 文件,这些是游戏运行的必备文件。所以关卡的文件目录应该如下:

Dancing Line|-- ...|-- Maps|    |-- Chinese Garden|   |       |-- Chinese Garden.csv|  |       |-- Chinese Garden.mp3|    ||    |-- Piano|    |     |-- Piano.csv|    |     |-- Piano.mp3|    ||    |-- Winter|    |     |-- Winter.csv|    |     |-- Winter.mp3|    ||

此外我们希望在玩家选择关卡的时候,游戏能为玩家播放每个关卡的示例音乐,以便让玩家对即将开始的关卡有一个预先的认知。因此我们在读取关卡时,也需要一并读取关卡的音乐文件。以下是读取关卡信息的实现代码:

private string project_path_;
private string maps_path_;
private string csv_path_;
private string mp3_path_;
private DirectoryInfo maps_info_;
private DirectoryInfo[] maps_list_;
private FileInfo csv_info_;
private FileInfo mp3_info_;public List<string> map_name_list;
public List<AudioClip> map_music_list;void Awake() {                                                      LoadAllMaps();                                                  // We must use "Awake()" rather than "Start()" since we need to load the game data file at the very beginning of the game.
}void LoadAllMaps() {project_path_ = System.Environment.CurrentDirectory;            // Get the directory of the game project filemaps_path_ = project_path_ + "/Maps";                           // Get the directory of the "maps" folderGlobalData.project_path = project_path_;GlobalData.maps_path = maps_path_;if (!Directory.Exists(maps_path_)) {                            // Create maps folder if it dose not exist yetDirectory.CreateDirectory(maps_path_);}maps_info_ = new DirectoryInfo(maps_path_);maps_list_ = maps_info_.GetDirectories();                       // Get all map folder names under the "map" folder into a listforeach (DirectoryInfo map_name in maps_list_) {csv_path_ = map_name + "/" + map_name.Name + ".csv";mp3_path_ = map_name + "/" + map_name.Name + ".mp3";csv_info_ = new FileInfo(csv_path_);mp3_info_ = new FileInfo(mp3_path_);if (csv_info_.Exists && mp3_info_.Exists) {                 // If the map folder does not contain both .csv file and .mp3 filemap_name_list.Add(map_name.Name);                       // it won't show up in the select menu}}StartCoroutine(LoadMusicFile(map_name_list));
}IEnumerator LoadMusicFile(List<string> map_name_list) {for (int i = 0; i < map_name_list.Count; i++) {string music_path = maps_path_ + "/" + map_name_list[i] + "/" + map_name_list[i] + ".mp3";       // Calculate the full music file path by given the file nameFileInfo music_info = new FileInfo(music_path);if (!music_info.Exists) {                                                       // If the targeted music file dose no exist in the folder,Debug.LogError("Music file does not exist!");                               // pop a message to warn user.yield break;}else {UnityWebRequest uwr = UnityWebRequestMultimedia.GetAudioClip(music_path, AudioType.MPEG);yield return uwr.SendWebRequest();                                          // Make sure the transmission is completedif (uwr.result == UnityWebRequest.Result.ConnectionError) {Debug.LogError(uwr.error);}else {map_music_list.Add(DownloadHandlerAudioClip.GetContent(uwr));           // Get audio clip from the transmission data}}}yield break;
}


填充菜单选项

在读取到所有关卡后,我们接下来需要把关卡制作成滚动菜单的选项,并用新的选项替换掉原有的预设选项。在这里很重要的一个步骤是我们需要对关卡选项进行编号,在后续实现选项自动对齐,选项自动调整大小,以及切换不同的示例音乐都需要依赖于我们设置的关卡编号。

不同于一般的编号是从 1 1 1 开始递增,上不设限。我们在此采用的是比例编号,第一个选项的编号为 0 0 0,最后一个选项的编号为 1 1 1,在这两个选项之间的编号则按比例地取其百分比值。举个例子,假如我们一共有五个选项,那么所有选项各自的编号应为:

| 选项1 | 0     |
| 选项2 | 0.25    |
| 选项3 | 0.50    |
| 选项4 | 0.75    |
| 选项5 | 1.00    |

在游戏中,每个选项都是游戏对象的形式存在,这样便于我们以后对每一个地图选项进行定制化处理。以下是填充菜单选项的实现代码:

  • 菜单控制脚本:
private float item_spacing_;
private float[] item_index_;
private List<string> map_name_list_;void Start() {ListItemInit();
}void ListItemInit() {map_name_list_ = file_controller_.map_name_list;item_index_ = new float[map_name_list_.Count];item_spacing_ = 1f / (map_name_list_.Count - 1f);                           // Calculate precentage indexfor (int i = 0; i < map_name_list_.Count; i++) {item_index_[i] = item_spacing_ * i;                                     // Set precentage index for each map itemGameObject map_item = Instantiate(item_template_) as GameObject;        map_item.transform.SetParent(this.transform, false);                    // Generate map item under the hierarchy of scroll view contentmap_item.GetComponent<MapItemController>().SetMapName(map_name_list_[i]);map_item.SetActive(true);}
}
  • 选项控制脚本
private Text map_title_;
private Button button;void Start() {button = transform.GetComponent<Button>();button.onClick.AddListener(LoadGameScene);
} public void SetMapName(string map_name) {map_title_.text = map_name;
}


选项自动对齐

完成了滚动菜单的选项填充后,我们接下来需要做的便是为选项添加自动对齐和自动调整大小的特效,首先是自动对齐的特效。

按照一般逻辑,当玩家对菜单进行了任何操作后(例如用鼠标拖动,鼠标滚轮滚动,拖动滚动轴,或是点击切换选项的UI交互件),程序都应该执行自动对齐操作。但由于上述的操作之间具有很强的耦合性,为每一种操作进行是否要触发自动对齐的判断会受到其他操作带来的干扰。因此在具体实现中,我们规定了在玩家每次点击UI交互件后会立即执行自动对齐,其他情况下游戏会每间隔一段时间执行一次自动对齐。

在上文中,我们用百分比例编号记录了每一个选项的索引。而菜单的滚动轴与菜单自身也是呈百分比例的关系,即滚动轴-滚动菜单的前端索引值为 0 0 0,滚动轴-滚动菜单的末端索引值为 1 1 1,中间任意位置的索引值为自己在菜单位置的百分比值。因此选项的索引值可以与滚动轴的索引值组成一个一一对应的映射关系。我们把滚动轴的索引值设为某个选项的索引值,便可把菜单调整到与那个选项对齐。

我们需要在每一帧计算出当前被激活的选项。在执行自动对齐操作时,程序会首先检查当前滚动轴的索引值与激活选项的索引值是否一致,如果不一致,那么我们把滚动轴的索引值设为激活选项的索引值,便可实现选项的自动对齐了。

以下是自动对齐的实现代码:

private int active_index_ = 0;
private float item_spacing_;
private float[] item_index_;
private float anime_time = 0.4f;
private float align_accuracy_ = 0.01f;
private Button left_arrow;
private Button right_arrow;void Start() {...left_arrow.onClick.AddListener(() => {ArrowClicked("Left");});right_arrow.onClick.AddListener(() => {ArrowClicked("Right");});this.InvokeRepeating("ListItemAlign", 1f, 1.5f);                            // Repeat Auto alignment
}void LateUpdate() {SetActiveIndex();                                                           // Update the current active option index in each frame
}void SetActiveIndex() {for (int i = 0; i < item_index_.Length; i++) {if (scrollbar_.value < item_index_[i] + item_spacing_/2 &&scrollbar_.value > item_index_[i] - item_spacing_/2 &&active_index_ != i) {  active_index_ = i;}}
}void ListItemAlign() {float origin_val = scrollbar_.value;if (Mathf.Abs(origin_val - item_index_[active_index_]) >= align_accuracy_)          // Only do alignment when the scroll value is tooStartCoroutine(Align(origin_val, item_index_[active_index_]));                  // far from the target value.
}IEnumerator Align(float origin_val, float target_val) {float timer = 0f;while (timer < anime_time) {if (Input.GetMouseButton(0) || Input.GetAxis("Mouse ScrollWheel") != 0) {       // Stop the coroutine if user wants to move the listyield break;                                                                // when coroutine is still processing.}timer += Time.deltaTime;if (timer > anime_time)timer = anime_time;scrollbar_.value = Mathf.Lerp(origin_val, target_val, anime_curve.Evaluate(timer / anime_time));yield return null;}yield break;
}void ArrowClicked(string direction) {switch (direction) {case "Left":if (active_index_ != 0) {active_index_ -= 1;StartCoroutine(Align(scrollbar_.value, item_index_[active_index_]));}break;case "Right":if (active_index_ != item_index_.Length-1) {active_index_ += 1;StartCoroutine(Align(scrollbar_.value, item_index_[active_index_]));}break;default:break;}
}


选项自动调整大小

选项自动调整大小的逻辑与自动对齐的逻辑是相似的。但自动调整大小不存在像自动对齐那样繁杂的情景判断,因此我们可以在每一帧都执行自动调整大小。

以下是自动调整的实现代码:

void LateUpdate() {ListItemScale();
}void ListItemScale() {for (int i = 0; i < item_index_.Length; i++) {Vector2 origin_scale = transform.GetChild(i).localScale;                            // Need to do scaling for all objectsif (i == active_index_) {if (Vector2.Distance(origin_scale, active_scale_) >= scale_accuracy_)           // Only do scaling when the item scale is tooStartCoroutine(Scale(i, origin_scale, active_scale_));                      // different from target scale}else {if (Vector2.Distance(origin_scale, deactive_scale_) >= scale_accuracy_)StartCoroutine(Scale(i, origin_scale, deactive_scale_));}}
}IEnumerator Scale(int item_index, Vector2 origin_scale, Vector2 target_scale) {float timer = 0f;while (timer < anime_time) {timer += Time.deltaTime;if (timer > anime_time)timer = anime_time;transform.GetChild(item_index).localScale = Vector2.Lerp(origin_scale, target_scale, anime_curve.Evaluate(timer / anime_time));yield return null;}yield break;
}


随选项的变换切换示例音乐

SetActiveIndex() 函数中,发现激活选项发生了更替,或用户点击了UI交互件,直接切换音乐即可。


最终效果

Unity3D-滚动式关卡选择菜单


参考资料

Swipe Control with Touch for Menu Level:https://www.youtube.com/watch?v=GURPmGoAOoM
Unity在协程内部终止自己: https://blog.csdn.net/weixin_41319239/article/details/92991989

Unity游戏开发文档(3.1.3):滚动式关卡选择菜单相关推荐

  1. Unity游戏开发文档(3.1.1):弹窗效果

    前言      该文档为<Unity游戏开发文档(3):Dancing Line>的附属文档,亦可看作是单独的技术总结文档. 目录 综述 对话框的非匀速滑动 对话框动画的异步运行 最终效果 ...

  2. Unity游戏开发文档(3.1.2):下拉式音乐选择菜单

    前言      该文档为<Unity游戏开发文档(3):Dancing Line>的附属文档,亦可看作是单独的技术总结文档. 目录 综述 构建下拉菜单 填充下拉菜单 切换背景音乐 最终效果 ...

  3. Unity游戏开发文档(1):飞行模拟

    前言     本篇的代码是基于Unity3D 系列课程 "Create with Code" 第一章 "Player Control" 改进而来 目录 背景 设 ...

  4. python飞机大战概要设计_飞机大战游戏开发文档(Android版)

    飞机大战游戏 开发文档 (Android版) 课程名称:飞机大战游戏 课程类型:Android游戏编程精彩内容,尽在百度攻略:https://gl.baidu.com 姓名:苏均灿 学号:131342 ...

  5. 微信小程序游戏开发文档以及开发工具地址

    微信小程序开发交流qq群   581478349    承接微信小程序开发.扫码加微信. 正文: 微信官方于 2017 - 12 - 28 日 开发微信小程序 开发小游戏 , 微信小程序小游戏开发官方 ...

  6. java小组坦克大战游戏开发文档开发日志_java实现坦克大战游戏

    本文实例为大家分享了java实现坦克大战游戏的具体代码,供大家参考,具体内容如下 一.实现的功能 1.游戏玩法介绍 2.自定义游戏(选择游戏难度.关卡等) 3.自定义玩家姓名 4.数据的动态显示 二. ...

  7. php赛车游戏开发文档,React 开发一款简单的赛车游戏

    写在开始之前 最近研究egret引擎时,在论坛看到了用egret引擎写的一款赛车游戏 玩法很简单,左右控制赛车躲避来车,碰撞即游戏失败 下面将为大家一步步讲解,如何用React写出一款纯 javasc ...

  8. 微信小游戏开发文档(4)

    微信小游戏系统API: wx.onTouchEnd wx.offTouchEnd wx.onTouchCancel wx.offTouchCancel Touch 触点 微信小游戏数据缓存接口: wx ...

  9. 火力篮球游戏源码完整版-带游戏开发文档

    火力篮球,通过模拟现实中的投篮游戏机,而投篮游戏机又是源于街头篮球,街头篮球起源于美国,现在已经流行于世界的体育竞技项目,将投篮部分独立出来做成投篮游戏机.成为了专门的投篮类游戏设备.而本游戏就是将该 ...

最新文章

  1. 语音识别——基于深度学习的中文语音识别tutorial(代码实践)
  2. Serverless,后端小程序的未来
  3. 使用 Label 类在 XNA 中显示文本,WPXNA(七)
  4. Activity Monitor 闪退 无法进入睡眠
  5. mysql 表设计 date_mysql 表 Date类型
  6. php 添加样式,添加样式到php html电子邮件
  7. 登陆时验证码的制作(asp.net)
  8. 手写数字识别代码,可以跑通
  9. 内容管理系统CMS学习总结
  10. extremecomponents -- 文档下载依赖使用
  11. python 爬阳光高考高校数据
  12. 6、Latex学习笔记之参考文献篇
  13. dreamweaver+cs6+android,使用Dreamweaver cs6开发移动应用
  14. 翻斗式雨量传感器VS压电式雨量传感器
  15. AI制作卷边文字效果
  16. 盛世长缨rt8188gu安装网卡驱动(Ubuntu)
  17. SPSS学习(1)之数据录入与数据获取
  18. 如何处理授权和监督?
  19. ROS-Melodic 编译Moveit全过程记录和错误解决方案
  20. 啊啊啊~~~~~ Ajax

热门文章

  1. 316、海康威视监控弱电工程师精心整理的技术知识点
  2. 程序员光会敲代码已经不行了 思维方式更重要,尤其是第二种!!
  3. JS的事件监听与委托机制
  4. js循环appendChild与jq循环append方法遇到的问题
  5. 尚医通 (二)项目搭建
  6. 【重装Windows系统后】电脑环境部署
  7. Redis管理工具(Redis Assistant)更新啦
  8. 【狂神说】JavaWeb笔记整理 | SMBMS项目 | 文件上传和邮件发送
  9. GDB【5】-嵌入式平台xxx-linux-gdb远程调试动态库
  10. 程序员面试常见问题-长期更新