diff --git a/README.md b/README.md index 1479f39..07f67c1 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,10 @@ func (s *MyScene) Update() error { } ``` +## Acknowledgments + +- When switching scenes (i.e. calling `SwitchTo`, `SwitchWithTransition` or `ProcessTrigger`) while a transition is running it will immediately be canceled and the new switch will be started. To prevent this behavior use a TransitionAwareScene and prevent this methods to be called. + ## Contribution Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you would like to contribute code, please fork the repository and submit a pull request. diff --git a/director.go b/director.go index 1113868..166ea90 100644 --- a/director.go +++ b/director.go @@ -24,6 +24,11 @@ func NewSceneDirector[T any](scene Scene[T], state T, RuleSet map[Scene[T]][]Dir // ProcessTrigger finds if a transition should be triggered func (d *SceneDirector[T]) ProcessTrigger(trigger SceneTransitionTrigger) { + if prevTransition, ok := d.current.(SceneTransition[T]); ok { + // previous transition is still running, end it to process trigger + prevTransition.End() + } + for _, directive := range d.RuleSet[d.current.(Scene[T])] { if directive.Trigger == trigger { if directive.Transition != nil { @@ -50,11 +55,11 @@ func (d *SceneDirector[T]) ProcessTrigger(trigger SceneTransitionTrigger) { } } -func (d *SceneDirector[T]) ReturnFromTransition(scene, orgin Scene[T]) { +func (d *SceneDirector[T]) ReturnFromTransition(scene, origin Scene[T]) { if c, ok := scene.(TransitionAwareScene[T]); ok { - c.PostTransition(orgin.Unload(), orgin) + c.PostTransition(origin.Unload(), origin) } else { - scene.Load(orgin.Unload(), d) + scene.Load(origin.Unload(), d) } d.current = scene } diff --git a/director_test.go b/director_test.go index f91d3bb..a131ca8 100644 --- a/director_test.go +++ b/director_test.go @@ -78,3 +78,48 @@ func TestSceneDirector_ProcessTriggerWithTransitionAwareness(t *testing.T) { rule.Transition.End() assert.Equal(t, rule.Dest, director.current) } + +func TestSceneDirector_ProcessTriggerCancelling(t *testing.T) { + mockScene := &MockScene{} + mockTransition := &baseTransitionImplementation{} + ruleSet := make(map[Scene[int]][]Directive[int]) + + director := NewSceneDirector[int](mockScene, 1, ruleSet) + + rule := Directive[int]{Dest: &MockScene{}, Trigger: 2, Transition: mockTransition} + ruleSet[mockScene] = []Directive[int]{rule} + director.ProcessTrigger(2) + + // Assert transition is running + assert.Equal(t, rule.Transition, director.current) + + director.ProcessTrigger(1) + assert.Equal(t, rule.Dest, director.current) +} + +func TestSceneDirector_ProcessTriggerCancellingToNewTransition(t *testing.T) { + mockSceneA := &MockScene{} + mockSceneB := &MockScene{} + mockTransitionA := &baseTransitionImplementation{} + mockTransitionB := &baseTransitionImplementation{} + ruleSet := make(map[Scene[int]][]Directive[int]) + + director := NewSceneDirector[int](mockSceneA, 1, ruleSet) + + ruleSet[mockSceneA] = []Directive[int]{ + Directive[int]{Dest: mockSceneB, Trigger: 2, Transition: mockTransitionA}, + } + ruleSet[mockSceneB] = []Directive[int]{ + Directive[int]{Dest: mockSceneA, Trigger: 2, Transition: mockTransitionB}, + } + director.ProcessTrigger(2) + + // Assert transition is running + assert.Equal(t, mockTransitionA, director.current) + + director.ProcessTrigger(2) + assert.Equal(t, mockTransitionB, director.current) + + mockTransitionB.End() + assert.Equal(t, mockSceneA, director.current) +} diff --git a/examples/director/main.go b/examples/director/main.go index 294086d..d0670eb 100644 --- a/examples/director/main.go +++ b/examples/director/main.go @@ -26,6 +26,7 @@ const ( type BaseScene struct { bounds image.Rectangle count State + active bool sm *stagehand.SceneDirector[State] } @@ -36,10 +37,12 @@ func (s *BaseScene) Layout(w, h int) (int, int) { func (s *BaseScene) Load(st State, sm stagehand.SceneController[State]) { s.count = st + s.active = true s.sm = sm.(*stagehand.SceneDirector[State]) } func (s *BaseScene) Unload() State { + s.active = false return s.count } @@ -51,7 +54,7 @@ func (s *FirstScene) Update() error { if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { s.count++ } - if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) && s.active { s.sm.ProcessTrigger(Trigger) } return nil @@ -59,7 +62,7 @@ func (s *FirstScene) Update() error { func (s *FirstScene) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{255, 0, 0, 255}) // Fill Red - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s", s.count, s.bounds.Max), s.bounds.Dx()/2, s.bounds.Dy()/2) + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s\nActive? %v", s.count, s.bounds.Max, s.active), s.bounds.Dx()/2, s.bounds.Dy()/2) } type SecondScene struct { @@ -70,7 +73,7 @@ func (s *SecondScene) Update() error { if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { s.count-- } - if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) && s.active { s.sm.ProcessTrigger(Trigger) } return nil @@ -78,7 +81,26 @@ func (s *SecondScene) Update() error { func (s *SecondScene) Draw(screen *ebiten.Image) { screen.Fill(color.RGBA{0, 0, 255, 255}) // Fill Blue - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s", s.count, s.bounds.Max), s.bounds.Dx()/2, s.bounds.Dy()/2) + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s\nActive? %v", s.count, s.bounds.Max, s.active), s.bounds.Dx()/2, s.bounds.Dy()/2) +} + +type ThirdScene struct { + BaseScene +} + +func (s *ThirdScene) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + s.count++ + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) && s.active { + s.sm.ProcessTrigger(Trigger) + } + return nil +} + +func (s *ThirdScene) Draw(screen *ebiten.Image) { + screen.Fill(color.RGBA{0, 255, 0, 255}) // Fill Green + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s\nActive? %v", s.count, s.bounds.Max, s.active), s.bounds.Dx()/2, s.bounds.Dy()/2) } func main() { @@ -90,16 +112,30 @@ func main() { s1 := &FirstScene{} s2 := &SecondScene{} - trans := stagehand.NewSlideTransition[State](stagehand.BottomToTop, 0.05) + s3 := &ThirdScene{} + trans := stagehand.NewSlideTransition[State](stagehand.BottomToTop, 0.02) + trans2 := stagehand.NewSlideTransition[State](stagehand.TopToBottom, 0.02) + trans3 := stagehand.NewSlideTransition[State](stagehand.LeftToRight, 0.02) rs := map[stagehand.Scene[State]][]stagehand.Directive[State]{ - s1: []stagehand.Directive[State]{ - stagehand.Directive[State]{Dest: s2, Trigger: Trigger}, + s1: { + stagehand.Directive[State]{ + Dest: s2, + Trigger: Trigger, + Transition: trans, + }, }, - s2: []stagehand.Directive[State]{ + s2: { + stagehand.Directive[State]{ + Dest: s3, + Trigger: Trigger, + Transition: trans2, + }, + }, + s3: { stagehand.Directive[State]{ Dest: s1, Trigger: Trigger, - Transition: trans, + Transition: trans3, }, }, } diff --git a/manager.go b/manager.go index b2f9bea..f6df33e 100644 --- a/manager.go +++ b/manager.go @@ -14,6 +14,10 @@ func NewSceneManager[T any](scene Scene[T], state T) *SceneManager[T] { // Scene Switching func (s *SceneManager[T]) SwitchTo(scene Scene[T]) { + if prevTransition, ok := s.current.(SceneTransition[T]); ok { + // previous transition is still running, end it first + prevTransition.End() + } if c, ok := s.current.(Scene[T]); ok { scene.Load(c.Unload(), s) s.current = scene @@ -21,6 +25,10 @@ func (s *SceneManager[T]) SwitchTo(scene Scene[T]) { } func (s *SceneManager[T]) SwitchWithTransition(scene Scene[T], transition SceneTransition[T]) { + if prevTransition, ok := s.current.(SceneTransition[T]); ok { + // previous transition is still running, end it to start the next transition + prevTransition.End() + } sc := s.current.(Scene[T]) transition.Start(sc, scene, s) if c, ok := sc.(TransitionAwareScene[T]); ok { @@ -31,11 +39,11 @@ func (s *SceneManager[T]) SwitchWithTransition(scene Scene[T], transition SceneT s.current = transition } -func (s *SceneManager[T]) ReturnFromTransition(scene, orgin Scene[T]) { +func (s *SceneManager[T]) ReturnFromTransition(scene, origin Scene[T]) { if c, ok := scene.(TransitionAwareScene[T]); ok { - c.PostTransition(orgin.Unload(), orgin) + c.PostTransition(origin.Unload(), origin) } else { - scene.Load(orgin.Unload(), s) + scene.Load(origin.Unload(), s) } s.current = scene } diff --git a/transition.go b/transition.go index 01181ff..8ff2abc 100644 --- a/transition.go +++ b/transition.go @@ -50,7 +50,7 @@ func (t *BaseTransition[T]) Layout(outsideWidth, outsideHeight int) (int, int) { // Ends transition to the next scene func (t *BaseTransition[T]) End() { - t.sm.ReturnFromTransition(t.toScene.(Scene[T]), t.fromScene.(Scene[T])) + t.sm.ReturnFromTransition(t.toScene, t.fromScene) } type FadeTransition[T any] struct { diff --git a/transition_test.go b/transition_test.go index 3c32e71..872dfc6 100644 --- a/transition_test.go +++ b/transition_test.go @@ -104,6 +104,35 @@ func TestBaseTransition_Awareness(t *testing.T) { assert.True(t, to.postTransitionCalled) } +func TestBaseTransition_SwitchCanceling(t *testing.T) { + from := &MockScene{} + to := &MockScene{} + trans := &baseTransitionImplementation{} + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) + + // Assert transition is running + assert.Equal(t, trans, sm.current) + + sm.SwitchTo(to) + assert.Equal(t, to, sm.current) +} + +func TestBaseTransition_TransitionCanceling(t *testing.T) { + from := &MockScene{} + to := &MockScene{} + transA := &baseTransitionImplementation{} + transB := &baseTransitionImplementation{} + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, transA) + + // Assert transition is running + assert.Equal(t, transA, sm.current) + + sm.SwitchWithTransition(to, transB) + assert.Equal(t, transB, sm.current) +} + func TestFadeTransition_UpdateOncePerFrame(t *testing.T) { var value float32 = .6 from := &MockScene{}