Serialization and Save/Load
This skill guides component serialization, JSON tags, and runtime-only fields.
JSON Tags
| Tag | Effect |
|---|---|
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:
- •Registered components: Data is loaded from JSON
- •Unregistered components: Data is ignored
- •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
- •
Always use
json:"-"for:- •Timers and counters
- •Entity references
- •Temporary state
- •Debug/tracking values
- •
Use inspector tags for saved fields designers need to edit
- •
Keep runtime state minimal - only what's needed per frame
- •
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