Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:19:25 +08:00
commit ffe519c2c6
23 changed files with 3791 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
---
name: unity-compile-fixer
description: Detect and resolve Unity C# compilation errors using VSCode diagnostics. Use this skill when Unity projects have compilation errors that need diagnosis and automated fixes. Analyzes errors from VSCode Language Server, proposes solutions based on error patterns, and handles version control conflicts for Unity projects.
---
# Unity Compile Fixer
## Overview
This skill enables automatic detection and resolution of Unity C# compilation errors by leveraging VSCode's diagnostic system. It collects real-time errors from the OmniSharp C# language server, analyzes error patterns against a curated database of common Unity issues, and proposes context-aware solutions for user approval before applying fixes.
## When to Use This Skill
Use this skill when:
- Unity projects report C# compilation errors in VSCode
- Need to diagnose the root cause of Unity compiler errors (CS* error codes)
- Want automated fix suggestions for common Unity scripting issues
- Working with Unity projects that have version control integration (Git, Unity Collaborate, Plastic SCM)
- Need to handle Unity .meta file conflicts
**Example user requests:**
- "Check Unity compilation errors and help me fix them"
- "My Unity project has compiler errors, can you diagnose and fix?"
- "Unity scripts are not compiling, what's wrong?"
- "Fix the C# errors in my Unity project"
## Workflow
Follow this workflow when the skill is invoked:
### 1. Detect Compilation Errors
Use the `mcp__ide__getDiagnostics` tool to collect errors from VSCode:
```typescript
// Collect all project diagnostics
mcp__ide__getDiagnostics()
// Or target specific Unity script files
mcp__ide__getDiagnostics({ uri: "file:///path/to/PlayerController.cs" })
```
Filter the diagnostics to focus on Unity-relevant errors:
- **Severity**: Only process errors with `severity: "Error"` (ignore warnings)
- **Source**: Only process `source: "csharp"` (OmniSharp C# diagnostics)
- **Error Codes**: Focus on CS* compiler error codes (e.g., CS0246, CS0029, CS1061)
### 2. Analyze Error Patterns
For each detected error:
1. **Extract error information:**
- File path and line number from `uri` and `range`
- Error code from `message` (e.g., "CS0246")
- Full error message text
2. **Match against error pattern database:**
- Load `references/error-patterns.json`
- Find the error code entry (e.g., CS0246)
- Retrieve common causes and solutions
3. **Read affected file context:**
- Use Read tool to load the file with errors
- Examine surrounding code for context
- Identify missing imports, incorrect types, or API misuse
### 3. Generate Solution Proposals
For each error, create a structured fix proposal:
```markdown
**Error**: CS0246 at PlayerController.cs:45
**Message**: The type or namespace name 'Rigidbody' could not be found
**Analysis**:
- Missing using directive for UnityEngine namespace
- Common Unity API usage pattern
**Proposed Solution**:
Add `using UnityEngine;` at the top of PlayerController.cs
**Changes Required**:
- File: Assets/Scripts/PlayerController.cs
- Action: Insert using directive at line 1
```
### 4. User Confirmation
Before applying any fixes:
1. **Present all proposed solutions** in a clear, structured format
2. **Use AskUserQuestion tool** to get user approval:
- List each error and proposed fix
- Allow user to approve all, select specific fixes, or cancel
3. **Wait for explicit confirmation** - do not apply fixes automatically
### 5. Apply Approved Fixes
For each approved fix:
1. **Use Edit tool** to modify the affected file
2. **Preserve code formatting** and existing structure
3. **Apply minimal changes** - only fix the specific error
Example:
```typescript
Edit({
file_path: "Assets/Scripts/PlayerController.cs",
old_string: "public class PlayerController : MonoBehaviour",
new_string: "using UnityEngine;\n\npublic class PlayerController : MonoBehaviour"
})
```
### 6. Verify Version Control Status
After applying fixes:
1. **Check for .meta file conflicts:**
- Use Grep to search for Unity .meta files
- Verify that script GUID hasn't changed
- Check for merge conflict markers (<<<<<<, ======, >>>>>>)
2. **Report VCS status:**
- List modified files
- Warn about any .meta file issues
- Suggest git operations if needed
### 7. Re-validate Compilation
After fixes are applied:
1. **Re-run diagnostics** using `mcp__ide__getDiagnostics()`
2. **Compare error count** before and after
3. **Report results** to the user:
- Number of errors fixed
- Remaining errors (if any)
- Success rate
## Error Pattern Database
The skill relies on `references/error-patterns.json` for error analysis. This database contains:
- **Error Code**: CS* compiler error code
- **Description**: Human-readable explanation
- **Common Causes**: Typical reasons this error occurs in Unity
- **Solutions**: Step-by-step fix instructions
- **Unity-Specific Notes**: Unity API considerations
To analyze an error, load the database and match the error code:
```typescript
Read({ file_path: "references/error-patterns.json" })
// Parse JSON and find errorCode entry
```
## Analysis Scripts
The skill includes Node.js scripts in `scripts/` for complex error analysis:
### scripts/analyze-diagnostics.js
Processes VSCode diagnostics JSON output and extracts Unity-relevant errors.
**Usage:**
```bash
node scripts/analyze-diagnostics.js <diagnostics-json-file>
```
**Output:**
- Filtered list of Unity C# compilation errors
- Error classification by type (missing imports, type errors, API issues)
- Severity and file location information
This script can be run independently or invoked from the SKILL.md workflow when detailed analysis is needed.
## Best Practices
When using this skill:
1. **Start with full project diagnostics** - use `mcp__ide__getDiagnostics()` without parameters to get complete error picture
2. **Prioritize errors by severity and dependency** - fix foundational errors (missing imports) before downstream errors
3. **Batch related fixes** - group errors from the same file for efficient editing
4. **Always verify VCS status** - Unity .meta files are critical for version control
5. **Re-validate after fixes** - ensure errors are actually resolved
## Resources
### scripts/analyze-diagnostics.js
Node.js script for processing VSCode diagnostics and filtering Unity-specific C# errors.
### references/error-patterns.json
Curated database of common Unity C# compilation errors with solutions and Unity-specific guidance.

View File

@@ -0,0 +1,368 @@
{
"CS0246": {
"errorCode": "CS0246",
"description": "The type or namespace name could not be found",
"commonCauses": [
"Missing using directive (e.g., using UnityEngine;)",
"Assembly definition reference not added",
"Typo in type name",
"Missing package or plugin"
],
"solutions": [
{
"step": 1,
"action": "Add missing using directive",
"example": "using UnityEngine;\nusing UnityEngine.UI;\nusing TMPro;"
},
{
"step": 2,
"action": "Check assembly definition references",
"details": "If using Assembly Definition files (.asmdef), ensure required assemblies are referenced"
},
{
"step": 3,
"action": "Verify type name spelling",
"details": "Check for typos in class, struct, or namespace names"
},
{
"step": 4,
"action": "Install missing packages",
"details": "Open Package Manager and install required Unity packages (TextMeshPro, Input System, etc.)"
}
],
"unityNotes": "Common Unity namespaces: UnityEngine, UnityEngine.UI, UnityEngine.SceneManagement, UnityEditor (editor-only)",
"examples": [
{
"error": "The type or namespace name 'Rigidbody' could not be found",
"fix": "Add 'using UnityEngine;' at the top of the file"
},
{
"error": "The type or namespace name 'TextMeshProUGUI' could not be found",
"fix": "Add 'using TMPro;' and ensure TextMeshPro package is installed"
}
]
},
"CS0029": {
"errorCode": "CS0029",
"description": "Cannot implicitly convert type",
"commonCauses": [
"Assigning incompatible types",
"Missing type cast",
"Incorrect GameObject/Component access",
"Unity API version changes"
],
"solutions": [
{
"step": 1,
"action": "Add explicit cast",
"example": "Rigidbody rb = (Rigidbody)GetComponent(typeof(Rigidbody));"
},
{
"step": 2,
"action": "Use generic GetComponent",
"example": "Rigidbody rb = GetComponent<Rigidbody>();"
},
{
"step": 3,
"action": "Check variable type declaration",
"details": "Ensure variable type matches assigned value type"
}
],
"unityNotes": "Unity uses generic methods extensively. Prefer GetComponent<T>() over non-generic versions",
"examples": [
{
"error": "Cannot implicitly convert type 'UnityEngine.Component' to 'UnityEngine.Rigidbody'",
"fix": "Use GetComponent<Rigidbody>() instead of GetComponent(typeof(Rigidbody))"
}
]
},
"CS1061": {
"errorCode": "CS1061",
"description": "Type does not contain a definition for member",
"commonCauses": [
"Typo in method or property name",
"Unity API version difference",
"Accessing wrong object type",
"Missing component reference"
],
"solutions": [
{
"step": 1,
"action": "Check spelling and capitalization",
"details": "C# is case-sensitive. Ensure method/property name is correct"
},
{
"step": 2,
"action": "Verify Unity API version",
"details": "Some APIs change between Unity versions. Check Unity documentation"
},
{
"step": 3,
"action": "Ensure correct component type",
"example": "transform.position (correct) vs gameObject.position (incorrect)"
},
{
"step": 4,
"action": "Use correct Unity API",
"details": "GameObject vs Component vs Transform have different members"
}
],
"unityNotes": "Common confusion: GameObject.transform.position (correct) vs GameObject.position (incorrect)",
"examples": [
{
"error": "'GameObject' does not contain a definition for 'position'",
"fix": "Use 'transform.position' or 'gameObject.transform.position'"
},
{
"error": "'Rigidbody' does not contain a definition for 'Translate'",
"fix": "Use 'transform.Translate()' instead - Translate is a Transform method, not Rigidbody"
}
]
},
"CS0101": {
"errorCode": "CS0101",
"description": "The namespace already contains a definition",
"commonCauses": [
"Duplicate class names in same namespace",
"Partial class definitions conflict",
"Multiple files with same class name",
"Copy-pasted scripts not renamed"
],
"solutions": [
{
"step": 1,
"action": "Rename one of the conflicting classes",
"details": "Each class in a namespace must have a unique name"
},
{
"step": 2,
"action": "Move class to different namespace",
"example": "namespace MyGame.Player { class Controller { } }"
},
{
"step": 3,
"action": "Check for duplicate files",
"details": "Search project for files with same class name"
}
],
"unityNotes": "Unity script file names should match class names. Rename both file and class together",
"examples": [
{
"error": "The namespace 'MyGame' already contains a definition for 'PlayerController'",
"fix": "Rename one class to 'PlayerMovement' or use different namespaces"
}
]
},
"CS1002": {
"errorCode": "CS1002",
"description": "Expected semicolon",
"commonCauses": [
"Missing semicolon at end of statement",
"Incomplete method call",
"Syntax error in expression"
],
"solutions": [
{
"step": 1,
"action": "Add missing semicolon",
"example": "int health = 100; // semicolon required"
},
{
"step": 2,
"action": "Check line above error",
"details": "Error often reported on line after the actual missing semicolon"
}
],
"unityNotes": "Check Unity lifecycle methods (Start, Update) for missing semicolons",
"examples": [
{
"error": "Expected ';' after 'transform.position = newPos'",
"fix": "Add semicolon: transform.position = newPos;"
}
]
},
"CS1003": {
"errorCode": "CS1003",
"description": "Syntax error, expected comma",
"commonCauses": [
"Missing comma in parameter list",
"Missing comma in array initialization",
"Incorrect method signature"
],
"solutions": [
{
"step": 1,
"action": "Add missing comma",
"example": "Vector3 pos = new Vector3(1.0f, 2.0f, 3.0f);"
},
{
"step": 2,
"action": "Check method parameters",
"details": "Ensure parameters are separated by commas"
}
],
"unityNotes": "Unity methods like Vector3, Quaternion require commas between components",
"examples": [
{
"error": "Expected ',' in Vector3 constructor",
"fix": "new Vector3(1.0f, 2.0f, 3.0f) // commas required"
}
]
},
"CS1525": {
"errorCode": "CS1525",
"description": "Invalid expression term",
"commonCauses": [
"Keyword used in wrong context",
"Incomplete expression",
"Missing opening brace"
],
"solutions": [
{
"step": 1,
"action": "Check for matching braces",
"details": "Ensure all { } are properly matched"
},
{
"step": 2,
"action": "Verify expression syntax",
"details": "Check for incomplete or malformed expressions"
}
],
"unityNotes": "Common in Unity lifecycle methods - ensure proper { } around method bodies",
"examples": [
{
"error": "Invalid expression term 'void'",
"fix": "Check for missing brace before method definition"
}
]
},
"CS0122": {
"errorCode": "CS0122",
"description": "Member is inaccessible due to its protection level",
"commonCauses": [
"Accessing private member from outside class",
"Missing public/protected modifier",
"Incorrect inheritance setup"
],
"solutions": [
{
"step": 1,
"action": "Change member to public",
"example": "public float speed = 5.0f;"
},
{
"step": 2,
"action": "Use SerializeField for private fields",
"example": "[SerializeField] private float speed;"
},
{
"step": 3,
"action": "Add public property or method",
"example": "public float Speed { get { return speed; } }"
}
],
"unityNotes": "Use [SerializeField] for private fields that should appear in Inspector",
"examples": [
{
"error": "'PlayerController.speed' is inaccessible due to its protection level",
"fix": "Change 'private float speed;' to 'public float speed;' or add [SerializeField]"
}
]
},
"CS0103": {
"errorCode": "CS0103",
"description": "The name does not exist in the current context",
"commonCauses": [
"Variable not declared",
"Typo in variable name",
"Variable out of scope",
"Missing component reference"
],
"solutions": [
{
"step": 1,
"action": "Declare variable before use",
"example": "Rigidbody rb;\nvoid Start() { rb = GetComponent<Rigidbody>(); }"
},
{
"step": 2,
"action": "Check variable name spelling",
"details": "Variable names are case-sensitive"
},
{
"step": 3,
"action": "Ensure variable is in scope",
"details": "Variables declared in methods are only accessible within that method"
}
],
"unityNotes": "Common with component references - ensure they're declared as class fields",
"examples": [
{
"error": "The name 'rb' does not exist in the current context",
"fix": "Declare 'Rigidbody rb;' at class level before using in methods"
}
]
},
"CS0119": {
"errorCode": "CS0119",
"description": "Member is a type, which is not valid in the given context",
"commonCauses": [
"Using type name instead of instance",
"Missing instantiation",
"Incorrect static/instance usage"
],
"solutions": [
{
"step": 1,
"action": "Create instance of type",
"example": "GameObject player = new GameObject(); // not just 'GameObject'"
},
{
"step": 2,
"action": "Use correct Unity API",
"example": "GameObject.Find('Player') // static method on type"
}
],
"unityNotes": "Unity has both static methods (GameObject.Find) and instance methods (gameObject.SetActive)",
"examples": [
{
"error": "'GameObject' is a type but is used like a variable",
"fix": "Use 'gameObject' (lowercase - current instance) or 'new GameObject()'"
}
]
},
"CS0117": {
"errorCode": "CS0117",
"description": "Type does not contain a definition",
"commonCauses": [
"Unity API version change",
"Incorrect class/namespace",
"Missing package reference"
],
"solutions": [
{
"step": 1,
"action": "Check Unity documentation",
"details": "API may have changed in your Unity version"
},
{
"step": 2,
"action": "Verify class name",
"details": "Ensure using correct Unity class"
},
{
"step": 3,
"action": "Update deprecated APIs",
"details": "Some APIs are replaced in newer Unity versions"
}
],
"unityNotes": "Unity 2020+ changed many APIs. Check Unity Upgrade Guide",
"examples": [
{
"error": "'Input' does not contain a definition for 'GetKey'",
"fix": "In new Input System, use 'Keyboard.current.spaceKey.isPressed' instead"
}
]
}
}

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env node
/**
* Unity Compile Error Analyzer
*
* Processes VSCode diagnostics JSON output and extracts Unity-relevant C# compilation errors.
* Filters by severity (Error only), source (csharp), and classifies errors by type.
*
* Usage:
* node analyze-diagnostics.js <diagnostics-json-file>
* node analyze-diagnostics.js (reads from stdin)
*
* Output: JSON array of classified Unity errors
*/
const fs = require('fs');
const path = require('path');
// Error classification patterns
const ERROR_CLASSIFICATIONS = {
MISSING_IMPORT: {
patterns: [/CS0246/, /type or namespace.*could not be found/i],
category: 'Missing Import/Namespace',
priority: 1
},
TYPE_MISMATCH: {
patterns: [/CS0029/, /cannot implicitly convert/i, /cannot convert type/i],
category: 'Type Mismatch',
priority: 2
},
MEMBER_NOT_FOUND: {
patterns: [/CS1061/, /does not contain a definition/i],
category: 'Member Not Found',
priority: 2
},
DUPLICATE_DEFINITION: {
patterns: [/CS0101/, /already contains a definition/i],
category: 'Duplicate Definition',
priority: 3
},
SYNTAX_ERROR: {
patterns: [/CS1002/, /CS1003/, /CS1525/, /expected/i],
category: 'Syntax Error',
priority: 1
},
ACCESS_MODIFIER: {
patterns: [/CS0122/, /inaccessible due to its protection level/i],
category: 'Access Modifier Issue',
priority: 2
},
UNITY_API: {
patterns: [/MonoBehaviour/, /GameObject/, /Transform/, /Component/],
category: 'Unity API Issue',
priority: 1
}
};
/**
* Classify error based on message content
*/
function classifyError(message) {
for (const [key, classifier] of Object.entries(ERROR_CLASSIFICATIONS)) {
for (const pattern of classifier.patterns) {
if (pattern.test(message)) {
return {
type: key,
category: classifier.category,
priority: classifier.priority
};
}
}
}
return {
type: 'UNKNOWN',
category: 'Other Error',
priority: 4
};
}
/**
* Extract error code from message (e.g., "CS0246")
*/
function extractErrorCode(message) {
const match = message.match(/CS\d{4}/);
return match ? match[0] : null;
}
/**
* Filter and process diagnostics
*/
function analyzeDiagnostics(diagnostics) {
if (!diagnostics || !Array.isArray(diagnostics)) {
console.error('Error: Invalid diagnostics format. Expected array.');
process.exit(1);
}
const unityErrors = diagnostics
.filter(diag => {
// Filter: Only errors (not warnings)
if (diag.severity !== 'Error') return false;
// Filter: Only C# errors from OmniSharp
if (diag.source !== 'csharp') return false;
// Filter: Must have valid message
if (!diag.message) return false;
return true;
})
.map(diag => {
const classification = classifyError(diag.message);
const errorCode = extractErrorCode(diag.message);
return {
file: diag.uri ? diag.uri.replace('file:///', '') : 'unknown',
line: diag.range?.start?.line ?? 0,
column: diag.range?.start?.character ?? 0,
errorCode: errorCode,
message: diag.message,
classification: classification.category,
type: classification.type,
priority: classification.priority,
source: diag.source
};
});
// Sort by priority (lower number = higher priority)
unityErrors.sort((a, b) => a.priority - b.priority);
return unityErrors;
}
/**
* Generate summary statistics
*/
function generateSummary(errors) {
const summary = {
totalErrors: errors.length,
byCategory: {},
byFile: {},
highPriority: errors.filter(e => e.priority === 1).length
};
errors.forEach(error => {
// Count by category
summary.byCategory[error.classification] =
(summary.byCategory[error.classification] || 0) + 1;
// Count by file
const fileName = path.basename(error.file);
summary.byFile[fileName] = (summary.byFile[fileName] || 0) + 1;
});
return summary;
}
/**
* Main execution
*/
function main() {
const args = process.argv.slice(2);
let diagnosticsData;
if (args.length === 0) {
// Read from stdin
console.error('Reading diagnostics from stdin...');
const input = fs.readFileSync(0, 'utf-8');
try {
diagnosticsData = JSON.parse(input);
} catch (error) {
console.error('Error parsing JSON from stdin:', error.message);
process.exit(1);
}
} else {
// Read from file
const filePath = args[0];
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(1);
}
try {
const fileContent = fs.readFileSync(filePath, 'utf-8');
diagnosticsData = JSON.parse(fileContent);
} catch (error) {
console.error(`Error reading/parsing file: ${error.message}`);
process.exit(1);
}
}
// Extract diagnostics array (handle different formats)
let diagnostics;
if (Array.isArray(diagnosticsData)) {
diagnostics = diagnosticsData;
} else if (diagnosticsData.diagnostics && Array.isArray(diagnosticsData.diagnostics)) {
diagnostics = diagnosticsData.diagnostics;
} else {
console.error('Error: Could not find diagnostics array in input');
process.exit(1);
}
// Analyze
const errors = analyzeDiagnostics(diagnostics);
const summary = generateSummary(errors);
// Output results
const output = {
summary: summary,
errors: errors
};
console.log(JSON.stringify(output, null, 2));
}
// Run if executed directly
if (require.main === module) {
main();
}
// Export for use as module
module.exports = {
analyzeDiagnostics,
classifyError,
extractErrorCode,
generateSummary
};

View File

@@ -0,0 +1,73 @@
---
name: Unity Scene Optimizer
description: Analyzes scenes for performance bottlenecks (draw calls, batching, textures, GameObjects). Use when optimizing scenes or investigating performance issues.
allowed-tools: Read, Grep, Glob
---
# Unity Scene Optimizer
Analyzes Unity scenes and provides performance optimization recommendations for rendering, physics, memory, and platform-specific concerns.
## What This Skill Analyzes
### 1. Rendering Performance
Analyzes draw calls (target: <100 mobile, <2000 desktop), identifies batching opportunities, recommends material consolidation and static batching.
### 2. Texture Optimization
Reviews compression formats (BC7/ASTC), mipmap usage, texture atlasing, and platform-specific import settings.
### 3. GameObject Hierarchy
Targets: <500 GameObjects mobile, <2000 desktop. Identifies deep nesting, recommends object pooling and LOD groups.
### 4. Lighting and Shadows
Recommends baked lighting over realtime (1-2 lights mobile, 3-4 desktop), minimal shadow-casting lights.
### 5. Physics Optimization
Analyzes Rigidbody count, collider complexity, collision matrix configuration. Recommends simple colliders over Mesh colliders.
### 6. Mobile-Specific
Platform targets: 60 FPS iOS (iPhone 12+), 30-60 FPS Android. See [mobile-checklist.md](mobile-checklist.md) for complete requirements.
## Optimization Workflow
1. **Measure**: Frame Debugger, Stats, Profiler metrics
2. **Identify**: GPU/CPU/Memory/Physics bottlenecks
3. **Apply**: Quick wins (static batching, compression) → Medium (atlases, pooling, LOD) → Major (hierarchy refactor, culling)
4. **Validate**: Compare before/after metrics
See [optimization-workflow.md](optimization-workflow.md) for detailed steps and timelines.
## Platform-Specific Targets
| Platform | Draw Calls | Triangles | Texture Memory | Lights |
|----------|-----------|-----------|----------------|--------|
| **Mobile Low** | <50 | <20k | <100MB | 1 |
| **Mobile Mid** | <100 | <50k | <250MB | 1-2 |
| **Mobile High** | <150 | <100k | <500MB | 2-3 |
| **PC Low** | <500 | <200k | <1GB | 3-4 |
| **PC Mid** | <1000 | <500k | <2GB | 4-6 |
| **PC High** | <2000 | <1M | <4GB | 6-8 |
| **Console** | <1000 | <800k | <3GB | 4-6 |
## Tools Reference
Frame Debugger, Profiler, Stats Window, Memory Profiler. See [tools-reference.md](tools-reference.md) for usage and commands.
## Output Format
Provides: Current metrics, bottleneck identification, prioritized recommendations, performance impact estimates, implementation steps.
## When to Use vs Other Components
**Use this Skill when**: Analyzing scene performance, identifying bottlenecks, or getting optimization recommendations
**Use @unity-performance agent when**: Implementing complex optimizations, profiling at runtime, or troubleshooting specific performance issues
**Use @unity-architect agent when**: Redesigning scene architecture, implementing object pooling systems, or planning large-scale optimizations
**Use /unity:optimize-scene command when**: Running comprehensive scene analysis with detailed reports
## Related Skills
- **unity-script-validator**: For script-level performance issues
- **unity-template-generator**: For optimized component templates

View File

@@ -0,0 +1,45 @@
---
name: Unity Script Validator
description: Validates C# scripts for best practices, performance, and Unity patterns. Use when reviewing scripts or checking code quality.
allowed-tools: Read, Grep, Glob
---
# Unity Script Validator
Validates Unity C# scripts against best practices and performance patterns specific to Unity game development.
## What This Skill Checks
- **Field declarations**: `[SerializeField] private` instead of public fields
- **Component caching**: GetComponent in Awake/Start, not Update (~100x faster)
- **String operations**: StringBuilder for frequent concatenation
- **GameObject.Find**: Cache references, avoid in Update (O(n) operation)
- **Code organization**: #region directives, consistent ordering
- **XML documentation**: `<summary>` tags on public methods
- **Update vs FixedUpdate**: Appropriate usage for physics/non-physics
- **Coroutines**: Prefer for intermittent tasks over Update
Provides: Issues found, specific fixes, performance impact estimates, refactored code examples.
## Compatibility
Applies to Unity 2019.4 LTS and later (including Unity 6).
See [patterns.md](patterns.md) and [examples.md](examples.md) for detailed optimization techniques.
## When to Use vs Other Components
**Use this Skill when**: Quick validation of existing Unity scripts for best practices and common issues
**Use @unity-scripter agent when**: Writing new code or implementing Unity features from scratch
**Use @unity-refactor agent when**: Improving code quality, applying design patterns, or modernizing legacy code
**Use @unity-performance agent when**: Deep performance profiling, memory optimization, or platform-specific tuning
**Use /unity:new-script command when**: Creating new scripts from production-ready templates
## Related Skills
- **unity-scene-optimizer**: For scene-level performance analysis
- **unity-template-generator**: For generating validated script templates

View File

@@ -0,0 +1,49 @@
---
name: Unity Template Generator
description: Generates production-ready C# script templates (MonoBehaviour, ScriptableObject, Editor, tests). Use when creating new scripts or setting up project structure.
allowed-tools: Write, Read, Glob
---
# Unity Template Generator
Assists with generating production-ready Unity C# script templates that follow best practices and Unity conventions.
## Available Templates
**MonoBehaviour** - GameObject components with lifecycle methods, serialized fields, component caching, and Gizmo helpers.
**ScriptableObject** - Data assets with `[CreateAssetMenu]`, validation, encapsulation, and clone methods.
**Editor Script** - Custom inspectors or windows. Asks for UGUI vs UI Toolkit preference (see [unity-ui-selector](../unity-ui-selector/SKILL.md)).
**Test Script** - NUnit/PlayMode tests with Setup/TearDown, `[UnityTest]`, performance tests, and Arrange-Act-Assert pattern.
## Template Features
All templates include:
- Unity coding conventions (`[SerializeField]`, PascalCase, XML docs)
- Performance patterns (component caching, no GetComponent in Update)
- Code organization (#region directives, consistent ordering)
- Safety features (null checks, OnValidate, Gizmos)
Placeholders: `{{CLASS_NAME}}`, `{{NAMESPACE}}`, `{{DESCRIPTION}}`, `{{MENU_PATH}}`, `{{FILE_NAME}}`
See [template-reference.md](template-reference.md) for detailed customization options.
## When to Use vs Other Components
**Use this Skill when**: Discussing template options, understanding template features, or getting guidance on script structure
**Use @unity-scripter agent when**: Writing custom scripts with specific requirements or implementing complex Unity features
**Use @unity-refactor agent when**: Improving existing scripts or restructuring code for better maintainability
**Use /unity:new-script command when**: Actually generating script files from templates with specific parameters
**Use /unity:setup-test command when**: Setting up complete test environments with test scripts
## Related Skills
- **unity-script-validator**: Validates generated scripts
- **unity-ui-selector**: Helps choose UI system for Editor scripts
- **unity-uitoolkit**: Assists with UI Toolkit implementation when generating Editor scripts

View File

@@ -0,0 +1,424 @@
---
name: unity-test-runner
description: Execute and analyze Unity Test Framework tests from the command line. This skill automates test execution for Unity projects by detecting the Unity Editor, configuring test parameters (EditMode/PlayMode), running tests via CLI, parsing XML results, and generating detailed failure reports. Use this when running Unity tests, validating game logic, or debugging test failures.
---
# Unity Test Runner
## Overview
This skill enables automated execution and analysis of Unity Test Framework tests directly from the command line. It handles the complete test workflow: detecting Unity Editor installations across platforms (Windows/macOS/Linux), configuring test parameters, executing tests in EditMode or PlayMode, parsing NUnit XML results, and generating detailed failure reports with actionable insights.
## When to Use This Skill
Use this skill when:
- Executing Unity Test Framework tests from command line
- Running PlayMode or EditMode tests for game logic validation
- Analyzing test failures and generating failure reports
- Integrating Unity tests into CI/CD pipelines
- Debugging test failures with detailed stack traces and file locations
- Validating Unity project changes before commits
**Example user requests:**
- "Run all Unity tests in my project"
- "Execute PlayMode tests and show me the results"
- "Run tests in the Combat category"
- "Check if my Unity tests are passing"
- "Run EditMode tests only"
## Workflow
Follow this workflow when the skill is invoked:
### 1. Detect Unity Editor Installation
Use the `find-unity-editor.js` script to automatically locate the Unity Editor:
```bash
node scripts/find-unity-editor.js --json
```
**Script behavior:**
- Scans platform-specific default installation paths
- Detects all installed Unity versions
- Returns the latest version by default
- Can target specific version with `--version <version>` flag
**Output:**
```json
{
"found": true,
"editorPath": "C:\\Program Files\\Unity\\Hub\\Editor\\2021.3.15f1\\Editor\\Unity.exe",
"version": "2021.3.15f1",
"platform": "win32",
"allVersions": ["2021.3.15f1", "2020.3.30f1"]
}
```
**If multiple versions are found:**
1. Present all available versions to the user
2. Ask user to confirm which version to use
3. Or use the latest version by default
**If no Unity Editor is found:**
- Report error with searched paths
- Ask user to provide Unity Editor path manually
- Store the path for future use
### 2. Verify Unity Project Path
Confirm the current directory contains a valid Unity project using cross-platform checks:
```typescript
// Use Read tool to check for Unity project indicators
Read({ file_path: "ProjectSettings/ProjectVersion.txt" })
// Use Glob to verify Assets directory exists
Glob({ pattern: "Assets/*", path: "." })
```
**Validation steps:**
1. Verify `Assets/` directory exists
2. Verify `ProjectSettings/ProjectVersion.txt` exists
3. Read `ProjectVersion.txt` to get Unity version
4. Warn if Editor version doesn't match project version
**Example ProjectVersion.txt:**
```
m_EditorVersion: 2021.3.15f1
m_EditorVersionWithRevision: 2021.3.15f1 (e8e88743f9e5)
```
### 3. Configure Test Settings
Determine test execution parameters. Use `AskUserQuestion` tool if parameters are not specified:
**Required settings:**
- **Test Mode**: EditMode, PlayMode, or Both
- **Test Platform**: EditMode tests use "EditMode", PlayMode can specify platform (e.g., "StandaloneWindows64", "Android", "iOS")
**Optional settings:**
- **Test Categories**: Semicolon-separated list (e.g., "Combat;AI;Physics")
- **Test Filter**: Regex pattern or semicolon-separated test names
- **Results Output Path**: Default to `TestResults.xml` in project root
**Configuration example:**
```typescript
AskUserQuestion({
questions: [{
question: "Which test mode should be executed?",
header: "Test Mode",
multiSelect: false,
options: [
{ label: "EditMode Only", description: "Fast unit tests without Play Mode" },
{ label: "PlayMode Only", description: "Full Unity engine tests" },
{ label: "Both Modes", description: "Run all tests (slower)" }
]
}]
})
```
### 4. Execute Tests via Command Line
Build and execute the Unity command line test command:
**Command structure:**
```bash
<UnityEditorPath> -runTests -batchmode -projectPath <ProjectPath> \
-testPlatform <EditMode|PlayMode> \
-testResults <OutputPath> \
[-testCategory <Categories>] \
[-testFilter <Filter>] \
-logFile -
```
**Example commands:**
**EditMode tests:**
```bash
"C:\Program Files\Unity\Hub\Editor\2021.3.15f1\Editor\Unity.exe" \
-runTests -batchmode \
-projectPath "D:\Projects\MyGame" \
-testPlatform EditMode \
-testResults "TestResults-EditMode.xml" \
-logFile -
```
**PlayMode tests with category filter:**
```bash
"C:\Program Files\Unity\Hub\Editor\2021.3.15f1\Editor\Unity.exe" \
-runTests -batchmode \
-projectPath "D:\Projects\MyGame" \
-testPlatform PlayMode \
-testResults "TestResults-PlayMode.xml" \
-testCategory "Combat;AI" \
-logFile -
```
**Execution notes:**
- Use `Bash` tool with `run_in_background: true` for long-running tests
- Set timeout appropriately (default: 5-10 minutes, adjust based on test count)
- Monitor output for progress indicators
- Capture both stdout and stderr
**Example execution:**
```typescript
Bash({
command: `"${unityPath}" -runTests -batchmode -projectPath "${projectPath}" -testPlatform EditMode -testResults "TestResults.xml" -logFile -`,
description: "Execute Unity EditMode tests",
timeout: 300000, // 5 minutes
run_in_background: true
})
```
### 5. Parse Test Results
After tests complete, parse the NUnit XML results using `parse-test-results.js`:
```bash
node scripts/parse-test-results.js TestResults.xml --json
```
**Script output:**
```json
{
"summary": {
"total": 10,
"passed": 7,
"failed": 2,
"skipped": 1,
"duration": 12.345
},
"failures": [
{
"name": "TestPlayerTakeDamage",
"fullName": "Tests.Combat.PlayerTests.TestPlayerTakeDamage",
"message": "Expected: 90\n But was: 100",
"stackTrace": "at Tests.Combat.PlayerTests.TestPlayerTakeDamage () [0x00001] in Assets/Tests/Combat/PlayerTests.cs:42",
"file": "Assets/Tests/Combat/PlayerTests.cs",
"line": 42
}
],
"allTests": [...]
}
```
**Result analysis:**
1. Extract test summary statistics
2. Identify all failed tests
3. Extract file paths and line numbers from stack traces
4. Categorize failures by type (assertion, exception, timeout)
### 6. Analyze Test Failures
For each failed test, analyze the failure using `references/test-patterns.json`:
**Analysis steps:**
1. **Load test patterns database:**
```typescript
Read({ file_path: "references/test-patterns.json" })
```
2. **Match failure message against patterns:**
- Assertion failures: `Expected: <X> But was: <Y>`
- Null reference failures: `Expected: not null But was: <null>`
- Timeout failures: `TimeoutException|Test exceeded time limit`
- Threading errors: `Can't be called from.*main thread`
- Object lifetime issues: `has been destroyed|MissingReferenceException`
3. **Determine failure category:**
- ValueMismatch: Incorrect assertion value
- NullValue: Unexpected null reference
- Performance: Timeout or slow execution
- TestSetup: Setup/TearDown failure
- ObjectLifetime: Destroyed object access
- Threading: Wrong thread execution
4. **Generate fix suggestions:**
- Load common solutions from test-patterns.json
- Match solutions to failure pattern
- Provide concrete code examples
**Example failure analysis:**
```markdown
**Test**: Tests.Combat.PlayerTests.TestPlayerTakeDamage
**Location**: Assets/Tests/Combat/PlayerTests.cs:42
**Result**: FAILED
**Failure Message**:
Expected: 90
But was: 100
**Analysis**:
- Category: ValueMismatch (Assertion Failure)
- Pattern: Expected/actual value mismatch
- Root Cause: Player health not decreasing after TakeDamage() call
**Possible Causes**:
1. TakeDamage() method not implemented correctly
2. Player health not initialized properly
3. Damage value passed incorrectly
**Suggested Solutions**:
1. Verify TakeDamage() implementation:
```csharp
public void TakeDamage(int damage) {
health -= damage; // Ensure this line exists
}
```
2. Check test setup:
```csharp
[SetUp]
public void SetUp() {
player = new Player();
player.Health = 100; // Ensure proper initialization
}
```
3. Verify test assertion:
```csharp
player.TakeDamage(10);
Assert.AreEqual(90, player.Health); // Expected: 90
```
```
### 7. Generate Test Report
Create a comprehensive test report for the user:
**Report structure:**
```markdown
# Unity Test Results
## Summary
- **Total Tests**: 10
- **✓ Passed**: 7 (70%)
- **✗ Failed**: 2 (20%)
- **⊘ Skipped**: 1 (10%)
- **Duration**: 12.35s
## Test Breakdown
- **EditMode Tests**: 5 passed, 1 failed
- **PlayMode Tests**: 2 passed, 1 failed
## Failed Tests
### 1. Tests.Combat.PlayerTests.TestPlayerTakeDamage
**Location**: Assets/Tests/Combat/PlayerTests.cs:42
**Failure**: Expected: 90, But was: 100
**Analysis**: Player health not decreasing after TakeDamage() call.
**Suggested Fix**: Verify TakeDamage() implementation decreases health correctly.
---
### 2. Tests.AI.EnemyTests.TestEnemyChasePlayer
**Location**: Assets/Tests/AI/EnemyTests.cs:67
**Failure**: TimeoutException - Test exceeded time limit (5s)
**Analysis**: Infinite loop or missing yield in coroutine test.
**Suggested Fix**: Add `[UnityTest]` attribute and use `yield return null` in test loop.
---
## Next Steps
1. Review failed test locations and fix implementation
2. Re-run tests after fixes by re-invoking the skill
3. Consider adding more assertions for edge cases
```
**Report delivery:**
- Present report in formatted Markdown
- Highlight critical failures
- Provide file:line references for quick navigation
- Offer to help fix specific failures if user requests
## Best Practices
When using this skill:
1. **Run EditMode tests first** - They're faster and catch basic logic errors
- Reserve PlayMode tests for Unity-specific features
- Use EditMode for pure C# logic and data structures
2. **Use test categories** - Filter tests for faster iteration
- `-testCategory "Combat"` runs only Combat tests
- Helpful during active development of specific features
3. **Monitor test duration** - Set appropriate timeouts
- EditMode: 1-3 minutes typical
- PlayMode: 5-15 minutes typical
- Adjust timeout based on test count
4. **Check Unity version compatibility** - Ensure Editor matches project version
- Mismatched versions may cause test failures
- Test results may be inconsistent across versions
5. **Parse results immediately** - Don't wait for manual review
- Automated parsing catches issues faster
- Provides actionable file:line information
6. **Analyze failure patterns** - Look for common causes
- Similar failures often indicate systemic issues
- Fix root cause instead of individual symptoms
7. **Preserve test results** - Keep XML files for debugging
- Results contain full stack traces
- Useful for comparing test runs
8. **Handle long-running tests** - Use background execution
- Monitor progress with `BashOutput` tool
- Provide status updates to user
## Resources
### scripts/find-unity-editor.js
Cross-platform Unity Editor path detection script. Automatically scans default installation directories for Windows, macOS, and Linux, detects all installed Unity versions, and returns the latest version or a specific requested version.
**Usage:**
```bash
# Find latest Unity version
node scripts/find-unity-editor.js --json
# Find specific version
node scripts/find-unity-editor.js --version 2021.3.15f1 --json
```
**Output**: JSON with Unity Editor path, version, platform, and all available versions.
### scripts/parse-test-results.js
NUnit XML results parser for Unity Test Framework output. Extracts test statistics, failure details, stack traces, and file locations from XML results.
**Usage:**
```bash
# Parse test results with JSON output
node scripts/parse-test-results.js TestResults.xml --json
# Parse with formatted console output
node scripts/parse-test-results.js TestResults.xml
```
**Output**: JSON with test summary, failure details including file paths and line numbers, and full test list.
### references/test-patterns.json
Comprehensive database of Unity testing patterns, NUnit assertions, common failure patterns, and best practices. Includes:
- NUnit assertion reference (equality, collections, exceptions, Unity-specific)
- Common failure patterns with regex matching
- Failure categories and root cause analysis
- Solution templates with code examples
- EditMode vs PlayMode guidance
- Unity-specific testing patterns (coroutines, scenes, prefabs, physics)
- Testing best practices
**Usage**: Load this file when analyzing test failures to match failure messages against patterns and generate fix suggestions.

View File

@@ -0,0 +1,384 @@
{
"nunitAssertions": {
"equality": {
"assertions": [
{
"method": "Assert.AreEqual(expected, actual)",
"description": "Verifies that two values are equal",
"example": "Assert.AreEqual(100, player.Health);"
},
{
"method": "Assert.AreNotEqual(expected, actual)",
"description": "Verifies that two values are not equal",
"example": "Assert.AreNotEqual(0, enemy.Speed);"
}
]
},
"identity": {
"assertions": [
{
"method": "Assert.AreSame(expected, actual)",
"description": "Verifies that two objects refer to the same object instance",
"example": "Assert.AreSame(playerInstance, savedPlayer);"
},
{
"method": "Assert.AreNotSame(expected, actual)",
"description": "Verifies that two objects do not refer to the same object instance",
"example": "Assert.AreNotSame(player1, player2);"
}
]
},
"nullity": {
"assertions": [
{
"method": "Assert.IsNull(object)",
"description": "Verifies that an object is null",
"example": "Assert.IsNull(destroyedEnemy);"
},
{
"method": "Assert.IsNotNull(object)",
"description": "Verifies that an object is not null",
"example": "Assert.IsNotNull(spawnedPlayer);"
}
]
},
"boolean": {
"assertions": [
{
"method": "Assert.IsTrue(condition)",
"description": "Verifies that a condition is true",
"example": "Assert.IsTrue(player.IsAlive);"
},
{
"method": "Assert.IsFalse(condition)",
"description": "Verifies that a condition is false",
"example": "Assert.IsFalse(enemy.IsInvincible);"
}
]
},
"collections": {
"assertions": [
{
"method": "Assert.Contains(item, collection)",
"description": "Verifies that a collection contains a specific item",
"example": "Assert.Contains(weapon, inventory.Items);"
},
{
"method": "CollectionAssert.AreEqual(expected, actual)",
"description": "Verifies that two collections are equal",
"example": "CollectionAssert.AreEqual(expectedItems, actualItems);"
},
{
"method": "CollectionAssert.IsEmpty(collection)",
"description": "Verifies that a collection is empty",
"example": "CollectionAssert.IsEmpty(emptyInventory);"
},
{
"method": "CollectionAssert.IsNotEmpty(collection)",
"description": "Verifies that a collection is not empty",
"example": "CollectionAssert.IsNotEmpty(player.Skills);"
}
]
},
"exceptions": {
"assertions": [
{
"method": "Assert.Throws<TException>(() => { code })",
"description": "Verifies that a specific exception type is thrown",
"example": "Assert.Throws<ArgumentNullException>(() => player.Attack(null));"
},
{
"method": "Assert.DoesNotThrow(() => { code })",
"description": "Verifies that no exception is thrown",
"example": "Assert.DoesNotThrow(() => player.Move(Vector3.zero));"
}
]
},
"unity": {
"assertions": [
{
"method": "LogAssert.Expect(LogType, message)",
"description": "Expects a specific Unity log message",
"example": "LogAssert.Expect(LogType.Warning, \"Player health low\");"
},
{
"method": "LogAssert.NoUnexpectedReceived()",
"description": "Verifies no unexpected log messages were received",
"example": "LogAssert.NoUnexpectedReceived();"
}
]
}
},
"commonFailurePatterns": [
{
"pattern": "Expected: <(.+?)>.*?But was: <(.+?)>",
"type": "AssertionFailure",
"category": "ValueMismatch",
"description": "Value assertion failed - expected value doesn't match actual value",
"commonCauses": [
"Incorrect expected value in test",
"Logic error in tested code",
"Timing issue (value not yet updated)",
"Floating-point precision error"
],
"solutions": [
{
"condition": "Floating-point comparison",
"fix": "Use Assert.AreEqual(expected, actual, delta) with tolerance",
"example": "Assert.AreEqual(1.0f, result, 0.001f);"
},
{
"condition": "Async operation",
"fix": "Add yield return to wait for operation completion",
"example": "yield return new WaitForSeconds(0.1f);"
},
{
"condition": "Frame-dependent value",
"fix": "Use yield return null to wait for next frame",
"example": "yield return null; // Wait one frame"
}
]
},
{
"pattern": "Expected: not null.*But was: <null>",
"type": "NullReferenceFailure",
"category": "NullValue",
"description": "Expected non-null value but received null",
"commonCauses": [
"Object not instantiated",
"Component not attached",
"Resource not loaded",
"Missing dependency injection"
],
"solutions": [
{
"condition": "GameObject component",
"fix": "Ensure GameObject has required component",
"example": "var component = gameObject.AddComponent<PlayerController>();"
},
{
"condition": "Resource loading",
"fix": "Use Resources.Load or proper asset loading",
"example": "var prefab = Resources.Load<GameObject>(\"Prefabs/Player\");"
},
{
"condition": "Scene object reference",
"fix": "Use GameObject.Find or proper scene setup",
"example": "var player = GameObject.FindGameObjectWithTag(\"Player\");"
}
]
},
{
"pattern": "TimeoutException|Test exceeded time limit",
"type": "TimeoutFailure",
"category": "Performance",
"description": "Test execution exceeded time limit",
"commonCauses": [
"Infinite loop in test or code",
"Deadlock in async operations",
"Slow operation without proper timeout",
"Missing yield in coroutine test"
],
"solutions": [
{
"condition": "Coroutine test",
"fix": "Add [UnityTest] attribute and use yield return",
"example": "[UnityTest] public IEnumerator TestCoroutine() { yield return null; }"
},
{
"condition": "Async operation",
"fix": "Add timeout and proper await/yield",
"example": "yield return new WaitForSecondsRealtime(5f);"
},
{
"condition": "Infinite loop detection",
"fix": "Add loop counter or timeout check",
"example": "int maxIterations = 100; while(condition && maxIterations-- > 0) { }"
}
]
},
{
"pattern": "SetUp.*TearDown.*failed",
"type": "FixtureFailure",
"category": "TestSetup",
"description": "Test setup or teardown method failed",
"commonCauses": [
"Scene loading failure",
"Resource initialization error",
"Missing test dependencies",
"Cleanup error in previous test"
],
"solutions": [
{
"condition": "Scene loading",
"fix": "Use SceneManager.LoadScene in UnitySetUp",
"example": "[UnitySetUp] public IEnumerator SetUp() { yield return SceneManager.LoadSceneAsync(\"TestScene\"); }"
},
{
"condition": "GameObject cleanup",
"fix": "Use Object.DestroyImmediate in TearDown",
"example": "[TearDown] public void TearDown() { Object.DestroyImmediate(testObject); }"
}
]
},
{
"pattern": "MissingReferenceException|The object of type.*has been destroyed",
"type": "DestroyedObjectReference",
"category": "ObjectLifetime",
"description": "Attempted to access a destroyed Unity object",
"commonCauses": [
"Object destroyed before test completes",
"Accessing object after scene unload",
"Component removed during test",
"Improper test cleanup order"
],
"solutions": [
{
"condition": "Test cleanup",
"fix": "Check if object exists before accessing",
"example": "if (testObject != null && testObject) { /* access */ }"
},
{
"condition": "DontDestroyOnLoad objects",
"fix": "Manually destroy objects in TearDown",
"example": "[TearDown] public void TearDown() { Object.DestroyImmediate(GameObject.Find(\"Persistent\")); }"
}
]
},
{
"pattern": "Can't be called from.*main thread",
"type": "ThreadingError",
"category": "Threading",
"description": "Unity API called from wrong thread",
"commonCauses": [
"Async/await without proper context",
"Threading operation accessing Unity API",
"Task.Run accessing GameObject",
"Background thread creating Unity objects"
],
"solutions": [
{
"condition": "Async operations",
"fix": "Use UnityMainThreadDispatcher or yield return",
"example": "yield return new WaitForSeconds(1f); // Keeps on main thread"
},
{
"condition": "Thread synchronization",
"fix": "Queue operations for main thread execution",
"example": "UnityMainThreadDispatcher.Instance().Enqueue(() => { /* Unity API calls */ });"
}
]
}
],
"testModes": {
"EditMode": {
"description": "Tests that run in the Unity Editor without entering Play Mode",
"useCases": [
"Editor scripts and tools testing",
"Non-MonoBehaviour class testing",
"Fast unit tests without scene loading",
"Utility and helper function testing"
],
"limitations": [
"Cannot test MonoBehaviour lifecycle methods (Start, Update, etc.)",
"Cannot test physics or coroutines",
"No scene loading or GameObject instantiation"
],
"attributes": [
"[Test] - Standard NUnit test",
"[TestFixture] - Marks test class",
"[SetUp] - Runs before each test",
"[TearDown] - Runs after each test"
]
},
"PlayMode": {
"description": "Tests that run in Play Mode with full Unity engine functionality",
"useCases": [
"MonoBehaviour lifecycle testing",
"Scene and GameObject testing",
"Physics and collision testing",
"Coroutine and async operation testing"
],
"features": [
"Full Unity engine available",
"Scene loading supported",
"Physics simulation active",
"Coroutines can be used"
],
"attributes": [
"[UnityTest] - Coroutine-based test (returns IEnumerator)",
"[UnitySetUp] - Async setup method",
"[UnityTearDown] - Async teardown method",
"[Test] - Standard synchronous test (also works in PlayMode)"
]
}
},
"bestPractices": [
{
"category": "Test Independence",
"practice": "Each test should be independent and not rely on other tests",
"rationale": "Tests may run in any order and should not affect each other",
"example": "Use [SetUp] to initialize test state, [TearDown] to clean up"
},
{
"category": "Test Naming",
"practice": "Use descriptive test names that explain what is being tested",
"rationale": "Clear names make test failures easier to diagnose",
"example": "TestPlayerTakesDamageWhenHitByEnemy() instead of TestDamage()"
},
{
"category": "Arrange-Act-Assert",
"practice": "Structure tests with clear Arrange, Act, Assert sections",
"rationale": "Makes test logic clear and maintainable",
"example": "// Arrange\nvar player = CreatePlayer();\n// Act\nplayer.TakeDamage(10);\n// Assert\nAssert.AreEqual(90, player.Health);"
},
{
"category": "PlayMode Performance",
"practice": "Use EditMode tests when possible for faster execution",
"rationale": "PlayMode tests are slower due to Unity engine initialization",
"example": "Test pure C# logic in EditMode, reserve PlayMode for Unity-specific features"
},
{
"category": "Async Testing",
"practice": "Use [UnityTest] with IEnumerator for async operations",
"rationale": "Properly handles Unity's frame-based execution",
"example": "[UnityTest] public IEnumerator TestAsync() { yield return new WaitForSeconds(1f); }"
},
{
"category": "Scene Management",
"practice": "Load minimal test scenes for PlayMode tests",
"rationale": "Reduces test execution time and potential side effects",
"example": "Create dedicated empty test scenes with only required objects"
},
{
"category": "Test Categorization",
"practice": "Use [Category] attribute to group related tests",
"rationale": "Enables selective test execution",
"example": "[Test, Category(\"Combat\")] public void TestPlayerAttack() { }"
},
{
"category": "Floating-Point Comparison",
"practice": "Use tolerance when comparing floating-point values",
"rationale": "Floating-point arithmetic is imprecise",
"example": "Assert.AreEqual(expected, actual, 0.001f);"
}
],
"unitySpecificPatterns": {
"coroutineTesting": {
"description": "Testing coroutines requires [UnityTest] attribute",
"example": "[UnityTest]\npublic IEnumerator TestCoroutine()\n{\n var go = new GameObject();\n var component = go.AddComponent<MyComponent>();\n component.StartCoroutine(component.MyCoroutine());\n yield return new WaitForSeconds(1f);\n Assert.IsTrue(component.IsComplete);\n}"
},
"sceneTesting": {
"description": "Loading scenes in tests requires async operations",
"example": "[UnitySetUp]\npublic IEnumerator SetUp()\n{\n yield return SceneManager.LoadSceneAsync(\"TestScene\", LoadSceneMode.Single);\n}"
},
"prefabTesting": {
"description": "Testing prefabs requires instantiation",
"example": "[Test]\npublic void TestPrefab()\n{\n var prefab = Resources.Load<GameObject>(\"Prefabs/Player\");\n var instance = Object.Instantiate(prefab);\n Assert.IsNotNull(instance.GetComponent<PlayerController>());\n Object.DestroyImmediate(instance);\n}"
},
"physicsTesting": {
"description": "Physics tests need time for simulation",
"example": "[UnityTest]\npublic IEnumerator TestPhysics()\n{\n var go = GameObject.CreatePrimitive(PrimitiveType.Sphere);\n var rb = go.AddComponent<Rigidbody>();\n rb.AddForce(Vector3.up * 10f);\n yield return new WaitForFixedUpdate();\n Assert.Greater(rb.velocity.y, 0f);\n}"
}
}
}

View File

@@ -0,0 +1,279 @@
#!/usr/bin/env node
/**
* Unity Editor Path Finder
*
* Cross-platform script to automatically detect Unity Editor installation paths.
* Supports Windows, macOS, and Linux.
*
* Usage:
* node find-unity-editor.js [--version <version>] [--json]
*
* Options:
* --version <version> Find specific Unity version (e.g., 2021.3.15f1)
* --json Output results as JSON
*
* Output (JSON):
* {
* "found": true,
* "editorPath": "/path/to/Unity",
* "version": "2021.3.15f1",
* "platform": "win32|darwin|linux",
* "allVersions": [...]
* }
*/
const fs = require('fs');
const path = require('path');
// Parse command line arguments
const args = process.argv.slice(2);
const requestedVersion = args.includes('--version')
? args[args.indexOf('--version') + 1]
: null;
const jsonOutput = args.includes('--json');
/**
* Get default Unity installation paths for current platform
*/
function getDefaultUnityPaths() {
const platform = process.platform;
const home = process.env.HOME || process.env.USERPROFILE;
switch (platform) {
case 'win32':
return [
'C:\\Program Files\\Unity\\Hub\\Editor',
'C:\\Program Files\\Unity',
path.join(home, 'AppData', 'Local', 'Unity', 'Hub', 'Editor')
];
case 'darwin':
return [
'/Applications/Unity/Hub/Editor',
'/Applications/Unity',
path.join(home, 'Applications', 'Unity', 'Hub', 'Editor')
];
case 'linux':
return [
path.join(home, 'Unity', 'Hub', 'Editor'),
'/opt/unity',
'/usr/share/unity'
];
default:
return [];
}
}
/**
* Get Unity executable name for current platform
*/
function getUnityExecutableName() {
const platform = process.platform;
switch (platform) {
case 'win32':
return 'Unity.exe';
case 'darwin':
return 'Unity.app/Contents/MacOS/Unity';
case 'linux':
return 'Unity';
default:
return 'Unity';
}
}
/**
* Get Unity executable path from version directory
*/
function getUnityExecutablePath(versionPath) {
const platform = process.platform;
if (platform === 'win32') {
return path.join(versionPath, 'Editor', 'Unity.exe');
} else if (platform === 'darwin') {
return path.join(versionPath, 'Unity.app', 'Contents', 'MacOS', 'Unity');
} else {
return path.join(versionPath, 'Editor', 'Unity');
}
}
/**
* Check if a path contains a valid Unity installation
*/
function isValidUnityInstallation(versionPath) {
return fs.existsSync(getUnityExecutablePath(versionPath));
}
/**
* Parse Unity version string for sorting
* Format: 2021.3.15f1 -> {year: 2021, major: 3, minor: 15, build: 'f', patch: 1}
*/
function parseUnityVersion(versionStr) {
const match = versionStr.match(/(\d+)\.(\d+)\.(\d+)([a-z])(\d+)/);
if (!match) return null;
return {
year: parseInt(match[1]),
major: parseInt(match[2]),
minor: parseInt(match[3]),
build: match[4],
patch: parseInt(match[5]),
full: versionStr
};
}
/**
* Compare two Unity versions
*/
function compareVersions(a, b) {
const vA = parseUnityVersion(a);
const vB = parseUnityVersion(b);
if (!vA || !vB) return 0;
if (vA.year !== vB.year) return vB.year - vA.year;
if (vA.major !== vB.major) return vB.major - vA.major;
if (vA.minor !== vB.minor) return vB.minor - vA.minor;
if (vA.build !== vB.build) return vB.build.localeCompare(vA.build);
return vB.patch - vA.patch;
}
/**
* Scan directory for Unity installations
*/
function scanForUnityVersions(basePath) {
if (!fs.existsSync(basePath)) {
return [];
}
try {
const entries = fs.readdirSync(basePath, { withFileTypes: true });
const versions = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const versionPath = path.join(basePath, entry.name);
// Check if this looks like a Unity version (e.g., 2021.3.15f1)
if (/^\d{4}\.\d+\.\d+[a-z]\d+$/.test(entry.name) && isValidUnityInstallation(versionPath)) {
versions.push({
version: entry.name,
path: versionPath,
executablePath: getUnityExecutablePath(versionPath)
});
}
}
return versions;
} catch (error) {
return [];
}
}
/**
* Find all Unity installations
*/
function findAllUnityInstallations() {
const searchPaths = getDefaultUnityPaths();
const allVersions = [];
for (const searchPath of searchPaths) {
const versions = scanForUnityVersions(searchPath);
allVersions.push(...versions);
}
// Remove duplicates based on version string
const uniqueVersions = allVersions.filter((v, index, self) =>
index === self.findIndex(t => t.version === v.version)
);
// Sort by version (newest first)
uniqueVersions.sort((a, b) => compareVersions(a.version, b.version));
return uniqueVersions;
}
/**
* Find Unity Editor
*/
function findUnityEditor() {
const allVersions = findAllUnityInstallations();
if (allVersions.length === 0) {
return {
found: false,
error: 'No Unity installations found',
platform: process.platform,
searchedPaths: getDefaultUnityPaths()
};
}
// If specific version requested, find it
if (requestedVersion) {
const found = allVersions.find(v => v.version === requestedVersion);
if (found) {
return {
found: true,
editorPath: found.executablePath,
version: found.version,
platform: process.platform,
allVersions: allVersions.map(v => v.version)
};
} else {
return {
found: false,
error: `Unity version ${requestedVersion} not found`,
platform: process.platform,
availableVersions: allVersions.map(v => v.version)
};
}
}
// Return latest version
const latest = allVersions[0];
return {
found: true,
editorPath: latest.executablePath,
version: latest.version,
platform: process.platform,
allVersions: allVersions.map(v => v.version)
};
}
// Main execution
try {
const result = findUnityEditor();
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.found) {
console.log(`✓ Unity ${result.version} found`);
console.log(` Path: ${result.editorPath}`);
console.log(` Platform: ${result.platform}`);
if (result.allVersions && result.allVersions.length > 1) {
console.log(`\n Other versions available: ${result.allVersions.slice(1).join(', ')}`);
}
} else {
console.error(`${result.error}`);
if (result.availableVersions && result.availableVersions.length > 0) {
console.error(`\n Available versions: ${result.availableVersions.join(', ')}`);
} else if (result.searchedPaths) {
console.error(`\n Searched paths:`);
result.searchedPaths.forEach(p => console.error(` - ${p}`));
}
process.exit(1);
}
}
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}

View File

@@ -0,0 +1,284 @@
#!/usr/bin/env node
/**
* Unity Test Results Parser
*
* Parses Unity Test Framework NUnit XML output and extracts test statistics and failure details.
*
* Usage:
* node parse-test-results.js <path-to-results.xml> [--json]
*
* Options:
* --json Output results as JSON
*
* Output (JSON):
* {
* "summary": {
* "total": 10,
* "passed": 7,
* "failed": 2,
* "skipped": 1,
* "duration": 1.234
* },
* "failures": [
* {
* "name": "TestName",
* "fullName": "Namespace.Class.TestName",
* "message": "Failure message",
* "stackTrace": "Stack trace",
* "file": "Assets/Tests/TestFile.cs",
* "line": 42
* }
* ]
* }
*/
const fs = require('fs');
const path = require('path');
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
console.log('Usage: node parse-test-results.js <path-to-results.xml> [--json]');
process.exit(args.length === 0 ? 1 : 0);
}
const resultsPath = args[0];
const jsonOutput = args.includes('--json');
/**
* Extract text content from XML tag
*
* Note: This uses regex-based parsing instead of a full XML parser library.
* While regex-based XML parsing is generally not recommended, it's sufficient
* for Unity Test Framework's consistent NUnit XML output format. The patterns
* are designed to be non-greedy and handle common variations (attributes, whitespace).
*
* For production use with arbitrary XML, consider using fast-xml-parser or xml2js.
*/
function extractTagContent(xml, tagName) {
// Non-greedy matching: [\s\S]*? ensures minimal capture between tags
// [^>]* allows for attributes without capturing them (stops at first >)
const regex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)<\\/${tagName}>`, 'i');
const match = xml.match(regex);
return match ? match[1].trim() : '';
}
/**
* Extract attribute value from XML tag
*
* Handles attributes with double quotes. Unity NUnit XML consistently uses
* double quotes for attributes, so this simple pattern is reliable.
*/
function extractAttribute(tag, attrName) {
// Escape special regex characters in attribute name
const escapedAttrName = attrName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`${escapedAttrName}="([^"]*)"`, 'i');
const match = tag.match(regex);
return match ? match[1] : null;
}
/**
* Extract file path and line number from stack trace
*/
function extractFileInfo(stackTrace) {
// Pattern: at Namespace.Class.Method () [0x00000] in /path/to/file.cs:42
// Pattern: at Namespace.Class.Method () in Assets/Tests/TestFile.cs:line 42
const patterns = [
/in (.+\.cs):(\d+)/i,
/in (.+\.cs):line (\d+)/i,
/\[0x[0-9a-f]+\] in (.+\.cs):(\d+)/i
];
for (const pattern of patterns) {
const match = stackTrace.match(pattern);
if (match) {
return {
file: match[1],
line: parseInt(match[2])
};
}
}
return {
file: null,
line: null
};
}
/**
* Parse test-case element
*
* Extracts test metadata from <test-case> XML element using attribute matching.
* All attributes are optional - defaults are provided for missing values.
*/
function parseTestCase(testCaseXml) {
// Extract attributes with fallback defaults
const nameMatch = testCaseXml.match(/name="([^"]*)"/);
const fullNameMatch = testCaseXml.match(/fullname="([^"]*)"/);
const resultMatch = testCaseXml.match(/result="([^"]*)"/);
const durationMatch = testCaseXml.match(/duration="([^"]*)"/);
const testCase = {
name: nameMatch ? nameMatch[1] : 'Unknown',
fullName: fullNameMatch ? fullNameMatch[1] : 'Unknown',
result: resultMatch ? resultMatch[1] : 'Unknown',
duration: durationMatch ? parseFloat(durationMatch[1]) || 0 : 0
};
// Extract failure information if test failed
if (testCase.result === 'Failed') {
const failureXml = extractTagContent(testCaseXml, 'failure');
if (failureXml) {
testCase.message = extractTagContent(failureXml, 'message');
testCase.stackTrace = extractTagContent(failureXml, 'stack-trace');
// Extract file and line from stack trace
if (testCase.stackTrace) {
const fileInfo = extractFileInfo(testCase.stackTrace);
testCase.file = fileInfo.file;
testCase.line = fileInfo.line;
}
}
}
return testCase;
}
/**
* Parse Unity Test Framework XML results
*
* Parses NUnit XML format produced by Unity Test Framework.
* Expects standard Unity test output structure with <test-run> root element.
*
* @param {string} xmlContent - Raw XML content from test results file
* @returns {Object} Parsed test results with summary, failures, and all tests
* @throws {Error} If XML structure is invalid or test-run element is missing
*/
function parseTestResults(xmlContent) {
// Validate XML has test-run root element
const testRunMatch = xmlContent.match(/<test-run[^>]*>/i);
if (!testRunMatch) {
throw new Error('Invalid Unity Test Framework XML: <test-run> element not found. ' +
'Ensure the file is a valid NUnit XML results file from Unity.');
}
const testRunTag = testRunMatch[0];
const summary = {
total: parseInt(extractAttribute(testRunTag, 'total') || '0'),
passed: parseInt(extractAttribute(testRunTag, 'passed') || '0'),
failed: parseInt(extractAttribute(testRunTag, 'failed') || '0'),
skipped: parseInt(extractAttribute(testRunTag, 'skipped') || '0') +
parseInt(extractAttribute(testRunTag, 'inconclusive') || '0'),
duration: parseFloat(extractAttribute(testRunTag, 'duration') || '0')
};
// Extract all test cases using non-greedy matching
// Pattern matches <test-case ...>...</test-case> with minimal capture
// [^>]* stops at first >, [\s\S]*? captures minimal content between tags
const testCaseRegex = /<test-case[^>]*>[\s\S]*?<\/test-case>/gi;
const testCaseMatches = xmlContent.match(testCaseRegex) || [];
const allTests = [];
const failures = [];
for (const testCaseXml of testCaseMatches) {
const testCase = parseTestCase(testCaseXml);
allTests.push(testCase);
if (testCase.result === 'Failed') {
failures.push(testCase);
}
}
return {
summary,
failures,
allTests
};
}
/**
* Format output for console
*/
function formatConsoleOutput(results) {
const { summary, failures } = results;
console.log('\n=== Unity Test Results ===\n');
// Summary
console.log(`Total Tests: ${summary.total}`);
console.log(`✓ Passed: ${summary.passed}`);
if (summary.failed > 0) {
console.log(`✗ Failed: ${summary.failed}`);
}
if (summary.skipped > 0) {
console.log(`⊘ Skipped: ${summary.skipped}`);
}
console.log(`Duration: ${summary.duration.toFixed(3)}s\n`);
// Failures
if (failures.length > 0) {
console.log('=== Failed Tests ===\n');
failures.forEach((failure, index) => {
console.log(`${index + 1}. ${failure.fullName}`);
if (failure.message) {
console.log(` Message: ${failure.message}`);
}
if (failure.file && failure.line) {
console.log(` Location: ${failure.file}:${failure.line}`);
}
if (failure.stackTrace) {
console.log(` Stack Trace:`);
const lines = failure.stackTrace.split('\n').slice(0, 3);
lines.forEach(line => console.log(` ${line.trim()}`));
if (failure.stackTrace.split('\n').length > 3) {
console.log(` ... (truncated)`);
}
}
console.log('');
});
} else {
console.log('✓ All tests passed!\n');
}
}
// Main execution
try {
// Check if file exists
if (!fs.existsSync(resultsPath)) {
console.error(`Error: Test results file not found: ${resultsPath}`);
process.exit(1);
}
// Read and parse XML
const xmlContent = fs.readFileSync(resultsPath, 'utf-8');
const results = parseTestResults(xmlContent);
// Output results
if (jsonOutput) {
console.log(JSON.stringify(results, null, 2));
} else {
formatConsoleOutput(results);
}
// Exit with error code if tests failed
if (results.summary.failed > 0) {
process.exit(1);
}
} catch (error) {
console.error(`Error parsing test results: ${error.message}`);
process.exit(1);
}

View File

@@ -0,0 +1,73 @@
---
name: Unity UI System Selector
description: Guides selection between UGUI and UI Toolkit for projects. Use when choosing UI framework or migrating UI systems.
---
# Unity UI System Selector
Helps you choose the appropriate UI system for your Unity project and provides implementation guidance for both UGUI and UI Toolkit.
## Two UI Systems
**UGUI (Legacy)** - GameObject-based (2014). Mature, works on all Unity versions, large community. Weaker: Complex UI performance, limited styling, no live reload.
**UI Toolkit (Modern)** - Retained mode, web-inspired UXML/USS (2021.2+). Better performance, live reload, data-binding. Weaker: Requires 2021.2+, smaller community, limited 3D world-space UI.
## Decision Framework
**Use UGUI if:**
- Unity < 2021.2
- Simple UI (menus, HUD)
- 3D world-space UI needed
- Team knows UGUI well / tight deadline
- Legacy project
**Use UI Toolkit if:**
- Unity 2021.2+ and new project (future-proof)
- Complex/data-driven UI (inventory, skill trees)
- Editor tools (inspectors, windows) - **strongly recommended**
- Web dev background (HTML/CSS)
- Large-scale UI (MMO, strategy games)
When in doubt: For new projects on Unity 2021.2+, **UI Toolkit is recommended**.
## Comparison
| Feature | UGUI | UI Toolkit |
|---------|------|-----------|
| **Version** | 4.6+ | 2021.2+ |
| **Performance** | Simple UIs | All UIs |
| **Styling** | Inspector | CSS-like USS |
| **Layout** | Manual/Groups | Flexbox-like |
| **Editor Tools** | Good | Excellent |
| **Runtime UI** | Excellent | Good |
| **3D World UI** | Excellent | Limited |
## Migration
See [migration-guide.md](migration-guide.md) for UGUI → UI Toolkit migration strategy (3-4 months for medium projects).
## UI System Support Matrix
| Unity Version | UGUI | UI Toolkit (Editor) | UI Toolkit (Runtime) |
|--------------|------|-------------------|---------------------|
| 2019.4 LTS | ✅ Full | ✅ Basic | ❌ No |
| 2020.3 LTS | ✅ Full | ✅ Good | ⚠️ Experimental |
| 2021.3 LTS | ✅ Full | ✅ Excellent | ✅ Production |
| 2022.3 LTS+ | ✅ Full | ✅ Primary | ✅ Full |
## When to Use vs Other Components
**Use this Skill when**: Choosing between UGUI and UI Toolkit, understanding UI system trade-offs, or planning UI migration
**Use @unity-scripter agent when**: Implementing UI components, writing custom UI scripts, or converting UI code
**Use @unity-architect agent when**: Designing complex UI architecture, planning UI data flow, or structuring large-scale UI systems
**Use /unity:new-script command when**: Generating Editor scripts with UI Toolkit or UGUI templates
## Related Skills
- **unity-uitoolkit**: Assists with UI Toolkit implementation (UXML, USS, VisualElement API)
- **unity-template-generator**: Generates Editor scripts using selected UI system
- **unity-script-validator**: Validates UI code patterns

View File

@@ -0,0 +1,86 @@
---
name: Unity UI Toolkit
description: Assists with Unity UI Toolkit development - UXML structure, USS styling, C# VisualElement manipulation, data binding, and custom controls. Use when implementing UI Toolkit interfaces.
allowed-tools: Read, Write, Glob
---
# Unity UI Toolkit
Assists with Unity UI Toolkit development including UXML markup, USS styling, C# VisualElement API, and modern UI patterns.
## What This Skill Helps With
### UXML Structure
- Proper element hierarchy and naming conventions
- Common controls: TextField, Button, Toggle, Slider, ObjectField, ListView
- Layout containers: VisualElement, ScrollView, Foldout, TwoPaneSplitView
- Data-driven UI with templates and bindings
### USS Styling
- Class-based styling and selectors
- Flexbox layout (flex-direction, justify-content, align-items)
- USS variables and dark theme optimization
- Pseudo-classes (:hover, :active, :disabled)
- Transitions and animations
### C# VisualElement API
- Query API: `rootElement.Q<Button>("my-button")`
- Event handling: `.clicked +=` and `.RegisterValueChangedCallback()`
- Dynamic UI creation with constructors
- Data binding with `Bind()` and `SerializedObject`
### Best Practices
- UXML for structure, USS for styling, C# for logic
- Name elements for Query API access
- Use classes for styling, not inline styles
- Cache VisualElement references in fields
- Proper event cleanup in `OnDestroy()`
## Common Patterns
**Editor Window Setup:**
```csharp
public void CreateGUI() {
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("path/to.uxml");
visualTree.CloneTree(rootVisualElement);
var button = rootVisualElement.Q<Button>("my-button");
button.clicked += OnButtonClick;
}
```
**USS Class Toggle:**
```csharp
element.AddToClassList("active");
element.RemoveFromClassList("active");
element.ToggleInClassList("active");
```
**Data Binding:**
```csharp
var so = new SerializedObject(target);
rootVisualElement.Bind(so);
```
## Unity Version Requirements
- **Unity 2021.2+** for runtime UI Toolkit
- **Unity 2019.4+** for editor-only UI Toolkit (limited features)
See [ui-toolkit-reference.md](ui-toolkit-reference.md) for complete API documentation.
## When to Use vs Other Components
**Use this Skill when**: Building UI Toolkit interfaces, writing UXML/USS, or manipulating VisualElements in C#
**Use unity-ui-selector skill when**: Choosing between UGUI and UI Toolkit for a project
**Use @unity-scripter agent when**: Implementing complex UI logic or custom VisualElement controls
**Use EditorScriptUIToolkit templates when**: Generating new UI Toolkit editor windows with UXML/USS files
## Related Skills
- **unity-ui-selector**: Helps choose between UGUI and UI Toolkit
- **unity-template-generator**: Generates UI Toolkit editor script templates
- **unity-script-validator**: Validates UI Toolkit code patterns

View File

@@ -0,0 +1,203 @@
# UI Toolkit Quick Reference
## Common VisualElement Types
### Input Controls
- `TextField` - Single/multi-line text input
- `IntegerField`, `FloatField`, `Vector3Field` - Numeric inputs
- `Toggle` - Boolean checkbox
- `Button` - Clickable button
- `Slider` - Value slider with optional input field
- `EnumField` - Dropdown for enum values
- `ObjectField` - Unity Object reference picker
### Layout Containers
- `VisualElement` - Generic container (like `<div>`)
- `ScrollView` - Scrollable area
- `Foldout` - Collapsible section
- `TwoPaneSplitView` - Resizable split panel
- `ListView` - Data-driven list with virtualization
### Display Elements
- `Label` - Text display
- `Image` - Sprite/Texture display
- `HelpBox` - Info/Warning/Error message box
- `ProgressBar` - Progress indicator
## USS Flexbox Layout
```css
.container {
flex-direction: row; /* or column */
justify-content: flex-start; /* flex-end, center, space-between */
align-items: stretch; /* flex-start, flex-end, center */
flex-grow: 1;
flex-shrink: 0;
}
```
## USS Common Properties
```css
/* Spacing */
margin: 10px;
padding: 5px 10px;
/* Sizing */
width: 200px;
height: 100px;
min-width: 50px;
max-height: 300px;
/* Background */
background-color: rgb(50, 50, 50);
background-image: url('path/to/image.png');
/* Border */
border-width: 1px;
border-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
/* Text */
color: rgb(200, 200, 200);
font-size: 14px;
-unity-font-style: bold; /* or italic */
-unity-text-align: middle-center;
```
## Query API Examples
```csharp
// By name (must set name in UXML)
var button = root.Q<Button>("my-button");
// By class
var items = root.Query<VisualElement>(className: "item").ToList();
// First match
var firstLabel = root.Q<Label>();
// All matches
var allButtons = root.Query<Button>().ToList();
// Complex query
var activeItems = root.Query<VisualElement>()
.Where(e => e.ClassListContains("active"))
.ToList();
```
## Event Handling
```csharp
// Button click
button.clicked += () => Debug.Log("Clicked!");
// Value change
textField.RegisterValueChangedCallback(evt => {
Debug.Log($"Changed: {evt.previousValue} -> {evt.newValue}");
});
// Mouse events
element.RegisterCallback<MouseDownEvent>(evt => {
Debug.Log($"Mouse down at {evt.localMousePosition}");
});
// Cleanup
void OnDestroy() {
button.clicked -= OnButtonClick;
}
```
## Data Binding
```csharp
// Bind to SerializedObject
var so = new SerializedObject(targetObject);
rootVisualElement.Bind(so);
// Manual binding
var property = so.FindProperty("fieldName");
var field = new PropertyField(property);
field.BindProperty(property);
```
## Custom VisualElement
```csharp
public class CustomElement : VisualElement
{
public new class UxmlFactory : UxmlFactory<CustomElement, UxmlTraits> { }
public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlStringAttribute customAttribute = new UxmlStringAttribute
{ name = "custom-value" };
public override void Init(VisualElement ve, IUxmlAttributes bag,
CreationContext cc)
{
base.Init(ve, bag, cc);
((CustomElement)ve).customValue = customAttribute.GetValueFromBag(bag, cc);
}
}
private string customValue;
public CustomElement()
{
AddToClassList("custom-element");
}
}
```
## Performance Tips
1. **Use USS classes** instead of inline styles
2. **Cache VisualElement references** instead of repeated queries
3. **Use ListView** for large lists (virtualized)
4. **Avoid excessive rebuilds** - update only changed elements
5. **Use USS variables** for maintainable themes
6. **Minimize UXML nesting** for better performance
## Common Pitfalls
**Querying before CreateGUI finishes**
```csharp
// Wrong
void OnEnable() {
var button = rootVisualElement.Q<Button>(); // null!
}
// Correct
public void CreateGUI() {
visualTree.CloneTree(rootVisualElement);
var button = rootVisualElement.Q<Button>(); // works!
}
```
**Forgetting to name elements**
```xml
<!-- Wrong: Can't query by name -->
<ui:Button text="Click" />
<!-- Correct -->
<ui:Button name="my-button" text="Click" />
```
**Not cleaning up events**
```csharp
// Memory leak!
button.clicked += OnClick;
// Correct
void OnDestroy() {
button.clicked -= OnClick;
}
```
## Resources
- Unity Manual: UI Toolkit
- Unity Scripting API: UnityEngine.UIElements
- Unity Forum: UI Toolkit section
- Sample Projects: UI Toolkit samples on GitHub