using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEngine;
using UnityEditor;
#if UNITY_VFX_GRAPH //Please enable the symbol in the project settings for VisualEffectGraph to work
using UnityEngine.VFX;
#endif
namespace MCPForUnity.Editor.Tools
{
///
/// Tool for managing Unity VFX components:
/// - ParticleSystem (legacy particle effects)
/// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work)
/// - LineRenderer (lines, bezier curves, shapes)
/// - TrailRenderer (motion trails)
/// - More to come based on demand and feedback!
///
[McpForUnityTool("manage_vfx", AutoRegister = false)]
public static class ManageVFX
{
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString();
if (string.IsNullOrEmpty(action))
{
return new { success = false, message = "Action is required" };
}
try
{
string actionLower = action.ToLowerInvariant();
// Route to appropriate handler based on action prefix
if (actionLower == "ping")
{
return new { success = true, tool = "manage_vfx", components = new[] { "ParticleSystem", "VisualEffect", "LineRenderer", "TrailRenderer" } };
}
// ParticleSystem actions (particle_*)
if (actionLower.StartsWith("particle_"))
{
return HandleParticleSystemAction(@params, actionLower.Substring(9));
}
// VFX Graph actions (vfx_*)
if (actionLower.StartsWith("vfx_"))
{
return HandleVFXGraphAction(@params, actionLower.Substring(4));
}
// LineRenderer actions (line_*)
if (actionLower.StartsWith("line_"))
{
return HandleLineRendererAction(@params, actionLower.Substring(5));
}
// TrailRenderer actions (trail_*)
if (actionLower.StartsWith("trail_"))
{
return HandleTrailRendererAction(@params, actionLower.Substring(6));
}
return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" };
}
catch (Exception ex)
{
return new { success = false, message = ex.Message, stackTrace = ex.StackTrace };
}
}
#region Common Helpers
// Parsing delegates for use with RendererHelpers
private static Color ParseColor(JToken token) => VectorParsing.ParseColorOrDefault(token);
private static Vector3 ParseVector3(JToken token) => VectorParsing.ParseVector3OrDefault(token);
private static Vector4 ParseVector4(JToken token) => VectorParsing.ParseVector4OrDefault(token);
private static Gradient ParseGradient(JToken token) => VectorParsing.ParseGradientOrDefault(token);
private static AnimationCurve ParseAnimationCurve(JToken token, float defaultValue = 1f)
=> VectorParsing.ParseAnimationCurveOrDefault(token, defaultValue);
// Object resolution - delegates to ObjectResolver
private static GameObject FindTargetGameObject(JObject @params)
=> ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
private static Material FindMaterialByPath(string path)
=> ObjectResolver.ResolveMaterial(path);
#endregion
// ==================== PARTICLE SYSTEM ====================
#region ParticleSystem
private static object HandleParticleSystemAction(JObject @params, string action)
{
switch (action)
{
case "get_info": return ParticleGetInfo(@params);
case "set_main": return ParticleSetMain(@params);
case "set_emission": return ParticleSetEmission(@params);
case "set_shape": return ParticleSetShape(@params);
case "set_color_over_lifetime": return ParticleSetColorOverLifetime(@params);
case "set_size_over_lifetime": return ParticleSetSizeOverLifetime(@params);
case "set_velocity_over_lifetime": return ParticleSetVelocityOverLifetime(@params);
case "set_noise": return ParticleSetNoise(@params);
case "set_renderer": return ParticleSetRenderer(@params);
case "enable_module": return ParticleEnableModule(@params);
case "play": return ParticleControl(@params, "play");
case "stop": return ParticleControl(@params, "stop");
case "pause": return ParticleControl(@params, "pause");
case "restart": return ParticleControl(@params, "restart");
case "clear": return ParticleControl(@params, "clear");
case "add_burst": return ParticleAddBurst(@params);
case "clear_bursts": return ParticleClearBursts(@params);
default:
return new { success = false, message = $"Unknown particle action: {action}. Valid: get_info, set_main, set_emission, set_shape, set_color_over_lifetime, set_size_over_lifetime, set_velocity_over_lifetime, set_noise, set_renderer, enable_module, play, stop, pause, restart, clear, add_burst, clear_bursts" };
}
}
private static ParticleSystem FindParticleSystem(JObject @params)
{
GameObject go = FindTargetGameObject(@params);
return go?.GetComponent();
}
private static ParticleSystem.MinMaxCurve ParseMinMaxCurve(JToken token, float defaultValue = 1f)
{
if (token == null)
return new ParticleSystem.MinMaxCurve(defaultValue);
if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
{
return new ParticleSystem.MinMaxCurve(token.ToObject());
}
if (token is JObject obj)
{
string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "constant";
switch (mode)
{
case "constant":
float constant = obj["value"]?.ToObject() ?? defaultValue;
return new ParticleSystem.MinMaxCurve(constant);
case "random_between_constants":
case "two_constants":
float min = obj["min"]?.ToObject() ?? 0f;
float max = obj["max"]?.ToObject() ?? 1f;
return new ParticleSystem.MinMaxCurve(min, max);
case "curve":
AnimationCurve curve = ParseAnimationCurve(obj, defaultValue);
return new ParticleSystem.MinMaxCurve(obj["multiplier"]?.ToObject() ?? 1f, curve);
default:
return new ParticleSystem.MinMaxCurve(defaultValue);
}
}
return new ParticleSystem.MinMaxCurve(defaultValue);
}
private static ParticleSystem.MinMaxGradient ParseMinMaxGradient(JToken token)
{
if (token == null)
return new ParticleSystem.MinMaxGradient(Color.white);
if (token is JArray arr && arr.Count >= 3)
{
return new ParticleSystem.MinMaxGradient(ParseColor(arr));
}
if (token is JObject obj)
{
string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "color";
switch (mode)
{
case "color":
return new ParticleSystem.MinMaxGradient(ParseColor(obj["color"]));
case "two_colors":
Color colorMin = ParseColor(obj["colorMin"]);
Color colorMax = ParseColor(obj["colorMax"]);
return new ParticleSystem.MinMaxGradient(colorMin, colorMax);
case "gradient":
return new ParticleSystem.MinMaxGradient(ParseGradient(obj));
default:
return new ParticleSystem.MinMaxGradient(Color.white);
}
}
return new ParticleSystem.MinMaxGradient(Color.white);
}
private static object ParticleGetInfo(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null)
{
return new { success = false, message = "ParticleSystem not found" };
}
var main = ps.main;
var emission = ps.emission;
var shape = ps.shape;
var renderer = ps.GetComponent();
return new
{
success = true,
data = new
{
gameObject = ps.gameObject.name,
isPlaying = ps.isPlaying,
isPaused = ps.isPaused,
particleCount = ps.particleCount,
main = new
{
duration = main.duration,
looping = main.loop,
startLifetime = main.startLifetime.constant,
startSpeed = main.startSpeed.constant,
startSize = main.startSize.constant,
gravityModifier = main.gravityModifier.constant,
simulationSpace = main.simulationSpace.ToString(),
maxParticles = main.maxParticles
},
emission = new
{
enabled = emission.enabled,
rateOverTime = emission.rateOverTime.constant,
burstCount = emission.burstCount
},
shape = new
{
enabled = shape.enabled,
shapeType = shape.shapeType.ToString(),
radius = shape.radius,
angle = shape.angle
},
renderer = renderer != null ? new {
renderMode = renderer.renderMode.ToString(),
sortMode = renderer.sortMode.ToString(),
material = renderer.sharedMaterial?.name,
trailMaterial = renderer.trailMaterial?.name,
minParticleSize = renderer.minParticleSize,
maxParticleSize = renderer.maxParticleSize,
// Shadows & lighting
shadowCastingMode = renderer.shadowCastingMode.ToString(),
receiveShadows = renderer.receiveShadows,
lightProbeUsage = renderer.lightProbeUsage.ToString(),
reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(),
// Sorting
sortingOrder = renderer.sortingOrder,
sortingLayerName = renderer.sortingLayerName,
renderingLayerMask = renderer.renderingLayerMask
} : null
}
};
}
private static object ParticleSetMain(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Main");
var main = ps.main;
var changes = new List();
if (@params["duration"] != null) { main.duration = @params["duration"].ToObject(); changes.Add("duration"); }
if (@params["looping"] != null) { main.loop = @params["looping"].ToObject(); changes.Add("looping"); }
if (@params["prewarm"] != null) { main.prewarm = @params["prewarm"].ToObject(); changes.Add("prewarm"); }
if (@params["startDelay"] != null) { main.startDelay = ParseMinMaxCurve(@params["startDelay"], 0f); changes.Add("startDelay"); }
if (@params["startLifetime"] != null) { main.startLifetime = ParseMinMaxCurve(@params["startLifetime"], 5f); changes.Add("startLifetime"); }
if (@params["startSpeed"] != null) { main.startSpeed = ParseMinMaxCurve(@params["startSpeed"], 5f); changes.Add("startSpeed"); }
if (@params["startSize"] != null) { main.startSize = ParseMinMaxCurve(@params["startSize"], 1f); changes.Add("startSize"); }
if (@params["startRotation"] != null) { main.startRotation = ParseMinMaxCurve(@params["startRotation"], 0f); changes.Add("startRotation"); }
if (@params["startColor"] != null) { main.startColor = ParseMinMaxGradient(@params["startColor"]); changes.Add("startColor"); }
if (@params["gravityModifier"] != null) { main.gravityModifier = ParseMinMaxCurve(@params["gravityModifier"], 0f); changes.Add("gravityModifier"); }
if (@params["simulationSpace"] != null && Enum.TryParse(@params["simulationSpace"].ToString(), true, out var simSpace)) { main.simulationSpace = simSpace; changes.Add("simulationSpace"); }
if (@params["scalingMode"] != null && Enum.TryParse(@params["scalingMode"].ToString(), true, out var scaleMode)) { main.scalingMode = scaleMode; changes.Add("scalingMode"); }
if (@params["playOnAwake"] != null) { main.playOnAwake = @params["playOnAwake"].ToObject(); changes.Add("playOnAwake"); }
if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject(); changes.Add("maxParticles"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
}
private static object ParticleSetEmission(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Emission");
var emission = ps.emission;
var changes = new List();
if (@params["enabled"] != null) { emission.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
if (@params["rateOverTime"] != null) { emission.rateOverTime = ParseMinMaxCurve(@params["rateOverTime"], 10f); changes.Add("rateOverTime"); }
if (@params["rateOverDistance"] != null) { emission.rateOverDistance = ParseMinMaxCurve(@params["rateOverDistance"], 0f); changes.Add("rateOverDistance"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated emission: {string.Join(", ", changes)}" };
}
private static object ParticleSetShape(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Shape");
var shape = ps.shape;
var changes = new List();
if (@params["enabled"] != null) { shape.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
if (@params["shapeType"] != null && Enum.TryParse(@params["shapeType"].ToString(), true, out var shapeType)) { shape.shapeType = shapeType; changes.Add("shapeType"); }
if (@params["radius"] != null) { shape.radius = @params["radius"].ToObject(); changes.Add("radius"); }
if (@params["radiusThickness"] != null) { shape.radiusThickness = @params["radiusThickness"].ToObject(); changes.Add("radiusThickness"); }
if (@params["angle"] != null) { shape.angle = @params["angle"].ToObject(); changes.Add("angle"); }
if (@params["arc"] != null) { shape.arc = @params["arc"].ToObject(); changes.Add("arc"); }
if (@params["position"] != null) { shape.position = ParseVector3(@params["position"]); changes.Add("position"); }
if (@params["rotation"] != null) { shape.rotation = ParseVector3(@params["rotation"]); changes.Add("rotation"); }
if (@params["scale"] != null) { shape.scale = ParseVector3(@params["scale"]); changes.Add("scale"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated shape: {string.Join(", ", changes)}" };
}
private static object ParticleSetColorOverLifetime(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime");
var col = ps.colorOverLifetime;
var changes = new List();
if (@params["enabled"] != null) { col.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
if (@params["color"] != null) { col.color = ParseMinMaxGradient(@params["color"]); changes.Add("color"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
}
private static object ParticleSetSizeOverLifetime(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime");
var sol = ps.sizeOverLifetime;
var changes = new List();
// Auto-enable module if size properties are being set (unless explicitly disabled)
bool hasSizeProperty = @params["size"] != null || @params["sizeX"] != null ||
@params["sizeY"] != null || @params["sizeZ"] != null;
if (hasSizeProperty && @params["enabled"] == null && !sol.enabled)
{
sol.enabled = true;
changes.Add("enabled");
}
else if (@params["enabled"] != null)
{
sol.enabled = @params["enabled"].ToObject();
changes.Add("enabled");
}
if (@params["separateAxes"] != null) { sol.separateAxes = @params["separateAxes"].ToObject(); changes.Add("separateAxes"); }
if (@params["size"] != null) { sol.size = ParseMinMaxCurve(@params["size"], 1f); changes.Add("size"); }
if (@params["sizeX"] != null) { sol.x = ParseMinMaxCurve(@params["sizeX"], 1f); changes.Add("sizeX"); }
if (@params["sizeY"] != null) { sol.y = ParseMinMaxCurve(@params["sizeY"], 1f); changes.Add("sizeY"); }
if (@params["sizeZ"] != null) { sol.z = ParseMinMaxCurve(@params["sizeZ"], 1f); changes.Add("sizeZ"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
}
private static object ParticleSetVelocityOverLifetime(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime");
var vol = ps.velocityOverLifetime;
var changes = new List();
if (@params["enabled"] != null) { vol.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
if (@params["space"] != null && Enum.TryParse(@params["space"].ToString(), true, out var space)) { vol.space = space; changes.Add("space"); }
if (@params["x"] != null) { vol.x = ParseMinMaxCurve(@params["x"], 0f); changes.Add("x"); }
if (@params["y"] != null) { vol.y = ParseMinMaxCurve(@params["y"], 0f); changes.Add("y"); }
if (@params["z"] != null) { vol.z = ParseMinMaxCurve(@params["z"], 0f); changes.Add("z"); }
if (@params["speedModifier"] != null) { vol.speedModifier = ParseMinMaxCurve(@params["speedModifier"], 1f); changes.Add("speedModifier"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated: {string.Join(", ", changes)}" };
}
private static object ParticleSetNoise(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Set ParticleSystem Noise");
var noise = ps.noise;
var changes = new List();
if (@params["enabled"] != null) { noise.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); }
if (@params["strength"] != null) { noise.strength = ParseMinMaxCurve(@params["strength"], 1f); changes.Add("strength"); }
if (@params["frequency"] != null) { noise.frequency = @params["frequency"].ToObject(); changes.Add("frequency"); }
if (@params["scrollSpeed"] != null) { noise.scrollSpeed = ParseMinMaxCurve(@params["scrollSpeed"], 0f); changes.Add("scrollSpeed"); }
if (@params["damping"] != null) { noise.damping = @params["damping"].ToObject(); changes.Add("damping"); }
if (@params["octaveCount"] != null) { noise.octaveCount = @params["octaveCount"].ToObject(); changes.Add("octaveCount"); }
if (@params["quality"] != null && Enum.TryParse(@params["quality"].ToString(), true, out var quality)) { noise.quality = quality; changes.Add("quality"); }
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Updated noise: {string.Join(", ", changes)}" };
}
private static object ParticleSetRenderer(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
var renderer = ps.GetComponent();
if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" };
Undo.RecordObject(renderer, "Set ParticleSystem Renderer");
var changes = new List();
// ParticleSystem-specific render modes
if (@params["renderMode"] != null && Enum.TryParse(@params["renderMode"].ToString(), true, out var renderMode)) { renderer.renderMode = renderMode; changes.Add("renderMode"); }
if (@params["sortMode"] != null && Enum.TryParse(@params["sortMode"].ToString(), true, out var sortMode)) { renderer.sortMode = sortMode; changes.Add("sortMode"); }
// Particle size limits
if (@params["minParticleSize"] != null) { renderer.minParticleSize = @params["minParticleSize"].ToObject(); changes.Add("minParticleSize"); }
if (@params["maxParticleSize"] != null) { renderer.maxParticleSize = @params["maxParticleSize"].ToObject(); changes.Add("maxParticleSize"); }
// Stretched billboard settings
if (@params["lengthScale"] != null) { renderer.lengthScale = @params["lengthScale"].ToObject(); changes.Add("lengthScale"); }
if (@params["velocityScale"] != null) { renderer.velocityScale = @params["velocityScale"].ToObject(); changes.Add("velocityScale"); }
if (@params["cameraVelocityScale"] != null) { renderer.cameraVelocityScale = @params["cameraVelocityScale"].ToObject(); changes.Add("cameraVelocityScale"); }
if (@params["normalDirection"] != null) { renderer.normalDirection = @params["normalDirection"].ToObject(); changes.Add("normalDirection"); }
// Alignment and pivot
if (@params["alignment"] != null && Enum.TryParse(@params["alignment"].ToString(), true, out var alignment)) { renderer.alignment = alignment; changes.Add("alignment"); }
if (@params["pivot"] != null) { renderer.pivot = ParseVector3(@params["pivot"]); changes.Add("pivot"); }
if (@params["flip"] != null) { renderer.flip = ParseVector3(@params["flip"]); changes.Add("flip"); }
if (@params["allowRoll"] != null) { renderer.allowRoll = @params["allowRoll"].ToObject(); changes.Add("allowRoll"); }
//special case for particle system renderer
if (@params["shadowBias"] != null) { renderer.shadowBias = @params["shadowBias"].ToObject(); changes.Add("shadowBias"); }
// Common Renderer properties (shadows, lighting, probes, sorting)
RendererHelpers.ApplyCommonRendererProperties(renderer, @params, changes);
// Material
if (@params["materialPath"] != null)
{
var findInst = new JObject { ["find"] = @params["materialPath"].ToString() };
Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material;
if (mat != null) { renderer.sharedMaterial = mat; changes.Add("material"); }
}
if (@params["trailMaterialPath"] != null)
{
var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() };
Material mat = ManageGameObject.FindObjectByInstruction(findInst, typeof(Material)) as Material;
if (mat != null) { renderer.trailMaterial = mat; changes.Add("trailMaterial"); }
}
EditorUtility.SetDirty(renderer);
return new { success = true, message = $"Updated renderer: {string.Join(", ", changes)}" };
}
private static object ParticleEnableModule(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
string moduleName = @params["module"]?.ToString()?.ToLowerInvariant();
bool enabled = @params["enabled"]?.ToObject() ?? true;
if (string.IsNullOrEmpty(moduleName)) return new { success = false, message = "Module name required" };
Undo.RecordObject(ps, $"Toggle {moduleName}");
switch (moduleName.Replace("_", ""))
{
case "emission": var em = ps.emission; em.enabled = enabled; break;
case "shape": var sh = ps.shape; sh.enabled = enabled; break;
case "coloroverlifetime": var col = ps.colorOverLifetime; col.enabled = enabled; break;
case "sizeoverlifetime": var sol = ps.sizeOverLifetime; sol.enabled = enabled; break;
case "velocityoverlifetime": var vol = ps.velocityOverLifetime; vol.enabled = enabled; break;
case "noise": var n = ps.noise; n.enabled = enabled; break;
case "collision": var coll = ps.collision; coll.enabled = enabled; break;
case "trails": var tr = ps.trails; tr.enabled = enabled; break;
case "lights": var li = ps.lights; li.enabled = enabled; break;
default: return new { success = false, message = $"Unknown module: {moduleName}" };
}
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Module '{moduleName}' {(enabled ? "enabled" : "disabled")}" };
}
private static object ParticleControl(JObject @params, string action)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
bool withChildren = @params["withChildren"]?.ToObject() ?? true;
switch (action)
{
case "play": ps.Play(withChildren); break;
case "stop": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmitting); break;
case "pause": ps.Pause(withChildren); break;
case "restart": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmittingAndClear); ps.Play(withChildren); break;
case "clear": ps.Clear(withChildren); break;
}
return new { success = true, message = $"ParticleSystem {action}" };
}
private static object ParticleAddBurst(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Add Burst");
var emission = ps.emission;
float time = @params["time"]?.ToObject() ?? 0f;
short minCount = (short)(@params["minCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30);
short maxCount = (short)(@params["maxCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30);
int cycles = @params["cycles"]?.ToObject() ?? 1;
float interval = @params["interval"]?.ToObject() ?? 0.01f;
var burst = new ParticleSystem.Burst(time, minCount, maxCount, cycles, interval);
burst.probability = @params["probability"]?.ToObject() ?? 1f;
int idx = emission.burstCount;
var bursts = new ParticleSystem.Burst[idx + 1];
emission.GetBursts(bursts);
bursts[idx] = burst;
emission.SetBursts(bursts);
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Added burst at t={time}", burstIndex = idx };
}
private static object ParticleClearBursts(JObject @params)
{
ParticleSystem ps = FindParticleSystem(@params);
if (ps == null) return new { success = false, message = "ParticleSystem not found" };
Undo.RecordObject(ps, "Clear Bursts");
var emission = ps.emission;
int count = emission.burstCount;
emission.SetBursts(new ParticleSystem.Burst[0]);
EditorUtility.SetDirty(ps);
return new { success = true, message = $"Cleared {count} bursts" };
}
#endregion
// ==================== VFX GRAPH ====================
#region VFX Graph
private static object HandleVFXGraphAction(JObject @params, string action)
{
#if !UNITY_VFX_GRAPH
return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" };
#else
switch (action)
{
// Asset management
case "create_asset": return VFXCreateAsset(@params);
case "assign_asset": return VFXAssignAsset(@params);
case "list_templates": return VFXListTemplates(@params);
case "list_assets": return VFXListAssets(@params);
// Runtime parameter control
case "get_info": return VFXGetInfo(@params);
case "set_float": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetFloat(n, v));
case "set_int": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetInt(n, v));
case "set_bool": return VFXSetParameter(@params, (vfx, n, v) => vfx.SetBool(n, v));
case "set_vector2": return VFXSetVector(@params, 2);
case "set_vector3": return VFXSetVector(@params, 3);
case "set_vector4": return VFXSetVector(@params, 4);
case "set_color": return VFXSetColor(@params);
case "set_gradient": return VFXSetGradient(@params);
case "set_texture": return VFXSetTexture(@params);
case "set_mesh": return VFXSetMesh(@params);
case "set_curve": return VFXSetCurve(@params);
case "send_event": return VFXSendEvent(@params);
case "play": return VFXControl(@params, "play");
case "stop": return VFXControl(@params, "stop");
case "pause": return VFXControl(@params, "pause");
case "reinit": return VFXControl(@params, "reinit");
case "set_playback_speed": return VFXSetPlaybackSpeed(@params);
case "set_seed": return VFXSetSeed(@params);
default:
return new { success = false, message = $"Unknown vfx action: {action}. Valid: create_asset, assign_asset, list_templates, list_assets, get_info, set_float, set_int, set_bool, set_vector2/3/4, set_color, set_gradient, set_texture, set_mesh, set_curve, send_event, play, stop, pause, reinit, set_playback_speed, set_seed" };
}
#endif
}
#if UNITY_VFX_GRAPH
private static VisualEffect FindVisualEffect(JObject @params)
{
GameObject go = FindTargetGameObject(@params);
return go?.GetComponent();
}
///
/// Creates a new VFX Graph asset file from a template
///
private static object VFXCreateAsset(JObject @params)
{
string assetName = @params["assetName"]?.ToString();
string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX";
string template = @params["template"]?.ToString() ?? "empty";
if (string.IsNullOrEmpty(assetName))
return new { success = false, message = "assetName is required" };
// Ensure folder exists
if (!AssetDatabase.IsValidFolder(folderPath))
{
string[] folders = folderPath.Split('/');
string currentPath = folders[0];
for (int i = 1; i < folders.Length; i++)
{
string newPath = currentPath + "/" + folders[i];
if (!AssetDatabase.IsValidFolder(newPath))
{
AssetDatabase.CreateFolder(currentPath, folders[i]);
}
currentPath = newPath;
}
}
string assetPath = $"{folderPath}/{assetName}.vfx";
// Check if asset already exists
if (AssetDatabase.LoadAssetAtPath(assetPath) != null)
{
bool overwrite = @params["overwrite"]?.ToObject() ?? false;
if (!overwrite)
return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." };
AssetDatabase.DeleteAsset(assetPath);
}
// Find and copy template
string templatePath = FindVFXTemplate(template);
UnityEngine.VFX.VisualEffectAsset newAsset = null;
if (!string.IsNullOrEmpty(templatePath) && System.IO.File.Exists(templatePath))
{
// templatePath is a full filesystem path, need to copy file directly
// Get the full destination path
string projectRoot = System.IO.Path.GetDirectoryName(Application.dataPath);
string fullDestPath = System.IO.Path.Combine(projectRoot, assetPath);
// Ensure directory exists
string destDir = System.IO.Path.GetDirectoryName(fullDestPath);
if (!System.IO.Directory.Exists(destDir))
System.IO.Directory.CreateDirectory(destDir);
// Copy the file
System.IO.File.Copy(templatePath, fullDestPath, true);
AssetDatabase.Refresh();
newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
}
else
{
// Create empty VFX asset using reflection to access internal API
// Note: Develop in Progress, TODO:// Find authenticated way to create VFX asset
try
{
// Try to use VisualEffectAssetEditorUtility.CreateNewAsset if available
var utilityType = System.Type.GetType("UnityEditor.VFX.VisualEffectAssetEditorUtility, Unity.VisualEffectGraph.Editor");
if (utilityType != null)
{
var createMethod = utilityType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static);
if (createMethod != null)
{
createMethod.Invoke(null, new object[] { assetPath });
AssetDatabase.Refresh();
newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
}
}
// Fallback: Create a ScriptableObject-based asset
if (newAsset == null)
{
// Try direct creation via internal constructor
var resourceType = System.Type.GetType("UnityEditor.VFX.VisualEffectResource, Unity.VisualEffectGraph.Editor");
if (resourceType != null)
{
var createMethod = resourceType.GetMethod("CreateNewAsset", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
if (createMethod != null)
{
var resource = createMethod.Invoke(null, new object[] { assetPath });
AssetDatabase.Refresh();
newAsset = AssetDatabase.LoadAssetAtPath(assetPath);
}
}
}
}
catch (Exception ex)
{
return new { success = false, message = $"Failed to create VFX asset: {ex.Message}" };
}
}
if (newAsset == null)
{
return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." };
}
return new
{
success = true,
message = $"Created VFX asset: {assetPath}",
data = new
{
assetPath = assetPath,
assetName = newAsset.name,
template = template
}
};
}
///
/// Finds VFX template path by name
///
private static string FindVFXTemplate(string templateName)
{
// Get the actual filesystem path for the VFX Graph package using PackageManager API
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph");
var searchPaths = new List();
if (packageInfo != null)
{
// Use the resolved path from PackageManager (handles Library/PackageCache paths)
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates"));
searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples"));
}
// Also search project-local paths
searchPaths.Add("Assets/VFX/Templates");
string[] templatePatterns = new[]
{
$"{templateName}.vfx",
$"VFX{templateName}.vfx",
$"Simple{templateName}.vfx",
$"{templateName}VFX.vfx"
};
foreach (string basePath in searchPaths)
{
if (!System.IO.Directory.Exists(basePath)) continue;
foreach (string pattern in templatePatterns)
{
string[] files = System.IO.Directory.GetFiles(basePath, pattern, System.IO.SearchOption.AllDirectories);
if (files.Length > 0)
return files[0];
}
// Also search by partial match
try
{
string[] allVfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories);
foreach (string file in allVfxFiles)
{
if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower()))
return file;
}
}
catch { }
}
// Search in project assets
string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName);
if (guids.Length > 0)
{
return AssetDatabase.GUIDToAssetPath(guids[0]);
}
return null;
}
///
/// Assigns a VFX asset to a VisualEffect component
///
private static object VFXAssignAsset(JObject @params)
{
VisualEffect vfx = FindVisualEffect(@params);
if (vfx == null) return new { success = false, message = "VisualEffect component not found" };
string assetPath = @params["assetPath"]?.ToString();
if (string.IsNullOrEmpty(assetPath))
return new { success = false, message = "assetPath is required" };
// Normalize path
if (!assetPath.StartsWith("Assets/") && !assetPath.StartsWith("Packages/"))
assetPath = "Assets/" + assetPath;
if (!assetPath.EndsWith(".vfx"))
assetPath += ".vfx";
var asset = AssetDatabase.LoadAssetAtPath(assetPath);
if (asset == null)
{
// Try searching by name
string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath);
string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}");
if (guids.Length > 0)
{
assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
asset = AssetDatabase.LoadAssetAtPath(assetPath);
}
}
if (asset == null)
return new { success = false, message = $"VFX asset not found: {assetPath}" };
Undo.RecordObject(vfx, "Assign VFX Asset");
vfx.visualEffectAsset = asset;
EditorUtility.SetDirty(vfx);
return new
{
success = true,
message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}",
data = new
{
gameObject = vfx.gameObject.name,
assetName = asset.name,
assetPath = assetPath
}
};
}
///
/// Lists available VFX templates
///
private static object VFXListTemplates(JObject @params)
{
var templates = new List