Skip to content

Commit

Permalink
Rewrite Scene / SceneContainer page (#1693)
Browse files Browse the repository at this point in the history
  • Loading branch information
soywiz authored Jun 13, 2023
1 parent 1a7bbc0 commit 76ecfed
Showing 1 changed file with 185 additions and 71 deletions.
256 changes: 185 additions & 71 deletions docs/korge/reference/scene/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,123 +7,237 @@ priority: 20
---

While you can create a small application in a few lines, when the application grows in size, you will want
to follow some patterns and to be able to split your application effectively.
to follow some patterns to be able to split your application effectively.

KorGE includes an asynchronous dependency injector and some tools like the modules and scenes to do so.

{% include toc_include.md %}

## Module
## Scenes

Usually, you have one single module in your application. In the module you can describe some stuff from your module,
as well as define a Scene entry point, and configure the asynchronous injector.
### Declaring a Scene

Scenes look like this:

```kotlin
object MyModule : Module() {
override val mainScene = MyScene::class
class MyScene : Scene() {
override fun SContainer.sceneInit() {
// Your initialization code before the scene transition starts
// Here you can place resource loading, adding views before they are displayed etc.
}

override fun SContainer.sceneMain() {
// Your main code, here you can add views, register for events, do tweens, etc.
}
}
```

override suspend fun AsyncInjector.configure() {
mapInstance(MyDependency("HELLO WORLD"))
mapPrototype { MyScene(get()) }
For simplicity, it is possible to only provide one of those methods. You can for example only use sceneMain in simple cases:

```kotlin
class MyScene : Scene() {
override fun SContainer.sceneMain() {
// Do loading, creating views, attaching events, tweens, etc. here.
}
}
```

And you use the module with:
### SceneContainer

`Scene`s must be added to a `SceneContainer`. `SceneContainer` is a `View`, so it can be attached to the scene graph.

```kotlin
suspend fun main() = Korge(Korge.Config(module = MyModule))
fun main() = Korge {
val sceneContainer = sceneContainer()
// or
val sceneContainer = SceneContainer().addTo(this)
}
```

You can create several mains in different classes and packages using different entry point modules.
### Changing to a Scene

## The `Scene` class
Once you have the SceneContainer view attached to the stage/scene graph, you can change it to actually show a specific Scene:

The `Scene` class looks like this:
```kotlin
sceneContainer.changeTo({ MyScene() })
```

It is possible to do like that, or if you want to use the injector:

```kotlin
abstract class Scene : InjectorAsyncDependency, ViewsContainer, CoroutineScope {
// Context stuff
val coroutineContext: CoroutineContext
var injector: AsyncInjector
var views: Views
val ag: AG

// Resources and lifecycle
var resourcesRoot: ResourcesRoot
val cancellables: CancellableGroup

// Related to the scene
var sceneContainer: SceneContainer
val sceneView: Container

// Main Entrypoint of the scene
abstract suspend fun Container.sceneInit(): Unit

// Lifecycle
open suspend fun sceneAfterInit()
open suspend fun sceneBeforeLeaving()
open suspend fun sceneDestroy()
open suspend fun sceneAfterDestroy()
}
injector.mapSingleton { MyScene() }

sceneContainer.changeTo<MyScene>()
```

And you usually declare scenes like this:
## Scene Parameters

It is possible to pass some parameter to scenes, either singletons or specific parameters for that scene.
You do so by adding those parameters to the `Scene` constructor.

### Declaration

So for example, let's consider we want to pass a singleton `service` and the `levelName` we want to load to the scene.
We would create the scene like this:

```kotlin
class MyScene : Scene() {
override suspend fun Container.sceneInit(): Unit {
solidRect(100, 100, Colors.RED)
}
class MyScene(val service: MyService, val levelName: String) : Scene() {
// ...
}
```

Scenes are like controllers and allow you to split the application by Screens or Scenes.
In a Scene you usually create Views and configure/decorate them giving them behaviour.
### Changing to a scene with parameters

Now we can change to the scene like this:

```kotlin
sceneContainer.changeTo({ MyScene(service, "mylevelname") })
```

In the case we are using the injector, and we want to be able to pass singletons, etc. without worrying
about having to change all the `changeTo` if we decide to add extra parameters:

```kotlin
injector.mapSingleton { MyService() }
injector.mapPrototype { MyScene(get(), get()) } // Note the get() here. One per parameter, but only required when configuring the injector once.

// Then we can:

sceneContainer.changeTo<MyScene>("mylevelname") // Now all the parameters provided to the changeTo, will be mapped to the scene subinjector and provided to the constructed scene
```

For each scene you will have to tell the injector how to construct it. Usually you do this mapping in the `Module` descriptor:
If you need to pass several strings or integers, or several values of a specific type, you can create and pass a data class wrapping them:

```kotlin
injector.mapPrototype { MyScene(get()) }
data class MySceneParameters(val levelName: String, val myid: String)
data class MyScene(val service: MyService, val params: MySceneParameters)

sceneContainer.changeTo<MyScene>(MySceneParameters("mylevelname", "myid"))
```

Inside the lambda of `mapPrototype`, you have injected `this: AsyncInjector`, that's why you can use the method `get()`.
Usually the `get()` method won't require type parameters since it is generic and the type is inferred from the arguments.
{:.note}
## Scene lifecycle

## The `SceneContainer` class
{:#SceneContainer}
In addition to `sceneInit` and `sceneMain`, Scenes provide other methods you can override:

The `SceneContainer` is a `View` that will contain the view of a `Scene`.
There are methods where suspensions will block further execution,
while others that will be executed in parallel even if we suspend for example waiting for a tween to complete.

```kotlin
class SceneContainer(val views: Views) : Container() {
val transitionView = TransitionView()
var currentScene: Scene? = null

suspend inline fun <reified TScene : Scene> changeTo(vararg injects: Any, time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): TScene
suspend inline fun <reified TScene : Scene> pushTo(vararg injects: Any, time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): TScene
suspend fun back(time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): Scene
suspend fun forward(time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): Scene
class MyScene() : Scene() {
override suspend fun SContainer.sceneInit() {
// BLOCK. This is called to setup the scene. **Nothing will be shown until this method completes**.
// Here you can read and wait for resources. No need to call super.
}

override suspend fun SContainer.sceneMain() {
// DO NOT BLOCK. This is called as a main method of the scene. This is called after [sceneInit].
// This method doesn't need to complete as long as it suspends.
// Its underlying job will be automatically closed on the [sceneAfterDestroy]. No need to call super.
}

override suspend fun sceneAfterInit() {
// DO NOT BLOCK. Called after the old scene has been destroyed and the transition has been completed.
}

override suspend fun sceneBeforeLeaving() {
// BLOCK. Called on the old scene after the new scene has been
// initialized, and before the transition is performed.
}

override suspend fun sceneDestroy() {
// BLOCK. Called on the old scene after the transition
// has been performed, and the old scene is not visible anymore.
}

override suspend fun sceneAfterDestroy() {
// DO NOT BLOCK. Called on the old scene after the transition has been performed, and the old scene is not visible anymore.
//
// At this stage the scene [coroutineContext] [Job] will be cancelled.
// Stopping [sceneMain] and other [launch] methods using this scene [CoroutineScope].
}

suspend fun <TScene : Scene> pushTo(clazz: KClass<TScene>, vararg injects: Any, time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): TScene
suspend fun <TScene : Scene> changeTo(clazz: KClass<TScene>, vararg injects: Any, time: TimeSpan = 0.seconds, transition: Transition = AlphaTransition): TScene
open fun onSizeChanged(size: Size) {
super.onSizeChanged(size)
// Do something here if the scene size is changed
}
}
```

Like other views, the `SceneContainer` has a builder method to construct the instance and add it to a container:
## Special Scenes

Instead of overriding `Scene` we can override other Scene subclasses simplifying some operations:

### ScaledScene & PixelatedScene

A Scene where the effective container has a fixed size `sceneWidth` and `sceneHeight`,
and scales and positions its SceneContainer based on `sceneScaleMode` and `sceneAnchor`.
Performs a linear or nearest neighborhood interpolation based `sceneSmoothing`.

This allows to have different scenes with different effective sizes.

The difference of PixelatedScene and ScaledScene is that sceneSmoothing is set to false or true.
The container is written to a texture, and then displayed in the available space by using linear or nearest neighborhood
interpolation methods (smooth or pixelated).

```kotlin
abstract class ScaledScene(
sceneWidth: Int,
sceneHeight: Int,
sceneScaleMode: ScaleMode = ScaleMode.SHOW_ALL,
sceneAnchor: Anchor = Anchor.CENTER,
sceneSmoothing: Boolean = true,
) : Scene()
```

```kotlin
abstract class PixelatedScene(
sceneWidth: Int,
sceneHeight: Int,
sceneScaleMode: ScaleMode = ScaleMode.SHOW_ALL,
sceneAnchor: Anchor = Anchor.CENTER,
sceneSmoothing: Boolean = false,
) : ScaledScene(sceneWidth, sceneHeight, sceneScaleMode, sceneAnchor, sceneSmoothing = sceneSmoothing)
```

## Transitions

It is also possible to provide timed transitions to scenes. Like doing an alpha transition or more sophisticated transitions like Mask Transitions:

```kotlin
inline fun Container.sceneContainer(views: Views, callback: SceneContainer.() -> Unit = {}): SceneContainer = SceneContainer(views).addTo(this).apply(callback)
rootSceneContainer.changeTo<IngameScene>(
transition = MaskTransition(transition = TransitionFilter.Transition.CIRCULAR, reversed = false, filtering = true),
//transition = AlphaTransition,
time = 0.5.seconds
)
```

Scenes have access to its container, so you can change the current scene to another one with:
### `AlphaTransition`

It acts like a singleton. So no parameters here. You can just put it in the `transition` argument.

### `MaskTransition`

It performs alpha blending per pixel following a pattern:

```kotlin
class MyScene : Scene() {
override suspend fun Container.sceneInit(): Unit {
solidRect(100, 100, Colors.RED).onClick { launchImmediately { sceneContainer.changeTo<OtherScene>() } }
}
}
fun MaskTransition(
transition: TransitionFilter.Transition = TransitionFilter.Transition.CIRCULAR,
reversed: Boolean = false,
spread: Float = 1f,
filtering: Boolean = true,
): Transition
```

It supports a transition from a `TransitionFilter` (that you can also use as a normal filter for views).

There are some predefined `TransitionFilter.Transition`, and you can also provide a custom greyscale bitmap:

```kotlin
fun TransitionFilter.Transition(bmp: Bitmap)
val TransitionFilter.Transition.VERTICAL
val TransitionFilter.Transition.HORIZONTAL
val TransitionFilter.Transition.DIAGONAL1
val TransitionFilter.Transition.DIAGONAL2
val TransitionFilter.Transition.CIRCULAR
val TransitionFilter.Transition.SWEEP
```

0 comments on commit 76ecfed

Please sign in to comment.