Skip to content

Commit 0afe7f9

Browse files
Animations, part 1.
1 parent f4dbb69 commit 0afe7f9

File tree

5 files changed

+110
-20
lines changed

5 files changed

+110
-20
lines changed

guides/animation/part-1-basics/README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,44 @@
33
There are essentially two forms of animation in Indigo.
44

55
1. Animations you have imported from a third party tool.
6-
2. Animations you have described in code.
6+
2. Animations you have described in code, known as procedural animations.
77

88
## Importing animations
99

1010
At the time of writing, the only built-in support for importing animations, is from [Aseprite](https://www.aseprite.org/) (an excellent pixel art editor and animation tool) into Indigo as a Sprite or a Clip. For working examples of this, please see the relevant section on Sprites and Clips.
1111

12-
## Programming Animations
12+
Aseprite animations can either be embedded using Indigo's built-in generators, or loaded from JSON data at runtime. Contributions to support other formats are welcome!
1313

14-
This series of examples focuses on how to describe your animations in code, what abstractions are available, and what the trade-offs are.
14+
## Procedural Animations
1515

16-
To get started, we're going to look at the fundamentals.
16+
This series of guides explores on how to describe your animations in code.
17+
18+
To get started, we're going to look at the fundamentals.
19+
20+
## Movies vs Video Games
21+
22+
**Question:** What do the earliest animations, from physical zoetropes to early animations on cellular film have in common with the latest and greatest 3D animated movie extravaganza?
23+
24+
The answer is: A fixed frame rate, say 24 frames per second (the standard for theatrical animation screenings at one time).
25+
26+
That means that if you want to animate something moving from one side of the screen to the other, at constant speed in a duration of 1 second, you need to move it exactly 1/24th of the total distance on every frame, for 24 frames.
27+
28+
Sounds good! Doing that in code is as simple as doing this every frame:
29+
30+
```scala
31+
val increment = totalDistance / 24
32+
33+
sprite.moveBy(increment, 0)
34+
```
35+
36+
The problem with this solution is that video games are not movies, and they DO NOT have a consistent frame rate. Each frame will take slightly less or more time to process and render than the last frame. Using the method above will result in jerky and jittery animation.
37+
38+
## Frame time deltas to the rescue
39+
40+
Luckily, there is an easy solution, all we need to do is multiple the desired number of units (i.e. distance) per second (in our case, the total distance) by the amount of time that has elapse since the last frame was processed.
41+
42+
You may recall from school that `speed = distance / time`, well all we're going to do is re-arrange that to `distance = speed * time` where distance is how far we need to move our sprite, speed is the desired distance to move every second, and time is therefore the frame delta. Which gives us something like:
43+
44+
```scala
45+
sprite.moveBy(totalDistance * frameDelta, 0)
46+
```

guides/animation/part-1-basics/src/AnimationPart1.scala

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,53 @@ import generated.Assets
66

77
import scala.scalajs.js.annotation.*
88

9+
/** ## Example of procedural animation
10+
*
11+
* Let's look at a real example and illustrate some of the problems a challenges we need to
12+
* overcome.
13+
*
14+
* In this example we will simply move a red circle across the screen from left to right, using the
15+
* frame delta to ensure a constant speed.
16+
*
17+
* How hard could it be?
18+
*/
19+
20+
/** The first thing we need to do is model some state. State?! Oh yes. In animation you either need
21+
* to know the current state, or write some clever code that can fully work on the current
22+
* animation position based only on the initial conditions and the running time. More on that
23+
* later, for now, we're going to update a known position.
24+
*
25+
* The position is wrapped in a model case class in a nod to realism, but curiously, the position
26+
* is modelled as a `Vertex`, which takes `Double`s, rather that a `Point` which uses `Int` as
27+
* its unit type. What is going on? `Point` would seem to be the obvious choice, since we're
28+
* moving across the screen in pixels, but let's do the math:
29+
*
30+
* 1. We want to move across the screen at 50 pixels per second, and our game runs at 60 frames
31+
* per second.
32+
* 1. 60 FPS is on average 16.667 milliseconds per frame, but we need to convert that to seconds,
33+
* so: 0.016667.
34+
* 1. 50 pixels per second * 0.016667 frame delta = 0.83335.
35+
*
36+
* So we need to move our circle 0.83335 pixels every frame, on average.
37+
*
38+
* If conversion of `Double` to `Int` _rounded_ the value then we'd jitter and stutter across the
39+
* screen - sometimes moving a bit, sometimes not at all.
40+
*
41+
* Actually, conversion of `Double` to `Int` _floors_ the value, meaning `0`, so we never move...
42+
* Oh dear!
43+
*
44+
* By modelling the position as a type based on `Double` and converting to pixels at the last
45+
* moment during presentation, we can avoid all these difficulties.
46+
*/
47+
// ```scala
48+
final case class Model(position: Vertex)
49+
// ```
50+
951
@JSExportTopLevel("IndigoGame")
10-
object AnimationPart1 extends IndigoSandbox[Unit, Unit]:
52+
object AnimationPart1 extends IndigoSandbox[Unit, Model]:
1153

1254
val config: GameConfig =
1355
Config.config.noResize
14-
.withMagnification(2)
1556

1657
val assets: Set[AssetType] =
1758
Assets.assets.assetSet
@@ -23,20 +64,42 @@ object AnimationPart1 extends IndigoSandbox[Unit, Unit]:
2364
def setup(assetCollection: AssetCollection, dice: Dice): Outcome[Startup[Unit]] =
2465
Outcome(Startup.Success(()))
2566

26-
def initialModel(startupData: Unit): Outcome[Unit] =
27-
Outcome(())
67+
// We need to initialise our state with some acceptable values.
68+
// ```scala
69+
def initialModel(startupData: Unit): Outcome[Model] =
70+
Outcome(Model(Vertex(60, 120)))
71+
// ```
72+
73+
// During update we do our now familiar bit of maths, multiplying the speed by the frame delta
74+
// ```scala
75+
def updateModel(context: Context[Unit], model: Model): GlobalEvent => Outcome[Model] =
76+
_ =>
77+
val pixelsPerSecond = 50
2878

29-
def updateModel(context: Context[Unit], model: Unit): GlobalEvent => Outcome[Unit] =
30-
_ => Outcome(model)
79+
Outcome(
80+
model.copy(
81+
position = model.position.withX(
82+
model.position.x + (pixelsPerSecond * context.frame.time.delta.toDouble)
83+
)
84+
)
85+
)
86+
// ```
87+
88+
// When we draw the circle, we simply move it to the position held in the model.
89+
// ```scala
90+
def present(context: Context[Unit], model: Model): Outcome[SceneUpdateFragment] =
91+
val circle =
92+
Shape.Circle(
93+
Circle(Point.zero, 50),
94+
Fill.Color(RGBA.Red),
95+
Stroke(2, RGBA.White)
96+
)
3197

32-
def present(context: Context[Unit], model: Unit): Outcome[SceneUpdateFragment] =
3398
Outcome(
3499
SceneUpdateFragment(
35-
Shape
36-
.Circle(
37-
Circle(Point.zero, 50),
38-
Fill.Color(RGBA.Red),
39-
Stroke(2, RGBA.White)
40-
)
100+
circle.moveTo(model.position.toPoint)
41101
)
42102
)
103+
// ```
104+
105+
// Gosh that was hard work! Imagine trying to do that for every moving thing on the screen! There must be another way, surely?

guides/animation/part-2-signals/src/AnimationPart2.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ object AnimationPart2 extends IndigoSandbox[Unit, Unit]:
1111

1212
val config: GameConfig =
1313
Config.config.noResize
14-
.withMagnification(2)
1514

1615
val assets: Set[AssetType] =
1716
Assets.assets.assetSet

guides/animation/part-3-timelines/src/AnimationPart3.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ object AnimationPart3 extends IndigoSandbox[Unit, Unit]:
1111

1212
val config: GameConfig =
1313
Config.config.noResize
14-
.withMagnification(2)
1514

1615
val assets: Set[AssetType] =
1716
Assets.assets.assetSet

guides/animation/part-4-shaders/src/AnimationPart4.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ object AnimationPart4 extends IndigoSandbox[Unit, Unit]:
1111

1212
val config: GameConfig =
1313
Config.config.noResize
14-
.withMagnification(2)
1514

1615
val assets: Set[AssetType] =
1716
Assets.assets.assetSet

0 commit comments

Comments
 (0)