Files
aego/anim/ctrl_text.go
T
sanspie 515f4ca1b3 Stage 11a: animation (D333–D340, D344, D354–D357)
Clips (.aeanim) and controllers (.aectrl) with importers, AnimationPlayer and Animator with crossfades, root motion and events on fixed steps, rewind support. Editor: Animation panel (dope sheet, curves, record, preview, trajectory), Animator panel, sprite sheet slicer and flipbooks, Programming workspace, export panel redesign. Motion 2D ledge climb. 11c: the component and Create palette.
2026-10-25 15:31:22 +03:00

341 lines
9.5 KiB
Go

package anim
import (
"strconv"
"aego/core/curve"
"aego/core/ids"
"aego/format/aetext"
)
const (
CtrlHead = "controller"
CtrlVersion = 1
AnyName = "any"
unresolved = int32(-2)
)
var (
paramNames = []string{"bool", "int", "float", "trigger"}
opNames = []string{"set", "eq", "ne", "gt", "ge", "lt", "le"}
policyNames = []string{"dominant", "both", "incoming"}
)
func (k ParamKind) String() string { return nameOf(paramNames, uint8(k)) }
func (o Op) String() string { return nameOf(opNames, uint8(o)) }
func (p EventPolicy) String() string { return nameOf(policyNames, uint8(p)) }
func DecodeGraph(n *aetext.Node) (*Graph, []GraphProblem, error) {
if n == nil || n.Head != CtrlHead {
return nil, nil, textErr(n, "the document is not an animation controller")
}
if err := strict(n, ctrlShape); err != nil {
return nil, nil, err
}
if v, ok := n.Attr("version"); ok && v.AsInt() > CtrlVersion {
return nil, nil, textErr(n, "the controller is written by a newer engine, version "+strconv.FormatInt(v.AsInt(), 10))
}
g := &Graph{Ease: curve.InOutQuad}
for _, at := range [...]struct {
name string
pos *[2]float32
has *bool
}{{"entryPos", &g.EntryPos, &g.HasEntryPos}, {"anyPos", &g.AnyPos, &g.HasAnyPos}} {
if v, ok := n.Attr(at.name); ok {
if len(v.List) != 2 {
return nil, nil, textErr(n, at.name+" needs two numbers")
}
*at.pos, *at.has = [2]float32{float32(v.List[0].AsFloat()), float32(v.List[1].AsFloat())}, true
}
}
if v, ok := n.Attr("ease"); ok {
e, known := curve.ParseEasing(word(v))
if !known {
return nil, nil, textErr(n, "unknown ease "+word(v))
}
g.Ease = e
}
for _, b := range n.Blocks {
switch b.Head {
case "param":
p, err := decodeParam(b)
if err != nil {
return nil, nil, err
}
if g.ParamIndex(p.Name) >= 0 {
return nil, nil, textErr(b, "parameter "+p.Name+" is declared twice")
}
g.Params = append(g.Params, p)
case "state":
s, err := decodeState(b)
if err != nil {
return nil, nil, err
}
if g.StateIndex(s.Name) >= 0 {
return nil, nil, textErr(b, "state "+s.Name+" is declared twice")
}
g.States = append(g.States, s)
case "transition":
default:
return nil, nil, textErr(b, "unknown block "+b.Head)
}
}
var probs []GraphProblem
if v, ok := n.Field("entry"); ok {
if g.Entry = g.StateIndex(word(v)); g.Entry < 0 {
probs = append(probs, GraphProblem{Kind: GraphState, Transition: -1, Name: word(v)})
}
}
for _, b := range n.Blocks {
if b.Head != "transition" {
continue
}
t, err := g.decodeTransition(b, len(g.Transitions), &probs)
if err != nil {
return nil, nil, err
}
g.Transitions = append(g.Transitions, t)
}
return g, probs, nil
}
func decodeParam(b *aetext.Node) (Param, error) {
p := Param{Name: b.Label}
if err := strict(b, paramShape); err != nil {
return p, err
}
if p.Name == "" {
return p, textErr(b, "a parameter needs a name")
}
if _, ok := b.Attr("kind"); !ok {
return p, textErr(b, "parameter "+p.Name+" needs kind")
}
k, err := attrName(b, "kind", paramNames)
if err != nil {
return p, err
}
p.Kind = ParamKind(k)
v, ok := b.Attr("default")
if !ok {
return p, nil
}
switch {
case p.Kind == ParamTrigger:
return p, textErr(b, "trigger "+p.Name+" has no default")
case p.Kind == ParamBool && v.Kind == aetext.VBool:
if v.Bool {
p.Default = 1
}
case p.Kind == ParamInt && v.Kind == aetext.VInt:
p.Default = float64(v.Int)
case p.Kind == ParamFloat && (v.Kind == aetext.VInt || v.Kind == aetext.VFloat):
p.Default = v.AsFloat()
default:
return p, textErr(b, "the default of "+p.Name+" does not match its kind "+p.Kind.String())
}
return p, nil
}
func decodeState(b *aetext.Node) (State, error) {
s := State{Name: b.Label, Speed: 1}
if err := strict(b, stateShape); err != nil {
return s, err
}
if s.Name == "" || s.Name == AnyName {
return s, textErr(b, "a state needs a name other than "+AnyName)
}
if v, ok := b.Attr("clip"); ok {
if v.Kind != aetext.VAsset {
return s, textErr(b, "the clip of "+s.Name+" is not an asset reference")
}
id, err := ids.ParseAssetID(v.Str)
if err != nil {
return s, textErr(b, "the clip of "+s.Name+" is not an asset id")
}
s.Clip = id
}
if v, ok := b.Attr("speed"); ok {
s.Speed = float32(v.AsFloat())
}
if v, ok := b.Attr("loop"); ok {
l, known := ParseLoop(word(v))
if !known {
return s, textErr(b, "unknown loop "+word(v))
}
s.Loop, s.HasLoop = l, true
}
if v, ok := b.Attr("pos"); ok {
p, ok := readLanes(v, 2)
if !ok {
return s, textErr(b, "the pos of "+s.Name+" is [x, y]")
}
s.Pos, s.HasPos = [2]float32{p[0], p[1]}, true
}
e, err := attrName(b, "events", policyNames)
if err != nil {
return s, err
}
s.Events = EventPolicy(e)
return s, nil
}
func (g *Graph) decodeTransition(b *aetext.Node, i int, probs *[]GraphProblem) (Transition, error) {
t := Transition{From: AnyState}
if err := strict(b, transShape); err != nil {
return t, err
}
if b.Label != AnyName {
if t.From = g.StateIndex(b.Label); t.From < 0 {
t.From = unresolved
*probs = append(*probs, GraphProblem{Kind: GraphState, Transition: i, Name: b.Label})
}
}
tv, ok := b.Attr("to")
if !ok {
return t, textErr(b, "the transition needs to")
}
if t.To = g.StateIndex(word(tv)); t.To < 0 {
*probs = append(*probs, GraphProblem{Kind: GraphState, Transition: i, Name: word(tv)})
}
if v, ok := b.Attr("duration"); ok {
t.Duration = float32(v.AsFloat())
}
if v, ok := b.Attr("exitTime"); ok {
t.ExitTime, t.HasExit = float32(v.AsFloat()), true
}
if v, ok := b.Attr("interrupt"); ok {
t.Interrupt = v.Bool
}
for _, cb := range b.Blocks {
if cb.Head != "cond" {
return t, textErr(cb, "a transition holds only cond blocks")
}
if err := strict(cb, condShape); err != nil {
return t, err
}
c := Cond{Param: g.ParamIndex(cb.Label)}
if c.Param < 0 {
*probs = append(*probs, GraphProblem{Kind: GraphParam, Transition: i, Name: cb.Label})
}
if v, ok := cb.Attr("op"); ok {
o, known := indexOf(opNames, word(v))
if !known {
return t, textErr(cb, "unknown op "+word(v))
}
c.Op = Op(o)
}
v, ok := cb.Attr("value")
switch {
case !ok && c.Op != OpSet:
return t, textErr(cb, "the condition on "+cb.Label+" needs value")
case !ok:
case v.Kind == aetext.VBool:
if v.Bool {
c.Value = 1
}
case v.Kind == aetext.VInt || v.Kind == aetext.VFloat:
c.Value = v.AsFloat()
default:
return t, textErr(cb, "the value of a condition is a number or true/false")
}
t.Conds = append(t.Conds, c)
}
return t, nil
}
func (g *Graph) stateName(i int32) string {
if i == AnyState {
return AnyName
}
if i >= 0 && int(i) < len(g.States) {
return g.States[i].Name
}
return ""
}
func EncodeGraph(g *Graph) *aetext.Node {
n := &aetext.Node{Head: CtrlHead, Attrs: []aetext.Field{field("version", aetext.Int(CtrlVersion))}}
if g.Ease != curve.InOutQuad {
n.Attrs = append(n.Attrs, field("ease", aetext.Ident(g.Ease.String())))
}
if g.HasEntryPos {
n.Attrs = append(n.Attrs, field("entryPos", aetext.List(aetext.Float32(g.EntryPos[0]), aetext.Float32(g.EntryPos[1]))))
}
if g.HasAnyPos {
n.Attrs = append(n.Attrs, field("anyPos", aetext.List(aetext.Float32(g.AnyPos[0]), aetext.Float32(g.AnyPos[1]))))
}
if g.Entry >= 0 && int(g.Entry) < len(g.States) {
n.Fields = append(n.Fields, field("entry", aetext.String(g.States[g.Entry].Name)))
}
for _, p := range g.Params {
b := &aetext.Node{Head: "param", Label: p.Name, Attrs: []aetext.Field{field("kind", aetext.Ident(p.Kind.String()))}}
if p.Default != 0 && p.Kind != ParamTrigger {
var v aetext.Value
switch p.Kind {
case ParamBool:
v = aetext.Bool(true)
case ParamInt:
v = aetext.Int(int64(p.Default))
default:
v = aetext.Float64(p.Default)
}
b.Attrs = append(b.Attrs, field("default", v))
}
n.Blocks = append(n.Blocks, b)
}
for _, s := range g.States {
b := &aetext.Node{Head: "state", Label: s.Name}
if !s.Clip.IsZero() {
b.Attrs = append(b.Attrs, field("clip", aetext.AssetRef(s.Clip.String())))
}
if s.Speed != 1 {
b.Attrs = append(b.Attrs, field("speed", aetext.Float32(s.Speed)))
}
if s.HasLoop {
b.Attrs = append(b.Attrs, field("loop", aetext.Ident(s.Loop.String())))
}
if s.Events != EventsDominant {
b.Attrs = append(b.Attrs, field("events", aetext.Ident(s.Events.String())))
}
if s.HasPos {
b.Attrs = append(b.Attrs, field("pos", aetext.List(aetext.Float32(s.Pos[0]), aetext.Float32(s.Pos[1]))))
}
n.Blocks = append(n.Blocks, b)
}
for i := range g.Transitions {
t := &g.Transitions[i]
b := &aetext.Node{Head: "transition", Label: g.stateName(t.From), Attrs: []aetext.Field{field("to", aetext.String(g.stateName(t.To)))}}
if t.Duration > 0 {
b.Attrs = append(b.Attrs, field("duration", aetext.Float32(t.Duration)))
}
if t.HasExit {
b.Attrs = append(b.Attrs, field("exitTime", aetext.Float32(t.ExitTime)))
}
if t.Interrupt {
b.Attrs = append(b.Attrs, field("interrupt", aetext.Bool(true)))
}
for _, c := range t.Conds {
cb := &aetext.Node{Head: "cond"}
kind := ParamFloat
if c.Param >= 0 && int(c.Param) < len(g.Params) {
cb.Label, kind = g.Params[c.Param].Name, g.Params[c.Param].Kind
}
if c.Op != OpSet {
var v aetext.Value
switch kind {
case ParamBool:
v = aetext.Bool(c.Value != 0)
case ParamInt:
v = aetext.Int(int64(c.Value))
default:
v = aetext.Float64(c.Value)
}
cb.Attrs = append(cb.Attrs, field("op", aetext.Ident(c.Op.String())), field("value", v))
}
b.Blocks = append(b.Blocks, cb)
}
n.Blocks = append(n.Blocks, b)
}
return n
}