

1. ๊ฐ์ ๋ฐ ๋์ ์๋ฆฌ
1. ์์ ์คํฐ ๋ฐ ํ๋ฉด ์ด๋ (birthDuration)
- ํํฐํด์ ์์ฑ ์
Vector3.zero(์์ )์์ ์์ํฉ๋๋ค.
age / birthDuration ๋น์จ์ ์ฌ์ฉํ์ฌ 0์ด์์ ์ค์ ๋ ์๊ฐ๊น์ง ์ ํ ๋ณด๊ฐ(Lerp)์ผ๋ก ํ๋ฉด๊น์ง ์ด๋ํฉ๋๋ค.
- ์ด ๊ธฐ๊ฐ ๋์์ ๋
ธ์ด์ฆ ํ๋ฆ์ด ์ ์ฉ๋์ง ์์, ๋ง์น ์ค์ฌ์์ ๋ถ์ถ๋์ด ํ๋ฉด์ผ๋ก ์์ฐฉํ๋ ๋ฏํ ์ฐ์ถ์ ๊ตฌํํฉ๋๋ค.
2. ํด๋ฅ์ ๊ฐ์ ๋
ธ์ด์ฆ ํ๋ ๊ตฌํ
- Unity์
Mathf.PerlinNoise๋ ๊ธฐ๋ณธ์ ์ผ๋ก 2D ํจ์์
๋๋ค. ์ด๋ฅผ 3D ๊ตฌ ํ๋ฉด์์ ํด๋ฅ์ฒ๋ผ ํ๋ฅด๋๋ก ๋ง๋ค๊ธฐ ์ํด ์์ฌ 3D ๋ฒกํฐ์ฅ(Pseudo-3D Vector Field) ๊ธฐ๋ฒ์ ์ฌ์ฉํฉ๋๋ค.
nx, ny, nz๋ฅผ ์๋ก ๋ค๋ฅธ ์ขํ ์กฐํฉ๊ณผ ์๊ฐ ์คํ์
์ผ๋ก ์ํ๋งํ์ฌ, ๊ณต๊ฐ์ ์ผ๋ก ์ผ๊ด๋๋ฉด์๋ ํ์ ์ฑ๋ถ์ ๊ฐ์ง ๋ฒกํฐ๋ฅผ ์์ฑํฉ๋๋ค.
Vector3.Dot(noiseVector, normal) * normal ๊ณ์ฐ์ ํตํด ๋
ธ์ด์ฆ ๋ฒกํฐ์์ ๋ฐ์ง๋ฆ ๋ฐฉํฅ ์ฑ๋ถ(๊ตฌ ๋ฐ์ผ๋ก ๋๊ฐ๋ ํ)์ ์ ๊ฑฐํฉ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ํํฐํด์ ํญ์ ๊ตฌ ํ๋ฉด์ ์ ํ๋ ๋ฐฉํฅ(Tangent)์ผ๋ก๋ง ํ์ ๋ฐ์ ํ๋ฉด ์๋ฅผ ๋ฏธ๋๋ฌ์ง๋ฏ ํ๋ฅด๊ฒ ๋ฉ๋๋ค.
- ๋ง์ง๋ง์ผ๋ก
.normalized * sphereRadius๋ฅผ ์ ์ฉํ์ฌ ์์น๊ฐ ๊ตฌ ํ๋ฉด์์ ๋ฒ์ด๋์ง ์๋๋ก ๊ฐ์ ๊ตฌ์ํฉ๋๋ค.
3. 1/n ๊ท ๋ฑ ํ๋ฅ ๋ฐ ํ๋ง
validPrefabs ๋ฐฐ์ด์ ๊ธธ์ด n์ ๋ํด Random.Range(0, n)์ ํธ์ถํ์ฌ ๊ฐ ํ๋ฆฌํน์ด ์ ํํ 1/n์ ํ๋ฅ ๋ก ์ ํ๋ฉ๋๋ค.
- ์ ํ๋ ํ๋ฆฌํน์ ํ ๋น๋ ์ ์ฉ ํ์์๋ง ๋นํ์ฑํ ๊ฐ์ฒด๋ฅผ ์ฌ์ฌ์ฉํ๋ฏ๋ก, ํ๋ฆฌํน ํ์
๋ณ ๋ฉ๋ชจ๋ฆฌ ์ง์ญ์ฑ์ด ์ ์ง๋๊ณ ๊ฐ๋น์ง ์ปฌ๋ ์
๋ถํ๊ฐ ์ต์ํ๋ฉ๋๋ค.
4. Inspector ๊ถ์ฅ ์ค์ ๊ฐ
| ํญ๋ชฉ | ์ค๋ช
| ์ถ์ฒ๊ฐ |
|---|
Max Particles | ๋์ ํ์ฑ ํํฐํด ์ (์ฑ๋ฅ์ ์ํฅ) | 200 ~ 600 |
Sphere Radius | ๊ตฌ์ ํฌ๊ธฐ | 5.0 |
Noise Frequency | ํ๋ฅด๋ ๋ฌผ๊ฒฐ์ ๋ฐ๋ | 0.4 ~ 0.8 |
Noise Strength | ํ๋ฆ์ ์งํญ (ํ๋ฉด์ ๋ฒ์ด๋์ง ์๊ฒ ์ ๊ทํ๋จ) | 1.5 ~ 3.0 |
Flow Speed | ํด๋ฅ์ ์ด๋ ์๋ | 1.0 ~ 3.0 |
Birth Duration | ์ค์ฌ์์ ํ๋ฉด๊น์ง ๋๋ฌ ์๊ฐ | 0.8 ~ 1.5 |
4. ์ ์ฉ ์ ์ฐธ๊ณ ์ฌํญ
- ํ๋ฆฌํน ์ค์ฌ์ถ ํ์ธ: ๋ชจ๋ธ๋ง ๋ฉ์์ ๋ก์ปฌ ์ค์ฌ(pivot)์ด ๊ธฐํํ์ ์ค์ฌ์ ์์นํด์ผ ํ๋ฉด ๊ตฌ์ ์ ๊ธฐ์ธ์ด์ง ์์ด ๋งค๋๋ฝ๊ฒ ์์ง์
๋๋ค. ์ค์ฌ์ด ์ด๊ธ๋๋ฉด ํ๋ฆฌํน ๋ด๋ถ์ ๋น GameObject๋ฅผ ์ค์ฌ์ผ๋ก ์ก๊ณ ๋ฉ์๋ฅผ ํ์๋ก ๋ฐฐ์นํ์ธ์.
- ์ฑ๋ฅ ๊ด๋ฆฌ:
maxParticles๋ ์ค์ ํ๋ฉด์ ํ์๋ก ํ๋ ์ต๋ ๊ฐ์๋ก ์ค์ ํ์ธ์. ํ์ด ๊ฐ๋ ์ฐจ๋ฉด ์ ํํฐํด ์์ฑ์ด ์คํต๋๋ฏ๋ก, ํ๋ ์ ๋๋กญ ์์ด ์์ ์ ์ผ๋ก ๋์ํฉ๋๋ค.
- ๋
ธ์ด์ฆ ํจํด ๋ค์ํ:
noiseFrequency๋ฅผ ๋ฎ์ถ๋ฉด ๋์ ํด๋ฅ์ฒ๋ผ ๋๋ฆฌ๊ณ ๊ฑฐ๋ํ ํ๋ฆ์ด, ๋์ผ๋ฉด ์์ ์์ฉ๋์ด๊ฐ ๋ง์ด ์๊ธฐ๋ ํจ๊ณผ๋ฅผ ์ป์ ์ ์์ต๋๋ค. ์คํ ์ค Inspector ์์ ์ค์๊ฐ์ผ๋ก ์กฐ์ ํ๋ฉฐ ์ํ๋ ํ๋ฆ์ ์ฐพ์ผ์ธ์.
2. ์คํฌ๋ฆฝํธ
using UnityEngine;
using System.Collections.Generic;
public class SphereFlowParticleSystem : MonoBehaviour
{
[Header("Prefab Settings")]
[Tooltip("์ต๋ 5๊ฐ์ ํ๋ฆฌํน์ ํ ๋นํ ์ ์์ต๋๋ค. ์ต์ 1๊ฐ๋ ํ์์
๋๋ค.")]
public GameObject[] particlePrefabs = new GameObject[5];
public int maxParticles = 300;
public float lifetime = 8f;
public float birthDuration = 1.2f;
[Header("Sphere & Flow Settings")]
public float sphereRadius = 5f;
public float noiseFrequency = 0.6f;
public float noiseStrength = 2.0f;
public float flowSpeed = 1.5f;
public float flowTimeScale = 0.8f;
[Header("Rotation Settings")]
public bool enableRandomStartRotation = true;
public bool enableContinuousRotation = true;
public Vector3 minAngularVelocity = new Vector3(-20f, -20f, -20f);
public Vector3 maxAngularVelocity = new Vector3(20f, 20f, 20f);
private List<GameObject> validPrefabs = new List<GameObject>();
private Dictionary<GameObject, List<ParticleData>> prefabPools = new Dictionary<GameObject, List<ParticleData>>();
private float spawnTimer = 0f;
private struct ParticleData
{
public GameObject gameObject;
public GameObject assignedPrefab;
public Vector3 initialDirection;
public Vector3 angularVelocity;
public float remainingLife;
public float age;
public bool isActive;
}
void OnValidate()
{
validPrefabs.Clear();
foreach (var p in particlePrefabs)
{
if (p != null && !validPrefabs.Contains(p))
validPrefabs.Add(p);
}
if (minAngularVelocity.x > maxAngularVelocity.x) maxAngularVelocity.x = minAngularVelocity.x;
if (minAngularVelocity.y > maxAngularVelocity.y) maxAngularVelocity.y = minAngularVelocity.y;
if (minAngularVelocity.z > maxAngularVelocity.z) maxAngularVelocity.z = minAngularVelocity.z;
if (birthDuration <= 0f) birthDuration = 0.1f;
if (sphereRadius <= 0.1f) sphereRadius = 0.1f;
}
void Start()
{
validPrefabs.Clear();
foreach (var p in particlePrefabs)
{
if (p != null && !validPrefabs.Contains(p))
validPrefabs.Add(p);
}
if (validPrefabs.Count == 0)
{
Debug.LogError("[SphereFlowParticleSystem] ๋์ํ๋ ค๋ฉด ์ต์ 1๊ฐ์ ํ๋ฆฌํน์ด ํ ๋น๋์ด์ผ ํฉ๋๋ค.");
enabled = false;
return;
}
InitializePools();
}
void InitializePools()
{
int validCount = validPrefabs.Count;
int baseCount = maxParticles / validCount;
int remainder = maxParticles % validCount;
foreach (var prefab in validPrefabs)
{
List<ParticleData> pool = new List<ParticleData>();
int countForPrefab = baseCount + (remainder > 0 ? 1 : 0);
remainder--;
for (int i = 0; i < countForPrefab; i++)
{
GameObject go = Instantiate(prefab, transform);
go.SetActive(false);
pool.Add(new ParticleData
{
gameObject = go,
assignedPrefab = prefab,
initialDirection = Vector3.zero,
angularVelocity = Vector3.zero,
remainingLife = 0f,
age = 0f,
isActive = false
});
}
prefabPools[prefab] = pool;
}
}
void Update()
{
spawnTimer += Time.deltaTime;
if (spawnTimer >= 1f / Mathf.Max(0.1f, maxParticles / lifetime))
{
spawnTimer = 0f;
SpawnParticle();
}
UpdateParticles();
}
void SpawnParticle()
{
if (validPrefabs.Count == 0) return;
GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
List<ParticleData> targetPool = prefabPools[selectedPrefab];
ParticleData? target = null;
for (int i = 0; i < targetPool.Count; i++)
{
if (!targetPool[i].isActive)
{
target = targetPool[i];
break;
}
}
if (target == null) return;
ParticleData p = target.Value;
Transform t = p.gameObject.transform;
t.position = Vector3.zero;
p.initialDirection = Random.onUnitSphere;
p.angularVelocity = new Vector3(
Random.Range(minAngularVelocity.x, maxAngularVelocity.x),
Random.Range(minAngularVelocity.y, maxAngularVelocity.y),
Random.Range(minAngularVelocity.z, maxAngularVelocity.z)
);
t.rotation = enableRandomStartRotation ? Random.rotationUniform : Quaternion.identity;
p.remainingLife = lifetime;
p.age = 0f;
p.isActive = true;
p.gameObject.SetActive(true);
int idx = targetPool.IndexOf(target.Value);
targetPool[idx] = p;
}
void UpdateParticles()
{
float dt = Time.deltaTime;
float currentTime = Time.time * flowTimeScale;
foreach (var pool in prefabPools.Values)
{
for (int i = 0; i < pool.Count; i++)
{
if (!pool[i].isActive) continue;
ParticleData p = pool[i];
Transform t = p.gameObject.transform;
p.age += dt;
p.remainingLife -= dt;
float birthProgress = Mathf.Clamp01(p.age / birthDuration);
Vector3 targetSurfacePos = p.initialDirection * sphereRadius;
if (birthProgress < 1f)
{
t.position = Vector3.Lerp(Vector3.zero, targetSurfacePos, birthProgress);
}
else
{
Vector3 pos = t.position;
Vector3 normal = pos.normalized;
float nx = Mathf.PerlinNoise(pos.y * noiseFrequency + currentTime, pos.z * noiseFrequency + currentTime * 0.3f) * 2f - 1f;
float ny = Mathf.PerlinNoise(pos.z * noiseFrequency + currentTime * 0.2f, pos.x * noiseFrequency + currentTime) * 2f - 1f;
float nz = Mathf.PerlinNoise(pos.x * noiseFrequency + currentTime * 0.4f, pos.y * noiseFrequency + currentTime * 0.1f) * 2f - 1f;
Vector3 noiseVector = new Vector3(nx, ny, nz) * noiseStrength;
Vector3 tangent = noiseVector - Vector3.Dot(noiseVector, normal) * normal;
Vector3 nextPos = pos + tangent * flowSpeed * dt;
nextPos = nextPos.normalized * sphereRadius;
t.position = nextPos;
}
if (enableContinuousRotation)
{
t.Rotate(
p.angularVelocity.x * dt,
p.angularVelocity.y * dt,
p.angularVelocity.z * dt,
Space.Self
);
}
if (p.remainingLife <= 0f)
{
p.gameObject.SetActive(false);
p.isActive = false;
}
pool[i] = p;
}
}
}
void OnDestroy()
{
foreach (var pool in prefabPools.Values)
{
foreach (var p in pool)
{
if (p.gameObject != null)
Destroy(p.gameObject);
}
pool.Clear();
}
prefabPools.Clear();
validPrefabs.Clear();
}
}