Skip to content

Commit d54b85e

Browse files
authored
Merge pull request #6 from joelschutz/1.1
1.1 Release
2 parents cf4a4bf + 299fe79 commit d54b85e

File tree

12 files changed

+631
-133
lines changed

12 files changed

+631
-133
lines changed

README.md

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type MyState struct {
3434

3535
type MyScene struct {
3636
// your scene fields
37+
sm *stagehand.SceneManager[MyState]
3738
}
3839

3940
func (s *MyScene) Update() error {
@@ -44,8 +45,9 @@ func (s *MyScene) Draw(screen *ebiten.Image) {
4445
// your draw code
4546
}
4647

47-
func (s *MyScene) Load(state MyState ,manager *stagehand.SceneManager) {
48+
func (s *MyScene) Load(state MyState ,manager stagehand.SceneController[MyState]) {
4849
// your load code
50+
s.sm = manager.(*stagehand.SceneManager[MyState]) // This type assertion is important
4951
}
5052

5153
func (s *MyScene) Unload() MyState {
@@ -63,11 +65,20 @@ func main() {
6365
manager := stagehand.NewSceneManager[MyState](scene1, state)
6466

6567
if err := ebiten.RunGame(sm); err != nil {
66-
log.Fatal(err)
67-
}
68+
log.Fatal(err)
69+
}
6870
}
6971
```
7072

73+
### Examples
74+
75+
We provide some example code so you can start fast:
76+
77+
- [Simple Example](https://github.com/joelschutz/stagehand/blob/master/examples/simple/main.go)
78+
- [Timed Transition Example](https://github.com/joelschutz/stagehand/blob/master/examples/timed/main.go)
79+
- [Transition Awareness Example](https://github.com/joelschutz/stagehand/blob/master/examples/aware/main.go)
80+
- [Scene Director Example](https://github.com/joelschutz/stagehand/blob/master/examples/director/main.go)
81+
7182
## Transitions
7283

7384
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
92103
func (s *MyScene) Update() error {
93104
// ...
94105
scene2 := &OtherScene{}
95-
s.manager.SwitchWithTransition(scene2. stagehand.NewFadeTransition(.05))
106+
s.manager.SwitchWithTransition(scene2, stagehand.NewFadeTransition(.05))
96107

97108
// ...
98109
}
99110
```
100111

101-
In this example, the `FadeTransition` will fade 5% every frame.
112+
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).
102113

103114
### Slide Transition
104115

@@ -108,13 +119,13 @@ The `SlideTransition` will slide out the current scene and slide in the new scen
108119
func (s *MyScene) Update() error {
109120
// ...
110121
scene2 := &OtherScene{}
111-
s.manager.SwitchWithTransition(scene2. stagehand.NewSlideTransition(stagehand.LeftToRight, .05))
122+
s.manager.SwitchWithTransition(scene2, stagehand.NewSlideTransition(stagehand.LeftToRight, .05))
112123

113124
// ...
114125
}
115126
```
116127

117-
In this example, the `SlideTransition` will slide in the new scene from the left 5% every frame.
128+
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).
118129

119130
### Custom Transitions
120131

@@ -123,30 +134,117 @@ You can also define your own transition, simply implement the `SceneTransition`
123134
```go
124135
type MyTransition struct {
125136
stagehand.BaseTransition
126-
progress float64 // An example factor
137+
progress float64 // An example factor
127138
}
128139

129-
func (t *MyTransition) Start(from, to stagehand.Scene[T]) {
140+
func (t *MyTransition) Start(from, to stagehand.Scene[MyState], sm *SceneManager[MyState]) {
130141
// Start the transition from the "from" scene to the "to" scene here
131-
t.BaseTransition.Start(fromScene, toScene)
142+
t.BaseTransition.Start(fromScene, toScene, sm)
132143
t.progress = 0
133144
}
134145

135146
func (t *MyTransition) Update() error {
136-
// Update the progress of the transition
147+
// Update the progress of the transition
137148
t.progress += 0.01
138-
return t.BaseTransition.Update()
149+
return t.BaseTransition.Update()
139150
}
140151

141152
func (t *MyTransition) Draw(screen *ebiten.Image) {
142-
// Optionally you can use a helper function to render each scene frame
143-
toImg, fromImg := stagehand.PreDraw(screen.Bounds(), t.fromScene, t.toScene)
153+
// Optionally you can use a helper function to render each scene frame
154+
toImg, fromImg := stagehand.PreDraw(screen.Bounds(), t.fromScene, t.toScene)
144155

145156
// Draw transition effect here
146157
}
147158

148159
```
149160

161+
### Transition Awareness
162+
163+
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.
164+
165+
```go
166+
func (s *MyScene) PreTransition(destination Scene[MyState]) MyState {
167+
// Runs before new scene is loaded
168+
}
169+
170+
func (s *MyScene) PostTransition(lastState MyState, original Scene[MyState]) {
171+
// Runs when old scene is unloaded
172+
}
173+
```
174+
175+
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:
176+
177+
```shell
178+
PreTransition Called on old scene
179+
Load Called on new scene
180+
Updated old scene
181+
Updated new scene
182+
...
183+
Updated old scene
184+
Updated new scene
185+
Unload Called on old scene
186+
PostTransition Called on new scene
187+
```
188+
189+
## SceneDirector
190+
191+
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`:
192+
193+
```go
194+
type MyScene struct {
195+
// your scene fields
196+
director *stagehand.SceneDirector[MyState]
197+
}
198+
199+
func (s *MyScene) Load(state MyState ,director stagehand.SceneController[MyState]) {
200+
// your load code
201+
s.director = director.(*stagehand.SceneDirector[MyState]) // This type assertion is important
202+
}
203+
```
204+
205+
Then define a ruleSet of `Directive` and `SceneTransitionTrigger` for the game.
206+
207+
```go
208+
// Triggers are int type underneath
209+
const (
210+
Trigger1 stagehand.SceneTransitionTrigger = iota
211+
Trigger2
212+
)
213+
214+
func main() {
215+
// ...
216+
scene1 := &MyScene{}
217+
scene2 := &OtherScene{}
218+
219+
// Create a rule set for transitioning between scenes based on Triggers
220+
ruleSet := make(map[stagehand.Scene[MyState]][]Directive[MyState])
221+
directive1 := Directive[MyState]{Dest: scene2, Trigger: Trigger1}
222+
directive2 := Directive[MyState]{Dest: scene1, Trigger: Trigger2, Transition: stagehand.NewFadeTransition(.05)} // Add transitions inside the directive
223+
224+
// Directives are mapped to each Scene pointer and can be shared
225+
ruleSet[scene1] = []Directive[MyState]{directive1, directive2}
226+
ruleSet[scene2] = []Directive[MyState]{directive2}
227+
228+
state := MyState{}
229+
manager := stagehand.NewSceneDirector[MyState](scene1, state, ruleSet)
230+
231+
if err := ebiten.RunGame(sm); err != nil {
232+
log.Fatal(err)
233+
}
234+
}
235+
```
236+
237+
Now you can now notify the `SceneDirector` about activated `SceneTransitionTrigger`, if no `Directive` match, the code will still run without errors.
238+
239+
```go
240+
func (s *MyScene) Update() error {
241+
// ...
242+
s.manager.ProcessTrigger(Trigger)
243+
244+
// ...
245+
}
246+
```
247+
150248
## Contribution
151249

152250
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 ./...
159257

160258
## License
161259

162-
Stagehand is released under the [MIT License](https://github.com/example/stagehand/blob/master/LICENSE).
260+
Stagehand is released under the [MIT License](https://github.com/joelschutz/stagehand/blob/master/LICENSE).

director.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package stagehand
2+
3+
type SceneTransitionTrigger int
4+
5+
// A Directive is a struct that represents how a scene should be transitioned
6+
type Directive[T any] struct {
7+
Dest Scene[T]
8+
Transition SceneTransition[T]
9+
Trigger SceneTransitionTrigger
10+
}
11+
12+
// A SceneDirector is a struct that manages the transitions between scenes
13+
type SceneDirector[T any] struct {
14+
SceneManager[T]
15+
RuleSet map[Scene[T]][]Directive[T]
16+
}
17+
18+
func NewSceneDirector[T any](scene Scene[T], state T, RuleSet map[Scene[T]][]Directive[T]) *SceneDirector[T] {
19+
s := &SceneDirector[T]{RuleSet: RuleSet}
20+
s.current = scene
21+
scene.Load(state, s)
22+
return s
23+
}
24+
25+
// ProcessTrigger finds if a transition should be triggered
26+
func (d *SceneDirector[T]) ProcessTrigger(trigger SceneTransitionTrigger) {
27+
for _, directive := range d.RuleSet[d.current.(Scene[T])] {
28+
if directive.Trigger == trigger {
29+
if directive.Transition != nil {
30+
// With transition
31+
// Equivalent to SwitchWithTransition
32+
sc := d.current.(Scene[T])
33+
directive.Transition.Start(sc, directive.Dest, d)
34+
if c, ok := sc.(TransitionAwareScene[T]); ok {
35+
directive.Dest.Load(c.PreTransition(directive.Dest), d)
36+
} else {
37+
directive.Dest.Load(sc.Unload(), d)
38+
}
39+
d.current = directive.Transition
40+
} else {
41+
// No transition
42+
// Equivalent to SwitchTo
43+
if c, ok := d.current.(Scene[T]); ok {
44+
directive.Dest.Load(c.Unload(), d)
45+
d.current = directive.Dest
46+
}
47+
}
48+
49+
}
50+
}
51+
}
52+
53+
func (d *SceneDirector[T]) ReturnFromTransition(scene, orgin Scene[T]) {
54+
if c, ok := scene.(TransitionAwareScene[T]); ok {
55+
c.PostTransition(orgin.Unload(), orgin)
56+
} else {
57+
scene.Load(orgin.Unload(), d)
58+
}
59+
d.current = scene
60+
}

director_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package stagehand
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestSceneDirector_NewSceneDirector(t *testing.T) {
10+
mockScene := &MockScene{}
11+
ruleSet := make(map[Scene[int]][]Directive[int])
12+
13+
director := NewSceneDirector[int](mockScene, 1, ruleSet)
14+
15+
assert.NotNil(t, director)
16+
assert.Equal(t, mockScene, director.current)
17+
}
18+
19+
func TestSceneDirector_ProcessTrigger(t *testing.T) {
20+
mockScene := &MockScene{}
21+
mockScene2 := &MockScene{}
22+
ruleSet := make(map[Scene[int]][]Directive[int])
23+
24+
director := NewSceneDirector[int](mockScene, 1, ruleSet)
25+
26+
rule := Directive[int]{Dest: mockScene2, Trigger: 2}
27+
ruleSet[mockScene] = []Directive[int]{rule}
28+
29+
// Call the ProcessTrigger method with wrong trigger
30+
director.ProcessTrigger(1)
31+
assert.NotEqual(t, rule.Dest, director.current)
32+
33+
// Call the ProcessTrigger method with correct trigger
34+
director.ProcessTrigger(2)
35+
assert.Equal(t, rule.Dest, director.current)
36+
}
37+
38+
func TestSceneDirector_ProcessTriggerWithTransition(t *testing.T) {
39+
mockScene := &MockScene{}
40+
mockTransition := &baseTransitionImplementation{}
41+
ruleSet := make(map[Scene[int]][]Directive[int])
42+
43+
director := NewSceneDirector[int](mockScene, 1, ruleSet)
44+
45+
rule := Directive[int]{Dest: &MockScene{}, Trigger: 2, Transition: mockTransition}
46+
ruleSet[mockScene] = []Directive[int]{rule}
47+
48+
// Call the ProcessTrigger method with wrong trigger
49+
director.ProcessTrigger(1)
50+
assert.NotEqual(t, rule.Transition, director.current)
51+
52+
// Call the ProcessTrigger method with correct trigger
53+
director.ProcessTrigger(2)
54+
assert.Equal(t, rule.Transition, director.current)
55+
56+
rule.Transition.End()
57+
assert.Equal(t, rule.Dest, director.current)
58+
}
59+
60+
func TestSceneDirector_ProcessTriggerWithTransitionAwareness(t *testing.T) {
61+
mockScene := &MockTransitionAwareScene{}
62+
mockTransition := &baseTransitionImplementation{}
63+
ruleSet := make(map[Scene[int]][]Directive[int])
64+
65+
director := NewSceneDirector[int](mockScene, 1, ruleSet)
66+
67+
rule := Directive[int]{Dest: &MockTransitionAwareScene{}, Trigger: 2, Transition: mockTransition}
68+
ruleSet[mockScene] = []Directive[int]{rule}
69+
70+
// Call the ProcessTrigger method with wrong trigger
71+
director.ProcessTrigger(1)
72+
assert.NotEqual(t, rule.Transition, director.current)
73+
74+
// Call the ProcessTrigger method with correct trigger
75+
director.ProcessTrigger(2)
76+
assert.Equal(t, rule.Transition, director.current)
77+
78+
rule.Transition.End()
79+
assert.Equal(t, rule.Dest, director.current)
80+
}

0 commit comments

Comments
 (0)