Unity3D RPG Core | 34 制作主菜单

这节我们制作游戏的主界面,包括开始新游戏、继续游戏和退出游戏的功能。

1. 制作主界面新场景

首先我们需要新建一个场景用作主界面场景,这边自己直接使用了素材里的场景。需要注意素材的场景色调和我们之前场景的不一样,我们需要删除素材中原本的后处理设置对象,复制替换成我们之前场景的后处理对象。后处理相关的内容,可以看之前的文章 《Unity3D RPG Core | 07 摄像机跟踪和后处理》

1.1 创建菜单画布

在 Hierarchy 界面上右击选择 UI - Canvas,创建一个新的画布。创建好之后调节 Screen Match ModeMatch 参数,使 UI 自适应界面大小。

接着右击选择 UI - Legacy - Text,创建一个 Text 控件,用于显示游戏标题。UI 控件的调节可以参考之前的 《Unity3D RPG Core | 29 添加玩家信息显示》 文章。

使用 Font Size 和 Rect Tool 调整字体大小。使用 Scale Tool 字体会失真。

最后我们选择 UI - Legacy - Button,添加三个按钮 “NEW GAME”、“CONTINUE”和“QUIT”。Button 组件下有 Text 子组件,按钮的文字在 Text 组件中设置。

我们还想让按钮有交互的效果,可以在图 1 所示的属性处进行设置。Normal Color 设置成透明的,alpha 通道设置为 0;指针悬空时对应 Highlighted Color;按下按钮对应 Pressed Color;选中对应 Selected Color;失效时对应 Disabled Color

图1 设置按钮交互颜色

1.2 让菜单具有立体感

菜单画布默认的 Render Mode 设置为 Screen Space - Overlay,这种设置是整个画面固定的。为了相机位置变动时,菜单的位置不变,以显示菜单的立体感,我们需要设置 Render Mode 为 World Space。

而如图 2 所示,画布的距离只能在 Screen Space - Camera 模式下的进行调节。所以为了在 World Space 中更好的显示画布,我们先在 Screen Space - Camera 模式下设定相机并指定 Plane Distance 值,最后再切换到 World Space 模式。

图2 设置画布位置

2. 场景中添加别的组件

设置好菜单之后,我们继续往场景中添加人物角色组件和传送门组件。

添加人物组件和传送门组件我们可以直接拖拽之前保存的预制体。为了不影响预制体,我们可以右击组件选择 Prefab - Unpack Completely,和之前的预制体解绑。

最终设置好的主菜单内容如图 3 所示:对于人物角色组件,我们删除其上除了动画以外的所有组件;对于传送门,我们可以指定不同颜色的传送门(注意需要创建新的材质)。

图3 主菜单场景

3. 实现功能

在场景内容都设置好之后,我们开始实现具体的功能。如代码清单 1 所示,我们创建脚本 MainMenuController。其中拖拽指定 3 个按钮,并使用 onClick.AddListener() 函数绑定点击事件。退出按钮功能比较简单,这边先进行实现,即调用 Application.Quit() 函数。

代码清单 1 待填空内容
  1. public class MainMenuController : MonoBehaviour
  2. {
  3.     public Button m_newGameButton = null;
  4.     public Button m_continueButton = null;
  5.     public Button m_QuitButton = null;
  6.  
  7.     private void Awake()
  8.     {
  9.         m_newGameButton.onClick.AddListener(OnNewGameClick);
  10.         m_continueButton.onClick.AddListener(OnContinueClick);
  11.         m_QuitButton.onClick.AddListener(OnQuitClick);
  12.     }
  13.  
  14.     void OnNewGameClick()
  15.     {
  16.  
  17.     }
  18.  
  19.     void OnContinueClick()
  20.     {
  21.  
  22.     }
  23.  
  24.     void OnQuitClick()
  25.     {
  26.         Application.Quit();
  27.     }
  28. }

3.1 实现新游戏功能

新游戏按钮需要实现的功能是,从主菜单场景传送到初始游戏场景。之前的传送函数的参数是传送门,此处我们直接指定场景名称和传送门名字就可以。因此如代码清单 2 所示,我们新增 TransferToDestinationInDifferentScene 接口,供主菜单场景调用。

开启新游戏时,我们清空一下原先存储的数据:PlayerPrefs.DeleteAll()

代码清单 2 新游戏
  1. public class SceneController : Singleton<SceneController>
  2. {
  3.     public void TransferToDestinationInDifferentScene(string sceneName, string destPortalName)
  4.     {
  5.         StartCoroutine(TransferDifferentScene(sceneName, destPortalName));
  6.     }
  7. }
  1. public class MainMenuController : MonoBehaviour
  2. {
  3.     void OnNewGameClick()
  4.     {
  5.         PlayerPrefs.DeleteAll();
  6.         SceneController.GetInstance().TransferToDestinationInDifferentScene("Land", "地图入口点");
  7.     }
  8. }

3.2 实现继续游戏功能

继续游戏功能和开始游戏类似,不同的是传送的场景可能不同。因此如代码清单 3 所示,我们在保存游戏数据时,再新建一个 CurrentScene 键保存当前场景名称。

点击继续游戏时,我们读取 CurrentScene 中的值,获取到场景名称再进行传送。需要注意的是,要保证每张地图上都需要有名称为 地图入口点 的传送门。

同时,把游戏数据的加载改为放在实例化角色人物的时候。

代码清单 3 继续游戏
  1. public class SaveManager : Singleton<SaveManager>
  2. {
  3.     void Save(object obj, string key)
  4.     {
  5.         string json = JsonUtility.ToJson(obj);
  6.         PlayerPrefs.SetString(key, json);
  7.         PlayerPrefs.SetString("CurrentScene", SceneManager.GetActiveScene().name);
  8.         PlayerPrefs.Save();
  9.     }
  10. }
  1. public class MainMenuController : MonoBehaviour
  2. {
  3.     void OnContinueClick()
  4.     {
  5.         if (PlayerPrefs.HasKey("CurrentScene"))
  6.         {
  7.             string sceneName = PlayerPrefs.GetString("CurrentScene");
  8.             SceneController.GetInstance().TransferToDestinationInDifferentScene(sceneName, "地图入口点");
  9.         }
  10.     }
  11. }
  1. public class PlayerController : MonoBehaviour
  2. {
  3.     // Start is called before the first frame update
  4.     void Start()
  5.     {
  6.         MouseManager.GetInstance().OnMoveMouseClick   += DoMoveAction;
  7.         MouseManager.GetInstance().OnAttackMouseClick += DoAttackAction;
  8.  
  9.         GameManager.GetInstance().RegisterPlayerData(this);
  10.  
  11.         CinemachineFreeLook cinemachine = FindObjectOfType<CinemachineFreeLook>();
  12.         if (cinemachine != null)
  13.         {
  14.             cinemachine.LookAt = m_LookAtPoint;
  15.             cinemachine.Follow = m_LookAtPoint;
  16.         }
  17.  
  18.         SaveManager.GetInstance().LoadPlayerData();
  19.     }
  20. }

3.2 实现返回主界面

最后我们实现返回主菜单界面的功能。返回主菜单界面不需要实例化人物,因此如代码清单 4 所示,我们需要再实现一个场景加载函数。

返回的方式是按 Escape 键。

代码清单 4 返回主界面
  1. public class SceneController : Singleton<SceneController>
  2. {
  3.     public void BackToMainMenu()
  4.     {
  5.         if (SceneManager.GetActiveScene().name != "MainMenu")
  6.         {
  7.             StartCoroutine(TransferDifferentScene("MainMenu"));
  8.         }
  9.     }
  10.  
  11.     IEnumerator TransferDifferentScene(string sceneName)
  12.     {
  13.         AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
  14.  
  15.         // Wait until the asynchronous scene fully loads
  16.         while (!asyncLoad.isDone)
  17.         {
  18.             yield return null;
  19.         }
  20.     }
  21. }
  1. public class SaveManager : Singleton<SaveManager>
  2. {
  3.     // Update is called once per frame
  4.     void Update()
  5.     {
  6.         if (Input.GetKeyDown(KeyCode.Escape))
  7.         {
  8.             SceneController.GetInstance().BackToMainMenu();
  9.         }
  10.         else if (Input.GetKeyDown(KeyCode.S))
  11.         {
  12.             SavePlayerData();
  13.             Debug.Log("SavePlayerData");
  14.         }
  15.         else if (Input.GetKeyDown(KeyCode.L))
  16.         {
  17.             LoadPlayerData();
  18.             Debug.Log("LoadPlayerData");
  19.         }
  20.     }
  21. }

4. 测试

测试前需要注意,主界面场景用到了大部分全局单例类,我们需要带上。我们可以把之前的单例类对象制作成预制体,然后拖拽到主菜单界面。

虽然两边都带单例类没有问题(单例本来就是这个作用),但是之前的实现方式是如果有重复创建的就删除掉。所以这边还是真正的只保留一份,重复的对象把它失效掉。

还有需要注意的是,主界面场景不需要附带 MouseManager。因为人物没有附带 NavMeshAgent 组件,点击传送门的时候会出现问题。

图 4 是最终实现的效果,点击新游戏会传送到主游戏场景,按 Escape 键会返回主菜单场景。

图4 测试结果