Forum begins after the advertisement:
[Part 17] Magnet Pickup
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [Part 17] Magnet Pickup
- This topic has 9 replies, 2 voices, and was last updated 7 months ago by Terence.
-
AuthorPosts
-
May 3, 2024 at 11:51 am #14488::
Been working on a magnet item cant get it working just yet its seems to be heading in the right direction just dont know yet
Pickup.cs
using UnityEngine; public class Pickup : MonoBehaviour { public enum PickupType { None, Health, Experience, Magnet } public float lifespan = 0.5f; protected PlayerStats target; protected float speed; Vector2 initialPosition; float initialOffset; public PickupType type = PickupType.None; public float magnetPullForce; // Adjust the pull force as needed [System.Serializable] public struct BobbingAnimation { public float frequency; public Vector2 direction; } public BobbingAnimation bobbingAnimation = new BobbingAnimation { frequency = 2f, direction = new Vector2(0,0.3f) }; [Header("Bonuses")] public int experience; public int health; protected virtual void Start() { initialPosition = transform.position; initialOffset = Random.Range(0, bobbingAnimation.frequency); } protected virtual void Update() { if(target) { Vector2 distance = target.transform.position - transform.position; if (distance.sqrMagnitude > speed * speed * Time.deltaTime) transform.position += (Vector3)distance.normalized * speed * Time.deltaTime; else Destroy(gameObject); } else { transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency); } if (type == PickupType.Magnet) { // Find all experience pickups in the scene Pickup[] pickups = FindObjectsOfType<Pickup>(); // Attract each experience pickup towards the player foreach (Pickup pickup in pickups) { if (pickup.type == PickupType.Experience) { Vector2 direction = (transform.position - pickup.transform.position).normalized; pickup.AttractToPlayer(direction, speed); } } } } public virtual bool Collect(PlayerStats target, float speed, float lifespan = 0f) { if (!this.target) { this.target = target; this.speed = speed; if (lifespan > 0) this.lifespan = lifespan; Destroy(gameObject, Mathf.Max(0.01f, this.lifespan)); return true; } return false; } protected virtual void OnDestroy() { if (!target) return; target.IncreaseExperience(experience); target.RestoreHealth(health); } public void AttractToPlayer(Vector2 playerPosition, float speed) { // Calculate the direction from the pickup to the player Vector2 direction = (playerPosition - (Vector2)transform.position).normalized; // Move towards the player's position at the specified speed transform.position += (Vector3)direction * speed * Time.deltaTime; } }
May 4, 2024 at 6:09 pm #14496::Hi Cam, sorry I missed your topic.
Don’t recode the
Pickup
class. Extend it instead, and override the Pickup functions to change its behaviour. That way, you don’t have to risk it affecting your existing pickups:public class MagnetPickup : Pickup { float trackTimer; // How long this pickup has been chasing the player. bool inEffect = false; // When the magnet touches the player, set this to true, so the Magnet performs its buff. [Header("Magnet")] public float effectDuration = 2f; // Here, we remove the Destroy() part of the original Update() function and just set inEffect to true. // Then, when the magnet is in effect, we do something else. protected override void Update() { if(target) { if(inEffect) { transform.position = target.position; // Move the pickup to the player's position. duration -= Time.deltaTime; if(duration > 0) // Do your magnet effect here. else Destroy(gameObject); } else { // Move it towards the player and check the distance between. Vector2 distance = target.transform.position - transform.position; if (distance.sqrMagnitude > speed * speed * Time.deltaTime) transform.position += (Vector3)distance.normalized * speed * Time.deltaTime; else { // When it becomes active, we hide all its colliders and renderers to "hide" it. inEffect = true; Collider2D[] allColliders = GetComponentsInChildren<Collider2D>(); Renderer[] allRenderers = GetComponentsInChildren<Renderer>(); foreach(Collider2D c in allColliders) c.enabled = false; foreach(Renderer r in allRenderers) r.enabled = false; } // Tracks how long we have been tracking the target. // If it's been doing it for too long, destroy this pickup. trackTimer += Time.deltaTime; if(trackTimer > lifespan) Destroy(gameObject); } } else { // Handle the animation of the object. transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency); } } // We override the old collect function because we don't want magnet pickups to destroy themselves. // We will use the trackTimer variable to track how long a MagnetPickup has been tracking an object instead. public override bool Collect(PlayerStats target, float speed, float lifespan = 0f) { if (!this.target) { this.target = target; this.speed = speed; if (lifespan > 0) this.lifespan = lifespan; return true; } return false; } }
I haven’t tested this code yet, but what I’m trying to do is, once it touches the player, I will hide all renderers and colliders it has, then make it follow the player and perform its effect for a certain amount of time, then destroy itself.
I override the
Collect()
andUpdate()
functions because I want to modify the pickup’s behaviour. Primarily, besides implementing the magnet functionality, I also want to stop it from destroying itself, as I’m using theUpdate()
function to enact the behaviour, and if the GameObject destroys itself, the script stops as well.Hope this helps!
May 7, 2024 at 9:21 am #14538::<code>using UnityEngine; public class MagnetPickup : Pickup { float trackTimer; // How long this pickup has been chasing the player. bool inEffect = false; // When the magnet touches the player, set this to true, so the Magnet performs its buff. float remainingEffectDuration; // Remaining duration of the magnet effect. [Header("Magnet")] public float effectDuration = 2f; public float magnetRange = 90f; // The range within which pickups are collected. protected override void Update() { if (target) { if (inEffect) { transform.position = target.transform.position; // Move the pickup to the player's position. remainingEffectDuration -= Time.deltaTime; } else { // Move it towards the player and check the distance between. Vector2 distance = target.transform.position - transform.position; if (distance.sqrMagnitude > speed * speed * Time.deltaTime) transform.position += (Vector3)distance.normalized * speed * Time.deltaTime; else { // When it becomes active, disable the pickup GameObject. inEffect = true; remainingEffectDuration = effectDuration; gameObject.SetActive(false); } // Tracks how long we have been tracking the target. trackTimer += Time.deltaTime; } // Check for nearby pickups and collect them Collider2D[] nearbyPickups = Physics2D.OverlapCircleAll(transform.position, magnetRange); foreach (Collider2D pickupCollider in nearbyPickups) { Pickup pickup = pickupCollider.GetComponent<Pickup>(); if (pickup) { pickup.Collect(target, speed, lifespan); } } } else { // Handle the animation of the object. transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency); } } public override bool Collect(PlayerStats target, float speed, float lifespan = 0f) { if (!this.target) { this.target = target; this.speed = speed; if (lifespan > 0) this.lifespan = lifespan; return true; } return false; } } </code>
this seems to be working now the next issue is when i collect a few xp gems, i only get one level up screen. eg i cooloected the magnet got like 6 gems had one level up screen but i was level 4 after collecting the gems
May 7, 2024 at 9:42 am #14541::Cam, do your gems give experience once you touch them? Or do they have to fly over to you first?
May 7, 2024 at 12:34 pm #14548May 7, 2024 at 1:29 pm #14549May 8, 2024 at 12:05 am #14555::Update 11 May 2024: Edited the code to reflect the changes highlighted by Cam below.
Hi Cam, thanks for this. It’s a great bug you spotted. Our current experience system in PlayerStats isn’t able to support multiple experience increases in a single frame, because they will all turn on the level-up screen together (you can’t turn on the level-up screen more than once).
I fixed this by limiting calls to
IncreaseExperience()
to 1 per frame. If you call it more than once, it will keep a backlog. Here’s how it looks like (changes in green):using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerStats : MonoBehaviour { CharacterData characterData; public CharacterData.Stats baseStats; [SerializeField] CharacterData.Stats actualStats; public CharacterData.Stats Stats { get { return actualStats; } set { actualStats = value; } } float health; #region Current Stats Properties public float CurrentHealth { get { return health; } // If we try and set the current health, the UI interface // on the pause screen will also be updated. set { //Check if the value has changed if (health != value) { health = value; UpdateHealthBar(); } } } #endregion [Header("Visuals")] public ParticleSystem damageEffect; // If damage is dealt. public ParticleSystem blockedEffect; // If armor completely blocks damage. //Experience and level of the player [Header("Experience/Level")] public int experience = 0; public int level = 1; public int experienceCap; bool hasIncreasedExperience = false; Queue<int> experienceBacklog = new Queue<int>(); //Class for defining a level range and the corresponding experience cap increase for that range [System.Serializable] public class LevelRange { public int startLevel; public int endLevel; public int experienceCapIncrease; } //I-Frames [Header("I-Frames")] public float invincibilityDuration; float invincibilityTimer; bool isInvincible; public List<LevelRange> levelRanges; PlayerInventory inventory; PlayerCollector collector; public int weaponIndex; public int passiveItemIndex; [Header("UI")] public Image healthBar; public Image expBar; public TMP_Text levelText; void Awake() { characterData = CharacterSelector.GetData(); if(CharacterSelector.instance) CharacterSelector.instance.DestroySingleton(); inventory = GetComponent<PlayerInventory>(); collector = GetComponentInChildren<PlayerCollector>(); //Assign the variables baseStats = actualStats = characterData.stats; collector.SetRadius(actualStats.magnet); health = actualStats.maxHealth; } void Start() { //Spawn the starting weapon inventory.Add(characterData.StartingWeapon); //Initialize the experience cap as the first experience cap increase experienceCap = levelRanges[0].experienceCapIncrease; GameManager.instance.AssignChosenCharacterUI(characterData); UpdateHealthBar(); UpdateExpBar(); UpdateLevelText(); } void Update() { hasIncreasedExperience = false; if(experienceBacklog.Count > 0 && GameManager.instance.currentState != GameManager.GameState.LevelUp) { IncreaseExperience(experienceBacklog.Dequeue()); } if (invincibilityTimer > 0) { invincibilityTimer -= Time.deltaTime; } //If the invincibility timer has reached 0, set the invincibility flag to false else if (isInvincible) { isInvincible = false; } Recover(); } public void RecalculateStats() { actualStats = baseStats; foreach (PlayerInventory.Slot s in inventory.passiveSlots) { Passive p = s.item as Passive; if (p) { actualStats += p.GetBoosts(); } } // Update the PlayerCollector's radius. collector.SetRadius(actualStats.magnet); } public void IncreaseExperience(int amount) { if(hasIncreasedExperience) { experienceBacklog.Enqueue(amount); return; } experience += amount; LevelUpChecker(); UpdateExpBar(); hasIncreasedExperience = true; } void LevelUpChecker() { if (experience >= experienceCap) { //Level up the player and reduce their experience by the experience cap level++; experience -= experienceCap; //Find the experience cap increase for the current level range int experienceCapIncrease = 0; foreach (LevelRange range in levelRanges) { if (level >= range.startLevel && level <= range.endLevel) { experienceCapIncrease = range.experienceCapIncrease; break; } } experienceCap += experienceCapIncrease; UpdateLevelText(); GameManager.instance.StartLevelUp(); } } void UpdateExpBar() { // Update exp bar fill amount expBar.fillAmount = (float)experience / experienceCap; } void UpdateLevelText() { // Update level text levelText.text = "LV " + level.ToString(); } public void TakeDamage(float dmg) { //If the player is not currently invincible, reduce health and start invincibility if (!isInvincible) { // Take armor into account before dealing the damage. dmg -= actualStats.armor; if (dmg > 0) { // Deal the damage. CurrentHealth -= dmg; // If there is a damage effect assigned, play it. if (damageEffect) Destroy(Instantiate(damageEffect, transform.position, Quaternion.identity), 5f); if (CurrentHealth <= 0) { Kill(); } } else { // If there is a blocked effect assigned, play it. if (blockedEffect) Destroy(Instantiate(blockedEffect, transform.position, Quaternion.identity), 5f); } invincibilityTimer = invincibilityDuration; isInvincible = true; } } void UpdateHealthBar() { //Update the health bar healthBar.fillAmount = CurrentHealth / actualStats.maxHealth; } public void Kill() { if (!GameManager.instance.isGameOver) { GameManager.instance.AssignLevelReachedUI(level); //GameManager.instance.AssignChosenWeaponsAndPassiveItemsUI(inventory.weaponSlots, inventory.passiveSlots); GameManager.instance.GameOver(); } } public void RestoreHealth(float amount) { // Only heal the player if their current health is less than their maximum health if (CurrentHealth < actualStats.maxHealth) { CurrentHealth += amount; // Make sure the player's health doesn't exceed their maximum health if (CurrentHealth > actualStats.maxHealth) { CurrentHealth = actualStats.maxHealth; } } } void Recover() { if (CurrentHealth < actualStats.maxHealth) { CurrentHealth += Stats.recovery * Time.deltaTime; // Make sure the player's health doesn't exceed their maximum health if (CurrentHealth > actualStats.maxHealth) { CurrentHealth = actualStats.maxHealth; } } } }
I found another bug though: If an experience gem gives you enough experience to increase your level by more than 1, this still registers only as 1 level up (even though the level up bar goes way up). I’ll be deploying a fix for this as well.
P.S. Added another bug spotter badge on your profile as thanks for pointing this out!
May 8, 2024 at 10:38 am #14558::i had to fix that code some more to get it to work but now its seems to work well
<code>using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerStats : MonoBehaviour { CharacterData characterData; public CharacterData.Stats baseStats; [SerializeField] CharacterData.Stats actualStats; public CharacterData.Stats Stats { get { return actualStats; } set { actualStats = value; } } float health; #region Current Stats Properties public float CurrentHealth { get { return health; } // If we try and set the current health, the UI interface // on the pause screen will also be updated. set { //Check if the value has changed if (health != value) { health = value; UpdateHealthBar(); } } } #endregion [Header("Visuals")] public ParticleSystem damageEffect; // If damage is dealt. public ParticleSystem blockedEffect; // If armor completely blocks damage. //Experience and level of the player [Header("Experience/Level")] public int experience = 0; public int level = 1; public int experienceCap; bool hasIncreasedExperience = false; Queue<int> experienceBacklog = new Queue<int>(); //Class for defining a level range and the corresponding experience cap increase for that range [System.Serializable] public class LevelRange { public int startLevel; public int endLevel; public int experienceCapIncrease; } //I-Frames [Header("I-Frames")] public float invincibilityDuration; float invincibilityTimer; bool isInvincible; public List<LevelRange> levelRanges; PlayerInventory inventory; PlayerCollector collector; public int weaponIndex; public int passiveItemIndex; [Header("UI")] public Image healthBar; public Image expBar; public TMP_Text levelText; void Awake() { characterData = CharacterSelector.GetData(); if(CharacterSelector.instance) CharacterSelector.instance.DestroySingleton(); inventory = GetComponent<PlayerInventory>(); // Assuming PlayerInventory is the type you want to get. collector = GetComponentInChildren<PlayerCollector>(); // Assuming PlayerCollector is the type you want to get. //Assign the variables baseStats = actualStats = characterData.stats; collector.SetRadius(actualStats.magnet); health = actualStats.maxHealth; } void Start() { //Spawn the starting weapon inventory.Add(characterData.StartingWeapon); //Initialize the experience cap as the first experience cap increase experienceCap = levelRanges[0].experienceCapIncrease; GameManager.instance.AssignChosenCharacterUI(characterData); UpdateHealthBar(); UpdateExpBar(); UpdateLevelText(); } void Update() { hasIncreasedExperience = false; if(experienceBacklog.Count > 0 && GameManager.instance.currentState != GameManager.GameState.LevelUp) { IncreaseExperience(experienceBacklog.Dequeue()); } if (invincibilityTimer > 0) { invincibilityTimer -= Time.deltaTime; } //If the invincibility timer has reached 0, set the invincibility flag to false else if (isInvincible) { isInvincible = false; } Recover(); } public void RecalculateStats() { actualStats = baseStats; foreach (PlayerInventory.Slot s in inventory.passiveSlots) { Passive p = s.item as Passive; if (p) { actualStats += p.GetBoosts(); } } // Update the PlayerCollector's radius. collector.SetRadius(actualStats.magnet); } public void IncreaseExperience(int amount) { if(hasIncreasedExperience) { experienceBacklog.Enqueue(amount); return; } experience += amount; LevelUpChecker(); UpdateExpBar(); hasIncreasedExperience = true; } void LevelUpChecker() { if (experience >= experienceCap) { //Level up the player and reduce their experience by the experience cap level++; experience -= experienceCap; //Find the experience cap increase for the current level range int experienceCapIncrease = 0; foreach (LevelRange range in levelRanges) { if (level >= range.startLevel && level <= range.endLevel) { experienceCapIncrease = range.experienceCapIncrease; break; } } experienceCap += experienceCapIncrease; UpdateLevelText(); GameManager.instance.StartLevelUp(); } } void UpdateExpBar() { // Update exp bar fill amount expBar.fillAmount = (float)experience / experienceCap; } void UpdateLevelText() { // Update level text levelText.text = "LV " + level.ToString(); } public void TakeDamage(float dmg) { //If the player is not currently invincible, reduce health and start invincibility if (!isInvincible) { // Take armor into account before dealing the damage. dmg -= actualStats.armor; if (dmg > 0) { // Deal the damage. CurrentHealth -= dmg; // If there is a damage effect assigned, play it. if (damageEffect) Destroy(Instantiate(damageEffect, transform.position, Quaternion.identity), 5f); if (CurrentHealth <= 0) { Kill(); } } else { // If there is a blocked effect assigned, play it. if (blockedEffect) Destroy(Instantiate(blockedEffect, transform.position, Quaternion.identity), 5f); } invincibilityTimer = invincibilityDuration; isInvincible = true; } } void UpdateHealthBar() { //Update the health bar healthBar.fillAmount = CurrentHealth / actualStats.maxHealth; } public void Kill() { if (!GameManager.instance.isGameOver) { GameManager.instance.AssignLevelReachedUI(level); //GameManager.instance.AssignChosenWeaponsAndPassiveItemsUI(inventory.weaponSlots, inventory.passiveSlots); GameManager.instance.GameOver(); } } public void RestoreHealth(float amount) { // Only heal the player if their current health is less than their maximum health if (CurrentHealth < actualStats.maxHealth) { CurrentHealth += amount; // Make sure the player's health doesn't exceed their maximum health if (CurrentHealth > actualStats.maxHealth) { CurrentHealth = actualStats.maxHealth; } } } void Recover() { if (CurrentHealth < actualStats.maxHealth) { CurrentHealth += Stats.recovery * Time.deltaTime; // Make sure the player's health doesn't exceed their maximum health if (CurrentHealth > actualStats.maxHealth) { CurrentHealth = actualStats.maxHealth; } } } } </code>
sorry dont know how to use the green text the changes was
<code> inventory = GetComponent<PlayerInventory>(); collector = GetComponentInChildren<PlayerCollector>(); </code>
<code> Queue<int> experienceBacklog = new Queue<int>(); </code>
<code> public List<LevelRange> levelRanges; </code>
May 9, 2024 at 2:46 pm #14563::No problem Cam. Thanks for helping to document this!
To green highlight text, you’ll can use the
<mark class="green">
and</mark>
tags.May 12, 2024 at 5:33 pm #14635::Cam, FYI, I’ve added a new section in the Part 19 article addressing the experience bug you mentioned: https://blog.terresquall.com/2024/04/creating-a-rogue-like-vampire-survivors-part-19/#bugfixes-for-experience-gain
The fixes in there are simpler than the code I’ve given you. Do take a look if you’re interested.
There is also a fix for when you give too much experience using
PlayerStats.IncreaseExperience()
, such that the player is able to gain multiple levels from the experience given. In such cases, you will only gain a single level:[video src="https://blog.terresquall.com/wp-content/uploads/2024/04/vampire-survivors-too-much-experience-given.webm" /]
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: