Forum begins after the advertisement:
[Part 15] Need help making a Santa Water clone
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [Part 15] Need help making a Santa Water clone
- This topic has 6 replies, 2 voices, and was last updated 1 day, 13 hours ago by
Jukers.
-
AuthorPosts
-
April 15, 2025 at 12:37 pm #17922::
Ive been trying for about two hours now, reading the scripts and trying to understand it with AI help, but i cant seem to understand how to implement a Santa Water-like weapon.
Apparently doesn’t work the same way as Whip Weapon for example, because the Aura weapons work as some other way Whip Weapons inherits from Projectile but there is no GarlicWeapon, by my understand its split between Aura and AuraWeapon, which use functions from Weapon… Im currently in part 23. If you could help me, that would be massive!
April 16, 2025 at 1:22 pm #17929::Hi Jukers, for Santa Water, you can think of it as a weapon that constantly spawns new prefabs that hold an
Aura
component. This Aura will be configured to deal damage equivalent to what you want your Santa Water to deal.You will then need a custom weapon script that constantly spawns these prefabs around you. You can create a projectile weapon that homes in on targets, kind of like the Fire Wand, but when colliding with an enemy, instead of dealing damage, it will create the aura prefab for the Santa Water.
Let me know if this helps.
April 17, 2025 at 7:26 am #17939::my idea is to use something similar to the Lighning weapon which picks a random enemy the problem is that when i instantiate the prefab (dont know if im doing the right way) it always stay in the player following it, tried to remove it as a child of the player but didnt succeed.
I managed to do this with some AI help, SantaWaterWeapon.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// Dispara uma aura (efeito de Holy Water) sobre inimigos selecionados, /// instanciando o prefab de Aura que aplica dano ao longo do tempo. /// Controla o lifespan e o cooldown antes de permitir nova instância. /// </summary> public class SantaWaterWeapon : AuraWeapon { private List<EnemyStats> allSelectedEnemies = new List<EnemyStats>(); private int currentAttackCount; private float currentAttackInterval; private Aura activeAura; void Awake() { // Número de ataques base vindo de WeaponData currentAttackCount = ((WeaponData)data).baseStats.number; } protected override bool Attack(int attackCount = 1) { // Usa currentStats (Weapon.Stats) para acessar prefab e parâmetros var stats = currentStats; // 1) Verifica se o prefab de Aura está definido if (stats.auraPrefab == null) { Debug.LogWarning($"Aura prefab não definido para {name}"); ActivateCooldown(true); return false; } // 2) Verifica cooldown geral e se já existe uma aura ativa if (!CanAttack() || activeAura != null) return false; // 3) Na primeira ativação do ciclo, popula lista e reseta contagem if (currentCooldown <= 0) { allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); ActivateCooldown(); currentAttackCount = attackCount; } // 4) Seleciona um inimigo aleatório para aplicar a aura EnemyStats target = PickEnemy(); if (target) { // Calcula offset vertical para spawn acima da cabeça SpriteRenderer sr = target.GetComponent<SpriteRenderer>(); float offsetY = sr ? sr.bounds.extents.y : 0.5f; Vector3 spawnPos = target.transform.position + Vector3.up * offsetY; Debug.Log($"Instanciando aura em {spawnPos} para {target.name}"); // Instancia o componente Aura (prefab) em spawnPos Aura auraComp = Instantiate(stats.auraPrefab, spawnPos, Quaternion.identity); auraComp.transform.SetParent(target.transform); // Configura o script Aura para usar esta arma auraComp.SetWeapon(this); // Ajusta o raio do Collider de acordo com a área da arma CircleCollider2D cc = auraComp.GetComponent<CircleCollider2D>(); if (cc) cc.radius = GetArea(); // Guarda referência e controla duração activeAura = auraComp; float duration = stats.lifespan; // Destrói o GameObject associado à aura após duração Destroy(auraComp.gameObject, duration); StartCoroutine(ClearActiveAura(duration)); } // Efeito proc no jogador, se existir if (stats.procEffect) Destroy(Instantiate(stats.procEffect, owner.transform), 5f); // Ajusta contagem de ataques remanescentes if (attackCount > 0) { currentAttackCount = attackCount - 1; currentAttackInterval = stats.projectileInterval; } return true; } private IEnumerator ClearActiveAura(float delay) { yield return new WaitForSeconds(delay); activeAura = null; } /// <summary> /// Seleciona aleatoriamente um EnemyStats visível, removendo-o da lista. /// </summary> private EnemyStats PickEnemy() { EnemyStats target = null; while (!target && allSelectedEnemies.Count > 0) { int idx = Random.Range(0, allSelectedEnemies.Count); target = allSelectedEnemies[idx]; if (!target) { allSelectedEnemies.RemoveAt(idx); continue; } Renderer r = target.GetComponent<Renderer>(); if (!r || !r.isVisible) { allSelectedEnemies.Remove(target); target = null; continue; } } if (target) allSelectedEnemies.Remove(target); return target; } }
Current beheaviour: spawns around the player just like the default Aura weapon, doesn’t disappear when lifespan expires and dont re-appear when
April 17, 2025 at 5:19 pm #17956::Let me have one of the guys working on the Vampire Survivors series help you with this next week, after the Easter weekend.
We’ll write a guide and post it on this forum. Keep you updated. We should be able to get it up around Tuesday next week.
April 18, 2025 at 5:33 am #17957::Don’t worry about it! I would love some help but i get that you are busy and all that stuff! thanks for replying to me.
I’m doing some work myself, i managed to assemble this “monster”
// SantaWaterWeapon.cs using System.Collections; using System.Collections.Generic; using UnityEditor.Analytics; using UnityEngine; /// <summary> /// Dispara uma aura (efeito de Holy Water) sobre inimigos selecionados, /// instanciando o prefab de Aura que aplica dano ao longo do tempo. /// Controla o lifespan e o cooldown antes de permitir nova instância. /// </summary> public class SantaWaterWeapon : AuraWeapon { private List<EnemyStats> allSelectedEnemies = new List<EnemyStats>(); private int currentAttackCount; private float currentAttackInterval; private Aura activeAura; void Awake() { // Número de ataques base vindo de WeaponData currentAttackCount = ((WeaponData)data).baseStats.number; } protected override bool Attack(int attackCount = 1) { // Usa currentStats (Weapon.Stats) para acessar prefab e parâmetros var stats = currentStats; // 1) Verifica se o prefab de Aura está definido if (stats.auraPrefab == null) { Debug.LogWarning($"Aura prefab não definido para {name}"); ActivateCooldown(true); return false; } // 2) Verifica cooldown geral e se já existe uma aura ativa if (!CanAttack() || activeAura != null) return false; // 3) Na primeira ativação do ciclo, popula lista e reseta contagem if (currentCooldown <= 0) { allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); ActivateCooldown(); currentAttackCount = attackCount; } // 4) Seleciona um inimigo aleatório para aplicar a aura EnemyStats target = PickEnemy(); if (target) { // Calcula offset vertical para spawn acima da cabeça SpriteRenderer sr = target.GetComponent<SpriteRenderer>(); float offsetY = sr ? sr.bounds.extents.y : 0.5f; Vector3 spawnPos = target.transform.position + Vector3.up * offsetY; Debug.Log($"Instanciando aura em {spawnPos} para {target.name}"); // Instancia o componente Aura (prefab) em spawnPos Aura auraComp = Instantiate(stats.auraPrefab, spawnPos, Quaternion.identity); auraComp.transform.SetParent(target.transform); // Configura o script Aura para usar esta arma auraComp.SetWeapon(this); // Ajusta o raio do Collider de acordo com a área da arma CircleCollider2D cc = auraComp.GetComponent<CircleCollider2D>(); if (cc) cc.radius = GetArea(); // Guarda referência e controla duração activeAura = auraComp; Debug.Log($"Lifespan do objeto: {stats.lifespan}"); float lifespan = stats.lifespan; // Destrói o GameObject associado à aura após duração Destroy(auraComp.gameObject, lifespan); StartCoroutine(ClearActiveAura(lifespan)); } // Efeito proc no jogador, se existir if (stats.procEffect) Destroy(Instantiate(stats.procEffect, owner.transform), 5f); // Ajusta contagem de ataques remanescentes if (attackCount > 0) { currentAttackCount = attackCount - 1; currentAttackInterval = stats.projectileInterval; } return true; } private IEnumerator ClearActiveAura(float delay) { yield return new WaitForSeconds(delay); activeAura = null; } /// <summary> /// Seleciona aleatoriamente um EnemyStats visível, removendo-o da lista. /// </summary> private EnemyStats PickEnemy() { EnemyStats target = null; while (!target && allSelectedEnemies.Count > 0) { int idx = Random.Range(0, allSelectedEnemies.Count); target = allSelectedEnemies[idx]; if (!target) { allSelectedEnemies.RemoveAt(idx); continue; } Renderer r = target.GetComponent<Renderer>(); if (!r || !r.isVisible) { allSelectedEnemies.Remove(target); target = null; continue; } } if (target) allSelectedEnemies.Remove(target); return target; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AuraWeapon : Weapon { protected Aura currentAura; protected bool isSpawning = false; protected bool SantaWaterBeheaviour = true; List<EnemyStats> allSelectedEnemies = new List<EnemyStats>(); protected override void Update() { if (SantaWaterBeheaviour && !isSpawning) { StartCoroutine(SpawnAurasCoroutine()); //Debug.Log("Tentando iniciar Coroutine"); } } public override void OnEquip() { if (!SantaWaterBeheaviour) { // Garlic-style aura if (currentStats.auraPrefab) { if (currentAura) Destroy(currentAura); currentAura = Instantiate(currentStats.auraPrefab, transform); currentAura.weapon = this; currentAura.owner = owner; float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } } public override void OnUnequip() { if (!SantaWaterBeheaviour) { if (currentAura) Destroy(currentAura); } } public override bool DoLevelUp() { if (!base.DoLevelUp()) return false; if (!SantaWaterBeheaviour) { if (currentAura) { float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } return true; } private IEnumerator SpawnAurasCoroutine() { //Debug.Log("Coroutine iniciada"); isSpawning = true; int count = currentStats.number; float interval = currentStats.projectileInterval; // Refresh enemy list allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); for (int i = 0; i < count; i++) { EnemyStats target = PickEnemy(); if (target) { SpawnAuraOnTarget(target); } if (i < count - 1) yield return new WaitForSeconds(interval); } yield return new WaitForSeconds(currentStats.cooldown); isSpawning = false; } private void SpawnAuraOnTarget(EnemyStats target) { if (!currentStats.auraPrefab || !target) return; Vector2 spawnPosition = target.transform.position; Aura aura = Instantiate(currentStats.auraPrefab, spawnPosition, Quaternion.identity); aura.weapon = this; aura.owner = owner; float area = GetArea(); aura.transform.localScale = new Vector3(area, area, area); Destroy(aura.gameObject, currentStats.lifespan); } // Randomly picks a visible enemy on screen private EnemyStats PickEnemy() { EnemyStats target = null; while (!target && allSelectedEnemies.Count > 0) { int idx = Random.Range(0, allSelectedEnemies.Count); target = allSelectedEnemies[idx]; if (!target) { allSelectedEnemies.RemoveAt(idx); continue; } Renderer r = target.GetComponent<Renderer>(); if (!r || !r.isVisible) { allSelectedEnemies.Remove(target); target = null; continue; } } if (target) allSelectedEnemies.Remove(target); return target; } }
Im manually switching up via script the beheaviour on the variable SantaWaterBeheaviour on AuraWeapon, it is currently working as intended but i need to understand it fully, another thing that i dont know why is that the script santawaterweapon starts by being disabled, so i did some tweaks on playerinventory to make sure that the scripts that spawn via weapon controller are active
//REST OF THE CODE ABOVE if (weaponType != null) { // Spawn the weapon controller game object GameObject go = new GameObject(data.baseStats.name + " Controller"); Weapon spawnedWeapon = (Weapon)go.AddComponent(weaponType); <strong>spawnedWeapon.enabled = true; //Make sure its script is enabled (santawater error)</strong> spawnedWeapon.transform.SetParent(transform); spawnedWeapon.transform.localPosition = Vector2.zero; spawnedWeapon.Initialise(data); spawnedWeapon.OnEquip(); //REST OF THE CODE BELOW
Theres a lot of stuff remaining to change, like aplying buffs and stats, but thats what i managed to do in about 4-5 hours Helped me understand the script developed by the team a lot better, there are still a lot of things that i dont completely understand, but thats part of the process!
April 18, 2025 at 5:09 pm #17960::@Jukers no problem at all. One tip about the code if you don’t mind: you shouldn’t be modifying the
AuraWeapon
script if you can help it. Instead of introducing theSantaWeaponBehaviour
variable, you should instead do this (usingUpdate()
as an example).public class SantaWaterWeapon : AuraWeapon { ... public override void Update() { base.Update(); // Calls Update() on AuraWeapon(). StartCoroutine(SpawnAurasCoroutine()); // Your additional functionality specific to SantaWaterWeapon. } }
Otherwise, when you make more weapons, you will find yourself needing to add more and more specific behaviours to the parent classes, which will not be sustainable over the long term.
As for the actual functionality, instead of subclassing
AuraWeapon
, an easy way to do it will be to subclassLightningWeapon
instead. I would then override theAttack()
function, and remove theDamageArea()
.protected override bool Attack(int attackCount = 1) { // If no projectile prefab is assigned, leave a warning message. if (!currentStats.hitEffect) { Debug.LogWarning(string.Format("Hit effect prefab has not been set for {0}", name)); currentCooldown = currentStats.cooldown; return false; } // If there is no projectile assigned, set the weapon on cooldown. if (!CanAttack()) return false; // If the cooldown is less than 0, this is the first firing of the weapon. // Refresh the array of selected enemies. if (currentCooldown <= 0) { allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); currentCooldown += currentStats.cooldown; currentAttackCount = attackCount; } // Find an enemy in the map to strike with lightning. EnemyStats target = PickEnemy(); if(target) {
DamageArea(target.transform.position, currentStats.area, GetDamage());Instantiate(currentStats.hitEffect, target.transform.position, Quaternion.identity); } // If we have more than 1 attack count. if(attackCount > 0) { currentAttackCount = attackCount - 1; currentAttackInterval = currentStats.projectileInterval; } return true; }For the damage, I will make a new hit effect for the Santa Water, and attach an
Aura
component (notAuraWeapon
!) to do the area damage over time. You also have to make sure the hit effect stays for a longer duration (instead of expiring after a short amount of time).I hope this makes sense!
April 20, 2025 at 10:43 am #17962::I didnt exactly followed up that path because im not that experieced… but i managed to do great, i think
AuraWeapon.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AuraWeapon : Weapon { protected Aura currentAura; protected bool isSpawning = false; private CharacterData.Stats actualStats; protected bool SantaWaterBeheaviour; private bool isInitialized = false; List<EnemyStats> allSelectedEnemies = new List<EnemyStats>(); private void Init() { if (isInitialized) return; SantaWaterBeheaviour = currentStats.isSantaWater; Debug.Log(currentStats.isSantaWater); isInitialized = true; } protected override void Update() { if (!isInitialized) Init(); if(!isInitialized) return; if (SantaWaterBeheaviour && !isSpawning) { StartCoroutine(SpawnAurasCoroutine()); } } public override void OnEquip() { Init(); if (!SantaWaterBeheaviour) { // Garlic-style aura if (currentStats.auraPrefab) { if (currentAura) Destroy(currentAura); currentAura = Instantiate(currentStats.auraPrefab, transform); currentAura.weapon = this; currentAura.owner = owner; float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } } public override void OnUnequip() { if (!SantaWaterBeheaviour) { if (currentAura) Destroy(currentAura); } } public override bool DoLevelUp() { if (!base.DoLevelUp()) return false; if (!SantaWaterBeheaviour) { if (currentAura) { float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } return true; } private IEnumerator SpawnAurasCoroutine() { isSpawning = true; int count = currentStats.number + Owner.Stats.amount; float interval = currentStats.projectileInterval; allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); for (int i = 0; i < count; i++) { EnemyStats target = PickClosestFreeEnemy(); if (target) { SpawnAuraOnTarget(target); } if (i < count - 1) yield return new WaitForSeconds(interval); } yield return new WaitForSeconds(currentStats.cooldown * Owner.Stats.cooldown); isSpawning = false; } private void SpawnAuraOnTarget(EnemyStats target) { if (!currentStats.auraPrefab || !target) return; Vector2 spawnPosition = target.transform.position; Aura aura = Instantiate(currentStats.auraPrefab, spawnPosition, Quaternion.identity); aura.weapon = this; aura.owner = owner; float area = GetArea(); aura.transform.localScale = new Vector3(area, area, area); Destroy(aura.gameObject, currentStats.lifespan * Owner.Stats.duration); } private EnemyStats PickClosestFreeEnemy() { Vector2 playerPosition = owner.transform.position; float minDistanceBetweenAuras = 1.5f; // Ajuste conforme o tamanho da aura List<EnemyStats> sortedEnemies = new List<EnemyStats>(allSelectedEnemies); sortedEnemies.Sort((a, b) => Vector2.Distance(a.transform.position, playerPosition) .CompareTo(Vector2.Distance(b.transform.position, playerPosition)) ); foreach (var enemy in sortedEnemies) { if (!enemy) continue; Renderer r = enemy.GetComponent<Renderer>(); if (!r || !r.isVisible) continue; Vector2 pos = enemy.transform.position; if (!IsAuraNear(pos, minDistanceBetweenAuras)) { allSelectedEnemies.Remove(enemy); return enemy; } } return null; } private bool IsAuraNear(Vector2 position, float minDistance) { Aura[] existingAuras = FindObjectsOfType<Aura>(); foreach (var aura in existingAuras) { if (Vector2.Distance(aura.transform.position, position) < minDistance) { return true; } } return false; } }
Changed the beheaviour to instantiate on the closest enemie and verify if there is an aura at that place, if there is, it will try to get another enemie. Made like this because i think makes more sense, the player will have a feeling that the item that it haves is being used
in order to make those changes you need to have a empty SantaWaterWeapon Script
using System.Collections; using System.Collections.Generic; using UnityEditor.Analytics; using UnityEngine; using UnityEngine.Rendering.Universal; public class SantaWaterWeapon : AuraWeapon { }
maybe in the future im going to port the functionality to there, to makes things clear, but for know, this works.
has upvoted this post. -
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: