ManageGameObject/Material + auto-select sole Unity instance (#502)
- ManageGameObject: support componentsToAdd string array + apply top-level componentProperties - ManageMaterial: safer create + optional color input with configurable property - Server: auto-select sole Unity instance middleware + integration testsmain
parent
35a5c75596
commit
9b153b6561
|
|
@ -1450,7 +1450,11 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
var compToken = componentsToAddArray.First;
|
||||
if (compToken.Type == JTokenType.String)
|
||||
{
|
||||
typeName = compToken.ToString();
|
||||
// Check for properties in top-level componentProperties parameter
|
||||
properties = @params["componentProperties"]?[typeName] as JObject;
|
||||
}
|
||||
else if (compToken is JObject compObj)
|
||||
{
|
||||
typeName = compObj["typeName"]?.ToString();
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
string materialPath = NormalizePath(@params["materialPath"]?.ToString());
|
||||
string shaderName = @params["shader"]?.ToString() ?? "Standard";
|
||||
JToken colorToken = @params["color"];
|
||||
string colorProperty = @params["property"]?.ToString();
|
||||
|
||||
JObject properties = null;
|
||||
JToken propsToken = @params["properties"];
|
||||
|
|
@ -494,25 +496,104 @@ namespace MCPForUnity.Editor.Tools
|
|||
return new { status = "error", message = $"Could not find shader: {shaderName}" };
|
||||
}
|
||||
|
||||
Material material = new Material(shader);
|
||||
|
||||
// Check for existing asset to avoid silent overwrite
|
||||
if (AssetDatabase.LoadAssetAtPath<Material>(materialPath) != null)
|
||||
{
|
||||
return new { status = "error", message = $"Material already exists at {materialPath}" };
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(material, materialPath);
|
||||
|
||||
if (properties != null)
|
||||
Material material = null;
|
||||
var shouldDestroyMaterial = true;
|
||||
try
|
||||
{
|
||||
MaterialOps.ApplyProperties(material, properties, ManageGameObject.InputSerializer);
|
||||
material = new Material(shader);
|
||||
|
||||
// Apply color param during creation (keeps Python tool signature and C# implementation consistent).
|
||||
// If "properties" already contains a color property, let properties win.
|
||||
bool shouldApplyColor = false;
|
||||
if (colorToken != null)
|
||||
{
|
||||
if (properties == null)
|
||||
{
|
||||
shouldApplyColor = true;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(colorProperty))
|
||||
{
|
||||
// If colorProperty is specified, only check that specific property.
|
||||
shouldApplyColor = !properties.ContainsKey(colorProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If colorProperty is not specified, check fallback properties.
|
||||
shouldApplyColor = !properties.ContainsKey("_BaseColor") && !properties.ContainsKey("_Color");
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldApplyColor)
|
||||
{
|
||||
Color color;
|
||||
try
|
||||
{
|
||||
color = MaterialOps.ParseColor(colorToken, ManageGameObject.InputSerializer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new { status = "error", message = $"Invalid color format: {e.Message}" };
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(colorProperty))
|
||||
{
|
||||
if (material.HasProperty(colorProperty))
|
||||
{
|
||||
material.SetColor(colorProperty, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new
|
||||
{
|
||||
status = "error",
|
||||
message = $"Specified color property '{colorProperty}' does not exist on this material."
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (material.HasProperty("_BaseColor"))
|
||||
{
|
||||
material.SetColor("_BaseColor", color);
|
||||
}
|
||||
else if (material.HasProperty("_Color"))
|
||||
{
|
||||
material.SetColor("_Color", color);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new
|
||||
{
|
||||
status = "error",
|
||||
message = "Could not find suitable color property (_BaseColor or _Color) on this material's shader."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.CreateAsset(material, materialPath);
|
||||
shouldDestroyMaterial = false; // material is now owned by the AssetDatabase
|
||||
|
||||
if (properties != null)
|
||||
{
|
||||
MaterialOps.ApplyProperties(material, properties, ManageGameObject.InputSerializer);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(material);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new { status = "success", message = $"Created material at {materialPath} with shader {shaderName}" };
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shouldDestroyMaterial && material != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(material);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(material);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new { status = "success", message = $"Created material at {materialPath} with shader {shaderName}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,11 +83,101 @@ class UnityInstanceMiddleware(Middleware):
|
|||
with self._lock:
|
||||
self._active_by_key.pop(key, None)
|
||||
|
||||
async def _maybe_autoselect_instance(self, ctx) -> str | None:
|
||||
"""
|
||||
Auto-select the sole Unity instance when no active instance is set.
|
||||
|
||||
Note: This method both *discovers* and *persists* the selection via
|
||||
`set_active_instance` as a side-effect, since callers expect the selection
|
||||
to stick for subsequent tool/resource calls in the same session.
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid circular dependencies / optional transport modules.
|
||||
from transport.unity_transport import _current_transport
|
||||
|
||||
transport = _current_transport()
|
||||
if PluginHub.is_configured():
|
||||
try:
|
||||
sessions_data = await PluginHub.get_sessions()
|
||||
sessions = sessions_data.sessions or {}
|
||||
ids: list[str] = []
|
||||
for session_info in sessions.values():
|
||||
project = getattr(session_info, "project", None) or "Unknown"
|
||||
hash_value = getattr(session_info, "hash", None)
|
||||
if hash_value:
|
||||
ids.append(f"{project}@{hash_value}")
|
||||
if len(ids) == 1:
|
||||
chosen = ids[0]
|
||||
self.set_active_instance(ctx, chosen)
|
||||
logger.info(
|
||||
"Auto-selected sole Unity instance via PluginHub: %s",
|
||||
chosen,
|
||||
)
|
||||
return chosen
|
||||
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
||||
logger.debug(
|
||||
"PluginHub auto-select probe failed (%s); falling back to stdio",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
||||
raise
|
||||
logger.debug(
|
||||
"PluginHub auto-select probe failed with unexpected error (%s); falling back to stdio",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if transport != "http":
|
||||
try:
|
||||
# Import here to avoid circular imports in legacy transport paths.
|
||||
from transport.legacy.unity_connection import get_unity_connection_pool
|
||||
|
||||
pool = get_unity_connection_pool()
|
||||
instances = pool.discover_all_instances(force_refresh=True)
|
||||
ids = [getattr(inst, "id", None) for inst in instances]
|
||||
ids = [inst_id for inst_id in ids if inst_id]
|
||||
if len(ids) == 1:
|
||||
chosen = ids[0]
|
||||
self.set_active_instance(ctx, chosen)
|
||||
logger.info(
|
||||
"Auto-selected sole Unity instance via stdio discovery: %s",
|
||||
chosen,
|
||||
)
|
||||
return chosen
|
||||
except (ConnectionError, ValueError, KeyError, TimeoutError, AttributeError) as exc:
|
||||
logger.debug(
|
||||
"Stdio auto-select probe failed (%s)",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
||||
raise
|
||||
logger.debug(
|
||||
"Stdio auto-select probe failed with unexpected error (%s)",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
|
||||
raise
|
||||
logger.debug(
|
||||
"Auto-select path encountered an unexpected error (%s)",
|
||||
type(exc).__name__,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def _inject_unity_instance(self, context: MiddlewareContext) -> None:
|
||||
"""Inject active Unity instance into context if available."""
|
||||
ctx = context.fastmcp_context
|
||||
|
||||
active_instance = self.get_active_instance(ctx)
|
||||
if not active_instance:
|
||||
active_instance = await self._maybe_autoselect_instance(ctx)
|
||||
if active_instance:
|
||||
# If using HTTP transport (PluginHub configured), validate session
|
||||
# But for stdio transport (no PluginHub needed or maybe partially configured),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from pathlib import Path
|
|||
SERVER_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(SERVER_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(SERVER_ROOT))
|
||||
SERVER_SRC = SERVER_ROOT / "src"
|
||||
if str(SERVER_SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SERVER_SRC))
|
||||
|
||||
# Ensure telemetry is disabled during test collection and execution to avoid
|
||||
# any background network or thread startup that could slow or block pytest.
|
||||
|
|
@ -86,3 +89,39 @@ fastmcp.server = fastmcp_server
|
|||
fastmcp_server.middleware = fastmcp_server_middleware
|
||||
sys.modules.setdefault("fastmcp.server", fastmcp_server)
|
||||
sys.modules.setdefault("fastmcp.server.middleware", fastmcp_server_middleware)
|
||||
|
||||
# Stub minimal starlette modules to avoid optional dependency imports.
|
||||
starlette = types.ModuleType("starlette")
|
||||
starlette_endpoints = types.ModuleType("starlette.endpoints")
|
||||
starlette_websockets = types.ModuleType("starlette.websockets")
|
||||
starlette_requests = types.ModuleType("starlette.requests")
|
||||
starlette_responses = types.ModuleType("starlette.responses")
|
||||
|
||||
|
||||
class _DummyWebSocketEndpoint:
|
||||
pass
|
||||
|
||||
|
||||
class _DummyWebSocket:
|
||||
pass
|
||||
|
||||
|
||||
class _DummyRequest:
|
||||
pass
|
||||
|
||||
|
||||
class _DummyJSONResponse:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
starlette_endpoints.WebSocketEndpoint = _DummyWebSocketEndpoint
|
||||
starlette_websockets.WebSocket = _DummyWebSocket
|
||||
starlette_requests.Request = _DummyRequest
|
||||
starlette_responses.JSONResponse = _DummyJSONResponse
|
||||
|
||||
sys.modules.setdefault("starlette", starlette)
|
||||
sys.modules.setdefault("starlette.endpoints", starlette_endpoints)
|
||||
sys.modules.setdefault("starlette.websockets", starlette_websockets)
|
||||
sys.modules.setdefault("starlette.requests", starlette_requests)
|
||||
sys.modules.setdefault("starlette.responses", starlette_responses)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
import asyncio
|
||||
import sys
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
|
||||
from .test_helpers import DummyContext
|
||||
|
||||
|
||||
class DummyMiddlewareContext:
|
||||
def __init__(self, ctx):
|
||||
self.fastmcp_context = ctx
|
||||
|
||||
|
||||
def test_auto_selects_single_instance_via_pluginhub(monkeypatch):
|
||||
plugin_hub = types.ModuleType("transport.plugin_hub")
|
||||
|
||||
class PluginHub:
|
||||
@classmethod
|
||||
def is_configured(cls) -> bool:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def get_sessions(cls):
|
||||
raise AssertionError("get_sessions should be stubbed in test")
|
||||
|
||||
plugin_hub.PluginHub = PluginHub
|
||||
monkeypatch.setitem(sys.modules, "transport.plugin_hub", plugin_hub)
|
||||
unity_transport = types.ModuleType("transport.unity_transport")
|
||||
unity_transport._current_transport = lambda: "http"
|
||||
monkeypatch.setitem(sys.modules, "transport.unity_transport", unity_transport)
|
||||
monkeypatch.delitem(sys.modules, "transport.unity_instance_middleware", raising=False)
|
||||
|
||||
from transport.unity_instance_middleware import UnityInstanceMiddleware, PluginHub as ImportedPluginHub
|
||||
assert ImportedPluginHub is plugin_hub.PluginHub
|
||||
|
||||
monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http")
|
||||
|
||||
middleware = UnityInstanceMiddleware()
|
||||
ctx = DummyContext()
|
||||
ctx.client_id = "client-1"
|
||||
middleware_context = DummyMiddlewareContext(ctx)
|
||||
|
||||
call_count = {"sessions": 0}
|
||||
|
||||
async def fake_get_sessions():
|
||||
call_count["sessions"] += 1
|
||||
return SimpleNamespace(
|
||||
sessions={
|
||||
"session-1": SimpleNamespace(project="Ramble", hash="deadbeef"),
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(plugin_hub.PluginHub, "get_sessions", fake_get_sessions)
|
||||
|
||||
selected = asyncio.run(middleware._maybe_autoselect_instance(ctx))
|
||||
|
||||
assert selected == "Ramble@deadbeef"
|
||||
assert middleware.get_active_instance(ctx) == "Ramble@deadbeef"
|
||||
assert call_count["sessions"] == 1
|
||||
|
||||
asyncio.run(middleware._inject_unity_instance(middleware_context))
|
||||
|
||||
assert ctx.get_state("unity_instance") == "Ramble@deadbeef"
|
||||
assert call_count["sessions"] == 1
|
||||
|
||||
|
||||
def test_auto_selects_single_instance_via_stdio(monkeypatch):
|
||||
plugin_hub = types.ModuleType("transport.plugin_hub")
|
||||
|
||||
class PluginHub:
|
||||
@classmethod
|
||||
def is_configured(cls) -> bool:
|
||||
return False
|
||||
|
||||
plugin_hub.PluginHub = PluginHub
|
||||
monkeypatch.setitem(sys.modules, "transport.plugin_hub", plugin_hub)
|
||||
unity_transport = types.ModuleType("transport.unity_transport")
|
||||
unity_transport._current_transport = lambda: "stdio"
|
||||
monkeypatch.setitem(sys.modules, "transport.unity_transport", unity_transport)
|
||||
monkeypatch.delitem(sys.modules, "transport.unity_instance_middleware", raising=False)
|
||||
|
||||
from transport.unity_instance_middleware import UnityInstanceMiddleware, PluginHub as ImportedPluginHub
|
||||
assert ImportedPluginHub is plugin_hub.PluginHub
|
||||
|
||||
monkeypatch.setenv("UNITY_MCP_TRANSPORT", "stdio")
|
||||
|
||||
middleware = UnityInstanceMiddleware()
|
||||
ctx = DummyContext()
|
||||
ctx.client_id = "client-1"
|
||||
middleware_context = DummyMiddlewareContext(ctx)
|
||||
|
||||
class PoolStub:
|
||||
def discover_all_instances(self, force_refresh=False):
|
||||
assert force_refresh is True
|
||||
return [SimpleNamespace(id="UnityMCPTests@cc8756d4")]
|
||||
|
||||
unity_connection = types.ModuleType("transport.legacy.unity_connection")
|
||||
unity_connection.get_unity_connection_pool = lambda: PoolStub()
|
||||
monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection)
|
||||
|
||||
selected = asyncio.run(middleware._maybe_autoselect_instance(ctx))
|
||||
|
||||
assert selected == "UnityMCPTests@cc8756d4"
|
||||
assert middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4"
|
||||
|
||||
asyncio.run(middleware._inject_unity_instance(middleware_context))
|
||||
|
||||
assert ctx.get_state("unity_instance") == "UnityMCPTests@cc8756d4"
|
||||
|
||||
|
||||
def test_auto_select_handles_stdio_errors(monkeypatch):
|
||||
plugin_hub = types.ModuleType("transport.plugin_hub")
|
||||
|
||||
class PluginHub:
|
||||
@classmethod
|
||||
def is_configured(cls) -> bool:
|
||||
return False
|
||||
|
||||
plugin_hub.PluginHub = PluginHub
|
||||
monkeypatch.setitem(sys.modules, "transport.plugin_hub", plugin_hub)
|
||||
unity_transport = types.ModuleType("transport.unity_transport")
|
||||
unity_transport._current_transport = lambda: "stdio"
|
||||
monkeypatch.setitem(sys.modules, "transport.unity_transport", unity_transport)
|
||||
monkeypatch.delitem(sys.modules, "transport.unity_instance_middleware", raising=False)
|
||||
|
||||
from transport.unity_instance_middleware import UnityInstanceMiddleware, PluginHub as ImportedPluginHub
|
||||
assert ImportedPluginHub is plugin_hub.PluginHub
|
||||
|
||||
middleware = UnityInstanceMiddleware()
|
||||
ctx = DummyContext()
|
||||
ctx.client_id = "client-1"
|
||||
|
||||
class PoolStub:
|
||||
def discover_all_instances(self, force_refresh=False):
|
||||
raise ConnectionError("stdio unavailable")
|
||||
|
||||
unity_connection = types.ModuleType("transport.legacy.unity_connection")
|
||||
unity_connection.get_unity_connection_pool = lambda: PoolStub()
|
||||
monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection)
|
||||
|
||||
selected = asyncio.run(middleware._maybe_autoselect_instance(ctx))
|
||||
|
||||
assert selected is None
|
||||
assert middleware.get_active_instance(ctx) is None
|
||||
|
|
@ -630,5 +630,137 @@ namespace MCPForUnityTests.Editor.Tools
|
|||
UnityEngine.Object.DestroyImmediate(material2);
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddComponent_StringArrayFormat_AppliesComponentProperties()
|
||||
{
|
||||
// Arrange - Create a GameObject to add component to
|
||||
var testObject = new GameObject("AddComponentTestObject");
|
||||
|
||||
// Create params using string array format with top-level componentProperties
|
||||
var addComponentParams = new JObject
|
||||
{
|
||||
["action"] = "add_component",
|
||||
["target"] = testObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentsToAdd"] = new JArray { "Rigidbody" },
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 7.5f,
|
||||
["useGravity"] = false,
|
||||
["drag"] = 2.0f
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(addComponentParams);
|
||||
|
||||
// Assert - Verify component was added
|
||||
var rigidbody = testObject.GetComponent<Rigidbody>();
|
||||
Assert.IsNotNull(rigidbody, "Rigidbody component should be added to GameObject");
|
||||
|
||||
// Verify properties were set correctly during component creation
|
||||
Assert.AreEqual(7.5f, rigidbody.mass, 0.001f,
|
||||
"Mass should be set to 7.5 via componentProperties during add_component");
|
||||
Assert.AreEqual(false, rigidbody.useGravity,
|
||||
"UseGravity should be set to false via componentProperties during add_component");
|
||||
Assert.AreEqual(2.0f, rigidbody.drag, 0.001f,
|
||||
"Drag should be set to 2.0 via componentProperties during add_component");
|
||||
|
||||
// Verify result indicates success
|
||||
Assert.IsNotNull(result, "Should return a result object");
|
||||
var resultObj = result as JObject ?? JObject.FromObject(result);
|
||||
Assert.IsTrue(resultObj.Value<bool>("success"),
|
||||
"Result should indicate success when adding component with properties");
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddComponent_ObjectFormat_StillAppliesComponentProperties()
|
||||
{
|
||||
// Arrange - Create a GameObject to add component to
|
||||
var testObject = new GameObject("AddComponentObjectFormatTestObject");
|
||||
|
||||
// Create params using object array format (existing behavior)
|
||||
var addComponentParams = new JObject
|
||||
{
|
||||
["action"] = "add_component",
|
||||
["target"] = testObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentsToAdd"] = new JArray
|
||||
{
|
||||
new JObject
|
||||
{
|
||||
["typeName"] = "Rigidbody",
|
||||
["properties"] = new JObject
|
||||
{
|
||||
["mass"] = 3.5f,
|
||||
["useGravity"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(addComponentParams);
|
||||
|
||||
// Assert - Verify component was added
|
||||
var rigidbody = testObject.GetComponent<Rigidbody>();
|
||||
Assert.IsNotNull(rigidbody, "Rigidbody component should be added to GameObject");
|
||||
|
||||
// Verify properties were set correctly
|
||||
Assert.AreEqual(3.5f, rigidbody.mass, 0.001f,
|
||||
"Mass should be set to 3.5 via inline properties");
|
||||
Assert.AreEqual(true, rigidbody.useGravity,
|
||||
"UseGravity should be set to true via inline properties");
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddComponent_ComponentNameFormat_AppliesComponentProperties()
|
||||
{
|
||||
// Arrange - Create a GameObject to add component to
|
||||
var testObject = new GameObject("AddComponentNameFormatTestObject");
|
||||
|
||||
// Create params using componentName format (existing behavior)
|
||||
var addComponentParams = new JObject
|
||||
{
|
||||
["action"] = "add_component",
|
||||
["target"] = testObject.name,
|
||||
["search_method"] = "by_name",
|
||||
["componentName"] = "Rigidbody",
|
||||
["componentProperties"] = new JObject
|
||||
{
|
||||
["Rigidbody"] = new JObject
|
||||
{
|
||||
["mass"] = 5.0f,
|
||||
["drag"] = 1.5f
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ManageGameObject.HandleCommand(addComponentParams);
|
||||
|
||||
// Assert - Verify component was added
|
||||
var rigidbody = testObject.GetComponent<Rigidbody>();
|
||||
Assert.IsNotNull(rigidbody, "Rigidbody component should be added to GameObject");
|
||||
|
||||
// Verify properties were set correctly
|
||||
Assert.AreEqual(5.0f, rigidbody.mass, 0.001f,
|
||||
"Mass should be set to 5.0 via componentName format");
|
||||
Assert.AreEqual(1.5f, rigidbody.drag, 0.001f,
|
||||
"Drag should be set to 1.5 via componentName format");
|
||||
|
||||
// Clean up
|
||||
UnityEngine.Object.DestroyImmediate(testObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue