

๊ฐ ํํฐํด์ ๋
๋ฆฝ์ ์ธ GameObject๋ก ์์ฑํ๊ณ ์คํฌ๋ฆฝํธ๋ก ์์น/์์/์๋ช
์ ๊ด๋ฆฌํฉ๋๋ค.
Particle Settings ์น์
์ Particle Prefabs ๋ฐฐ์ด ํฌ๊ธฐ ํ์ธ (๊ธฐ๋ณธ 5).Max Particles ์ค์ ์, ํ ๋น๋ ํ๋ฆฌํน ๊ฐ์(n)๋ก ๊ท ๋ฑ ๋ถํ ๋จ. (์: MaxParticles=100, ํ๋ฆฌํน 2๊ฐ -> ๊ฐ 50๊ฐ ํ ํ ๋น)GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
Random.Range(0, n) ์ 0 ๋ถํฐ n-1 ๊น์ง์ ์ ์๋ฅผ ๊ท ๋ฑํ ํ๋ฅ ๋ก ๋ฐํ.validPrefabs ์์ฑ ์ ์๋์ผ๋ก ์ ์ธ๋๋ฏ๋ก ํ๋ฅ ๊ณ์ฐ์ ์ํฅ์ ์ฃผ์ง ์์.SpawnParticle() ํธ์ถ ์ ์ ํ๋ ํ๋ฆฌํน์ ํด๋นํ๋ ํ์์๋ง ๋นํ์ฑํ ๊ฐ์ฒด๋ฅผ ์ฌ์ฌ์ฉ.Max Particles ๊ฐ์ ์ค์ ํ์ ๊ฐ์๋ณด๋ค ์ฌ์ ์๊ฒ ์ค์ ํ๋ ๊ฒ์ ๊ถ์ฅ.Mesh Renderer ๋๋ Skinned Mesh Renderer ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ํ ๋น๋์ด ์๋์ง ํ์ธ.enableContinuousRotation) ์ด ํ์ฑํ๋ ๊ฒฝ์ฐ, ํ๋ฆฌํน์ ๋ก์ปฌ ์ถ์ด ๋น๋์นญ์ด๋ฉด ํ์ ๋ฐฉํฅ์ด ์ ๊ฐ๊ฐ์ผ๋ก ๋ณด์ฌ ๋ ์์ฐ์ค๋ฌ์ด ํจ๊ณผ๊ฐ ๋์ด.Start() ์์๋ง ํ์ด ์ด๊ธฐํ๋๋ฏ๋ก Scene ์ ์ฌ์คํํ๊ฑฐ๋ InitializePools() ๋ฅผ ์ง์ ํธ์ถํด์ผ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์๋จ.using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class MeshParticleSystem : MonoBehaviour
{
// =========================================================
// [Inspector ๋
ธ์ถ ๋ณ์: ํํฐํด ๊ธฐ๋ณธ ์ค์ ]
// =========================================================
[Header("Particle Settings")]
[Tooltip("์ต๋ 5๊ฐ์ ํ๋ฆฌํน์ ํ ๋นํ ์ ์์ต๋๋ค. ์ต์ 1๊ฐ๋ ํ์์
๋๋ค.")]
public GameObject[] particlePrefabs = new GameObject[5]; // 5๊ฐ ์ฌ๋กฏ ๋ฐฐ์ด
public int maxParticles = 100; // ์ ์ฒด ์์คํ
์์ ๊ด๋ฆฌํ ํํฐํด ์ด ๊ฐ์
public float spawnRate = 10f; // ์ด๋น ์์ฑ ๊ฐ์
public Vector3 spawnAreaSize = new Vector3(5, 5, 5); // ์์ฑ ์์ญ ํฌ๊ธฐ
// =========================================================
// [Inspector ๋
ธ์ถ ๋ณ์: ์ด๋ ๋ฐ ํ์ ์ค์ ]
// =========================================================
[Header("Motion Settings")]
public float minSpeed = 1f; // ์ต์ ์ด๋ ์๋
public float maxSpeed = 5f; // ์ต๋ ์ด๋ ์๋
public float lifetime = 3f; // ํํฐํด ์๋ช
[Header("Rotation Settings")]
public bool enableRandomStartRotation = true; // ์์ ์ ๋๋ค ํ์ ์ ์ฉ ์ฌ๋ถ
public bool enableContinuousRotation = true; // ์ง์์ ํ์ ์ ์ฉ ์ฌ๋ถ
public Vector3 minAngularVelocity = new Vector3(-30f, -30f, -30f); // ์ต์ ๊ฐ์๋ (๋/์ด)
public Vector3 maxAngularVelocity = new Vector3(30f, 30f, 30f); // ์ต๋ ๊ฐ์๋ (๋/์ด)
// =========================================================
// [Private ๋ฉค๋ฒ ๋ณ์: ํ๋ง ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ]
// =========================================================
private List<GameObject> validPrefabs = new List<GameObject>(); // null์ด ์๋ ํ๋ฆฌํน๋ง ํํฐ๋งํ ๋ฆฌ์คํธ
private Dictionary<GameObject, List<ParticleData>> prefabPools = new Dictionary<GameObject, List<ParticleData>>(); // ํ๋ฆฌํน๋ณ ํํฐํด ํ
private float spawnTimer = 0f;
private int totalAllocatedParticles = 0; // ์ค์ ๋ก ํ ๋น๋ ํํฐํด ์ดํฉ
// =========================================================
// [ํํฐํด ๋ฐ์ดํฐ ๊ตฌ์กฐ์ฒด]
// =========================================================
private struct ParticleData
{
public GameObject gameObject; // ํํฐํด GameObject
public GameObject assignedPrefab; // ์ด ํํฐํด์ด ์ํ ํ๋ฆฌํน ํ์
public Vector3 velocity; // ์ด๋ ์๋ ๋ฒกํฐ
public Vector3 angularVelocity; // ๊ฐ ์ถ๋ณ ํ์ ์๋ (๋/์ด)
public float remainingLife; // ๋จ์ ์๋ช
public bool isActive; // ํ์ฑํ ์ํ
}
// =========================================================
// [Unity LifeCycle: OnValidate (Inspector ๋ณ๊ฒฝ ์ ์๋ ํธ์ถ)]
// =========================================================
void OnValidate()
{
// Inspector ์์ ๋ฐฐ์ด ๋ณ๊ฒฝ ์ ์ ํจ ํ๋ฆฌํน ๋ชฉ๋ก ๊ฐฑ์
validPrefabs.Clear();
foreach (var p in particlePrefabs)
{
if (p != null && !validPrefabs.Contains(p))
validPrefabs.Add(p);
}
// ์ต์ 1๊ฐ ํ ๋น ๊ฒฝ๊ณ
if (validPrefabs.Count == 0 && Application.isPlaying)
Debug.LogWarning("[ManualParticleSystem] ์ต์ 1๊ฐ์ ํ๋ฆฌํน์ ํ ๋นํด์ผ ์์คํ
์ด ๋์ํฉ๋๋ค.");
// ์๋/ํ์ ๋ฒ์ ๋ณด์
if (minSpeed > maxSpeed) maxSpeed = minSpeed;
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;
}
// =========================================================
// [Unity LifeCycle: Start]
// =========================================================
void Start()
{
// ์ ํจ ํ๋ฆฌํน ์ต์ข
๊ฒ์ฆ
validPrefabs.Clear();
foreach (var p in particlePrefabs)
{
if (p != null && !validPrefabs.Contains(p))
validPrefabs.Add(p);
}
if (validPrefabs.Count == 0)
{
Debug.LogError("[ManualParticleSystem] ๋์ํ๋ ค๋ฉด ์ต์ 1๊ฐ์ ํ๋ฆฌํน์ด ํ ๋น๋์ด์ผ ํฉ๋๋ค. ์คํฌ๋ฆฝํธ๋ฅผ ๋นํ์ฑํํฉ๋๋ค.");
enabled = false;
return;
}
// ํ๋ฆฌํน๋ณ ํํฐํด ํ ์ด๊ธฐํ ๋ฐ ๊ฐ์ ๋ถ๋ฐฐ
InitializePools();
}
// =========================================================
// [Private Method: ํ๋ฆฌํน๋ณ ํํฐํด ํ ์์ฑ ๋ฐ ๊ท ๋ฑ ๋ถ๋ฐฐ]
// =========================================================
void InitializePools()
{
int validCount = validPrefabs.Count;
int baseCount = maxParticles / validCount;
int remainder = maxParticles % validCount;
totalAllocatedParticles = 0;
foreach (var prefab in validPrefabs)
{
List<ParticleData> pool = new List<ParticleData>();
// ๋๋จธ์ง ํํฐํด์ ์์๋๋ก ์ถ๊ฐํ์ฌ ์ดํฉ์ด maxParticles ๊ฐ ๋๋๋ก ๊ท ๋ฑ ๋ถ๋ฐฐ
int countForThisPrefab = baseCount + (remainder > 0 ? 1 : 0);
remainder--;
for (int i = 0; i < countForThisPrefab; i++)
{
GameObject go = Instantiate(prefab, transform);
go.SetActive(false);
pool.Add(new ParticleData
{
gameObject = go,
assignedPrefab = prefab,
velocity = Vector3.zero,
angularVelocity = Vector3.zero,
remainingLife = 0f,
isActive = false
});
totalAllocatedParticles++;
}
prefabPools[prefab] = pool;
}
Debug.Log($"[ManualParticleSystem] {validCount}๊ฐ ํ๋ฆฌํน์ ์ด {totalAllocatedParticles}๊ฐ ํํฐํด ํ ํ ๋น ์๋ฃ.");
}
// =========================================================
// [Unity LifeCycle: Update]
// =========================================================
void Update()
{
// ํํฐํด ์์ฑ ํ์ด๋จธ ์ฒ๋ฆฌ
spawnTimer += Time.deltaTime;
if (spawnTimer >= 1f / spawnRate)
{
spawnTimer = 0f;
SpawnParticle();
}
// ๋ชจ๋ ํํฐํด ์
๋ฐ์ดํธ (์ด๋ + ํ์ + ์๋ช
)
UpdateParticles();
}
// =========================================================
// [Private Method: ํํฐํด ์์ฑ (1/n ํ๋ฅ ๊ท ๋ฑ ๋ถ๋ฐฐ)]
// =========================================================
void SpawnParticle()
{
// 1/n ๊ท ๋ฑ ํ๋ฅ ๋ก ์ ํจ ํ๋ฆฌํน ์ค ํ๋ ์ ํ
GameObject selectedPrefab = validPrefabs[Random.Range(0, validPrefabs.Count)];
List<ParticleData> targetPool = prefabPools[selectedPrefab];
// ํด๋น ํ๋ฆฌํน ํ์์ ๋นํ์ฑํ๋ ํํฐํด ํ์
ParticleData? foundParticle = null;
for (int i = 0; i < targetPool.Count; i++)
{
if (!targetPool[i].isActive)
{
foundParticle = targetPool[i];
break;
}
}
// ํ์ด ๊ฐ๋ ์ฐฌ ๊ฒฝ์ฐ ์ ์ธ์คํด์ค ์์ฑ (์ฑ๋ฅ ๊ฒฝ๊ณ ๋ฐ์)
if (foundParticle == null)
{
Debug.LogWarning($"[ManualParticleSystem] {selectedPrefab.name} ํ์ด ๊ฐ๋ ์ฐผ์ต๋๋ค. ์ ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค. (maxParticles ์ฆ๊ฐ ๊ถ์ฅ)");
GameObject go = Instantiate(selectedPrefab, transform);
go.SetActive(false);
ParticleData newP = new ParticleData
{
gameObject = go,
assignedPrefab = selectedPrefab,
velocity = Vector3.zero,
angularVelocity = Vector3.zero,
remainingLife = 0f,
isActive = false
};
targetPool.Add(newP);
foundParticle = newP;
totalAllocatedParticles++;
}
// -------------------------------------------------
// [ํํฐํด ํ์ฑํ ๋ฐ ๋ฐ์ดํฐ ์ค์ ]
// -------------------------------------------------
ParticleData p = foundParticle.Value;
// ๋๋ค ์์น ์์ฑ
Vector3 spawnPos = transform.position +
new Vector3(
Random.Range(-spawnAreaSize.x * 0.5f, spawnAreaSize.x * 0.5f),
Random.Range(-spawnAreaSize.y * 0.5f, spawnAreaSize.y * 0.5f),
Random.Range(-spawnAreaSize.z * 0.5f, spawnAreaSize.z * 0.5f)
);
// ๋๋ค ์ด๋ ์๋
Vector3 randomVelocity = Random.onUnitSphere * Random.Range(minSpeed, maxSpeed);
// ๋๋ค ๊ฐ์๋
Vector3 randomAngularVelocity = new Vector3(
Random.Range(minAngularVelocity.x, maxAngularVelocity.x),
Random.Range(minAngularVelocity.y, maxAngularVelocity.y),
Random.Range(minAngularVelocity.z, maxAngularVelocity.z)
);
// ์์น/ํ์ ์ ์ฉ
p.gameObject.transform.position = spawnPos;
if (enableRandomStartRotation)
p.gameObject.transform.rotation = Random.rotationUniform;
else
p.gameObject.transform.rotation = Quaternion.identity;
// ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
p.velocity = randomVelocity;
p.angularVelocity = randomAngularVelocity;
p.remainingLife = lifetime;
p.isActive = true;
p.gameObject.SetActive(true);
// ๋ณ๊ฒฝ ์ฌํญ ํ์ ๋ฐ์ (๊ฐ ํ์
struct ์ด๋ฏ๋ก ๋ช
์์ ์ฌํ ๋น ํ์)
int index = targetPool.IndexOf(foundParticle.Value);
targetPool[index] = p;
}
// =========================================================
// [Private Method: ํํฐํด ์
๋ฐ์ดํธ]
// =========================================================
void UpdateParticles()
{
// ๋ชจ๋ ํ์ ์ํํ๋ฉฐ ์
๋ฐ์ดํธ
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;
// ์์น ์
๋ฐ์ดํธ
t.position += p.velocity * Time.deltaTime;
// ํ์ ์
๋ฐ์ดํธ
if (enableContinuousRotation)
{
t.Rotate(
p.angularVelocity.x * Time.deltaTime,
p.angularVelocity.y * Time.deltaTime,
p.angularVelocity.z * Time.deltaTime,
Space.Self
);
}
// ์๋ช
๊ฐ์
p.remainingLife -= Time.deltaTime;
// ์๋ช
์ข
๋ฃ ์ ๋นํ์ฑํ
if (p.remainingLife <= 0f)
{
p.gameObject.SetActive(false);
p.isActive = false;
}
pool[i] = p;
}
}
}
// =========================================================
// [Unity LifeCycle: OnDestroy]
// =========================================================
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();
}
}