Forum begins after the advertisement:
[Part 8] MaxHealth increases from heart shards not saved
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 8] MaxHealth increases from heart shards not saved
- This topic has 3 replies, 2 voices, and was last updated 4 months ago by
Joseph Tang.
-
AuthorPosts
-
May 5, 2024 at 10:13 am #14503
Aria
Bronze Supporter (Patron)::This is probably a stupid question but how exactly would i go about saving the health increase from heart shards, but also making heart shards 1 time use.
May 5, 2024 at 2:19 pm #14504Joseph Tang
Moderator::1. Saving Max Health
I’ve recently went and done some updates and improvements to the codes for the Metroidvania series, and will be putting up some posts about the bugfixes and improvements.
So, up to Article Part 9, and soon Part 10, there should be new improved codes with slightly updated instructions and notes.
There is a new code for SaveData.cs in Part 8 that will take into account the maxhealth of the player and save that data.[System.Serializable] public struct SaveData { public static SaveData Instance; //player stuff public int playerHealth; public int playerMaxHealth; public int playerHeartShards; public float playerMana; public bool playerHalfMana; public Vector2 playerPosition; public string lastScene; public bool playerUnlockedWallJump, playerUnlockedDash, playerUnlockedVarJump; public bool playerUnlockedSideCast, playerUnlockedUpCast, playerUnlockedDownCast; #region Player stuff public void SavePlayerData() { using(BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.player.data"))) { playerHealth = PlayerController.Instance.Health; writer.Write(playerHealth); playerMaxHealth = PlayerController.Instance.maxHealth; writer.Write(playerMaxHealth); playerHeartShards = PlayerController.Instance.heartShards; writer.Write(playerHeartShards); playerMana = PlayerController.Instance.Mana; writer.Write(playerMana); playerHalfMana = PlayerController.Instance.halfMana; writer.Write(playerHalfMana); playerUnlockedWallJump = PlayerController.Instance.unlockedWallJump; writer.Write(playerUnlockedWallJump); playerUnlockedDash = PlayerController.Instance.unlockedDash; writer.Write(playerUnlockedDash); playerUnlockedVarJump = PlayerController.Instance.unlockedVarJump; writer.Write(playerUnlockedVarJump); playerUnlockedSideCast = PlayerController.Instance.unlockedSideCast; writer.Write(playerUnlockedSideCast); playerUnlockedUpCast = PlayerController.Instance.unlockedUpCast; writer.Write(playerUnlockedUpCast); playerUnlockedDownCast = PlayerController.Instance.unlockedDownCast; writer.Write(playerUnlockedDownCast); playerPosition = PlayerController.Instance.transform.position; writer.Write(playerPosition.x); writer.Write(playerPosition.y); lastScene = SceneManager.GetActiveScene().name; writer.Write(lastScene); } Debug.Log("saved player data"); } public void LoadPlayerData() { if(File.Exists(Application.persistentDataPath + "/save.player.data")) { using(BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.player.data"))) { playerHealth = reader.ReadInt32(); playerMaxHealth = reader.ReadInt32(); playerHeartShards = reader.ReadInt32(); playerMana = reader.ReadSingle(); playerHalfMana = reader.ReadBoolean(); playerUnlockedWallJump = reader.ReadBoolean(); playerUnlockedDash = reader.ReadBoolean(); playerUnlockedVarJump = reader.ReadBoolean(); playerUnlockedSideCast = reader.ReadBoolean(); playerUnlockedUpCast = reader.ReadBoolean(); playerUnlockedDownCast = reader.ReadBoolean(); playerPosition.x = reader.ReadSingle(); playerPosition.y = reader.ReadSingle(); lastScene = reader.ReadString(); SceneManager.LoadScene(lastScene); PlayerController.Instance.transform.position = playerPosition; PlayerController.Instance.halfMana = playerHalfMana; PlayerController.Instance.Health = playerHealth; PlayerController.Instance.maxHealth = playerMaxHealth; PlayerController.Instance.heartShards = playerHeartShards; PlayerController.Instance.Mana = playerMana; PlayerController.Instance.unlockedWallJump = playerUnlockedWallJump; PlayerController.Instance.unlockedDash = playerUnlockedDash; PlayerController.Instance.unlockedVarJump = playerUnlockedVarJump; PlayerController.Instance.unlockedSideCast = playerUnlockedSideCast; PlayerController.Instance.unlockedUpCast = playerUnlockedUpCast; PlayerController.Instance.unlockedDownCast = playerUnlockedDownCast; } Debug.Log("load player data"); Debug.Log(playerHalfMana); } else { Debug.Log("File doesnt exist"); PlayerController.Instance.halfMana = false; PlayerController.Instance.Health = PlayerController.Instance.maxHealth; PlayerController.Instance.Mana = 0.5f; PlayerController.Instance.heartShards = 0; PlayerController.Instance.unlockedWallJump = false; PlayerController.Instance.unlockedDash = false; PlayerController.Instance.unlockedVarJump = false; PlayerController.Instance.unlockedSideCast = false; PlayerController.Instance.unlockedUpCast = false; PlayerController.Instance.unlockedDownCast = false; } } #endregion }
2. Checking for pickups/Chests being already used.
As for the saving of the Heart Shards being picked up, that’s a bit more complicated.
Like what was discussed here:Here’s a code i modified from ChatGPT [Should work in theory but I haven’t yet tested it] that uses the idea in the above statement:
New Script: PickupManager.cs
using System.Collections.Generic; using UnityEngine; public class PickupManager : MonoBehaviour { public static PickupManager Instance; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } DontDestroyOnLoad(gameObject); } void Start() { LoadAndDeactivateInteractedObjects(); // Load saved data and deactivate pickups/chests } void LoadAndDeactivateInteractedObjects() { HashSet
collectedPickups = SaveData.Instance.collectedPickups; HashSet openedChests = SaveData.Instance.openedChests; // Deactivate pickups that have been collected foreach (GameObject pickup in GameObject.FindGameObjectsWithTag("Pickup")) { string pickupID = pickup.name; // Assuming the name of the object is its ID if (collectedPickups.Contains(pickupID)) { pickup.SetActive(false); // Deactivate the pickup } } // Deactivate chests that have been opened foreach (GameObject chest in GameObject.FindGameObjectsWithTag("Chest")) { string chestID = chest.name; // Assuming the name of the object is its ID if (openedChests.Contains(chestID)) { chest.SetActive(false); // Deactivate the chest } } } }
SaveData.cs
[System.Serializable] public struct SaveData { public static SaveData Instance; // Other data... // Pickups and Chests data public HashSet
collectedPickups; // HashSet to store collected pickups public HashSet openedChests; // HashSet to store opened chests public void Initialize() { // Initialize other data... if (collectedPickups == null) { collectedPickups = new HashSet (); } if (openedChests == null) { openedChests = new HashSet (); } } public void AddCollectedPickup(string pickupID) { // Add pickup ID to collected pickups collectedPickups.Add(pickupID); SavePickupsAndChests(); } public void AddOpenedChest(string chestID) { // Add chest ID to opened chests openedChests.Add(chestID); SavePickupsAndChests(); } public void SavePickupsAndChests() { using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "/save.pickups_chests.data"))) { // Save collected pickups writer.Write(collectedPickups.Count); foreach (string pickupID in collectedPickups) { writer.Write(pickupID); } // Save opened chests writer.Write(openedChests.Count); foreach (string chestID in openedChests) { writer.Write(chestID); } } } public void LoadPickupsAndChests() { if (File.Exists(Application.persistentDataPath + "/save.pickups_chests.data")) { using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "/save.pickups_chests.data"))) { // Load collected pickups int numCollectedPickups = reader.ReadInt32(); for (int i = 0; i < numCollectedPickups; i++) { collectedPickups.Add(reader.ReadString()); } // Load opened chests int numOpenedChests = reader.ReadInt32(); for (int i = 0; i < numOpenedChests; i++) { openedChests.Add(reader.ReadString()); } } } } }
GameManager.cs
public class GameManager : MonoBehaviour { // Method to trigger pickup interaction event public void TriggerPickupInteract(string pickupID) { SaveData.Instance.AddCollectedPickup(pickupID); // Add pickup to SaveData } // Method to trigger chest interaction event public void TriggerChestInteract(string chestID) { SaveData.Instance.AddOpenedChest(chestID); // Add chest to SaveData } }
PlayerController.cs
public class PlayerController : MonoBehaviour public IEnumerator WalkIntoNewScene(Vector2 _exitDir, float _delay) { PickupManager.Instance.LoadAndDeactivateInteractedObjects(); pstate.invincible = true; //If exit direction is upwards if(_exitDir.y > 0) { rb.velocity = jumpForce * _exitDir; } //If exit direction requires horizontal movement if(_exitDir.x != 0) { xAxis = _exitDir.x > 0 ? 1 : -1; Move(); } Flip(); yield return new WaitForSeconds(_delay); pstate.invincible = false; pState.cutscene = false; }
To summarize,
1) SaveData.cs will have new code to keep a Hashset of Chest names and pickup names, as well as add them.2) A new singleton script, PickupManager.cs, will then take the Hashset of names and compare them to any and all Gameobjects that have been tagged appropriately with “Chest” or “Pickup”. If the hashset of names matches the names of the tagged gameobjects, that gameobject will be set to false.
3) GameManager.cs will host a method that will call the method in SaveData.cs to save names of the pickups in the Hashset.
4) PlayerController.cs will call the PickupManager’s
LoadAndDeactivateInteractedObjects()
on entering a new scene to set up all the pickups in the scene.
Preparation:
1) Tags: Create a “Pickup” and “Chest” tag and set them on the related gameobjects.2) A way to call GameManager’s
TriggerChestInteract()
orTriggerPickupInteract()
. This will primarily be from scripts, you can add a line into, let’s say, orbShard or heartShard scripts that when picked up, they call the GameManager and give the gameobject’s name. For instance:GameManager.Instance.TriggerPickupInteract(gameObject.name)
.3) Create a null object and set the pickupmanager script onto it.
This is all theory and could possibly work, if it doesn’t, it possibly only needs some tweaking to the code a bit and fix up some errors. I’ve cleaned up the code as much as i can for the theory of it.
May 6, 2024 at 9:52 am #14525Aria
Bronze Supporter (Patron)::I had a look with the save data, but i have an issue where the amount of hearts being instantiated when i load the game being only 1 for some reason
here is my save data scriptusing System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; using UnityEngine.SceneManagement; [System.Serializable] public struct SaveData { public static SaveData Instance; public HashSet<string> sceneNames; public string typewriterSceneName; public Vector2 typewriterPos; public int playerHealth; public Vector2 playerPos; public string lastScene; public string lastFrame; public bool playerUnlockedDash; public bool playerUnlockedDoubleJump; public int playerHeartShards; public int playerMaxHealth; public void Initialized() { if(!File.Exists(Application.persistentDataPath + "saving.typewriter.data")) { BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "saving.typewriter.data")); } if (!File.Exists(Application.persistentDataPath + "saving.player.data")) //if file doesnt exist, well create the file { BinaryWriter writer = new BinaryWriter(File.Create(Application.persistentDataPath + "saving.player.data")); } if (sceneNames == null) { sceneNames = new HashSet<string>(); } } public void SaveTypewriter() { using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "saving.typewriter.data"))) { writer.Write(typewriterSceneName); writer.Write(typewriterPos.x); writer.Write(typewriterPos.y); } } public void LoadTypewriter() { if(File.Exists(Application.persistentDataPath + "saving.typewriter.data")) { using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "saving.typewriter.data"))) { typewriterSceneName = reader.ReadString(); typewriterPos.x = reader.ReadSingle(); typewriterPos.y = reader.ReadSingle(); } } else { Debug.Log("Typewriter does not exist"); } } public void SavePlayerData() { using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(Application.persistentDataPath + "save.user.data"))) { playerHealth = PlayerMovement.Instance.Health; writer.Write(playerHealth); playerHeartShards = PlayerMovement.Instance.heartShards; writer.Write(playerHeartShards); playerMaxHealth = PlayerMovement.Instance.maxHealth; writer.Write(playerMaxHealth); playerUnlockedDash = PlayerMovement.Instance.unlockedDash; writer.Write(playerUnlockedDash); playerUnlockedDoubleJump = PlayerMovement.Instance.unlockedDoubleJump; writer.Write(playerUnlockedDoubleJump); playerPos = PlayerMovement.Instance.transform.position; writer.Write(playerPos.x); writer.Write(playerPos.y); lastScene = SceneManager.GetActiveScene().name; writer.Write(lastScene); } Debug.Log("saved player data"); } public void LoadPlayerData() { if (File.Exists(Application.persistentDataPath + "save.user.data")) { string savePath = Application.persistentDataPath + "save.user.data"; if (File.Exists(savePath) && new FileInfo(savePath).Length > 0) { using (BinaryReader reader = new BinaryReader(File.OpenRead(Application.persistentDataPath + "save.user.data"))) { playerHealth = reader.ReadInt32(); playerMaxHealth = reader.ReadInt32(); playerHeartShards = reader.ReadInt32(); playerUnlockedDash = reader.ReadBoolean(); playerUnlockedDoubleJump = reader.ReadBoolean(); playerPos.x = reader.ReadSingle(); playerPos.y = reader.ReadSingle(); lastScene = reader.ReadString(); SceneManager.LoadScene(lastScene); PlayerMovement.Instance.transform.position = playerPos; PlayerMovement.Instance.Health = playerHealth; PlayerMovement.Instance.maxHealth = playerMaxHealth; PlayerMovement.Instance.unlockedDash = playerUnlockedDash; PlayerMovement.Instance.unlockedDoubleJump = playerUnlockedDoubleJump; PlayerMovement.Instance.heartShards = playerHeartShards; } Debug.Log("load player data"); } } else { Debug.Log("File doesnt exist"); PlayerMovement.Instance.health = PlayerMovement.Instance.maxHealth; PlayerMovement.Instance.unlockedDash = false; PlayerMovement.Instance.unlockedDoubleJump = false; PlayerMovement.Instance.heartShards = 0; PlayerMovement.Instance.maxHealth = 5; } } }
May 6, 2024 at 1:52 pm #14528Joseph Tang
Moderator::Oh I believe I also added a part for that, which i forgot to send here.
I think what’s happening is your HeartController.cs is taking your Player’s health values before its updated with the
SaveData.Instance.LoadPlayerData();
. [Although I can’t be certain since I’m not sure if you set your MaxHealth to be 1. I reccomend testing to see if your Health is really at 1 or will be updated after getting hit, but regardless I think you should do this.]You can try calling
onHealthChangedCallback.Invoke();
in your PlayerController.cs after theSaveData.Instance.LoadPlayerData();
code, to update the HeartController.cs.// Start is called before the first frame update void Start() { pState = GetComponent
(); rb = GetComponent (); sr = GetComponent (); anim = GetComponent (); SaveData.Instance.LoadPlayerData(); if (halfMana) { UIManager.Instance.SwitchMana(UIManager.ManaState.HalfMana); } else { UIManager.Instance.SwitchMana(UIManager.ManaState.FullMana); } onHealthChangedCallback.Invoke(); gravity = rb.gravityScale; if (Health == 0) { pState.alive = false; GameManager.Instance.RespawnPlayer(); } Mana = mana; manaStorage.fillAmount = Mana; Health = maxHealth; } If this does not solve the issue, I will probably need a video of you entering the game, getting hit, resetting the game or healing, to understand what exactly the issue looks like.
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: