diff --git a/README.md b/README.md index 58787e3..1479f39 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ type MyState struct { type MyScene struct { // your scene fields + sm *stagehand.SceneManager[MyState] } func (s *MyScene) Update() error { @@ -44,8 +45,9 @@ func (s *MyScene) Draw(screen *ebiten.Image) { // your draw code } -func (s *MyScene) Load(state MyState ,manager *stagehand.SceneManager) { +func (s *MyScene) Load(state MyState ,manager stagehand.SceneController[MyState]) { // your load code + s.sm = manager.(*stagehand.SceneManager[MyState]) // This type assertion is important } func (s *MyScene) Unload() MyState { @@ -63,11 +65,20 @@ func main() { manager := stagehand.NewSceneManager[MyState](scene1, state) if err := ebiten.RunGame(sm); err != nil { - log.Fatal(err) - } + log.Fatal(err) + } } ``` +### Examples + +We provide some example code so you can start fast: + +- [Simple Example](https://github.com/joelschutz/stagehand/blob/master/examples/simple/main.go) +- [Timed Transition Example](https://github.com/joelschutz/stagehand/blob/master/examples/timed/main.go) +- [Transition Awareness Example](https://github.com/joelschutz/stagehand/blob/master/examples/aware/main.go) +- [Scene Director Example](https://github.com/joelschutz/stagehand/blob/master/examples/director/main.go) + ## Transitions You can switch scenes by calling `SwitchTo` method on the `SceneManager` giving the scene instance you wanna switch to. @@ -92,13 +103,13 @@ The `FadeTransition` will fade out the current scene while fading in the new sce func (s *MyScene) Update() error { // ... scene2 := &OtherScene{} - s.manager.SwitchWithTransition(scene2. stagehand.NewFadeTransition(.05)) + s.manager.SwitchWithTransition(scene2, stagehand.NewFadeTransition(.05)) // ... } ``` -In this example, the `FadeTransition` will fade 5% every frame. +In this example, the `FadeTransition` will fade 5% every frame. There is also the option for a timed transition using `NewTicksTimedFadeTransition`(for a ticks based timming) or `NewDurationTimedFadeTransition`(for a real-time based timming). ### Slide Transition @@ -108,13 +119,13 @@ The `SlideTransition` will slide out the current scene and slide in the new scen func (s *MyScene) Update() error { // ... scene2 := &OtherScene{} - s.manager.SwitchWithTransition(scene2. stagehand.NewSlideTransition(stagehand.LeftToRight, .05)) + s.manager.SwitchWithTransition(scene2, stagehand.NewSlideTransition(stagehand.LeftToRight, .05)) // ... } ``` -In this example, the `SlideTransition` will slide in the new scene from the left 5% every frame. +In this example, the `SlideTransition` will slide in the new scene from the left 5% every frame. There is also the option for a timed transition using `NewTicksTimedSlideTransition`(for a ticks based timming) or `NewDurationTimedSlideTransition`(for a real-time based timming). ### Custom Transitions @@ -123,30 +134,117 @@ You can also define your own transition, simply implement the `SceneTransition` ```go type MyTransition struct { stagehand.BaseTransition - progress float64 // An example factor + progress float64 // An example factor } -func (t *MyTransition) Start(from, to stagehand.Scene[T]) { +func (t *MyTransition) Start(from, to stagehand.Scene[MyState], sm *SceneManager[MyState]) { // Start the transition from the "from" scene to the "to" scene here - t.BaseTransition.Start(fromScene, toScene) + t.BaseTransition.Start(fromScene, toScene, sm) t.progress = 0 } func (t *MyTransition) Update() error { - // Update the progress of the transition + // Update the progress of the transition t.progress += 0.01 - return t.BaseTransition.Update() + return t.BaseTransition.Update() } func (t *MyTransition) Draw(screen *ebiten.Image) { - // Optionally you can use a helper function to render each scene frame - toImg, fromImg := stagehand.PreDraw(screen.Bounds(), t.fromScene, t.toScene) + // Optionally you can use a helper function to render each scene frame + toImg, fromImg := stagehand.PreDraw(screen.Bounds(), t.fromScene, t.toScene) // Draw transition effect here } ``` +### Transition Awareness + +When a scene is transitioned, the `Load` and `Unload` methods are called **twice** for the destination and original scenes respectively. Once at the start and again at the end of the transition. This behavior can be changed for additional control by implementing the `TransitionAwareScene` interface. + +```go +func (s *MyScene) PreTransition(destination Scene[MyState]) MyState { + // Runs before new scene is loaded +} + +func (s *MyScene) PostTransition(lastState MyState, original Scene[MyState]) { + // Runs when old scene is unloaded +} +``` + +With this you can insure that those methods are only called once on transitions and can control your scenes at each point of the transition. The execution order will be: + +```shell +PreTransition Called on old scene +Load Called on new scene +Updated old scene +Updated new scene +... +Updated old scene +Updated new scene +Unload Called on old scene +PostTransition Called on new scene +``` + +## SceneDirector + +The `SceneDirector` is an alternative way to manage the transitions between scenes. It provides transitioning between scenes based on a set of rules just like a FSM. The `Scene` implementation is the same, with only a feel differences, first you need to assert the `SceneDirector` instead of the `SceneManager`: + +```go +type MyScene struct { + // your scene fields + director *stagehand.SceneDirector[MyState] +} + +func (s *MyScene) Load(state MyState ,director stagehand.SceneController[MyState]) { + // your load code + s.director = director.(*stagehand.SceneDirector[MyState]) // This type assertion is important +} +``` + +Then define a ruleSet of `Directive` and `SceneTransitionTrigger` for the game. + +```go +// Triggers are int type underneath +const ( + Trigger1 stagehand.SceneTransitionTrigger = iota + Trigger2 +) + +func main() { + // ... + scene1 := &MyScene{} + scene2 := &OtherScene{} + + // Create a rule set for transitioning between scenes based on Triggers + ruleSet := make(map[stagehand.Scene[MyState]][]Directive[MyState]) + directive1 := Directive[MyState]{Dest: scene2, Trigger: Trigger1} + directive2 := Directive[MyState]{Dest: scene1, Trigger: Trigger2, Transition: stagehand.NewFadeTransition(.05)} // Add transitions inside the directive + + // Directives are mapped to each Scene pointer and can be shared + ruleSet[scene1] = []Directive[MyState]{directive1, directive2} + ruleSet[scene2] = []Directive[MyState]{directive2} + + state := MyState{} + manager := stagehand.NewSceneDirector[MyState](scene1, state, ruleSet) + + if err := ebiten.RunGame(sm); err != nil { + log.Fatal(err) + } +} +``` + +Now you can now notify the `SceneDirector` about activated `SceneTransitionTrigger`, if no `Directive` match, the code will still run without errors. + +```go +func (s *MyScene) Update() error { + // ... + s.manager.ProcessTrigger(Trigger) + + // ... +} +``` + ## 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. @@ -159,4 +257,4 @@ go test ./... ## License -Stagehand is released under the [MIT License](https://github.com/example/stagehand/blob/master/LICENSE). +Stagehand is released under the [MIT License](https://github.com/joelschutz/stagehand/blob/master/LICENSE). diff --git a/director.go b/director.go new file mode 100644 index 0000000..1113868 --- /dev/null +++ b/director.go @@ -0,0 +1,60 @@ +package stagehand + +type SceneTransitionTrigger int + +// A Directive is a struct that represents how a scene should be transitioned +type Directive[T any] struct { + Dest Scene[T] + Transition SceneTransition[T] + Trigger SceneTransitionTrigger +} + +// A SceneDirector is a struct that manages the transitions between scenes +type SceneDirector[T any] struct { + SceneManager[T] + RuleSet map[Scene[T]][]Directive[T] +} + +func NewSceneDirector[T any](scene Scene[T], state T, RuleSet map[Scene[T]][]Directive[T]) *SceneDirector[T] { + s := &SceneDirector[T]{RuleSet: RuleSet} + s.current = scene + scene.Load(state, s) + return s +} + +// ProcessTrigger finds if a transition should be triggered +func (d *SceneDirector[T]) ProcessTrigger(trigger SceneTransitionTrigger) { + for _, directive := range d.RuleSet[d.current.(Scene[T])] { + if directive.Trigger == trigger { + if directive.Transition != nil { + // With transition + // Equivalent to SwitchWithTransition + sc := d.current.(Scene[T]) + directive.Transition.Start(sc, directive.Dest, d) + if c, ok := sc.(TransitionAwareScene[T]); ok { + directive.Dest.Load(c.PreTransition(directive.Dest), d) + } else { + directive.Dest.Load(sc.Unload(), d) + } + d.current = directive.Transition + } else { + // No transition + // Equivalent to SwitchTo + if c, ok := d.current.(Scene[T]); ok { + directive.Dest.Load(c.Unload(), d) + d.current = directive.Dest + } + } + + } + } +} + +func (d *SceneDirector[T]) ReturnFromTransition(scene, orgin Scene[T]) { + if c, ok := scene.(TransitionAwareScene[T]); ok { + c.PostTransition(orgin.Unload(), orgin) + } else { + scene.Load(orgin.Unload(), d) + } + d.current = scene +} diff --git a/director_test.go b/director_test.go new file mode 100644 index 0000000..f91d3bb --- /dev/null +++ b/director_test.go @@ -0,0 +1,80 @@ +package stagehand + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSceneDirector_NewSceneDirector(t *testing.T) { + mockScene := &MockScene{} + ruleSet := make(map[Scene[int]][]Directive[int]) + + director := NewSceneDirector[int](mockScene, 1, ruleSet) + + assert.NotNil(t, director) + assert.Equal(t, mockScene, director.current) +} + +func TestSceneDirector_ProcessTrigger(t *testing.T) { + mockScene := &MockScene{} + mockScene2 := &MockScene{} + ruleSet := make(map[Scene[int]][]Directive[int]) + + director := NewSceneDirector[int](mockScene, 1, ruleSet) + + rule := Directive[int]{Dest: mockScene2, Trigger: 2} + ruleSet[mockScene] = []Directive[int]{rule} + + // Call the ProcessTrigger method with wrong trigger + director.ProcessTrigger(1) + assert.NotEqual(t, rule.Dest, director.current) + + // Call the ProcessTrigger method with correct trigger + director.ProcessTrigger(2) + assert.Equal(t, rule.Dest, director.current) +} + +func TestSceneDirector_ProcessTriggerWithTransition(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} + + // Call the ProcessTrigger method with wrong trigger + director.ProcessTrigger(1) + assert.NotEqual(t, rule.Transition, director.current) + + // Call the ProcessTrigger method with correct trigger + director.ProcessTrigger(2) + assert.Equal(t, rule.Transition, director.current) + + rule.Transition.End() + assert.Equal(t, rule.Dest, director.current) +} + +func TestSceneDirector_ProcessTriggerWithTransitionAwareness(t *testing.T) { + mockScene := &MockTransitionAwareScene{} + mockTransition := &baseTransitionImplementation{} + ruleSet := make(map[Scene[int]][]Directive[int]) + + director := NewSceneDirector[int](mockScene, 1, ruleSet) + + rule := Directive[int]{Dest: &MockTransitionAwareScene{}, Trigger: 2, Transition: mockTransition} + ruleSet[mockScene] = []Directive[int]{rule} + + // Call the ProcessTrigger method with wrong trigger + director.ProcessTrigger(1) + assert.NotEqual(t, rule.Transition, director.current) + + // Call the ProcessTrigger method with correct trigger + director.ProcessTrigger(2) + assert.Equal(t, rule.Transition, director.current) + + rule.Transition.End() + assert.Equal(t, rule.Dest, director.current) +} diff --git a/examples/aware/main.go b/examples/aware/main.go new file mode 100644 index 0000000..01021cf --- /dev/null +++ b/examples/aware/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/joelschutz/stagehand" +) + +const ( + screenWidth = 640 + screenHeight = 480 +) + +type State struct { + Count int + OnTransition bool +} + +type BaseScene struct { + bounds image.Rectangle + count State + sm *stagehand.SceneManager[State] +} + +func (s *BaseScene) Layout(w, h int) (int, int) { + s.bounds = image.Rect(0, 0, w, h) + return w, h +} + +func (s *BaseScene) Load(st State, sm stagehand.SceneController[State]) { + s.count = st + s.sm = sm.(*stagehand.SceneManager[State]) +} + +func (s *BaseScene) Unload() State { + return s.count +} + +func (s *BaseScene) PreTransition(toScene stagehand.Scene[State]) State { + s.count.OnTransition = true + return s.count +} + +func (s *BaseScene) PostTransition(state State, fromScene stagehand.Scene[State]) { + s.count.OnTransition = false +} + +type FirstScene struct { + BaseScene +} + +func (s *FirstScene) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + s.count.Count++ + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + s.sm.SwitchWithTransition(&SecondScene{}, stagehand.NewSlideTransition[State](stagehand.TopToBottom, .05)) + } + return nil +} + +func (s *FirstScene) Draw(screen *ebiten.Image) { + if s.count.OnTransition { + screen.Fill(color.RGBA{0, 0, 0, 255}) // Fill Black + } else { + screen.Fill(color.RGBA{255, 0, 0, 255}) // Fill Red + } + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s", s.count.Count, s.bounds.Max), s.bounds.Dx()/2, s.bounds.Dy()/2) +} + +type SecondScene struct { + BaseScene +} + +func (s *SecondScene) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + s.count.Count-- + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + s.sm.SwitchWithTransition(&FirstScene{}, stagehand.NewSlideTransition[State](stagehand.BottomToTop, .05)) + } + return nil +} + +func (s *SecondScene) Draw(screen *ebiten.Image) { + if s.count.OnTransition { + screen.Fill(color.RGBA{255, 255, 255, 255}) // Fill White + } else { + screen.Fill(color.RGBA{0, 0, 255, 255}) // Fill Blue + } + + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Count: %v, WindowSize: %s", s.count.Count, s.bounds.Max), s.bounds.Dx()/2, s.bounds.Dy()/2) +} + +func main() { + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("My Game") + ebiten.SetWindowResizable(true) + + state := State{Count: 10} + + s := &FirstScene{} + sm := stagehand.NewSceneManager[State](s, state) + + if err := ebiten.RunGame(sm); err != nil { + log.Fatal(err) + } +} diff --git a/examples/director/main.go b/examples/director/main.go new file mode 100644 index 0000000..294086d --- /dev/null +++ b/examples/director/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "image" + "image/color" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/joelschutz/stagehand" +) + +const ( + screenWidth = 640 + screenHeight = 480 +) + +type State int + +const ( + Trigger stagehand.SceneTransitionTrigger = iota +) + +type BaseScene struct { + bounds image.Rectangle + count State + sm *stagehand.SceneDirector[State] +} + +func (s *BaseScene) Layout(w, h int) (int, int) { + s.bounds = image.Rect(0, 0, w, h) + return w, h +} + +func (s *BaseScene) Load(st State, sm stagehand.SceneController[State]) { + s.count = st + s.sm = sm.(*stagehand.SceneDirector[State]) +} + +func (s *BaseScene) Unload() State { + return s.count +} + +type FirstScene struct { + BaseScene +} + +func (s *FirstScene) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + s.count++ + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + s.sm.ProcessTrigger(Trigger) + } + return nil +} + +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) +} + +type SecondScene struct { + BaseScene +} + +func (s *SecondScene) Update() error { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + s.count-- + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) { + s.sm.ProcessTrigger(Trigger) + } + return nil +} + +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) +} + +func main() { + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("My Game") + ebiten.SetWindowResizable(true) + + state := State(10) + + s1 := &FirstScene{} + s2 := &SecondScene{} + trans := stagehand.NewSlideTransition[State](stagehand.BottomToTop, 0.05) + rs := map[stagehand.Scene[State]][]stagehand.Directive[State]{ + s1: []stagehand.Directive[State]{ + stagehand.Directive[State]{Dest: s2, Trigger: Trigger}, + }, + s2: []stagehand.Directive[State]{ + stagehand.Directive[State]{ + Dest: s1, + Trigger: Trigger, + Transition: trans, + }, + }, + } + sm := stagehand.NewSceneDirector[State](s1, state, rs) + + if err := ebiten.RunGame(sm); err != nil { + log.Fatal(err) + } +} diff --git a/examples/simple/main.go b/examples/simple/main.go index 6a0f93d..3ba9645 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -30,9 +30,9 @@ func (s *BaseScene) Layout(w, h int) (int, int) { return w, h } -func (s *BaseScene) Load(st State, sm *stagehand.SceneManager[State]) { +func (s *BaseScene) Load(st State, sm stagehand.SceneController[State]) { s.count = st - s.sm = sm + s.sm = sm.(*stagehand.SceneManager[State]) } func (s *BaseScene) Unload() State { diff --git a/examples/timed/main.go b/examples/timed/main.go index 3b40e36..41919fc 100644 --- a/examples/timed/main.go +++ b/examples/timed/main.go @@ -31,9 +31,9 @@ func (s *BaseScene) Layout(w, h int) (int, int) { return w, h } -func (s *BaseScene) Load(st State, sm *stagehand.SceneManager[State]) { +func (s *BaseScene) Load(st State, sm stagehand.SceneController[State]) { s.count = st - s.sm = sm + s.sm = sm.(*stagehand.SceneManager[State]) } func (s *BaseScene) Unload() State { diff --git a/manager.go b/manager.go new file mode 100644 index 0000000..b2f9bea --- /dev/null +++ b/manager.go @@ -0,0 +1,54 @@ +package stagehand + +import ebiten "github.com/hajimehoshi/ebiten/v2" + +type SceneManager[T any] struct { + current ProtoScene[T] +} + +func NewSceneManager[T any](scene Scene[T], state T) *SceneManager[T] { + s := &SceneManager[T]{current: scene} + scene.Load(state, s) + return s +} + +// Scene Switching +func (s *SceneManager[T]) SwitchTo(scene Scene[T]) { + if c, ok := s.current.(Scene[T]); ok { + scene.Load(c.Unload(), s) + s.current = scene + } +} + +func (s *SceneManager[T]) SwitchWithTransition(scene Scene[T], transition SceneTransition[T]) { + sc := s.current.(Scene[T]) + transition.Start(sc, scene, s) + if c, ok := sc.(TransitionAwareScene[T]); ok { + scene.Load(c.PreTransition(scene), s) + } else { + scene.Load(sc.Unload(), s) + } + s.current = transition +} + +func (s *SceneManager[T]) ReturnFromTransition(scene, orgin Scene[T]) { + if c, ok := scene.(TransitionAwareScene[T]); ok { + c.PostTransition(orgin.Unload(), orgin) + } else { + scene.Load(orgin.Unload(), s) + } + s.current = scene +} + +// Ebiten Interface +func (s *SceneManager[T]) Update() error { + return s.current.Update() +} + +func (s *SceneManager[T]) Draw(screen *ebiten.Image) { + s.current.Draw(screen) +} + +func (s *SceneManager[T]) Layout(w, h int) (int, int) { + return s.current.Layout(w, h) +} diff --git a/scene.go b/scene.go index a2bdf1c..3c617f4 100644 --- a/scene.go +++ b/scene.go @@ -4,42 +4,23 @@ import ( ebiten "github.com/hajimehoshi/ebiten/v2" ) -type Scene[T any] interface { +type ProtoScene[T any] interface { ebiten.Game - Load(T, *SceneManager[T]) - Unload() T -} - -type SceneManager[T any] struct { - current Scene[T] -} - -func NewSceneManager[T any](scene Scene[T], state T) *SceneManager[T] { - s := &SceneManager[T]{current: scene} - scene.Load(state, s) - return s } -// Scene Switching -func (s *SceneManager[T]) SwitchTo(scene Scene[T]) { - scene.Load(s.current.Unload(), s) - s.current = scene +type SceneController[T any] interface { + // *SceneManager[T] | *SceneDirector[T] + ReturnFromTransition(scene, orgin Scene[T]) } -func (s *SceneManager[T]) SwitchWithTransition(scene Scene[T], transition SceneTransition[T]) { - transition.Start(s.current, scene) - s.SwitchTo(transition) -} - -// Ebiten Interface -func (s *SceneManager[T]) Update() error { - return s.current.Update() -} - -func (s *SceneManager[T]) Draw(screen *ebiten.Image) { - s.current.Draw(screen) +type Scene[T any] interface { + ProtoScene[T] + Load(T, SceneController[T]) // Runs when scene is first started, must keep state and SceneManager + Unload() T // Runs when scene is discarted, must return last state } -func (s *SceneManager[T]) Layout(w, h int) (int, int) { - return s.current.Layout(w, h) +type TransitionAwareScene[T any] interface { + Scene[T] + PreTransition(Scene[T]) T // Runs before new scene is loaded, must return last state + PostTransition(T, Scene[T]) // Runs when old scene is unloaded } diff --git a/scene_test.go b/scene_test.go index 233572e..4f95abc 100644 --- a/scene_test.go +++ b/scene_test.go @@ -16,7 +16,7 @@ type MockScene struct { unloadReturns int } -func (m *MockScene) Load(state int, sm *SceneManager[int]) { +func (m *MockScene) Load(state int, sm SceneController[int]) { m.loadCalled = true m.unloadReturns = state } @@ -44,12 +44,13 @@ type MockTransition[T any] struct { fromScene Scene[T] toScene Scene[T] startCalled bool - state T } -func NewMockTransition[T any]() *MockTransition[T] { return &MockTransition[T]{} } +func NewMockTransition[T any]() *MockTransition[T] { + return &MockTransition[T]{} +} -func (t *MockTransition[T]) Start(fromScene, toScene Scene[T]) { +func (t *MockTransition[T]) Start(fromScene, toScene Scene[T], sm SceneController[T]) { t.fromScene = fromScene t.toScene = toScene t.startCalled = true @@ -61,10 +62,6 @@ func (t *MockTransition[T]) Update() error { return nil } func (t *MockTransition[T]) Draw(screen *ebiten.Image) {} -func (t *MockTransition[T]) Load(state T, sm *SceneManager[T]) { t.state = state } - -func (t *MockTransition[T]) Unload() T { return t.state } - func (t *MockTransition[T]) Layout(w, h int) (int, int) { return w, h } func TestSceneManager_SwitchTo(t *testing.T) { @@ -113,10 +110,12 @@ func TestSceneManager_Layout(t *testing.T) { } func TestSceneManager_Load_Unload(t *testing.T) { - sm := NewSceneManager[int](&MockScene{}, 42) - mockScene := &MockScene{} - sm.SwitchTo(mockScene) - unloaded := sm.current.Unload() - assert.True(t, mockScene.unloadCalled) - assert.Equal(t, 42, unloaded) + from := &MockScene{} + to := &MockScene{} + sm := NewSceneManager[int](from, 42) + sm.SwitchTo(to) + + assert.True(t, to.loadCalled) + assert.True(t, from.unloadCalled) + assert.Equal(t, 42, sm.current.(Scene[int]).Unload()) } diff --git a/transition.go b/transition.go index 8d1d9a9..01181ff 100644 --- a/transition.go +++ b/transition.go @@ -7,20 +7,21 @@ import ( ) type SceneTransition[T any] interface { - Scene[T] - Start(fromScene, toScene Scene[T]) + ProtoScene[T] + Start(fromScene, toScene Scene[T], sm SceneController[T]) End() } type BaseTransition[T any] struct { fromScene Scene[T] toScene Scene[T] - sm *SceneManager[T] + sm SceneController[T] } -func (t *BaseTransition[T]) Start(fromScene, toScene Scene[T]) { +func (t *BaseTransition[T]) Start(fromScene, toScene Scene[T], sm SceneController[T]) { t.fromScene = fromScene t.toScene = toScene + t.sm = sm } // Update updates the transition state @@ -47,20 +48,9 @@ func (t *BaseTransition[T]) Layout(outsideWidth, outsideHeight int) (int, int) { return MaxInt(sw, tw), MaxInt(sh, th) } -// Loads the next scene -func (t *BaseTransition[T]) Load(state T, manager *SceneManager[T]) { - t.sm = manager - t.toScene.Load(state, manager) -} - -// Unloads the last scene -func (t *BaseTransition[T]) Unload() T { - return t.fromScene.Unload() -} - // Ends transition to the next scene func (t *BaseTransition[T]) End() { - t.sm.SwitchTo(t.toScene) + t.sm.ReturnFromTransition(t.toScene.(Scene[T]), t.fromScene.(Scene[T])) } type FadeTransition[T any] struct { @@ -78,8 +68,8 @@ func NewFadeTransition[T any](factor float32) *FadeTransition[T] { } // Start starts the transition from the given "from" scene to the given "to" scene -func (t *FadeTransition[T]) Start(fromScene, toScene Scene[T]) { - t.BaseTransition.Start(fromScene, toScene) +func (t *FadeTransition[T]) Start(fromScene, toScene Scene[T], sm SceneController[T]) { + t.BaseTransition.Start(fromScene, toScene, sm) t.alpha = 0 t.isFadingIn = true } @@ -155,8 +145,8 @@ func NewSlideTransition[T any](direction SlideDirection, factor float64) *SlideT } // Start starts the transition from the given "from" scene to the given "to" scene -func (t *SlideTransition[T]) Start(fromScene Scene[T], toScene Scene[T]) { - t.BaseTransition.Start(fromScene, toScene) +func (t *SlideTransition[T]) Start(fromScene Scene[T], toScene Scene[T], sm SceneController[T]) { + t.BaseTransition.Start(fromScene, toScene, sm) t.offset = 0 } @@ -228,8 +218,8 @@ func NewDurationTimedFadeTransition[T any](duration time.Duration) *TimedFadeTra } } -func (t *TimedFadeTransition[T]) Start(fromScene, toScene Scene[T]) { - t.FadeTransition.Start(fromScene, toScene) +func (t *TimedFadeTransition[T]) Start(fromScene, toScene Scene[T], sm SceneController[T]) { + t.FadeTransition.Start(fromScene, toScene, sm) t.initialTime = Clock.Now() } @@ -274,8 +264,8 @@ func NewDurationTimedSlideTransition[T any](direction SlideDirection, duration t } } -func (t *TimedSlideTransition[T]) Start(fromScene, toScene Scene[T]) { - t.SlideTransition.Start(fromScene, toScene) +func (t *TimedSlideTransition[T]) Start(fromScene, toScene Scene[T], sm SceneController[T]) { + t.SlideTransition.Start(fromScene, toScene, sm) t.initialTime = Clock.Now() } diff --git a/transition_test.go b/transition_test.go index 81810ef..3c32e71 100644 --- a/transition_test.go +++ b/transition_test.go @@ -1,6 +1,7 @@ package stagehand import ( + "fmt" "testing" "time" @@ -23,11 +24,26 @@ type baseTransitionImplementation struct { func (b *baseTransitionImplementation) Draw(screen *ebiten.Image) {} +type MockTransitionAwareScene struct { + MockScene + preTransitionCalled bool + postTransitionCalled bool +} + +func (m *MockTransitionAwareScene) PreTransition(fromScene Scene[int]) int { + m.preTransitionCalled = true + return 0 +} + +func (m *MockTransitionAwareScene) PostTransition(state int, toScene Scene[int]) { + m.postTransitionCalled = true +} + func TestBaseTransition_Update(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := &baseTransitionImplementation{} - trans.Start(from, to) + trans.Start(from, to, nil) err := trans.Update() assert.NoError(t, err) @@ -39,7 +55,7 @@ func TestBaseTransition_Layout(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := &baseTransitionImplementation{} - trans.Start(from, to) + trans.Start(from, to, nil) sw, sh := trans.Layout(100, 100) assert.Equal(t, 100, sw) @@ -49,37 +65,15 @@ func TestBaseTransition_Layout(t *testing.T) { assert.True(t, to.layoutCalled) } -func TestBaseTransition_Load(t *testing.T) { - from := &MockScene{} - to := &MockScene{} - trans := &baseTransitionImplementation{} - trans.Start(from, to) - trans.Load(42, &SceneManager[int]{}) - - assert.True(t, to.loadCalled) - assert.False(t, from.loadCalled) - -} - -func TestBaseTransition_Unload(t *testing.T) { - from := &MockScene{} - to := &MockScene{} - trans := &baseTransitionImplementation{} - trans.Start(from, to) - - trans.Unload() - assert.True(t, from.unloadCalled) - assert.False(t, to.unloadCalled) -} - func TestBaseTransition_End(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := &baseTransitionImplementation{} - trans.Start(from, to) - sm := NewSceneManager[int](trans, 0) - + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) trans.End() + + fmt.Println(sm.current.(Scene[int]), to) assert.Equal(t, to, sm.current) } @@ -87,18 +81,35 @@ func TestBaseTransition_Start(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := &baseTransitionImplementation{} - trans.Start(from, to) + trans.Start(from, to, nil) assert.Equal(t, from, trans.fromScene) assert.Equal(t, to, trans.toScene) } +func TestBaseTransition_Awareness(t *testing.T) { + from := &MockTransitionAwareScene{} + to := &MockTransitionAwareScene{} + sm := NewSceneManager[int](from, 0) + trans := &baseTransitionImplementation{} + sm.SwitchWithTransition(to, trans) + + assert.True(t, from.preTransitionCalled) + assert.True(t, to.loadCalled) + assert.False(t, from.unloadCalled) + assert.False(t, to.postTransitionCalled) + + trans.End() + assert.True(t, from.unloadCalled) + assert.True(t, to.postTransitionCalled) +} + func TestFadeTransition_UpdateOncePerFrame(t *testing.T) { var value float32 = .6 from := &MockScene{} to := &MockScene{} trans := NewFadeTransition[int](value) - trans.Start(from, to) + trans.Start(from, to, nil) err := trans.Update() assert.NoError(t, err) @@ -114,8 +125,8 @@ func TestFadeTransition_Update(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewFadeTransition[int](.5) - trans.Start(from, to) - sm := NewSceneManager[int](trans, 0) + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) err := sm.Update() assert.NoError(t, err) @@ -149,7 +160,7 @@ func TestFadeTransition_Start(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewFadeTransition[int](.5) - trans.Start(from, to) + trans.Start(from, to, nil) assert.Equal(t, from, trans.fromScene) assert.Equal(t, to, trans.toScene) @@ -161,7 +172,7 @@ func TestFadeTransition_Draw(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewFadeTransition[int](.5) - trans.Start(from, to) + trans.Start(from, to, nil) trans.Update() trans.Draw(ebiten.NewImage(100, 100)) @@ -177,7 +188,7 @@ func TestSlideTransition_UpdateOncePerFrame(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewSlideTransition[int](RightToLeft, value) - trans.Start(from, to) + trans.Start(from, to, nil) err := trans.Update() assert.NoError(t, err) @@ -198,8 +209,8 @@ func TestSlideTransition_Update(t *testing.T) { for _, direction := range variations { trans := NewSlideTransition[int](direction, .5) - trans.Start(from, to) - sm := NewSceneManager[int](trans, 0) + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) err := sm.Update() assert.NoError(t, err) @@ -224,7 +235,7 @@ func TestSlideTransition_Start(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewSlideTransition[int](TopToBottom, .5) - trans.Start(from, to) + trans.Start(from, to, nil) assert.Equal(t, from, trans.fromScene) assert.Equal(t, to, trans.toScene) @@ -241,7 +252,7 @@ func TestSlideTransition_Draw(t *testing.T) { for _, direction := range variations { trans := NewSlideTransition[int](direction, .5) - trans.Start(from, to) + trans.Start(from, to, nil) trans.Update() trans.Draw(ebiten.NewImage(100, 100)) @@ -255,8 +266,8 @@ func TestTimedFadeTransition_Update(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewDurationTimedFadeTransition[int](time.Second) - trans.Start(from, to) - sm := NewSceneManager[int](trans, 0) + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) // Should not update if no time passed err := sm.Update() @@ -304,7 +315,7 @@ func TestTimedFadeTransition_Start(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewDurationTimedFadeTransition[int](time.Second) - trans.Start(from, to) + trans.Start(from, to, nil) assert.Equal(t, from, trans.fromScene) assert.Equal(t, to, trans.toScene) @@ -322,8 +333,8 @@ func TestTimedSlideTransition_Update(t *testing.T) { for _, direction := range variations { Clock = &MockClock{currentTime: time.Now()} trans := NewDurationTimedSlideTransition[int](direction, time.Second) - trans.Start(from, to) - sm := NewSceneManager[int](trans, 0) + sm := NewSceneManager[int](from, 0) + sm.SwitchWithTransition(to, trans) // Should not update if no time passed err := sm.Update() @@ -360,7 +371,7 @@ func TestTimedSlideTransition_Start(t *testing.T) { from := &MockScene{} to := &MockScene{} trans := NewDurationTimedSlideTransition[int](TopToBottom, time.Second) - trans.Start(from, to) + trans.Start(from, to, nil) assert.Equal(t, from, trans.fromScene) assert.Equal(t, to, trans.toScene)