652 lines
14 KiB
Go
652 lines
14 KiB
Go
package play
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"time"
|
|
|
|
"aego/asset"
|
|
"aego/contracts/persistence"
|
|
"aego/core/clock"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/core/rewind"
|
|
"aego/core/rng"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/history"
|
|
"aego/editor/core/history/commands"
|
|
"aego/engine"
|
|
"aego/engine/recorder"
|
|
"aego/platform/input"
|
|
"aego/render/rt"
|
|
"aego/render/view"
|
|
"aego/scene"
|
|
"aego/scene/frame"
|
|
"aego/scene/sceneio"
|
|
"aego/schema"
|
|
)
|
|
|
|
type State uint8
|
|
|
|
const (
|
|
Stopped State = iota
|
|
Playing
|
|
Paused
|
|
)
|
|
|
|
type Stats struct {
|
|
Update float64
|
|
Render float64
|
|
Nodes int
|
|
Sprites int
|
|
Views int
|
|
Frame uint64
|
|
Updates uint64
|
|
Tick uint64
|
|
Alpha float64
|
|
Scale float64
|
|
DroppedFixed uint64
|
|
}
|
|
|
|
type Controller struct {
|
|
beforeStop func()
|
|
ctx *context.Editor
|
|
state State
|
|
world *engine.World
|
|
runner *frame.Runner
|
|
scene *scene.Scene
|
|
doc document.ID
|
|
scale float64
|
|
input input.State
|
|
captured bool
|
|
origin mathx.Vec2
|
|
size mathx.Vec2
|
|
stats Stats
|
|
proxy *document.SceneDocument
|
|
subs []stateSub
|
|
nextSub uint64
|
|
detach func()
|
|
unrec func()
|
|
breakFn func() (string, bool)
|
|
breakWhy string
|
|
release func(*input.State) bool
|
|
hint func() string
|
|
hist *recording
|
|
limit int
|
|
cursor int
|
|
beyond bool
|
|
replaying bool
|
|
stepReq uint8
|
|
seed uint64
|
|
blocked string
|
|
assetRev uint64
|
|
editUnsub func()
|
|
breaks int32
|
|
broke int32
|
|
kf []rewind.Keyframer
|
|
kfOff string
|
|
keys []*snapshot
|
|
spare []*snapshot
|
|
keyBytes int
|
|
keyEvery uint64
|
|
lastTick uint64
|
|
floor int
|
|
rebase bool
|
|
replayed int
|
|
keyTime time.Duration
|
|
held bool
|
|
hasher *rewind.Hasher
|
|
}
|
|
|
|
func New(ctx *context.Editor) *Controller {
|
|
return &Controller{ctx: ctx, scale: 1}
|
|
}
|
|
|
|
func (c *Controller) SetRelease(pressed func(*input.State) bool, hint func() string) {
|
|
c.release, c.hint = pressed, hint
|
|
}
|
|
|
|
func (c *Controller) ReleasePressed(in *input.State) bool {
|
|
return c.release != nil && c.release(in)
|
|
}
|
|
|
|
func (c *Controller) ReleaseHint() string {
|
|
if c.hint == nil {
|
|
return ""
|
|
}
|
|
if k := c.hint(); k != "" {
|
|
return k + " releases input"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *Controller) State() State {
|
|
return c.state
|
|
}
|
|
|
|
func (c *Controller) Stats() Stats {
|
|
return c.stats
|
|
}
|
|
|
|
func (c *Controller) Scene() *scene.Scene {
|
|
return c.scene
|
|
}
|
|
|
|
func (c *Controller) Views() []view.View {
|
|
if c.state == Stopped || c.runner == nil {
|
|
return nil
|
|
}
|
|
return c.runner.Views()
|
|
}
|
|
|
|
func (c *Controller) World() *engine.World {
|
|
return c.world
|
|
}
|
|
|
|
func (c *Controller) Doc() document.ID {
|
|
return c.doc
|
|
}
|
|
|
|
func (c *Controller) Frames() uint64 {
|
|
if c.world == nil {
|
|
return 0
|
|
}
|
|
return c.world.Time.Frame()
|
|
}
|
|
func (c *Controller) Ticks() uint64 {
|
|
if c.world == nil {
|
|
return 0
|
|
}
|
|
return c.world.Time.Tick()
|
|
}
|
|
|
|
func (c *Controller) SimTime() float64 {
|
|
if c.world == nil {
|
|
return 0
|
|
}
|
|
return c.world.Time.Span(clock.Game).Elapsed
|
|
}
|
|
func (c *Controller) TimeScale() float64 {
|
|
return c.scale
|
|
}
|
|
func (c *Controller) SetTimeScale(s float64) {
|
|
c.scale = min(max(s, 0), clock.MaxScale)
|
|
if c.world != nil {
|
|
c.world.Time.SetScale(c.scale)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) SetBreak(fn func() (string, bool)) {
|
|
c.breakFn = fn
|
|
}
|
|
|
|
func (c *Controller) Recorder() *recorder.Recorder {
|
|
if c.world == nil {
|
|
return nil
|
|
}
|
|
return c.world.Recorder
|
|
}
|
|
|
|
func (c *Controller) checkBreak() bool {
|
|
if c.breakFn == nil {
|
|
return false
|
|
}
|
|
c.breaks++
|
|
why, hit := c.breakFn()
|
|
if hit {
|
|
c.breakWhy = why
|
|
c.broke = c.breaks
|
|
}
|
|
return hit
|
|
}
|
|
|
|
func (c *Controller) resolver() asset.Resolver {
|
|
if c.ctx.Loader != nil {
|
|
return c.ctx.Loader
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type stateSub struct {
|
|
id uint64
|
|
fn func(State)
|
|
}
|
|
|
|
func (c *Controller) Subscribe(fn func(State)) func() {
|
|
c.nextSub++
|
|
id := c.nextSub
|
|
c.subs = append(c.subs, stateSub{id: id, fn: fn})
|
|
return func() {
|
|
for i := range c.subs {
|
|
if c.subs[i].id == id {
|
|
c.subs = append(c.subs[:i], c.subs[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Controller) notify() {
|
|
for i := range c.subs {
|
|
c.subs[i].fn(c.state)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) PlayDoc() *document.SceneDocument {
|
|
return c.proxy
|
|
}
|
|
|
|
func (c *Controller) History() *history.History {
|
|
if c.proxy == nil {
|
|
return nil
|
|
}
|
|
return c.proxy.History
|
|
}
|
|
|
|
func (c *Controller) Start(d *document.SceneDocument) error {
|
|
if c.ctx.Scripts != nil {
|
|
if n := c.ctx.Scripts.Errors(); n > 0 {
|
|
return errs.New(errs.EditorCommand, strconv.Itoa(n)+" script package error(s), fix them before Play")
|
|
}
|
|
}
|
|
if first, n := c.ctx.Problems.Blocking(); n > 0 {
|
|
return errs.New(errs.EditorCommand, strconv.Itoa(n)+" problem(s) block Play, first: "+first.Message)
|
|
}
|
|
if c.state != Stopped {
|
|
c.Stop()
|
|
}
|
|
real := c.ctx.Docs.Get(d.ID)
|
|
if real == nil {
|
|
return errs.New(errs.EditorDocument, "scene is not open")
|
|
}
|
|
data := sceneio.Save(real.Scene())
|
|
c.seed = uint64(time.Now().UnixNano()) | 1
|
|
if err := c.start(data, real.Path); err != nil {
|
|
return err
|
|
}
|
|
c.hist = newHistory(data, real.Path, c.limit)
|
|
c.cursor, c.beyond, c.stepReq, c.rebase = 0, false, stepNone, false
|
|
c.assetRev = c.ctx.AssetRev
|
|
c.doc = real.ID
|
|
c.proxy = real.PlayCopy(c.scene)
|
|
c.ctx.Docs.SetPlay(c.proxy, c.world)
|
|
c.watchEdits()
|
|
c.bindKeys()
|
|
if c.keysOn() {
|
|
c.capture(0)
|
|
}
|
|
c.notify()
|
|
c.ctx.Notify(context.LevelInfo, "Play mode started")
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) SceneID() ids.AssetID {
|
|
real := c.ctx.Docs.Get(c.doc)
|
|
if real == nil || c.ctx.Pipeline == nil {
|
|
return ids.AssetID{}
|
|
}
|
|
id, _ := c.ctx.Pipeline.Lookup(real.Path)
|
|
return id
|
|
}
|
|
|
|
func (c *Controller) LoadScene(id ids.AssetID) error {
|
|
ctx := c.ctx
|
|
if ctx.Pipeline == nil {
|
|
return errs.New(errs.EditorDocument, "the asset pipeline is not available")
|
|
}
|
|
rel, ok := ctx.Pipeline.PathOf(id)
|
|
if !ok {
|
|
return errs.New(errs.EditorDocument, "the saved scene is not in the project")
|
|
}
|
|
d, err := ctx.Docs.Open(rel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
held := c.held
|
|
if err := c.Start(d); err != nil {
|
|
return err
|
|
}
|
|
if held {
|
|
c.Pause()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) StartProject() error {
|
|
ctx := c.ctx
|
|
p := ctx.Project
|
|
if p == nil || p.MainScene.IsZero() || ctx.Pipeline == nil {
|
|
return errs.New(errs.EditorDocument, "project has no main scene")
|
|
}
|
|
rel, ok := ctx.Pipeline.PathOf(p.MainScene)
|
|
if !ok {
|
|
return errs.New(errs.EditorDocument, "main scene is not in the asset pipeline")
|
|
}
|
|
d, err := ctx.Docs.Open(rel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Start(d)
|
|
}
|
|
|
|
func (c *Controller) start(data []byte, name string) error {
|
|
ctx := c.ctx
|
|
res := c.resolver()
|
|
s, report, err := sceneio.Load(data, name, ctx.Types, res, sceneio.LoadOptions{KeepIDs: true})
|
|
if err != nil {
|
|
return errs.Wrap(err, errs.EditorDocument, "cannot clone the scene for play mode")
|
|
}
|
|
for _, u := range report.Unknown {
|
|
ctx.Notify(context.LevelWarn, "component "+u+" is not loaded in this build and does nothing in play mode")
|
|
}
|
|
c.scene = s
|
|
c.world = engine.NewWorld("play")
|
|
c.world.Input = &c.input
|
|
c.world.Random = rng.New(c.seed)
|
|
c.world.Time.Configure(ctx.Engine.TimeConfig())
|
|
c.world.Time.SetScale(c.scale)
|
|
c.world.Recorder = recorder.New(4096)
|
|
c.world.Break = c.checkBreak
|
|
c.runner = frame.New()
|
|
c.runner.Bind(s, ctx.Renderer, res)
|
|
c.world.SetView(frame.ServiceID, c.runner)
|
|
c.world.SetService(scene.ServiceID, s)
|
|
c.world.SetView(persistence.HostID, c)
|
|
c.unrec = frame.Record(s, c.world.Recorder)
|
|
c.world.Recorder.Add(recorder.SceneChange, ids.NodeID{}, ids.NodeID{}, name, "play")
|
|
c.input = input.State{}
|
|
if ctx.Scripts != nil {
|
|
detach, err := ctx.Scripts.Attach(c.world, s)
|
|
if err != nil {
|
|
c.world, c.runner, c.scene = nil, nil, nil
|
|
return errs.Wrap(err, errs.EditorDocument, "cannot start the scripts")
|
|
}
|
|
c.detach = detach
|
|
}
|
|
c.state, c.held = Playing, false
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) teardown() {
|
|
if c.editUnsub != nil {
|
|
c.editUnsub()
|
|
c.editUnsub = nil
|
|
}
|
|
if c.unrec != nil {
|
|
c.unrec()
|
|
c.unrec = nil
|
|
}
|
|
if c.detach != nil {
|
|
c.detach()
|
|
c.detach = nil
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Pause() {
|
|
switch c.state {
|
|
case Playing:
|
|
c.state = Paused
|
|
case Paused:
|
|
c.state = Playing
|
|
default:
|
|
return
|
|
}
|
|
c.held = c.state == Paused
|
|
c.world.Time.SetPaused(c.held)
|
|
c.notify()
|
|
}
|
|
|
|
func (c *Controller) Step() {
|
|
if c.state == Paused {
|
|
c.world.Time.StepFrame()
|
|
c.stepReq = stepFrame
|
|
}
|
|
}
|
|
|
|
func (c *Controller) StepFixed() {
|
|
if c.state == Paused {
|
|
c.world.Time.StepFixed()
|
|
c.stepReq = stepFixed
|
|
}
|
|
}
|
|
|
|
func (c *Controller) SetBeforeStop(fn func()) {
|
|
c.beforeStop = fn
|
|
}
|
|
|
|
func (c *Controller) Stop() {
|
|
if c.state == Stopped {
|
|
return
|
|
}
|
|
if c.beforeStop != nil {
|
|
c.beforeStop()
|
|
}
|
|
if real := c.ctx.Docs.Get(c.doc); real != nil && c.proxy != nil {
|
|
real.View.Camera = c.proxy.View.Camera
|
|
real.View.Mode = c.proxy.View.Mode
|
|
real.View.Orbit = c.proxy.View.Orbit
|
|
}
|
|
c.teardown()
|
|
c.ctx.Docs.ClearPlay()
|
|
c.proxy = nil
|
|
c.state, c.held = Stopped, false
|
|
c.dropKeys(-1)
|
|
c.kf, c.kfOff, c.spare, c.floor, c.rebase = c.kf[:0], "", nil, 0, false
|
|
c.world, c.runner, c.scene = nil, nil, nil
|
|
c.doc = 0
|
|
c.captured = false
|
|
c.input = input.State{}
|
|
c.stats = Stats{}
|
|
c.hist, c.cursor, c.beyond, c.stepReq = nil, 0, false, stepNone
|
|
c.notify()
|
|
c.ctx.Notify(context.LevelInfo, "Play mode stopped")
|
|
}
|
|
|
|
func (c *Controller) CanCopyToEdit(nodes []ids.NodeID, t schema.TypeID) bool {
|
|
if c.state == Stopped || c.scene == nil || len(nodes) == 0 {
|
|
return false
|
|
}
|
|
real := c.ctx.Docs.Get(c.doc)
|
|
if real == nil {
|
|
return false
|
|
}
|
|
es := real.Scene()
|
|
for _, n := range nodes {
|
|
if eh := es.Handle(n); eh.IsZero() || !es.IsAlive(eh) {
|
|
return false
|
|
}
|
|
ph := c.scene.Handle(n)
|
|
if ph.IsZero() {
|
|
return false
|
|
}
|
|
if _, ok := c.scene.GetByType(ph, t); !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
|
|
if !c.CanCopyToEdit(nodes, t) {
|
|
return errs.New(errs.EditorCommand, "these nodes do not exist in the edit scene")
|
|
}
|
|
real := c.ctx.Docs.Get(c.doc)
|
|
es := real.Scene()
|
|
td, ok := es.Registry().ByID(t)
|
|
if !ok {
|
|
return errs.New(errs.SchemaUnknownType, "component type is not registered")
|
|
}
|
|
env := commands.EnvFor(es)
|
|
tx := real.History.Begin("Copy " + td.FullName + " from Play")
|
|
for _, n := range nodes {
|
|
if _, has := es.GetByType(es.Handle(n), t); !has {
|
|
values := make([]commands.FieldValue, 0, len(td.Fields))
|
|
for i := range td.Fields {
|
|
v, err := c.proxy.Props.Get(n, t, td.Fields[i].ID)
|
|
if err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
values = append(values, commands.FieldValue{Field: td.Fields[i].ID, Value: v})
|
|
}
|
|
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(es, n, td, values))); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
for i := range td.Fields {
|
|
fd := &td.Fields[i]
|
|
if fd.Flags.Has(schema.FlagReadOnly) {
|
|
continue
|
|
}
|
|
v, err := c.proxy.Props.Get(n, t, fd.ID)
|
|
if err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
if err := real.Props.Set(tx, []ids.NodeID{n}, t, fd.ID, v); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
tx.Commit()
|
|
c.ctx.Notify(context.LevelInfo, "Copied "+td.FullName+" to the edit scene")
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) SetCapture(on bool, origin, size mathx.Vec2) {
|
|
if c.state == Stopped {
|
|
on = false
|
|
}
|
|
c.captured = on
|
|
c.origin, c.size = origin, size
|
|
if !on {
|
|
c.input = input.State{}
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Captured() bool {
|
|
return c.captured
|
|
}
|
|
|
|
func (c *Controller) syncInput() {
|
|
in := c.ctx.Engine.Input()
|
|
if !c.captured {
|
|
c.input = input.State{}
|
|
c.input.CopyPads(in)
|
|
return
|
|
}
|
|
f := in.Snapshot()
|
|
pos := f.MousePos.Sub(c.origin)
|
|
outside := pos.X < 0 || pos.Y < 0 || pos.X >= c.size.X || pos.Y >= c.size.Y
|
|
click := in.ButtonPressed(input.MouseLeft) || in.ButtonPressed(input.MouseRight) || in.ButtonPressed(input.MouseMiddle)
|
|
if c.ReleasePressed(in) || (outside && click) {
|
|
c.captured = false
|
|
c.input = input.State{}
|
|
c.input.CopyPads(in)
|
|
return
|
|
}
|
|
f.MousePos = pos
|
|
c.input.Restore(f)
|
|
c.input.CopyPads(in)
|
|
}
|
|
|
|
func (c *Controller) guard(stage string) {
|
|
if rec := recover(); rec != nil {
|
|
if c.ctx.Crash != nil {
|
|
c.ctx.Crash("play "+stage, rec, debug.Stack())
|
|
}
|
|
c.ctx.Notify(context.LevelError, fmt.Sprintf("play mode stopped, %s panicked: %v", stage, rec))
|
|
c.ctx.Log.Error("gameplay panic", log.F("stage", stage), log.F("panic", fmt.Sprint(rec)))
|
|
c.Stop()
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Tick(dt float64) {
|
|
if c.state == Stopped {
|
|
return
|
|
}
|
|
if c.doc != 0 && c.ctx.Docs.Get(c.doc) == nil {
|
|
c.Stop()
|
|
return
|
|
}
|
|
defer c.guard("update")
|
|
c.syncInput()
|
|
c.applyRebase()
|
|
h := c.hist
|
|
if c.stepReq == stepNone && (c.held || (h != nil && c.cursor < len(h.frames) && c.state == Paused)) {
|
|
if f, ok := engine.ServiceOf[persistence.Flusher](c.world, persistence.ServiceID); ok {
|
|
f.Flush()
|
|
}
|
|
return
|
|
}
|
|
w := c.world
|
|
recorded := h != nil && c.record(dt)
|
|
c.breaks, c.broke = 0, 0
|
|
start := time.Now()
|
|
plan := c.advance(dt)
|
|
if c.state == Stopped {
|
|
return
|
|
}
|
|
if recorded {
|
|
f := &h.frames[len(h.frames)-1]
|
|
f.tick, f.brk = w.Time.Tick(), c.broke
|
|
if c.keysOn() && w.Time.Tick() >= c.lastTick+c.every() {
|
|
c.capture(c.cursor)
|
|
}
|
|
}
|
|
c.stats.Update = 0
|
|
if plan.Update || plan.Steps > 0 {
|
|
c.stats.Update = time.Since(start).Seconds()
|
|
}
|
|
t := w.Time
|
|
c.stats.Frame, c.stats.Updates, c.stats.Tick, c.stats.Alpha = t.Frame(), t.Updates(), t.Tick(), t.Alpha()
|
|
c.stats.Scale, c.stats.DroppedFixed = t.Scale(), t.DroppedSteps()
|
|
if c.scene != nil {
|
|
c.stats.Nodes = c.scene.Stats().Alive - 1
|
|
}
|
|
if c.broke > 0 {
|
|
c.held = true
|
|
}
|
|
if paused := t.Paused(); paused != (c.state == Paused) {
|
|
c.held = c.held && paused
|
|
c.state = Playing
|
|
if paused {
|
|
c.state = Paused
|
|
}
|
|
c.notify()
|
|
}
|
|
if c.breakWhy != "" {
|
|
c.ctx.Notify(context.LevelWarn, "Paused on tick "+strconv.FormatUint(t.Tick(), 10)+": "+c.breakWhy)
|
|
c.breakWhy = ""
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Render(t *rt.Target) (cameras bool) {
|
|
if c.state == Stopped || c.runner == nil || t == nil {
|
|
return false
|
|
}
|
|
defer c.guard("render")
|
|
start := time.Now()
|
|
c.runner.SetTarget(t.Texture, t.BucketW, t.BucketH)
|
|
c.runner.SetDepthTarget(t.Depth)
|
|
c.runner.SetViewportSize(t.W, t.H)
|
|
c.runner.ClearFixedView()
|
|
e := c.ctx.Engine
|
|
alpha := c.world.Time.Alpha()
|
|
e.RunPhase(c.world, engine.PhaseRenderPrepare, engine.ScopePlay, 0, alpha)
|
|
e.RunPhase(c.world, engine.PhaseRender, engine.ScopePlay, 0, alpha)
|
|
e.RunPhase(c.world, engine.PhaseRenderUI, engine.ScopePlay, 0, alpha)
|
|
rs := c.runner.Stats()
|
|
c.stats.Render = time.Since(start).Seconds()
|
|
c.stats.Sprites = rs.Sprites + rs.Sprites3D + rs.Meshes
|
|
c.stats.Views = rs.Views
|
|
return rs.Views > 0
|
|
}
|