Stagehand is a scene manager library for Ebitengine that makes it easy to manage game scenes and state. With Stagehand, you can quickly create and transition between scenes, allowing you to focus on the content and gameplay of your game.
To use Stagehand, simply import it into your project:
import "github.com/joelschutz/stagehand"
- Lightweight and easy-to-use scene management for Ebitengine
- Simple and flexible API for creating and switching between scenes
- Managed type-safe states with the power of generics
- Built-in support for transition effects between scenes
- Supports custom transition effects by implementing the
SceneTransition
interface
To use Stagehand, you first need to create a struct that implements the Scene
interface:
type MyState struct {
// your state data
}
type MyScene struct {
// your scene fields
sm *stagehand.SceneManager[MyState]
}
func (s *MyScene) Update() error {
// your update code
}
func (s *MyScene) Draw(screen *ebiten.Image) {
// your draw code
}
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 {
// your unload code
}
Then, create an instance of the SceneManager
passing initial scene and state.
func main() {
// ...
scene1 := &MyScene{}
state := MyState{}
manager := stagehand.NewSceneManager[MyState](scene1, state)
if err := ebiten.RunGame(sm); err != nil {
log.Fatal(err)
}
}
We provide some example code so you can start fast:
You can switch scenes by calling SwitchTo
method on the SceneManager
giving the scene instance you wanna switch to.
func (s *MyScene) Update() error {
// ...
scene2 := &OtherScene{}
s.manager.SwitchTo(scene2)
// ...
}
You can use the SwitchWithTransition
method to switch between scenes with a transition effect. Stagehand provides two built-in transition effects: FadeTransition
and SlideTransition
.
The FadeTransition
will fade out the current scene while fading in the new scene.
func (s *MyScene) Update() error {
// ...
scene2 := &OtherScene{}
s.manager.SwitchWithTransition(scene2, stagehand.NewFadeTransition(.05))
// ...
}
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).
The SlideTransition
will slide out the current scene and slide in the new scene.
func (s *MyScene) Update() error {
// ...
scene2 := &OtherScene{}
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. There is also the option for a timed transition using NewTicksTimedSlideTransition
(for a ticks based timming) or NewDurationTimedSlideTransition
(for a real-time based timming).
You can also define your own transition, simply implement the SceneTransition
interface, we provide a helper BaseTransition
that you can use like this:
type MyTransition struct {
stagehand.BaseTransition
progress float64 // An example factor
}
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, sm)
t.progress = 0
}
func (t *MyTransition) Update() error {
// Update the progress of the transition
t.progress += 0.01
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)
// Draw transition effect here
}
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.
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:
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
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
:
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.
// 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.
func (s *MyScene) Update() error {
// ...
s.manager.ProcessTrigger(Trigger)
// ...
}
- When switching scenes (i.e. calling
SwitchTo
,SwitchWithTransition
orProcessTrigger
) 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.
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.
Before submitting a pull request, please make sure to run the tests:
go test ./...
Stagehand is released under the MIT License.