Forum begins after the advertisement:
[General] Guide to create the Attractorb pickup
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [General] Guide to create the Attractorb pickup
- This topic has 3 replies, 3 voices, and was last updated 5 days, 21 hours ago by
Terence.
-
AuthorPosts
-
May 5, 2025 at 6:56 am #18069::
Hello,
I thought I would share with you my attempt at creating the Attractorb. This pick up sucks in all exp gems within a predefined radius. Apologies if the code is messy or doesn’t make sense. this is still new to me so I had Ai assist me with the tricky bits.
View post on imgur.com
The characteristics for my Attractorb are as follows:
- Set a radius for the zone of magnetism.
- Set a layer for items we want to attract (In this case I made the “Pick up” layer).
- Set a list of objects to attract.
- Attracted items pause when in level up screen.
- Newly generated pick ups don’t get included in magnetism once attraction has started thus not to start a continuous loop
I started by creating an empty game object and giving it a sprite. Then I assigned it the pick up script.
To get this working, I had to create a new script “PickupAttractorOrb” and assign this to the pick up.
The code:
Pickup.cs
using UnityEngine; public class Pickup : Sortable { protected PlayerStats target; protected float collectStartTime; protected Vector2 initialPosition; protected float initialOffset; protected Vector2 repelDirection; private bool isBeingCollected = false; [Header("Movement")] public float repelSpeed = 5f; public float approachSpeed = 8f; public float attractionDelay = 0.15f; [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 override void Start() { base.Start(); initialPosition = transform.position; initialOffset = Random.Range(0, bobbingAnimation.frequency); } protected virtual void Update() { if (target) { float timeSinceCollect = Time.unscaledTime - collectStartTime; if (timeSinceCollect < attractionDelay) { // Repel for a short time before attraction transform.position += (Vector3)(repelDirection * repelSpeed * Time.deltaTime); } else { // Attract the pickup towards the player Vector2 distance = target.transform.position - transform.position; if (distance.sqrMagnitude > 0.05f) // Close enough? { transform.position += (Vector3)(distance.normalized * approachSpeed * Time.deltaTime); } } } else { // Idle bobbing animation when not being collected transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.unscaledTime + initialOffset) * bobbingAnimation.frequency); } } public virtual bool Collect(PlayerStats target, float speedIgnored = 0f, float lifespanOverride = 0f) { if (!this.target) { this.target = target; collectStartTime = Time.unscaledTime; repelDirection = -(target.transform.position - transform.position).normalized; // Calculate repel direction initially isBeingCollected = true; return true; } return false; } private void OnTriggerEnter2D(Collider2D col) { if (!isBeingCollected || !target) return; if (col.TryGetComponent<PlayerStats>(out PlayerStats player)) { if (player == target) { target.IncreaseExperience(experience); target.RestoreHealth(health); Destroy(gameObject); // Destroy the pickup after collection } } } }
PickupAttractorOrb.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PickupAttractorOrb : Pickup { [Header("Attractor Settings")] public float radius = 6f; public LayerMask attractionLayer; [Tooltip("List of pickup scripts (from prefabs or scene objects) to attract.")] public List<MonoBehaviour> objectsToAttract; private List<System.Type> attractTypes = new List<System.Type>(); private List<Pickup> pickupsToAttract = new List<Pickup>(); public override bool Collect(PlayerStats target, float speedIgnored = 0f, float lifespanOverride = 0f) { if (!base.Collect(target, speedIgnored, lifespanOverride)) return false; attractTypes.Clear(); pickupsToAttract.Clear(); foreach (var obj in objectsToAttract) { if (obj != null) attractTypes.Add(obj.GetType()); } if (attractTypes.Count > 0) { // Cache valid pickups at time of collection Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, radius, attractionLayer); foreach (Collider2D col in hits) { foreach (var type in attractTypes) { var component = col.GetComponent(type); if (component != null && component is Pickup pickup && pickup != this) { pickupsToAttract.Add(pickup); break; } } } if (pickupsToAttract.Count > 0) StartCoroutine(AttractCachedPickups()); } else { Debug.LogWarning("PickupAttractorOrb: No attractable objects set."); } return true; } private IEnumerator AttractCachedPickups() { while (pickupsToAttract.Count > 0) { for (int i = pickupsToAttract.Count - 1; i >= 0; i--) { var pickup = pickupsToAttract[i]; if (pickup == null) { pickupsToAttract.RemoveAt(i); continue; } if (pickup.Collect(target)) { pickupsToAttract.RemoveAt(i); } } yield return new WaitForSeconds(0.1f); } Destroy(gameObject); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(0.3f, 0.9f, 1f, 0.3f); Gizmos.DrawWireSphere(transform.position, radius); } }
The changes to pick up add a short repel animation before being collecting as well as only collecting the item upon collision with the player since the item will always touch the player eventually. I believe I also added a 2d circle collider with is trigger true on pick ups and done away with the rigid bodies.
PickupAttractOrb inherits form pickup to allow for collection. Radius determines the zone for attraction, attractionLayer tells the object what layers to include when scanning for pickups, objectsToAttract is a list of objects you want to specify for attraction.
We basically overrider Collect() and fill a list of attractTypes with types of pickups based on objectsToAttract. Then we check using an overlap circle with the size of radius to create a list of all those objects.
For each object in the circle we:
- Check if it has a component matching any of the types in attractTypes
- If it does and isn’t the attractorOrb itself, add to pickupsToAttract list.
Then start collecting if there are items in the list.
The gizmo aids in visualising the area of attraction.
Please let me know your thoughts for improvement. Typing this out I now realise I have probably overlooked duplicate types and a null exception on target.
Thanks, Nathaniel
has upvoted this post. May 5, 2025 at 7:06 am #18070::Hi Nathaniel,
Thanks for sharing your project! It sounds like a fun and interesting feature to implement. Here are some suggestions to improve your code and address the potential issues you mentioned:
-
Duplicate Types: To handle duplicate types in your attractTypes list, you can use a HashSet instead of a List. A HashSet automatically ensures that all elements are unique. Additionally, you can check if an item is already in the set before adding it.
HashSet attractTypes = new HashSet();
-
Null Reference Check: You mentioned a potential null reference on the target. To prevent this, you can add a null check in the
Update
method to ensure that the target is still valid before performing operations on it.if (target == null) { // Handle the case when the target is null (e.g., stop attraction) return; }
-
Optimize Attraction Logic: Instead of using a fixed approach speed for all pickups, consider making the attraction speed proportional to the distance from the target. This can make the attraction feel more natural.
float distanceMagnitude = distance.magnitude; float dynamicApproachSpeed = Mathf.Lerp(approachSpeed * 0.5f, approachSpeed, distanceMagnitude / maxDistance); transform.position += (Vector3)(distance.normalized * dynamicApproachSpeed * Time.deltaTime);
-
Gizmos for Visualization: If you want to visualize the attraction radius in the editor, you can override the
OnDrawGizmos
method in yourPickupAttractorOrb
class. This can help you see the area of attraction while developing.void OnDrawGizmos() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, radius); }
-
Refactor Common Code: Since both
Pickup
andPickupAttractorOrb
share a lot of code, consider extracting common functionality into a base class. This way, you can reduce duplication and improve maintainability. -
Implementation of Layer Masking: When checking for nearby pickups, you can utilize Layer Masks to filter which objects should be detected by the attractor. This can be more efficient than checking components.
-
Pause Attraction: For the feature to pause attraction while in the level-up screen, you can introduce a boolean variable to track the game state and check this condition in your
Update
method. -
Debugging: Consider adding debug logs to trace the collection process and see if the items are being attracted as expected. This can help you identify and resolve any issues during development.
Feel free to implement any of these suggestions as you see fit! If you have any specific questions about your code or run into any issues while implementing these suggestions, don’t hesitate to ask. Good luck with your project!
Best, [Your Name]
May 5, 2025 at 2:24 pm #18071::Thanks for sharing the code Nathaniel. It looks really good!
Can I trouble you to upload your PickupAttractorOrb code? You uploaded the Pickup code twice instead.
May 5, 2025 at 3:22 pm #18072::Sorry about that, the code is:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PickupAttractorOrb : Pickup { [Header("Attractor Settings")] public float radius = 6f; public LayerMask attractionLayer; [Tooltip("List of pickup scripts (from prefabs or scene objects) to attract.")] public List<MonoBehaviour> objectsToAttract; private List<System.Type> attractTypes = new List<System.Type>(); private List<Pickup> pickupsToAttract = new List<Pickup>(); public override bool Collect(PlayerStats target, float speedIgnored = 0f, float lifespanOverride = 0f) { if (!base.Collect(target, speedIgnored, lifespanOverride)) return false; attractTypes.Clear(); pickupsToAttract.Clear(); foreach (var obj in objectsToAttract) { if (obj != null) attractTypes.Add(obj.GetType()); } if (attractTypes.Count > 0) { // Cache valid pickups at time of collection Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, radius, attractionLayer); foreach (Collider2D col in hits) { foreach (var type in attractTypes) { var component = col.GetComponent(type); if (component != null && component is Pickup pickup && pickup != this) { pickupsToAttract.Add(pickup); break; } } } if (pickupsToAttract.Count > 0) StartCoroutine(AttractCachedPickups()); } else { Debug.LogWarning("PickupAttractorOrb: No attractable objects set."); } return true; } private IEnumerator AttractCachedPickups() { while (pickupsToAttract.Count > 0) { for (int i = pickupsToAttract.Count - 1; i >= 0; i--) { var pickup = pickupsToAttract[i]; if (pickup == null) { pickupsToAttract.RemoveAt(i); continue; } if (pickup.Collect(target)) { pickupsToAttract.RemoveAt(i); } } yield return new WaitForSeconds(0.1f); } Destroy(gameObject); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(0.3f, 0.9f, 1f, 0.3f); Gizmos.DrawWireSphere(transform.position, radius); } }
Please could you edit my original post to include this? Thank you.
May 6, 2025 at 12:32 am #18073::Of course Nathaniel. I’ve edited your post accordingly.
Your code looks very good! On a macro level, there isn’t anything I would do very differently from what you did — the room for improvement parts are minor tweaks to certain parts of your code to optimise speed, as well as things like remove null reference exceptions, and Alp has succinctly summarised a good list of them above.
Let me know if you’d like me to clarify any of the pointers above that Alp raised.
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: