【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili
教程源地址:https://www.udemy.com/course/2d-rpg-alexdev/
本章节实现了类似于黑魂3,艾尔登法环中的传送点
火堆
死亡后传送到最近的火堆
警告!!!
本章节遇到了几个更加棘手的bug,脚本的加载顺序错误,各种实例化错误,以下是我的解决办法
1.三个管理器脚本执行顺序如以下,GameManager一定要比SaveManager提前
2.Checkpoint.cs中实例化直接放到Awake()中
3.GameManager.cs()中的实例化也放到Awake()中
如果后续因为这些修改遇到bug会及时回来更改的
Checkpoint.cs
ContextMenu 方法
ContextMenu
属性使得在Unity编辑器的右键菜单中为这个方法添加一个按钮。点击该按钮时会执行GenerateId
方法。GenerateId
方法使用System.Guid.NewGuid().ToString()
生成一个新的唯一标识符,并将其赋值给id
变量。System.Guid.NewGuid()
生成一个新的全局唯一标识符(GUID),通过ToString()
转换成字符串。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//2024.11.28 19:38 疑似有点生病
//
public class Checkpoint : MonoBehaviour
{
private Animator anim;
public string id;
public bool activationStatus;
private void Awake()
{
anim = GetComponent<Animator>();
}
[ContextMenu("产生检查点ID")]//在编辑器中生成一个按钮
private void GenerateId()
{
id = System.Guid.NewGuid().ToString();
}
private void OnTriggerEnter2D(Collider2D collision)//检测到碰撞
{
if (collision.GetComponent<Player>()!=null)//检测到玩家
{
ActivateCheckPoint();//激活检查点
}
}
public void ActivateCheckPoint()//激活检查点
{
activationStatus = true;
anim.SetBool("active", true);
}
}
GameManager.cs
SaveData 方法
SaveData
方法用于保存当前游戏的数据。- 它首先通过
FindClosestCheckpoint()
方法获取最近激活的检查点,并将该检查点的 ID 存入GameData
中。 - 然后清空之前保存的检查点数据,并遍历场景中的所有检查点,将每个检查点的 ID 和激活状态保存到
_data.checkpoints
字典中。
LoadData 方法
LoadData
方法用于加载保存的数据。- 它遍历保存的数据中的
checkpoints
字典,其中包含每个检查点的 ID 和激活状态。 - 然后与场景中的所有检查点进行对比,匹配 ID 后,如果该检查点应该被激活,就调用
ActivateCheckPoint()
激活该检查点。 - 设置
closestCheckpointId
为加载数据中的最近检查点 ID,并使用Invoke
延迟调用PlacePlayerAtClosestCheckpoint()
,将玩家放置到最近激活的检查点。
RestartScene 方法
RestartScene
方法用来重启当前场景。- 首先调用
SaveManager.instance.SaveGame()
保存当前游戏的进度。 - 然后使用
SceneManager.GetActiveScene()
获取当前场景,并通过SceneManager.LoadScene()
重新加载该场景,达到重启场景的效果。
FindClosestCheckpoint 方法
FindClosestCheckpoint
方法用于查找玩家当前位置最近的已激活检查点。- 它通过
Vector2.Distance
计算玩家和每个检查点之间的距离,如果当前检查点的距离比已找到的最近距离小,并且该检查点已激活,就更新最近检查点。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
//2024.11.28 17:04
public class GameManager : MonoBehaviour, ISaveManager
{
public static GameManager instance;//单例模式,全局变量
[SerializeField] private Checkpoint[] checkpoints;
[SerializeField] private string closestCheckpointId;
private void Awake()
{
if (instance != null)
Destroy(instance.gameObject);
else
instance = this;
checkpoints = FindObjectsOfType<Checkpoint>();//查找所有的检查点
}
private void Start()
{
}
public void RestartScene()
{
SaveManager.instance.SaveGame();//保存游戏
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
public void LoadData(GameData _data)//就是点过灯的数据保存了,那么加载的时候就要把点过灯的数据加载出来
{
foreach (KeyValuePair<string, bool> pair in _data.checkpoints)//遍历数据中的检查点
{
foreach (Checkpoint checkpoint in checkpoints)//遍历场景中的检查点
{
if (checkpoint.id == pair.Key && pair.Value == true) //如果检查点的ID和数据中的ID相同且激活状态为真
checkpoint.ActivateCheckPoint();
}
}
closestCheckpointId = _data.closestCheckpointId; ;//将最近检查点ID存入变量
Invoke("PlacePlayerAtClosestCheckpoint", 0.1f);//延迟调用PlacePlayerAtClosetCheckpoint方法
}
private void PlacePlayerAtClosestCheckpoint()
{
foreach (Checkpoint checkpoint in checkpoints)
{
if (closestCheckpointId == checkpoint.id)
PlayerManager.instance.player.transform.position = checkpoint.transform.position;
}
}
public void SaveData(ref GameData _data)
{
_data.closestCheckpointId = FindClosestCheckpoint().id;//将最近的检查点ID存入数据
_data.checkpoints.Clear();
foreach (Checkpoint checkpoint in checkpoints)
{
_data.checkpoints.Add(checkpoint.id, checkpoint.activationStatus);//将检查点的ID和激活状态存入数据
}
}
private Checkpoint FindClosestCheckpoint()//找到最近的检查点
{
float closestDistance = Mathf.Infinity;//正无穷
Checkpoint closestCheckpoint = null;
foreach (var checkpoint in checkpoints)//遍历所有的检查点
{
float distanceToCheckpoint = Vector2.Distance(PlayerManager.instance.player.transform.position, checkpoint.transform.position);//计算玩家和检查点之间的距离
if (distanceToCheckpoint < closestDistance && checkpoint.activationStatus == true)//如果距离小于最近距离且检查点激活
{
closestDistance = distanceToCheckpoint;//更新最近距离
closestCheckpoint = checkpoint;//更新最近检查点
}
}
return closestCheckpoint;
}
}
GameData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//2024.11.25
[System.Serializable]
public class GameData
{
public int currency;
public SerializableDictionary<string, bool>skillTree;
public SerializableDictionary<string, int> inventory;//物品的名字和数量
public List<string> equipmentId;//装备的ID
public SerializableDictionary<string, bool> checkpoints;//检查点的ID和激活状态
public string closestCheckpointId;//最近的检查点ID
public GameData()
{
this.currency = 0;
skillTree = new SerializableDictionary<string, bool>();
inventory = new SerializableDictionary<string, int>();
equipmentId = new List<string>();
closestCheckpointId =string.Empty;
checkpoints = new SerializableDictionary<string, bool>();
}
}
UI.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class UI : MonoBehaviour
{
[Header("End screens")]
[SerializeField] private UI_FadeScreen fadeScreen;
[SerializeField] private GameObject endText;
[SerializeField] private GameObject restartButton;
[Space]
[SerializeField] private GameObject characterUI;
[SerializeField] private GameObject skillTreeUI;
[SerializeField] private GameObject craftUI;
[SerializeField] private GameObject optionsUI;
[SerializeField] private GameObject inGameUI;
//物品提示框和状态提示框
public UI_SkillToolTip skillToolTip;
public UI_ItemTooltip itemToolTip;
public UI_StatToolTip statToolTip;
public UI_CraftWindow craftWindow;
private void Awake()
{
SwitchTo(skillTreeUI);//2024年11月22日,P138 Skill Tree Hot Fix,启动时默认显示技能树界面
fadeScreen.gameObject.SetActive(true);
}
void Start()
{
SwitchTo(inGameUI);
itemToolTip.gameObject.SetActive(false);//戏启动时隐藏物品提示框和状态提示框
statToolTip.gameObject.SetActive(false);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.C))
SwitchWithKeyTo(characterUI);
if (Input.GetKeyDown(KeyCode.B))
SwitchWithKeyTo(craftUI);
if (Input.GetKeyDown(KeyCode.K))
SwitchWithKeyTo(skillTreeUI);
if (Input.GetKeyDown(KeyCode.O))
SwitchWithKeyTo(optionsUI);
}
public void SwitchTo(GameObject _menu)// 该方法用于切换到指定的UI界面
{
for (int i = 0; i < transform.childCount; i++)//遍历当前UI对象的所有子物体
{
bool fadeScreen = transform.GetChild(i).GetComponent<UI_FadeScreen>() != null;//检查UI界面是否有FadeScreens
if (fadeScreen==false)
transform.GetChild(i).gameObject.SetActive(false);//遍历并隐藏所有子元素,确保了在显示新的UI界面时,所有其他的UI界面都会被隐藏
}
if (_menu != null)//传入的菜单不为空
{
_menu.SetActive(true);//显示
}
}
public void SwitchWithKeyTo(GameObject _menu)//处理切换UI的逻辑
{
if (_menu != null && _menu.activeSelf)// UI界面已经显示,隐藏, 如果目标UI界面未显示,调用 SwitchTo 显示。
{
_menu.SetActive(false);
CheckForInGameUI();
return;
}
SwitchTo(_menu);
}
private void CheckForInGameUI()//关闭其他UI都会回到InGameUI
{
for (int i = 0; i < transform.childCount; i++)
{
if (transform.GetChild(i).gameObject.activeSelf)//检查当前 UI 对象的第 i 个子对象是否处于激活状态。
return;
}
SwitchTo(inGameUI);
}
public void SwitchOnEndScreen()
{
fadeScreen.FadeOut();
StartCoroutine(EndScreenCorutione());
}
IEnumerator EndScreenCorutione()
{
yield return new WaitForSeconds(1);
endText.SetActive(true);
yield return new WaitForSeconds(1.7f);
restartButton.SetActive(true);
}
public void RestartGameButton() => GameManager.instance.RestartScene();
}