Repository navigation
Advanced Features
Yaroslav Sarchuk edited this page Aug 29, 2025
·
1 revision
This guide covers advanced features and techniques for power users of the Atomic Plugin.
The plugin generates code with preserve regions for custom additions:
// Generated file structure
public static class EntityExtensions
{
#region Generated Code
// Auto-generated methods here
#endregion
#region Custom Code
// Your custom additions here - preserved during regeneration
#endregion
}Create partial classes to extend generated code:
entityType: "IEntity"
namespace: "Game.Extensions"
className: "EntityExtensions"
# Enable partial class generation
Your custom partial:
namespace Game.Extensions
{
public static partial class EntityExtensions
{
// Custom methods that complement generated ones
public static void TakeDamage(this IEntity entity, float damage, IEntity source)
{
float currentHealth = entity.GetHealth();
float actualDamage = CalculateDamage(damage, entity, source);
entity.SetHealth(currentHealth - actualDamage);
if (currentHealth - actualDamage <= 0)
{
entity.AddDeadTag();
OnEntityDeath?.Invoke(entity, source);
}
}
public static event Action<IEntity, IEntity> OnEntityDeath;
}
}Configure split generation:
entityType: "IEntity"
namespace: "Game.Generated"
className: "EntityExtensions"
directory: "Scripts/Generated"
# Advanced: Split into multiple files
splitFiles: true
filesPerCategory: true
imports:
System
UnityEngine
# Each category generates a separate file
tags:
# Combat tags โ EntityExtensions.Combat.cs
combat:
InCombat
Attacking
Defending
# Status tags โ EntityExtensions.Status.cs
status:
Dead
Stunned
Invisible
values:
# Stats โ EntityExtensions.Stats.cs
stats:
Health: int
Mana: int
Stamina: int
# Combat โ EntityExtensions.Combat.cs
combat:
Damage: float
CritChance: float
AttackSpeed: float
Create custom templates for generated methods:
// CustomTemplates.cs
public static class AtomicTemplates
{
public const string GetterTemplate = @"
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static {Type} Get{Name}(this {EntityType} entity)
{{
#if DEBUG
Debug.Assert(entity != null, ""Entity is null"");
#endif
return entity.Get<{Type}>(""{Name}"");
}}";
public const string SetterTemplate = @"
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Set{Name}(this {EntityType} entity, {Type} value)
{{
#if DEBUG
Debug.Assert(entity != null, ""Entity is null"");
ValidateValue(value);
#endif
entity.Set(""{Name}"", value);
OnValueChanged?.Invoke(entity, ""{Name}"", value);
}}";
}-
From .atomic to C#:
Ctrl+Clickon type name -
From C# to .atomic:
Ctrl+Clickon generated method
- Right-click on tag/value in
.atomic - Select "Find Usages"
- Shows all generated method usages
-
Ctrl+Shift+F12- Search for symbols - Includes
.atomicdefined symbols
values:
Health: int # Right-click โ Find Usages shows:
# - GetHealth() calls
# - SetHealth() calls
# - HasHealth() calls
# - RefHealth() calls
Add to your .csproj:
<Project>
<!-- Auto-generate before build -->
<Target Name="GenerateAtomicFiles" BeforeTargets="BeforeBuild">
<ItemGroup>
<AtomicFiles Include="**\*.atomic" />
</ItemGroup>
<Message Text="Generating from @(AtomicFiles)" Importance="high" />
<!-- Custom MSBuild task -->
<GenerateAtomic Files="@(AtomicFiles)" />
</Target>
<!-- Clean generated files -->
<Target Name="CleanGenerated" BeforeTargets="Clean">
<Delete Files="**\*.generated.cs" />
</Target>
</Project>name: Build with Atomic Generation
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Generate Atomic Files
run: |
dotnet tool install -g AtomicGenerator
atomic-gen **/*.atomic
- name: Build
run: dotnet build
- name: Test
run: dotnet testControl access levels in generation:
entityType: "IEntity"
namespace: "Game.Internal"
className: "InternalExtensions"
# Access modifiers
accessLevel: "internal" # public, internal, private
values:
# Per-value access control
PublicHealth: int
internal InternalData: string
private PrivateCache: object
Generated:
internal static class InternalExtensions
{
public static int GetPublicHealth(this IEntity entity) { }
internal static string GetInternalData(this IEntity entity) { }
private static object GetPrivateCache(this IEntity entity) { }
}Track generated method usage:
public static class AtomicMetrics
{
private static Dictionary<string, int> methodCalls = new();
public static void TrackCall(string methodName)
{
#if ANALYTICS_ENABLED
methodCalls.TryGetValue(methodName, out int count);
methodCalls[methodName] = count + 1;
#endif
}
public static void LogMetrics()
{
foreach (var kvp in methodCalls.OrderByDescending(x => x.Value))
{
Debug.Log($"{kvp.Key}: {kvp.Value} calls");
}
}
}Integration in generated code:
public static int GetHealth(this IEntity entity)
{
AtomicMetrics.TrackCall(nameof(GetHealth));
return entity.Get<int>("Health");
}public class DynamicEntityFactory
{
private Dictionary<string, Type> dynamicTypes = new();
public IEntity CreateDynamicEntity(string schemaPath)
{
// Load .atomic file at runtime
var schema = LoadAtomicSchema(schemaPath);
// Generate type dynamically
var entityType = GenerateDynamicType(schema);
// Create instance
return Activator.CreateInstance(entityType) as IEntity;
}
private Type GenerateDynamicType(AtomicSchema schema)
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicEntities"),
AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
var typeBuilder = moduleBuilder.DefineType(
schema.ClassName,
TypeAttributes.Public | TypeAttributes.Static);
// Generate methods based on schema
foreach (var value in schema.Values)
{
GenerateGetter(typeBuilder, value);
GenerateSetter(typeBuilder, value);
}
return typeBuilder.CreateType();
}
}Create custom quick fixes for .atomic files:
class CustomAtomicQuickFix : LocalQuickFix {
override fun getName() = "Add Missing Import"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement
val atomicFile = element.containingFile as? AtomicFile ?: return
// Add import to file
val import = AtomicPsiFactory.createImport(project, "System.Collections.Generic")
atomicFile.importsSection?.add(import)
}
}class UnusedValueInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : AtomicVisitor() {
override fun visitValueItem(item: AtomicValueItem) {
val usages = ReferencesSearch.search(item).findAll()
if (usages.isEmpty()) {
holder.registerProblem(
item,
"Unused value '${item.name}'",
ProblemHighlightType.LIKE_UNUSED_SYMBOL,
RemoveValueQuickFix()
)
}
}
}
}
}public class AtomicValidator
{
public static void ValidateSchema(AtomicSchema schema)
{
// Check for naming conflicts
var allNames = schema.Tags.Concat(schema.Values.Keys);
var duplicates = allNames.GroupBy(x => x)
.Where(g => g.Count() > 1)
.Select(g => g.Key);
if (duplicates.Any())
{
throw new ValidationException($"Duplicate names: {string.Join(", ", duplicates)}");
}
// Validate type compatibility
foreach (var value in schema.Values)
{
if (!IsValidType(value.Value))
{
throw new ValidationException($"Invalid type: {value.Value}");
}
}
}
}public class AtomicMigration
{
public void MigrateV1ToV2(string atomicFilePath)
{
var content = File.ReadAllText(atomicFilePath);
var lines = content.Split('\n').ToList();
// Update old syntax to new
for (int i = 0; i < lines.Count; i++)
{
// Old: "value: Health int"
// New: "Health: int"
if (lines[i].Contains("value:"))
{
var match = Regex.Match(lines[i], @"value:\s+(\w+)\s+(\w+)");
if (match.Success)
{
lines[i] = $" {match.Groups[1]}: {match.Groups[2]}";
}
}
}
File.WriteAllLines(atomicFilePath, lines);
}
}# .atomicformat configuration
formatting:
indentSize: 4
indentStyle: space
maxLineLength: 120
alignValues: true
sortImports: true
sortTags: alphabetical
sortValues: alphabetical
Result:
imports:
System
System.Collections.Generic
UnityEngine
tags:
Ally
Boss
Enemy
Player
values:
Agility: int
Damage: float
Health: int
Intelligence: int
Strength: int
/// <summary>
/// Auto-generated from Character.atomic
/// </summary>
/// <remarks>
/// Tags: Player, Enemy, NPC
/// Values: Health (int), Damage (float), Position (Vector3)
/// </remarks>
public static class CharacterExtensions
{
/// <summary>
/// Gets the Health value from the entity.
/// </summary>
/// <param name="entity">The entity to get the value from.</param>
/// <returns>The Health value, or default(int) if not present.</returns>
public static int GetHealth(this IEntity entity) { }
}[Conditional("PROFILING")]
public static class AtomicProfiler
{
private static Dictionary<string, ProfileData> profiles = new();
public static void BeginSample(string name)
{
profiles[name] = new ProfileData { StartTime = Time.realtimeSinceStartup };
}
public static void EndSample(string name)
{
if (profiles.TryGetValue(name, out var data))
{
data.Duration = Time.realtimeSinceStartup - data.StartTime;
data.CallCount++;
}
}
}[PublicAPI]
[UsedImplicitly]
public static class EntityExtensions
{
[Pure]
[ContractAnnotation("entity:null => false")]
public static bool HasHealth([NotNull] this IEntity entity) { }
[ContractAnnotation("entity:null => halt")]
public static void SetHealth([NotNull] this IEntity entity, int value) { }
}[Generator]
public class AtomicSourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Find all .atomic files
var atomicFiles = context.AdditionalFiles
.Where(f => Path.GetExtension(f.Path) == ".atomic");
foreach (var file in atomicFiles)
{
var content = file.GetText(context.CancellationToken);
var generated = GenerateFromAtomic(content);
context.AddSource($"{Path.GetFileNameWithoutExtension(file.Path)}.g.cs", generated);
}
}
}Next Steps: Explore Performance Optimization or check Known Issues for current limitations.