AgentSkillsCN

Engine Serialization

引擎序列化

SKILL.md

Serialization and Save/Load

This skill guides component serialization, JSON tags, and runtime-only fields.

JSON Tags

TagEffect
json:"fieldname"Serialize with custom name
json:"-"Exclude from serialization
(no tag)Serialize with field name

Basic Serialization

go
type MyComponent struct {
    Name  string  `json:"Name"`
    Value float32 `json:"Value"`
    Count int     `json:"Count"`
}

Serializes to:

json
{
  "Name": "My Entity",
  "Value": 3.14,
  "Count": 42
}

Runtime-Only Fields

Use json:"-" for fields that should not be saved:

go
type TimerData struct {
    Interval  float32 `json:"Interval"`   // Saved
    Elapsed   float32 `json:"-"`          // Not saved
    TickCount int     `json:"-"`          // Not saved
}

type EnemyData struct {
    Health float32 `json:"Health"`      // Saved
    State  int     `json:"State"`       // Saved
    Cooldown float32 `json:"-"`         // Runtime only
}

Complete Component Example

go
package component

import "github.com/TheLazyLemur/engine/math"

type EnemyData struct {
    // Saved fields
    Health       float32 `inspector:"Health,min:1,max:100"`
    Speed        float32 `inspector:"Speed,min:0.1,max:20"`
    PatrolRadius float32 `inspector:"Patrol Radius"`
    
    // Dropdown state (saved)
    State        int     `inspector:"State,type:dropdown,options:Idle|Patrol|Chase"`
    
    // Vector3 (saved)
    HomePosition math.Vector3 `inspector:"Home Position,type:vector3"`
    
    // Runtime-only fields (not saved)
    StateTimer   float32 `json:"-"`
    TargetEntity uint64  `json:"-"`
    LastSeenTime float32 `json:"-"`
}

func NewEnemy() EnemyData {
    return EnemyData{
        Health:       50,
        Speed:        5,
        PatrolRadius: 10,
        State:        0,
        HomePosition: math.Vector3{},
    }
}

Component Registration

For serialization to work, register components:

go
// In game/components.go
func RegisterComponents(eng *engine.Engine) {
    engine.RegisterComponent(eng, "Enemy", gameComponent.Enemy)
    engine.RegisterComponent(eng, "Timer", gameComponent.Timer)
    // ... other components
}

Scene Serialization

Loading Scenes

The engine automatically loads scenes from assets/scenes/:

go
eng.LoadOrCreateScene("scenes/default.json")

If the scene doesn't exist, an empty scene is created.

What Gets Saved

When you modify entities in the editor and save, the scene JSON is updated with:

  • All entity IDs
  • All component data for registered components
  • Excludes runtime-only fields (json:"-")

Custom Serialization

For complex types, implement custom marshaling:

go
// If you need custom JSON behavior, wrap the type
type Vector3Wrapper struct {
    Value math.Vector3
}

func (v Vector3Wrapper) MarshalJSON() ([]byte, error) {
    return json.Marshal(v.Value)
}

func (v *Vector3Wrapper) UnmarshalJSON(data []byte) error {
    return json.Unmarshal(data, &v.Value)
}

Common Patterns

Cooldown Timer

go
type AbilityData struct {
    CooldownTime float32 `inspector:"Cooldown,min:0.1,max:60"`
    DurationTime float32 `inspector:"Duration,min:0.1,max:10"`
    
    // Runtime state (not saved)
    CurrentCooldown float32 `json:"-"`
    IsActive        bool    `json:"-"`
}

func (s *AbilitySystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    for entry := range query.Iter(world) {
        ability := gameComponent.Ability.Get(entry)
        
        if ability.IsActive {
            ability.CurrentCooldown -= dt
            if ability.CurrentCooldown <= 0 {
                ability.IsActive = false
                ability.CurrentCooldown = ability.CooldownTime
            }
        }
    }
}

Entity References

go
type SpawnerData struct {
    PrefabName string `inspector:"Prefab"`
    SpawnRate  float32 `inspector:"Spawn Rate"`
    
    // Reference to spawned entity (runtime only)
    SpawnedEntity uint64 `json:"-"`
    
    // Time until next spawn
    NextSpawnTime float32 `json:"-"`
}

Accumulated Values

go
type DamageData struct {
    DamagePerHit float32 `inspector:"Damage Per Hit"`
    MaxHealth    float32 `inspector:"Max Health"`
    
    // Runtime state
    CurrentHealth float32 `json:"-"`
    TotalDamage   float32 `json:"-"`
}

Deserialization Behavior

When loading a scene:

  1. Registered components: Data is loaded from JSON
  2. Unregistered components: Data is ignored
  3. Runtime fields: Initialize to zero/empty values
go
// Component with defaults
type MyData struct {
    Value float32 `json:"Value"`  // If not in JSON, defaults to 0
}

func NewMyData() MyData {
    return MyData{Value: 10}  // But NewMyData() returns 10
}

Note: Deserialization uses zero values, not New*() constructor values.

Best Practices

  1. Always use json:"-" for:

    • Timers and counters
    • Entity references
    • Temporary state
    • Debug/tracking values
  2. Use inspector tags for saved fields designers need to edit

  3. Keep runtime state minimal - only what's needed per frame

  4. Reset runtime state on scene load if needed:

go
func (s *MySystem) Init(world donburi.World) {
    query := donburi.NewQuery(filter.Contains(
        gameComponent.MyComponent,
    ))
    
    for entry := range query.Iter(world) {
        comp := gameComponent.MyComponent.Get(entry)
        comp.RuntimeState = 0  // Reset on game start
    }
}

See Also

  • engine-component: Creating component data structs
  • engine-inspector: Making fields editable in editor
  • engine-scene: Scene file format