Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]1.1 Release #6

Merged
merged 19 commits into from
Mar 10, 2024
128 changes: 113 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type MyState struct {

type MyScene struct {
// your scene fields
sm *stagehand.SceneManager[MyState]
}

func (s *MyScene) Update() error {
Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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.
Expand All @@ -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).
60 changes: 60 additions & 0 deletions director.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions director_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading