diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index ac042eeb00d..e9c15ceeb56 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -34,6 +34,7 @@ multitap # support from a device to recognize many taps at the same time orientable # can be oriented pathfinding # computer algorithm to find the best path through a world or maze perlin # Perlin Noise, a type of noise generating algorithm +platformers # plural of platformer, a genre of video game quadtree # a tree-based data structure where each node has exactly 4 children rasterizing respawn # when the player character dies and is brought back after some time and penalties @@ -42,6 +43,7 @@ retarget # to direct (something) toward a different target RGBA # red green blue alpha RGBO # red green blue opacity scos # cosine of a rotation multiplied by the scale factor +scrollers # plural of scroller, a genre of video game shaderbundle # a file extension used to bundle shaders for GLSL slerp # short for spherical linear interpolation, a method to interpolate quaternions spritesheet # a single image packing multiple sprites, normally in a grid diff --git a/doc/README.md b/doc/README.md index 1f2c7a5b8be..69ad3da9818 100644 --- a/doc/README.md +++ b/doc/README.md @@ -64,7 +64,7 @@ void main() { In Flame we provide a concept called the Flame Component System (FCS), which is a way to organize your game objects in a way that makes it easy to manage them. You can read more about it in the -[Components](flame/components.md) section. +[Components](flame/components/components.md) section. When you want to start a new game you either have to extend the `FlameGame` class or the `World` class. The `FlameGame` is the root of your game and is responsible for managing the game loop and @@ -126,7 +126,7 @@ overridden the `onLoad` method to set the sprite of the component to a sprite th image file called `player.png`. The image has to be in the `assets/images` directory in your project (see the [Assets Directory Structure](flame/structure.md)) and you have to add it to the [assets section](https://docs.flutter.dev/ui/assets/assets-and-images) of your `pubspec.yaml` file. -In this class we also set the size of the component to 200x200 and the [anchor](flame/components.md#anchor) +In this class we also set the size of the component to 200x200 and the [anchor](flame/components/position_component.md#anchor) to the center of the component by sending them to the `super` constructor. We also let the user of the `Player` class set the position of the component when creating it (`Player(position: Vector2(0, 0))`). diff --git a/doc/flame/camera.md b/doc/flame/camera.md index 7fb6ef7fed8..f94da02068f 100644 --- a/doc/flame/camera.md +++ b/doc/flame/camera.md @@ -1,5 +1,12 @@ # Camera & World +In most games the world is larger than what fits on screen at once. The camera controls which +portion of the game world is visible and how it is projected onto the player's display, handling +panning, zooming, and following characters. This is similar to how a +[`Viewport`](https://api.flutter.dev/flutter/rendering/RenderViewport-class.html) in Flutter determines +which part of a scrollable area is visible, but tailored for the free-form 2D coordinate space of +a game. + Example of a simple game structure: ```text @@ -39,7 +46,7 @@ statically rendered below the world. This component should be used to host all other components that comprise your game world. The main property of the `World` class is that it does not render -through traditional means -- instead it is rendered by one or more +through traditional means; instead it is rendered by one or more [CameraComponent](#cameracomponent)s to "look at" the world. In the `FlameGame` class there is one `World` called `world` which is added by default and paired together with the default `CameraComponent` called `camera`. @@ -82,7 +89,7 @@ which is paired together with the default `world`, so you don't need to create or add your own `CameraComponent` if your game doesn't need to. A `CameraComponent` has two other components inside: a [Viewport](#viewport) and a -[Viewfinder](#viewfinder), those components are always children of a camera. +[Viewfinder](#viewfinder). Those components are always children of a camera. The `FlameGame` class has a `camera` field in its constructor, so you can set what type of default camera that you want, like this camera with a @@ -124,8 +131,8 @@ final camera = CameraComponent.withFixedResolution( ); ``` -This will create a camera with a viewport centered in the middle of the screen, taking as much -space as possible while still maintaining the 4:3 (800x600) aspect ratio, and showing a game world region +This will create a camera with a viewport centered in the middle of the screen, taking as much space +as possible while still maintaining the 4:3 (800x600) aspect ratio, and showing a game world region of size 800 x 600. A "fixed resolution" is very simple to work with, but it will underutilize the user's available @@ -148,14 +155,14 @@ components. The following viewports are available: -- `MaxViewport` (default) -- this viewport expands to the maximum size allowed +- `MaxViewport` (default): this viewport expands to the maximum size allowed by the game, i.e. it will be equal to the size of the game canvas. -- `FixedResolutionViewport` -- keeps the resolution and aspect ratio fixed, with black bars on the +- `FixedResolutionViewport`: keeps the resolution and aspect ratio fixed, with black bars on the sides if it doesn't match the aspect ratio. -- `FixedSizeViewport` -- a simple rectangular viewport with predefined size. -- `FixedAspectRatioViewport` -- a rectangular viewport which expands to fit +- `FixedSizeViewport`: a simple rectangular viewport with predefined size. +- `FixedAspectRatioViewport`: a rectangular viewport which expands to fit into the game canvas, but preserving its aspect ratio. -- `CircularViewport` -- a viewport in the shape of a circle, fixed size. +- `CircularViewport`: a viewport in the shape of a circle, fixed size. If you add children to the `Viewport` they will appear as static HUDs in front of the world. diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index ad746b7a93e..2a922e91284 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -1,24 +1,29 @@ # Collision Detection +Almost every game needs to know when objects touch or overlap. Without collision detection a player +could walk through walls, bullets would pass through enemies, and coins could never be collected. +Flame provides a built-in collision detection system so you can focus on what *happens* when objects +collide rather than writing the intersection math yourself. + Collision detection is needed in most games to detect and act upon two components intersecting each -other. For example an arrow hitting an enemy or the player picking up a coin. +other. For example, an arrow hitting an enemy or the player picking up a coin. In most collision detection systems you use something called hitboxes to create more precise bounding boxes of your components. In Flame the hitboxes are areas of the component that can react -to collisions (and make [gesture input](inputs/gesture_input.md#gesturehitboxes)) more accurate. +to collisions and make [gesture input](inputs/gesture_input.md#gesturehitboxes) more accurate. The collision detection system supports three different types of shapes that you can build hitboxes -from, these shapes are Polygon, Rectangle and Circle. Multiple hitbox can be added -to a component to form the area which can be used to either detect collisions -or whether it contains a point or not, -the latter is very useful for accurate gesture detection. The collision detection does not handle -what should happen when two hitboxes collide, so it is up to the user to implement what will happen -when for example two `PositionComponent`s have intersecting hitboxes. +from, these shapes are Polygon, Rectangle and Circle. Multiple hitboxes can be added to a +component to form the area which can be used to either detect collisions or determine whether it +contains a point. The latter is very useful for accurate gesture detection. The collision +detection does not handle what should happen when two hitboxes collide, so it is up to the user +to implement what will happen when for example two `PositionComponent`s have intersecting +hitboxes. Do note that the built-in collision detection system does not take collisions between two hitboxes -that overshoot each other into account, this could happen when they either move very fast or -`update` being called with a large delta time (for example if your app is not in the foreground). -This behavior is called tunneling, if you want to read more about it. +that overshoot each other into account. This could happen when they either move very fast or +`update` is called with a large delta time (for example if your app is not in the foreground). +This behavior is called tunneling. Also note that the collision detection system has a limitation that makes it not work properly if you have certain types of combinations of flips and scales of the ancestors of the hitboxes. @@ -104,7 +109,7 @@ The set of points is where the edges of the hitboxes intersect. Note that the `onCollision` method will be called on both `PositionComponent`s if they have both implemented the `onCollision` method, and also on both hitboxes. The same goes for the `onCollisionStart` and `onCollisionEnd` methods, which are called when two components and hitboxes -starts or stops colliding with each other. +start or stop colliding with each other. When a `PositionComponent` (and hitbox) starts to collide with another `PositionComponent` both `onCollisionStart` and `onCollision` are called, so if you don't need to do something specific @@ -172,11 +177,10 @@ class MyComponent extends PositionComponent { ``` If you don't add any arguments to the hitbox, like above, the hitbox will try to fill its parent as -much as possible. Except for having the hitboxes trying to fill their parents, -there are two ways to -initiate hitboxes and it is with the normal constructor where you define the hitbox by itself, with -a size and a position etc. The other way is to use the `relative` constructor which defines the -hitbox in relation to the size of its intended parent. +much as possible. Apart from having the hitboxes fill their parents, there are two ways to +initialize hitboxes. One is with the normal constructor where you define the hitbox by itself, +with a size and a position etc. The other way is to use the `relative` constructor which defines +the hitbox in relation to the size of its intended parent. In some specific cases you might want to handle collisions only between hitboxes, without @@ -217,7 +221,7 @@ class MySpecialHitbox extends RectangleHitbox { ``` You can read more about how the different shapes are defined in the -[ShapeComponents](components.md#shapecomponents) section. +[ShapeComponents](components/shape_components.md) section. Remember that you can add as many `ShapeHitbox`s as you want to your `PositionComponent` to make up more complex areas. For example a snowman with a hat could be represented by three `CircleHitbox`s @@ -256,7 +260,7 @@ about at the moment but that might later come back in to view so they are not co from the game. These are just examples of how you could use these types, there will be a lot more use cases for -them so don't doubt to use them even if your use case isn't listed here. +them so don't hesitate to use them even if your use case isn't listed here. ### PolygonHitbox @@ -270,20 +274,20 @@ default calculated from the size of the collidable that they are attached to, bu polygon can be made in an infinite number of ways inside of a bounding box you have to add the definition in the constructor for this shape. -The `PolygonHitbox` has the same constructors as the [](components.md#polygoncomponent), see that -section for documentation regarding those. +The `PolygonHitbox` has the same constructors as the [](components/shape_components.md#polygoncomponent), +see that section for documentation regarding those. ### RectangleHitbox -The `RectangleHitbox` has the same constructors as the [](components.md#rectanglecomponent), see -that section for documentation regarding those. +The `RectangleHitbox` has the same constructors as the [](components/shape_components.md#rectanglecomponent), +see that section for documentation regarding those. ### CircleHitbox -The `CircleHitbox` has the same constructors as the [](components.md#circlecomponent), see that -section for documentation regarding those. +The `CircleHitbox` has the same constructors as the [](components/shape_components.md#circlecomponent), +see that section for documentation regarding those. ## ScreenHitbox @@ -315,7 +319,7 @@ worry about the broad phase system that is used, so if the standard implementati enough for you, you probably don't have to read this section. A broad phase is the first step of collision detection where potential collisions are calculated. -Calculating these potential collisions is faster than to checking the intersections exactly, +Calculating these potential collisions is faster than checking the intersections exactly, and it removes the need to check all hitboxes against each other and therefore avoiding O(n²). @@ -386,9 +390,9 @@ class Bullet extends PositionComponent with CollisionCallbacks { // do NOT collide with Player or Water return false; } - // Just return true if you're not interested in the parent's type check result. - // Or call super and you will be able to override the result with the parent's - // result. + // Just return true if you're not interested in + // the parent's type check result. Or call super + // to override the result with the parent's result. return super.onComponentTypeCheck(other); } @@ -470,7 +474,7 @@ range. For such cases, an optional `maxDistance` can be provided. To use the ray casting functionality you have to have the `HasCollisionDetection` mixin on your game. After you have added that, you can call `collisionDetection.raycast(...)` on your game class, -or with the `HasGameReference` Mixin from other components as well. +or with the `HasGameReference` mixin from other components as well. Example: @@ -505,10 +509,10 @@ The result from this operation will either be `null` if the ray didn't hit anyth - Which hitbox the ray hit - The intersection point of the collision -- The reflection ray, i.e. how the ray would reflect on the hitbox that it hix +- The reflection ray, i.e. how the ray would reflect on the hitbox that it hit - The normal of the collision, i.e. a vector perpendicular to the face of the hitbox that it hits -If you are concerned about performance you can pre create a `RaycastResult` object that you send in +If you are concerned about performance you can pre-create a `RaycastResult` object that you send in to the method with the `out` argument, this will make it possible for the method to reuse this object instead of creating a new one for each iteration. This can be good if you do a lot of ray casting in your `update` methods. @@ -587,7 +591,7 @@ class MyGame extends FlameGame with HasCollisionDetection { ``` In the example above we send out a ray from (0, 100) diagonally down to the right -and we say that we want it the bounce on at most 100 hitboxes, +and we say that we want it to bounce on at most 100 hitboxes, it doesn't necessarily have to get 100 results since at some point one of the reflection rays might not hit a hitbox and then the method is done. @@ -613,14 +617,13 @@ as a dependency. But if you have a simpler use-case and just want to check for collisions of components and improve the accuracy of gestures, Flame's built-in collision detection will serve you very well. -If you have the following needs you should at least consider to use -[Forge2D](https://github.com/flame-engine/forge2d): +If you have the following needs you should at least consider using [Forge2D](https://github.com/flame-engine/forge2d): -- Interacting realistic forces +- Realistic interacting forces - Particle systems that can interact with other bodies - Joints between bodies -It is a good idea to just use the Flame collision detection system if you on the other hand only +On the other hand, it is a good idea to just use the Flame collision detection system if you only need some of the following things (since it is simpler to not involve Forge2D): - The ability to act on some of your components colliding diff --git a/doc/flame/components.md b/doc/flame/components.md deleted file mode 100644 index 3b2f7ea359d..00000000000 --- a/doc/flame/components.md +++ /dev/null @@ -1,1536 +0,0 @@ -# Components - -```{include} diagrams/component.md -``` - -This diagram might look intimidating, but don't worry, it is not as complex as it looks. - - -## Component - -All components inherit from the `Component` class and can have other `Component`s as children. -This is the base of what we call the Flame Component System, or FCS for short. - -Children can be added either with the `add(Component c)` method or directly in the constructor. - -Example: - -```dart -void main() { - final component1 = Component(children: [Component(), Component()]); - final component2 = Component(); - component2.add(Component()); - component2.addAll([Component(), Component()]); -} -``` - -The `Component()` here could of course be any subclass of `Component`. - -Every `Component` has a few methods that you can optionally implement, which are used by the -`FlameGame` class. - - -### Component lifecycle - -```{include} diagrams/component_life_cycle.md -``` - -The `onGameResize` method is called whenever the screen is resized, and also when this component -gets added into the component tree, before the `onMount`. - -The `onParentResize` method is similar: it is also called when the component is mounted into the -component tree, and also whenever the parent of the current component changes its size. - -The `onRemove` method can be overridden to run code before the component is removed from the game. -It is only run once even if the component is removed both by using the parents remove method and -the `Component` remove method. - -The `onLoad` method can be overridden to run asynchronous initialization code for the component, -like loading an image for example. This method is executed before `onGameResize` and -`onMount`. This method is guaranteed to execute only once during the lifetime of the component, so -you can think of it as an "asynchronous constructor". - -The `onMount` method runs every time when the component is mounted into a game tree. This means that -you should not initialize `late final` variables here, since this method might run several times -throughout the component's lifetime. This method will only run if the parent is already mounted. -If the parent is not mounted yet, then this method will wait in a queue (this will have no effect -on the rest of the game engine). - -The `onChildrenChanged` method can be overridden if it's needed to detect changes in a parent's -children. This method is called whenever a child is added to or removed from a parent (this includes -if a child is changing its parent). Its parameters contain the targeting child and the type of -change it went through (`added` or `removed`). - -A component lifecycle state can be checked by a series of getters: - -- `isLoaded`: Returns a bool with the current loaded state. -- `loaded`: Returns a future that will complete once the component has finished loading. -- `isMounted`: Returns a bool with the current mounted state. -- `mounted`: Returns a future that will complete once the component has finished mounting. -- `isRemoved`: Returns a bool with the current removed state. -- `removed`: Returns a future that will complete once the component has been removed. - - -### Priority - -In Flame every `Component` has the `int priority` property, which determines -that component's sorting order within its parent's children. This is sometimes referred to -as `z-index` in other languages and frameworks. The higher the `priority` is set to, the -closer the component will appear on the screen, since it will be rendered on top of any components -with lower priority that were rendered before it. - -If you add two components and set one of their priorities to 1 for example, then that component will -be rendered on top of the other component (if they overlap), because the default priority is 0. - -All components take in `priority` as a named argument, so if you know the priority that you want -your component at compile time, then you can pass it in to the constructor. - -Example: - -```dart -class MyGame extends FlameGame { - @override - void onLoad() { - final myComponent = PositionComponent(priority: 5); - add(myComponent); - } -} -``` - -To update the priority of a component you have to set it to a new value, like -`component.priority = 2`, and it will be updated in the current tick before the rendering stage. - -In the following example we first initialize the component with priority 1, and then when the -user taps the component we change its priority to 2: - -```dart -class MyComponent extends PositionComponent with TapCallbacks { - - MyComponent() : super(priority: 1); - - @override - void onTapDown(TapDownEvent event) { - priority = 2; - } -} -``` - - -### Composability of components - -Sometimes it is useful to wrap other components inside of your component. For example by grouping -visual components through a hierarchy. You can do this by adding child components to any component, -for example `PositionComponent`. - -When you have child components on a component every time the parent is updated and rendered, all the -children are rendered and updated with the same conditions. - -Here's an example where visibility of two components are handled by a wrapper: - -```dart -class GameOverPanel extends PositionComponent { - bool visible = false; - final Image spriteImage; - - GameOverPanel(this.spriteImage); - - @override - void onLoad() { - final gameOverText = GameOverText(spriteImage); // GameOverText is a Component - final gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent - - add(gameOverText); - add(gameOverButton); - } - - @override - void render(Canvas canvas) { - if (visible) { - } // If not visible none of the children will be rendered - } -} -``` - -There are two methods for adding children components to your component. First, -you have methods `add()`, `addAll()`, and `addToParent()`, which can be used -at any time during the game. Traditionally, children will be created and added -from the component's `onLoad()` method, but it is also common to add new -children during the course of the game. - -The second method is to use the `children:` parameter in the component's -constructor. This approach more closely resembles the standard Flutter API: - -```dart -class MyGame extends FlameGame { - @override - void onLoad() { - add( - PositionComponent( - position: Vector2(30, 0), - children: [ - HighScoreDisplay(), - HitPointsDisplay(), - FpsComponent(), - ], - ), - ); - } -} -``` - -The two approaches can be combined freely: the children specified within the -constructor will be added first, and then any additional child components -after. - -Note that the children added via either methods are only guaranteed to be -available eventually: after they are loaded and mounted. We can only assure -that they will appear in the children list in the same order as they were -scheduled for addition. - - -### Access to the World from a Component - -If a component that has a `World` as an ancestor and requires access to that `World` object, one can -use the `HasWorldReference` mixin. - -Example: - -```dart -class MyComponent extends Component with HasWorldReference, - TapCallbacks { - @override - void onTapDown(TapDownEvent info) { - // world is of type MyWorld - world.add(AnotherComponent()); - } -} -``` - -If you try to access `world` from a component that doesn't have a `World` -ancestor of the correct type an assertion error will be thrown. - - -### Ensuring a component has a given parent - -When a component requires to be added to a specific parent type the -`ParentIsA` mixin can be used to enforce a strongly typed parent. - -Example: - -```dart -class MyComponent extends Component with ParentIsA { - @override - void onLoad() { - // parent is of type MyParentComponent - print(parent.myValue); - } -} -``` - -If you try to add `MyComponent` to a parent that is not `MyParentComponent`, -an assertion error will be thrown. - - -### Ensuring a component has a given ancestor - -When a component requires to have a specific ancestor type somewhere in the -component tree, `HasAncestor` mixin can be used to enforce that relationship. - -The mixin exposes the `ancestor` field that will be of the given type. - -Example: - -```dart -class MyComponent extends Component with HasAncestor { - @override - void onLoad() { - // ancestor is of type MyAncestorComponent. - print(ancestor.myValue); - } -} -``` - -If you try to add `MyComponent` to a tree that does not contain `MyAncestorComponent`, -an assertion error will be thrown. - - -### Component Keys - -Components can have an identification key that allows them to be retrieved from the component tree, from -any point of the tree. - -To register a component with a key, simply pass a key to the `key` argument on the component's -constructor: - -```dart -final myComponent = Component( - key: ComponentKey.named('player'), -); -``` - -Then, to retrieve it in a different point of the component tree: - -```dart -flameGame.findByKey(ComponentKey.named('player')); -``` - -There are two types of keys, `unique` and `named`. Unique keys are based on equality of the key -instance, meaning that: - -```dart -final key = ComponentKey.unique(); -final key2 = key; -print(key == key2); // true -print(key == ComponentKey.unique()); // false -``` - -Named ones are based on the name that it receives, so: - -```dart -final key1 = ComponentKey.named('player'); -final key2 = ComponentKey.named('player'); -print(key1 == key2); // true -``` - -When named keys are used, the `findByKeyName` helper can also be used to retrieve the component. - - -```dart -flameGame.findByKeyName('player'); -``` - - -### Querying child components - -The children that have been added to a component live in a `QueryableOrderedSet` called -`children`. To query for a specific type of components in the set, the `query()` function can be -used. By default `strictMode` is `false` in the children set, but if you set it to true, then the -queries will have to be registered with `children.register` before a query can be used. - -If you know in compile time that you later will run a query of a specific type it is recommended to -register the query, no matter if the `strictMode` is set to `true` or `false`, since there are some -performance benefits to gain from it. The `register` call is usually done in `onLoad`. - -Example: - -```dart -@override -void onLoad() { - children.register(); -} -``` - -In the example above a query is registered for `PositionComponent`s, and an example of how to query -the registered component type can be seen below. - -```dart -@override -void update(double dt) { - final allPositionComponents = children.query(); -} -``` - - -### Querying components at a specific point on the screen - -The method `componentsAtPoint()` allows you to check which components were rendered at some point -on the screen. The returned value is an iterable of components, but you can also obtain the -coordinates of the initial point in each component's local coordinate space by providing a writable -`List` as a second parameter. - -The iterable retrieves the components in the front-to-back order, i.e. first the components in the -front, followed by the components in the back. - -This method can only return components that implement the method `containsLocalPoint()`. The -`PositionComponent` (which is the base class for many components in Flame) provides such an -implementation. However, if you're defining a custom class that derives from `Component`, you'd have -to implement the `containsLocalPoint()` method yourself. - -Here is an example of how `componentsAtPoint()` can be used: - -```dart -void onDragUpdate(DragUpdateInfo info) { - game.componentsAtPoint(info.widget).forEach((component) { - if (component is DropTarget) { - component.highlight(); - } - }); -} -``` - - -### Visibility of components - -The recommended way to hide or show a component is usually to add or remove it from the tree -using the `add` and `remove` methods. - -However, adding and removing components from the tree will trigger lifecycle steps for that -component (such as calling `onRemove` and `onMount`). It is also an asynchronous process and care -needs to be taken to ensure the component has finished removing before it is added again if you -are removing and adding a component in quick succession. - -```dart -/// Example of handling the removal and adding of a child component -/// in quick succession -void show() async { - // Need to await the [removed] future first, just in case the - // component is still in the process of being removed. - await myChildComponent.removed; - add(myChildComponent); -} - -void hide() { - remove(myChildComponent); -} -``` - -These behaviors are not always desirable. - -An alternative method to show and hide a component is to use the `HasVisibility` mixin, which may -be used on any class that inherits from `Component`. This mixin introduces the `isVisible` property. -Simply set `isVisible` to `false` to hide the component, and `true` to show it again, without -removing it from the tree. This affects the visibility of the component and all it's descendants -(children). - -```dart -/// Example that implements HasVisibility -class MyComponent extends PositionComponent with HasVisibility {} - -/// Usage of the isVisible property -final myComponent = MyComponent(); -add(myComponent); - -myComponent.isVisible = false; -``` - -The mixin only affects whether the component is rendered, and will not affect other behaviors. - -```{note} -Important! Even when the component is not visible, it is still in the tree and -will continue to receive calls to 'update' and all other lifecycle events. It -will still respond to input events, and will still interact with other -components, such as collision detection for example. -``` - -The mixin works by preventing the `renderTree` method, therefore if `renderTree` is being -overridden, a manual check for `isVisible` should be included to retain this functionality. - -```dart -class MyComponent extends PositionComponent with HasVisibility { - - @override - void renderTree(Canvas canvas) { - // Check for visibility - if (isVisible) { - // Custom code here - - // Continue rendering the tree - super.renderTree(canvas); - } - } -} -``` - - -### Render Contexts - -If you want a parent component to pass render-specific properties down its children tree, you -can override the `renderContext` property on the parent component. You can return a custom -class that inherits from `RenderContext`, and then use `findRenderContext` on the children -while rendering. Render Contexts are stored as a stack and propagated whenever the render -tree is navigated for rendering. - -For example: - -```dart -class IntContext extends ComponentRenderContext { - int value; - - IntContext(this.value); -} - -class ParentWithContext extends Component { - @override - IntContext renderContext = IntContext(42); -} - -class ChildReadsContext extends Component { - @override - void render(Canvas canvas) { - final context = findRenderContext(); - // context.value available - } -} -``` - -Each component will have access to the context of any parent that is above it in the -component tree. If multiple components add the contexts matching the selected type -`T`, the "closest" one will be returned (though typically you would create a unique -context type for each component). - - -## PositionComponent - -This class represents a positioned object on the screen, being a floating rectangle, a rotating -sprite, or anything else with position and size. It can also represent a group of positioned -components if children are added to it. - -The base of the `PositionComponent` is that it has a `position`, `size`, `scale`, `angle` and -`anchor` which transforms how the component is rendered. - - -### Position - -The `position` is just a `Vector2` which represents the position of the component's anchor in -relation to its parent; if the parent is a `FlameGame`, it is in relation to the viewport. - - -### Size - -The `size` of the component when the zoom level of the camera is 1.0 (no zoom, default). -The `size` is *not* in relation to the parent of the component. - - -### Scale - -The `scale` is how much the component and its children should be scaled. Since it is represented -by a `Vector2`, you can scale in a uniform way by changing `x` and `y` with the same amount, or in a -non-uniform way, by change `x` or `y` by different amounts. - - -### Angle - -The `angle` is the rotation angle around the anchor, represented as a double in radians. It is -relative to the parent's angle. - - -### Native Angle - -The `nativeAngle` is an angle in radians, measured clockwise, representing the default orientation -of the component. It can be used to define the direction in which the component is facing when -[angle](#angle) is zero. - -It is specially helpful when making a sprite based component look at a specific target. If the -original image of the sprite is not facing in the up/north direction, the calculated angle to make -the component look at the target will need some offset to make it look correct. For such cases, -`nativeAngle` can be used to let the component know what direction the original image is faces. - -An example could be a bullet image pointing in east direction. In this case `nativeAngle` can be set -to pi/2 radians. Following are some common directions and their corresponding native angle values. - -Direction | Native Angle | In degrees -----------|--------------|------------- -Up/North | 0 | 0 -Down/South| pi or -pi | 180 or -180 -Left/West | -pi/2 | -90 -Right/East| pi/2 | 90 - - -### Anchor - -```{flutter-app} -:sources: ../flame/examples -:page: anchor -:show: widget code infobox -This example shows effect of changing `anchor` point of parent (red) and child (blue) -components. Tap on them to cycle through the anchor points. Note that the local -position of the child component is (0, 0) at all times. -``` - -The `anchor` is where on the component that the position and rotation should be defined from (the -default is `Anchor.topLeft`). So if you have the anchor set as `Anchor.center` the component's -position on the screen will be in the center of the component and if an `angle` is applied, it is -rotated around the anchor, so in this case around the center of the component. You can think of it -as the point within the component by which Flame "grabs" it. - -When `position` or `absolutePosition` of a component is queried, the returned coordinates are that of -the `anchor` of the component. In case if you want to find the position of a specific anchor point -of a component which is not actually the `anchor` of that component, you can use the `positionOfAnchor` -and `absolutePositionOfAnchor` method. - -```dart -final comp = PositionComponent( - size: Vector2.all(20), - anchor: Anchor.center, -); - -// Returns (0,0) -final p1 = component.position; - -// Returns (10, 10) -final p2 = component.positionOfAnchor(Anchor.bottomRight); -``` - -A common pitfall when using `anchor` is confusing it as being the attachment point for children -components. For example, setting `anchor` to `Anchor.center` for a parent component does not mean -that the children components will be placed w.r.t the center of parent. - -```{note} -Local origin for a child component is always the top-left corner of its parent component, -irrespective of their `anchor` values. -``` - - -### PositionComponent children - -All children of the `PositionComponent` will be transformed in relation to the parent, which means -that the `position`, `angle` and `scale` will be relative to the parents state. -So if you, for example, wanted to position a child in the center of the parent you would do this: - -```dart -@override -void onLoad() { - final parent = PositionComponent( - position: Vector2(100, 100), - size: Vector2(100, 100), - ); - final child = PositionComponent( - position: parent.size / 2, - anchor: Anchor.center, - ); - parent.add(child); -} -``` - -Remember that most components that are rendered on the screen are `PositionComponent`s, so -this pattern can be used in for example [](#spritecomponent) and [](#spriteanimationcomponent) too. - - -### Render PositionComponent - -When implementing the `render` method for a component that extends `PositionComponent` remember to -render from the top left corner (0.0). Your render method should not handle where on the screen your -component should be rendered. To handle where and how your component should be rendered use the -`position`, `angle` and `anchor` properties and Flame will automatically handle the rest for you. - -If you want to know where on the screen the bounding box of the component is you can use the -`toRect` method. - -In the event that you want to change the direction of your components rendering, you can also use -`flipHorizontally()` and `flipVertically()` to flip anything drawn to canvas during -`render(Canvas canvas)`, around the anchor point. These methods are available on all -`PositionComponent` objects, and are especially useful on `SpriteComponent` and -`SpriteAnimationComponent`. - -In case you want to flip a component around its center without having to change the anchor to -`Anchor.center`, you can use `flipHorizontallyAroundCenter()` and `flipVerticallyAroundCenter()`. - - -## SpriteComponent - -The most commonly used implementation of `PositionComponent` is `SpriteComponent`, and it can be -created with a `Sprite`: - -```dart -import 'package:flame/components/component.dart'; - -class MyGame extends FlameGame { - late final SpriteComponent player; - - @override - Future onLoad() async { - final sprite = await Sprite.load('player.png'); - final size = Vector2.all(128.0); - final player = SpriteComponent(size: size, sprite: sprite); - - // Vector2(0.0, 0.0) by default, can also be set in the constructor - player.position = Vector2(10, 20); - - // 0 by default, can also be set in the constructor - player.angle = 0; - - // Adds the component - add(player); - } -} -``` - - -## SpriteAnimationComponent - -This class is used to represent a Component that has sprites that run in a single cyclic animation. - -This will create a simple three frame animation using 3 different images: - -```dart -@override -Future onLoad() async { - final sprites = [0, 1, 2] - .map((i) => Sprite.load('player_$i.png')); - final animation = SpriteAnimation.spriteList( - await Future.wait(sprites), - stepTime: 0.01, - ); - this.player = SpriteAnimationComponent( - animation: animation, - size: Vector2.all(64.0), - ); -} -``` - -If you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData` -class (check more details on [Images > Animation](rendering/images.md#animation)): - -```dart -@override -Future onLoad() async { - final size = Vector2.all(64.0); - final data = SpriteAnimationData.sequenced( - textureSize: size, - amount: 2, - stepTime: 0.1, - ); - this.player = SpriteAnimationComponent.fromFrameData( - await images.load('player.png'), - data, - ); -} -``` - -All animation components internally maintain a `SpriteAnimationTicker` which ticks the `SpriteAnimation`. -This allows multiple components to share the same animation object. - -Example: - -```dart -final sprites = [/*Your sprite list here*/]; -final animation = SpriteAnimation.spriteList(sprites, stepTime: 0.01); - -final animationTicker = SpriteAnimationTicker(animation); - -// or alternatively, you can ask the animation object to create one for you. - -final animationTicker = animation.createTicker(); // creates a new ticker - -animationTicker.update(dt); -``` - -To listen when the animation is done (when it reaches the last frame and is not looping) you can -use `animationTicker.completed`. - -Example: - -```dart -await animationTicker.completed; - -doSomething(); - -// or alternatively - -animationTicker.completed.whenComplete(doSomething); -``` - -Additionally, `SpriteAnimationTicker` also has the following optional event callbacks: `onStart`, `onFrame`, -and `onComplete`. To listen to these events, you can do the following: - -```dart -final animationTicker = SpriteAnimationTicker(animation) - ..onStart = () { - // Do something on start. - }; - -final animationTicker = SpriteAnimationTicker(animation) - ..onComplete = () { - // Do something on completion. - }; - -final animationTicker = SpriteAnimationTicker(animation) - ..onFrame = (index) { - if (index == 1) { - // Do something for the second frame. - } - }; -``` - -To reset the animation to the first frame when the component is removed, you can set -`resetOnRemove` to `true`: - -```dart -SpriteAnimationComponent( - animation: animation, - size: Vector2.all(64.0), - resetOnRemove: true, -); -``` - - -## SpriteAnimationGroupComponent - -`SpriteAnimationGroupComponent` is a simple wrapper around `SpriteAnimationComponent` which enables -your component to hold several animations and change the current playing animation at runtime. Since -this component is just a wrapper, the event listeners can be implemented as described in -[](#spriteanimationcomponent). - -Its use is very similar to the `SpriteAnimationComponent` but instead of being initialized with a -single animation, this component receives a Map of a generic type `T` as key and a -`SpriteAnimation` as value, and the current animation. - -Example: - -```dart -enum RobotState { - idle, - running, -} - -final running = await loadSpriteAnimation(/* omitted */); -final idle = await loadSpriteAnimation(/* omitted */); - -final robot = SpriteAnimationGroupComponent( - animations: { - RobotState.running: running, - RobotState.idle: idle, - }, - current: RobotState.idle, -); - -// Changes current animation to "running" -robot.current = RobotState.running; -``` - -As this component works with multiple `SpriteAnimation`s, naturally it needs equal number of animation -tickers to make all those animation tick. Use `animationsTickers` getter to access a map containing tickers -for each animation state. This can be useful if you want to register callbacks for `onStart`, `onComplete` -and `onFrame`. - -Example: - -```dart -enum RobotState { idle, running, jump } - -final running = await loadSpriteAnimation(/* omitted */); -final idle = await loadSpriteAnimation(/* omitted */); - -final robot = SpriteAnimationGroupComponent( - animations: { - RobotState.running: running, - RobotState.idle: idle, - }, - current: RobotState.idle, -); - -robot.animationTickers?[RobotState.running]?.onStart = () { - // Do something on start of running animation. -}; - -robot.animationTickers?[RobotState.jump]?.onStart = () { - // Do something on start of jump animation. -}; - -robot.animationTickers?[RobotState.jump]?.onComplete = () { - // Do something on complete of jump animation. -}; - -robot.animationTickers?[RobotState.idle]?.onFrame = (currentIndex) { - // Do something based on current frame index of idle animation. -}; -``` - - -## SpriteGroupComponent - -`SpriteGroupComponent` is pretty similar to its animation counterpart, but especially for sprites. - -Example: - -```dart -class PlayerComponent extends SpriteGroupComponent - with HasGameReference, TapCallbacks { - @override - Future? onLoad() async { - final pressedSprite = await gameRef.loadSprite(/* omitted */); - final unpressedSprite = await gameRef.loadSprite(/* omitted */); - - sprites = { - ButtonState.pressed: pressedSprite, - ButtonState.unpressed: unpressedSprite, - }; - - current = ButtonState.unpressed; - } - - // tap methods handler omitted... -} -``` - - -## IconComponent - -`IconComponent` renders a Flutter `IconData` (such as `Icons.star`) as a Flame component. The icon -is rasterized to an image once during `onLoad()` and then drawn each frame using -`canvas.drawImageRect()` with the component's `Paint`. Because the icon is rendered as a cached -image rather than as text, all paint-based effects work out of the box — including `tint()`, -`setOpacity()`, `ColorEffect`, `OpacityEffect`, `GlowEffect`, and custom `ColorFilter`s. - - -### Basic usage - -```dart -import 'package:flame/components.dart'; -import 'package:flutter/material.dart'; - -class MyGame extends FlameGame { - @override - Future onLoad() async { - final star = IconComponent( - icon: Icons.star, - iconSize: 64, - position: Vector2(100, 100), - ); - add(star); - } -} -``` - - -### Tinting and effects - -The icon is rasterized in white, which allows you to tint it to any color using -`HasPaint` methods: - -```dart -// Tint the icon gold -final star = IconComponent( - icon: Icons.star, - iconSize: 64, - position: Vector2(100, 100), -)..tint(const Color(0xFFFFD700)); - -// Set opacity -star.setOpacity(0.5); - -// Or use a custom paint -final icon = IconComponent( - icon: Icons.favorite, - iconSize: 48, - paint: Paint()..colorFilter = const ColorFilter.mode( - Color(0xFFFF0000), - BlendMode.srcATop, - ), -); -``` - - -### Constructor parameters - -- `icon` — The `IconData` to render (e.g., `Icons.star`, `Icons.favorite`). -- `iconSize` — The resolution at which the icon is rasterized (default `64`). This is independent - of the component's display `size`. -- `size` — The display size of the component. Defaults to `Vector2.all(iconSize)` if not provided. -- `paint` — Optional `Paint` for rendering effects. -- All standard `PositionComponent` parameters (`position`, `scale`, `angle`, `anchor`, etc.). - - -### Changing the icon at runtime - -Both the `icon` and `iconSize` properties can be changed after creation. The component will -automatically re-rasterize the icon on the next frame: - -```dart -final iconComponent = IconComponent( - icon: Icons.play_arrow, - iconSize: 64, -); - -// Later, swap the icon -iconComponent.icon = Icons.pause; - -// Or change the rasterization resolution -iconComponent.iconSize = 128; -``` - - -## SpawnComponent - -This component is a non-visual component that spawns other components inside of the parent of the -`SpawnComponent`. It's great if you for example want to spawn enemies or power-ups randomly within -an area. - -The `SpawnComponent` takes a factory function that it uses to create new components and an area -where the components should be spawned within (or along the edges of). - -For the area, you can use the `Circle`, `Rectangle` or `Polygon` class, and if you want to only -spawn components along the edges of the shape set the `within` argument to false (defaults to true). - -This would for example spawn new components of the type `MyComponent` every 0.5 seconds randomly -within the defined circle: - -The component supports two types of factories. The `factory` returns a single component and the -`multiFactory` returns a list of components that are added in a single step. - -The factory functions takes an `int` as an argument, which is the number of components that have -been spawned, so if for example 4 components have been spawned already the 5th call of the factory -method will be called with the `amount=4`, since the counting starts at 0 for the first call. - -The `factory` with a single component is for backward compatibility, so you should use the -`multiFactory` if in doubt. A single component `factory` will be wrapped internally to return a -single item list and then used as the `multiFactory`. - -If you only want to spawn a certain amount of components, you can use the `spawnCount` argument, -and once the limit is reached the `SpawnComponent` will stop spawning and remove itself. - -By default, the `SpawnComponent` will spawn components to its parent, but if you want to spawn -components to another component you can set the `target` argument. Remember that it should be a -`Component` that has a size if you don't use the `area` or `selfPositioning` arguments. - - -```dart -SpawnComponent( - factory: (i) => MyComponent(size: Vector2(10, 20)), - period: 0.5, - area: Circle(Vector2(100, 200), 150), -); -``` - -If you don't want the spawning rate to be static, you can use the `SpawnComponent.periodRange` -constructor with the `minPeriod` and `maxPeriod` arguments instead. -In the following example the component would be spawned randomly within the circle and the time -between each new spawned component is between 0.5 to 10 seconds. - -```dart -SpawnComponent.periodRange( - factory: (i) => MyComponent(size: Vector2(10, 20)), - minPeriod: 0.5, - maxPeriod: 10, - area: Circle(Vector2(100, 200), 150), -); -``` - -If you want to set the position yourself within the `factory` function, you can use set -`selfPositioning = true` in the constructors and you will be able to set the positions yourself and -ignore the `area` argument. - -```dart -SpawnComponent( - factory: (i) => - MyComponent(position: Vector2(100, 200), size: Vector2(10, 20)), - selfPositioning: true, - period: 0.5, -); -``` - - -## SvgComponent - -**Note**: To use SVG with Flame, use the [`flame_svg`](https://github.com/flame-engine/flame_svg) -package. - -This component uses an instance of `Svg` class to represent a Component that has a svg that is -rendered in the game: - -```dart -@override -Future onLoad() async { - final svg = await Svg.load('android.svg'); - final android = SvgComponent.fromSvg( - svg, - position: Vector2.all(100), - size: Vector2.all(100), - ); -} -``` - - -## ParallaxComponent - -This `Component` can be used to render backgrounds with a depth feeling by drawing several -transparent images on top of each other, where each image or animation (`ParallaxRenderer`) is -moving with a different velocity. - -The rationale is that when you look at the horizon and moving, closer objects seem to move faster -than distant ones. - -This component simulates this effect, making a more realistic background effect. - -The simplest `ParallaxComponent` is created like this: - -```dart -@override -Future onLoad() async { - final parallaxComponent = await loadParallaxComponent([ - ParallaxImageData('bg.png'), - ParallaxImageData('trees.png'), - ]); - add(parallaxComponent); -} -``` - -A ParallaxComponent can also "load itself" by implementing the `onLoad` method: - -```dart -class MyParallaxComponent extends ParallaxComponent { - @override - Future onLoad() async { - parallax = await gameRef.loadParallax([ - ParallaxImageData('bg.png'), - ParallaxImageData('trees.png'), - ]); - } -} - -class MyGame extends FlameGame { - @override - void onLoad() { - add(MyParallaxComponent()); - } -} -``` - -This creates a static background. If you want a moving parallax (which is the whole point of a -parallax), you can do it in a few different ways depending on how fine-grained you want to set the -settings for each layer. - -They simplest way is to set the named optional parameters `baseVelocity` and -`velocityMultiplierDelta` in the `load` helper function. For example if you want to move your -background images along the X-axis with a faster speed the "closer" the image is: - -```dart -@override -Future onLoad() async { - final parallaxComponent = await loadParallaxComponent( - _dataList, - baseVelocity: Vector2(20, 0), - velocityMultiplierDelta: Vector2(1.8, 1.0), - ); -} -``` - -You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your -game speeds up. - -```dart -@override -void onLoad() { - final parallax = parallaxComponent.parallax; - parallax.baseSpeed = Vector2(100, 0); - parallax.velocityMultiplierDelta = Vector2(2.0, 1.0); -} -``` - -By default, the images are aligned to the bottom left, repeated along the X-axis and scaled -proportionally so that the image covers the height of the screen. If you want to change this -behavior, for example if you are not making a side-scrolling game, you can set the `repeat`, -`alignment` and `fill` parameters for each `ParallaxRenderer` and add them to `ParallaxLayer`s that -you then pass in to the `ParallaxComponent`'s constructor. - -Advanced example: - -```dart -final images = [ - loadParallaxImage( - 'stars.jpg', - repeat: ImageRepeat.repeat, - alignment: Alignment.center, - fill: LayerFill.width, - ), - loadParallaxImage( - 'planets.jpg', - repeat: ImageRepeat.repeatY, - alignment: Alignment.bottomLeft, - fill: LayerFill.none, - ), - loadParallaxImage( - 'dust.jpg', - repeat: ImageRepeat.repeatX, - alignment: Alignment.topRight, - fill: LayerFill.height, - ), -]; - -final layers = images.map( - (image) => ParallaxLayer( - await image, - velocityMultiplier: images.indexOf(image) * 2.0, - ) -); - -final parallaxComponent = ParallaxComponent.fromParallax( - Parallax( - await Future.wait(layers), - baseVelocity: Vector2(50, 0), - ), -); -``` - -- The stars image in this example will be repeatedly drawn in both axis, align in the center and be - scaled to fill the screen width. -- The planets image will be repeated in Y-axis, aligned to the bottom left of the screen and not be - scaled. -- The dust image will be repeated in X-axis, aligned to the top right and scaled to fill the screen - height. - -Once you are done setting up your `ParallaxComponent`, add it to the game like with any other -component (`game.add(parallaxComponent`). -Also, don't forget to add you images to the `pubspec.yaml` file as assets or they wont be found. - -The `Parallax` file contains an extension of the game which adds `loadParallax`, `loadParallaxLayer` -, `loadParallaxImage` and `loadParallaxAnimation` so that it automatically uses your game's image -cache instead of the global one. The same goes for the `ParallaxComponent` file, but that provides -`loadParallaxComponent`. - -If you want a fullscreen `ParallaxComponent` simply omit the `size` argument and it will take the -size of the game, it will also resize to fullscreen when the game changes size or orientation. - -Flame provides two kinds of `ParallaxRenderer`: `ParallaxImage` and `ParallaxAnimation`, -`ParallaxImage` is a static image renderer and `ParallaxAnimation` is, as it's name implies, an -animation and frame based renderer. -It is also possible to create custom renderers by extending the `ParallaxRenderer` class. - -Three example implementations can be found in the -[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax). - - -## ShapeComponents - -A `ShapeComponent` is the base class for representing a scalable geometrical shape. The shapes have -different ways of defining how they look, but they all have a size and angle that can be modified -and the shape definition will scale or rotate the shape accordingly. - -These shapes are meant as a tool for using geometrical shapes in a more general way than together -with the collision detection system, where you want to use the -[ShapeHitbox](collision_detection.md#shapehitbox)es. - - -### PolygonComponent - -A `PolygonComponent` is created by giving it a list of points in the constructor, called vertices. -This list will be transformed into a polygon with a size, which can still be scaled and rotated. - -For example, this would create a square going from (50, 50) to (100, 100), with it's center in -(75, 75): - -```dart -void main() { - PolygonComponent([ - Vector2(100, 100), - Vector2(100, 50), - Vector2(50, 50), - Vector2(50, 100), - ]); -} -``` - -A `PolygonComponent` can also be created with a list of relative vertices, which are points defined -in relation to the given size, most often the size of the intended parent. - -For example you could create a diamond shapes polygon like this: - -```dart -void main() { - PolygonComponent.relative( - [ - Vector2(0.0, -1.0), // Middle of top wall - Vector2(1.0, 0.0), // Middle of right wall - Vector2(0.0, 1.0), // Middle of bottom wall - Vector2(-1.0, 0.0), // Middle of left wall - ], - size: Vector2.all(100), - ); -} -``` - -The vertices in the example defines percentages of the length from the center to the edge of the -screen in both x and y axis, so for our first item in our list (`Vector2(0.0, -1.0)`) we are pointing -on the middle of the top wall of the bounding box, since the coordinate system here is defined from -the center of the polygon. - -![An example of how to define a polygon shape](../images/polygon_shape.png) - -In the image you can see how the polygon shape formed by the purple arrows is defined by the red -arrows. - - -### RectangleComponent - -A `RectangleComponent` is created very similarly to how a `PositionComponent` is created, since it -also has a bounding rectangle. - -Something like this for example: - -```dart -void main() { - RectangleComponent( - position: Vector2(10.0, 15.0), - size: Vector2.all(10), - angle: pi/2, - anchor: Anchor.center, - ); -} -``` - -Dart also already has an excellent way to create rectangles and that class is called `Rect`, you can -create a Flame `RectangleComponent` from a `Rect` by using the `RectangleComponent.fromRect` factory, -and just like when setting the vertices of the `PolygonComponent`, your rectangle will be sized -according to the `Rect` if you use this constructor. - -The following would create a `RectangleComponent` with its top left corner in `(10, 10)` and a size -of `(100, 50)`. - -```dart -void main() { - RectangleComponent.fromRect( - Rect.fromLTWH(10, 10, 100, 50), - ); -} -``` - -You can also create a `RectangleComponent` by defining a relation to the intended parent's size, -you can use the default constructor to build your rectangle from a position, size and angle. The -`relation` is a vector defined in relation to the parent size, for example a `relation` that is -`Vector2(0.5, 0.8)` would create a rectangle that is 50% of the width of the parent's size and -80% of its height. - -In the example below a `RectangleComponent` of size `(25.0, 30.0)` positioned at `(100, 100)` would -be created. - -```dart -void main() { - RectangleComponent.relative( - Vector2(0.5, 1.0), - position: Vector2.all(100), - size: Vector2(50, 30), - ); -} -``` - -Since a square is a simplified version of a rectangle, there is also a constructor for creating a -square `RectangleComponent`, the only difference is that the `size` argument is a `double` instead -of a `Vector2`. - -```dart -void main() { - RectangleComponent.square( - position: Vector2.all(100), - size: 200, - ); -} -``` - - -### CircleComponent - -If you know how long your circle's position and/or how long the radius is going to be from the start -you can use the optional arguments `radius` and `position` to set those. - -The following would create a `CircleComponent` with its center in `(100, 100)` with a radius of 5, -and therefore a size of `Vector2(10, 10)`. - -```dart -void main() { - CircleComponent(radius: 5, position: Vector2.all(100), anchor: Anchor.center); -} -``` - -When creating a `CircleComponent` with the `relative` constructor you can define how long the -radius is in comparison to the shortest edge of the of the bounding box defined by `size`. - -The following example would result in a `CircleComponent` that defines a circle with a radius of 40 -(a diameter of 80). - -```dart -void main() { - CircleComponent.relative(0.8, size: Vector2.all(100)); -} -``` - - -## IsometricTileMapComponent - -This component allows you to render an isometric map based on a cartesian matrix of blocks and an -isometric tileset. - -A simple example on how to use it: - -```dart -// Creates a tileset, the block ids are automatically assigned sequentially -// starting at 0, from left to right and then top to bottom. -final tilesetImage = await images.load('tileset.png'); -final tileset = SpriteSheet(image: tilesetImage, srcSize: Vector2.all(32)); -// Each element is a block id, -1 means nothing -final matrix = [[0, 1, 0], [1, 0, 0], [1, 1, 1]]; -add(IsometricTileMapComponent(tileset, matrix)); -``` - -It also provides methods for converting coordinates so you can handle clicks, hovers, render -entities on top of tiles, add a selector, etc. - -You can also specify the `tileHeight`, which is the vertical distance between the bottom and top -planes of each cuboid in your tile. Basically, it's the height of the front-most edge of your -cuboid; normally it's half (default) or a quarter of the tile size. On the image below you can see -the height colored in the darker tone: - -![An example of how to determine the tileHeight](../images/tile-height-example.png) - -This is an example of how a quarter-length map looks like: - -![An example of a isometric map with selector](../images/isometric.png) - -Flame's Example app contains a more in-depth example, featuring how to parse coordinates to make a -selector. The code can be found -[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/isometric_tile_map_example.dart), -and a live version can be seen [here](https://examples.flame-engine.org/#/Rendering_Isometric_Tile_Map). - - -## NineTileBoxComponent - -A Nine Tile Box is a rectangle drawn using a grid sprite. - -The grid sprite is a 3x3 grid and with 9 blocks, representing the 4 corners, the 4 sides and the -middle. - -The corners are drawn at the same size, the sides are stretched on the side direction and the middle -is expanded both ways. - -Using this, you can get a box/rectangle that expands well to any sizes. This is useful for making -panels, dialogs, borders. - -Check the example app -[nine_tile_box](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/nine_tile_box_example.dart) -for details on how to use it. - - -## CustomPainterComponent - -A `CustomPainter` is a Flutter class used with the `CustomPaint` widget to render custom -shapes inside a Flutter application. - -Flame provides a component that can render a `CustomPainter` called `CustomPainterComponent`, it -receives a custom painter and renders it on the game canvas. - -This can be used for sharing custom rendering logic between your Flame game, and your Flutter -widgets. - -Check the example app -[custom_painter_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/custom_painter_example.dart) -for details on how to use it. - - -## ComponentsNotifier - -Most of the time just accessing children and their attributes is enough to build the logic of -your game. - -But sometimes, reactivity can help the developer to simplify and write better code, to help with -that Flame provides the `ComponentsNotifier`, which is an implementation of a -`ChangeNotifier` that notifies listeners every time a component is added, removed or manually -changed. - -For example, lets say that we want to show a game over text when the player's lives reach zero. - -To make the component automatically report when new instances are added or removed, the `Notifier` -mixin can be applied to the component class: - -```dart -class Player extends SpriteComponent with Notifier {} -``` - -Then to listen to changes on that component the `componentsNotifier` method from `FlameGame` can -be used: - -```dart -class MyGame extends FlameGame { - int lives = 2; - - @override - void onLoad() { - final playerNotifier = componentsNotifier() - ..addListener(() { - final player = playerNotifier.single; - if (player == null) { - lives--; - if (lives == 0) { - add(GameOverComponent()); - } else { - add(Player()); - } - } - }); - } -} -``` - -A `Notifier` component can also manually notify its listeners that something changed. Lets expand -the example above to make a hud component to blink when the player has half of their health. In -order to do so, we need that the `Player` component notify a change manually, example: - -```dart -class Player extends SpriteComponent with Notifier { - double health = 1; - - void takeHit() { - health -= .1; - if (health == 0) { - removeFromParent(); - } else if (health <= .5) { - notifyListeners(); - } - } -} -``` - -Then our hud component could look like: - -```dart -class Hud extends PositionComponent with HasGameReference { - - @override - void onLoad() { - final playerNotifier = gameRef.componentsNotifier() - ..addListener(() { - final player = playerNotifier.single; - if (player != null) { - if (player.health <= .5) { - add(BlinkEffect()); - } - } - }); - } -} -``` - -`ComponentsNotifier`s can also come in handy to rebuild widgets when state changes inside a -`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget. - -To see an example of its use check the running example -[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/components_notifier_example.dart). - - -## ClipComponent - -A `ClipComponent` is a component that will clip the canvas to its size and shape. This means that -if the component itself or any child of the `ClipComponent` renders outside of the -`ClipComponent`'s boundaries, the part that is not inside the area will not be shown. - -A `ClipComponent` receives a builder function that should return the `Shape` that will define the -clipped area, based on its size. - -To make it easier to use that component, there are three factories that offers common shapes: - -- `ClipComponent.rectangle`: Clips the area in the form a rectangle based on its size. -- `ClipComponent.circle`: Clips the area in the form of a circle based on its size. -- `ClipComponent.polygon`: Clips the area in the form of a polygon based on the points received -in the constructor. - -Check the example app -[clip_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/clip_component_example.dart) -for details on how to use it. - - -## Effects - -Flame provides a set of effects that can be applied to a certain type of components, these effects -can be used to animate some properties of your components, like position or dimensions. -You can check the list of those effects [here](effects.md). - -Examples of the running effects can be found [here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/effects); - - -## When not using `FlameGame` - -If you are not using `FlameGame`, don't forget that all components needs to be updated every time your -game updates. This lets component perform their internal processing and update their state. - -For example, the `SpriteAnimationTicker` inside all the `SpriteAnimation` based components needs to tick -the animation object to decide which animation frame will be displayed next. This can be done by manually -calling `component.update()` when not using `FlameGame`. This also means, if you are implementing your -own sprite animation based component, you can directly use a `SpriteAnimationTicker` to update the `SpriteAnimation`. diff --git a/doc/flame/components/components.md b/doc/flame/components/components.md new file mode 100644 index 00000000000..fec38247d2a --- /dev/null +++ b/doc/flame/components/components.md @@ -0,0 +1,500 @@ +# Components + +In game development, a component is a self-contained unit that encapsulates a specific piece of game +behavior or visual. Flame uses the [Flame Component System](../game.md) (FCS) where every object in +your game (players, enemies, backgrounds, UI elements) is a component. This makes games easier to +build and maintain because each piece of logic lives in its own class and components can be freely +composed into a tree, much like +[Flutter's widget tree](https://docs.flutter.dev/get-started/fundamentals/widgets). + +- [Position Component](position_component.md) +- [Sprite Components](sprite_components.md) +- [Parallax Component](parallax_component.md) +- [Shape Components](shape_components.md) +- [Utility Components](utility_components.md) + +```{include} ../diagrams/component.md +``` + +This diagram might look intimidating, but don't worry, it is not as complex as it looks. + + +## Component + +All components inherit from the `Component` class and can have other `Component`s as children. +This is the base of what we call the Flame Component System, or FCS for short. + +Children can be added either with the `add(Component c)` method or directly in the constructor. + +Example: + +```dart +void main() { + final component1 = Component(children: [Component(), Component()]); + final component2 = Component(); + component2.add(Component()); + component2.addAll([Component(), Component()]); +} +``` + +The `Component()` here could of course be any subclass of `Component`. + +Every `Component` has a few methods that you can optionally implement, which are used by the +`FlameGame` class. + + +### Component lifecycle + +```{include} ../diagrams/component_life_cycle.md +``` + +The `onGameResize` method is called whenever the screen is resized, and also when this component +gets added into the component tree, before the `onMount`. + +The `onParentResize` method is similar: it is also called when the component is mounted into the +component tree, and also whenever the parent of the current component changes its size. + +The `onRemove` method can be overridden to run code before the component is removed from the game. +It is only run once even if the component is removed both by using the parent's remove method and +the `Component` remove method. + +The `onLoad` method can be overridden to run asynchronous initialization code for the component, +like loading an image for example. This method is executed before `onGameResize` and +`onMount`. This method is guaranteed to execute only once during the lifetime of the component, so +you can think of it as an "asynchronous constructor". + +The `onMount` method runs every time the component is mounted into a game tree. This means that +you should not initialize `late final` variables here, since this method might run several times +throughout the component's lifetime. This method will only run if the parent is already mounted. +If the parent is not mounted yet, then this method will wait in a queue (this will have no effect +on the rest of the game engine). + +The `onChildrenChanged` method can be overridden if it's needed to detect changes in a parent's +children. This method is called whenever a child is added to or removed from a parent (this includes +if a child is changing its parent). Its parameters contain the target child and the type of +change it went through (`added` or `removed`). + +A component's lifecycle state can be checked by a series of getters: + +- `isLoaded`: Returns a bool with the current loaded state. +- `loaded`: Returns a future that will complete once the component has finished loading. +- `isMounted`: Returns a bool with the current mounted state. +- `mounted`: Returns a future that will complete once the component has finished mounting. +- `isRemoved`: Returns a bool with the current removed state. +- `removed`: Returns a future that will complete once the component has been removed. + + +### Priority + +In Flame every `Component` has the `int priority` property, which determines +that component's sorting order within its parent's children. This is sometimes referred to +as `z-index` in other languages and frameworks. The higher the `priority` is set to, the +closer the component will appear on the screen, since it will be rendered on top of any components +with lower priority that were rendered before it. + +If you add two components and set one of their priorities to 1 for example, then that component will +be rendered on top of the other component (if they overlap), because the default priority is 0. + +All components take in `priority` as a named argument, so if you know the priority that you want +your component at compile time, then you can pass it in to the constructor. + +Example: + +```dart +class MyGame extends FlameGame { + @override + void onLoad() { + final myComponent = PositionComponent(priority: 5); + add(myComponent); + } +} +``` + +To update the priority of a component you have to set it to a new value, like +`component.priority = 2`, and it will be updated in the current tick before the rendering stage. + +In the following example we first initialize the component with priority 1, and then when the +user taps the component we change its priority to 2: + +```dart +class MyComponent extends PositionComponent with TapCallbacks { + + MyComponent() : super(priority: 1); + + @override + void onTapDown(TapDownEvent event) { + priority = 2; + } +} +``` + + +### Composability of components + +Sometimes it is useful to wrap other components inside of your component. For example by grouping +visual components through a hierarchy. You can do this by adding child components to any component, +for example `PositionComponent`. + +When you have child components on a component every time the parent is updated and rendered, all the +children are rendered and updated with the same conditions. + +Here's an example where the visibility of two components is handled by a wrapper: + +```dart +class GameOverPanel extends PositionComponent { + bool visible = false; + final Image spriteImage; + + GameOverPanel(this.spriteImage); + + @override + void onLoad() { + // GameOverText is a Component + final gameOverText = GameOverText(spriteImage); + // GameOverRestart is a SpriteComponent + final gameOverButton = GameOverButton(spriteImage); + + add(gameOverText); + add(gameOverButton); + } + + @override + void render(Canvas canvas) { + if (visible) { + } // If not visible none of the children will be rendered + } +} +``` + +There are two methods for adding child components to your component. First, you have methods +`add()`, `addAll()`, and `addToParent()`, which can be used at any time during the game. +Traditionally, children will be created and added from the component's `onLoad()` method, but it +is also common to add new children during the course of the game. + +The second method is to use the `children:` parameter in the component's constructor. This +approach more closely resembles the standard Flutter API: + +```dart +class MyGame extends FlameGame { + @override + void onLoad() { + add( + PositionComponent( + position: Vector2(30, 0), + children: [ + HighScoreDisplay(), + HitPointsDisplay(), + FpsComponent(), + ], + ), + ); + } +} +``` + +The two approaches can be combined freely: the children specified within the constructor will be +added first, and then any additional child components after. + +Note that the children added via either method are only guaranteed to be available eventually: +after they are loaded and mounted. We can only assure that they will appear in the children list +in the same order as they were scheduled for addition. + + +### Access to the World from a Component + +If a component that has a `World` as an ancestor and requires access to that `World` object, one +can use the `HasWorldReference` mixin. + +Example: + +```dart +class MyComponent extends Component with HasWorldReference, + TapCallbacks { + @override + void onTapDown(TapDownEvent info) { + // world is of type MyWorld + world.add(AnotherComponent()); + } +} +``` + +If you try to access `world` from a component that doesn't have a `World` ancestor of the +correct type an assertion error will be thrown. + + +### Ensuring a component has a given parent + +When a component needs to be added to a specific parent type, the `ParentIsA` mixin can be used +to enforce a strongly typed parent. + +Example: + +```dart +class MyComponent extends Component with ParentIsA { + @override + void onLoad() { + // parent is of type MyParentComponent + print(parent.myValue); + } +} +``` + +If you try to add `MyComponent` to a parent that is not `MyParentComponent`, an assertion error +will be thrown. + + +### Ensuring a component has a given ancestor + +When a component needs to have a specific ancestor type somewhere in the component tree, the +`HasAncestor` mixin can be used to enforce that relationship. + +The mixin exposes the `ancestor` field that will be of the given type. + +Example: + +```dart +class MyComponent extends Component with HasAncestor { + @override + void onLoad() { + // ancestor is of type MyAncestorComponent. + print(ancestor.myValue); + } +} +``` + +If you try to add `MyComponent` to a tree that does not contain `MyAncestorComponent`, an +assertion error will be thrown. + + +### Component Keys + +Components can have an identification key that allows them to be retrieved from the component +tree, from any point of the tree. + +To register a component with a key, simply pass a key to the `key` argument on the component's +constructor: + +```dart +final myComponent = Component( + key: ComponentKey.named('player'), +); +``` + +Then, to retrieve it in a different point of the component tree: + +```dart +flameGame.findByKey(ComponentKey.named('player')); +``` + +There are two types of keys, `unique` and `named`. Unique keys are based on equality of the key +instance, meaning that: + +```dart +final key = ComponentKey.unique(); +final key2 = key; +print(key == key2); // true +print(key == ComponentKey.unique()); // false +``` + +Named ones are based on the name that it receives, so: + +```dart +final key1 = ComponentKey.named('player'); +final key2 = ComponentKey.named('player'); +print(key1 == key2); // true +``` + +When named keys are used, the `findByKeyName` helper can also be used to retrieve the component. + + +```dart +flameGame.findByKeyName('player'); +``` + + +### Querying child components + +The children that have been added to a component live in a `QueryableOrderedSet` called +`children`. To query for a specific type of components in the set, the `query()` function can be +used. By default `strictMode` is `false` in the children set, but if you set it to true, then the +queries will have to be registered with `children.register` before a query can be used. + +If you know at compile time that you later will run a query of a specific type it is recommended to +register the query, no matter if the `strictMode` is set to `true` or `false`, since there are some +performance benefits to gain from it. The `register` call is usually done in `onLoad`. + +Example: + +```dart +@override +void onLoad() { + children.register(); +} +``` + +In the example above a query is registered for `PositionComponent`s, and an example of how to +query the registered component type can be seen below. + +```dart +@override +void update(double dt) { + final allPositionComponents = children.query(); +} +``` + + +### Querying components at a specific point on the screen + +The method `componentsAtPoint()` allows you to check which components were rendered at some point +on the screen. The returned value is an iterable of components, but you can also obtain the +coordinates of the initial point in each component's local coordinate space by providing a writable +`List` as a second parameter. + +The iterable retrieves the components in the front-to-back order, i.e. first the components in +the front, followed by the components in the back. + +This method can only return components that implement the method `containsLocalPoint()`. The +`PositionComponent` (which is the base class for many components in Flame) provides such an +implementation. However, if you're defining a custom class that derives from `Component`, you'd have +to implement the `containsLocalPoint()` method yourself. + +Here is an example of how `componentsAtPoint()` can be used: + +```dart +void onDragUpdate(DragUpdateInfo info) { + game.componentsAtPoint(info.widget).forEach((component) { + if (component is DropTarget) { + component.highlight(); + } + }); +} +``` + + +### Visibility of components + +The recommended way to hide or show a component is usually to add or remove it from the tree using +the `add` and `remove` methods. + +However, adding and removing components from the tree will trigger lifecycle steps for that +component (such as calling `onRemove` and `onMount`). It is also an asynchronous process and care +needs to be taken to ensure the component has finished removing before it is added again if you +are removing and adding a component in quick succession. + +```dart +/// Example of handling the removal and adding of a child component +/// in quick succession +void show() async { + // Need to await the [removed] future first, just in case the + // component is still in the process of being removed. + await myChildComponent.removed; + add(myChildComponent); +} + +void hide() { + remove(myChildComponent); +} +``` + +These behaviors are not always desirable. + +An alternative method to show and hide a component is to use the `HasVisibility` mixin, which may +be used on any class that inherits from `Component`. This mixin introduces the `isVisible` property. +Simply set `isVisible` to `false` to hide the component, and `true` to show it again, without +removing it from the tree. This affects the visibility of the component and all its descendants +(children). + +```dart +/// Example that implements HasVisibility +class MyComponent extends PositionComponent with HasVisibility {} + +/// Usage of the isVisible property +final myComponent = MyComponent(); +add(myComponent); + +myComponent.isVisible = false; +``` + +The mixin only affects whether the component is rendered, and will not affect other behaviors. + +```{note} +Important! Even when the component is not visible, it is still in the tree and +will continue to receive calls to 'update' and all other lifecycle events. It +will still respond to input events, and will still interact with other +components, such as collision detection for example. +``` + +The mixin works by preventing the `renderTree` method, therefore if `renderTree` is being +overridden, a manual check for `isVisible` should be included to retain this functionality. + +```dart +class MyComponent extends PositionComponent with HasVisibility { + + @override + void renderTree(Canvas canvas) { + // Check for visibility + if (isVisible) { + // Custom code here + + // Continue rendering the tree + super.renderTree(canvas); + } + } +} +``` + + +### Render Contexts + +If you want a parent component to pass render-specific properties down to its children tree, you +can override the `renderContext` property on the parent component. You can return a custom class +that inherits from `RenderContext`, and then use `findRenderContext` on the children while +rendering. Render Contexts are stored as a stack and propagated whenever the render tree is +navigated for rendering. + +For example: + +```dart +class IntContext extends ComponentRenderContext { + int value; + + IntContext(this.value); +} + +class ParentWithContext extends Component { + @override + IntContext renderContext = IntContext(42); +} + +class ChildReadsContext extends Component { + @override + void render(Canvas canvas) { + final context = findRenderContext(); + // context.value available + } +} +``` + +Each component will have access to the context of any parent that is above it in the component +tree. If multiple components add the contexts matching the selected type `T`, the "closest" one +will be returned (though typically you would create a unique context type for each component). + + +## Effects + +Flame provides a set of effects that can be applied to a certain type of components. These effects +can be used to animate some properties of your components, like position or dimensions. You can +check the [list of available effects](../effects/effects.md). + +Examples of the running effects can be found in the +[effects examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/effects). + +```{toctree} +:hidden: + +Position Component +Sprite Components +Parallax Component +Shape Components +Utility Components +``` diff --git a/doc/flame/components/parallax_component.md b/doc/flame/components/parallax_component.md new file mode 100644 index 00000000000..bd30a536dba --- /dev/null +++ b/doc/flame/components/parallax_component.md @@ -0,0 +1,148 @@ +# ParallaxComponent + +Parallax scrolling is a classic game development technique where background layers move at different +speeds to create an illusion of depth. Objects closer to the camera appear to move faster than those +far away. Just as when looking out a car window, nearby trees fly by while distant mountains barely +move. This effect makes 2D game worlds feel more immersive and is commonly used in side-scrollers, +platformers, and menu screens. + +This `Component` can be used to render backgrounds with a depth feeling by drawing several +transparent images on top of each other, where each image or animation (`ParallaxRenderer`) is +moving with a different velocity. + +The simplest `ParallaxComponent` is created like this: + +```dart +@override +Future onLoad() async { + final parallaxComponent = await loadParallaxComponent([ + ParallaxImageData('bg.png'), + ParallaxImageData('trees.png'), + ]); + add(parallaxComponent); +} +``` + +A ParallaxComponent can also "load itself" by implementing the `onLoad` method: + +```dart +class MyParallaxComponent extends ParallaxComponent { + @override + Future onLoad() async { + parallax = await game.loadParallax([ + ParallaxImageData('bg.png'), + ParallaxImageData('trees.png'), + ]); + } +} + +class MyGame extends FlameGame { + @override + void onLoad() { + add(MyParallaxComponent()); + } +} +``` + +This creates a static background. If you want a moving parallax (which is the whole point of a +parallax), you can do it in a few different ways depending on how fine-grained you want to set the +settings for each layer. + +The simplest way is to set the named optional parameters `baseVelocity` and +`velocityMultiplierDelta` in the `load` helper function. For example if you want to move your +background images along the X-axis with a faster speed the "closer" the image is: + +```dart +@override +Future onLoad() async { + final parallaxComponent = await loadParallaxComponent( + _dataList, + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + ); +} +``` + +You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your +game speeds up. + +```dart +@override +void onLoad() { + final parallax = parallaxComponent.parallax; + parallax.baseSpeed = Vector2(100, 0); + parallax.velocityMultiplierDelta = Vector2(2.0, 1.0); +} +``` + +By default, the images are aligned to the bottom left, repeated along the X-axis and scaled +proportionally so that the image covers the height of the screen. If you want to change this +behavior, for example if you are not making a side-scrolling game, you can set the `repeat`, +`alignment` and `fill` parameters for each `ParallaxRenderer` and add them to `ParallaxLayer`s that +you then pass in to the `ParallaxComponent`'s constructor. + +Advanced example: + +```dart +final images = [ + loadParallaxImage( + 'stars.jpg', + repeat: ImageRepeat.repeat, + alignment: Alignment.center, + fill: LayerFill.width, + ), + loadParallaxImage( + 'planets.jpg', + repeat: ImageRepeat.repeatY, + alignment: Alignment.bottomLeft, + fill: LayerFill.none, + ), + loadParallaxImage( + 'dust.jpg', + repeat: ImageRepeat.repeatX, + alignment: Alignment.topRight, + fill: LayerFill.height, + ), +]; + +final layers = images.map( + (image) => ParallaxLayer( + await image, + velocityMultiplier: images.indexOf(image) * 2.0, + ) +); + +final parallaxComponent = ParallaxComponent.fromParallax( + Parallax( + await Future.wait(layers), + baseVelocity: Vector2(50, 0), + ), +); +``` + +- The stars image in this example will be repeatedly drawn in both axes, align in the center and be + scaled to fill the screen width. +- The planets image will be repeated in Y-axis, aligned to the bottom left of the screen and not be + scaled. +- The dust image will be repeated in X-axis, aligned to the top right and scaled to fill the screen + height. + +Once you are done setting up your `ParallaxComponent`, add it to the game like with any other +component (`game.add(parallaxComponent`). +Also, don't forget to add your images to the `pubspec.yaml` file as assets or they won't be found. + +The `Parallax` file contains an extension of the game which adds `loadParallax`, +`loadParallaxLayer`, `loadParallaxImage` and `loadParallaxAnimation` so that it automatically +uses your game's image cache instead of the global one. The same goes for the `ParallaxComponent` +file, but that provides `loadParallaxComponent`. + +If you want a fullscreen `ParallaxComponent` simply omit the `size` argument and it will take the +size of the game, it will also resize to fullscreen when the game changes size or orientation. + +Flame provides two kinds of `ParallaxRenderer`: `ParallaxImage` and `ParallaxAnimation`, +`ParallaxImage` is a static image renderer and `ParallaxAnimation` is, as its name implies, an +animation and frame based renderer. +It is also possible to create custom renderers by extending the `ParallaxRenderer` class. + +Three example implementations can be found in the +[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax). diff --git a/doc/flame/components/position_component.md b/doc/flame/components/position_component.md new file mode 100644 index 00000000000..aaf893c5508 --- /dev/null +++ b/doc/flame/components/position_component.md @@ -0,0 +1,155 @@ +# PositionComponent + +Most visible objects in a game need a position, size, and rotation. `PositionComponent` provides +these transform properties, making it the base class for nearly every visual element in Flame: +sprites, animations, shapes, and your own custom components. It mirrors the concept of a +[`Positioned`](https://api.flutter.dev/flutter/widgets/Positioned-class.html) widget in Flutter, but +in a game-oriented coordinate system. + +This class represents a positioned object on the screen, be it a floating rectangle, a rotating +sprite, or anything else with position and size. It can also represent a group of positioned +components if children are added to it. + +The base of the `PositionComponent` is that it has a `position`, `size`, `scale`, `angle` and +`anchor` which transforms how the component is rendered. + + +## Position + +The `position` is just a `Vector2` which represents the position of the component's anchor in +relation to its parent; if the parent is a `FlameGame`, it is in relation to the viewport. + + +## Size + +The `size` of the component when the zoom level of the camera is 1.0 (no zoom, default). +The `size` is *not* in relation to the parent of the component. + + +## Scale + +The `scale` is how much the component and its children should be scaled. Since it is represented +by a `Vector2`, you can scale in a uniform way by changing `x` and `y` with the same amount, or in a +non-uniform way, by changing `x` or `y` by different amounts. + + +## Angle + +The `angle` is the rotation angle around the anchor, represented as a double in radians. It is +relative to the parent's angle. + + +## Native Angle + +The `nativeAngle` is an angle in radians, measured clockwise, representing the default orientation +of the component. It can be used to define the direction in which the component is facing when +[angle](#angle) is zero. + +It is especially helpful when making a sprite based component look at a specific target. If the +original image of the sprite is not facing in the up/north direction, the calculated angle to make +the component look at the target will need some offset to make it look correct. For such cases, +`nativeAngle` can be used to let the component know what direction the original image is facing. + +An example could be a bullet image pointing in the east direction. In this case `nativeAngle` can +be set to pi/2 radians. Following are some common directions and their corresponding native +angle values. + +Direction | Native Angle | In degrees +----------|--------------|------------- +Up/North | 0 | 0 +Down/South| pi or -pi | 180 or -180 +Left/West | -pi/2 | -90 +Right/East| pi/2 | 90 + + +## Anchor + +```{flutter-app} +:sources: ../../flame/examples +:page: anchor +:show: widget code infobox +This example shows effect of changing `anchor` point of parent +(red) and child (blue) components. Tap on them to cycle through +the anchor points. Note that the local position of the child +component is (0, 0) at all times. +``` + +The `anchor` is where on the component that the position and rotation should be defined from (the +default is `Anchor.topLeft`). So if you have the anchor set as `Anchor.center` the component's +position on the screen will be in the center of the component and if an `angle` is applied, it is +rotated around the anchor, so in this case around the center of the component. You can think of it +as the point within the component by which Flame "grabs" it. + +When `position` or `absolutePosition` of a component is queried, the returned coordinates are that +of the `anchor` of the component. In case you want to find the position of a specific anchor +point of a component which is not actually the `anchor` of that component, you can use the +`positionOfAnchor` and `absolutePositionOfAnchor` methods. + +```dart +final comp = PositionComponent( + size: Vector2.all(20), + anchor: Anchor.center, +); + +// Returns (0,0) +final p1 = component.position; + +// Returns (10, 10) +final p2 = component.positionOfAnchor(Anchor.bottomRight); +``` + +A common pitfall when using `anchor` is confusing it as being the attachment point for children +components. For example, setting `anchor` to `Anchor.center` for a parent component does not mean +that the children components will be placed w.r.t the center of parent. + +```{note} +Local origin for a child component is always the top-left +corner of its parent component, irrespective of their +`anchor` values. +``` + + +## PositionComponent children + +All children of the `PositionComponent` will be transformed in relation to the parent, which means +that the `position`, `angle` and `scale` will be relative to the parent's state. +So if you, for example, wanted to position a child in the center of the parent you would do this: + +```dart +@override +void onLoad() { + final parent = PositionComponent( + position: Vector2(100, 100), + size: Vector2(100, 100), + ); + final child = PositionComponent( + position: parent.size / 2, + anchor: Anchor.center, + ); + parent.add(child); +} +``` + +Remember that most components that are rendered on the screen are `PositionComponent`s, so +this pattern can be used in for example [SpriteComponent](sprite_components.md#spritecomponent) +and [SpriteAnimationComponent](sprite_components.md#spriteanimationcomponent) too. + + +## Render PositionComponent + +When implementing the `render` method for a component that extends `PositionComponent` remember to +render from the top left corner (0.0). Your render method should not handle where on the screen your +component should be rendered. To handle where and how your component should be rendered use the +`position`, `angle` and `anchor` properties and Flame will automatically handle the rest for you. + +If you want to know where on the screen the bounding box of the component is you can use the +`toRect` method. + +In the event that you want to change the direction of your component's rendering, you can also use +`flipHorizontally()` and `flipVertically()` to flip anything drawn to canvas during +`render(Canvas canvas)`, around the anchor point. These methods are available on all +`PositionComponent` objects, and are especially useful on `SpriteComponent` and +`SpriteAnimationComponent`. + +In case you want to flip a component around its center without having to change the anchor to +`Anchor.center`, you can use `flipHorizontallyAroundCenter()` and `flipVerticallyAroundCenter()`. diff --git a/doc/flame/components/shape_components.md b/doc/flame/components/shape_components.md new file mode 100644 index 00000000000..2996ef81d80 --- /dev/null +++ b/doc/flame/components/shape_components.md @@ -0,0 +1,161 @@ +# ShapeComponents + +Geometric shapes are useful in many game scenarios: debug visualizations, procedurally generated +graphics, UI elements, or simple game objects that don't need sprite art. Flame's shape components +let you render polygons, rectangles, and circles as first-class components with all the transform +properties of `PositionComponent`. They also serve as the foundation for the +[collision detection hitboxes](../collision_detection.md#shapehitbox). + + +A `ShapeComponent` is the base class for representing a scalable geometrical shape. The shapes have +different ways of defining how they look, but they all have a size and angle that can be modified +and the shape definition will scale or rotate the shape accordingly. + +These shapes are meant as a tool for using geometrical shapes in a more general way than together +with the collision detection system, where you want to use the +[ShapeHitbox](../collision_detection.md#shapehitbox)es. + + +## PolygonComponent + +A `PolygonComponent` is created by giving it a list of points in the constructor, called vertices. +This list will be transformed into a polygon with a size, which can still be scaled and rotated. + +For example, this would create a square going from (50, 50) to (100, 100), with its center in +(75, 75): + +```dart +void main() { + PolygonComponent([ + Vector2(100, 100), + Vector2(100, 50), + Vector2(50, 50), + Vector2(50, 100), + ]); +} +``` + +A `PolygonComponent` can also be created with a list of relative vertices, which are points defined +in relation to the given size, most often the size of the intended parent. + +For example you could create a diamond-shaped polygon like this: + +```dart +void main() { + PolygonComponent.relative( + [ + Vector2(0.0, -1.0), // Middle of top wall + Vector2(1.0, 0.0), // Middle of right wall + Vector2(0.0, 1.0), // Middle of bottom wall + Vector2(-1.0, 0.0), // Middle of left wall + ], + size: Vector2.all(100), + ); +} +``` + +The vertices in the example define percentages of the length from the center to the edge of the +screen in both x and y axis, so for our first item in our list (`Vector2(0.0, -1.0)`) we are +pointing on the middle of the top wall of the bounding box, since the coordinate system here is +defined from +the center of the polygon. + +![An example of how to define a polygon shape](../../images/polygon_shape.png) + +In the image you can see how the polygon shape formed by the purple arrows is defined by the red +arrows. + + +## RectangleComponent + +A `RectangleComponent` is created very similarly to how a `PositionComponent` is created, since it +also has a bounding rectangle. + +Something like this for example: + +```dart +void main() { + RectangleComponent( + position: Vector2(10.0, 15.0), + size: Vector2.all(10), + angle: pi/2, + anchor: Anchor.center, + ); +} +``` + +Dart also already has an excellent way to create rectangles and that class is called `Rect`, you +can create a Flame `RectangleComponent` from a `Rect` by using the +`RectangleComponent.fromRect` factory, and just like when setting the vertices of the +`PolygonComponent`, your rectangle will be sized +according to the `Rect` if you use this constructor. + +The following would create a `RectangleComponent` with its top left corner in `(10, 10)` and a size +of `(100, 50)`. + +```dart +void main() { + RectangleComponent.fromRect( + Rect.fromLTWH(10, 10, 100, 50), + ); +} +``` + +You can also create a `RectangleComponent` by defining a relation to the intended parent's size, +you can use the default constructor to build your rectangle from a position, size and angle. The +`relation` is a vector defined in relation to the parent size, for example a `relation` that is +`Vector2(0.5, 0.8)` would create a rectangle that is 50% of the width of the parent's size and +80% of its height. + +In the example below a `RectangleComponent` of size `(25.0, 30.0)` positioned at `(100, 100)` would +be created. + +```dart +void main() { + RectangleComponent.relative( + Vector2(0.5, 1.0), + position: Vector2.all(100), + size: Vector2(50, 30), + ); +} +``` + +Since a square is a simplified version of a rectangle, there is also a constructor for creating a +square `RectangleComponent`, the only difference is that the `size` argument is a `double` instead +of a `Vector2`. + +```dart +void main() { + RectangleComponent.square( + position: Vector2.all(100), + size: 200, + ); +} +``` + + +## CircleComponent + +If you know your circle's position and/or how long the radius is going to be from the start +you can use the optional arguments `radius` and `position` to set those. + +The following would create a `CircleComponent` with its center in `(100, 100)` with a radius of 5, +and therefore a size of `Vector2(10, 10)`. + +```dart +void main() { + CircleComponent(radius: 5, position: Vector2.all(100), anchor: Anchor.center); +} +``` + +When creating a `CircleComponent` with the `relative` constructor you can define how long the +radius is in comparison to the shortest edge of the bounding box defined by `size`. + +The following example would result in a `CircleComponent` that defines a circle with a radius of 40 +(a diameter of 80). + +```dart +void main() { + CircleComponent.relative(0.8, size: Vector2.all(100)); +} +``` diff --git a/doc/flame/components/sprite_components.md b/doc/flame/components/sprite_components.md new file mode 100644 index 00000000000..aee17c50fc4 --- /dev/null +++ b/doc/flame/components/sprite_components.md @@ -0,0 +1,331 @@ +# Sprite Components + +Sprites are 2D images (or regions of images) that represent the visual appearance of game objects. +They are the most common way to display characters, items, backgrounds, and other visuals in 2D +games. Flame provides several sprite-based components that make it easy to load images, play +animations, and switch between visual states, all while benefiting from the transform properties +inherited from `PositionComponent`. + + +## SpriteComponent + +The most commonly used implementation of `PositionComponent` is `SpriteComponent`, and it can be +created with a `Sprite`: + +```dart +import 'package:flame/components/component.dart'; + +class MyGame extends FlameGame { + late final SpriteComponent player; + + @override + Future onLoad() async { + final sprite = await Sprite.load('player.png'); + final size = Vector2.all(128.0); + final player = SpriteComponent(size: size, sprite: sprite); + + // Vector2(0.0, 0.0) by default, can also be set in the constructor + player.position = Vector2(10, 20); + + // 0 by default, can also be set in the constructor + player.angle = 0; + + // Adds the component + add(player); + } +} +``` + + +## SpriteAnimationComponent + +This class is used to represent a Component that has sprites that run in a single cyclic animation. + +This will create a simple three frame animation using 3 different images: + +```dart +@override +Future onLoad() async { + final sprites = [0, 1, 2] + .map((i) => Sprite.load('player_$i.png')); + final animation = SpriteAnimation.spriteList( + await Future.wait(sprites), + stepTime: 0.01, + ); + this.player = SpriteAnimationComponent( + animation: animation, + size: Vector2.all(64.0), + ); +} +``` + +If you have a sprite sheet, you can use the `sequenced` constructor from the `SpriteAnimationData` +class (check more details on [Images > Animation](../rendering/images.md#animation)): + +```dart +@override +Future onLoad() async { + final size = Vector2.all(64.0); + final data = SpriteAnimationData.sequenced( + textureSize: size, + amount: 2, + stepTime: 0.1, + ); + this.player = SpriteAnimationComponent.fromFrameData( + await images.load('player.png'), + data, + ); +} +``` + +All animation components internally maintain a `SpriteAnimationTicker` which ticks the +`SpriteAnimation`. This allows multiple components to share the same animation object. + +Example: + +```dart +final sprites = [/*Your sprite list here*/]; +final animation = SpriteAnimation.spriteList(sprites, stepTime: 0.01); + +final animationTicker = SpriteAnimationTicker(animation); + +// or alternatively, you can ask the animation object to create one for you. + +final animationTicker = animation.createTicker(); // creates a new ticker + +animationTicker.update(dt); +``` + +To listen when the animation is done (when it reaches the last frame and is not looping) you can +use `animationTicker.completed`. + +Example: + +```dart +await animationTicker.completed; + +doSomething(); + +// or alternatively + +animationTicker.completed.whenComplete(doSomething); +``` + +Additionally, `SpriteAnimationTicker` also has the following optional event callbacks: `onStart`, +`onFrame`, and `onComplete`. To listen to these events, you can do the following: + +```dart +final animationTicker = SpriteAnimationTicker(animation) + ..onStart = () { + // Do something on start. + }; + +final animationTicker = SpriteAnimationTicker(animation) + ..onComplete = () { + // Do something on completion. + }; + +final animationTicker = SpriteAnimationTicker(animation) + ..onFrame = (index) { + if (index == 1) { + // Do something for the second frame. + } + }; +``` + +To reset the animation to the first frame when the component is removed, you can set +`resetOnRemove` to `true`: + +```dart +SpriteAnimationComponent( + animation: animation, + size: Vector2.all(64.0), + resetOnRemove: true, +); +``` + + +## SpriteAnimationGroupComponent + +`SpriteAnimationGroupComponent` is a simple wrapper around `SpriteAnimationComponent` which enables +your component to hold several animations and change the current playing animation at runtime. Since +this component is just a wrapper, the event listeners can be implemented as described in +[SpriteAnimationComponent](#spriteanimationcomponent). + +Its use is very similar to the `SpriteAnimationComponent` but instead of being initialized with a +single animation, this component receives a Map of a generic type `T` as key and a +`SpriteAnimation` as value, and the current animation. + +Example: + +```dart +enum RobotState { + idle, + running, +} + +final running = await loadSpriteAnimation(/* omitted */); +final idle = await loadSpriteAnimation(/* omitted */); + +final robot = SpriteAnimationGroupComponent( + animations: { + RobotState.running: running, + RobotState.idle: idle, + }, + current: RobotState.idle, +); + +// Changes current animation to "running" +robot.current = RobotState.running; +``` + +As this component works with multiple `SpriteAnimation`s, naturally it needs an equal number of +animation tickers to make all those animations tick. Use `animationsTickers` getter to access a map +containing tickers for each animation state. This can be useful if you want to register callbacks +for `onStart`, `onComplete` and `onFrame`. + +Example: + +```dart +enum RobotState { idle, running, jump } + +final running = await loadSpriteAnimation(/* omitted */); +final idle = await loadSpriteAnimation(/* omitted */); + +final robot = SpriteAnimationGroupComponent( + animations: { + RobotState.running: running, + RobotState.idle: idle, + }, + current: RobotState.idle, +); + +robot.animationTickers?[RobotState.running]?.onStart = () { + // Do something on start of running animation. +}; + +robot.animationTickers?[RobotState.jump]?.onStart = () { + // Do something on start of jump animation. +}; + +robot.animationTickers?[RobotState.jump]?.onComplete = () { + // Do something on complete of jump animation. +}; + +robot.animationTickers?[RobotState.idle]?.onFrame = (currentIndex) { + // Do something based on current frame index of idle animation. +}; +``` + + +## SpriteGroupComponent + +`SpriteGroupComponent` is pretty similar to its animation counterpart, but especially for sprites. + +Example: + +```dart +class PlayerComponent extends SpriteGroupComponent + with HasGameReference, TapCallbacks { + @override + Future onLoad() async { + final pressedSprite = await game.loadSprite(/* omitted */); + final unpressedSprite = await game.loadSprite(/* omitted */); + + sprites = { + ButtonState.pressed: pressedSprite, + ButtonState.unpressed: unpressedSprite, + }; + + current = ButtonState.unpressed; + } + + // tap methods handler omitted... +} +``` + + +## IconComponent + +`IconComponent` renders a Flutter `IconData` (such as `Icons.star`) as a Flame component. The icon +is rasterized to an image once during `onLoad()` and then drawn each frame using +`canvas.drawImageRect()` with the component's `Paint`. Because the icon is rendered as a cached +image rather than as text, all paint-based effects work out of the box, including `tint()`, +`setOpacity()`, `ColorEffect`, `OpacityEffect`, `GlowEffect`, and custom `ColorFilter`s. + + +### Basic usage + +```dart +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +class MyGame extends FlameGame { + @override + Future onLoad() async { + final star = IconComponent( + icon: Icons.star, + iconSize: 64, + position: Vector2(100, 100), + ); + add(star); + } +} +``` + + +### Tinting and effects + +The icon is rasterized in white, which allows you to tint it to any color using +`HasPaint` methods: + +```dart +// Tint the icon gold +final star = IconComponent( + icon: Icons.star, + iconSize: 64, + position: Vector2(100, 100), +)..tint(const Color(0xFFFFD700)); + +// Set opacity +star.setOpacity(0.5); + +// Or use a custom paint +final icon = IconComponent( + icon: Icons.favorite, + iconSize: 48, + paint: Paint()..colorFilter = const ColorFilter.mode( + Color(0xFFFF0000), + BlendMode.srcATop, + ), +); +``` + + +### Constructor parameters + +- `icon`: The `IconData` to render (e.g., `Icons.star`, `Icons.favorite`). +- `iconSize`: The resolution at which the icon is rasterized (default `64`). This is independent + of the component's display `size`. +- `size`: The display size of the component. Defaults to `Vector2.all(iconSize)` if not provided. +- `paint`: Optional `Paint` for rendering effects. +- All standard `PositionComponent` parameters (`position`, `scale`, `angle`, `anchor`, etc.). + + +### Changing the icon at runtime + +Both the `icon` and `iconSize` properties can be changed after creation. The component will +automatically re-rasterize the icon on the next frame: + +```dart +final iconComponent = IconComponent( + icon: Icons.play_arrow, + iconSize: 64, +); + +// Later, swap the icon +iconComponent.icon = Icons.pause; + +// Or change the rasterization resolution +iconComponent.iconSize = 128; +``` diff --git a/doc/flame/components/utility_components.md b/doc/flame/components/utility_components.md new file mode 100644 index 00000000000..1a348e457ab --- /dev/null +++ b/doc/flame/components/utility_components.md @@ -0,0 +1,282 @@ +# Utility Components + +Beyond the core visual components, Flame provides several utility components that handle common +game development tasks: spawning objects over time, rendering tiled maps, clipping render areas, +and bridging Flutter widgets into the game. These components save you from writing boilerplate so +you can focus on game-specific logic. + + +## SpawnComponent + +This component is a non-visual component that spawns other components inside of the parent of the +`SpawnComponent`. It's great if you for example want to spawn enemies or power-ups randomly within +an area. + +The `SpawnComponent` takes a factory function that it uses to create new components and an area +where the components should be spawned within (or along the edges of). + +For the area, you can use the `Circle`, `Rectangle` or `Polygon` class, and if you want to only +spawn components along the edges of the shape set the `within` argument to false (defaults to true). + +This would for example spawn new components of the type `MyComponent` every 0.5 seconds randomly +within the defined circle: + +The component supports two types of factories. The `factory` returns a single component and the +`multiFactory` returns a list of components that are added in a single step. + +The factory functions take an `int` as an argument, which is the number of components that have +been spawned, so if for example 4 components have been spawned already the 5th call of the factory +method will be called with the `amount=4`, since the counting starts at 0 for the first call. + +The `factory` with a single component is for backward compatibility, so you should use the +`multiFactory` if in doubt. A single component `factory` will be wrapped internally to return a +single item list and then used as the `multiFactory`. + +If you only want to spawn a certain amount of components, you can use the `spawnCount` argument, +and once the limit is reached the `SpawnComponent` will stop spawning and remove itself. + +By default, the `SpawnComponent` will spawn components to its parent, but if you want to spawn +components to another component you can set the `target` argument. Remember that it should be a +`Component` that has a size if you don't use the `area` or `selfPositioning` arguments. + + +```dart +SpawnComponent( + factory: (i) => MyComponent(size: Vector2(10, 20)), + period: 0.5, + area: Circle(Vector2(100, 200), 150), +); +``` + +If you don't want the spawning rate to be static, you can use the `SpawnComponent.periodRange` +constructor with the `minPeriod` and `maxPeriod` arguments instead. +In the following example the component would be spawned randomly within the circle and the time +between each new spawned component is between 0.5 to 10 seconds. + +```dart +SpawnComponent.periodRange( + factory: (i) => MyComponent(size: Vector2(10, 20)), + minPeriod: 0.5, + maxPeriod: 10, + area: Circle(Vector2(100, 200), 150), +); +``` + +If you want to set the position yourself within the `factory` function, you can set +`selfPositioning = true` in the constructors and you will be able to set the positions yourself and +ignore the `area` argument. + +```dart +SpawnComponent( + factory: (i) => + MyComponent(position: Vector2(100, 200), size: Vector2(10, 20)), + selfPositioning: true, + period: 0.5, +); +``` + + +## SvgComponent + +**Note**: To use SVG with Flame, use the [`flame_svg`](https://github.com/flame-engine/flame_svg) +package. + +This component uses an instance of `Svg` class to represent a Component that has an SVG that is +rendered in the game: + +```dart +@override +Future onLoad() async { + final svg = await Svg.load('android.svg'); + final android = SvgComponent.fromSvg( + svg, + position: Vector2.all(100), + size: Vector2.all(100), + ); +} +``` + + +## IsometricTileMapComponent + +Isometric tile maps are commonly used in strategy, simulation, and RPG games to give a 2D map a +pseudo-3D perspective. This component allows you to render an isometric map based on a cartesian +matrix of blocks and an isometric tileset. + +A simple example on how to use it: + +```dart +// Creates a tileset, the block ids are automatically assigned sequentially +// starting at 0, from left to right and then top to bottom. +final tilesetImage = await images.load('tileset.png'); +final tileset = SpriteSheet(image: tilesetImage, srcSize: Vector2.all(32)); +// Each element is a block id, -1 means nothing +final matrix = [[0, 1, 0], [1, 0, 0], [1, 1, 1]]; +add(IsometricTileMapComponent(tileset, matrix)); +``` + +It also provides methods for converting coordinates so you can handle clicks, hovers, render +entities on top of tiles, add a selector, etc. + +You can also specify the `tileHeight`, which is the vertical distance between the bottom and top +planes of each cuboid in your tile. Basically, it's the height of the front-most edge of your +cuboid; normally it's half (default) or a quarter of the tile size. On the image below you can see +the height colored in the darker tone: + +![An example of how to determine the tileHeight](../../images/tile-height-example.png) + +This is an example of what a quarter-length map looks like: + +![An example of a isometric map with selector](../../images/isometric.png) + +Flame's Example app contains a more in-depth example, featuring how to parse coordinates to make a +selector. The +[source code](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/isometric_tile_map_example.dart) +is available on GitHub, and a +[live version](https://examples.flame-engine.org/#/Rendering_Isometric_Tile_Map) +can be viewed in the browser. + + +## NineTileBoxComponent + +A Nine Tile Box is a rectangle drawn using a grid sprite. + +The grid sprite is a 3x3 grid with 9 blocks, representing the 4 corners, the 4 sides and the +middle. + +The corners are drawn at the same size, the sides are stretched on the side direction and the middle +is expanded both ways. + +Using this, you can get a box/rectangle that expands well to any sizes. This is useful for making +panels, dialogs, borders. + +Check the example app +[nine_tile_box](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/nine_tile_box_example.dart) +for details on how to use it. + + +## CustomPainterComponent + +A `CustomPainter` is a Flutter class used with the `CustomPaint` widget to render custom +shapes inside a Flutter application. + +Flame provides a component that can render a `CustomPainter` called `CustomPainterComponent`. It +receives a custom painter and renders it on the game canvas. + +This can be used for sharing custom rendering logic between your Flame game, and your Flutter +widgets. + +Check the example app +[custom_painter_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/widgets/custom_painter_example.dart) +for details on how to use it. + + +## ComponentsNotifier + +Most of the time just accessing children and their attributes is enough to build the logic of +your game. + +But sometimes, reactivity can help the developer to simplify and write better code, to help with +that Flame provides the `ComponentsNotifier`, which is an implementation of a +`ChangeNotifier` that notifies listeners every time a component is added, removed or manually +changed. + +For example, let's say that we want to show a game over text when the player's lives reach zero. + +To make the component automatically report when new instances are added or removed, the `Notifier` +mixin can be applied to the component class: + +```dart +class Player extends SpriteComponent with Notifier {} +``` + +Then to listen to changes on that component the `componentsNotifier` method from `FlameGame` can +be used: + +```dart +class MyGame extends FlameGame { + int lives = 2; + + @override + void onLoad() { + final playerNotifier = componentsNotifier() + ..addListener(() { + final player = playerNotifier.single; + if (player == null) { + lives--; + if (lives == 0) { + add(GameOverComponent()); + } else { + add(Player()); + } + } + }); + } +} +``` + +A `Notifier` component can also manually notify its listeners that something changed. Let's expand +the example above to make a HUD component blink when the player has half of their health. In +order to do so, we need the `Player` component to notify a change manually: + +```dart +class Player extends SpriteComponent with Notifier { + double health = 1; + + void takeHit() { + health -= .1; + if (health == 0) { + removeFromParent(); + } else if (health <= .5) { + notifyListeners(); + } + } +} +``` + +Then our hud component could look like: + +```dart +class Hud extends PositionComponent with HasGameReference { + + @override + void onLoad() { + final playerNotifier = game.componentsNotifier() + ..addListener(() { + final player = playerNotifier.single; + if (player != null) { + if (player.health <= .5) { + add(BlinkEffect()); + } + } + }); + } +} +``` + +`ComponentsNotifier`s can also come in handy to rebuild widgets when state changes inside a +`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget. + +To see an example of its use, check the +[ComponentsNotifier example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/components_notifier_example.dart). + + +## ClipComponent + +A `ClipComponent` is a component that will clip the canvas to its size and shape. This means that +if the component itself or any child of the `ClipComponent` renders outside of the +`ClipComponent`'s boundaries, the part that is not inside the area will not be shown. + +A `ClipComponent` receives a builder function that should return the `Shape` that will define the +clipped area, based on its size. + +To make it easier to use that component, there are three factories that offer common shapes: + +- `ClipComponent.rectangle`: Clips the area in the form of a rectangle based on its size. +- `ClipComponent.circle`: Clips the area in the form of a circle based on its size. +- `ClipComponent.polygon`: Clips the area in the form of a polygon based on the points received +in the constructor. + +Check the example app +[clip_component](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/clip_component_example.dart) +for details on how to use it. diff --git a/doc/flame/effects/color_effects.md b/doc/flame/effects/color_effects.md index 898c8bd98d3..efbd903bca2 100644 --- a/doc/flame/effects/color_effects.md +++ b/doc/flame/effects/color_effects.md @@ -105,8 +105,9 @@ final effect = OpacityEffect.by( ); ``` -Currently this effect can only be applied to components that have a `HasPaint` mixin. If the target component -uses multiple paints, the effect can target any individual color using the `paintId` parameter. +Currently this effect can only be applied to components that have a `HasPaint` mixin. If the +target component uses multiple paints, the effect can target any individual color using the +`paintId` parameter. ## GlowEffect diff --git a/doc/flame/effects/effect_controllers.md b/doc/flame/effects/effect_controllers.md index ce2d499e0b7..2df25c99a02 100644 --- a/doc/flame/effects/effect_controllers.md +++ b/doc/flame/effects/effect_controllers.md @@ -46,43 +46,43 @@ EffectController({ }); ``` -- *`duration`* -- the length of the main part of the effect, i.e. how long it should take to go +- *`duration`*: the length of the main part of the effect, i.e. how long it should take to go from 0 to 100%. This parameter cannot be negative, but can be zero. If this is the only parameter specified then the effect will grow linearly over the `duration` seconds. -- *`curve`* -- if given, creates a non-linear effect that grows from 0 to 100% according to the +- *`curve`*: if given, creates a non-linear effect that grows from 0 to 100% according to the provided [curve](https://api.flutter.dev/flutter/animation/Curves-class.html). -- *`reverseDuration`* -- if provided, adds an additional step to the controller: after the effect +- *`reverseDuration`*: if provided, adds an additional step to the controller: after the effect has grown from 0 to 100% over the `duration` seconds, it will then go backwards from 100% to 0 over the `reverseDuration` seconds. In addition, the effect will complete at progress level of 0 (normally the effect completes at progress 1). -- *`reverseCurve`* -- the curve to be used during the "reverse" step of the effect. If not given, +- *`reverseCurve`*: the curve to be used during the "reverse" step of the effect. If not given, this will default to `curve.flipped`. -- *`alternate`* -- setting this to true is equivalent to specifying the `reverseDuration` equal +- *`alternate`*: setting this to true is equivalent to specifying the `reverseDuration` equal to the `duration`. If the `reverseDuration` is already set, this flag has no effect. -- *`atMaxDuration`* -- if non-zero, this inserts a pause after the effect reaches its max +- *`atMaxDuration`*: if non-zero, this inserts a pause after the effect reaches its max progress and before the reverse stage. During this time the effect is kept at 100% progress. If there is no reverse stage, then this will simply be a pause before the effect is marked as completed. -- *`atMinDuration`* -- if non-zero, this inserts a pause after the reaches its lowest progress +- *`atMinDuration`*: if non-zero, this inserts a pause after the reaches its lowest progress (0) at the end of the reverse stage. During this time, the effect's progress is at 0%. If there is no reverse stage, then this pause will still be inserted after the "at-max" pause if it's present, or after the forward stage otherwise. In addition, the effect will now complete at progress level of 0. -- *`repeatCount`* -- if greater than one, it will cause the effect to repeat itself the prescribed +- *`repeatCount`*: if greater than one, it will cause the effect to repeat itself the prescribed number of times. Each iteration will consists of the forward stage, pause at max, reverse stage, then pause at min (skipping those that were not specified). -- *`infinite`* -- if true, the effect will repeat infinitely and never reach completion. This is +- *`infinite`*: if true, the effect will repeat infinitely and never reach completion. This is equivalent to as if `repeatCount` was set to infinity. -- *`startDelay`* -- an additional wait time inserted before the beginning of the effect. This +- *`startDelay`*: an additional wait time inserted before the beginning of the effect. This wait time is executed only once, even if the effect is repeating. During this time the effect's `.started` property returns false. The effect's `onStart()` callback will be executed at the end of this waiting period. @@ -90,10 +90,10 @@ EffectController({ Using this parameter is the simplest way to create a chain of effects that execute one after another (or with an overlap). -- *`onMax`* -- callback function which will be invoked right after reaching its max progress and +- *`onMax`*: callback function which will be invoked right after reaching its max progress and before the optional pause and reverse stage. -- *`onMin`* -- callback function which will be invoked right after reaching its lowest progress +- *`onMin`*: callback function which will be invoked right after reaching its lowest progress at the end of the reverse stage and before the optional pause and forward stage. The effect controller returned by this factory constructor will be composited of multiple simpler @@ -103,16 +103,16 @@ needs, you can always create your own combination from the same building blocks. In addition to the factory constructor, the `EffectController` class defines a number of properties common for all effect controllers. These properties are: -- `.started` -- true if the effect has already started. For most effect controllers this property +- `.started`: true if the effect has already started. For most effect controllers this property is always true. The only exception is the `DelayedEffectController` which returns false while the effect is in the waiting stage. -- `.completed` -- becomes true when the effect controller finishes execution. +- `.completed`: becomes true when the effect controller finishes execution. -- `.progress` -- current value of the effect controller, a floating-point value from 0 to 1. This +- `.progress`: current value of the effect controller, a floating-point value from 0 to 1. This variable is the main "output" value of an effect controller. -- `.duration` -- total duration of the effect, or `null` if the duration cannot be determined (for +- `.duration`: total duration of the effect, or `null` if the duration cannot be determined (for example if the duration is random or infinite). @@ -223,8 +223,10 @@ effect. For example, for move effects, they refer to the distance traveled; for the units are radians. ```dart -final speedController = SpeedEffectController(LinearEffectController(0), speed: 1); -final controller = EffectController(speed: 1); // same as speedController +final speedController = + SpeedEffectController(LinearEffectController(0), speed: 1); +final controller = + EffectController(speed: 1); // same as speedController ``` @@ -264,7 +266,7 @@ final controller = RandomEffectController.uniform( ``` The user has the ability to control which `Random` source to use, as well as the exact distribution -of the produced random durations. Two distributions -- `.uniform` and `.exponential` are included, +of the produced random durations. Two distributions, `.uniform` and `.exponential`, are included, any other can be implemented by the user. diff --git a/doc/flame/effects/effects.md b/doc/flame/effects/effects.md index 5a4b7ebee22..6ef5ba4632b 100644 --- a/doc/flame/effects/effects.md +++ b/doc/flame/effects/effects.md @@ -1,5 +1,11 @@ # Effects +In game development, smoothly animating properties over time (moving a character, fading an element, +scaling a power-up) is a constant need. Writing manual interpolation code in every `update` method +is repetitive and error-prone. Effects provide a declarative way to describe these time-based +changes: you attach an effect to a component, and it automatically handles the animation, then +removes itself when finished. + An effect is a special component that can attach to another component in order to modify its properties or appearance. diff --git a/doc/flame/flame.md b/doc/flame/flame.md index b9a26e77709..2db4d0b6905 100644 --- a/doc/flame/flame.md +++ b/doc/flame/flame.md @@ -4,7 +4,7 @@ - [Game Widget](game_widget.md) - [The game class](game.md) - [Assets Structure](structure.md) -- [Components](components.md) +- [Components](components/components.md) - [Inputs](inputs/inputs.md) - [Camera](camera.md) - [Effects](effects/effects.md) @@ -23,7 +23,7 @@ Getting Started <../README.md> Game Widget The game class Assets Structure -Components +Components Inputs Camera Effects diff --git a/doc/flame/game.md b/doc/flame/game.md index 75f7ebef814..1b13823667a 100644 --- a/doc/flame/game.md +++ b/doc/flame/game.md @@ -1,6 +1,12 @@ # FlameGame -The base of almost all Flame games is the `FlameGame` class, this is the root of your component +Every game needs a central object that owns the game loop, the continuous cycle of updating state +and rendering frames that drives all real-time games. In Flame, `FlameGame` fills that role while +also serving as the root of the component tree. If you are familiar with Flutter, think of +`FlameGame` as the equivalent of `MaterialApp`: the top-level entry point that everything else +hangs off of. + +The base of almost all Flame games is the `FlameGame` class. It is the root of your component tree. We refer to this component-based system as the Flame Component System (FCS). Throughout the documentation, FCS is used to reference this system. @@ -9,7 +15,7 @@ and calls the `update` and `render` methods of all components that have been add Components can be added to the `FlameGame` directly in the constructor with the named `children` argument, or from anywhere else with the `add`/`addAll` methods. Most of the time however, you want -to add your children to a `World`, the default world exist under `FlameGame.world` and you add +to add your children to a `World`, the default world exists under `FlameGame.world` and you add components to it just like you would to any other component. A simple `FlameGame` implementation that adds two components, one in `onLoad` and one directly in @@ -64,7 +70,7 @@ class MyGame extends FlameGame { MyGame() : super(world: MyWorld()); void incrementScore() { - // No cast needed — `world` is already typed as `MyWorld`. + // No cast needed, `world` is already typed as `MyWorld`. world.score++; } } @@ -84,7 +90,7 @@ constructor. To remove components from the list on a `FlameGame` the `remove` or `removeAll` methods can be used. The first can be used if you just want to remove one component, and the second can be used when you -want to remove a list of components. These methods exist on all `Component`s, including the world. +want to remove a list of components. These methods exist on all `Component`s, including the `World`. The `FlameGame` has a built-in `World` called `world` and a `CameraComponent` instance called `camera`, you can read more about those in the [Camera section](camera.md). @@ -96,7 +102,7 @@ The `GameLoop` module is a simple abstraction of the game loop concept. Basicall built upon two methods: - The render method takes the canvas for drawing the current state of the game. -- The update method receives the delta time in microseconds since the last update and allows you to +- The update method receives the delta time in seconds since the last update and allows you to move to the next state. The `GameLoop` is used by all of Flame's `Game` implementations. @@ -105,7 +111,7 @@ The `GameLoop` is used by all of Flame's `Game` implementations. ## Resizing Every time the game needs to be resized, for example when the orientation is changed, `FlameGame` -will call all of the `Component`s `onGameResize` methods and it will also pass this information to +will call all of the `Component`'s `onGameResize` methods and it will also pass this information to the camera and viewport. The `FlameGame.camera` controls which point in the coordinate space that should be at the anchor of @@ -121,11 +127,11 @@ The `FlameGame` lifecycle callbacks, `onLoad`, `render`, etc. are called in the When a `FlameGame` is first added to a `GameWidget` the lifecycle methods `onGameResize`, `onLoad` and `onMount` will be called in that order. Then `update` and `render` are called in sequence for -every game tick. If the `FlameGame` is removed from the `GameWidget` then `onRemove` is called. +every game tick. If the `FlameGame` is removed from the `GameWidget` then `onRemove` is called. If the `FlameGame` is added to a new `GameWidget` the sequence repeats from `onGameResize`. ```{note} -The order of `onGameResize`and `onLoad` are reversed from that of other +The order of `onGameResize` and `onLoad` are reversed from that of other `Component`s. This is to allow game element sizes to be calculated before resources are loaded or generated. ``` @@ -237,7 +243,7 @@ The `Game` class allows for more freedom of how to implement things, but you are also missing out on all of the built-in features in Flame if you use it. ``` -An example of how a `Game` implementation could look like is: +An example of what a `Game` implementation could look like: ```dart class MyGameSubClass extends Game { @@ -273,10 +279,9 @@ A Flame `Game` can be paused and resumed in two ways: When pausing a `Game`, the `GameLoop` is effectively paused, meaning that no updates or new renders will happen until it is resumed. -While the game is paused, it is possible to advanced it frame by frame using the `stepEngine` -method. -It might not be much useful in the final game, but can be very helpful in inspecting game state step -by step during the development cycle. +While the game is paused, it is possible to advance it frame by frame using the `stepEngine` +method. It might not be very useful in the final game, but it can be very helpful for inspecting +game state step by step during the development cycle. ### Backgrounding @@ -293,14 +298,14 @@ class MyGame extends FlameGame { } ``` -On the current Flutter stable (3.13), this flag is effectively ignored on -non-mobile platforms including the web. +This flag currently only works on Android and iOS. ## HasPerformanceTracker mixin -While optimizing a game, it can be useful to track the time it took for the game to update and render -each frame. This data can help in detecting areas of the code that are running hot. It can also help +While optimizing a game, it can be useful to track the time it took for the game to update and +render each frame. This data can help in detecting areas of the code that are running hot. It can +also help in detecting visual areas of the game that are taking the most time to render. To get the update and render times, just add the `HasPerformanceTracker` mixin to the game class. diff --git a/doc/flame/game_widget.md b/doc/flame/game_widget.md index 41049fd26f8..5b9b95896d5 100644 --- a/doc/flame/game_widget.md +++ b/doc/flame/game_widget.md @@ -1,5 +1,11 @@ # Game Widget +The `GameWidget` is the bridge between Flutter and Flame. Since Flame games are not Flutter widgets +by themselves, the `GameWidget` wraps a `Game` instance and places it into the Flutter widget tree, +just like any other [widget](https://docs.flutter.dev/get-started/fundamentals/widgets). This lets +you combine a full-screen game with Flutter UI elements (navigation bars, overlays, dialogs) or +embed a game as only part of your app's layout. + ```{dartdoc} :package: flame :symbol: GameWidget @@ -29,9 +35,9 @@ There are three possible values from Flutter's `HitTestBehavior`: top of Flutter UI and you want the underlying widgets to remain interactive in areas the game doesn't need to handle. -- **`HitTestBehavior.translucent`**: The game receives events where it has event-handling components, - but always allows widgets behind it to be hit-tested as well. Both the game and the widgets behind - it can receive the same event. +- **`HitTestBehavior.translucent`**: The game receives events where it has event-handling + components, but always allows widgets behind it to be hit-tested as well. Both the game and the + widgets behind it can receive the same event. ### Allowing taps to pass through diff --git a/doc/flame/inputs/drag_events.md b/doc/flame/inputs/drag_events.md index 43f970b3654..d910ec24f34 100644 --- a/doc/flame/inputs/drag_events.md +++ b/doc/flame/inputs/drag_events.md @@ -10,10 +10,10 @@ will be handled correctly by Flame, and you can even keep track of the events by For those components that you want to respond to drags, add the `DragCallbacks` mixin. - This mixin adds four overridable methods to your component: `onDragStart`, `onDragUpdate`, - `onDragEnd`, and `onDragCancel`. By default, these methods do nothing -- they need to be - overridden in order to perform any function. + `onDragEnd`, and `onDragCancel`. By default, these methods do nothing; they need to be overridden + in order to perform any function. - In addition, the component must implement the `containsLocalPoint()` method (already implemented - in `PositionComponent`, so most of the time you don't need to do anything here) -- this method + in `PositionComponent`, so most of the time you don't need to do anything here). This method allows Flame to know whether the event occurred within the component or not. ```dart @@ -71,9 +71,9 @@ if the user moves their finger away from the component, the property `event.loca return a point whose coordinates are NaNs. Likewise, the `event.renderingTrace` in this case will be empty. However, the `canvasPosition` and `devicePosition` properties of the event will be valid. -In addition, the `DragUpdateEvent` will contain `delta` -- the amount the finger has moved since the -previous `onDragUpdate`, or since the `onDragStart` if this is the first drag-update after a drag- -start. +In addition, the `DragUpdateEvent` will contain `delta`, the amount the finger has moved since +the previous `onDragUpdate`, or since the `onDragStart` if this is the first drag-update after +a drag-start. The `event.timestamp` property measures the time elapsed since the beginning of the drag. It can be used, for example, to compute the speed of the movement. diff --git a/doc/flame/inputs/gesture_input.md b/doc/flame/inputs/gesture_input.md index aa8d7e594a1..be6991e5467 100644 --- a/doc/flame/inputs/gesture_input.md +++ b/doc/flame/inputs/gesture_input.md @@ -108,8 +108,8 @@ Flame's GestureApi is provided by Flutter's Gesture Widgets, including [GestureDetector widget](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html), [RawGestureDetector widget](https://api.flutter.dev/flutter/widgets/RawGestureDetector-class.html) and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-class.html), you can -also read more about Flutter's gestures -[here](https://api.flutter.dev/flutter/gestures/gestures-library.html). +also read more about +[Flutter's gesture system](https://api.flutter.dev/flutter/gestures/gestures-library.html). ## PanDetector and ScaleDetector @@ -222,8 +222,8 @@ class MyGame extends FlameGame with TapDetector { } ``` -You can also check more complete examples -[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/). +You can also check more complete examples in the +[input examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/input/). ### GestureHitboxes @@ -241,5 +241,5 @@ added in the below `Collidable` example. More information about how to define hitboxes can be found in the hitbox section of the [collision detection](../collision_detection.md#shapehitbox) docs. -An example of how to use it can be seen -[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/gesture_hitboxes_example.dart). +An example of how to use it can be seen in the +[gesture hitboxes example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/gesture_hitboxes_example.dart). diff --git a/doc/flame/inputs/inputs.md b/doc/flame/inputs/inputs.md index d5fced5b0f4..252ec9b8aea 100644 --- a/doc/flame/inputs/inputs.md +++ b/doc/flame/inputs/inputs.md @@ -1,5 +1,13 @@ # Inputs +Games are interactive by nature, so handling player input is essential. Flame provides input +handling that works on all platforms Flutter supports: touch on mobile, mouse and keyboard on +desktop, and +pointer events on the web. These APIs are designed as mixins that you add to your components, so +each component can independently decide which input events it cares about. This is similar to how +Flutter's [GestureDetector](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html) +works, but adapted for Flame's component tree. + - [Tap Events](tap_events.md) - [Drag Events](drag_events.md) - [Gesture Input](gesture_input.md) diff --git a/doc/flame/inputs/keyboard_input.md b/doc/flame/inputs/keyboard_input.md index 3aab488f652..ccf8e410f82 100644 --- a/doc/flame/inputs/keyboard_input.md +++ b/doc/flame/inputs/keyboard_input.md @@ -132,5 +132,5 @@ the game is focused or not. By default `GameWidget` has its `autofocus` set to true, which means it will get focused once it is mounted. To override that behavior, set `autofocus` to false. -For a more complete example see -[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/keyboard_example.dart). +For a more complete example, see the +[keyboard input example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/input/keyboard_example.dart). diff --git a/doc/flame/inputs/other_inputs.md b/doc/flame/inputs/other_inputs.md index e221191df28..926ca63c20a 100644 --- a/doc/flame/inputs/other_inputs.md +++ b/doc/flame/inputs/other_inputs.md @@ -60,8 +60,8 @@ class Player extends SpriteComponent with HasGameReference { @override Future onLoad() async { - sprite = await gameRef.loadSprite('layers/player.png'); - position = gameRef.size / 2; + sprite = await game.loadSprite('layers/player.png'); + position = game.size / 2; } @override diff --git a/doc/flame/inputs/pointer_events.md b/doc/flame/inputs/pointer_events.md index 830a4fc14df..338669189c7 100644 --- a/doc/flame/inputs/pointer_events.md +++ b/doc/flame/inputs/pointer_events.md @@ -32,7 +32,7 @@ By default, each of these methods does nothing, they need to be overridden in or function. In addition, the component must implement the `containsLocalPoint()` method (already implemented in -`PositionComponent`, so most of the time you don't need to do anything here) -- this method allows +`PositionComponent`, so most of the time you don't need to do anything here). This method allows Flame to know whether the event occurred within the component or not. Note that only mouse events happening within your component will be proxied along. However, diff --git a/doc/flame/inputs/scale_events.md b/doc/flame/inputs/scale_events.md index 1d7c5b2e783..df99a42ae27 100644 --- a/doc/flame/inputs/scale_events.md +++ b/doc/flame/inputs/scale_events.md @@ -6,10 +6,10 @@ Only one single scale gesture can occur at the same time. For those components that you want to respond to scale events, add the `ScaleCallbacks` mixin. - This mixin adds three overridable methods to your component: `onScaleStart`, `onScaleUpdate`, - `onScaleEnd`. By default, these methods do nothing -- they need to be - overridden in order to perform any function. + `onScaleEnd`. By default, these methods do nothing; they need to be overridden in order to + perform any function. - In addition, the component must implement the `containsLocalPoint()` method (already implemented - in `PositionComponent`, so most of the time you don't need to do anything here) -- this method + in `PositionComponent`, so most of the time you don't need to do anything here). This method allows Flame to know whether the event occurred within the component or not. ```dart diff --git a/doc/flame/inputs/tap_events.md b/doc/flame/inputs/tap_events.md index 8474d1fc36d..356e8860c8c 100644 --- a/doc/flame/inputs/tap_events.md +++ b/doc/flame/inputs/tap_events.md @@ -8,7 +8,7 @@ which is still supported, is described in [](gesture_input.md). **Tap events** are one of the most basic methods of interaction with a Flame game. These events occur when the user touches the screen with a finger, or clicks with a mouse, or taps with a stylus. A tap can be "long", but the finger isn't supposed to move during the gesture. Thus, touching the -screen, then moving the finger, and then releasing -- is not a tap but a drag. Similarly, clicking +screen, then moving the finger, and then releasing is not a tap but a drag. Similarly, clicking a mouse button while the mouse is moving will also be registered as a drag. Multiple tap events can occur at the same time, especially if the user has multiple fingers. Such @@ -21,7 +21,7 @@ For those components that you want to respond to taps, add the `TapCallbacks` mi `onTapCancel`, and `onLongTapDown`. By default, each of these methods does nothing, they need to be overridden in order to perform any function. - In addition, the component must implement the `containsLocalPoint()` method (already implemented - in `PositionComponent`, so most of the time you don't need to do anything here) -- this method + in `PositionComponent`, so most of the time you don't need to do anything here). This method allows Flame to know whether the event occurred within the component or not. ```dart @@ -237,7 +237,7 @@ for the new API: event object `TapDownEvent event`. - There is no return value anymore, but if you need to make a component to pass-through the taps to the components below, then set `event.continuePropagation` to true. This is only needed for - `onTapDown` events -- all other events will pass-through automatically. + `onTapDown` events; all other events will pass-through automatically. - If your component needs to know the coordinates of the point of touch, use `event.localPosition` instead of computing it manually. Properties `event.canvasPosition` and `event.devicePosition` are also available. diff --git a/doc/flame/layout/layout.md b/doc/flame/layout/layout.md index 4ac4438ffe7..0e2a5e4fff3 100644 --- a/doc/flame/layout/layout.md +++ b/doc/flame/layout/layout.md @@ -1,5 +1,11 @@ # Layout +Positioning game elements manually with pixel coordinates works for simple cases, but quickly +becomes tedious when building HUDs, menus, or any UI that needs to adapt to different screen +sizes. Flame's layout components bring familiar concepts from Flutter's layout system (rows, +columns, padding, alignment) into the game world, so you can arrange components declaratively +rather than calculating positions by hand. + - [Align Component](align_component.md) - [Row Component](row_component.md) - [Column Component](column_component.md) diff --git a/doc/flame/other/other.md b/doc/flame/other/other.md index c77a37efed1..942dd83c0a6 100644 --- a/doc/flame/other/other.md +++ b/doc/flame/other/other.md @@ -1,5 +1,9 @@ # Other +This section covers additional tools and utilities that don't fit neatly into the other categories +but are still important for day-to-day game development: debugging helpers, performance profiling, +Flutter widget integration, and general-purpose utility functions. + - [Debugging](debug.md) - [Utils](util.md) - [Widgets](widgets.md) diff --git a/doc/flame/other/performance.md b/doc/flame/other/performance.md index 2b025d00fa8..ef3b45e77ba 100644 --- a/doc/flame/other/performance.md +++ b/doc/flame/other/performance.md @@ -22,16 +22,18 @@ Creating objects of a class is very common in any kind of project/game. But obje involved operation. Depending on the frequency and amount of objects that are being created, the application can experience some slow down. -In games, this is something to be very careful of because games generally have a game loop that updates -as fast as possible, where each update is called a frame. Depending on the hardware, a game can be updating -30, 60, 120 or even higher frames per second. This means if a new object is created in a frame, the game -will end up creating as many number of objects as the frame count per second. - -Flame users, generally tend to run into this problem when they override the `update` and `render` method -of a `Component`. For example, in the following innocent looking code, a new `Vector2` and a new `Paint` -object is spawned every frame. But the data inside the objects is essentially the same across all frames. -Now imagine if there are 100 instances of `MyComponent` in a game running at 60 FPS. That would essentially -mean 6000 (100 * 60) new instances of `Vector2` and `Paint` each will be created every second. +In games, this is something to be very careful of because games generally have a game loop that +updates as fast as possible, where each update is called a frame. Depending on the hardware, a +game can be updating 30, 60, 120 or even higher frames per second. This means if a new object is +created in a frame, the game will end up creating as many number of objects as the frame count +per second. + +Flame users generally tend to run into this problem when they override the `update` and `render` +method of a `Component`. For example, in the following innocent looking code, a new `Vector2` and +a new `Paint` object is spawned every frame. But the data inside the objects is essentially the +same across all frames. Now imagine if there are 100 instances of `MyComponent` in a game running +at 60 FPS. That would essentially mean 6000 (100 * 60) new instances of `Vector2` and `Paint` +each will be created every second. ```{note} It is like buying a new computer every time you want to send an email or buying @@ -84,15 +86,17 @@ small object can affect the performance if spawned in high volume. ## Unwanted collision checks -Flame has a built-in collision detection system which can detect when any two `Hitbox`es intersect with -each other. In an ideal case, this system runs on every frame and checks for collision. It is also smart -enough to filter out only the possible collisions before performing the actual intersection checks. +Flame has a built-in collision detection system which can detect when any two `Hitbox`es intersect +with each other. In an ideal case, this system runs on every frame and checks for collision. It is +also smart enough to filter out only the possible collisions before performing the actual +intersection checks. -Despite this, it is safe to assume that the cost of collision detection will increase as the number of -hitboxes increases. But in many games, the developers are not always interested in detecting collision -between every possible pair. For example, consider a simple game where players can fire a `Bullet` component -that has a hitbox. In such a game it is likely that the developers are not interested in detecting collision -between any two bullets, but Flame will still perform those collision checks. +Despite this, it is safe to assume that the cost of collision detection will increase as the +number of hitboxes increases. But in many games, the developers are not always interested in +detecting collision between every possible pair. For example, consider a simple game where players +can fire a `Bullet` component that has a hitbox. In such a game it is likely that the developers +are not interested in detecting collision between any two bullets, but Flame will still perform +those collision checks. To avoid this, you can set the `collisionType` for bullet component to `CollisionType.passive`. Doing so will cause Flame to completely skip any kind of collision check between all the passive hitboxes. @@ -155,7 +159,7 @@ void spawnBullet(Vector2 position, Vector2 velocity) { **Returning components to the pool:** Components are returned to the pool **automatically** when they are removed from the game -tree. Simply call `removeFromParent()` on the component — there is no manual release step. +tree. Simply call `removeFromParent()` on the component. There is no manual release step. ```dart class Bullet extends SpriteComponent with CollisionCallbacks { @@ -166,7 +170,7 @@ class Bullet extends SpriteComponent with CollisionCallbacks { super.update(dt); position.add(velocity * dt); - // Remove bullet if it goes off screen — automatically returned to pool + // Remove bullet if it goes off screen. Automatically returned to pool. if (position.x < -100 || position.x > game.size.x + 100) { removeFromParent(); } @@ -175,7 +179,7 @@ class Bullet extends SpriteComponent with CollisionCallbacks { @override void onCollisionStart(Set points, PositionComponent other) { super.onCollisionStart(points, other); - // Return to pool on collision — no manual release needed + // Return to pool on collision. No manual release needed. removeFromParent(); } diff --git a/doc/flame/other/widgets.md b/doc/flame/other/widgets.md index 5afc3835f37..d0156ec856f 100644 --- a/doc/flame/other/widgets.md +++ b/doc/flame/other/widgets.md @@ -7,8 +7,8 @@ mind. Here you can find all the available widgets provided by Flame. You can also see all the widgets showcased inside a -[Dashbook](https://github.com/bluefireteam/dashbook) sandbox -[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/widgets) +[Dashbook](https://github.com/bluefireteam/dashbook) sandbox in the +[widgets examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/widgets). ## NineTileBoxWidget @@ -23,8 +23,8 @@ is expanded both ways. The `NineTileBoxWidget` implements a `Container` using that standard. This pattern is also implemented as a component in the `NineTileBoxComponent` so that you can add this feature directly -to your `FlameGame`. To get to know more about this, check the component docs -[here](../components.md#ninetileboxcomponent). +to your `FlameGame`. To learn more, check the +[NineTileBoxComponent docs](../components/utility_components.md#ninetileboxcomponent). Here you can find an example of how to use it (without using the `NineTileBoxComponent`): @@ -34,7 +34,7 @@ import 'package:flame/widgets'; NineTileBoxWidget( image: image, // dart:ui image instance tileSize: 16, // The width/height of the tile on your grid image - destTileSize: 50, // The dimensions to be used when drawing the tile on the canvas + destTileSize: 50, // The dimensions for the tile on canvas child: SomeWidget(), // Any Flutter widget ) ``` diff --git a/doc/flame/overlays.md b/doc/flame/overlays.md index d6eefd1e311..ae7dc848b66 100644 --- a/doc/flame/overlays.md +++ b/doc/flame/overlays.md @@ -1,5 +1,10 @@ # Overlays +Games often need to display Flutter widgets on top of the game canvas for things like pause menus, +score displays, or chat interfaces. Because Flame games live inside a Flutter widget tree, you can +layer any Flutter widget over the game surface. The Overlays API makes this especially convenient +by letting you toggle named widget overlays on and off from within your game code. + Since a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter widgets in your tree. However, if you want to easily show widgets on top of your Flame game, like messages, menu screens or something of that nature, you can use the Widgets Overlay API to make diff --git a/doc/flame/platforms.md b/doc/flame/platforms.md index 21116b86e7c..7db13a523a3 100644 --- a/doc/flame/platforms.md +++ b/doc/flame/platforms.md @@ -1,32 +1,37 @@ # Supported Platforms -Since Flame runs on top of Flutter, so its supported platforms depend on which platforms that are +One of Flame's biggest advantages is that it inherits Flutter's cross-platform reach. A single +codebase can produce games for phones, desktops, and the web. This section covers platform +support details and shows how to deploy your finished game to popular hosting services. + +Since Flame runs on top of Flutter, its supported platforms depend on which platforms are supported by Flutter. -At the moment, Flame supports web, mobile(Android and iOS) and desktop (Windows, MacOS and Linux). +At the moment, Flame supports web, mobile (Android and iOS) and desktop (Windows, macOS and Linux). ## Flutter channels -Flame keeps it support on the stable channel. The dev, beta and master channel should work, but we -don't support them. This means that issues happening outside the stable channel are not a priority. +Flame keeps its support on the stable channel. The dev, beta and master channels should work, +but we don't support them. This means that issues happening outside the stable channel are not +a priority. ## Deploy your game to GitHub Pages -One easy way to deploy your game online, is to use [GitHub Pages](https://pages.github.com/). +One easy way to deploy your game online is to use [GitHub Pages](https://pages.github.com/). It is a cool feature from GitHub, by which you can easily host web content from your repository. Here we will explain the easiest way to get your game hosted using GitHub pages. -First thing, lets create the branch where your deployed files will live: +First, let's create the branch where your deployed files will live: ```shell git checkout -b gh-pages ``` -This branch can be created from `main` or any other place, it doesn't matter much. After you push that -branch go back to your `main` branch. +This branch can be created from `main` or any other place, it doesn't matter much. After you push +that branch go back to your `main` branch. Now you should add the [flutter-gh-pages](https://github.com/bluefireteam/flutter-gh-pages) action to your repository, you can do that by creating a file `gh-pages.yaml` under the folder @@ -57,7 +62,7 @@ Be sure to change `NAME_OF_YOUR_REPOSITORY` to the name of your GitHub repositor Now, whenever you push something to the `main` branch, the action will run and update your deployed game. -The game should be available at an URL like this: +The game should be available at a URL like this: `https://YOUR_GITHUB_USERNAME.github.io/NAME_OF_YOUR_REPOSITORY/` @@ -146,10 +151,10 @@ repository. When using Flame on the web some methods may not work. For example `Flame.device.setOrientation` and `Flame.device.fullScreen` won't work on web, they can be called, but nothing will happen. -Another example: pre caching audio using `flame_audio` package also doesn't work due to Audioplayers -not supporting it on web. This can be worked around by using the `http` package, and requesting a -get to the audio file, that will make the browser cache the file producing the same effect as on -mobile. +Another example: pre-caching audio using the `flame_audio` package also doesn't work due to +Audioplayers not supporting it on web. This can be worked around by using the `http` package, +and requesting a get to the audio file, that will make the browser cache the file producing the +same effect as on mobile. If you want to create instances of `ui.Image` on the web you can use our `Flame.images.decodeImageFromPixels` method. This wraps the `decodeImageFromPixels` from the `ui` diff --git a/doc/flame/rendering/decorators.md b/doc/flame/rendering/decorators.md index 11285154faf..fb15f8d557d 100644 --- a/doc/flame/rendering/decorators.md +++ b/doc/flame/rendering/decorators.md @@ -185,16 +185,16 @@ an alternative logic for how the component shall be positioned on the screen. It is possible to apply several decorators simultaneously to the same component: the `Decorator` class supports chaining. That is, if you have an existing decorator on a component and you want to -add another one, then you can call `component.decorator.addLast(newDecorator)` -- this will add +add another one, then you can call `component.decorator.addLast(newDecorator)`. This will add the new decorator at the end of the existing chain. The method `removeLast()` can remove that decorator later. Several decorators can be chain that way. For example, if `A` is an initial decorator, then -`A.addLast(B)` can be followed by either `A.addLast(C)` or `B.addLast(C)` -- and in both cases the +`A.addLast(B)` can be followed by either `A.addLast(C)` or `B.addLast(C)`, and in both cases the chain `A -> B -> C` will be created. In practice, it means that the entire chain can be manipulated from its root, which usually is `component.decorator`. -[Component]: ../../flame/components.md#component +[Component]: ../components/components.md#component [Effect]: ../../flame/effects.md [HasDecorator]: #hasdecorator-mixin diff --git a/doc/flame/rendering/post_processing.md b/doc/flame/rendering/post_processing.md index fe29d499158..8964f8c70cc 100644 --- a/doc/flame/rendering/post_processing.md +++ b/doc/flame/rendering/post_processing.md @@ -1,8 +1,8 @@ # Post Processing and Shaders Post processing is a technique used in game development to apply visual effects to a component tree -after it has been rendered. Once a frame is rendered — either directly or rasterized into an -image—post processing can modify or enhance the visuals. +after it has been rendered. Once a frame is rendered, either directly or rasterized into an +image, post processing can modify or enhance the visuals. Post processing leverages fragment shaders to create dynamic visual effects such as blur, bloom, color grading, distortion, and lighting adjustments. diff --git a/doc/flame/rendering/rendering.md b/doc/flame/rendering/rendering.md index 38cbb1887ad..0c0199258d8 100644 --- a/doc/flame/rendering/rendering.md +++ b/doc/flame/rendering/rendering.md @@ -1,5 +1,11 @@ # Rendering +Rendering is how your game draws everything the player sees: sprites, text, particle effects, and +custom shapes. Flame builds on Flutter's +[Canvas](https://api.flutter.dev/flutter/dart-ui/Canvas-class.html) API and adds game-oriented utilities +for loading sprite sheets, animating frames, rendering text with bitmap fonts, applying visual +decorators, and running post-processing shaders. + - [Post Processing and Shaders](post_processing.md) - [Colors and Palette](palette.md) - [Decorators](decorators.md) diff --git a/doc/flame/router.md b/doc/flame/router.md index aad9fd3aa90..e125ae6408c 100644 --- a/doc/flame/router.md +++ b/doc/flame/router.md @@ -12,9 +12,11 @@ visual effects to the content of the page below it. # RouterComponent -The **RouterComponent**'s job is to manage navigation across multiple screens within the game. It is -similar in spirit to Flutter's [Navigator][Flutter Navigator] class, except that it works with Flame -components instead of Flutter widgets. +Most games consist of more than a single screen: there is a main menu, a settings page, the +gameplay screen, pop-up dialogs, and so on. Managing the transitions between these screens can +quickly become messy. The `RouterComponent` solves this by providing a stack-based navigation +model, similar in spirit to Flutter's [Navigator][Flutter Navigator] class, except that it works +with Flame components instead of Flutter widgets. A typical game will usually consist of multiple pages: the splash screen, the starting menu page, the settings page, credits, the main game page, several pop-ups, etc. The router will organize @@ -59,9 +61,10 @@ class PauseRoute extends Route { ... } ``` ```{note} -Use `hide Route` if any of your imported packages export another class called `Route` +Use `hide Route` if any of your imported packages export +another class called `Route` -eg: `import 'package:flutter/material.dart' hide Route;` +e.g.: `import 'package:flutter/material.dart' hide Route;` ``` @@ -73,13 +76,13 @@ eg: `import 'package:flutter/material.dart' hide Route;` The **Route** component holds information about the content of a particular page. `Route`s are mounted as children to the `RouterComponent`. -The main property of a `Route` is its `builder` -- the function that creates the component with +The main property of a `Route` is its `builder`, the function that creates the component with the content of its page. -In addition, the routes can be either transparent or opaque (default). An opaque prevents the route -below it from rendering or receiving pointer events, a transparent route doesn't. As a rule of -thumb, declare the route opaque if it is full-screen, and transparent if it is supposed to cover -only a part of the screen. +In addition, the routes can be either transparent or opaque (default). An opaque route prevents +the route below it from rendering or receiving pointer events, while a transparent route does +not. As a rule of thumb, declare the route opaque if it is full-screen, and transparent if it +is supposed to cover only a part of the screen. By default, routes maintain the state of the page component after being popped from the stack and the `builder` function is only called the first time a route is activated. Setting @@ -157,7 +160,7 @@ final router = RouterComponent( Overlays that were defined within the `GameWidget` don't even need to be declared within the routes map beforehand: the `RouterComponent.pushOverlay()` method can do it for you. Once an overlay route was registered, it can be activated either via the regular `.pushNamed()` method, or via the -`.pushOverlay()` -- the two methods will do exactly the same, though you can use the second one to +`.pushOverlay()`. The two methods will do exactly the same, though you can use the second one to make it more clear in your code that an overlay is being added instead of a regular route. The current overlay can be replaced using `pushReplacementOverlay`. This method executes diff --git a/doc/flame/structure.md b/doc/flame/structure.md index 9e610f99666..32896090343 100644 --- a/doc/flame/structure.md +++ b/doc/flame/structure.md @@ -1,5 +1,10 @@ # Assets Directory Structure +Games rely heavily on external assets like images for sprites, audio files for sound effects, and +tile maps for levels. Organizing these files consistently ensures that Flame's built-in loaders +(and Flutter's own [asset system](https://docs.flutter.dev/ui/assets/assets-and-images)) can find +them without extra configuration. + Flame has a proposed structure for your project that includes the standard Flutter `assets` directory in addition to some children: `audio`, `images` and `tiles`. @@ -58,5 +63,5 @@ global ones provided by Flame. Additionally, `AssetsCache` and `Images` can receive a custom [`AssetBundle`](https://api.flutter.dev/flutter/services/AssetBundle-class.html). -This can be used to make Flame look for assets in a different location other the `rootBundle`, +This can be used to make Flame look for assets in a different location other than the `rootBundle`, like the file system for example. diff --git a/doc/tutorials/klondike/step5.md b/doc/tutorials/klondike/step5.md index 9f55cb73887..36499dd63df 100644 --- a/doc/tutorials/klondike/step5.md +++ b/doc/tutorials/klondike/step5.md @@ -19,7 +19,7 @@ play. The topics to be covered are: The Klondike patience game (or solitaire game in the USA) has two main variants: Draw 3 and Draw 1. Currently the Klondike Flame Game is Draw 3, which is a lot more difficult than Draw 1, because although you can see 3 cards, you can only move one of them and that move changes the "phase" of -other cards. So different cards are going to become available — not easy. +other cards. So different cards are going to become available, not easy. In Klondike Draw 1 just one card at a time is drawn from the Stock and shown, so every card in it is available, and you can go through the Stock as many times as you like, just as in Klondike Draw 3. @@ -274,7 +274,7 @@ for `reverseDuration: time / 2`. Everything is reversed: the view of the card ex into 2-D of its 3-D position. Wow! That's a lot of work for a little EffectController! We are not there yet! If you were to run just the `add()` part of the code, you would see some -ugly things happening. Yeah, yeah, been there, done that — when I was preparing this code! +ugly things happening. Yeah, yeah, been there, done that... when I was preparing this code! First off, the card shrinks to a line at its left. That is because all cards in this game have an `Anchor` at `topLeft`, which is the point used to set the card's `position`. We would like the card to flip around its vertical center-line. Easy, just set `anchor = Anchor.topCenter` @@ -330,7 +330,7 @@ dilemma by using two definitions of "face-up": a Model type and a View type. The used in rendering and animation (i.e. what appears on the screen) and the Model version in the logic of the game, the gameplay and its error-checks. That way, we do not have to revise all the logic of the Piles in this game in order to animate some of it. A more complex game might benefit from -separating the Model and the View during the design and early coding stages — even into +separating the Model and the View during the design and early coding stages, even into separate classes. In this game we are using just a little separation of Model and View. The `_isAnimatedFlip` variable is `true` while there is an animated flip going on, otherwise `false`, and the `Card` class's `flip()` function is expanded to: @@ -583,7 +583,7 @@ now includes some animation: First we implement the `Action` value for this game. In the very first game, the KlondikeGame class sets defaults of `Action.newDeal` and `klondikeDraw = 1`, but after that the player can select an -action by pressing and releasing a button and KlondikeWorld saves it in KlondikeGame — or the player +action by pressing and releasing a button and KlondikeWorld saves it in KlondikeGame, or the player wins the game, in which case `Action.newDeal` is selected and saved automatically. The action usually generates and saves a new seed, but that is skipped if we have `Action.sameDeal`. Then we shuffle the cards, using whatever `seed` applies. @@ -677,7 +677,7 @@ Step 2 Scaffolding. ## Winning the game You win the game when all cards in all suits, Ace to King, have been moved to the Foundation Piles, -13 cards in each pile. The game now has code to recognize that event — an `isFull` test added to +13 cards in each pile. The game now has code to recognize that event: an `isFull` test added to the `FoundationPile`'s `acquireCard()` method, a callback to `KlondikeWorld` and a test as to whether all four Foundations are full. Here is the code: @@ -729,7 +729,7 @@ It is often possible to calculate whether you can win from a given position of t Klondike game, or could have won but missed a vital move. It is frequently possible to calculate whether the initial deal is winnable: a percentage of Klondike deals are not. But all that is far beyond the scope of this Tutorial, so for now it is up to the player to work out whether to keep -playing and try to win — or give up and press one of the buttons. +playing and try to win, or give up and press one of the buttons. ## Ending a game and re-starting it @@ -769,7 +769,7 @@ The `letsCelebrate()` method ends with similar code, but forces a new deal: ## The `Have fun` button When you win the Klondike Game, the `letsCelebrate()` method puts on a little display. To save you -having to play and win a whole game before you see it — **and** to test the method, we have +having to play and win a whole game before you see it (**and** to test the method), we have provided the `Have fun` button. Of course a real game could not have such a button... Well, this is it! The game is now more playable. diff --git a/doc/tutorials/platformer/step_6.md b/doc/tutorials/platformer/step_6.md index 8a32b17bced..0441f53c660 100644 --- a/doc/tutorials/platformer/step_6.md +++ b/doc/tutorials/platformer/step_6.md @@ -73,7 +73,7 @@ class HeartHealthComponent extends SpriteGroupComponent ``` -The `HeartHealthComponent` is just a [SpriteGroupComponent](../../flame/components.md#spritegroupcomponent) +The `HeartHealthComponent` is just a [SpriteGroupComponent](../../flame/components/sprite_components.md#spritegroupcomponent) that uses the heart images that were created early on. The unique thing that is being done, is when the component is created, it requires a `heartNumber`, so in the `update` method, we check to see if the `game.health` is less than the `heartNumber` and if so, change the state of the component to