Unity 高级游戏开发教程(四)
欢迎到本文结尾!如果你正在阅读这篇文章,那么我假设你已经完成了对一个完整的、相当复杂的 Unity 赛车游戏的深入审查。这没什么可大惊小怪的。这是一个严峻的挑战,审查这么多的材料,并建立自己的游戏,一个小小的示范赛道。你克服了挑战,我赞扬你。让我们花点时间来回顾一下我们在这篇课文中共同完成的一些事情。
十、玩家和游戏状态类:第二部分
在这一章中,我们将通过观察游戏的大脑来继续回顾游戏的状态类,即GameState
类。这节课复杂而漫长,深呼吸,做好心理准备。我们有艰巨的工作要做。我们开始吧。
课堂回顾:游戏状态
嗯,我们已经设法复习了整个游戏中几乎所有的职业,非常详细,有一些相当不错的演示场景。我们刚刚复习完游戏中最长、最重要的职业之一,我们还有一个同样复杂的职业要复习。playerState
类通过轨迹交互更新,导致悬停赛车修改器,它还从用户输入接收一些信息。
所有这些数据都由GameState
类和游戏的 HUD 屏幕组织、共享、存储和呈现。该类负责将活动的悬停赛车连接到游戏的 HUD,以便正确显示所有的修改器、状态字段和通知。这个类还负责管理不同的菜单屏幕和执行游戏状态。我们将遵循下面列出的审查步骤:
-
枚举
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
我们要看的第一个复习部分是枚举部分。
枚举:游戏状态
GameState
类有两个枚举供我们细读。它们用于游戏状态管理和准备赛道。
public enum GameStateIndex {
FIRST,
NONE,
MAIN_MENU_SCREEN,
GAME_OVER_SCREEN,
GAME_PAUSE_SCREEN,
GAME_PLAY_SCREEN,
GAME_EXIT_PROMPT
};
public enum GameDifficulty {
LOW,
MED,
HIGH
};
Listing 10-1GameState Enumerations 1
列出的第一个枚举GameStateIndex
,用于帮助管理游戏的当前状态。它也有助于改变状态。在这种情况下,游戏状态代表游戏中唯一的屏幕。例如,开始菜单是一种状态,而帮助菜单和实际游戏是其他游戏状态。请注意,有一个NONE
状态、FIRST
状态和每个菜单屏幕状态的条目。列出的下一个枚举是GameDifficulty
枚举,用于帮助跟踪游戏的当前难度。在下一节中,我们将看看这个类的静态成员。
静态/常量/只读类成员:GameState
GameState
类有一些我们需要查看的静态和只读类成员。让我们来看看。
private static bool FIRST_RUN = true;
public static bool ON_GUI_SHOW_CAR_DETAILS = false;
public static bool ON_GUI_SHOW_EXIT_BUTTON = false;
public static bool SHOW_WAYPOINT_OUTPUT = false;
public static readonly float START_GAME_SECONDS = 5.0f;
public static readonly float TRACK_HELP_SECONDS = 2.0f;
public static readonly int DEFAULT_TOTAL_LAPS = 10;
Listing 10-2PlayerState Static/Constants/Read-Only Class Members 1
FIRST_RUN
字段是一个布尔标志,表示这是否是游戏的第一次运行。以下字段用于“main 13 演示”场景。如果ON_GUI_SHOW_CAR_DETAILS
字段设置为真,屏幕上将显示大量玩家和游戏状态信息。下一个字段ON_GUI_SHOW_EXIT_BUTTON
用于控制调试退出按钮的显示。接下来,SHOW_WAYPOINT_OUTPUT
字段用于控制航路点调试输出。
START_GAME_SECONDS
字段控制比赛开始前显示的秒数。类似地,TRACK_HELP_SECONDS
字段保存显示帮助通知的秒数。最后,DEFAULT_TOTAL_LAPS
字段用于保存每条赛道的默认圈数。静态/只读类成员审查到此结束。在下一个复习部分,我们将讨论该课程的剩余字段。
类别字段:游戏状态
GameState
类是一个重要的中央集权和国家管理类,因此,它有大量的类字段供我们查看。我们将在这里详细讨论它们。我们将从回顾中省略类的内部变量,因为这些字段被用作局部方法变量。我们有很多材料要讲,所以慢慢来。如果你没有在一次阅读中吸收全部,不要沮丧。可能需要一点时间才能真正适应这门课。让我们看看第一组字段,好吗?
//***** Class Fields *****
public ArrayList players = null;
public PlayerState p0;
public PlayerState p1;
public PlayerState p2;
public PlayerState p3;
public PlayerState p4;
public PlayerState p5;
public PlayerState currentPlayer = null;
Listing 10-3GameState Class Fields 1
players
字段是一个ArrayList
实例,用于保存游戏活动玩家的一组PlayerState
对象实例。这包括人类和人工智能控制的玩家。请注意,该游戏配置为支持六名玩家。最后一个条目是引用当前玩家的PlayerState
的currentPlayer
字段。当前玩家是其状态和摄像机插入游戏显示器的玩家。我们要研究的下一组变量是类字段和相关责任的随机分类。
//***** Internal Variables: Start *****
public int[] positions = null;
public bool sortingPositions = false;
public int currentIndex = 0;
public int player1Index = 0;
public LapTimeManager lapTimeManager = null;
public int totalLaps = DEFAULT_TOTAL_LAPS;
public int gameSettingsSet = 0; //track type
public int gameSettingsSubSet = 0; //track difficulty
public bool debugOn = false;
public bool forceGameStart = false;
public bool scsMode = false;
private GUIStyle style1 = null;
private GUIStyle style2 = null;
public WaypointCompare wpc = new WaypointCompare();
public ArrayList waypointRoutes = null;
public ArrayList waypointData = null;
public AudioSource audioBgroundSound = null;
private AudioSource audioS = null;
private bool nightTime = false;
private bool player1AiOn = true;
private bool prepped = false;
private bool ready = false;
private bool startGame = false;
private float startGameTime = 0.0f;
public bool gameWon = false;
public bool gameRunning = false;
public bool gamePaused = false;
Listing 10-4GameState Class Fields 2
这个集合中列出的第一个字段positions
是一个整数数组,用于存储悬停赛车的位置索引,并进行正确排序。通过这种方式,可以在索引零处找到比赛中的领先汽车。下一个字段是一个布尔标志,用于指示位置数组当前正在排序。接下来的两个字段看起来是多余的,但是仔细观察,我们会发现它们不是多余的。currentIndex
字段表示可用玩家数组中当前活动玩家的数组索引。下一个字段player1Index
,是玩家一的索引,默认为零。因此,虽然这两个字段看起来像是重复的,但请记住,当前玩家的索引可以改变,但玩家 1 的索引将始终为零。
集合中的下一个字段应该是熟悉的。它是LapTimeManager
类的一个实例,用于管理存储在游戏首选项中的一圈时间。该字段被恰当地命名为lapTimeManager
。totalLaps
字段代表当前赛道配置的总圈数。该值根据赛道相关的TrackScript
、比赛类型和比赛难度进行设置。接下来列出的两个字段,gameSettingsSet
和gameSettingSubSet
,是用于设置曲目类型和难度的类别字段。下一个字段debugOn
用于打开GameState
类的调试文本。该字段与我们之前讨论过的ON_GUI_SHOW_CAR_DETAILS
静态类字段一起工作。
forceGameState
字段是一个布尔标志,用于绕过某些正常的GameState
类功能,并被大多数游戏演示场景使用。该值通常通过 Unity 编辑器的“检查器”面板设置,并保存为场景配置的一部分。scsMode
字段用于帮助设置游戏,以便可以在不触发特定菜单屏幕(如游戏暂停菜单屏幕)的情况下拍摄游戏截图。style1
和style2
字段是 Unity 的GUIStyle
类的实例,由类的OnGUI
方法用来直接在游戏屏幕上显示调试信息。
wc
类字段是WaypointCompare
类的一个实例,用于对给定轨迹的路点数组进行排序。下一个字段是一个ArrayList
实例,存储在当前轨迹上找到的不同的路点路线。我应该提一下,这个游戏,在它目前的状态下,不使用路点路线,所以这个领域不会得到太好的支持。下一个区域waypointData
用于保存在航迹上发现的所有航路点条目。接下来的两个条目由班级的音频责任使用。第一个是audioBgroundSound
,用于保存音轨的背景音乐。第二个条目audioS
,用于保存菜单音效。当菜单屏幕接收到用户输入时,它播放声音效果来指示输入事件。很多情况下,菜单画面会要求游戏状态类播放菜单音效。
随后,接下来的五个类字段是布尔标志,用于指示游戏的当前状态。nightTime
字段是由轨道的TrackScript MonoBehaviour
和它的headLightsOn
字段设置的布尔标志。下一个条目,player1AiOn
,是一个布尔标志,指示玩家一辆车应该由 AI 控制。这在游戏第一次加载时就出现了,“街机”风格的人工智能竞赛开始了。prepped
字段用于指示游戏已经通过初始化players
数组中的所有PlayerState
实例而被正确准备好。一旦这个标志被设置为 true,对类’Prep
方法的调用将被转义。
ready
布尔字段是表示游戏准备开始的标志。startGame
字段用于指示比赛是否开始,并应启动倒计时定时器。下一个字段startGameTime
,用于记录比赛开始的倒计时。该集合中的最后三个字段是gameWon
、gameRunning
和gamePaused
布尔标志。gameWon
区域表示当前玩家已经完成比赛。这并不一定意味着他们排在第一位。随后,gameRunning
字段表明游戏正在运行,正如您所料。最后,gamePaused
字段表示游戏正在运行,但已经暂停。
//***** Track Help Variables *****
public bool trackHelpAccelOn = false;
public float trackHelpAccelTime = 0.0f;
public bool trackHelpSlowOn = false;
public float trackHelpSlowTime = 0.0f;
public bool trackHelpTurnOn = false;
public float trackHelpTurnTime = 0.0f;
//***** Track Settings *****
public int raceTrack = 0;
public bool easyOn = false;
public int raceType = 0;
public int waypointWidthOverride = 6;
public int waypointZeroOverride = 1;
public bool trackHelpOn = false;
private TrackScript trackScript = null;
public GameDifficulty difficulty = GameDifficulty.LOW;
public GameStateIndex gameStateIndex = GameStateIndex.FIRST;
//***** Camera Variables *****
private GameObject blimpCamera = null;
public string sceneName = "";
public Camera gameCamera = null;
public Camera rearCamera = null;
Listing 10-5GameState Class Fields 3
前面列出了我们要查看的下一组类字段。该组中的第一组字段是“跟踪帮助变量”组。该组中的第一个字段trackHelpAccelOn
和随后的条目trackHelpAccelTime
用于打开帮助通知图像并跟踪其显示持续时间。类似地,下面列出的四个字段用于控制帮助减速和帮助转向通知,如果它们是由当前玩家触发的话。我们要看的下一组字段是“Track Settings”组。
“轨道设置”组从raceTrack
字段开始。这个字段是我们当前正在比赛的赛道的数字表示。easyOn
字段是一个布尔标志,指示当前比赛的难度设置是否为简单。随后的字段raceType
用于指示当前比赛的模式。接下来的两个字段用于标准化轨迹的航路点的某些方面。waypointWidthOverride
用于设置当前轨道上航路点的标准宽度。
以类似的方式,waypointZeroOverride
域用于覆盖 Y 值为零的航路点标记的 Y 位置。trackHelpOn
字段是一个布尔标志,用于控制音轨是否支持显示帮助通知。接下来的两个条目,difficulty
和gameStateIndex
,用于管理赛道的难度设置和游戏的状态。该组中要查看的最后一组字段是相机字段。blimpCamera
字段是一个GameObject
实例,用于引用游戏的飞艇摄像机功能。列表中的下一个字段sceneName
是一个表示当前场景名称的字符串。最后,gameCamera
和rearCamera
类字段用于支持游戏的标准和后视摄像头。在结束本复习部分之前,我们还有一组课程字段要复习。
//***** Menu System Variables *****
private GameObject gamePauseMenu = null;
private GameObject gameStartMenu = null;
private GameObject gameOverMenu = null;
private GameObject gameExitMenu = null;
private GameObject gameHelpMenu = null;
public GameHUDNewScript hudNewScript = null;
public GameOverMenu gameOverMenuScript = null;
//***** Touch screen Variables *****
public bool accelOn = false;
public bool newTouch = false;
public bool touchScreen = false;
//***** Input Variables *****
private bool handleKeyA = false;
private bool handleKeyD = false;
private bool handleKey1 = false;
private bool handleKey2 = false;
private bool handleKey3 = false;
private bool handleKey4 = false;
private bool handleKey5 = false;
private bool handleKey6 = false;
Listing 10-6GameState Class Fields 4
该组中的第一组字段是“菜单系统”字段。有五个条目代表游戏支持的不同菜单屏幕。hudNewScript
字段是对与游戏 HUD 相关联的脚本组件的引用。下一个字段gameOverMenuScript
是对与菜单屏幕上的游戏相关联的脚本组件的引用。注意,在我们需要更细粒度控制的情况下,我们得到对MonoBehaviour
实例的引用。在其他情况下,有一个对相关游戏对象的引用就足够了。
在这个组之后,我们有“触摸屏”字段。accelOn
条目是一个布尔标志,表示触摸屏加速输入处于活动状态。newTouch
字段指示新的触摸交互正在发生。该组中的最后一个条目touchScreen
是一个布尔标志,表示触摸屏输入处于活动状态。我们要查看的最后一组字段是“输入”组。这一套不言自明。每个条目启用或禁用某些键盘键的输入。这就引出了“类字段回顾”部分的结论。在下一节中,我们将看看相关的方法大纲和类头回顾部分。
相关的方法大纲/类头:GameState
这个GameState
类的方法大纲有一系列的方法供我们回顾。别担心,我们将在详细的回顾中省略简单的支持方法,以加快速度。我们仍然会在这里列出它们。由于它们很简单,我们就不详细介绍了。让我们开始吧。
//Main Methods
public void PauseGame();
public void UnPauseGame();
public void FindWaypoints();
public void SetCarDetails();
public void ResetGame();
public void SetCarDetailsByGameType(PlayerState player);
public void SetActiveCar(int j);
public void PrepGame();
public void OnApplicationPause(bool pauseStatus);
void Start();
void Update();
//Support Methods
private int GetOnGuiPosY(int idx, int rowHeight);
public void OnGUI();
//Support Methods Menu Is Showing
private bool AreMenusShowing();
public bool IsHelpMenuShowing();
public bool IsPauseMenuShowing();
public bool IsEndMenuShowing();
public bool IsStartMenuShowing();
public bool IsExitMenuShowing();
public bool IsTrackHelpOn();
//Support Methods Show/Hide Menu
public void HideHelpMenu();
public void ShowHelpMenu();
public void HidePauseMenu();
public void ShowPauseMenu();
public void HideExitMenu();
public void ShowExitMenu();
public void HideStartMenu();
public void ShowStartMenu();
public void HideEndMenu();
public void ShowEndMenu();
//Support Methods Misc. 1
public void PlayMenuSound();
public void PrintWaypoints();
public ArrayList GetWaypoints(int index);
public void ToggleDebugOn();
public void ToggleCurrentCarAi();
private bool PlayerStateIdxCheck(int idx);
public PlayerState GetPlayer1();
public PlayerState GetCurrentPlayer();
public PlayerState GetPlayer(int i);
//Support Methods Track Features On/Off
private void TurnOffArmorMarkers();
private void TurnOnArmorMarkers();
private void TurnOffGunMarkers();
private void TurnOnGunMarkers();
private void TurnOffHealthMarkers();
private void TurnOnHealthMarkers();
private void TurnOffInvincMarkers();
private void TurnOnInvincMarkers();
private void TurnOffHittableMarkers();
private void TurnOnHittableMarkers();
private void TurnOffOilDrumStackMarkers();
private void TurnOnOilDrumStackMarkers();
private void TurnOffFunBoxMarkers();
private void TurnOnFunBoxMarkers();
//Support Methods Misc. 2
private void AdjustTagActive(bool active, string tag);
public void LogLapTime(PlayerState p);
public void StartDemoScene();
public int GetPosition(int idx, int currentPosition);
public void SetPositions();
public int PlayerStateCompare(int i1, int i2);
Listing 10-7GameState Pertinent Method Outline/Class Headers 1
花点时间看看与这个类相关的方法。发挥你的想象力,试着想象这个类及其使用的方法。我们一会儿将回顾这些方法。在我们继续之前,让我们看看这个类的 import 语句和声明。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameState : MonoBehaviour {}
Listing 10-8GameState Pertinent Method Outline/Class Headers 2
在接下来的回顾部分,我们将讨论类的支持方法。
支持方法详细信息:游戏状态
我们要看的第一组支持方法简单而直接。我把方法列在这里给你看看。由于它们的简单性,我不会详细讨论它们。请确保您在继续之前回顾并理解了这些方法。
01 private int GetOnGuiPosY(int idx, int rowHeight) {
02 return (idx * rowHeight);
03 }
01 private bool AreMenusShowing() {
02 if (IsStartMenuShowing() == true || IsEndMenuShowing() == true || IsHelpMenuShowing() == true || IsExitMenuShowing() == true) {
03 return true;
04 } else {
05 return false;
06 }
07 }
01 public bool IsHelpMenuShowing() {
02 if (gameHelpMenu != null) {
03 return gameHelpMenu.activeSelf;
04 } else {
05 return false;
06 }
07 }
01 public bool IsPauseMenuShowing() {
02 if (gamePauseMenu != null) {
03 return gamePauseMenu.activeSelf;
04 } else {
05 return false;
06 }
07 }
01 public bool IsEndMenuShowing() {
02 if (gameOverMenu != null) {
03 return gameOverMenu.activeSelf;
04 } else {
05 return false;
06 }
07 }
01 public bool IsStartMenuShowing() {
02 if (gameStartMenu != null) {
03 return gameStartMenu.activeSelf;
04 } else {
05 return false;
06 }
07 }
01 public bool IsExitMenuShowing() {
02 if (gameExitMenu != null) {
03 return gameExitMenu.activeSelf;
04 } else {
05 return false;
06 }
07 }
01 public bool IsTrackHelpOn() {
02 return trackHelpOn;
03 }
01 public void HideHelpMenu() {
02 if (gameHelpMenu != null) {
03 gameHelpMenu.SetActive(false);
04 UnPauseGame();
05 }
06 }
01 public void ShowHelpMenu() {
02 if (gameHelpMenu != null) {
03 gameHelpMenu.SetActive(true);
04 PauseGame();
05 }
06 }
01 public void HidePauseMenu() {
02 if (gamePauseMenu != null) {
03 gamePauseMenu.SetActive(false);
04 UnPauseGame();
05 }
06 }
01 public void ShowPauseMenu() {
02 if (gamePauseMenu != null) {
03 gamePauseMenu.SetActive(true);
04 PauseGame();
05 }
06 }
01 public void HideExitMenu() {
02 if (gameExitMenu != null) {
03 gameExitMenu.SetActive(false);
04 UnPauseGame();
05 }
06 }
01 public void ShowExitMenu() {
02 if (gameExitMenu != null) {
03 gameExitMenu.SetActive(true);
04 PauseGame();
05 }
06 }
01 public void HideStartMenu() {
02 if (gameStartMenu != null) {
03 gameStartMenu.SetActive(false);
04 }
05 }
01 public void ShowStartMenu() {
02 if (gameStartMenu != null) {
03 gameStartMenu.SetActive(true);
04 }
05 }
01 public void HideEndMenu() {
02 if (gameOverMenu != null) {
03 gameOverMenu.SetActive(false);
04 }
05 }
01 public void ShowEndMenu() {
02 if (gameOverMenu != null) {
03 gameOverMenu.SetActive(true);
04 }
05 }
01 public void PlayMenuSound() {
02 if (audioS != null) {
03 audioS.Play();
04 }
05 }
01 public void ToggleDebugOn() {
02 if (debugOn == true) {
03 debugOn = false;
04 } else {
05 debugOn = true;
06 }
07 }
01 private bool PlayerStateIdxCheck(int idx) {
02 if (players != null && idx >= 0 && idx < players.Count) {
03 return true;
04 } else {
05 return false;
06 }
07 }
01 public PlayerState GetPlayer1() {
02 return (PlayerState)players[player1Index];
03 }
01 public PlayerState GetCurrentPlayer() {
02 return (PlayerState)players[currentIndex];
03 }
01 public PlayerState GetPlayer(int i) {
02 if (i >= 0 && i < players.Count) {
03 return (PlayerState)players[i];
04 } else {
05 return null;
06 }
07 }
01 private void TurnOffArmorMarkers() {
02 AdjustTagActive(false, "ArmorMarker");
03 }
01 private void TurnOnArmorMarkers() {
02 AdjustTagActive(true, "ArmorMarker");
03 }
01 private void TurnOffGunMarkers() {
02 AdjustTagActive(false, "GunMarker");
03 }
01 private void TurnOnGunMarkers() {
02 AdjustTagActive(true, "GunMarker");
03 }
01 private void TurnOffHealthMarkers() {
02 AdjustTagActive(false, "HealthMarker");
03 }
01 private void TurnOnHealthMarkers() {
02 AdjustTagActive(true, "HealthMarker");
03 }
01 private void TurnOffInvincMarkers() {
02 AdjustTagActive(false, "InvincibilityMarker");
03 }
01 private void TurnOnInvincMarkers() {
02 AdjustTagActive(true, "InvincibilityMarker");
03 }
01 private void TurnOffHittableMarkers() {
02 AdjustTagActive(false, "Hittable");
03 }
01 private void TurnOnHittableMarkers() {
02 AdjustTagActive(true, "Hittable");
03 }
01 private void TurnOffOilDrumStackMarkers() {
02 AdjustTagActive(false, "OilDrumStack");
03 }
01 private void TurnOnOilDrumStackMarkers() {
02 AdjustTagActive(true, "OilDrumStack");
03 }
01 private void TurnOffFunBoxMarkers() {
02 AdjustTagActive(false, "FullFunBox");
03 }
01 private void TurnOnFunBoxMarkers() {
02 AdjustTagActive(true, "FullFunBox");
03 }
01 public void StartDemoScene() {
02 PlayerPrefs.SetInt("GameStateIndex", 5);
03 PlayerPrefs.Save();
04 ResetGame();
05 SceneManager.LoadScene(SceneManager.GetActiveScene().name);
06 }
01 public int GetPosition(int idx, int currentPosition) {
02 int i = 0;
03 int l = 0;
04 l = positions.Length;
05
06 for (i = 0; i < l; i++) {
07 if (positions[i] == idx) {
08 return (i + 1);
09 }
10 }
11
12 return 6;
13 }
01 public void SetPositions() {
02 sortingPositions = true;
03 System.Array.Sort(positions, PlayerStateCompare);
04 sortingPositions = false;
05 }
Listing 10-9GameState Support Method Details 1
前面列出的类支持方法非常简单,所以我将它们留给您自己去回顾。在接下来的支持方法列表中,我们将回顾这些方法来解释它们是如何工作的。这些支持方法稍微复杂一点,所以它们值得更多的关注。我应该很快提到,我没有在主方法列表或支持方法列表中列出OnGUI
方法。这是因为该方法相当长,除了将调试值打印到屏幕上之外,它没做什么。该方法的审查是可选的。我将把它留给你来判断。
01 public void PrintWaypoints() {
02 if (waypointData != null && waypointData.Count > 0) {
03 ArrayList data = (ArrayList)waypointData[0];
04 int l = data.Count;
05 WaypointCheck wc = null;
06 for (int j = 0; j < l; j++) {
07 wc = (WaypointCheck)data[j];
08 if (SHOW_WAYPOINT_OUTPUT) {
09 Utilities.wr(j + " Found waypoint: " + wc.waypointIndex + ", Center: " + wc.transform.position);
10 }
11 }
12 }
13 }
01 public ArrayList GetWaypoints(int index) {
02 if (waypointData == null) {
03 return null;
04 } else {
05 if (index >= 0 && index < waypointData.Count) {
06 return (ArrayList)waypointData[index];
07 } else {
08 return null;
09 }
10 }
11 }
01 public void ToggleCurrentCarAi() {
02 PlayerState player;
03 player = GetCurrentPlayer();
04
05 if (player1AiOn == true) {
06 player1AiOn = false;
07 } else {
08 player1AiOn = true;
09 }
10
11 if (player1AiOn == true) {
12 player.aiOn = true;
13 player.cm.aiOn = true;
14 player.fpsInput.aiOn = true;
15 player.mouseInput.aiOn = true;
16 player.offTrackSeconds = 5.0f;
17 } else {
18 player.aiOn = false;
19 player.cm.aiOn = false;
20 player.fpsInput.aiOn = false;
21 player.mouseInput.aiOn = false;
22 player.offTrackSeconds = 10.0f;
23 }
24
25 if (forceGameStart) {
26 player.offTrackSeconds = 10000.0f;
27 player.wrongDirectionSeconds = 10000.0f;
28 }
29 }
Listing 10-10GameState Support Method Details 2
前面列出了第一组更复杂的支持方法。我们有两个航路点方法和一个人工智能相关的方法要复习。列出的第一种方法PrintWaypoints
用于列出所有与默认航路相关的航路点。如果在第 2 行定义并填充了waypointData
字段,那么我们在该方法的第 3 行加载默认路线的路点。对于找到的数据中的每个航路点,我们打印出其设置的摘要,第 7-10 行。我们要回顾的下一个航路点是GetWaypoints
方法。如果定义了waypointData
字段,那么我们为给定的路线索引找到一组路点。如果未定义数据的索引,则该方法返回空值。这一套方法中我们要复习的最后一个方法是ToggleCurrentAi
法。
这个方法用于切换当前玩家汽车的 AI 状态。注意,AI 标志随后被设置在与当前玩家相关联的所有输入类上。只有当forceGameStart
布尔值被设置为真时,最后一位代码才会运行。我们设置了一些大的定时值,以防止偏离轨道和错误的方向修改器触发。还有几个支持方法让我们看看。我把它们列在这里。
01 private void AdjustTagActive(bool active, string tag) {
02 GameObject[] gos = GameObject.FindGameObjectsWithTag(tag);
03 int l = gos.Length;
04 for (int i = 0; i < l; i++) {
05 gos[i].SetActive(active);
06 }
07 }
01 public void LogLapTime(PlayerState p) {
02 string time = p.time;
03 int timeNum = p.timeNum;
04 int lap = p.currentLap;
05 int track = raceTrack;
06 int type = gameSettingsSet;
07 int diff = gameSettingsSubSet;
08
09 LapTime lt = new LapTime();
10 lt.time = time
;
11 lt.timeNum = timeNum;
12 lt.lap = lap;
13 lt.type = type;
14 lt.diff = diff;
15 lt.track = track;
16
17 lapTimeManager.AddEntry(lt);
18 lapTimeManager.CleanTimes();
19 lapTimeManager.FindBestLapTimeByLastEntry();
20 PlayerPrefs.SetString("LapTimes", lapTimeManager.Serialize());
21 }
01 public int PlayerStateCompare(int i1, int i2) {
02 if (!PlayerStateIdxCheck(i1) || !PlayerStateIdxCheck(i2)) {
03 return 0;
04 }
05
06 PlayerState obj1 = (PlayerState)players[i1];
07 PlayerState obj2 = (PlayerState)players[i2];
08
09 if (obj1.currentLap > obj2.currentLap) {
10 return -1;
11 } else if (obj1.currentLap < obj2.currentLap) {
12 return 1;
13 } else {
14 if (obj1.aiWaypointIndex > obj2.aiWaypointIndex) {
15 return -1;
16 } else if (obj1.aiWaypointIndex < obj2.aiWaypointIndex) {
17 return 1;
18 } else {
19 if (obj1.aiWaypointTime < obj2.aiWaypointTime) {
20 return 1;
21 } else if (obj1.aiWaypointTime > obj2.aiWaypointTime) {
22 return -1;
23 } else {
24 return 0;
25 }
26 }
27 }
28 }
Listing 10-11GameState Support Method Details 3
前面列出的最后一组支持方法是一个组合组。列出的第一种方法非常有用。AdjustTagActive
方法用于定位所有具有指定标签的GameObjects
。这些对象将它们的活动标志设置为第 5 行提供的参数值。下面列出的方法用于将一圈时间添加到玩家的圈时间日志中。在第 2-7 行,根据提供的PlayerState
值、p
和赛道的当前配置设置存储圈速所需的值。接下来,在第 9–15 行,创建了一个新的LapTime
对象实例,并根据准备好的方法变量设置了对象的字段。单圈时间被添加到游戏的单圈时间管理器的第 17 行。第 18 行清除了分段时间,第 19 行的方法调用确定了最佳分段时间。最后,用第 20 行的lapTimeManager
字段中新的序列化值更新玩家偏好。
我们将在本节中回顾的最后一个方法是PlayerStateCompare
方法。这种方法用于比较两个玩家,以确定哪个玩家在当前比赛中处于哪个位置。首先,在第 2-4 行检查提供的玩家索引i1
和i2
的有效性。在第 9-27 行,两名选手在比赛中的顺序由当前圈决定,然后是当前路点索引,最后是最快圈速。这就是本复习部分的结论。接下来,我们将看看这个类的主要方法。
主要方法细节:GameState
GameState
类有几个主要的方法让我们复习。让我们开始写代码吧!
01 public void PauseGame() {
02 gamePaused = true;
03 Time.timeScale = 0;
04
05 if (players != null) {
06 iPg = 0;
07 pPg = null;
08 lPg = players.Count;
09 for (iPg = 0; iPg < lPg; iPg++) {
10 pPg = (PlayerState)players[iPg];
11 if (pPg != null) {
12 pPg.PauseSound();
13 }
14 }
15 }
16
17 if (audioBgroundSound != null) {
18 audioBgroundSound.Stop();
19 }
20 }
01 public void UnPauseGame() {
02 gamePaused = false;
03 Time.timeScale = 1;
04
05 if (players != null) {
06 iUpg = 0;
07 pUpg = null;
08 lUpg = players.Count;
09 for (iUpg = 0; iUpg < lUpg; iUpg++) {
10 pUpg = (PlayerState)players[iUpg];
11 if (pUpg != null) {
12 pUpg.UnPauseSound();
13 }
14 }
15 }
16
17 if (audioBgroundSound != null) {
18 audioBgroundSound.Play();
19 }
20 }
01 public void FindWaypoints() {
02 GameObject[] list = GameObject.FindGameObjectsWithTag("Waypoint");
03 ArrayList routes = new ArrayList();
04 int l = list.Length;
05 WaypointCheck wc = null;
06 int i = 0;
07 int j = 0;
08
09 for (i = 0; i < l; i++) {
10 if (list[i].activeSelf == true) {
11 wc = (list[i].GetComponent<WaypointCheck>());
12 if (wc != null) {
13 if (routes.Contains(wc.waypointRoute + "") == false) {
14 routes.Add(wc.waypointRoute + "");
15 }
16 }
17 }
18 }
19
20 ArrayList waypoints = new ArrayList();
21 ArrayList row = new ArrayList();
22 l = routes.Count;
23
24 for (i = 0; i < l; i++) {
25 row.Clear();
26 int l2 = list.Length;
27 for (j = 0; j < l2; j++) {
28 if (list[j].activeSelf == true) {
29 if (waypointWidthOverride != -1) {
30 if (list[j].transform.localScale.z < 10) {
31 list[j].transform.localScale.Set(list[j].transform.localScale.x, list[j].transform.localScale.y, waypointWidthOverride);
32 }
33 }
34
35 if (waypointZeroOverride != -1) {
36 if (list[j].transform.localPosition.y == 0) {
37 list[j].transform.localScale.Set(list[j].transform.localScale.x, 1, waypointWidthOverride);
38 }
39 }
40
41 wc = (list[j].GetComponent<WaypointCheck>());
42 if (wc != null) {
43 if ((wc.waypointRoute + "") == (routes[i] + "")) {
44 row.Add(wc);
45 }
46 }
47 }
48 }
49
50 object[] ar = row.ToArray();
51 System.Array.Sort(ar, wpc);
52 row = new ArrayList(ar);
53 l2 = row.Count;
54
55 for (j = 0; j < l2; j++) {
56 wc = (WaypointCheck)row[j];
57 wc.waypointIndex = j;
58 }
59
60 waypoints.Add(row);
61 }
62
63 waypointRoutes = routes;
64 waypointData = waypoints;
65 }
Listing 10-12GameState Main Method Details 1
我们要看的第一组主要方法包括游戏的暂停和取消暂停方法以及路点加载方法。让我们来看看!PauseGame
方法用于在游戏窗口失去焦点时停止游戏。这可以在 Unity 编辑器中通过运行主游戏并在运行时切换到另一个应用来测试。注意第 2 行的gamePaused
布尔标志被设置为真,并且Time.timeScale
字段的值被设置为零。
这有停止游戏引擎的效果,第 3 行。如果定义了活动玩家的数组,则在第 6–8 行设置循环控制变量。循环遍历游戏的玩家,并暂停每个玩家的音频,第 9-14 行。随后,在第 17-19 行,背景音乐暂停。集合中列出的第二种方法是UnPauseGame
方法。这个方法颠倒了游戏的暂停方法。仔细检查这个方法,注意时间刻度恢复为 1。列出的最后一种方法负责查找和准备所有的路点。所有带有“航路点”标签的 Unity GameObjects
位于第 2 行。在第 3-7 行设置了临时路线ArrayList
和一些回路控制变量。接下来,在第 9–18 行,我们遍历所有的路点,并将任何唯一的路点路线添加到路线数组中。在第 20–22 行,我们准备了一些方法变量。row
变量用作航路点数据的临时保存器,而waypoints
变量保存最终的航路点数据。
我们在第 24 行的已知路线上循环,变量temp
在第 25 行被重置,一个新的长度变量在第 26 行被设置。然后在第 27 行,我们循环遍历已知航路点的列表。如果航路点是激活的,我们检查是否必须应用航路点宽度覆盖,第 29-33 行,和航路点零点覆盖,第 35-39 行。第 41 行设置了WaypointCheck
脚本组件。如果该航路点是航路点组的成员,我们将其添加到row
变量,第 42–46 行。在第 50–52 行,我们对找到的路点进行排序,并重置 row 变量。在第 53 行设置l2
变量,在第 55-58 行重置航路航路点索引。在第 60 行,路线航点被添加到waypoints
变量中。最后,在第 63-64 行,找到的航路点数据存储在类别字段waypointRoutes
和waypointData
中。
01 public void SetCarDetails() {
02 PlayerState player = null;
03 int i = 0;
04 int l = players.Count;
05
06 for (i = 0; i < l; i++) {
07 if (PlayerStateIdxCheck(i)) {
08 player = (PlayerState)players[i];
09 if (player != null) {
10 if (i == player1Index) {
11 gameCamera = player.camera;
12 player.camera.enabled = true;
13
14 rearCamera = player.rearCamera;
15 player.rearCamera.enabled = true;
16 player.audioListener.enabled = true;
17
18 if (player1AiOn == true) {
19 player.aiOn = true;
20 player.cm.aiOn = true;
21 player.fpsInput.aiOn = true;
22 player.mouseInput.aiOn = true;
23 } else {
24 player.aiOn = false;
25 player.cm.aiOn = false;
26 player.fpsInput.aiOn = false;
27 player.mouseInput.aiOn = false;
28 }
29 } else {
30 player.camera.enabled = false;
31 player.rearCamera.enabled = false;
32 player.audioListener.enabled = false;
33 player.aiOn = true;
34 player.cm.aiOn = true;
35 player.fpsInput.aiOn = true;
36 player.mouseInput.aiOn = true;
37 }
38 player.waypoints = GetWaypoints(0);
39 }
40 }
41 }
42 }
01 public void ResetGame() {
02 gamePaused = true;
03 Time.timeScale = 0;
04
05 prepped = false;
06 ready = false;
07 startGame = false;
08 startGameTime = 0.0f;
09 gameRunning = false;
10 gameWon = false;
11
12 Time.timeScale = 1;
13 gamePaused = false;
14 }
01 public void SetCarDetailsByGameType(PlayerState player) {
02 int idx = player.index;
03 player.player = GameObject.Find(Utilities.NAME_PLAYER_ROOT + idx);
04 player.player.transform.position = GameObject.Find(Utilities.NAME_START_ROOT + idx).transform.position;
05
06 player.maxSpeed = PlayerState.DEFAULT_MAX_SPEED;
07 player.gravity = PlayerState.DEFAULT_GRAVITY;
08
09 player.maxForwardSpeedSlow = 50;
10 player.maxSidewaysSpeedSlow = 12;
11 player.maxBackwardsSpeedSlow = 5;
12 player.maxGroundAccelerationSlow = 25;
13
14 player.maxForwardSpeedNorm = 200;
15 player.maxSidewaysSpeedNorm = 50;
16 player.maxBackwardsSpeedNorm = 20;
17 player.maxGroundAccelerationNorm = 100;
18
19 player.maxForwardSpeedBoost = 250;
20 player.maxSidewaysSpeedBoost = 60;
21 player.maxBackwardsSpeedBoost = 30;
22 player.maxGroundAccelerationBoost = 120;
23
24 if (idx != player1Index) {
25 if (difficulty == GameDifficulty.LOW) {
26 player.maxSpeed = PlayerState.DEFAULT_MAX_SPEED;
27 player.maxGroundAccelerationNorm += 5;
28 } else if (difficulty == GameDifficulty.MED) {
29 player.maxSpeed = PlayerState.DEFAULT_MAX_SPEED + 5;
30 player.maxForwardSpeedNorm += 10;
31 player.maxGroundAccelerationNorm += 10;
32 } else if (difficulty == GameDifficulty.HIGH) {
33 player.maxSpeed = PlayerState.DEFAULT_MAX_SPEED + 10;
34 player.maxForwardSpeedNorm += 15;
35 player.maxGroundAccelerationNorm += 40;
36 player.maxForwardSpeedBoost += 15;
37 player.maxGroundAccelerationBoost += 15;
38 }
39 } else if (idx == player1Index) {
40 player.maxSpeed += Random.Range(0, 12);
41 player.maxForwardSpeedNorm += Random.Range(0, 6);
42 player.maxGroundAccelerationNorm += Random.Range(0, 6);
43 player.maxForwardSpeedBoost += Random.Range(0, 6);
44 player.maxGroundAccelerationBoost += Random.Range(0, 6);
45 }
46 }
01 public void SetActiveCar(int j) {
02 if (debugOn == false) {
03 Utilities.wr("Method SetActiveCar says: debugOn is false, returning.");
04 return;
05 }
06
07 PlayerState player = null;
08 int l = players.Count;
09 for (int i = 0; i < l; i++) {
10 player = (PlayerState)players[i];
11 if (player != null && player.player != null) {
12 if (j == i) {
13 currentIndex = i;
14 currentPlayer = (PlayerState)players[currentIndex];
15 player.camera.enabled = true;
16 player.rearCamera.enabled = true;
17 player.audioListener.enabled = true;
18 } else {
19 player.camera.enabled = false;
20 player.rearCamera.enabled = false;
21 player.audioListener.enabled = false;
22 }
23 }
24 }
25 }
Listing 10-13GameState Main Method Details 2
这组主要方法中列出的第一个方法是SetCarDetails
方法。这个方法用于将每辆车配置为 AI 或玩家控制的车。在第 2–4 行,该方法准备了一些局部变量,用于在活动玩家数组中循环。循环从第 6 行开始,如果PlayerState
实例player
不为空,那么我们在第 9–39 行处理这个玩家。关于这个代码块,第 10–28 行代码的第一个分支被应用到应该由玩家控制的汽车上。在第 11-165 行,悬停赛车被插入游戏的 HUD。如果这辆车应该是人工智能控制的,那么它在第 19-22 行被配置成这样。否则,第 24–27 行的代码会关闭这辆车的 AI。剩下的悬浮赛车都在 30-36 行被配置为人工智能控制。
列出的下一个主要方法是ResetGame
方法。请注意该方法的第 2-3 行。请注意,游戏被标记为暂停,游戏的时间刻度被设置为零。在第 5–10 行,该方法重置键类字段。在第 12–13 行的方法结束时,游戏暂停标记被设置为 false,时间刻度返回到值 1。集合中列出的第三种方法是SetCarDetailsByGameType
,它用于根据当前游戏类型为每个悬停参赛者准备正确的设置。第一行代码从第 2 行传入的PlayerState
实例中获取玩家的索引。玩家的GameObject
和Transform
在 3、4 线得到强化。悬停赛车的最大速度、重力以及慢速、正常和加速速度设置在第 6-22 行。如果赛车的指数不是玩家一的指数,那么我们根据赛道的难度调整赛车的配置,第 25-38 行。
第 40–44 行的最后一个代码块为人类玩家配置汽车。这就把我们带到了这一组中的最后一个方法,即SetActiveCar
方法。这个方法用于设置比赛中当前活跃的玩家。这不会使汽车受到人工智能或人类的控制,但它会将汽车连接到游戏的 HUD 中。要检查这个功能,在 Unity 编辑器中运行“Main13Demonstration”场景,并尝试按键盘上的数字键,数字 1 到 6。你会注意到,你可以使用这个功能切换到不同的汽车。回到代码,注意如果debugOn
布尔字段被设置为 false,那么这个方法被转义。
如果没有,则在第 7–8 行准备循环控制变量。我们循环当前的一组玩家,如果定义了当前玩家,我们将它设置为游戏的当前玩家,游戏现在将显示汽车的摄像头、后视摄像头和飞艇摄像头,它们会相应地进行调整。在第 19–21 行,如果悬停赛车与指定的玩家索引不匹配,它将被设置为非活动模式。在GameState
类中还有一些主要的方法需要复习。下一个我们将要讨论的是非常重要的PrepGame
方法。这个方法被许多类调用,以确保游戏和它的玩家被正确配置。由于该方法的长度,我们将分块回顾它。让我们看一下该方法的第一个块。
001 public void PrepGame() {
002 if (prepped == true) {
003 return;
004 }
005 prepped = true;
006
007 //Prep waypoints and track script settings
008 FindWaypoints();
009 trackScript = GetComponent<TrackScript>();
010 totalLaps = trackScript.laps;
011 nightTime = trackScript.headLightsOn;
012 sceneName = trackScript.sceneName;
013
014 //Prep menu screens
015 if (hudNewScript == null) {
016 if (GameObject.Find("GameHUD") != null) {
017 hudNewScript = GameObject.Find("GameHUD").GetComponent<GameHUDNewScript>();
018 }
019 }
020
021 if (hudNewScript != null) {
022 hudNewScript.HideAll();
023 }
024
025 if (gameOverMenuScript == null) {
026 if (GameObject.Find("GameOverMenu") != null) {
027 gameOverMenuScript = GameObject.Find("GameOverMenu").GetComponent<GameOverMenu>();
028 }
029 }
030
031 if (gameOverMenuScript != null) {
032 gameOverMenuScript.HideWinImage();
033 gameOverMenuScript.ShowLoseImage();
034 }
035
036 if (audioBgroundSound == null) {
037 if (GameObject.Find("BgMusic") != null) {
038 audioBgroundSound = GameObject.Find("BgMusic").GetComponent<AudioSource>();
039 }
040 }
041
042 if (gamePauseMenu == null) {
043 gamePauseMenu = GameObject.Find("GamePauseMenu");
044 if (gamePauseMenu != null) {
045 gamePauseMenu.SetActive(false);
046 }
047 }
048
049 if (gameStartMenu == null) {
050 gameStartMenu = GameObject.Find("GameStartMenu");
051 if (gameStartMenu != null) {
052 gameStartMenu.SetActive(false);
053 }
054 }
055
056 if (gameOverMenu == null) {
057 gameOverMenu = GameObject.Find("GameOverMenu");
058 if (gameOverMenu != null) {
059 gameOverMenu.SetActive(false);
060 }
061 }
062
063 if (gameExitMenu == null) {
064 gameExitMenu = GameObject.Find("GameExitMenu");
065 if (gameExitMenu != null) {
066 gameExitMenu.SetActive(false);
067 }
068 }
069
070 if (gameHelpMenu == null) {
071 gameHelpMenu = GameObject.Find("GameHelpMenu");
072 if (gameHelpMenu != null) {
073 gameHelpMenu.SetActive(false);
074 }
075 }
076
077 //Prep player prefs default values
078 if (FIRST_RUN && gameStateIndex == GameStateIndex.FIRST) {
079 PlayerPrefs.DeleteKey("GameStateIndex");
080 if (PlayerPrefs.HasKey("EasyOn") == false && PlayerPrefs.HasKey("BattleOn") == false && PlayerPrefs.HasKey("ClassicOn") == false) {
081 PlayerPrefs.SetInt("EasyOn", 1);
082 PlayerPrefs.SetInt("BattleOn", 0);
083 PlayerPrefs.SetInt("ClassicOn", 0);
084 }
085
086 if (PlayerPrefs.HasKey("LowOn") == false && PlayerPrefs.HasKey("MedOn") == false && PlayerPrefs.HasKey("HighOn") == false) {
087 PlayerPrefs.SetInt("LowOn", 1);
088 PlayerPrefs.SetInt("MedOn", 0);
089 PlayerPrefs.SetInt("HighOn", 0);
090 }
091 PlayerPrefs.Save();
092 }
093
094 //Prep lap time manager
095 string tmpStr = PlayerPrefs.GetString("LapTimes", "");
096 lapTimeManager = new LapTimeManager();
097 if (tmpStr != null && tmpStr != "") {
098 Utilities.wr("Found lap times: " + tmpStr);
099 lapTimeManager.Deserialize(tmpStr);
100 }
101
Listing 10-14GameState Main Method Details 3
顾名思义,这种方法的主要目的是为比赛做准备。让我们一次一个地了解不同的职责。首先,如果已经调用了PrepGame
方法,就对其进行转义,第 2–4 行。航路点被加载,任何TrackScript
值被应用于第 8-12 行的准备航路点部分。对所有菜单系统游戏对象的引用配置在第 15–75 行。代码很简单。每个菜单屏幕游戏对象都是按名称加载的,如果有定义,随后会被停用。注意,在第 16–18 行,为hudNewScript
字段创建了一个脚本组件引用。在第 27 行,同样的过程用于加载对gameOverMenuScript
的引用。使用这些对象,我们可以调用类方法来调整菜单屏幕上的 HUD 和游戏的设置。
第 78–92 行的下一部分代码负责通过为比赛类型和难度设置一些默认值来准备玩家偏好。接下来,在第 95–100 行的代码块中,lapTimeManager
被初始化,当前存储的分段时间(如果有的话)被反序列化并在第 99 行的lapTimeManager
实例中激活。我们将在下面列出的下一个代码块中继续回顾这个方法。
102 //Prep difficulty
103 if (PlayerPrefs.HasKey("LowOn") == true && PlayerPrefs.GetInt("LowOn") == 1) {
104 difficulty = GameDifficulty.LOW;
105 } else if (PlayerPrefs.HasKey("MedOn") == true && PlayerPrefs.GetInt("MedOn") == 1) {
106 difficulty = GameDifficulty.MED;
107 } else if (PlayerPrefs.HasKey("HighOn") == true && PlayerPrefs.GetInt("HighOn") == 1) {
108 difficulty = GameDifficulty.HIGH;
109 }
110
111 //Prep track configuration
112 if (PlayerPrefs.HasKey("EasyOn") && PlayerPrefs.GetInt("EasyOn") == 1) {
113 gameSettingsSet = 0;
114 totalLaps = 2;
115 TurnOffArmorMarkers();
116 TurnOffGunMarkers();
117 TurnOffInvincMarkers();
118 TurnOffHealthMarkers();
119 TurnOffHittableMarkers();
120 if (difficulty == GameDifficulty.LOW) {
121 gameSettingsSubSet = 0;
122 TurnOffOilDrumStackMarkers();
123 TurnOffFunBoxMarkers();
124 } else if (difficulty == GameDifficulty.MED) {
125 gameSettingsSubSet = 1;
126 TurnOffOilDrumStackMarkers();
127 TurnOnFunBoxMarkers();
128 } else if (difficulty == GameDifficulty.HIGH) {
129 gameSettingsSubSet = 2;
130 TurnOnOilDrumStackMarkers();
131 TurnOnFunBoxMarkers();
132 }
133 } else if (PlayerPrefs.HasKey("BattleOn") && PlayerPrefs.GetInt("BattleOn") == 1) {
134 gameSettingsSet = 1;
135 totalLaps = trackScript.laps;
136 TurnOnArmorMarkers();
137 TurnOnGunMarkers();
138 TurnOnInvincMarkers();
139 TurnOnHealthMarkers();
140 TurnOnHittableMarkers();
141 if (difficulty == GameDifficulty.LOW) {
142 gameSettingsSubSet = 0;
143 TurnOffOilDrumStackMarkers();
144 TurnOffFunBoxMarkers();
145 } else if (difficulty == GameDifficulty.MED) {
146 gameSettingsSubSet = 1;
147 TurnOffOilDrumStackMarkers();
148 TurnOnFunBoxMarkers();
149 } else if (difficulty == GameDifficulty.HIGH) {
150 gameSettingsSubSet = 2;
151 TurnOnOilDrumStackMarkers();
152 TurnOnFunBoxMarkers();
153 }
154 } else if (PlayerPrefs.HasKey("ClassicOn") && PlayerPrefs.GetInt("ClassicOn") == 1) {
155 gameSettingsSet = 2;
156 totalLaps = 4;
157 TurnOffArmorMarkers();
158 TurnOffGunMarkers();
159 TurnOffInvincMarkers();
160 TurnOffHealthMarkers();
161 TurnOnHittableMarkers();
162 if (difficulty == GameDifficulty.LOW) {
163 gameSettingsSubSet = 0;
164 TurnOffOilDrumStackMarkers();
165 TurnOffFunBoxMarkers();
166 } else if (difficulty == GameDifficulty.MED) {
167 gameSettingsSubSet = 1;
168 TurnOffOilDrumStackMarkers();
169 TurnOnFunBoxMarkers();
170 } else if (difficulty == GameDifficulty.HIGH) {
171 gameSettingsSubSet = 2;
172 TurnOnOilDrumStackMarkers();
173 TurnOnFunBoxMarkers();
174 }
175 }
176
177 //Prep game state
178 if (!FIRST_RUN && PlayerPrefs.HasKey("GameStateIndex") == true) {
179 gsiTmp = PlayerPrefs.GetInt("GameStateIndex");
180 if (gsiTmp == 0) {
181 gameStateIndex = GameStateIndex.FIRST;
182 } else if (gsiTmp == 1) {
183 gameStateIndex = GameStateIndex.NONE;
184 } else if (gsiTmp == 2) {
185 gameStateIndex = GameStateIndex.MAIN_MENU_SCREEN;
186 } else if (gsiTmp == 3) {
187 gameStateIndex = GameStateIndex.GAME_OVER_SCREEN;
188 } else if (gsiTmp == 4) {
189 gameStateIndex = GameStateIndex.GAME_PAUSE_SCREEN;
190 } else if (gsiTmp == 5) {
191 gameStateIndex = GameStateIndex.GAME_PLAY_SCREEN;
192 } else if (gsiTmp == 6) {
193 gameStateIndex = GameStateIndex.MAIN_MENU_SCREEN;
194 }
195 }
196
197 if (gameStateIndex == GameStateIndex.NONE || gameStateIndex == GameStateIndex.FIRST) {
198 gameStateIndex = GameStateIndex.MAIN_MENU_SCREEN;
199 }
200
201 if (gameStateIndex == GameStateIndex.MAIN_MENU_SCREEN) {
202 player1AiOn = true;
203 ShowStartMenu();
204 HidePauseMenu();
205 HideEndMenu();
206 } else if (gameStateIndex == GameStateIndex.GAME_OVER_SCREEN) {
207 player1AiOn = true;
208 ShowStartMenu();
209 HidePauseMenu();
210 HideEndMenu();
211 } else if (gameStateIndex == GameStateIndex.GAME_PAUSE_SCREEN) {
212 ShowPauseMenu();
213 } else if (gameStateIndex == GameStateIndex.GAME_PLAY_SCREEN) {
214 HidePauseMenu();
215 HideEndMenu();
216 HideStartMenu();
217 }
218
Listing 10-15GameState Main Method Details 4
类别字段difficulty
根据当前曲目难度的玩家偏好值进行设置。该值在方法行 112–175 的下一段代码中被广泛使用。Hover Racers 游戏能够调整赛道特性,以反映比赛类型和难度。这段代码非常直接。跟踪它,你会看到基于不同的轨道设置打开或关闭了哪些轨道功能。这就把我们带到了第 178–217 行代码的一个重要部分。这段代码负责通过隐藏或显示游戏的菜单屏幕来准备游戏状态。该方法中的下一个代码块如下所示。
219 //Prep blimp camera
220 if (blimpCamera == null) {
221 blimpCamera = GameObject.Find("BlimpCamera");
222 }
223
224 //Prep track settings
225 raceTrack = PlayerPrefs.GetInt("RaceTrack");
226 int tmp = PlayerPrefs.GetInt("EasyOn");
227 if (tmp == 0) {
228 easyOn = false;
229 } else {
230 easyOn = true;
231 }
232
233 raceType = PlayerPrefs.GetInt("RaceType");
234 Utilities.wr("RaceTrack: " + raceTrack);
235 Utilities.wr("EasyOn: " + easyOn);
236 Utilities.wr("RaceType: " + raceType);
237
238 if (PlayerPrefs.GetInt("RaceTrackHelp" + raceTrack) != 1) {
239 trackHelpOn = true;
240 } else {
241 trackHelpOn = false;
242 }
243
244 //Prep player positions
245 positions = new int[6];
246 positions[0] = 0;
247 positions[1] = 1;
248 positions[2] = 2;
249 positions[3] = 3;
250 positions[4] = 4;
251 positions[5] = 5;
252 players = new ArrayList();
253 players.AddRange(GameObject.Find("GameState").GetComponents<PlayerState>());
254
255 //Prep player states
256 int l = players.Count;
257 PlayerState player;
258 Transform t;
259 for (int i = 0; i < l; i++) {
260 Utilities.wr("Setting up player " + i);
261 player = (PlayerState)players[i];
262 if (player != null) {
263 player.index = i;
264 player.carType = i;
265 player.position = i;
266 SetCarDetailsByGameType(player); //sets the model and speeds
267
268 if (player.player != null) {
269 player.active = true;
270 player.controller = player.player.GetComponent<CharacterController>();
271 player.cm = player.player.GetComponent<CharacterMotor>();
272 player.camera = player.player.transform.Find("Main Camera").GetComponent<Camera>();
273 player.rearCamera = player.player.transform.Find("Rear Camera").GetComponent<Camera>();
274 player.audioListener = player.player.transform.Find("Main Camera").GetComponent<AudioListener>();
275 player.mouseInput = player.player.GetComponent<MouseLookNew>();
276 player.fpsInput = player.player.GetComponent<FPSInputController>();
277
278 t = player.player.transform.Find("Car");
279 if (t != null) {
280 player.gun = (GameObject)t.Find("Minigun_Head").gameObject;
281 player.gunBase = (GameObject)t.Find("Minigun_Base").gameObject;
282 }
283
284 player.lightHeadLight = (GameObject)player.player.transform.Find("HeadLight").gameObject;
285 if (player.lightHeadLight != null && nightTime == false) {
286 player.lightHeadLight.SetActive(false);
287 } else {
288 player.lightHeadLight.SetActive(true);
289 }
290
291 player.totalLaps = totalLaps;
292 player.currentLap = 0;
293 player.aiWaypointIndex = 0;
294 player.aiWaypointRoute = 0;
295 player.waypoints = GetWaypoints(player.aiWaypointRoute);
296 player.flame = (GameObject)player.player.transform.Find("Flame").gameObject;
297 player.gunExplosion = (GameObject)player.player.transform.Find("GunExplosion").gameObject;
298 //TODO //player.gunExplosionParticleSystem = player.gunExplosion.GetComponent<ParticleEmitter>();
299 player.gunHitSmoke = (GameObject)player.player.transform.Find("GunHitSmoke").gameObject;
300 //TODO //player.gunHitSmokeParticleSystem = player.gunHitSmoke.GetComponent<ParticleEmitter>();
301
302 if (player.gunOn == true) {
303 player.gun.SetActive(true);
304 player.gunBase.SetActive(true);
305 } else {
306 player.gun.SetActive(false);
307 player.gunBase.SetActive(false);
308 }
309
310 player.flame.SetActive(false);
311 player.gunExplosion.SetActive(false);
312 //TODO //player.gunExplosionParticleSystem.emit = false;
313 player.gunHitSmoke.SetActive(false);
314 //TODO //player.gunHitSmokeParticleSystem.emit = false;
315 player.LoadAudio();
316 } else {
317 Utilities.wr("Player model " + i + " is NULL. Deactivating...");
318 player.active = false;
319 player.prepped = false;
320 }
321 } else {
322 Utilities.wr("Player " + i + " is NULL. Removing...");
323 players.RemoveAt(i);
324 l--;
325 }
326
327 player.prepped = true;
328 }
329 SetCarDetails();
330
331 //Start game //line 324
332 ready = true;
333 FIRST_RUN = false;
334 }
Listing 10-16GameState Main Method Details 5
在前面列出的代码块的开头,在第 220–222 行配置了飞艇摄像机。第 225-242 行处理了更多的轨道设置,我们已经处理了所有的游戏准备工作。接下来,玩家位置数组被初始化为默认的六个悬浮赛车。玩家状态对象实例被加载到第 252 和 253 行的初始化玩家的ArrayList
中。现在我们必须配置每个PlayerState
对象。如果一切设置正确,我们将有六个PlayerState
对象,每个玩家一个。
“准备玩家状态”标题下的下一部分代码可以说是最重要的。这个代码负责准备球员和他们的汽车的所有方面。局部变量设置在第 256 到 258 行,玩家数组从第 259 到 261 行开始迭代。玩家的索引、汽车类型和位置是在第 263–265 行设置的,它们的模型和速度值是通过调用SetCarDetailsByGameType
方法配置的。
如果汽车的模型被成功加载,那么从 268 到 315 的代码就会执行。如果不是,则通过将prepped
字段设置为假来停用数组条目。仔细查看这段代码,注意PlayerState
类是如何准备好所有字段的;模型、摄像机和控制器都在这里设置。最后的设置是通过调用SetCarDetails
方法来执行的,第 329 行。该方法负责设置主摄像机和后视摄像机、音频监听器以及 AI 或用户输入控件。在将ready
字段设置为真并将FIRST_RUN
字段设置为假之后,该方法返回。游戏现在可以运行了!
到目前为止,我们已经在本章中讲述了大量的代码,但是我们还没有脱离险境。我想讨论几个剩下的方法。下面列出了下一组要检查的方法。
01 public void OnApplicationPause(bool pauseStatus) {
02 if (AreMenusShowing()) {
03 if (pauseStatus == true) {
04 PauseGame();
05 } else {
06 UnPauseGame();
07 }
08 } else {
09 if (pauseStatus == true) {
10 if (gameStateIndex == GameStateIndex.GAME_PLAY_SCREEN) {
11 ShowPauseMenu();
12 } else {
13 PauseGame();
14 }
15 } else {
16 if (gameStateIndex == GameStateIndex.GAME_PLAY_SCREEN) {
17 HidePauseMenu();
18 } else {
19 UnPauseGame();
20 }
21 }
22 }
23 }
01 void Start() {
02 if (style1 == null) {
03 style1 = new GUIStyle();
04 style1.normal.textColor = Color.red;
05 style1.fontStyle = FontStyle.Bold;
06 style1.fontSize = 16;
07 }
08
09 if (style2 == null) {
10 style2 = new GUIStyle();
11 style2.normal.textColor = Color.black;
12 style2.fontStyle = FontStyle.Bold;
13 style2.fontSize = 16;
14 }
15
16 if (forceGameStart == true) {
17 if (SceneManager.GetActiveScene().name == "DemoCollideTrackHelp") {
18 PlayerPrefs.DeleteAll();
19 PlayerPrefs.Save();
20 } else if (SceneManager.GetActiveScene().name == "DemoCollideScript") {
21 PlayerPrefs.DeleteAll();
22 PlayerPrefs.SetInt("BattleOn", 1);
23 PlayerPrefs.SetInt("HighOn", 1);
24 PlayerPrefs.Save();
25 } else if (SceneManager.GetActiveScene().name == "DemoCarSensorScriptAutoPass") {
26 CarSensorScript.TRIGGER_SPEED_PASSING = 0.00f;
27 } else if (SceneManager.GetActiveScene().name == "DemoCarSensorScriptGunShot") {
28 PlayerPrefs.DeleteAll();
29 PlayerPrefs.SetInt("BattleOn", 1);
30 PlayerPrefs.SetInt("HighOn", 1);
31 PlayerPrefs.Save();
32 } else if (SceneManager.GetActiveScene().name == "DemoCameraFollowXz") {
33 GameStartMenu.TRACK_NAME_1 = "DemoCameraFollowXz";
34 GameStartMenu.TRACK_NAME_2 = "DemoCameraFollowXz";
35 } else if (SceneManager.GetActiveScene().name == "Main13Demonstration") {
36 GameState.ON_GUI_SHOW_CAR_DETAILS = true;
37 debugOn = true;
38 PlayerState.SHOW_AI_LOGIC = true;
39 }
40 }
41
42 audioS = GetComponent<AudioSource>();
43 if (audioS == null) {
44 Utilities.wrForce("GameState: audioS is null!");
45 }
46 }
Listing 10-17GameState Main Method Details 6
OnApplicationPause
方法是一个 Unity 游戏引擎回调方法,在游戏失去焦点时触发。如果有菜单显示并且pauseStatus
为真,那么我们想通过调用第 4 行的PauseGame
方法暂停游戏。如果没有,我们想通过调用第 6 行的UnPauseGame
方法来解除游戏暂停。但是,如果没有菜单显示,则执行第 9–21 行的代码。在这种情况下,如果pauseStatus
为真,游戏在主游戏屏幕上,那么我们只显示暂停菜单屏幕,第 11 行。如果没有,那么我们暂停游戏。类似地,在第 16–20 行,如果pauseStatus
参数为假并且游戏处于活动状态,我们调用HidePauseMenu
方法。否则,我们取消游戏暂停,第 19 行。
这一组中的下一个方法是Start
方法。这种方法的主要职责是为课堂准备一些东西。首先,该方法加载一些在OnGUI
方法中使用的样式,以在屏幕上显示调试文本,第 2–14 行。接下来,在第 16 行,如果游戏被配置为演示场景,那么forceGameStart
标志将为真。第 17–39 行的代码用于准备代码支持的不同演示场景。最后,在第 42–45 行,音轨的背景音乐被加载。
这个类中还有最后一个方法我们还没有介绍,那就是Update
方法。尽管这是一个相当长的方法,但代码非常简单。这个方法更新游戏的 HUD 回想一下,Update
方法运行每个游戏帧,以反映当前玩家在赛道上比赛时汽车的变化,体验不同的交互并触发不同的修改器。仔细阅读这个方法,确保在继续之前理解它是如何工作的。这就是我们对GameState
课的总结。这意味着我们已经仔细检查了游戏中的每一个职业、领域和方法。
演示:游戏状态
演示GameState
类最好的方法就是打开“Main13Demonstration”场景并播放。在街机演示模式运行时,使用数字键 1-6 在汽车之间跳跃,并注意 HUD 如何自动更新以显示当前所选悬停赛车的状态。另一个很好的示范可能是玩游戏,让你所有关于游戏如何运作的知识在你的脑海中流动,就像你在游戏中实际体验一样。
第二章结论
在这一章中,我们通过完成GameState
代码回顾,看完了玩家和游戏状态类。这不是一个小壮举。这个类中有很多东西在进行,因为它是整个游戏的中心控制点。花点时间拍拍自己的背。这给我们带来了游戏的代码审查的结论。我们已经涵盖了第二章中概述的所有游戏规范,在文本的这一点上,你应该已经很好地理解了游戏对象、物理、碰撞和脚本组件如何相互作用来创建一个游戏。如果你第一次没有完全吸收,不要沮丧。发生了很多事情,你可能需要给自己更多的时间来真正掌握这一切。
十一、使其专业化
欢迎来到第十一章。如果您已经做到了这一步,那么您已经审查了大量的代码。我认为你已经赢得了你的军衔。在剩下的章节中,我们将不会回顾太多的代码,至少不会太长。相反,我们将密切关注 Unity 编辑器和游戏创作的各个方面,这些方面使你的游戏与众不同。这一章是关于使你的游戏专业化的特征、步骤和机制。我们将讨论以下主题以及如何在 Unity 中解决这些问题:
-
构建设置
-
输入映射
-
用户界面/菜单系统
-
数据持久性
-
内存管理
-
声音和音乐
-
静态对象
-
标签和层
-
人工智能对手
-
摄像机
-
项目设置
这是一个相当多样的主题列表。其中一些,如果仔细研究,可以自己写满一整本书。我们要让事情变得轻松,把注意力集中在手头主题的重要的、一般的方面。我们要处理的第一个主题是构建设置。这是一个简洁的、特定于 Unity 的主题,非常适合我们的第一次讨论。就这样,让我们开始吧。
构建设置
Unity 构建设置用于选择目标平台、配置您的构建以及启动构建和测试过程。这是任何严肃的 Unity 项目的重要组成部分,因为它用于创建游戏的开发和生产版本。Unity 支持许多构建目标,但我们将关注最常见的目标,以及我认为对于迭代开发构建和生产构建质量最重要的设置。我将重点关注影响游戏开发过程效率和游戏本身性能的构建设置。我们将看看以下平台的一些选择构建设置:
-
通用平台
-
PC、Mac 和 Linux 桌面
-
通用 Windows 平台(UWP)
-
ios
-
机器人
-
web GL(web GL)
Unity 还支持其他构建目标,我们不会在这里讨论,但是这篇评论会让你对管理任何平台的构建设置有所了解和信心。
通用平台设置
通用平台设置是一组适用于所有平台的生成设置。
开发版本:此设置用于在项目的版本中启用脚本调试和探查器支持。你可能想知道为什么你会使用这个选项,当你可以在 Unity 编辑器中调试和分析你的游戏时。事实是,目标设备的行为可以而且将会与您的开发环境不同。在项目开发过程中,尽早开始在目标设备上测试游戏非常重要。
脚本调试:该选项仅在“开发构建”设置被激活且在 WebGL 平台上不可用时可用。如果希望调试脚本组件代码,请启用此选项。我个人在需要调试的时候会启用这样的选项。我尽量保持我的开发测试尽可能的纯净。
只构建脚本:对于拥有大量资产的项目来说,这是一个非常有用的特性。如果您的项目构建时间阻碍了您的测试迭代,那么尝试使用这个构建选项。为了使用此设置,您必须进行项目的完整构建。然而,一旦完成了这些,您将能够重建项目,只需要脚本,从而更快地解决代码问题。此设置要求启用“开发构建”设置。
压缩方法:压缩方法设置很重要。根据您的目标平台,您可以设置一些压缩选项。全部测试。找到最适合您的目标设备的版本。不要忽视这个设定。你的选择会对你的游戏加载时间产生显著的影响。
PC、Mac 和 Linux 桌面设置
正如您可能已经猜到的,这类构建设置适用于桌面构建目标。
架构:macOS 上没有这个选项。它适用于 Windows 和 Linux。优化此设置以匹配目标设备的体系结构。再次,测试不同的设置,找到最适合你的游戏。
复制 PDB 文件:复制 PDB 文件选项仅在您的目标是 Windows 平台时可用。这是一个很有用的设置,可以将调试信息添加到游戏版本中。这可以让你在追踪开发过程中出现的顽固错误时获得优势。不用说,对于生产版本,应该关闭这个设置。
创建 Visual Studio 解决方案/创建 XCode 项目:此设置分别适用于 Windows 和 Mac。虽然您可能并不是在所有情况下都需要此功能,但是如果您需要对生成的项目进行更多的控制,此功能可能会有所帮助。如果您想要创建一个项目,并且该项目在编译后将生成您的最终产品,请使用它。
通用 Windows 平台(UWP)设置
本节介绍 UWP 构建设置。除了我们在这里讨论的选项之外,还有一些选项可用,所以我鼓励你看看 Unity 文档以获得更多信息。
架构:UWP 版本的这个构建设置有几个不同的目标供你选择。您可以指定 x86、x64、ARM 和 ARM64,但仅当与 Unity 的“构建和运行”选项一起使用时。这很可能是由于这个平台的普遍性。它很可能包含了之前在开发或生产版本中列出的所有架构的二进制文件。该选项允许您使用特定的架构进行测试、调试和评估。
生成类型:此设置用于控制如何根据 UWP 和 Visual Studio 生成您的项目。您可以选择 XAML、直接 3D 或仅可执行。如果您想在项目中使用 Windows XAML,您的性能会受到影响,但您可以在项目中 XAML 元素。对于大多数游戏来说,这可能是一个不常见的选择。直接 3D 选项提供最佳性能,并在基本应用窗口中呈现游戏。最后一个选项,仅可执行,是一个有趣的特性。此设置在不生成 Visual Studio 项目的预生成可执行文件中承载项目。使用这个选项来减少你的构建时间,这样你可以更快地迭代,更快地完成你的测试和调试。
构建配置:这个构建设置只适用于 Unity 的“构建和运行”特性。该设置的选项与 Unity 生成的 Visual Studio 项目中的选项相同。调试选项包括调试符号并启用 Unity Profiler。release 选项没有调试代码,但也启用了探查器。最后,主选项针对发布版本进行了全面优化。使用此构建设置来优化您的游戏并准备发布。
深度分析:该选项用于分析所有脚本代码,包括记录函数调用。使用此设置来查明游戏中的性能问题,但要小心;它使用大量的内存,可能无法像预期的那样处理非常复杂的脚本。
自动连接分析器:自动将分析器连接到游戏版本。此设置要求启用“开发构建”选项。
iOS 设置
iOS 平台有许多构建设置与我们已经介绍过的平台重叠,所以我们在这里不再赘述。但是,我们将回顾一些特定于 iOS 的选项。
在 XCode 中运行:此选项仅在 macOS 上可用,用于指定用于运行结果项目的 XCode 版本。
以 XCode 身份运行:此选项允许您指定项目是以调试模式还是发布模式运行,从而帮助您调试 iOS 游戏。如果您需要调试代码并希望使用 XCode 来完成,请使用此功能。在开始设备测试之前,您还可以使用它来运行发布版本,以检查 XCode 中的功能。
Symlink Unity 库:该选项允许您引用 Unity 库,而不是将它们复制到项目中。使用这个特性可以减小 XCode 项目的大小,并且由于项目构建时间更短,可以帮助您更快地迭代。
Android 设置
与 iOS 设置类似,Android build 设置与我们已经讨论过的选项部分重叠,因此我们在此不再赘述。我们将着重于帮助你优化和测试你的游戏的设置。
纹理压缩:在撰写本文时,Android 平台支持以下纹理压缩格式:DXT、PVRTC 等、ETC2 和 ASTC。默认设置是 ETC,但您应该了解目标设备的功能,并选择一个能为您提供最佳支持和效率平衡的设置。
ETC2 回退:这个设置我就不细说了。如果使用 ETC2 纹理压缩格式,请注意该选项。它可以帮助您的游戏在不支持 ETC2 和 OpenGL ES 3 的设备上更高效地运行。
运行设备:这个构建设置允许您指定目标附加的 Android 设备,然后您可以使用它来测试和调试您的构建。
网络光设置
WebGL 平台有许多构建设置与我们已经介绍过的选项重叠。您可以在为此平台配置构建设置时应用这些知识。
输入映射
在我看来,输入映射是职业游戏的一个重要方面。通过使用输入映射,您可以在输入和游戏之间创建一个抽象层。这允许您将相似的输入映射到一个输入标签。为什么我的游戏需要这个?好吧,如果你正在经历构建一个游戏的麻烦,为什么要把它的输入限制在一个或两个直接映射的输入。花时间改进和使用输入映射来无缝地支持多个输入。下面的屏幕截图演示了在 Hover Racers 游戏中使用的这种输入映射配置。
图 11-1
输入映射示例该图像描述了映射到输入标签的多个原始输入
正如您在前面列出的图像中看到的,我们已经将键盘输入和操纵杆输入映射到同一个标签“水平”现在,让我们看看一些输入代码,看看标签是如何使用的。
01 if (Input.GetAxis("Turn") < 0.0f) {
02 if (Input.GetAxis("Horizontal") < 0.0f) {
03 transform.Rotate(0, -1.75f, 0);
04 } else {
05 transform.Rotate(0, -1.25f, 0);
06 }
07 }
08
09 if (Input.GetAxis("Turn") > 0.0f) {
10 if (Input.GetAxis("Horizontal") > 0.0f) {
11 transform.Rotate(0, 1.75f, 0);
12 } else {
13 transform.Rotate(0, 1.25f, 0);
14 }
15 }
Listing 11-1Input Mapping in Use
请注意,在前面列出的代码中,无论输入源是什么,都会使用“水平”输入映射。玩家可以使用键盘、控制器或鼠标来使悬停赛车转弯;我们不在乎哪个。花点时间在你的游戏输入上努力吧!一个伟大的游戏可能会被糟糕的控制毁掉。相反,一个看起来不怎么样的游戏,如果控制正确,可能真的很有趣,会让人上瘾。
用户界面/菜单系统
菜单系统是另一个功能,如果它没有很好的实现,会影响你的游戏。用户习惯于在他们的游戏中使用相当不错的 UI。这是你在制作游戏时应该记住的事情。菜单系统应该简单直观。限制任何给定菜单屏幕上的选项和信息的数量,让你的玩家更容易理解正在发生的事情。
除了提供实际游戏中的菜单系统作为一个坚实的例子,我想谈谈 Unity UI 系统的两个方面,它们将帮助你更快地启动和运行。第一个是设置一个新的Canvas
,第二个是设置一个带有一些按钮的Panel
。我们还会将这些按钮连接到相关的脚本组件。打开主项目并创建一个名为“MyMenuSystemSample”的新场景打开场景,你会看到一个默认的,有点空白的“层次”面板。
右键单击“层级”面板,选择上下文菜单的“UI”部分,然后选择Canvas
条目。您将看到层次结构的两个版本:一个Canvas
和一个EventSystem
对象。我们现在将关注于Canvas
对象。选择它,并将您的注意力转向“检查器”面板。展开“画布”条目,并将“渲染模式”设置为“屏幕空间–覆盖”这将在屏幕上显示菜单,是你的菜单系统的一个好的起点。如果您想确保菜单图形尽可能清晰地缩放,请选中“像素完美”选项。确保“目标显示”设置为“显示 1”
图 11-2
Canvas Hierarchy 示例描述添加 Canvas 和 Panel 对象后的层次的图像片段
接下来,展开“Canvas Scaler”条目,并将“UI Scale Mode”设置为“Scale with Screen Size”值。我们将进行设置,使菜单屏幕居中,并随着游戏的屏幕大小上下缩放。“参考分辨率”条目应该与用于创建菜单屏幕资产的尺寸相匹配,特别是所使用的背景图像。在这种情况下,我们将“X”值设置为 640,“Y”值设置为 960。“屏幕匹配模式”应该设置为“匹配宽度或高度”,并且“匹配”选项的值应该为 0.5。这是宽度和高度的平衡。最后,“每单位像素”条目应该与原始菜单背景图像的像素密度相匹配。在这种情况下,将其设置为 326。
图 11-3
画布层次和设置示例描述完整演示场景层次并关注画布设置的屏幕截图
现在我们将向我们的Canvas
添加一个Panel
对象。在层级中选择Canvas
对象,并右键单击。选择“UI”选项,然后选择“Panel”条目。您的Canvas
对象现在将有一个Panel
子对象。选择新的子对象,并将注意力放在“检查器”面板上。展开“矩形变换”条目,并将“宽度”和“高度”值分别设置为 460 和 240。这些是我们将使用的背景图像的自然尺寸。
我们希望我们的菜单场景保持居中,所以我们接下来将研究“锚”和“枢轴”选项。“X”和“Y”的“最小”和“最大”锚值应该设置为 0.5。这些值代表一个百分比,0.0 到 1.0 或 0%到 100%,如果你想这样想的话。现在将“X”和“Y”的“轴”值也设置为 0.5。这将把轴心点和锚点设置到菜单的中心。“旋转”、“PosY”、“PosX”和“PosZ”字段都应设置为零。
接下来让我们看看Panel
对象在“检查器”面板中的“图像”条目。展开它并选择“源图像”选项,单击选择按钮,在弹出窗口中键入“菜单”并找到名为“MenuPanel_512x512”的条目在这一步之后,您可能需要重新设置“矩形变换”条目的“宽度”和“高度”值,所以一定要仔细检查它们。将“图像类型”选项设置为“切片”并选中“填充中心”复选框。
图 11-4
面板层次和设置示例显示面板对象的“矩形变换”和“图像”设置的屏幕截图
我们在这一部分要做的最后一件事是添加菜单屏幕特性、一些文本和两个按钮。然后我们将按钮连接到一个脚本,运行一些测试,然后就到此为止。
步骤 1:添加一个文本对象
-
从层级中选择
Panel
对象并右键单击。 -
选择“UI”选项,然后选择
Text
条目。一个新的Text
对象将被添加为Panel
对象的子对象。 -
选择它,然后注意“检查器”面板中的“矩形变换”部分。
步骤 2:配置新的文本对象
-
将“PosY”字段的值设为 60。
-
现在向下滚动到“文本”部分并展开它。将“文本”字段的值更改为“Hello World”将“字体样式”改为“粗体”,将“字体大小”改为 20。
-
在“段落”小节下,将“对齐”选项设置为“文本居中”。
步骤 3:添加两个按钮对象
- 遵循与添加
Text
对象到Panel
相同的过程,除了这次添加两个Button
对象。所有三个 UI 元素都应该是Panel
的子对象。
步骤 4:配置按钮对象
-
将第一个按钮对象重命名为“ButtonOk”,将第二个按钮命名为“ButtonCancel”
-
选择
ButtonOk
对象,在“检查器”面板中展开“矩形变换”部分。将“PosX”字段的值设置为–90。对ButtonCancel
对象做同样的事情,除了使用值 90。
简单的菜单屏幕正在形成。请注意,这两个按钮本质上是父对象。展开第一个按钮并选择Text
子对象。将其“文本”值设置为“确定”对第二个按钮重复此步骤,只是将其“文本”值设置为“取消”
图 11-5
具有 UI 元素的面板层次示例描述添加了面板对象和 UI 元素的场景层次的图像片段
我们将向Canvas
对象添加一个脚本组件。在层次中选择Canvas
对象。现在,转到“项目”面板,搜索以下字符串,“DemoMenuSystemSample”。找到同名的脚本组件,并将其添加到Canvas
对象中。接下来,右击Canvas
对象并选择“属性…”入口。将产生的弹出窗口向旁边移动一点。选择ButtonOk
对象,在“检查器”面板中展开“按钮”部分。
向下滚动到“点击时”部分,然后单击“+”按钮。将结果行条目的类型设置为“编辑器和运行时”将“Demo Menu System Simple”脚本组件从属性弹出菜单拖到“On Click”行条目的“Object”字段。选择“BtnOkClick”功能,将“无功能”的值更改为“DemoMenuSystemSample”。对“取消”按钮做同样的事情,只是这次选择“BtnCancelClick”功能。我们把所有东西都装好了。拿着它转一转,检查日志中哪个按钮被点击的指示。打开“DemoMenuSystemSample”场景可以找到这个简单屏幕的演示。
图 11-7
完成的 UI 层次示例描述完成的 UI 演示的层次和场景的屏幕截图
图 11-6
面板层次结构和设置示例描述 ButtonOk 对象配置的屏幕截图
数据持久性
我们在审查 Hover Racers 代码库时讨论了数据持久性。这是使用 Unity APIPlayerPrefs
类的数据持久性的简化形式。虽然它非常适合存储简单的数据,但对于复杂的信息,它可能不是最佳的解决方案。序列化技术,就像我们用来存储跟踪时间数据的那种,可能是一种选择,但你不应该把它用于大量数据或非常复杂的数据。在这些情况下,您应该探索数据文件的读写。
内存管理
既然你是用 C#,一种内存管理语言来编写你的 Unity 游戏,那么你就不用担心内存管理,对吗?错了。垃圾收集使用资源,垃圾收集器要做的工作越多,它破坏游戏流畅帧速率的机会就越大。确保跟踪每一帧运行的方法,并尽量减少悬挂对象的创建,这些对象在其他任何地方都不会被引用,并且在方法完成时会丢失。
在 Hover Racers 游戏中,我们使用私有类字段作为局部方法变量的替代,以避开垃圾收集器。然而,这种方法很快会变得很麻烦,不推荐用于更复杂的方法、类。当你编码的时候,记住内存管理,你已经完成了一半。通过剖析你的游戏,仔细检查 Unity 引擎的Update
方法或其他频繁触发的方法(如碰撞回调方法)所涉及的方法和类,清理任何遗留问题。
声音和音乐
这似乎是显而易见的,但我还是要回顾一下。音效和音乐对任何游戏都非常重要,包括你的。如果您无法创建音频资源,请不要担心。包括 Unity store 在内,有很多地方可以让你接触到美妙的音乐和声音。一般来说,玩家的每一次交互,有时通过他们的角色,都应该引出某种声音效果。如果可以的话,你还应该找一个像样的背景音乐和环境声音。我知道这对于一个游戏版本来说是一个很大的挑战,但是如果你记住这一点,并且在开发过程中使用占位符,那么当需要润色和完善你的项目时,你将会处于一个很好的位置。
静态对象
如果你在 Unity 编辑器中选择任何GameObject
并在“检查器”面板中检查该对象的配置,你会注意到面板右上角名为“静态”的标签旁边有一个小复选框。如果你游戏中的一个物体不移动,不与角色或 AI 对手交互,也不与其他物体交互,你应该将其标记为静态。这样做可以提高游戏的效率,因为静态对象会从某些运行时计算中逃逸出来。
标签和层
标签和层是在游戏中组织交互的重要功能。你可以找到他们的管理屏幕下的“编辑”➤“项目设置……”主菜单选项。在出现的设置窗口左侧选择“标签和层”条目。标签是您可以分配给一个或多个GameObject
的参考名称。例如,游戏中所有的悬停赛车都有标签“玩家”标签帮助你识别特定的GameObject
,并且可以帮助以编程方式将游戏对象连接到脚本字段。
Unity 中的层用于定义哪些GameObject
可以相互交互。正如 Unity 文档中提到的,它们通常被Camera
对象用来渲染场景的一部分,被灯光用来照亮场景的一部分。我们并没有真正在 Hover Racers 代码库中使用层。然而,如果你看看游戏屏幕右上方的飞艇摄像机,你可以想象一个场景,摄像机没有显示赛道的完整渲染,就像现在这样。例如,可以使用层对其进行配置,以仅显示某些对象,仅此而已。在游戏开发过程中,请记住这些特性。
人工智能对手
这是一个有点棘手的话题。首先,在这种情况下,AI 有点用词不当。目前,或许还有一段时间,游戏人工智能不是真正的人工智能或机器学习。就像某些物理计算可以被近似从而被简化一样,游戏人工智能意味着尽可能地模仿人类玩家,并且这样做看起来很聪明。在不太遥远的未来,每台像样的计算机都将拥有类似于今天 GPU 工作方式的专用 AI/ML 硬件,在某些情况下,它们已经存在。看看谷歌的 TPU 和苹果的 M1 芯片。
但是现在,我们将不得不接受近似和简化的游戏人工智能。这是一个很大的话题,很大程度上依赖于你正在制作的游戏类型。游戏人工智能实现的一个主要方面是模仿用户输入并管理这些输入来创建一个真实的人工智能玩家。考虑到这一点,您可能希望从实际的输入中抽象出输入处理,以便可以通过编程实现相同的功能。
当实现悬停赛车的人工智能,我们有简单的好处。盘旋赛车手有一个固定的,指定的,他们可以移动的地方,赛道。没有它,你将不得不使用像 A*或 Unity 的导航网格系统的寻路技术。此外,参赛者只能加速、减速或转弯。赛道的航路点系统告诉他们向哪个方向前进,赛道的中心点在哪里,转弯多少,以及何时减速。这就是我们在这场比赛中像样的人工智能对手所需要的一切。我建议在项目的早期就考虑和规划人工智能。
图 11-8
Hove Racer 人工智能逻辑示例一个截图,描述了人工智能对手的计算,决定向哪里移动以及转弯多少
摄像机
摄像机可以为你的游戏增添一份精彩。很多游戏使用两个或更多的摄像头来提供当前关卡的不同视角。你最喜欢的 FPS 上的导航 HUD 很可能是一个摄像机,它被设置为只能看到特定的对象层,这些对象层被用来描述给定关卡上玩家环境或位置的简化版本。用新的独特的方式给你的游戏添加摄像头真的可以让你的游戏脱颖而出。关于如何定位和调整相机大小的例子,请查看“Main13”或“Main14”场景,这是该项目的主要游戏场景。
项目绩效
最后但同样重要的是,还有项目设置。你的 Unity 项目涉及到很多设置,其中一些我们已经提到过了。有比我希望在这篇文章中解决的更多的问题。然而,我想花一点时间来讨论“质量”设置。您可以在主菜单的“编辑”条目下找到项目设置选项。可以从弹出窗口的左侧选择“质量”部分。
花点时间测试这些设置,以获得游戏质量和性能的正确平衡。您可以使用一些 Unity 性能监控工具来检查游戏的运行情况。我们要看的第一个工具是“游戏”面板的“统计”功能。打开主游戏场景,“Main13”或“Main14”,在 Unity 编辑器中运行游戏。请注意,在面板的右上角有一个“Stats”按钮。点击它,你应该会看到类似下面的截图。
图 11-9
描述场景统计对话框的截图
这个小弹出窗口有很多关于你的游戏的有用的高级信息,可以用来识别你的项目的问题。“为什么我的游戏只能以 30 FPS 的速度运行?”你问。问得好。要获得游戏性能的真实画面,请停止游戏,然后单击“统计”按钮旁边的“游戏时最大化”按钮。现在重新开始游戏,再次打开“统计”弹出窗口。此功能的一个示例如下。
图 11-10
Hove Racers 最大化统计数据示例一个截图,描述了在 Unity 编辑器中运行的主游戏,最大化,显示了统计数据弹出窗口
看看前面列出的图像中的帧速率。请注意,它在 1441 × 731 像素下以每秒 91 帧的速度运行。那还不算太寒酸。计划定期检查游戏的性能,尤其是在添加新功能和游戏机制之后。但是如果我在 Unity 编辑器中测试时发现性能问题,会发生什么呢?统计弹出窗口没有给我足够的信息来解决这个问题。不要害怕!Unity Profiler 可以提供帮助。可以在以下主菜单位置找到该分析器:“窗口”➤“分析”➤“分析器”让我们再次运行这个游戏,观察数据流入分析器的图表和摘要部分。看看下面的例子截图。
图 11-11
Hove Racers Profiler 示例描述 Unity profiler 的屏幕截图,其中包含 Hover Racer 游戏的运行数据
花点时间玩玩分析器。在左侧切换不同的指标,以查明是什么导致您的游戏行为不当。单击图表将在窗口的底部面板中显示详细信息。您可以访问关于垃圾收集器占用了多少时间、某些方法调用完成了多少时间等信息。分析器是一个强大的工具。知道如何使用它的开发者可以胜任并快速地解决他们游戏中的低效问题。成为那些开发者中的一员。
第二章结论
这就引出了本章的结论。在这一章中,我们看了一些主题,我觉得它们会帮助你把下一个 Unity 游戏做得更好。我们讨论了各种各样的主题,涉及到游戏效率和优化实现方面的问题。让我们在这里回顾一下这些主题。
构建设置:在这一部分,我们介绍了一些关键的构建设置,并指出了一些会影响游戏性能的有用选项。我们还讨论了许多选项,可以帮助你在一系列不同的平台上测试和调试你的游戏,随后列出。
-
通用平台设置
-
PC、Mac、Linux 桌面设置
-
UWP 设置
-
IOS 设置
-
Android 设置
-
网络光设置
输入映射:输入映射部分讨论了如何设置输入,以便输入映射特性能够对它们进行抽象。我们讨论并观察了应用于一个游戏的多个功能相同的输入是如何共享同一个标签的。这有效地创建了一个抽象层,允许您针对输入标签而不是直接针对输入源进行编码。
UI/菜单系统:在这一部分,我们讨论了一个坚固的 UI 如何增强你的游戏,并为你的玩家提供良好的体验。我们还逐步完成了构建一个简单的双按钮菜单屏幕的过程,附带了处理按钮单击事件的脚本,并提供了该场景的完整演示版本供您阅读。
数据持久性:我们花了一点时间讨论了数据持久性的主题,并提到了 Hover Racers 游戏使用的PlayerPrefs
类。我们还简要讨论了使用序列化/反序列化技术来存储稍微复杂一些的数据。最后,我们建议对高度复杂和/或大量的数据使用数据文件。
内存管理:关于内存管理,我们提出了一些关于如何控制垃圾收集的想法,正如在所提供的游戏项目中实现的那样。我们还强调了了解并主动解决代码如何影响垃圾收集器的重要性。
声音和音乐:在这一部分,我们讨论了井…声音和音乐。我们建议尽可能为所有玩家交互、背景音乐和环境声音设置音效。我们还提到了在开发过程中使用占位符,允许您专注于游戏代码,同时让您可以在以后灵活地润色和完善您的游戏声音。
静态对象:静态对象部分提醒你尽可能花时间让游戏中的对象保持静态。当 Hover Racers 游戏项目在“Main13”和“Main14”场景中使用静态对象时,您可以查看该功能的使用情况。看一看。
标签和层:在这一节中,我们简要地谈到了标签和层,并讨论了如何在游戏中使用它们。Hover Racers 代码库经常使用标签来帮助以编程方式识别某些游戏对象。
人工智能对手:人工智能对手部分列出了一些关于游戏人工智能的一般想法,并谈到了应用于悬停赛车游戏的具体实现。
相机:在这一部分,我们讨论了相机以及如何使用它们来增强你的游戏。我们还提到了检查游戏的多摄像机设置是如何实现的。
项目设置:有大量的项目设置,要涵盖所有的设置需要相当多的时间和很多页的文字。我们所做的是把重点放在项目设置的“质量”部分,并把它作为 Unity 编辑器的统计弹出和剖析工具的一个延续。
我希望这一章为你提供了一些思考的素材。至少,当你开发下一个伟大的游戏或者改进当前的游戏时,你需要记住一些事情。在这篇文章的下一章,我们将看看如何添加一个新的赛道到悬停赛车游戏!
十二、增加一条新赛道
我们已经从头到尾审查了代码。你已经看到了每一个游戏机制和交互,现在我们要在 Unity 编辑器中建立一个新的轨道,并将其插入到游戏中。这将强化你在代码审查过程中学到的概念,并向你展示GameObject
在哪里遇到代码。我们将一步一步地进行,将预设的游戏对象添加到场景中,建造一个新的赛道,然后通过将赛道连接到游戏的开始菜单屏幕,最终将赛道添加到游戏中。好了,我们已经有了计划,让我们开始吧!
跟踪环境和清理脚本
我们要做的第一件事是创建一个名为“MyTrack15”的新场景并打开它。一旦完成,我们将需要一个地方来放置我们的新赛道。让我们把注意力转向“层次”面板。右键单击面板内部并选择“创建空白”将新的GameObject
重命名为“特性”再执行两次该操作,并创建以下两个对象:“Menus”和“SceneOther”将默认的Main Camera
和Direction Light
游戏对象移动到刚刚创建的SceneOther
对象中。您的层次结构面板应该在层次结构的根处有以下条目。
图 12-1
新赛道层级示例 1 描述新赛道开发过程中场景层级的图像片段
我们将使用这些空的GameObject
就像文件系统中的文件夹一样。从性能的角度来看,这是非常好的,并且实际上是保持项目的游戏对象有组织的一个很好的方法。为了给赛道设置一个简单的环境,我们将在场景中创建一个现有预设对象的实例。在“项目”面板中,找到“预设”文件夹,并找到名为“板”的条目把它拖到“层级”面板,然后放到“特色”游戏对象中。对名为“毁灭者”的预设重复这个过程您的层次结构应该如下所示。
图 12-2
新赛道层级示例 2 添加棋盘和破坏者对象后,描述场景层级的图像片段
我们需要“棋盘”游戏对象稍微宽一点。在层次中选择它,并将检查器中的“缩放 X”值从 1 更改为 1.5。那会让板子更宽一点,让我们的轨道能放进去。您的设置应该如下图所示。游戏对象应该比棋盘大得多,并且位于棋盘下方。
图 12-3
新赛道场景示例 1 描述棋盘和破坏者游戏对象及其相对位置的屏幕截图
显然,它不必完全匹配,但您的设置应该类似于前面列出的屏幕截图所示。在下一节中,我们将设置赛车和游戏状态对象以及相关的脚本。
悬停赛车和游戏状态对象
在本节中,我们将向场景中添加赛车手、游戏状态对象和相关脚本。在我们拥有一条功能齐全的综合赛道之前,我们还需要完成一些步骤,但这将使我们离目标更近一步。我们将从悬停赛车出发。把你的注意力放在“项目”面板上,找到“预设”文件夹。搜索名为“StartingSet”的预设,并将其拖动到“Hierarchy”面板中,使其成为一个根GameObject
条目,如下图所示。
图 12-4
新赛道层次示例 3 描述添加 StartingSet 对象后的场景层次的图像片段
执行相同的步骤,除了这一次找到名为“游戏状态”的预设,并将其拖动到层次结构中,使其成为SceneOther
对象的子对象。下图描述了当前场景层次。在你的层级面板中应该有一个匹配的设置。
图 12-5
新赛道层级示例 4 添加 GameState 对象后描述层级的图像片段
我们必须调整StartingSet
对象的位置。选择它并将“位置 Y”值调整到–66。您可以有稍微不同的定位,因此如果值-66 没有使参赛者靠近棋盘表面,请使用“场景”面板并重新定位他们,以便他们靠近棋盘表面但不接触它。接下来,展开SceneOther
父对象并选择Main Camera
子对象。在“检查器”面板中,点按“名称”栏左侧的复选框以停用摄像机。
当我们这样做的时候,让我们为一些我们知道不会移动的场景对象切换静态标志。展开Features
游戏对象,并将Board
和Destroyer
游戏对象设置为静态,如果它们还没有这样配置的话。如果出现提示,将静态标志应用于所有子对象。我们来测试一下场景。单击播放按钮;你应该通过玩家的摄像头看到棋盘。等待几秒钟,你就可以控制赛车了。这类似于您以前使用过的演示场景。当我们完成设置后,会有一个倒计时显示,表明这一时间的流逝。现在,我们只能等待几秒钟。
运行场景后,如果悬停赛车在场景开始时向下浮动,那么它有点太高了。将StartingSet
向下调整一点,然后再次测试。同样,如果比赛者在场景开始时弹跳到空中,或者落到棋盘表面以下,那么比赛者就有点太低了。将StartingSet
调高一点,然后再次测试。请记住,当场景演示停止时,在场景运行时所做的场景更改将会丢失。停止场景后进行调整,以便正确存储和保存。在场景运行时进行调整,以便在运行时进行测试。接下来,我们将处理赛道和航路点。
轨迹和航路点对象
新跑道开始成形了。我们已经设置了许多GameObject
和脚本组件。我们可以运行场景,开车四处转转,但事情相当贫瘠。只有一个灰色的大白板,没别的了。让我们在我们的板上添加一个轨迹和航路点。我创建了一个简单的轨道供我们使用。通常情况下,你可以将跑道模型或预设拖到层级上来建立一个跑道。在这种情况下,我已经为你做了工作。
将你的注意力转移到“项目”面板中的“预设”文件夹,并找到“简单轨迹”预设。将预设拖放到层级中,使其成为Features
游戏对象的子对象。将SimpleTrack
对象的位置设置为“X”= 35,“Y”= 0,“Z”=-9。你可以对你的板有稍微不同的定位,如果是这样,不要担心,只要移动轨道,使它在板的中心。确保道路是可见的,并且轨迹没有在棋盘表面上盘旋。设置的截图如下。
图 12-6
新的赛道场景示例 2A 屏幕截图描绘了赛道在棋盘内居中的场景
现在我们有了一条赛道,让我们为这条赛道重新定位悬停赛车到一个好的起点。将StartingSet
对象的位置设置为" X" = -430," Y" = -66," Z" = -254。这应该定位在前面列出的截图中显示的赛车。如果你的对象的位置不同,那么就不要使用这里列出的值。相反,使用 Unity 编辑器重新定位汽车,使其类似于所示的设置。
接下来让我们添加赛道的航点。找到“SimpleWaypoints”预设,并将其拖动到层次结构中,使其成为Features
游戏对象的直接子对象。如有必要,调整路点的位置,使其与赛道的弯道和直道对齐。调整StartingSet
的位置,使悬停赛车位于赛道该侧的航路点之间。在我们进入下一部分之前,我还想做最后一件事。
展开SceneOther
父游戏对象并选择GameState
子对象。在“检查器”面板中展开“TrackScript”脚本组件条目。将“圈数”字段设置为一个较大的数字;因为这条赛道不是很大,我们会比平时多跑几圈。用八圈之类的。下图描述了场景的当前设置。在你的场景中应该有一些非常相似的东西。
图 12-7
新的赛道场景示例 3A 屏幕截图描述了配置了板、赛道和航路点以及一组悬停赛车的场景
我应该提到的是,在某些情况下,比赛类型会覆盖您刚刚配置的圈数。如果你查看赛道上的路点列表,你会注意到其中一些会触发帮助通知。一个这样的帮助通知航路点TrackHelpTurn
,实际上关闭了航迹帮助通知系统,应该是航迹上最后一个这样的航路点。否则,它将过早关闭帮助通知。我还想指出,包含所有悬停赛车的StartingSet
位于最后一个航路点之后,第一个航路点之前。确保在你的场景中有一个相似的设置。
跳转、增强、菜单屏幕等等
在这一节中,我们将为曲目添加一些有趣的功能。我们还会将菜单屏幕添加到曲目中,并将我们的曲目连接到开始菜单。请注意,我们已经能够添加新的特点和功能到轨道,它只是无缝地插入到游戏中。当我们在游戏板上添加了悬浮赛车后,这个场景就可以玩了。如果你回忆一下代码回顾章节,代码被设计成通过关闭一个不能被正确配置的类来对丢失的脚本组件和游戏对象做出反应。这使得我们可以分阶段建造新的赛道,每个阶段结束时都有一个功能场景。请记住,您总是可以在项目的发布版本上注释掉这些检查,以确保最高的性能。
要添加一组新的轨道功能,包括助推、跳跃、趣味框和战斗模式标记,请进入“项目”面板中的“预设”文件夹,找到“简单功能”预设。将条目拖动到层次结构中,使其成为Features
父游戏对象的子对象。确保新的轨迹对象与轨迹正确对齐,如下图所示。
图 12-8
新的赛道场景示例 4A 截图描绘了配置了助推、跳跃、趣味框和战斗模式标记的赛道
我们将对下面的预设、BgMusic
和BlimpCamera
做同样的事情,除了我们将它们拖放到SceneOther
父对象中,这样它们就是SceneOther
父游戏对象的直接子对象。将这些对象添加到场景中会为场景启用背景音乐和飞艇摄像机功能。接下来,我们将添加菜单系统,并配置代码从开始菜单运行您的曲目。为了设置菜单系统,我们需要将以下预设对象拖放到场景层次中的Menus
父游戏对象中:
-
GameExitMenu
-
游戏帮助菜单
-
GameHUD
-
GameOverMenu
-
GamePauseMenu
-
游戏开始菜单
我们需要一个EventSystem
对象来让 UI/菜单系统正常运行。右键点击Menus
父游戏对象,选择“UI”选项,然后选择EventSystem
条目。你应该有一个EventSystem
游戏对象作为父菜单的子菜单,以及所有支持的菜单屏幕。您应该有一个类似于下面截图的设置。
图 12-9
新赛道层级示例 5 描述配置菜单系统后场景层级的图像片段
花点时间想想我们迄今为止为让赛道恢复运行做了些什么。我们主要使用预置,但这只是为了节省时间,减少启动和运行所需的步骤。发展的阶段是相同的;它们只是为了加快我们的速度而压缩的。我们需要做的最后一件事是将我们的新曲目插入菜单系统代码,这样我们就可以将它作为游戏的一部分,然后进行测试。
图 12-10
新赛道场景示例 5A 描述配置了菜单系统的场景的当前状态的屏幕截图
要将我们的新赛道连接到游戏,打开GameStartMenu
和GameOverMenu
类进行编辑。更改每个类中的TRACK_NAME_4
类字段,并用你正在处理的场景名称替换列出的场景名称。如果你严格遵守文本,那么这个名字应该是“MyTrack15”我们还需要将场景添加到项目构建配置中。为此,请从以下菜单位置打开构建设置:“文件”➤“构建设置”在结果窗口的顶部是项目构建过程中包含的场景列表。确保“MyTrack15”场景已打开,然后单击“添加打开的场景”按钮。如果需要,可以在列表中上下拖动场景条目来重新定位它。
图 12-11
新赛道构建设置示例描述项目构建设置中包含的场景列表的屏幕截图
为了比较和调试的目的,可以使用“开始”菜单上的“赛道 3”按钮来访问这个赛道的演示版本。恭喜你!您已经成功地在游戏中添加了一条新赛道!
第二章结论
这就引出了本章的结论。我们看到了如何在 Unity 编辑器中使用预设对象来加速赛道的创建。预设允许我们使用预先配置的对象作为构建模块,大大减少了开发时间。请注意,我们已经详细讨论过的所有不同脚本都与正确的游戏对象相关联,当我们将它们拖放到场景的层次结构中时,它们可以插入到我们的核心游戏代码中。
这是通过设计实现的。我们已经回顾过的所有方法转义代码,我们已经多次提到这个短语,用于关闭代码库中未使用或配置不当的类,以在整个赛道开发过程中尽可能保持游戏的功能。让我们回顾一下创建新赛道的步骤。
轨迹环境和清理脚本:在轨迹开发的这个阶段,我们为轨迹创建了一个简单的位置。我们还添加了一个销毁器脚本来处理删除不小心从板上掉下来的对象。你自己试试。如果你在触发一个助推调节器后击中一个跳跃,你可以设法飞离棋盘。看看你做的时候会发生什么。
Hover Racers 和 GameState 对象:赛道开发过程的这一部分将 hover racers 的初始集合添加到棋盘上,并创建了游戏大脑的一个实例,即带有关联的GameState
脚本组件的GameState
对象。在赛道开发的这一点上,你将能够演示冲浪板并在场景中驾驶。
轨迹和航路点对象:添加轨迹和航路点对象会打开正在开发的轨迹上的许多功能。人工智能的对手现在开始在赛道上比赛,并且像偏离赛道,错误的方式和卡住的赛车支持这样的功能现在已经启用。
跳跃、助推、菜单屏幕等等:如果没有一些跳跃和其他酷的方面,悬浮车赛道有什么好的?在赛道开发的这一阶段,我们通过在赛道上添加背景音乐、飞艇摄像机、游戏菜单屏幕以及许多助推、跳跃、趣味框和战斗模式标记来完成事情。最后,我们通过在菜单系统中注册来连接赛道和比赛。
花点时间思考一下我们刚刚完成的曲目创建流程。浏览不同的游戏对象,看看有哪些脚本在使用。回想一下我们对这些脚本的审查,并尝试将您在赛道上比赛时看到的功能与驱动它的代码联系起来。
十三、总结
欢迎到本文结尾!如果你正在阅读这篇文章,那么我假设你已经完成了对一个完整的、相当复杂的 Unity 赛车游戏的深入审查。这没什么可大惊小怪的。这是一个严峻的挑战,审查这么多的材料,并建立自己的游戏,一个小小的示范赛道。你克服了挑战,我赞扬你。让我们花点时间来回顾一下我们在这篇课文中共同完成的一些事情。
造诣
在这篇课文中,我们已经设法涵盖了很多内容。我们从基础开始,建立并运行我们的 Unity 开发环境。我们对书中包含的游戏项目进行了测试,然后认真地、详细地审查了大量代码。以下是我们在这段旅程中取得的一些显著成就。
游戏规范:我们对游戏开发过程采取了专业的方法,并概述了在赛道上比赛时游戏机制遇到的所有不同的交互。这些概念被描述为带有支持文本的图表,以清楚地描述相关的游戏情况。
简单的交互脚本:您必须详细了解驱动赛道和弹跳障碍游戏机制的更简单的交互脚本。这些类向我们展示了代码是如何通过碰撞处理程序与游戏对象交互的,并给了我们简单的例子,演示场景,我们可以通过游戏来可视化。
复杂的交互脚本:复杂的交互脚本为游戏的战斗模式和许多与碰撞相关的游戏机制提供动力,如助推、跳跃和战斗模式标记。事情变得有点复杂,但是我们详细地回顾了代码,并且通过尝试不同的演示场景,有机会看到代码的运行。
助手类:我们深入研究了游戏代码库使用的助手类。这给了我们一个很好的例子,说明常规 C# 类如何与脚本组件混合来处理像排序和序列化/反序列化数据这样的深奥任务。
代码结构:虽然我们没有直接解决这个问题,但它一直在幕后。我们回顾了代码库中几乎每个脚本组件都使用的基类。我们还看到,作为菜单系统类的一部分,专门的基类集中了几个菜单屏幕的类似功能。最后,我们熟悉了游戏代码库和类的整体结构。
项目结构:有组织的项目结构的一个很好的例子,包括在“项目”和“层次”面板中组织资源和游戏对象,通过 Hover Racers 项目本身来表达。从项目的组织方面、层次结构和代码来看,花时间将项目作为一个整体进行评审绝对是值得的。
输入类:我们一起回顾了所有不同的输入类,包括驱动 Hover Racers 游戏的输入映射。这为您提供了一个处理来自键盘、鼠标、控制器和触摸屏的输入的很好的工作示例。游戏项目提供了一个处理来自不同来源的抽象输入的很好的例子。一定要复习课文的这个方面,因为它将来肯定会派上用场。
人工智能对手:你必须亲眼看到人工智能对手的实现。最初是通过实现路点来引导人工智能控制的赛车,随后是抽象的输入处理程序和模拟用户输入的人工智能特定的输入方法。游戏人工智能总是一个挑战,我遵循的规则是通过向你的人工智能控制的角色提供尽可能多的数据来尽可能多地作弊。我的第二个经验法则,前面提到过,是从实际的功能代码中抽象出输入处理程序,这样 AI 逻辑和人类玩家都可以使用它。
菜单系统类:我们回顾了菜单系统类,展示了如何通过游戏状态和玩家状态类将与菜单屏幕相关的脚本组件连接到游戏的其余部分。我们还构建了一个简单的菜单屏幕,演示基本的定位、调整大小和基于脚本的事件处理。
游戏和玩家状态管理:通过使用BaseScript
类的初始化方法,你可以直接看到游戏状态控制在项目范围内的实现。我们看到了每个主脚本组件是如何扩展这个类的,并且如果检测到配置错误,就会“关闭”该类的每个实例。如果应用得当,这个特性会增加代码的稳定性,并允许游戏在场景中缺少脚本组件或游戏对象时运行。我们在第十二章中为游戏建造一个新的赛马场时经历了这一点。您还可以体验这种方法的集中化优势,因为代码库中的每个主要脚本组件都建立了对GameState
对象和相关脚本的引用。
Unity 提示:我们讨论了一些 Unity 提示,你可以用它们来让你的下一个游戏变得更好。我们讨论了提高效率和加快构建时间的技巧,这样您可以更快地迭代,完成更多的测试和调试。我们还谈到了被动提示,也就是你在构建游戏时应该记住的东西。
添加一个新的轨道:你获得了一些直接的经验,可以使用预设的对象为悬停赛车游戏构建一个新的轨道。你可能没有意识到的是,每一条赛道其实都是一个完整的游戏。主菜单屏幕提供了一些按钮,可以让你跳到游戏中不同的主场景,因为没有更好的词了。这个练习向我们展示了 Unity GameObject
如何与相关的脚本组件和代码库一起工作来制作一个完整的游戏。
承认
- Katia Pouleva:一位出色的艺术家,他创作了大量的悬停赛车游戏艺术,还清理了本文中的所有截图。链接:
https://katiapouleva.com
-亚采克·扬科夫斯基:在“Main13”场景和其他演示场景中使用的“简单模块化街道套件”的创造者。
链接:不适用
- Unity Technologies:游戏主场景中使用的“棚户区:混凝土墙粗糙”和“棚户区:混凝土管道”模型的创造者。
链接:不适用
- Reikan Studio:游戏中用于悬停赛车手的“Hover9k”原始模型的创造者。
链接:不适用
- BOXY KU:创作了一些在 Hover Racers 游戏中用作背景音乐的音乐。
链接: assetstore。团结。com/packages/audio/music/electronic/electronic-future-city-free-21756
- Duane’s Mind:“混凝土护栏、木制托盘和油桶道具”模型的创造者,该模型用于主轨道和一些演示场景。
链接: assetstore。团结。com/packages/3d/props/industrial/concrete-barrier-wood-pallet-oil-drum-props-2698
-发行人 971:在游戏的两个主要场景“主 13”和“主 14”中使用的“混凝土屏障”的创造者链接:不适用
-盖伊·科克罗夫特:游戏中一些音效所用的“8 位复古狂暴:免费版”的创造者。
链接: assetstore。团结。com/ packages/ audio/ sound-fx/ 8 位-复古-横行-自由版-7946
-MoppySound:“8 位自由动作”的创造者,它是游戏中一些音效的来源。
链接: assetstore。团结。com/packages/audio/music/electronic/8-bit-action-free-19827
你将何去何从
从这里你可以进入很多方向。请允许我提几点建议。
修改现有的游戏:您可以添加由新游戏对象驱动的新游戏机制,以及它们与现有对象集之间的交互。您可以创建新的赛道进行比赛,或者为 Hover Racers 游戏添加多人支持。天空是无限的。
给游戏添加粒子效果:最初的粒子效果是用来给赛车添加尘埃云,触发助推修改器的火焰条纹,以及当赛车的大炮开火时爆炸的云,这些都被否决了,所以我把它们注释掉了,但保留了注释。一个很好的练习就是在游戏中加入新的更新的粒子效果。
创建一个全新的赛车游戏:使用悬停赛车项目作为一个新的赛车游戏的起点,或者把整个事情扔出去,从头开始;需要的时候随时可以作为参考。
添加新的模型、音乐和音效:访问 Unity 资产商店,寻找新的模型、音乐或音效,并将它们添加到游戏中。
创建一个全新的游戏:利用你已经获得的知识,开始做你一直想做的游戏。
说再见
嗯,我该走了。我希望这本书能在你的游戏开发之旅中帮助你,并为你提供一些知识、娱乐或智慧。我祝你在未来的努力中好运和成功!再见,再见。
更多推荐
所有评论(0)