├── .github └── workflows │ └── web.yml ├── .gitignore ├── .pubignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── doc ├── camera.md ├── cell_layers.md ├── getting_started.md ├── tiled_maps_basics.md └── tiled_maps_worlds.md ├── example ├── README.md ├── analysis_options.yaml ├── assets │ ├── images │ │ └── retro_tiles.png │ └── tiles │ │ ├── another_map.tmx │ │ ├── example.tmx │ │ ├── example.world │ │ └── tileset.tsx ├── lib │ ├── game.dart │ ├── main.dart │ └── minimal_game.dart ├── pubspec.yaml └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png │ ├── index.html │ ├── manifest.json │ └── spatial_grid_optimizer_worker.js ├── lib ├── flame_spatial_grid.dart └── src │ ├── collisions │ ├── broadphase.dart │ ├── broadphase │ │ ├── bloom_filter_provider.dart │ │ ├── collisions_cache.dart │ │ ├── comparator.dart │ │ ├── schedule_hitbox_operation.dart │ │ └── typedef.dart │ ├── collision_detection.dart │ ├── collision_prospect │ │ ├── collision_prospect.dart │ │ └── prospect_pool.dart │ ├── hitboxes │ │ ├── bounding_hitbox.dart │ │ ├── group_hitbox.dart │ │ ├── polygon_rect_component.dart │ │ ├── rectangle_hitbox_extensions.dart │ │ ├── rectangle_hitbox_optimized.dart │ │ └── shape_hitbox_extensions.dart │ └── optimizer │ │ ├── collision_optimizer.dart │ │ ├── extensions.dart │ │ ├── isolate │ │ ├── entry_point.dart │ │ ├── extensions.dart │ │ ├── flat_buffers │ │ │ ├── flat_buffers_optimizer.dart │ │ │ ├── generated │ │ │ │ ├── aabb2_optimizer_generated.dart │ │ │ │ ├── bounding_hitbox_optimizer_generated.dart │ │ │ │ ├── optimized_collisions_optimizer_generated.dart │ │ │ │ ├── rect_optimizer_generated.dart │ │ │ │ ├── request_optimizer_generated.dart │ │ │ │ ├── response_optimizer_generated.dart │ │ │ │ └── vector2_optimizer_generated.dart │ │ │ └── schema │ │ │ │ ├── aabb2.fbs │ │ │ │ ├── bounding_hitbox.fbs │ │ │ │ ├── optimized_collisions.fbs │ │ │ │ ├── rect.fbs │ │ │ │ ├── request.fbs │ │ │ │ ├── response.fbs │ │ │ │ └── vector2.fbs │ │ ├── geometry_universal.dart │ │ └── spatial_grid_optimizer_worker.dart │ │ └── optimized_collisions_list.dart │ ├── components │ ├── animation_global │ │ ├── animation_global_component.dart │ │ ├── ticker_global.dart │ │ └── tickers_manager.dart │ ├── camera_wrapper.dart │ ├── has_grid_support.dart │ ├── layers │ │ ├── cell_layer.dart │ │ ├── cell_static_animation_layer.dart │ │ ├── cell_static_layer.dart │ │ ├── cell_trail_layer.dart │ │ ├── has_trail_support.dart │ │ ├── layers_manager.dart │ │ └── scheduled_layer_operation.dart │ ├── macro_object.dart │ ├── restorable_state_mixin.dart │ ├── tile_component.dart │ └── utility │ │ ├── action_notifier.dart │ │ ├── debug_component.dart │ │ ├── on_demand_actions.dart │ │ ├── pure_type_check_interface.dart │ │ └── scheduler │ │ ├── action_provider.dart │ │ ├── scheduler.dart │ │ └── with_action_provider_mixin.dart │ ├── core │ ├── cell.dart │ ├── flame_override │ │ ├── base_game.dart │ │ ├── component_extensions.dart │ │ ├── component_tree_root_ex.dart │ │ └── flame_game_ex.dart │ ├── has_spatial_grid_framework.dart │ ├── spatial_grid.dart │ └── vector2_simd.dart │ ├── tiled │ ├── map_loader.dart │ ├── sprite_loader.dart │ ├── tile_builder_context.dart │ ├── tile_builder_context_provider.dart │ ├── tile_cache.dart │ ├── tile_data_provider.dart │ ├── tileset_manager.dart │ ├── tileset_parser.dart │ ├── world_data.dart │ └── world_loader.dart │ └── ui │ └── loading_progress_manager.dart └── pubspec.yaml /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | name: Gh-Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 # Only works with v2 13 | - uses: subosito/flutter-action@v1 14 | - run: cd example; flutter config --enable-web 15 | - run: cd example; flutter build web --release --base-href /flame_spatial_grid/ 16 | - run: git config user.name github-actions 17 | - run: git config user.email github-actions@github.com 18 | - run: cd example; git --work-tree build/web add --all 19 | - run: git commit -m "Automatic deployment by github-actions" 20 | - run: git push origin HEAD:gh-pages --force -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | example/linux 2 | example/windows 3 | example/macos 4 | example/web 5 | *.iml 6 | .idea/ 7 | tags 8 | flatc.exe -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.2 2 | 3 | * Flame 1.16.0 support 4 | * onCurrentCellChanged callback for HasGridSupport mixin 5 | 6 | 7 | # 0.9.1 8 | 9 | * Minimum Flame version is 1.14.0 10 | * Bug fix with collision cache for objects that are grouped into layer 11 | * onActivate callback cor component, when it's cell is moved from inactive state into active 12 | 13 | # 0.9.0 14 | 15 | * Support Flame 1.12.0 and upper 16 | * Multithreading for collision optimisation (including web) 17 | * Raycasting speed optimized 18 | * 2x faster collision detection for active collisions 19 | 20 | # 0.8.4 21 | 22 | * Flame 1.9.0 support 23 | 24 | # 0.8.3 25 | 26 | * Basic raycast support 27 | * Hotfix `recreateBoundingHitbox` function 28 | * BREAKING: `noMapCellBuilder` accepts list of map rects instead of `isFullyOutside` variable 29 | 30 | # 0.8.2 31 | 32 | * Flame 1.8.2 compatibility fixes 33 | * Tiled world: streaming maps 34 | * Broadphase and collision detection optimisations 35 | * Improving CollisionOptimizer performance 36 | * CollisionProspect pool support 37 | * StaticCellLayer reusing Image instances 38 | * Loading maps and building cells is much faster now 39 | * BREAKING CHANGES in API during refactoring... 40 | 41 | # 0.8.1 42 | 43 | * Fixing links to examples 44 | 45 | # 0.8.0 46 | 47 | * Publishing working version after almost year of development -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alexey Volkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API DOCS ARE OUTDATED 2 | 3 | Hope I'll have a week of free time (or even two) to make them actual... 4 | 5 | # Overview 6 | 7 | This library is a framework and set of tools for working with infinite game fields, loading 8 | infinite maps for one game level, creating an infinite count of game objects and active game players 9 | and NPCs - as much as possible. 10 | 11 | The framework is fully compatible with Flame and does not contain any strict requirements for the 12 | game structure. Even existing games could be migrated to this framework if necessary. 13 | 14 | This is still in beta. API and whole architecture might be changed at any time, including breaking 15 | changes. 16 | 17 | ## Features 18 | 19 | There are a lot of utility functions and the list of its features is too long to describe at a fast 20 | glance. 21 | So here is a list of only top-level features: 22 | 23 | - __Building endless game levels__ without the need to change the screen for rendering another map. 24 | System gives you the ability to create big open worlds with seamless loading of new maps while a 25 | player approaches to them. 26 | - __Build game levels with a destructible environment.__ Every Tiled tile could be converted into 27 | separate game component and handle interactions with other game elements individually 28 | - __Building procedural-generated__ pieces of a map (or whole maps) on the fly, as the player 29 | approaches to them. 30 | - __Wise resource management__: the system does not eat resources greedily, it takes care of proper 31 | allocation and de-allocation so you can enjoy your game even in a browser or weak mobile phone. 32 | - __New visual effects__: lean resource management system allows to create trails for many players 33 | and persists during a long game session, blending with other game elements like ground. 34 | 35 | Some of possible features might still be undiscovered :-) 36 | 37 | ## Core concepts 38 | 39 | The core of the framework is a spatial grid that is building on-the-fly and controls component's 40 | visibility and activity, loads and unloads maps by chunks. It allows optimizing rendering by 41 | pre-rendering statical components into images, but keeps images size small enough and unloads unused 42 | chunks from memory. 43 | 44 | ## Usage: minimal setup 45 | 46 | 1. Add `HasSpatialGridFramework` mixin into you game 47 | 2. Add `HasGridSupport` mixin to every game component 48 | 3. Call `initializeSpatialGrid` at your game's `onLoad` function before adding any component into 49 | game. 50 | 4. Enjoy! 51 | 52 | See detailed "minimal start" tutorial at [Getting Started](doc/getting_started.md) section. 53 | 54 | See [game.dart](example/lib/game.dart) for working code example 55 | Check out working demo at https://asgalex.github.io/flame_spatial_grid/ 56 | 57 | ## Future usage instructions 58 | 59 | 0. [Getting Started Guide](doc/getting_started.md), if you still did not read it 60 | 1. [Working with Tiled maps](doc/tiled_maps_basics.md) 61 | 2. [Working with Tiled worlds](doc/tiled_maps_worlds.md) 62 | 3. [Optimizing static objects rendering and collisions using CellLayers](doc/cell_layers.md) 63 | 4. [Interacting with Flame's Cameras](doc/camera.md) 64 | 5. [TODO] Creating trails and other temporary marks on the ground. 65 | 66 | ## Advanced section 67 | 68 | 1. [TODO] How custom collision detection system works 69 | 2. [TODO] How CellLayers optimizes collision detection. 70 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | analyzer: 13 | exclude: 14 | - "**/*.g.dart" 15 | - "lib/src/collisions/optimizer/isolate/flat_buffers/generated/*.dart" 16 | - "lib/src/collisions/optimizer/isolate/geometry_universal.dart" 17 | - "lib/src/collisions/optimizer/isolate/spatial_grid_optimizer_worker.dart" 18 | - "lib/src/components/macro_object.dart" 19 | 20 | 21 | linter: 22 | # The lint rules applied to this project can be customized in the 23 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 24 | # included above or to enable additional rules. A list of all available lints 25 | # and their documentation is published at 26 | # https://dart-lang.github.io/linter/lints/index.html. 27 | # 28 | # Instead of disabling a lint rule for the entire project in the 29 | # section below, it can also be suppressed for a single line of code 30 | # or a specific dart file by using the `// ignore: name_of_lint` and 31 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 32 | # producing the lint. 33 | rules: 34 | - always_declare_return_types 35 | - always_put_control_body_on_new_line 36 | - always_use_package_imports 37 | - annotate_overrides 38 | - avoid_bool_literals_in_conditional_expressions 39 | - avoid_double_and_int_checks 40 | - avoid_dynamic_calls 41 | - avoid_empty_else 42 | - avoid_equals_and_hash_code_on_mutable_classes 43 | - avoid_escaping_inner_quotes 44 | - avoid_field_initializers_in_const_classes 45 | - avoid_final_parameters 46 | - avoid_init_to_null 47 | - avoid_js_rounded_ints 48 | - avoid_null_checks_in_equality_operators 49 | - avoid_print 50 | - avoid_private_typedef_functions 51 | - avoid_redundant_argument_values 52 | - avoid_relative_lib_imports 53 | - avoid_return_types_on_setters 54 | - avoid_shadowing_type_parameters 55 | - avoid_single_cascade_in_expression_statements 56 | - avoid_slow_async_io 57 | - avoid_type_to_string 58 | - avoid_types_as_parameter_names 59 | - avoid_unused_constructor_parameters 60 | - avoid_void_async 61 | - await_only_futures 62 | - camel_case_extensions 63 | - camel_case_types 64 | - cancel_subscriptions 65 | - cast_nullable_to_non_nullable 66 | - close_sinks 67 | - comment_references 68 | - constant_identifier_names 69 | - control_flow_in_finally 70 | - curly_braces_in_flow_control_structures 71 | - depend_on_referenced_packages 72 | - deprecated_consistency 73 | - directives_ordering 74 | - do_not_use_environment 75 | - empty_catches 76 | - empty_constructor_bodies 77 | - empty_statements 78 | - exhaustive_cases 79 | - file_names 80 | - flutter_style_todos 81 | - hash_and_equals 82 | - implementation_imports 83 | - collection_methods_unrelated_type 84 | - join_return_with_assignment 85 | - library_names 86 | - library_prefixes 87 | - lines_longer_than_80_chars 88 | - literal_only_boolean_expressions 89 | - missing_whitespace_between_adjacent_strings 90 | - no_adjacent_strings_in_list 91 | - no_duplicate_case_values 92 | - no_runtimeType_toString 93 | - non_constant_identifier_names 94 | - noop_primitive_operations 95 | - null_closures 96 | - omit_local_variable_types 97 | - package_api_docs 98 | - package_names 99 | - package_prefixed_library_names 100 | - parameter_assignments 101 | - prefer_adjacent_string_concatenation 102 | - prefer_asserts_in_initializer_lists 103 | - prefer_collection_literals 104 | - prefer_conditional_assignment 105 | - prefer_const_constructors 106 | - prefer_const_constructors_in_immutables 107 | - prefer_const_declarations 108 | - prefer_const_literals_to_create_immutables 109 | - prefer_constructors_over_static_methods 110 | - prefer_contains 111 | - prefer_final_fields 112 | - prefer_final_in_for_each 113 | - prefer_final_locals 114 | - prefer_for_elements_to_map_fromIterable 115 | - prefer_foreach 116 | - prefer_function_declarations_over_variables 117 | - prefer_generic_function_type_aliases 118 | - prefer_if_elements_to_conditional_expressions 119 | - prefer_if_null_operators 120 | - prefer_initializing_formals 121 | - prefer_inlined_adds 122 | - prefer_interpolation_to_compose_strings 123 | - prefer_is_empty 124 | - prefer_is_not_empty 125 | - prefer_is_not_operator 126 | - prefer_iterable_whereType 127 | - prefer_null_aware_operators 128 | - prefer_single_quotes 129 | - prefer_spread_collections 130 | - prefer_typing_uninitialized_variables 131 | - prefer_void_to_null 132 | - provide_deprecation_message 133 | - recursive_getters 134 | - require_trailing_commas 135 | - slash_for_doc_comments 136 | - sort_child_properties_last 137 | - sort_unnamed_constructors_first 138 | - test_types_in_equals 139 | - throw_in_finally 140 | - type_annotate_public_apis 141 | - type_init_formals 142 | - unnecessary_await_in_return 143 | - unnecessary_brace_in_string_interps 144 | - unnecessary_const 145 | - unnecessary_constructor_name 146 | - unnecessary_getters_setters 147 | - unnecessary_lambdas 148 | - unnecessary_new 149 | - unnecessary_null_aware_assignments 150 | - unnecessary_null_checks 151 | - unnecessary_null_in_if_null_operators 152 | - unnecessary_nullable_for_final_variable_declarations 153 | - unnecessary_overrides 154 | - unnecessary_parenthesis 155 | - unnecessary_raw_strings 156 | - unnecessary_statements 157 | - unnecessary_string_escapes 158 | - unnecessary_string_interpolations 159 | - unnecessary_this 160 | - unnecessary_to_list_in_spreads 161 | - unrelated_type_equality_checks 162 | - unsafe_html 163 | - use_enums 164 | - use_full_hex_values_for_flutter_colors 165 | - use_function_type_syntax_for_parameters 166 | - use_if_null_to_convert_nulls_to_bools 167 | - use_is_even_rather_than_modulo 168 | - use_key_in_widget_constructors 169 | - use_late_for_private_fields_and_variables 170 | - use_named_constants 171 | - use_raw_strings 172 | - use_rethrow_when_possible 173 | - use_super_parameters 174 | - use_test_throws_matchers 175 | - valid_regexps 176 | - void_checks 177 | 178 | # Additional information about this file can be found at 179 | # https://dart.dev/guides/language/analysis-options 180 | -------------------------------------------------------------------------------- /doc/camera.md: -------------------------------------------------------------------------------- 1 | # Interacting with Flame's Cameras 2 | 3 | Framework interactions with a camera might be a too complicated thing, but thanks to the new camera 4 | API it is not. 5 | 6 | If you use `CameraComponent`, you just need to wrap it into the `SpatialGridCameraWrapper` class and 7 | pass this class into `initializeSpatialGrid` as the `trackedComponent` parameter. That's all! The 8 | spatial grid framework will react to the camera's movement, and expand or shrink the active area on 9 | zoom events automatically. 10 | 11 | 12 | Here is example of setup: 13 | 14 | ```dart 15 | 16 | @override 17 | Future onLoad() async { 18 | super.onLoad(); 19 | 20 | cameraComponent = CameraComponent(world: world); 21 | cameraComponent.viewfinder.zoom = 5; 22 | cameraComponent.follow(player, maxSpeed: 40); 23 | 24 | await initializeSpatialGrid( 25 | 26 | /// other initialization parameters are omitted 27 | /// 28 | /// Just wrap the cameraComponent into the SpatialGridCameraWrapper and pass to the parameter. 29 | /// There is no need to add either cameraComponent or wrapper into game explicitly. 30 | trackedComponent: SpatialGridCameraWrapper(cameraComponent), 31 | ); 32 | } 33 | 34 | ``` -------------------------------------------------------------------------------- /doc/cell_layers.md: -------------------------------------------------------------------------------- 1 | # Optimizing static objects rendering and collisions using CellLayers 2 | 3 | ## Overview 4 | 5 | The Framework offers your a way to improve the speed of rendering static objects. Every game Cell 6 | could have a special `CellLayer` component whose purpose is batching components rendering, compiling 7 | component's sprites into single `Image`, and updating the image in case some component did change. 8 | Of course, such approach is only effective for rarely modifiable objects. 9 | 10 | Every `CellLayer` class is a component, so you can easily create and add it into the game manually. 11 | But it is recommended to use game's `LayersManager` instance, embedded 12 | into `HasSpatialGridFramework` mixin. This allows you to forget about resource management because 13 | CellLayer's lifecycle will be controlled by the Framework. 14 | 15 | ## Optimizing SpriteComponent's rendering 16 | 17 | To add a `SpriteComponent` to a layer, the component must meet one general condition: it must 18 | have `HasGridSupport` mixin! 19 | 20 | Suppose, you have `HasSpatialGridFramework` instance in the `game` variable. Then, instead of 21 | calling `add` method, use game's `layersManager` as follows: 22 | 23 | ```dart 24 | game.layersManager.addComponent 25 | ( 26 | component: anSpriteComponent, 27 | layerType: MapLayerType.static, // Layer's type 28 | layerName: "Layer's unique name", 29 | priority: 2, // Layer's priority 30 | ); 31 | ``` 32 | 33 | That's all! All components, added to the layer, will be rendered as `Image`, and the `Image` will 34 | be updated only when layer's components parameters being changed. 35 | 36 | ## Optimizing SpriteAnimationComponent's rendering 37 | 38 | Animated components also could be optimized in the same way as SpriteComponents. But you should 39 | notice that this works only for components with the same animations. Components with different 40 | animations should be added to different layers. 41 | 42 | ```dart 43 | game.layersManager.addComponent 44 | ( 45 | component: anSpriteAnimationComponent, 46 | layerType: MapLayerType.animated, // Layer's type 47 | layerName: "Layer's unique name", 48 | priority: 2, // Layer's priority 49 | ); 50 | ``` 51 | 52 | As you can see, everything is the same, only `layerType` was changed. 53 | 54 | ## Re-using layers images 55 | 56 | There are many cases when two layers can generate same images. Simplest example - a ground pattern 57 | of game map. 58 | To avoid creating a lot of similar `Image` instances, every `CellLayer` class generates a key using 59 | significant tile's parameters. By default it is component's position, component's size and 60 | component's `runtimeType` or tile type from tiled data. 61 | If your component have different set of parameters, which could affect rendered image, you might 62 | want to override component's string used for key generation by adding `LayerCacheKeyProvider` mixin 63 | and re-implementing `getComponentUniqueString` function. 64 | It is also recommended to keep `cellSize` value multiple of map's tile size. For example, with tile 65 | size 8 and `cellSize` 100 there will be much less reused `Image` instances than with `cellSize` 128. 66 | 67 | ## Layers rendering mode 68 | 69 | Layer can be rendered by several ways: 70 | 71 | 1. Just using `renderTree`, as ordinary component 72 | 2. Call `renderTree` once and save result into `Picture` 73 | 3. Do pt.2 and then rasterize `Picture` to `Image` 74 | 4. Choose between 2 and 3 automatically. 75 | 76 | You can control rendering method using `LayerRenderMode` enum and `CellLayer`'s `renderMode` 77 | parameter. The `LayersManager` also have such option in `addComponent`. 78 | 79 | The last variant is most complex. In this mode every layer is rendered into `Picture`. And next 5 80 | seconds the layer is rendered from Picture. If no re-compiling layer event did happen, the `Picture` 81 | will be rasterized to `Image` and `Image` will be used for rendering till next layer recompilation. 82 | 83 | Such approach is useful, when layer is not permanent and sometimes can be updated very often. Such 84 | approach helps to skip expensive image rasterization step when layer is updated too intensively. 85 | 86 | ## Collisions optimizing 87 | 88 | Every layer offers a way of collision optimization. It's enabled by default. To disable this, you 89 | should use the `optimizeCollisions` parameter. 90 | 91 | The optimization logic is simple: all objects in the layer are checked for being overlapped. If so, 92 | a new `GroupHitbox` is created for a set of overlapped components, and this special kind of hitbox 93 | is involved in the collision detection broad phase. And only if a component collides 94 | with `GroupHitbox` - the second pass of the broad phase is started to find out concrete components 95 | in the grouped set. 96 | 97 | Items count in one group is limited to 25 items by default. This limitation 98 | allows to avoid iterating hundreds of grouped items in a moment of collision and prevents heavy 99 | performance drops. Use `collisionOptimizerGroupLimit` parameter of `initializeSpatialGrid` to change 100 | default limit to your value, or change `HasSpatialGridFramework.collisionOptimizerDefaultGroupLimit` 101 | directly anywhere in application's runtime. You also can change this value for a layer individually 102 | by changing `CellLayer.collisionOptimizer.maximumItemsInGroup`, but all global changes will be 103 | ignored for the Layer then. 104 | 105 | If you enable debug mode either in `initializeSpatialGrid` or using the `isSpatialGridDebugEnabled` 106 | setter, you will see blue lines in place of group hitboxes. 107 | 108 | This approach allows the collision detection system to operate with spaces smaller than a Cell's 109 | space. This is an attempt to obtain the QuadTree approach's advantages without its drawbacks 110 | 111 | ## Collision approximation 112 | 113 | In a game, you might have a situation where some of game objects do not need to have very 114 | accurate information about collisions. For example, imagine that your NPC might be in two modes: 115 | random walking and chasing. In the second mode, it needs to know exact collision information because 116 | it tries to chase and hit a player and needs to avoid any small obstacles. Whereas in the first 117 | mode, the NPC does not try to achieve any goal, it just shows you a random pointless movement. So, 118 | there is no problem if some of the map areas become unreachable for the NPC due to inaccurate 119 | collision calculation. 120 | 121 | In the Framework collision approximation works for layers with optimized collisions. Normally, if an 122 | object is colliding with `GroupHitbox`, the second pass of the broad phase is performed to find out, 123 | what objects from `GroupHitbox` are colliding with the component. But you can skip this phase if 124 | collision approximation is enabled. So, onCollision callbacks will not report you about a collision 125 | with a game component, but with a CellLayer instead. So you also should modify your component's 126 | collision handling to support approximated mode. 127 | 128 | Let's look to a code example: 129 | 130 | ```dart 131 | class Npc extends SpriteComponent with HasGridSupport { 132 | Npc() { 133 | /// This will enable collision approximation for 134 | /// listed CellLayer names 135 | boundingBox.groupCollisionsTags..add('Brick')..add('Water'); 136 | } 137 | 138 | @override 139 | void onCollision(Set intersectionPoints, 140 | PositionComponent other,) { 141 | if (other is CellLayer) { 142 | /// Collision approximation is enabled and we just have to collide with an 143 | /// GroupHitbox of "other" CellLayer 144 | } else { 145 | /// Normal collisions with components 146 | } 147 | } 148 | } 149 | 150 | /// Somewhere in cell builder function: 151 | Future onBuildWater(CellBuilderContext context) async { 152 | final waterAnimation = 153 | getPreloadedTileData('tileset', 'Water')?.spriteAnimation; 154 | final water = Water( 155 | position: context.position, 156 | animation: waterAnimation, 157 | context: context, 158 | ); 159 | water.currentCell = context.cell; 160 | game.layersManager.addComponent( 161 | component: water, 162 | layerType: MapLayerType.animated, 163 | 164 | /// The layer's name is important because it should match with tags we added to NPC's hitbox 165 | layerName: 'Water', 166 | ); 167 | } 168 | ``` 169 | 170 | So, you can add or remove tags in the `groupCollisionsTags` list at any time. This will allow you to 171 | do 25 fewer checks on every collision with `GroupHitbox` 172 | 173 | ## Component storage modes 174 | 175 | In some causes it does not worth to store layer's components in default component's tree of 176 | FlameGame. For example: 177 | 1. If layer will be generated only once and will not be changed forever. 178 | 2. If layer could be updated but we do not need to call `update` function of layer's children 179 | 180 | The `LayerComponentsStorageMode` enum and `componentsStorageMode` parameter of 181 | `LayersManager.addComponent` function allows you to control this behavior. By default every layer's 182 | child will be pushed into FlameGame's component tree. Alternatively, you can save it into 183 | internal layer's storage and adding\removing components into layer will be faster in this case. 184 | Finally, you might use `LayerComponentsStorageMode.removeAfterCompile` option which means that all 185 | added components will be removed after compilation of layer's `Image`. 186 | 187 | Keep in mind, that only default `LayerComponentsStorageMode.defaultComponentTree` option keeps 188 | layer's components interactable, all other options will disable collision behavior for components, 189 | in fact you can use them only for rendering image. -------------------------------------------------------------------------------- /doc/tiled_maps_basics.md: -------------------------------------------------------------------------------- 1 | # Working with Tiled maps 2 | 3 | ## Basics 4 | 5 | In general the approach to working with Tiled maps is concluded that every map's tile should be 6 | converted to a component - more complex or less. 7 | 8 | Every tiled map should be described by special class, subclassed from `TiledMapLoader`. This class 9 | will describe map's core parameters and tiles build logics. For every additional map you should to 10 | implement additional loader class. 11 | 12 | The minimal functional custom map loader would be looks as follows: 13 | 14 | ```dart 15 | class ExampleMapLoader extends TiledMapLoader { 16 | @override 17 | String fileName = 'example.tmx'; 18 | 19 | @override 20 | Vector2 get destTileSize => Vector2.all(16); 21 | 22 | @override 23 | Map? get tileBuilders => null; 24 | } 25 | ``` 26 | 27 | Also you should to list you map in `initializeSpatialGrid` function: 28 | 29 | ```dart 30 | FutureOr onLoad() async { 31 | await initializeSpatialGrid( 32 | cellSize: 50, 33 | activeRadius: const Size(2, 2), 34 | unloadRadius: const Size(2, 2), 35 | trackWindowSize: false, 36 | 37 | /// You can list multiple maps here, including multiple instances of the same map, but with 38 | /// different initial position 39 | maps: [ 40 | ExampleMapLoader(), 41 | ], 42 | ); 43 | 44 | return super.onLoad(); 45 | } 46 | ``` 47 | 48 | This will result in rendering your map just as a simple background image without any interactive 49 | elements. So it is very close to core `RenderableTiledMap` functionality, but this class was 50 | designed to offer your much more power. 51 | 52 | Suppose we have any [objects](https://doc.mapeditor.org/en/stable/manual/objects/) in our tiled map. 53 | We can to assign a 'personal' builder for object of any class using `tileBuilders` property: 54 | 55 | ```dart 56 | class ExampleMapLoader extends TiledMapLoader { 57 | @override 58 | String fileName = 'example.tmx'; 59 | 60 | @override 61 | Vector2 get destTileSize => Vector2.all(16); 62 | 63 | @override 64 | Map? get tileBuilders => 65 | { 66 | 'TestObject': onBuildTestObject, 67 | }; 68 | 69 | Future onBuildTestObject(CellBuilderContext context) async { 70 | /// add game components related to object here 71 | /// access to object's data, using [context.tiledObject] property 72 | } 73 | 74 | } 75 | ``` 76 | 77 | But this is not limited only to objects. You also can handle every tile processing, according to 78 | the tile's class as well: 79 | 80 | ```dart 81 | class ExampleMapLoader extends TiledMapLoader { 82 | @override 83 | Map? get tileBuilders => 84 | { 85 | 'TestObject': onBuildTestObject, 86 | 'AnyTileClass': onBuildAnyTile, 87 | }; 88 | 89 | Future onBuildAnyTile(CellBuilderContext context) async { 90 | /// add tile representation here 91 | /// access to object's data, using [context.tileDataProvider] property 92 | } 93 | } 94 | ``` 95 | 96 | `CellBuilderContext` is a special class with all information about a tile or object. It offers you 97 | easy access to the most wanted properties. If you need to read additional tile's or object's 98 | parameters, you can access core classes `tileObject` - for objects processing - 99 | and `tileDataProvider.tile` - for tiles processing. 100 | 101 | The `tileDataProvider` provides such useful functions like `getSprite` for accessing tile's sprite, 102 | and `getSpriteAnimation` to get tile's `SpriteAnimation`. For animations variable step times are 103 | 104 | ## Customizing map's background layer building 105 | 106 | The `notFoundBuilder` is supposed to be used for processing all tile classes, not described 107 | explicitly in `tileBuilders` property. It offers default functionality through `genericTileBuilder` 108 | function of core `TiledMapLoader` class. 109 | 110 | In general it should work fine for most of cases, but the one thing you might want to customize 111 | there is `priority` of components. Especially for this case `context.priorityOverride` variable 112 | exists. You should to change it before calling `genericTileBuilder`. 113 | 114 | Let's see an example. Here individual priority is specified according to layer's name: 115 | 116 | ```dart 117 | @override 118 | TileBuilderFunction? get notFoundBuilder => onBackgroundBuilder; 119 | 120 | Future onBackgroundBuilder(CellBuilderContext context) async { 121 | var priority = -3; 122 | if (context.layerInfo.name == 'moss') { 123 | priority = -2; 124 | } else if (context.layerInfo.name == 'flowers') { 125 | priority = -1; 126 | } 127 | context.priorityOverride = priority; 128 | 129 | super.genericTileBuilder(context); 130 | } 131 | ``` 132 | 133 | ## Map's tiles lifecycle 134 | 135 | Because the Framework could occasionally unload old cells with all components inside, map builders 136 | functions could be called many times till a game process. 137 | 138 | Every map tile's information normally is stored into `CellBuilderContext`. These context classes are 139 | stored whole game and are reused in time when a piece of map should be rebuilt. 140 | 141 | That means that you can safely save `CellBuilderContext` somewhere in your game and change some of 142 | it's parameters to keep your environment changes between map's chunks reload. But keep in mind 143 | that `position` of tile should be kept inside the `cellRect`. You can also use `remove` property to 144 | indicate to the Framework that this tile or object should not be recreated in future map cell's 145 | reload. 146 | 147 | In general, this part of API still is not too user-friendly. TODO: will be fixed in future releases. 148 | 149 | -------------------------------------------------------------------------------- /doc/tiled_maps_worlds.md: -------------------------------------------------------------------------------- 1 | # Working with Tiled worlds 2 | 3 | ## Overview 4 | 5 | Tiled *.world file format is an easy way to load multiple maps into a game. With Flame, you will 6 | most probably need to unload a previous map and load a next. Additionally, flame_tiled does not 7 | support *.world files at all. 8 | 9 | With this Framework loading multiple maps into single game area become easy. All maps from the 10 | "world" would be rendered as one big map, but the Framework's resource management system will 11 | preserve system resources for you. 12 | 13 | By default, the Framework loads full map, on which the tracked component stays now - to prevent 14 | loading freezes during intensive game process. This behavior can be disabled by `loadWholeMap` 15 | para meter of `WorldLoader`. 16 | 17 | The Framework takes care about resources and it automatically builds only current map, and also 18 | neighbour maps, if exists. The `SpatialGrid` class is re-used to perform that calculations. 19 | `loadWholeMap` option disables this behavior, if set to `false`; 20 | 21 | ## Usage 22 | 23 | Unlike with `TiledMapLoader` you need not to create new subclasses of the `WorldLoader` class. But 24 | you still need to implement a class for every map of the "world", as described in section 25 | [Working with Tiled maps](tiled_maps_basics.md) 26 | 27 | After that, just add a new parameter into `initializeSpatialGrid` function: 28 | 29 | ```dart 30 | 31 | @override 32 | Future onLoad() async { 33 | await initializeSpatialGrid( 34 | /// 35 | ///omitting other parameters 36 | /// 37 | worldLoader: WorldLoader( 38 | fileName: 'example.world', 39 | mapLoader: {'example': DemoMapLoader(), 'another_map': AnotherMapLoader()}, 40 | ), 41 | ); 42 | } 43 | ``` 44 | 45 | That's all! 46 | 47 | You can safely combine `worldLoader` parameter with `maps` parameter. In fact, `WorldLoader` is just 48 | automation of `maps` parameter functionality. 49 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Flame spatial grid example 2 | 3 | Core features demonstration 4 | 5 | ## Getting Started 6 | 7 | In this example the spatial partitioning algorithm work. 8 | Algorithm takes control over collision detection, components rendering and 9 | components lifecycle and frequency of updates. This allows to gain application 10 | performance by saving additional resources. Also a special 'Layer-components' 11 | are used to compile statical components to single layer but keeping ability to 12 | update layer's image as soon as components parameters are changed. 13 | 14 | Use WASD to move the player and use the mouse scroll to change zoom. 15 | Hold direction button and press space to fire a bullet. 16 | Notice that bullet will fly above water but collides with bricks. 17 | 18 | Press LShift button to toggle firing bullets which also destroys water. 19 | 20 | Press T button to toggle player to collide with other objects. 21 | 22 | Press at any screen point to teleport Player instantly to the click position. 23 | 24 | Press C to spawn more active components 25 | 26 | Press M button to show clusters debugging info. Green clusters are active 27 | clusters, where components are viewed as usual, update() and collision detection 28 | works its ordinary way. 29 | Grey clusters are inactive clusters. Such clusters intend to be 30 | "out-of-the-screen", components are not rendering inside such clusters. 31 | Dark clusters shown if you moved too far for your's starting position. Such 32 | clusters are suspended: components are not rendering, update() not work and 33 | all collisions are disabled. -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Source of linter options: 2 | # https://dart-lang.github.io/linter/lints/options/options.html 3 | 4 | analyzer: 5 | exclude: 6 | - "**/*.g.dart" 7 | 8 | linter: 9 | rules: 10 | - always_declare_return_types 11 | - always_put_control_body_on_new_line 12 | - always_use_package_imports 13 | - annotate_overrides 14 | - avoid_bool_literals_in_conditional_expressions 15 | - avoid_double_and_int_checks 16 | - avoid_catches_without_on_clauses 17 | - avoid_dynamic_calls 18 | - avoid_empty_else 19 | - avoid_equals_and_hash_code_on_mutable_classes 20 | - avoid_escaping_inner_quotes 21 | - avoid_field_initializers_in_const_classes 22 | - avoid_final_parameters 23 | - avoid_init_to_null 24 | - avoid_js_rounded_ints 25 | - avoid_null_checks_in_equality_operators 26 | - avoid_print 27 | - avoid_private_typedef_functions 28 | - avoid_redundant_argument_values 29 | - avoid_relative_lib_imports 30 | - avoid_return_types_on_setters 31 | - avoid_shadowing_type_parameters 32 | - avoid_single_cascade_in_expression_statements 33 | - avoid_slow_async_io 34 | - avoid_type_to_string 35 | - avoid_types_as_parameter_names 36 | - avoid_unused_constructor_parameters 37 | - avoid_void_async 38 | - await_only_futures 39 | - camel_case_extensions 40 | - camel_case_types 41 | - cancel_subscriptions 42 | - cast_nullable_to_non_nullable 43 | - close_sinks 44 | - comment_references 45 | - constant_identifier_names 46 | - control_flow_in_finally 47 | - curly_braces_in_flow_control_structures 48 | - depend_on_referenced_packages 49 | - deprecated_consistency 50 | - directives_ordering 51 | - do_not_use_environment 52 | - empty_catches 53 | - empty_constructor_bodies 54 | - empty_statements 55 | - exhaustive_cases 56 | - file_names 57 | - flutter_style_todos 58 | - hash_and_equals 59 | - implementation_imports 60 | - join_return_with_assignment 61 | - library_names 62 | - library_prefixes 63 | - lines_longer_than_80_chars 64 | - collection_methods_unrelated_type 65 | - literal_only_boolean_expressions 66 | - missing_whitespace_between_adjacent_strings 67 | - no_adjacent_strings_in_list 68 | - no_duplicate_case_values 69 | - no_runtimeType_toString 70 | - non_constant_identifier_names 71 | - noop_primitive_operations 72 | - null_closures 73 | - omit_local_variable_types 74 | - package_api_docs 75 | - package_names 76 | - package_prefixed_library_names 77 | - parameter_assignments 78 | - prefer_adjacent_string_concatenation 79 | - prefer_asserts_in_initializer_lists 80 | - prefer_collection_literals 81 | - prefer_conditional_assignment 82 | - prefer_const_constructors 83 | - prefer_const_constructors_in_immutables 84 | - prefer_const_declarations 85 | - prefer_const_literals_to_create_immutables 86 | - prefer_constructors_over_static_methods 87 | - prefer_contains 88 | - prefer_final_fields 89 | - prefer_final_in_for_each 90 | - prefer_final_locals 91 | - prefer_for_elements_to_map_fromIterable 92 | - prefer_foreach 93 | - prefer_function_declarations_over_variables 94 | - prefer_generic_function_type_aliases 95 | - prefer_if_elements_to_conditional_expressions 96 | - prefer_if_null_operators 97 | - prefer_initializing_formals 98 | - prefer_inlined_adds 99 | - prefer_interpolation_to_compose_strings 100 | - prefer_is_empty 101 | - prefer_is_not_empty 102 | - prefer_is_not_operator 103 | - prefer_iterable_whereType 104 | - prefer_null_aware_operators 105 | - prefer_single_quotes 106 | - prefer_spread_collections 107 | - prefer_typing_uninitialized_variables 108 | - prefer_void_to_null 109 | - provide_deprecation_message 110 | - recursive_getters 111 | - require_trailing_commas 112 | - slash_for_doc_comments 113 | - sort_child_properties_last 114 | - sort_unnamed_constructors_first 115 | - test_types_in_equals 116 | - throw_in_finally 117 | - type_annotate_public_apis 118 | - type_init_formals 119 | - unnecessary_await_in_return 120 | - unnecessary_brace_in_string_interps 121 | - unnecessary_const 122 | - unnecessary_constructor_name 123 | - unnecessary_getters_setters 124 | - unnecessary_lambdas 125 | - unnecessary_new 126 | - unnecessary_null_aware_assignments 127 | - unnecessary_null_checks 128 | - unnecessary_null_in_if_null_operators 129 | - unnecessary_nullable_for_final_variable_declarations 130 | - unnecessary_overrides 131 | - unnecessary_parenthesis 132 | - unnecessary_raw_strings 133 | - unnecessary_statements 134 | - unnecessary_string_escapes 135 | - unnecessary_string_interpolations 136 | - unnecessary_this 137 | - unnecessary_to_list_in_spreads 138 | - unrelated_type_equality_checks 139 | - unsafe_html 140 | - use_enums 141 | - use_full_hex_values_for_flutter_colors 142 | - use_function_type_syntax_for_parameters 143 | - use_if_null_to_convert_nulls_to_bools 144 | - use_is_even_rather_than_modulo 145 | - use_key_in_widget_constructors 146 | - use_late_for_private_fields_and_variables 147 | - use_named_constants 148 | - use_raw_strings 149 | - use_rethrow_when_possible 150 | - use_super_parameters 151 | - use_test_throws_matchers 152 | - valid_regexps 153 | - void_checks 154 | -------------------------------------------------------------------------------- /example/assets/images/retro_tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/assets/images/retro_tiles.png -------------------------------------------------------------------------------- /example/assets/tiles/example.world: -------------------------------------------------------------------------------- 1 | { 2 | "maps": [ 3 | { 4 | "fileName": "example.tmx", 5 | "height": 400, 6 | "width": 400, 7 | "x": -72, 8 | "y": 16 9 | }, 10 | { 11 | "fileName": "another_map.tmx", 12 | "height": 800, 13 | "width": 800, 14 | "x": -72, 15 | "y": 472 16 | } 17 | ], 18 | "onlyShowAdjacentMaps": false, 19 | "type": "world" 20 | } 21 | -------------------------------------------------------------------------------- /example/assets/tiles/tileset.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flame/game.dart'; 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | import 'package:flame_spatial_grid_example/game.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | void main() { 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | const MyApp({super.key}); 14 | 15 | // This widget is the root of your application. 16 | @override 17 | Widget build(BuildContext context) { 18 | return MaterialApp( 19 | title: 'Flutter Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.purple, 22 | ), 23 | home: GameWidget.controlled( 24 | gameFactory: SpatialGridExample.new, 25 | overlayBuilderMap: { 26 | 'loading': (BuildContext ctx, SpatialGridExample game) { 27 | return Material( 28 | type: MaterialType.transparency, 29 | child: BackdropFilter( 30 | filter: ImageFilter.blur( 31 | sigmaX: 5.0, 32 | sigmaY: 5.0, 33 | ), 34 | child: StreamBuilder>( 35 | stream: game.loadingStream, 36 | builder: (context, snapshot) { 37 | final progress = snapshot.data?.progress ?? 0; 38 | return Center( 39 | child: Text( 40 | 'Loading: $progress% ', 41 | style: const TextStyle( 42 | color: Colors.white, 43 | fontWeight: FontWeight.bold, 44 | fontSize: 24, 45 | ), 46 | ), 47 | ); 48 | }, 49 | ), 50 | ), 51 | ); 52 | }, 53 | }, 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/minimal_game.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flame/collisions.dart'; 5 | import 'package:flame/components.dart'; 6 | import 'package:flame/extensions.dart'; 7 | import 'package:flame/game.dart'; 8 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 9 | import 'package:flutter/material.dart' hide Image, Draggable; 10 | 11 | class MinimalGame extends FlameGameEx with HasSpatialGridFramework { 12 | MinimalGame(); 13 | 14 | @override 15 | FutureOr onLoad() async { 16 | final player = Player(position: Vector2(160, 190), isPrimary: true); 17 | await initializeSpatialGrid( 18 | cellSize: 50, 19 | // debug: true, 20 | activeRadius: const Size(2, 2), 21 | unloadRadius: const Size(2, 2), 22 | trackWindowSize: false, 23 | trackedComponent: player, 24 | cellBuilderNoMap: onBuildNewCell, 25 | suspendedCellLifetime: const Duration(seconds: 30), 26 | ); 27 | 28 | add(player); 29 | for (var i = 0; i < 100; i++) { 30 | add(Player(position: Vector2(i * 10.0, 20))); 31 | } 32 | add(FpsTextComponent()); 33 | return super.onLoad(); 34 | } 35 | 36 | Future onBuildNewCell( 37 | Cell cell, 38 | Component rootComponent, 39 | Iterable mapRectOnCell, 40 | ) async { 41 | final random = Random(); 42 | final doCreation = random.nextBool(); 43 | if (doCreation) { 44 | add(Player(position: cell.center)..currentCell = cell); 45 | } 46 | } 47 | } 48 | 49 | class Player extends PositionComponent 50 | with HasGridSupport, HasPaint, CollisionCallbacks { 51 | Player({super.position, bool? isPrimary}) : super(size: Vector2(10, 10)) { 52 | _isPrimary = isPrimary ?? false; 53 | paint.color = _isPrimary ? Colors.indigoAccent : Colors.brown; 54 | _rect = Rect.fromLTWH(0, 0, size.x, size.y); 55 | 56 | if (!_isPrimary) { 57 | // debugMode = true; 58 | } 59 | 60 | boundingBox.collisionType = 61 | boundingBox.defaultCollisionType = CollisionType.active; 62 | 63 | boundingBox.parentSpeedGetter = () => _dtSpeed; 64 | } 65 | 66 | late final Rect _rect; 67 | late final bool _isPrimary; 68 | var _dtSpeed = 0.0; 69 | 70 | @override 71 | void render(Canvas canvas) { 72 | canvas.drawRect(_rect, paint); 73 | } 74 | 75 | final speed = 80.0; 76 | final vector = Vector2.zero(); 77 | double dtElapsed = 0; 78 | final dtMax = 400; 79 | 80 | @override 81 | void update(double dt) { 82 | dtElapsed++; 83 | if (dtElapsed >= dtMax || _outOfBounds()) { 84 | vector.setZero(); 85 | dtElapsed = 0; 86 | } 87 | if (vector.isZero()) { 88 | _createNewVector(); 89 | } 90 | 91 | final newSpeed = speed * dt; 92 | if (newSpeed - _dtSpeed > 0.1) { 93 | boundingBox.onParentSpeedChange(); 94 | } 95 | _dtSpeed = newSpeed; 96 | final newStep = vector * _dtSpeed; 97 | if (!vector.isZero()) { 98 | position.add(newStep); 99 | } 100 | super.update(dt); 101 | } 102 | 103 | void _createNewVector() { 104 | final rand = Random(); 105 | var xSign = rand.nextBool() ? -1 : 1; 106 | var ySign = rand.nextBool() ? -1 : 1; 107 | if (position.x >= 900) { 108 | xSign = -1; 109 | } else if (position.x <= 0) { 110 | xSign = 1; 111 | } 112 | 113 | if (position.y >= 500) { 114 | ySign = -1; 115 | } else if (position.y <= 0) { 116 | ySign = 1; 117 | } 118 | final xValue = rand.nextDouble(); 119 | final yValue = rand.nextDouble(); 120 | vector.setValues(xValue * xSign, yValue * ySign); 121 | } 122 | 123 | bool _outOfBounds() => 124 | position.x <= 0 || 125 | position.y <= 0 || 126 | position.x >= 900 || 127 | position.y >= 500; 128 | 129 | @override 130 | void onCollision(Set intersectionPoints, PositionComponent other) { 131 | if (other is Player) { 132 | vector.setZero(); 133 | } 134 | 135 | super.onCollision(intersectionPoints, other); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flame_spatial_grid_example 2 | description: Example application to show core flame_spatial_grid functions 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | # In Windows, build-name is used as the major, minor, and patch parts 19 | # of the product and file versions while build-number is used as the build suffix. 20 | version: 1.0.0+1 21 | 22 | environment: 23 | sdk: '>=3.0.0 <4.0.0' 24 | 25 | # Dependencies specify other packages that your package needs in order to work. 26 | # To automatically upgrade your package dependencies to the latest versions 27 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 28 | # dependencies can be manually updated by changing the version numbers below to 29 | # the latest version available on pub.dev. To see which dependencies have newer 30 | # versions available, run `flutter pub outdated`. 31 | dependencies: 32 | flame: ^1.24.0 33 | flame_spatial_grid: 34 | path: ../ 35 | 36 | 37 | flame_message_stream: 0.0.2 38 | flame_fast_touch: ^1.0.1 39 | 40 | flutter: 41 | sdk: flutter 42 | 43 | # The following adds the Cupertino Icons font to your application. 44 | # Use with the CupertinoIcons class for iOS style icons. 45 | # cupertino_icons: ^1.0.2 46 | 47 | dev_dependencies: 48 | flutter_test: 49 | sdk: flutter 50 | 51 | # The "flutter_lints" package below contains a set of recommended lints to 52 | # encourage good coding practices. The lint set provided by the package is 53 | # activated in the `analysis_options.yaml` file located at the root of your 54 | # package. See that file for information about deactivating specific lint 55 | # rules and activating additional ones. 56 | flutter_lints: ^2.0.0 57 | 58 | # For information on the generic Dart part of this file, see the 59 | # following page: https://dart.dev/tools/pub/pubspec 60 | 61 | # The following section is specific to Flutter packages. 62 | flutter: 63 | 64 | # The following line ensures that the Material Icons font is 65 | # included with your application, so that you can use the icons in 66 | # the material Icons class. 67 | uses-material-design: true 68 | 69 | assets: 70 | - assets/images/ 71 | - assets/tiles/ 72 | 73 | # To add assets to your application, add an assets section, like this: 74 | # assets: 75 | # - images/a_dot_burr.jpeg 76 | # - images/a_dot_ham.jpeg 77 | 78 | # An image asset can refer to one or more resolution-specific "variants", see 79 | # https://flutter.dev/assets-and-images/#resolution-aware 80 | 81 | # For details regarding adding assets from package dependencies, see 82 | # https://flutter.dev/assets-and-images/#from-packages 83 | 84 | # To add custom fonts to your application, add a fonts section here, 85 | # in this "flutter" section. Each entry in this list should have a 86 | # "family" key with the font family name, and a "fonts" key with a 87 | # list giving the asset and other descriptors for the font. For 88 | # example: 89 | # fonts: 90 | # - family: Schyler 91 | # fonts: 92 | # - asset: fonts/Schyler-Regular.ttf 93 | # - asset: fonts/Schyler-Italic.ttf 94 | # style: italic 95 | # - family: Trajan Pro 96 | # fonts: 97 | # - asset: fonts/TrajanPro.ttf 98 | # - asset: fonts/TrajanPro_Bold.ttf 99 | # weight: 700 100 | # 101 | # For details regarding fonts from package dependencies, 102 | # see https://flutter.dev/custom-fonts/#from-packages 103 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ASGAlex/flame_spatial_grid/ffad0930d76f23edd1db9fa309d0125aab2aafa8/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flame_spatial_grid_example 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flame_spatial_grid_example", 3 | "short_name": "flame_spatial_grid_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Clusterizer Test", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/flame_spatial_grid.dart: -------------------------------------------------------------------------------- 1 | library flame_spatial_grid; 2 | 3 | export 'src/collisions/broadphase.dart' hide ScheduledHitboxOperation; 4 | export 'src/collisions/collision_detection.dart'; 5 | export 'src/collisions/hitboxes/bounding_hitbox.dart'; 6 | export 'src/collisions/hitboxes/group_hitbox.dart'; 7 | export 'src/components/animation_global/animation_global_component.dart'; 8 | export 'src/components/animation_global/ticker_global.dart'; 9 | export 'src/components/animation_global/tickers_manager.dart'; 10 | export 'src/components/camera_wrapper.dart'; 11 | export 'src/components/has_grid_support.dart'; 12 | export 'src/components/layers/cell_layer.dart'; 13 | export 'src/components/layers/cell_static_animation_layer.dart' 14 | hide StaticAnimationLayerCacheEntry; 15 | export 'src/components/layers/cell_static_layer.dart' hide ImageCacheEntry; 16 | export 'src/components/layers/cell_trail_layer.dart'; 17 | export 'src/components/layers/has_trail_support.dart'; 18 | export 'src/components/layers/layers_manager.dart'; 19 | export 'src/components/restorable_state_mixin.dart'; 20 | export 'src/components/tile_component.dart'; 21 | export 'src/components/utility/debug_component.dart'; 22 | export 'src/components/utility/on_demand_actions.dart'; 23 | export 'src/components/utility/scheduler/action_provider.dart'; 24 | export 'src/components/utility/scheduler/scheduler.dart'; 25 | export 'src/components/utility/scheduler/with_action_provider_mixin.dart'; 26 | export 'src/core/cell.dart'; 27 | export 'src/core/flame_override/base_game.dart'; 28 | export 'src/core/flame_override/flame_game_ex.dart'; 29 | export 'src/core/has_spatial_grid_framework.dart'; 30 | export 'src/core/spatial_grid.dart'; 31 | export 'src/tiled/map_loader.dart'; 32 | export 'src/tiled/sprite_loader.dart'; 33 | export 'src/tiled/tile_builder_context.dart'; 34 | export 'src/tiled/tile_builder_context_provider.dart'; 35 | export 'src/tiled/tile_cache.dart'; 36 | export 'src/tiled/tile_data_provider.dart'; 37 | export 'src/tiled/tileset_manager.dart'; 38 | export 'src/tiled/world_data.dart'; 39 | export 'src/tiled/world_loader.dart'; 40 | export 'src/ui/loading_progress_manager.dart'; 41 | -------------------------------------------------------------------------------- /lib/src/collisions/broadphase/bloom_filter_provider.dart: -------------------------------------------------------------------------------- 1 | part of '../broadphase.dart'; 2 | 3 | class BloomFilterProvider { 4 | BloomFilter? _checkByTypeCacheBloomTrue; 5 | 6 | //ignore: use_late_for_private_fields_and_variables 7 | BloomFilter? _checkByTypeCacheBloomFalse; 8 | 9 | void init(Map result) { 10 | _checkByTypeCacheBloomTrue = BloomFilter(result.length, 0.01); 11 | _checkByTypeCacheBloomFalse = BloomFilter(result.length, 0.01); 12 | for (final item in result.entries) { 13 | if (item.value) { 14 | _checkByTypeCacheBloomTrue!.add(item: item.key); 15 | } else { 16 | _checkByTypeCacheBloomFalse!.add(item: item.key); 17 | } 18 | } 19 | } 20 | 21 | bool? check(int key) { 22 | if (_checkByTypeCacheBloomTrue == null) { 23 | return null; 24 | } 25 | 26 | return _checkByTypeCacheBloomTrue!.contains(item: key); 27 | // if (collide) { 28 | // final noCollide = _checkByTypeCacheBloomFalse!.contains(item: key); 29 | // if (!noCollide) { 30 | // return true; 31 | // } else { 32 | // return false; 33 | // } 34 | // } else { 35 | // return false; 36 | // } 37 | } 38 | 39 | int generateKey(Type type1, Type type2) => type1.hashCode & type2.hashCode; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/collisions/broadphase/collisions_cache.dart: -------------------------------------------------------------------------------- 1 | part of '../broadphase.dart'; 2 | 3 | class CollisionsCache { 4 | @protected 5 | final activeCollisions = {}; 6 | 7 | @internal 8 | final allCollisionsByCell = >{}; 9 | 10 | @internal 11 | final passiveCollisionsByCell = >>{}; 12 | 13 | @internal 14 | final activeCollisionsByCell = >>{}; 15 | 16 | @internal 17 | final passiveByCellUnmodifiable = ?>>{}; 18 | 19 | @internal 20 | final activeByCellUnmodifiable = ?>>{}; 21 | 22 | void activeUnmodifiableCacheClear() { 23 | _activeCollisionsChanged.forEach(activeByCellUnmodifiable.remove); 24 | } 25 | 26 | void passiveUnmodifiableCacheClear() { 27 | _passiveCollisionsChanged.forEach(passiveByCellUnmodifiable.remove); 28 | } 29 | 30 | final _activeCollisionsChanged = {}; 31 | final _passiveCollisionsChanged = {}; 32 | 33 | bool get activeCollisionsChanged => _activeCollisionsChanged.isNotEmpty; 34 | 35 | bool get passiveCollisionsChanged => _passiveCollisionsChanged.isNotEmpty; 36 | 37 | void preUpdate() { 38 | _activeCollisionsChanged.clear(); 39 | _passiveCollisionsChanged.clear(); 40 | } 41 | 42 | void processOperation(ScheduledHitboxOperation operation) { 43 | if (operation.add) { 44 | final cell = operation.cell; 45 | if (operation.all) { 46 | var list = allCollisionsByCell[cell]; 47 | list ??= allCollisionsByCell[cell] = HashSet(); 48 | list.add(operation.hitbox); 49 | } else { 50 | if (operation.active) { 51 | activeCollisions.add(operation.hitbox); 52 | _addOperation(operation, activeCollisionsByCell); 53 | _activeCollisionsChanged.add(cell); 54 | } else { 55 | _addOperation(operation, passiveCollisionsByCell); 56 | _passiveCollisionsChanged.add(cell); 57 | } 58 | } 59 | } else { 60 | final cell = operation.cell; 61 | if (operation.all) { 62 | final cellCollisions = allCollisionsByCell[cell]; 63 | if (cellCollisions != null) { 64 | cellCollisions.remove(operation.hitbox); 65 | if (cellCollisions.isEmpty) { 66 | allCollisionsByCell.remove(cell); 67 | } 68 | } 69 | } else { 70 | if (operation.active) { 71 | activeCollisions.remove(operation.hitbox); 72 | operation.hitbox.broadphaseActiveIndex = -1; 73 | _removeOperation(operation, activeCollisionsByCell); 74 | _activeCollisionsChanged.add(cell); 75 | } else { 76 | _removeOperation(operation, passiveCollisionsByCell); 77 | _passiveCollisionsChanged.add(cell); 78 | } 79 | } 80 | } 81 | } 82 | 83 | void _addOperation( 84 | ScheduledHitboxOperation operation, 85 | Map>> storage, 86 | ) { 87 | var typeStorage = storage[operation.cell]; 88 | typeStorage ??= storage[operation.cell] = >{}; 89 | var type = operation.hitbox.runtimeType; 90 | final component = operation.hitbox.hitboxParent; 91 | if (component is CellLayer) { 92 | type = component.primaryHitboxCollisionType ?? type; 93 | } 94 | var list = storage[operation.cell]![type]; 95 | list ??= typeStorage[type] = HashSet(); 96 | 97 | list.add(operation.hitbox); 98 | } 99 | 100 | void _removeOperation( 101 | ScheduledHitboxOperation operation, 102 | Map>> storage, 103 | ) { 104 | final cellCollisions = storage[operation.cell]; 105 | if (cellCollisions != null) { 106 | final list = cellCollisions[operation.hitbox.runtimeType]; 107 | if (list != null) { 108 | list.remove(operation.hitbox); 109 | if (list.isEmpty) { 110 | cellCollisions.remove(operation.hitbox.runtimeType); 111 | if (cellCollisions.isEmpty) { 112 | storage.remove(operation.cell); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | List unmodifiableCacheStore( 120 | Cell cell, 121 | Type type, 122 | Iterable data, { 123 | required bool isActive, 124 | }) { 125 | final storage = 126 | isActive ? activeByCellUnmodifiable : passiveByCellUnmodifiable; 127 | var list = storage[cell]?[type]; 128 | if (list == null) { 129 | list = data.toList(growable: false); 130 | if (storage[cell] == null) { 131 | storage[cell] = ?>{}; 132 | } 133 | if (storage[cell]![type] == null) { 134 | storage[cell]![type] = list; 135 | } 136 | } 137 | return list; 138 | } 139 | 140 | void clear() { 141 | activeCollisions.clear(); 142 | allCollisionsByCell.clear(); 143 | passiveCollisionsByCell.clear(); 144 | activeCollisionsByCell.clear(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/collisions/broadphase/comparator.dart: -------------------------------------------------------------------------------- 1 | part of '../broadphase.dart'; 2 | 3 | class Comparator { 4 | late final BloomFilterProvider _bloomFilter; 5 | PureTypeCheck? _globalPureTypeCheck; 6 | 7 | final _checkByTypeCache = {}; 8 | 9 | bool componentFullTypeCheck( 10 | PureTypeCheckInterface active, 11 | PureTypeCheckInterface potential, { 12 | bool potentialCanBeActive = false, 13 | }) { 14 | final aType = active.runtimeType; 15 | final pType = potential.runtimeType; 16 | final canToCollide = globalTypeCheck( 17 | aType, 18 | pType, 19 | potentialCanBeActive: potentialCanBeActive, 20 | ); 21 | 22 | if (canToCollide) { 23 | return active.pureTypeCheck(pType) && potential.pureTypeCheck(aType); 24 | } 25 | return false; 26 | } 27 | 28 | bool globalTypeCheck( 29 | Type active, 30 | Type potential, { 31 | bool potentialCanBeActive = false, 32 | }) { 33 | final key = _bloomFilter.generateKey( 34 | active, 35 | potential, 36 | ); 37 | var canToCollide = true; 38 | final bloomCheck = _bloomFilter.check(key); 39 | if (bloomCheck == null) { 40 | final cache = _checkByTypeCache[key]; 41 | if (cache == null) { 42 | canToCollide = _globalTypeCheckNoBloom( 43 | active, 44 | potential, 45 | potentialCanBeActive: potentialCanBeActive, 46 | ); 47 | _checkByTypeCache[key] = canToCollide; 48 | } else { 49 | canToCollide = cache; 50 | } 51 | } else { 52 | canToCollide = bloomCheck; 53 | } 54 | 55 | return canToCollide; 56 | } 57 | 58 | bool _globalTypeCheckNoBloom( 59 | Type active, 60 | Type potential, { 61 | bool potentialCanBeActive = false, 62 | }) { 63 | if (_globalPureTypeCheck == null) { 64 | return true; 65 | } 66 | 67 | final canCollide = _globalPureTypeCheck!.call( 68 | active, 69 | potential, 70 | ); 71 | if (potentialCanBeActive) { 72 | return canCollide && 73 | _globalPureTypeCheck!.call( 74 | potential, 75 | active, 76 | ); 77 | } 78 | return canCollide; 79 | } 80 | 81 | void clear() => _checkByTypeCache.clear(); 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/collisions/broadphase/schedule_hitbox_operation.dart: -------------------------------------------------------------------------------- 1 | part of '../broadphase.dart'; 2 | 3 | @internal 4 | @immutable 5 | class ScheduledHitboxOperation { 6 | const ScheduledHitboxOperation({ 7 | required this.add, 8 | required this.active, 9 | required this.hitbox, 10 | required this.cell, 11 | required this.all, 12 | }); 13 | 14 | const ScheduledHitboxOperation.addActive({ 15 | required this.hitbox, 16 | required this.cell, 17 | }) : add = true, 18 | active = true, 19 | all = false; 20 | 21 | const ScheduledHitboxOperation.addPassive({ 22 | required this.hitbox, 23 | required this.cell, 24 | }) : add = true, 25 | active = false, 26 | all = false; 27 | 28 | const ScheduledHitboxOperation.removeActive({ 29 | required this.hitbox, 30 | required this.cell, 31 | }) : add = false, 32 | active = true, 33 | all = false; 34 | 35 | const ScheduledHitboxOperation.removePassive({ 36 | required this.hitbox, 37 | required this.cell, 38 | }) : add = false, 39 | active = false, 40 | all = false; 41 | 42 | const ScheduledHitboxOperation.addToAll({ 43 | required this.hitbox, 44 | required this.cell, 45 | }) : add = true, 46 | active = false, 47 | all = true; 48 | 49 | const ScheduledHitboxOperation.removeFromAll({ 50 | required this.hitbox, 51 | required this.cell, 52 | }) : add = false, 53 | active = false, 54 | all = true; 55 | 56 | final bool add; 57 | final bool active; 58 | final bool all; 59 | final ShapeHitbox hitbox; 60 | final Cell cell; 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/collisions/broadphase/typedef.dart: -------------------------------------------------------------------------------- 1 | part of '../broadphase.dart'; 2 | 3 | typedef ExternalMinDistanceCheckSpatialGrid = bool Function( 4 | ShapeHitbox activeItem, 5 | ShapeHitbox potential, 6 | ); 7 | 8 | typedef PureTypeCheck = bool Function( 9 | Type activeItemType, 10 | Type potentialItemType, 11 | ); 12 | 13 | typedef ComponentExternalTypeCheck = bool Function( 14 | PositionComponent first, 15 | PositionComponent second, 16 | ); 17 | 18 | enum RayTraceMode { 19 | allHitboxes, 20 | groupedHitboxes, 21 | } 22 | 23 | class DummyHitbox extends BoundingHitbox {} 24 | -------------------------------------------------------------------------------- /lib/src/collisions/collision_prospect/collision_prospect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/collisions.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | 4 | /// A [CollisionProspect] is a tuple that is used to contain two potentially 5 | /// colliding hitboxes. 6 | class CollisionProspectGrouped extends CollisionProspect { 7 | @override 8 | int get hash => _hashGrouped; 9 | int _hashGrouped; 10 | 11 | CollisionProspectGrouped(super.a, super.b) 12 | : _hashGrouped = a.hashCode ^ 13 | ((b is GroupHitbox) ? b.hashCodeForCollisions : b.hashCode); 14 | 15 | @override 16 | void set(ShapeHitbox a, ShapeHitbox b) { 17 | super.set(a, b); 18 | _hashGrouped = a.hashCode ^ 19 | ((b is GroupHitbox) ? b.hashCodeForCollisions : b.hashCode); 20 | } 21 | 22 | /// Sets the prospect to contain the content of [other]. 23 | @override 24 | void setFrom(CollisionProspect other) { 25 | super.setFrom(other); 26 | if (other is CollisionProspectGrouped) { 27 | _hashGrouped = other._hashGrouped; 28 | } 29 | } 30 | 31 | /// Creates a new prospect object with the same content. 32 | @override 33 | CollisionProspect clone() => CollisionProspectGrouped(a, b); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/collisions/collision_prospect/prospect_pool.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/collisions.dart'; 2 | import 'package:flame_spatial_grid/src/collisions/collision_prospect/collision_prospect.dart'; 3 | 4 | /// This pool is used to not create unnecessary [CollisionProspect] objects 5 | /// during collision detection, but to re-use the ones that have already been 6 | /// created. 7 | class ProspectPoolGrouped extends ProspectPool { 8 | ProspectPoolGrouped({super.incrementSize = 1000}); 9 | 10 | final _storage = []; 11 | 12 | @override 13 | int get length => _storage.length; 14 | 15 | /// The size of the pool will expand with [incrementSize] amount of 16 | /// [CollisionProspect]s that are initially populated with two [dummyItem]s. 17 | @override 18 | void expand(ShapeHitbox dummyItem) { 19 | for (var i = 0; i < incrementSize; i++) { 20 | _storage.add(CollisionProspectGrouped(dummyItem, dummyItem)); 21 | } 22 | } 23 | 24 | @override 25 | CollisionProspect operator [](int index) => _storage[index]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/collisions/hitboxes/group_hitbox.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: must_call_super 2 | import 'dart:ui'; 3 | 4 | import 'package:flame/collisions.dart'; 5 | import 'package:flame/components.dart'; 6 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | class GroupHitbox extends BoundingHitbox { 10 | GroupHitbox({ 11 | super.position, 12 | super.size, 13 | super.parentWithGridSupport, 14 | required this.tag, 15 | }) { 16 | isSolid = true; 17 | collisionType = CollisionType.passive; 18 | defaultCollisionType = collisionType; 19 | fastCollisionForRects = true; 20 | } 21 | 22 | final String tag; 23 | 24 | @override 25 | bool get optimized => false; 26 | 27 | int get hashCodeForCollisions => parentWithGridSupport.hashCode; 28 | 29 | @override 30 | void renderDebugMode(Canvas canvas) { 31 | canvas.drawRect( 32 | Rect.fromLTWH(position.x, position.y, size.x, size.y), 33 | Paint() 34 | ..color = const Color.fromRGBO(0, 0, 255, 1) 35 | ..style = PaintingStyle.stroke, 36 | ); 37 | } 38 | 39 | @override 40 | @mustCallSuper 41 | void onCollisionStart(Set intersectionPoints, ShapeHitbox other) { 42 | activeCollisions.add(other); 43 | onCollisionStartCallback?.call(intersectionPoints, other); 44 | } 45 | 46 | @override 47 | @mustCallSuper 48 | void onCollisionEnd(ShapeHitbox other) { 49 | activeCollisions.remove(other); 50 | onCollisionEndCallback?.call(other); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/collisions/hitboxes/rectangle_hitbox_extensions.dart: -------------------------------------------------------------------------------- 1 | part of 'bounding_hitbox.dart'; 2 | 3 | extension SpatialGridRectangleHitbox on RectangleHitbox { 4 | Vector2 get aabbCenter { 5 | final hitbox = this; 6 | if (hitbox is BoundingHitbox) { 7 | return hitbox.aabbCenter; 8 | } 9 | 10 | var cache = HasGridSupport.cachedCenters[this]; 11 | if (cache == null) { 12 | HasGridSupport.cachedCenters[this] = aabb.center; 13 | cache = HasGridSupport.cachedCenters[this]; 14 | } 15 | return cache!; 16 | } 17 | 18 | bool isFullyInsideRect(Rect rect) { 19 | final boundingRect = aabb.toRect(); 20 | return rect.topLeft < boundingRect.topLeft && 21 | rect.bottomRight > boundingRect.bottomRight; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/collisions/hitboxes/rectangle_hitbox_optimized.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: implementation_imports 2 | import 'package:flame/collisions.dart'; 3 | import 'package:flame/components.dart'; 4 | import 'package:flame/src/geometry/polygon_ray_intersection.dart'; 5 | import 'package:flame_spatial_grid/src/collisions/hitboxes/polygon_rect_component.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | 8 | class RectangleHitboxOptimized extends PolygonRectComponent 9 | with ShapeHitbox, PolygonRayIntersection 10 | implements RectangleHitbox { 11 | final bool _shouldFeelParentInitial; 12 | 13 | RectangleHitboxOptimized({ 14 | super.position, 15 | super.size, 16 | super.angle, 17 | super.anchor, 18 | super.priority, 19 | bool isSolid = false, 20 | CollisionType collisionType = CollisionType.active, 21 | }) : _shouldFeelParentInitial = size == null && position == null, 22 | super(sizeToVertices(size ?? Vector2.zero(), anchor)) { 23 | this.isSolid = isSolid; 24 | this.collisionType = collisionType; 25 | shouldFillParent = _shouldFeelParentInitial; 26 | size.addListener(() { 27 | shrinkToBounds = false; 28 | shouldFillParent = false; 29 | refreshVertices( 30 | newVertices: sizeToVertices(size, anchor), 31 | ); 32 | }); 33 | 34 | position.addListener(() { 35 | manuallyPositioned = true; 36 | shouldFillParent = false; 37 | }); 38 | } 39 | 40 | @override 41 | void fillParent() { 42 | refreshVertices( 43 | newVertices: sizeToVertices(size, anchor), 44 | ); 45 | } 46 | 47 | @protected 48 | static List sizeToVertices( 49 | Vector2 size, 50 | Anchor? componentAnchor, 51 | ) { 52 | final anchor = componentAnchor ?? Anchor.topLeft; 53 | return [ 54 | Vector2(-size.x * anchor.x, -size.y * anchor.y), 55 | Vector2(size.x - size.x * anchor.x, -size.y * anchor.y), 56 | Vector2(size.x - size.x * anchor.x, size.y - size.y * anchor.y), 57 | Vector2(-size.x * anchor.x, size.y - size.y * anchor.y), 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/collisions/hitboxes/shape_hitbox_extensions.dart: -------------------------------------------------------------------------------- 1 | part of 'bounding_hitbox.dart'; 2 | 3 | extension SpatialGridShapeHitbox on ShapeHitbox { 4 | Vector2 get aabbCenter { 5 | final hitbox = this; 6 | if (hitbox is BoundingHitbox) { 7 | return hitbox.aabbCenter; 8 | } 9 | var cache = HasGridSupport.cachedCenters[this]; 10 | if (cache == null) { 11 | HasGridSupport.cachedCenters[this] = aabb.center; 12 | cache = HasGridSupport.cachedCenters[this]; 13 | } 14 | return cache!; 15 | } 16 | 17 | Float64x2 get aabbCenterStorage { 18 | final hitbox = this; 19 | if (hitbox is BoundingHitbox) { 20 | return hitbox.aabbCenterStorage; 21 | } 22 | var cache = HasGridSupport.cachedCenters[this]; 23 | if (cache == null) { 24 | HasGridSupport.cachedCenters[this] = aabb.center; 25 | cache = HasGridSupport.cachedCenters[this]; 26 | } 27 | return Float64x2(cache!.x, cache.y); 28 | } 29 | 30 | bool get doExtendedTypeCheck => true; 31 | 32 | @internal 33 | set broadphaseActiveIndex(int value) { 34 | if (this is BoundingHitbox) { 35 | (this as BoundingHitbox).broadphaseActiveIndex = value; 36 | } else { 37 | if (value == -1) { 38 | HasGridSupport.shapeHitboxIndex.remove(this); 39 | } else { 40 | HasGridSupport.shapeHitboxIndex[this] = value; 41 | } 42 | } 43 | } 44 | 45 | @internal 46 | int get broadphaseActiveIndex { 47 | if (this is BoundingHitbox) { 48 | return (this as BoundingHitbox).broadphaseActiveIndex; 49 | } else { 50 | return HasGridSupport.shapeHitboxIndex[this] ?? -1; 51 | } 52 | } 53 | 54 | void storeBroadphaseCheckCache(ShapeHitbox item, bool canCollide) { 55 | var cache = SpatialGridBroadphase.broadphaseCheckCache[this]; 56 | cache ??= SpatialGridBroadphase.broadphaseCheckCache[this] = 57 | HashMap(); 58 | cache[item] = canCollide; 59 | } 60 | 61 | bool? getBroadphaseCheckCache(ShapeHitbox item) => 62 | SpatialGridBroadphase.broadphaseCheckCache[this]?[item]; 63 | 64 | HasGridSupport? get parentWithGridSupport { 65 | final hitbox = this; 66 | if (hitbox is BoundingHitbox) { 67 | return hitbox.parentWithGridSupport; 68 | } 69 | 70 | var component = HasGridSupport.componentHitboxes[this]; 71 | if (component == null) { 72 | try { 73 | component = ancestors().firstWhere( 74 | (c) => c is HasGridSupport, 75 | ) as HasGridSupport; 76 | HasGridSupport.componentHitboxes[this] = component; 77 | return component; 78 | // ignore: avoid_catches_without_on_clauses 79 | } catch (e) { 80 | return null; 81 | } 82 | } 83 | return component; 84 | } 85 | 86 | @internal 87 | void clearGridComponentCaches() { 88 | HasGridSupport.componentHitboxes.remove(this); 89 | HasGridSupport.defaultCollisionType.remove(this); 90 | HasGridSupport.cachedCenters.remove(this); 91 | } 92 | 93 | set defaultCollisionType(CollisionType defaultCollisionType) { 94 | final hitbox = this; 95 | if (hitbox is BoundingHitbox) { 96 | hitbox.defaultCollisionType = defaultCollisionType; 97 | } else { 98 | HasGridSupport.defaultCollisionType[this] = defaultCollisionType; 99 | } 100 | } 101 | 102 | CollisionType get defaultCollisionType { 103 | final hitbox = this; 104 | if (hitbox is BoundingHitbox) { 105 | return hitbox.defaultCollisionType; 106 | } 107 | 108 | var cache = HasGridSupport.defaultCollisionType[this]; 109 | if (cache == null) { 110 | HasGridSupport.defaultCollisionType[this] = collisionType; 111 | cache = HasGridSupport.defaultCollisionType[this]; 112 | } 113 | return cache!; 114 | } 115 | 116 | bool get canBeActive { 117 | if (this is BoundingHitbox) { 118 | return (this as BoundingHitbox).canBeActive; 119 | } 120 | return collisionType == CollisionType.active; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/collision_optimizer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flame/collisions.dart'; 4 | import 'package:flame/components.dart'; 5 | import 'package:flame/extensions.dart'; 6 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 7 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/entry_point.dart'; 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/extensions.dart'; 9 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart' 10 | as fb; 11 | import 'package:flame_spatial_grid/src/collisions/optimizer/optimized_collisions_list.dart'; 12 | import 'package:isolate_manager/isolate_manager.dart'; 13 | import 'package:meta/meta.dart'; 14 | 15 | class CollisionOptimizer { 16 | CollisionOptimizer(this.parentLayer) { 17 | _isolateManager ??= IsolateManager.create( 18 | findOverlappingRectsIsolated, 19 | concurrent: 4, 20 | workerName: 'spatial_grid_optimizer_worker', 21 | ); 22 | } 23 | 24 | static IsolateManager? _isolateManager; 25 | 26 | final CellLayer parentLayer; 27 | final _createdCollisionLists = []; 28 | 29 | bool get isEmpty => _createdCollisionLists.isEmpty; 30 | 31 | int? _maximumItemsInGroup; 32 | 33 | set maximumItemsInGroup(int? value) { 34 | _maximumItemsInGroup = value; 35 | } 36 | 37 | int get maximumItemsInGroup => 38 | _maximumItemsInGroup ?? game.collisionOptimizerGroupLimit; 39 | 40 | HasSpatialGridFramework get game => parentLayer.game; 41 | 42 | @internal 43 | static final rectCache = {}; 44 | 45 | Future optimize() async { 46 | final cell = clear(); 47 | if (cell == null) { 48 | return; 49 | } 50 | 51 | final optimizedCollisionsByGroupBox = 52 | game.collisionDetection.broadphase.optimizedCollisionsByGroupBox; 53 | final collisionsListByGroup = optimizedCollisionsByGroupBox[cell]!; 54 | 55 | final componentsForOptimization = 56 | parentLayer.children.query().toList(); 57 | final toCheck = List.filled( 58 | componentsForOptimization.length, 59 | defaultBoundingHitboxObjectBuilder, 60 | ); 61 | 62 | for (var i = 0; i < toCheck.length; i++) { 63 | final child = componentsForOptimization[i]; 64 | if (cell.state != CellState.inactive) { 65 | child.boundingBox.collisionType = 66 | child.boundingBox.defaultCollisionType; 67 | child.boundingBox.group = null; 68 | } 69 | toCheck[i] = child.boundingBox.toBuilder(i); 70 | } 71 | 72 | final params = fb.OverlappingSearchRequestObjectBuilder( 73 | hitboxes: toCheck, 74 | maximumItemsInGroup: maximumItemsInGroup, 75 | ); 76 | final buffer = params.toBytes(); 77 | 78 | final responseData = await _isolateManager!.compute( 79 | buffer, 80 | ); 81 | final response = fb.OverlappedSearchResponse(responseData); 82 | for (final collisionsList in response.optimizedCollisions!) { 83 | final hydratedHitboxes = List.filled( 84 | collisionsList.indicies!.length, 85 | _emptyBoundingHitbox, 86 | ); 87 | for (var i = 0; i < hydratedHitboxes.length; i++) { 88 | try { 89 | final index = collisionsList.indicies![i]; 90 | final component = componentsForOptimization[index]; 91 | component.boundingBox.collisionType = CollisionType.inactive; 92 | hydratedHitboxes[i] = component.boundingBox; 93 | } on RangeError catch (_) {} 94 | } 95 | final rect = Rect.fromLTRB( 96 | collisionsList.optimizedBoundingRect!.left, 97 | collisionsList.optimizedBoundingRect!.top, 98 | collisionsList.optimizedBoundingRect!.right, 99 | collisionsList.optimizedBoundingRect!.bottom, 100 | ); 101 | final optimized = OptimizedCollisionList( 102 | hydratedHitboxes, 103 | parentLayer, 104 | rect, 105 | ); 106 | _createdCollisionLists.add(optimized); 107 | collisionsListByGroup[optimized.boundingBox] = optimized; 108 | } 109 | } 110 | 111 | Future buildMacroObjects() { 112 | throw UnimplementedError(); 113 | } 114 | 115 | Cell? clear() { 116 | final cell = parentLayer.currentCell; 117 | if (cell == null) { 118 | return null; 119 | } 120 | 121 | final optimizedCollisionsByGroupBox = 122 | game.collisionDetection.broadphase.optimizedCollisionsByGroupBox; 123 | var collisionsListByGroup = optimizedCollisionsByGroupBox[cell]; 124 | 125 | if (collisionsListByGroup == null) { 126 | optimizedCollisionsByGroupBox[cell] = collisionsListByGroup = {}; 127 | } 128 | for (final optimized in _createdCollisionLists) { 129 | collisionsListByGroup.remove(optimized.boundingBox); 130 | optimized.clear(); 131 | } 132 | _createdCollisionLists.clear(); 133 | return cell; 134 | } 135 | } 136 | 137 | final _emptyBoundingHitbox = BoundingHitbox(); 138 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/extensions.dart'; 3 | import 'package:flame_spatial_grid/src/collisions/optimizer/collision_optimizer.dart'; 4 | 5 | extension ToRectSpecial on PositionComponent { 6 | Rect toRectSpecial() { 7 | final cache = CollisionOptimizer.rectCache[this]; 8 | if (cache != null) { 9 | return cache; 10 | } else { 11 | final parentPosition = (parent as PositionComponent?)?.position; 12 | if (parentPosition == null) { 13 | return Rect.zero; 14 | } 15 | final cache = Rect.fromLTWH( 16 | parentPosition.x + position.x, 17 | parentPosition.y + position.y, 18 | size.x, 19 | size.y, 20 | ); 21 | CollisionOptimizer.rectCache[this] = cache; 22 | return cache; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/entry_point.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart' 4 | as fb; 5 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/geometry_universal.dart'; 6 | import 'package:isolate_manager/isolate_manager.dart'; 7 | import 'package:vector_math/vector_math_64.dart'; 8 | 9 | @IsolateManagerWorker('spatial_grid_optimizer_worker') 10 | Uint8List findOverlappingRectsIsolated( 11 | Uint8List parameters, 12 | ) { 13 | final request = fb.OverlappingSearchRequest(parameters); 14 | final hitboxes = request.hitboxes!; 15 | final skip = {}; 16 | final optimizedCollisions = []; 17 | // final optimizedCollisions = []; 18 | for (var i = 0; i < hitboxes.length; i++) { 19 | if (skip.contains(i)) { 20 | continue; 21 | } 22 | final target = hitboxes[i]; 23 | if (target.skip) { 24 | continue; 25 | } 26 | final hitboxesUnsorted = _findOverlappingRects(target, hitboxes); 27 | for (final element in hitboxesUnsorted) { 28 | skip.add(element.index); 29 | } 30 | 31 | if (hitboxesUnsorted.length > 1) { 32 | if (hitboxesUnsorted.length > request.maximumItemsInGroup && 33 | request.maximumItemsInGroup > 0) { 34 | final hitboxesSorted = hitboxesUnsorted.toList(growable: false); 35 | hitboxesSorted.sort((a, b) { 36 | if (a.aabbCenter == b.aabbCenter) { 37 | return 0; 38 | } 39 | if (a.aabbCenter!.y < b.aabbCenter!.y) { 40 | return -1; 41 | } else if (a.aabbCenter!.y == b.aabbCenter!.y) { 42 | return a.aabbCenter!.x < b.aabbCenter!.x ? -1 : 1; 43 | } else { 44 | return 1; 45 | } 46 | }); 47 | var totalInChunk = 0; 48 | final chunk = List.filled(request.maximumItemsInGroup, -1); 49 | 50 | for (var sortedIndex = 0; 51 | sortedIndex < hitboxesSorted.length; 52 | sortedIndex++) { 53 | if (totalInChunk == request.maximumItemsInGroup) { 54 | var boundingRect = Rect.zero; 55 | final indices = List.filled(chunk.length, 0); 56 | for (var i = 0; i < chunk.length; i++) { 57 | final hitbox = hitboxesSorted[chunk[i]]; 58 | indices[i] = hitbox.index; 59 | if (boundingRect == Rect.zero) { 60 | boundingRect = hitbox.toRectSpecial(); 61 | } else { 62 | boundingRect = 63 | boundingRect.expandToInclude(hitbox.toRectSpecial()); 64 | } 65 | } 66 | final optimized = fb.OptimizedCollisionsObjectBuilder( 67 | indicies: indices, 68 | optimizedBoundingRect: boundingRect.toFlatBufferRect(), 69 | ); 70 | optimizedCollisions.add(optimized); 71 | totalInChunk = 0; 72 | chunk.fillRange(0, request.maximumItemsInGroup, -1); 73 | } else { 74 | chunk[totalInChunk] = sortedIndex; 75 | totalInChunk++; 76 | } 77 | } 78 | if (totalInChunk != 0) { 79 | final indices = List.filled(chunk.length, 0); 80 | var boundingRect = Rect.zero; 81 | for (var i = 0; i < chunk.length; i++) { 82 | final index = chunk[i]; 83 | if (index == -1) { 84 | break; 85 | } 86 | final hitbox = hitboxesSorted[index]; 87 | indices[i] = hitbox.index; 88 | if (boundingRect == Rect.zero) { 89 | boundingRect = hitbox.toRectSpecial(); 90 | } else { 91 | boundingRect = 92 | boundingRect.expandToInclude(hitbox.toRectSpecial()); 93 | } 94 | } 95 | final optimized = fb.OptimizedCollisionsObjectBuilder( 96 | indicies: indices, 97 | optimizedBoundingRect: boundingRect.toFlatBufferRect(), 98 | ); 99 | optimizedCollisions.add(optimized); 100 | } 101 | } else { 102 | final indices = List.filled(hitboxesUnsorted.length, 0); 103 | var boundingRect = Rect.zero; 104 | for (var i = 0; i < hitboxesUnsorted.length; i++) { 105 | final hitbox = hitboxesUnsorted[i]; 106 | indices[i] = hitbox.index; 107 | if (boundingRect == Rect.zero) { 108 | boundingRect = hitbox.toRectSpecial(); 109 | } else { 110 | boundingRect = boundingRect.expandToInclude(hitbox.toRectSpecial()); 111 | } 112 | } 113 | final optimized = fb.OptimizedCollisionsObjectBuilder( 114 | indicies: indices, 115 | optimizedBoundingRect: boundingRect.toFlatBufferRect(), 116 | ); 117 | optimizedCollisions.add(optimized); 118 | } 119 | if (hitboxesUnsorted.length >= hitboxes.length) { 120 | break; 121 | } 122 | } 123 | } 124 | 125 | final responseBuilder = fb.OverlappedSearchResponseObjectBuilder( 126 | optimizedCollisions: optimizedCollisions, 127 | ); 128 | return responseBuilder.toBytes(); 129 | } 130 | 131 | List _findOverlappingRects( 132 | fb.BoundingHitbox target, 133 | List hitboxesForOptimization, [ 134 | Set? excludedIndices, 135 | ]) { 136 | final hitboxes = []; 137 | hitboxes.add(target); 138 | if (excludedIndices != null) { 139 | excludedIndices.add(target.index); 140 | } else { 141 | excludedIndices = {}; 142 | } 143 | for (final otherHitbox in hitboxesForOptimization) { 144 | if (otherHitbox.skip || 145 | otherHitbox.index == target.index || 146 | excludedIndices.contains(otherHitbox.index)) { 147 | continue; 148 | } 149 | if (target.toRectSpecial().overlapsSpecial(otherHitbox.toRectSpecial())) { 150 | hitboxes.addAll( 151 | _findOverlappingRects( 152 | otherHitbox, 153 | hitboxesForOptimization, 154 | excludedIndices, 155 | ), 156 | ); 157 | } 158 | } 159 | return hitboxes; 160 | } 161 | 162 | extension ToFlatBuffersRect on Rect { 163 | fb.RectObjectBuilder toFlatBufferRect() => fb.RectObjectBuilder( 164 | left: left, 165 | right: right, 166 | top: top, 167 | bottom: bottom, 168 | ); 169 | } 170 | 171 | extension ToRectSpecial on fb.BoundingHitbox { 172 | Rect toRectSpecial() { 173 | final cache = rectCache; 174 | if (cache != null) { 175 | _aabbCenterUpdate(); 176 | return cache; 177 | } else { 178 | if (this.parentPosition == null) { 179 | return Rect.zero; 180 | } 181 | final parentPosition = 182 | Vector2(this.parentPosition!.x, this.parentPosition!.y); 183 | final position = Vector2(this.position!.x, this.position!.y); 184 | final size = Vector2(this.size!.x, this.size!.y); 185 | final cache = Rect.fromLTWH( 186 | parentPosition.x + position.x, 187 | parentPosition.y + position.y, 188 | size.x, 189 | size.y, 190 | ); 191 | 192 | rectCache = cache; 193 | _aabbCenterUpdate(); 194 | return cache; 195 | } 196 | } 197 | 198 | void _aabbCenterUpdate() { 199 | if (aabbCenter == null) { 200 | final aabb = Aabb2(); 201 | aabb.min.setValues(this.aabb!.min.x, this.aabb!.min.y); 202 | aabb.max.setValues(this.aabb!.max.x, this.aabb!.max.y); 203 | aabbCenter = aabb.center; 204 | } 205 | } 206 | } 207 | 208 | extension RectSpecialOverlap on Rect { 209 | /// Whether `other` has a nonzero area of overlap with this rectangle. 210 | bool overlapsSpecial(Rect other) { 211 | if (topLeft == other.bottomRight || 212 | topRight == other.bottomLeft || 213 | bottomLeft == other.topRight || 214 | bottomRight == other.topLeft) { 215 | return false; 216 | } 217 | if (right < other.left || other.right < left) { 218 | return false; 219 | } 220 | if (bottom < other.top || other.bottom < top) { 221 | return false; 222 | } 223 | return true; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/collisions.dart'; 2 | import 'package:flame/components.dart'; 3 | import 'package:flame_spatial_grid/src/collisions/hitboxes/bounding_hitbox.dart'; 4 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart' 5 | as fb; 6 | 7 | final fb.BoundingHitboxObjectBuilder defaultBoundingHitboxObjectBuilder = 8 | fb.BoundingHitboxObjectBuilder(); 9 | 10 | extension FlatBufferHitbox on BoundingHitbox { 11 | fb.BoundingHitboxObjectBuilder toBuilder(int index) { 12 | final position = fb.Vector2ObjectBuilder( 13 | x: this.position.x, 14 | y: this.position.y, 15 | ); 16 | fb.Vector2ObjectBuilder? parentPosition; 17 | final parentPositionVector = (parent as PositionComponent?)?.position; 18 | if (parentPositionVector != null) { 19 | parentPosition = fb.Vector2ObjectBuilder( 20 | x: parentPositionVector.x, 21 | y: parentPositionVector.y, 22 | ); 23 | } 24 | final size = fb.Vector2ObjectBuilder(x: this.size.x, y: this.size.y); 25 | final aabb = fb.Aabb2ObjectBuilder( 26 | min: fb.Vector2ObjectBuilder( 27 | x: this.aabb.min.x, 28 | y: this.aabb.min.y, 29 | ), 30 | max: fb.Vector2ObjectBuilder( 31 | x: this.aabb.max.x, 32 | y: this.aabb.max.y, 33 | ), 34 | ); 35 | return fb.BoundingHitboxObjectBuilder( 36 | position: position, 37 | parentPosition: parentPosition, 38 | size: size, 39 | index: index, 40 | skip: collisionType == CollisionType.inactive, 41 | aabb: aabb, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart: -------------------------------------------------------------------------------- 1 | library optimizer; 2 | 3 | export 'generated/aabb2_optimizer_generated.dart'; 4 | // import 'generated/bounding_hitbox_flame_spatial_grid.optimizer_generated.dart'; 5 | 6 | export 'generated/bounding_hitbox_optimizer_generated.dart'; 7 | export 'generated/optimized_collisions_optimizer_generated.dart'; 8 | export 'generated/rect_optimizer_generated.dart'; 9 | export 'generated/request_optimizer_generated.dart'; 10 | export 'generated/response_optimizer_generated.dart'; 11 | export 'generated/vector2_optimizer_generated.dart'; 12 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/aabb2_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart'; 9 | import 'package:flat_buffers/flat_buffers.dart' as fb; 10 | 11 | class Aabb2 { 12 | Aabb2._(this._bc, this._bcOffset); 13 | 14 | static const fb.Reader reader = _Aabb2Reader(); 15 | 16 | final fb.BufferContext _bc; 17 | final int _bcOffset; 18 | 19 | Vector2 get min => Vector2.reader.read(_bc, _bcOffset + 0); 20 | 21 | Vector2 get max => Vector2.reader.read(_bc, _bcOffset + 8); 22 | 23 | @override 24 | String toString() { 25 | return 'Aabb2{min: $min, max: $max}'; 26 | } 27 | 28 | Aabb2T unpack() => Aabb2T(min: min.unpack(), max: max.unpack()); 29 | 30 | static int pack(fb.Builder fbBuilder, Aabb2T? object) { 31 | if (object == null) return 0; 32 | return object.pack(fbBuilder); 33 | } 34 | } 35 | 36 | class Aabb2T implements fb.Packable { 37 | Vector2T min; 38 | Vector2T max; 39 | 40 | Aabb2T({required this.min, required this.max}); 41 | 42 | @override 43 | int pack(fb.Builder fbBuilder) { 44 | max.pack(fbBuilder); 45 | min.pack(fbBuilder); 46 | return fbBuilder.offset; 47 | } 48 | 49 | @override 50 | String toString() { 51 | return 'Aabb2T{min: $min, max: $max}'; 52 | } 53 | } 54 | 55 | class _Aabb2Reader extends fb.StructReader { 56 | const _Aabb2Reader(); 57 | 58 | @override 59 | int get size => 16; 60 | 61 | @override 62 | Aabb2 createObject(fb.BufferContext bc, int offset) => Aabb2._(bc, offset); 63 | } 64 | 65 | class Aabb2Builder { 66 | Aabb2Builder(this.fbBuilder); 67 | 68 | final fb.Builder fbBuilder; 69 | 70 | int finish(fb.StructBuilder min, fb.StructBuilder max) { 71 | max(); 72 | min(); 73 | return fbBuilder.offset; 74 | } 75 | } 76 | 77 | class Aabb2ObjectBuilder extends fb.ObjectBuilder { 78 | final Vector2ObjectBuilder _min; 79 | final Vector2ObjectBuilder _max; 80 | 81 | Aabb2ObjectBuilder({ 82 | required Vector2ObjectBuilder min, 83 | required Vector2ObjectBuilder max, 84 | }) : _min = min, 85 | _max = max; 86 | 87 | /// Finish building, and store into the [fbBuilder]. 88 | @override 89 | int finish(fb.Builder fbBuilder) { 90 | _max.finish(fbBuilder); 91 | _min.finish(fbBuilder); 92 | return fbBuilder.offset; 93 | } 94 | 95 | /// Convenience method to serialize to byte list. 96 | @override 97 | Uint8List toBytes([String? fileIdentifier]) { 98 | final fbBuilder = fb.Builder(deduplicateTables: false); 99 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 100 | return fbBuilder.buffer; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/bounding_hitbox_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart'; 9 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/geometry_universal.dart' 10 | as ui; 11 | import 'package:flat_buffers/flat_buffers.dart' as fb; 12 | import 'package:vector_math/vector_math_64.dart' as vector; 13 | 14 | class BoundingHitbox { 15 | factory BoundingHitbox(List bytes) { 16 | final rootRef = fb.BufferContext.fromBytes(bytes); 17 | return reader.read(rootRef, 0); 18 | } 19 | BoundingHitbox._(this._bc, this._bcOffset); 20 | 21 | static const fb.Reader reader = _BoundingHitboxReader(); 22 | 23 | final fb.BufferContext _bc; 24 | final int _bcOffset; 25 | 26 | ui.Rect? rectCache; 27 | vector.Vector2? aabbCenter; 28 | 29 | Vector2? get position => Vector2.reader.vTableGetNullable(_bc, _bcOffset, 4); 30 | 31 | Vector2? get size => Vector2.reader.vTableGetNullable(_bc, _bcOffset, 6); 32 | 33 | Vector2? get parentPosition => 34 | Vector2.reader.vTableGetNullable(_bc, _bcOffset, 8); 35 | 36 | bool get skip => const fb.BoolReader().vTableGet(_bc, _bcOffset, 10, false); 37 | 38 | int get index => const fb.Int32Reader().vTableGet(_bc, _bcOffset, 12, 0); 39 | 40 | Aabb2? get aabb => Aabb2.reader.vTableGetNullable(_bc, _bcOffset, 14); 41 | 42 | @override 43 | String toString() { 44 | return 'BoundingHitbox{position: $position, size: $size, parentPosition: $parentPosition, skip: $skip, index: $index, aabb: $aabb}'; 45 | } 46 | 47 | BoundingHitboxT unpack() => BoundingHitboxT( 48 | position: position?.unpack(), 49 | size: size?.unpack(), 50 | parentPosition: parentPosition?.unpack(), 51 | skip: skip, 52 | index: index, 53 | aabb: aabb?.unpack()); 54 | 55 | static int pack(fb.Builder fbBuilder, BoundingHitboxT? object) { 56 | if (object == null) return 0; 57 | return object.pack(fbBuilder); 58 | } 59 | } 60 | 61 | class BoundingHitboxT implements fb.Packable { 62 | Vector2T? position; 63 | Vector2T? size; 64 | Vector2T? parentPosition; 65 | bool skip; 66 | int index; 67 | Aabb2T? aabb; 68 | 69 | BoundingHitboxT( 70 | {this.position, 71 | this.size, 72 | this.parentPosition, 73 | this.skip = false, 74 | this.index = 0, 75 | this.aabb}); 76 | 77 | @override 78 | int pack(fb.Builder fbBuilder) { 79 | fbBuilder.startTable(6); 80 | if (position != null) { 81 | fbBuilder.addStruct(0, position!.pack(fbBuilder)); 82 | } 83 | if (size != null) { 84 | fbBuilder.addStruct(1, size!.pack(fbBuilder)); 85 | } 86 | if (parentPosition != null) { 87 | fbBuilder.addStruct(2, parentPosition!.pack(fbBuilder)); 88 | } 89 | fbBuilder.addBool(3, skip); 90 | fbBuilder.addInt32(4, index); 91 | if (aabb != null) { 92 | fbBuilder.addStruct(5, aabb!.pack(fbBuilder)); 93 | } 94 | return fbBuilder.endTable(); 95 | } 96 | 97 | @override 98 | String toString() { 99 | return 'BoundingHitboxT{position: $position, size: $size, parentPosition: $parentPosition, skip: $skip, index: $index, aabb: $aabb}'; 100 | } 101 | } 102 | 103 | class _BoundingHitboxReader extends fb.TableReader { 104 | const _BoundingHitboxReader(); 105 | 106 | @override 107 | BoundingHitbox createObject(fb.BufferContext bc, int offset) => 108 | BoundingHitbox._(bc, offset); 109 | } 110 | 111 | class BoundingHitboxBuilder { 112 | BoundingHitboxBuilder(this.fbBuilder); 113 | 114 | final fb.Builder fbBuilder; 115 | 116 | void begin() { 117 | fbBuilder.startTable(6); 118 | } 119 | 120 | int addPosition(int offset) { 121 | fbBuilder.addStruct(0, offset); 122 | return fbBuilder.offset; 123 | } 124 | 125 | int addSize(int offset) { 126 | fbBuilder.addStruct(1, offset); 127 | return fbBuilder.offset; 128 | } 129 | 130 | int addParentPosition(int offset) { 131 | fbBuilder.addStruct(2, offset); 132 | return fbBuilder.offset; 133 | } 134 | 135 | int addSkip(bool? skip) { 136 | fbBuilder.addBool(3, skip); 137 | return fbBuilder.offset; 138 | } 139 | 140 | int addIndex(int? index) { 141 | fbBuilder.addInt32(4, index); 142 | return fbBuilder.offset; 143 | } 144 | 145 | int addAabb(int offset) { 146 | fbBuilder.addStruct(5, offset); 147 | return fbBuilder.offset; 148 | } 149 | 150 | int finish() { 151 | return fbBuilder.endTable(); 152 | } 153 | } 154 | 155 | class BoundingHitboxObjectBuilder extends fb.ObjectBuilder { 156 | final Vector2ObjectBuilder? _position; 157 | final Vector2ObjectBuilder? _size; 158 | final Vector2ObjectBuilder? _parentPosition; 159 | final bool? _skip; 160 | final int? _index; 161 | final Aabb2ObjectBuilder? _aabb; 162 | 163 | BoundingHitboxObjectBuilder({ 164 | Vector2ObjectBuilder? position, 165 | Vector2ObjectBuilder? size, 166 | Vector2ObjectBuilder? parentPosition, 167 | bool? skip, 168 | int? index, 169 | Aabb2ObjectBuilder? aabb, 170 | }) : _position = position, 171 | _size = size, 172 | _parentPosition = parentPosition, 173 | _skip = skip, 174 | _index = index, 175 | _aabb = aabb; 176 | 177 | /// Finish building, and store into the [fbBuilder]. 178 | @override 179 | int finish(fb.Builder fbBuilder) { 180 | fbBuilder.startTable(6); 181 | if (_position != null) { 182 | fbBuilder.addStruct(0, _position!.finish(fbBuilder)); 183 | } 184 | if (_size != null) { 185 | fbBuilder.addStruct(1, _size!.finish(fbBuilder)); 186 | } 187 | if (_parentPosition != null) { 188 | fbBuilder.addStruct(2, _parentPosition!.finish(fbBuilder)); 189 | } 190 | fbBuilder.addBool(3, _skip); 191 | fbBuilder.addInt32(4, _index); 192 | if (_aabb != null) { 193 | fbBuilder.addStruct(5, _aabb!.finish(fbBuilder)); 194 | } 195 | return fbBuilder.endTable(); 196 | } 197 | 198 | /// Convenience method to serialize to byte list. 199 | @override 200 | Uint8List toBytes([String? fileIdentifier]) { 201 | final fbBuilder = fb.Builder(deduplicateTables: false); 202 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 203 | return fbBuilder.buffer; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/optimized_collisions_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart'; 9 | import 'package:flat_buffers/flat_buffers.dart' as fb; 10 | 11 | class OptimizedCollisions { 12 | OptimizedCollisions._(this._bc, this._bcOffset); 13 | factory OptimizedCollisions(List bytes) { 14 | final rootRef = fb.BufferContext.fromBytes(bytes); 15 | return reader.read(rootRef, 0); 16 | } 17 | 18 | static const fb.Reader reader = 19 | _OptimizedCollisionsReader(); 20 | 21 | final fb.BufferContext _bc; 22 | final int _bcOffset; 23 | 24 | List? get indicies => const fb.ListReader(fb.Int32Reader()) 25 | .vTableGetNullable(_bc, _bcOffset, 4); 26 | Rect? get optimizedBoundingRect => 27 | Rect.reader.vTableGetNullable(_bc, _bcOffset, 6); 28 | 29 | @override 30 | String toString() { 31 | return 'OptimizedCollisions{indicies: $indicies, optimizedBoundingRect: $optimizedBoundingRect}'; 32 | } 33 | 34 | OptimizedCollisionsT unpack() => OptimizedCollisionsT( 35 | indicies: const fb.ListReader(fb.Int32Reader(), lazy: false) 36 | .vTableGetNullable(_bc, _bcOffset, 4), 37 | optimizedBoundingRect: optimizedBoundingRect?.unpack()); 38 | 39 | static int pack(fb.Builder fbBuilder, OptimizedCollisionsT? object) { 40 | if (object == null) return 0; 41 | return object.pack(fbBuilder); 42 | } 43 | } 44 | 45 | class OptimizedCollisionsT implements fb.Packable { 46 | List? indicies; 47 | RectT? optimizedBoundingRect; 48 | 49 | OptimizedCollisionsT({this.indicies, this.optimizedBoundingRect}); 50 | 51 | @override 52 | int pack(fb.Builder fbBuilder) { 53 | final int? indiciesOffset = 54 | indicies == null ? null : fbBuilder.writeListInt32(indicies!); 55 | fbBuilder.startTable(2); 56 | fbBuilder.addOffset(0, indiciesOffset); 57 | if (optimizedBoundingRect != null) { 58 | fbBuilder.addStruct(1, optimizedBoundingRect!.pack(fbBuilder)); 59 | } 60 | return fbBuilder.endTable(); 61 | } 62 | 63 | @override 64 | String toString() { 65 | return 'OptimizedCollisionsT{indicies: $indicies, optimizedBoundingRect: $optimizedBoundingRect}'; 66 | } 67 | } 68 | 69 | class _OptimizedCollisionsReader extends fb.TableReader { 70 | const _OptimizedCollisionsReader(); 71 | 72 | @override 73 | OptimizedCollisions createObject(fb.BufferContext bc, int offset) => 74 | OptimizedCollisions._(bc, offset); 75 | } 76 | 77 | class OptimizedCollisionsBuilder { 78 | OptimizedCollisionsBuilder(this.fbBuilder); 79 | 80 | final fb.Builder fbBuilder; 81 | 82 | void begin() { 83 | fbBuilder.startTable(2); 84 | } 85 | 86 | int addIndiciesOffset(int? offset) { 87 | fbBuilder.addOffset(0, offset); 88 | return fbBuilder.offset; 89 | } 90 | 91 | int addOptimizedBoundingRect(int offset) { 92 | fbBuilder.addStruct(1, offset); 93 | return fbBuilder.offset; 94 | } 95 | 96 | int finish() { 97 | return fbBuilder.endTable(); 98 | } 99 | } 100 | 101 | class OptimizedCollisionsObjectBuilder extends fb.ObjectBuilder { 102 | final List? _indicies; 103 | final RectObjectBuilder? _optimizedBoundingRect; 104 | 105 | OptimizedCollisionsObjectBuilder({ 106 | List? indicies, 107 | RectObjectBuilder? optimizedBoundingRect, 108 | }) : _indicies = indicies, 109 | _optimizedBoundingRect = optimizedBoundingRect; 110 | 111 | /// Finish building, and store into the [fbBuilder]. 112 | @override 113 | int finish(fb.Builder fbBuilder) { 114 | final int? indiciesOffset = 115 | _indicies == null ? null : fbBuilder.writeListInt32(_indicies!); 116 | fbBuilder.startTable(2); 117 | fbBuilder.addOffset(0, indiciesOffset); 118 | if (_optimizedBoundingRect != null) { 119 | fbBuilder.addStruct(1, _optimizedBoundingRect!.finish(fbBuilder)); 120 | } 121 | return fbBuilder.endTable(); 122 | } 123 | 124 | /// Convenience method to serialize to byte list. 125 | @override 126 | Uint8List toBytes([String? fileIdentifier]) { 127 | final fbBuilder = fb.Builder(deduplicateTables: false); 128 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 129 | return fbBuilder.buffer; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/rect_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | import 'package:flat_buffers/flat_buffers.dart' as fb; 8 | 9 | 10 | class Rect { 11 | Rect._(this._bc, this._bcOffset); 12 | 13 | static const fb.Reader reader = _RectReader(); 14 | 15 | final fb.BufferContext _bc; 16 | final int _bcOffset; 17 | 18 | double get left => const fb.Float32Reader().read(_bc, _bcOffset + 0); 19 | double get top => const fb.Float32Reader().read(_bc, _bcOffset + 4); 20 | double get right => const fb.Float32Reader().read(_bc, _bcOffset + 8); 21 | double get bottom => const fb.Float32Reader().read(_bc, _bcOffset + 12); 22 | 23 | @override 24 | String toString() { 25 | return 'Rect{left: $left, top: $top, right: $right, bottom: $bottom}'; 26 | } 27 | 28 | RectT unpack() => RectT( 29 | left: left, 30 | top: top, 31 | right: right, 32 | bottom: bottom); 33 | 34 | static int pack(fb.Builder fbBuilder, RectT? object) { 35 | if (object == null) return 0; 36 | return object.pack(fbBuilder); 37 | } 38 | } 39 | 40 | class RectT implements fb.Packable { 41 | double left; 42 | double top; 43 | double right; 44 | double bottom; 45 | 46 | RectT({ 47 | required this.left, 48 | required this.top, 49 | required this.right, 50 | required this.bottom}); 51 | 52 | @override 53 | int pack(fb.Builder fbBuilder) { 54 | fbBuilder.putFloat32(bottom); 55 | fbBuilder.putFloat32(right); 56 | fbBuilder.putFloat32(top); 57 | fbBuilder.putFloat32(left); 58 | return fbBuilder.offset; 59 | } 60 | 61 | @override 62 | String toString() { 63 | return 'RectT{left: $left, top: $top, right: $right, bottom: $bottom}'; 64 | } 65 | } 66 | 67 | class _RectReader extends fb.StructReader { 68 | const _RectReader(); 69 | 70 | @override 71 | int get size => 16; 72 | 73 | @override 74 | Rect createObject(fb.BufferContext bc, int offset) => 75 | Rect._(bc, offset); 76 | } 77 | 78 | class RectBuilder { 79 | RectBuilder(this.fbBuilder); 80 | 81 | final fb.Builder fbBuilder; 82 | 83 | int finish(double left, double top, double right, double bottom) { 84 | fbBuilder.putFloat32(bottom); 85 | fbBuilder.putFloat32(right); 86 | fbBuilder.putFloat32(top); 87 | fbBuilder.putFloat32(left); 88 | return fbBuilder.offset; 89 | } 90 | 91 | } 92 | 93 | class RectObjectBuilder extends fb.ObjectBuilder { 94 | final double _left; 95 | final double _top; 96 | final double _right; 97 | final double _bottom; 98 | 99 | RectObjectBuilder({ 100 | required double left, 101 | required double top, 102 | required double right, 103 | required double bottom, 104 | }) 105 | : _left = left, 106 | _top = top, 107 | _right = right, 108 | _bottom = bottom; 109 | 110 | /// Finish building, and store into the [fbBuilder]. 111 | @override 112 | int finish(fb.Builder fbBuilder) { 113 | fbBuilder.putFloat32(_bottom); 114 | fbBuilder.putFloat32(_right); 115 | fbBuilder.putFloat32(_top); 116 | fbBuilder.putFloat32(_left); 117 | return fbBuilder.offset; 118 | } 119 | 120 | /// Convenience method to serialize to byte list. 121 | @override 122 | Uint8List toBytes([String? fileIdentifier]) { 123 | final fbBuilder = fb.Builder(deduplicateTables: false); 124 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 125 | return fbBuilder.buffer; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/request_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart'; 9 | import 'package:flat_buffers/flat_buffers.dart' as fb; 10 | 11 | class OverlappingSearchRequest { 12 | OverlappingSearchRequest._(this._bc, this._bcOffset); 13 | factory OverlappingSearchRequest(List bytes) { 14 | final rootRef = fb.BufferContext.fromBytes(bytes); 15 | return reader.read(rootRef, 0); 16 | } 17 | 18 | static const fb.Reader reader = 19 | _OverlappingSearchRequestReader(); 20 | 21 | final fb.BufferContext _bc; 22 | final int _bcOffset; 23 | 24 | List? get hitboxes => 25 | const fb.ListReader(BoundingHitbox.reader) 26 | .vTableGetNullable(_bc, _bcOffset, 4); 27 | int get maximumItemsInGroup => 28 | const fb.Int32Reader().vTableGet(_bc, _bcOffset, 6, 0); 29 | 30 | @override 31 | String toString() { 32 | return 'OverlappingSearchRequest{hitboxes: $hitboxes, maximumItemsInGroup: $maximumItemsInGroup}'; 33 | } 34 | 35 | OverlappingSearchRequestT unpack() => OverlappingSearchRequestT( 36 | hitboxes: hitboxes?.map((e) => e.unpack()).toList(), 37 | maximumItemsInGroup: maximumItemsInGroup); 38 | 39 | static int pack(fb.Builder fbBuilder, OverlappingSearchRequestT? object) { 40 | if (object == null) return 0; 41 | return object.pack(fbBuilder); 42 | } 43 | } 44 | 45 | class OverlappingSearchRequestT implements fb.Packable { 46 | List? hitboxes; 47 | int maximumItemsInGroup; 48 | 49 | OverlappingSearchRequestT({this.hitboxes, this.maximumItemsInGroup = 0}); 50 | 51 | @override 52 | int pack(fb.Builder fbBuilder) { 53 | final int? hitboxesOffset = hitboxes == null 54 | ? null 55 | : fbBuilder.writeList(hitboxes!.map((b) => b.pack(fbBuilder)).toList()); 56 | fbBuilder.startTable(2); 57 | fbBuilder.addOffset(0, hitboxesOffset); 58 | fbBuilder.addInt32(1, maximumItemsInGroup); 59 | return fbBuilder.endTable(); 60 | } 61 | 62 | @override 63 | String toString() { 64 | return 'OverlappingSearchRequestT{hitboxes: $hitboxes, maximumItemsInGroup: $maximumItemsInGroup}'; 65 | } 66 | } 67 | 68 | class _OverlappingSearchRequestReader 69 | extends fb.TableReader { 70 | const _OverlappingSearchRequestReader(); 71 | 72 | @override 73 | OverlappingSearchRequest createObject(fb.BufferContext bc, int offset) => 74 | OverlappingSearchRequest._(bc, offset); 75 | } 76 | 77 | class OverlappingSearchRequestBuilder { 78 | OverlappingSearchRequestBuilder(this.fbBuilder); 79 | 80 | final fb.Builder fbBuilder; 81 | 82 | void begin() { 83 | fbBuilder.startTable(2); 84 | } 85 | 86 | int addHitboxesOffset(int? offset) { 87 | fbBuilder.addOffset(0, offset); 88 | return fbBuilder.offset; 89 | } 90 | 91 | int addMaximumItemsInGroup(int? maximumItemsInGroup) { 92 | fbBuilder.addInt32(1, maximumItemsInGroup); 93 | return fbBuilder.offset; 94 | } 95 | 96 | int finish() { 97 | return fbBuilder.endTable(); 98 | } 99 | } 100 | 101 | class OverlappingSearchRequestObjectBuilder extends fb.ObjectBuilder { 102 | final List? _hitboxes; 103 | final int? _maximumItemsInGroup; 104 | 105 | OverlappingSearchRequestObjectBuilder({ 106 | List? hitboxes, 107 | int? maximumItemsInGroup, 108 | }) : _hitboxes = hitboxes, 109 | _maximumItemsInGroup = maximumItemsInGroup; 110 | 111 | /// Finish building, and store into the [fbBuilder]. 112 | @override 113 | int finish(fb.Builder fbBuilder) { 114 | final int? hitboxesOffset = _hitboxes == null 115 | ? null 116 | : fbBuilder.writeList( 117 | _hitboxes!.map((b) => b.getOrCreateOffset(fbBuilder)).toList()); 118 | fbBuilder.startTable(2); 119 | fbBuilder.addOffset(0, hitboxesOffset); 120 | fbBuilder.addInt32(1, _maximumItemsInGroup); 121 | return fbBuilder.endTable(); 122 | } 123 | 124 | /// Convenience method to serialize to byte list. 125 | @override 126 | Uint8List toBytes([String? fileIdentifier]) { 127 | final fbBuilder = fb.Builder(deduplicateTables: false); 128 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 129 | return fbBuilder.buffer; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/response_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | 8 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/flat_buffers/flat_buffers_optimizer.dart'; 9 | import 'package:flat_buffers/flat_buffers.dart' as fb; 10 | 11 | class OverlappedSearchResponse { 12 | OverlappedSearchResponse._(this._bc, this._bcOffset); 13 | 14 | factory OverlappedSearchResponse(List bytes) { 15 | final rootRef = fb.BufferContext.fromBytes(bytes); 16 | return reader.read(rootRef, 0); 17 | } 18 | 19 | static const fb.Reader reader = 20 | _OverlappedSearchResponseReader(); 21 | 22 | final fb.BufferContext _bc; 23 | final int _bcOffset; 24 | 25 | List? get optimizedCollisions => 26 | const fb.ListReader(OptimizedCollisions.reader) 27 | .vTableGetNullable(_bc, _bcOffset, 4); 28 | 29 | @override 30 | String toString() { 31 | return 'OverlappedSearchResponse{optimizedCollisions: $optimizedCollisions}'; 32 | } 33 | 34 | OverlappedSearchResponseT unpack() => OverlappedSearchResponseT( 35 | optimizedCollisions: 36 | optimizedCollisions?.map((e) => e.unpack()).toList()); 37 | 38 | static int pack(fb.Builder fbBuilder, OverlappedSearchResponseT? object) { 39 | if (object == null) return 0; 40 | return object.pack(fbBuilder); 41 | } 42 | } 43 | 44 | class OverlappedSearchResponseT implements fb.Packable { 45 | List? optimizedCollisions; 46 | 47 | OverlappedSearchResponseT({this.optimizedCollisions}); 48 | 49 | @override 50 | int pack(fb.Builder fbBuilder) { 51 | final int? optimizedCollisionsOffset = optimizedCollisions == null 52 | ? null 53 | : fbBuilder.writeList( 54 | optimizedCollisions!.map((b) => b.pack(fbBuilder)).toList()); 55 | fbBuilder.startTable(1); 56 | fbBuilder.addOffset(0, optimizedCollisionsOffset); 57 | return fbBuilder.endTable(); 58 | } 59 | 60 | @override 61 | String toString() { 62 | return 'OverlappedSearchResponseT{optimizedCollisions: $optimizedCollisions}'; 63 | } 64 | } 65 | 66 | class _OverlappedSearchResponseReader 67 | extends fb.TableReader { 68 | const _OverlappedSearchResponseReader(); 69 | 70 | @override 71 | OverlappedSearchResponse createObject(fb.BufferContext bc, int offset) => 72 | OverlappedSearchResponse._(bc, offset); 73 | } 74 | 75 | class OverlappedSearchResponseBuilder { 76 | OverlappedSearchResponseBuilder(this.fbBuilder); 77 | 78 | final fb.Builder fbBuilder; 79 | 80 | void begin() { 81 | fbBuilder.startTable(1); 82 | } 83 | 84 | int addOptimizedCollisionsOffset(int? offset) { 85 | fbBuilder.addOffset(0, offset); 86 | return fbBuilder.offset; 87 | } 88 | 89 | int finish() { 90 | return fbBuilder.endTable(); 91 | } 92 | } 93 | 94 | class OverlappedSearchResponseObjectBuilder extends fb.ObjectBuilder { 95 | final List? _optimizedCollisions; 96 | 97 | OverlappedSearchResponseObjectBuilder({ 98 | List? optimizedCollisions, 99 | }) : _optimizedCollisions = optimizedCollisions; 100 | 101 | /// Finish building, and store into the [fbBuilder]. 102 | @override 103 | int finish(fb.Builder fbBuilder) { 104 | final int? optimizedCollisionsOffset = _optimizedCollisions == null 105 | ? null 106 | : fbBuilder.writeList(_optimizedCollisions! 107 | .map((b) => b.getOrCreateOffset(fbBuilder)) 108 | .toList()); 109 | fbBuilder.startTable(1); 110 | fbBuilder.addOffset(0, optimizedCollisionsOffset); 111 | return fbBuilder.endTable(); 112 | } 113 | 114 | /// Convenience method to serialize to byte list. 115 | @override 116 | Uint8List toBytes([String? fileIdentifier]) { 117 | final fbBuilder = fb.Builder(deduplicateTables: false); 118 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 119 | return fbBuilder.buffer; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/generated/vector2_optimizer_generated.dart: -------------------------------------------------------------------------------- 1 | // automatically generated by the FlatBuffers compiler, do not modify 2 | // ignore_for_file: unused_import, unused_field, unused_element, unused_local_variable 3 | 4 | library optimizer; 5 | 6 | import 'dart:typed_data' show Uint8List; 7 | import 'package:flat_buffers/flat_buffers.dart' as fb; 8 | 9 | 10 | class Vector2 { 11 | Vector2._(this._bc, this._bcOffset); 12 | 13 | static const fb.Reader reader = _Vector2Reader(); 14 | 15 | final fb.BufferContext _bc; 16 | final int _bcOffset; 17 | 18 | double get x => const fb.Float32Reader().read(_bc, _bcOffset + 0); 19 | double get y => const fb.Float32Reader().read(_bc, _bcOffset + 4); 20 | 21 | @override 22 | String toString() { 23 | return 'Vector2{x: $x, y: $y}'; 24 | } 25 | 26 | Vector2T unpack() => Vector2T( 27 | x: x, 28 | y: y); 29 | 30 | static int pack(fb.Builder fbBuilder, Vector2T? object) { 31 | if (object == null) return 0; 32 | return object.pack(fbBuilder); 33 | } 34 | } 35 | 36 | class Vector2T implements fb.Packable { 37 | double x; 38 | double y; 39 | 40 | Vector2T({ 41 | required this.x, 42 | required this.y}); 43 | 44 | @override 45 | int pack(fb.Builder fbBuilder) { 46 | fbBuilder.putFloat32(y); 47 | fbBuilder.putFloat32(x); 48 | return fbBuilder.offset; 49 | } 50 | 51 | @override 52 | String toString() { 53 | return 'Vector2T{x: $x, y: $y}'; 54 | } 55 | } 56 | 57 | class _Vector2Reader extends fb.StructReader { 58 | const _Vector2Reader(); 59 | 60 | @override 61 | int get size => 8; 62 | 63 | @override 64 | Vector2 createObject(fb.BufferContext bc, int offset) => 65 | Vector2._(bc, offset); 66 | } 67 | 68 | class Vector2Builder { 69 | Vector2Builder(this.fbBuilder); 70 | 71 | final fb.Builder fbBuilder; 72 | 73 | int finish(double x, double y) { 74 | fbBuilder.putFloat32(y); 75 | fbBuilder.putFloat32(x); 76 | return fbBuilder.offset; 77 | } 78 | 79 | } 80 | 81 | class Vector2ObjectBuilder extends fb.ObjectBuilder { 82 | final double _x; 83 | final double _y; 84 | 85 | Vector2ObjectBuilder({ 86 | required double x, 87 | required double y, 88 | }) 89 | : _x = x, 90 | _y = y; 91 | 92 | /// Finish building, and store into the [fbBuilder]. 93 | @override 94 | int finish(fb.Builder fbBuilder) { 95 | fbBuilder.putFloat32(_y); 96 | fbBuilder.putFloat32(_x); 97 | return fbBuilder.offset; 98 | } 99 | 100 | /// Convenience method to serialize to byte list. 101 | @override 102 | Uint8List toBytes([String? fileIdentifier]) { 103 | final fbBuilder = fb.Builder(deduplicateTables: false); 104 | fbBuilder.finish(finish(fbBuilder), fileIdentifier); 105 | return fbBuilder.buffer; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/aabb2.fbs: -------------------------------------------------------------------------------- 1 | include "vector2.fbs"; 2 | 3 | namespace Optimizer; 4 | 5 | struct Aabb2 { 6 | min: Vector2; 7 | max: Vector2; 8 | } -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/bounding_hitbox.fbs: -------------------------------------------------------------------------------- 1 | include "vector2.fbs"; 2 | include "aabb2.fbs"; 3 | 4 | namespace Optimizer; 5 | 6 | table BoundingHitbox { 7 | position: Vector2; 8 | size: Vector2; 9 | parentPosition: Vector2; 10 | skip: bool; 11 | index: int; 12 | aabb: Aabb2; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/optimized_collisions.fbs: -------------------------------------------------------------------------------- 1 | include "rect.fbs"; 2 | 3 | namespace Optimizer; 4 | 5 | table OptimizedCollisions { 6 | indicies: [int]; 7 | optimizedBoundingRect: Rect; 8 | } -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/rect.fbs: -------------------------------------------------------------------------------- 1 | namespace Optimizer; 2 | 3 | struct Rect { 4 | left: float; 5 | top: float; 6 | right: float; 7 | bottom: float; 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/request.fbs: -------------------------------------------------------------------------------- 1 | include "bounding_hitbox.fbs"; 2 | 3 | namespace Optimizer; 4 | 5 | 6 | table OverlappingSearchRequest { 7 | hitboxes: [BoundingHitbox]; 8 | maximumItemsInGroup: int; 9 | } -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/response.fbs: -------------------------------------------------------------------------------- 1 | include "optimized_collisions.fbs"; 2 | 3 | namespace Optimizer; 4 | 5 | table OverlappedSearchResponse { 6 | optimizedCollisions: [OptimizedCollisions]; 7 | } -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/flat_buffers/schema/vector2.fbs: -------------------------------------------------------------------------------- 1 | namespace Optimizer; 2 | 3 | struct Vector2{ 4 | x: float; 5 | y: float; 6 | } -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/isolate/spatial_grid_optimizer_worker.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_web_libraries_in_flutter, depend_on_referenced_packages 2 | 3 | import 'dart:async'; 4 | import 'dart:html' as html; 5 | import 'dart:js' as js; 6 | 7 | import 'package:flame_spatial_grid/src/collisions/optimizer/isolate/entry_point.dart'; 8 | import 'package:isolate_manager/isolate_manager.dart'; 9 | import 'package:js/js.dart' as pjs; 10 | import 'package:js/js_util.dart' as js_util; 11 | 12 | @pjs.JS('self') 13 | external dynamic get globalScopeSelf; 14 | 15 | /// dart compile js worker.dart -o worker.js -O4 16 | 17 | /// In most cases you don't need to modify this function 18 | main() { 19 | callbackToStream('onmessage', (html.MessageEvent e) { 20 | return js_util.getProperty(e, 'data'); 21 | }).listen((message) async { 22 | final Completer completer = Completer(); 23 | completer.future.then( 24 | (value) => jsSendMessage(value), 25 | onError: (err, stack) => 26 | jsSendMessage(IsolateException(err, stack).toJson()), 27 | ); 28 | try { 29 | completer.complete(worker(message)); 30 | } catch (err, stack) { 31 | jsSendMessage(IsolateException(err, stack).toJson()); 32 | } 33 | }); 34 | } 35 | 36 | /// TODO: Modify your function here: 37 | /// 38 | /// Do this if you need to throw an exception 39 | /// 40 | /// You should only throw the `message` instead of a whole Object because it may 41 | /// not show as expected when sending back to the main app. 42 | /// 43 | /// ``` dart 44 | /// return throw 'This is an error that you need to catch in your main app'; 45 | /// ``` 46 | FutureOr worker(dynamic message) { 47 | // Best way to use this method is encoding the result to JSON 48 | // before sending to the main app, then you can decode it back to 49 | // the return type you want with `workerConverter`. 50 | return findOverlappingRectsIsolated(message); 51 | } 52 | 53 | /// Internal function 54 | Stream callbackToStream( 55 | String name, T Function(J jsValue) unwrapValue) { 56 | var controller = StreamController.broadcast(sync: true); 57 | js_util.setProperty(js.context['self'], name, js.allowInterop((J event) { 58 | controller.add(unwrapValue(event)); 59 | })); 60 | return controller.stream; 61 | } 62 | 63 | /// Internal function 64 | void jsSendMessage(dynamic m) { 65 | js.context.callMethod('postMessage', [m]); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/collisions/optimizer/optimized_collisions_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/collisions.dart'; 2 | import 'package:flame/extensions.dart'; 3 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 4 | import 'package:flame_spatial_grid/src/collisions/optimizer/extensions.dart'; 5 | 6 | class OptimizedCollisionList { 7 | OptimizedCollisionList( 8 | Iterable hitboxes, 9 | this.parentLayer, [ 10 | Rect expandedBoundingRect = Rect.zero, 11 | ]) { 12 | _hitboxes.addAll(hitboxes); 13 | _updateBoundingBox(expandedBoundingRect); 14 | } 15 | 16 | List get hitboxes => _hitboxes; 17 | final _hitboxes = []; 18 | var _boundingBox = GroupHitbox(tag: ''); 19 | final CellLayer parentLayer; 20 | 21 | GroupHitbox get boundingBox => _boundingBox; 22 | 23 | void add(ShapeHitbox hitbox) { 24 | if (!_hitboxes.contains(hitbox)) { 25 | _hitboxes.add(hitbox); 26 | _updateBoundingBox(); 27 | } 28 | } 29 | 30 | void remove(ShapeHitbox hitbox) { 31 | final found = _hitboxes.remove(hitbox); 32 | if (found) { 33 | _updateBoundingBox(); 34 | } 35 | } 36 | 37 | void _updateBoundingBox([ 38 | Rect expandedBoundingRect = Rect.zero, 39 | ]) { 40 | _boundingBox.removeFromParent(); 41 | var rect = Rect.zero; 42 | if (expandedBoundingRect == Rect.zero) { 43 | for (final hitbox in _hitboxes) { 44 | if (rect == Rect.zero) { 45 | rect = hitbox.toRectSpecial(); 46 | continue; 47 | } 48 | rect = rect.expandToInclude(hitbox.toRectSpecial()); 49 | } 50 | } else { 51 | rect = expandedBoundingRect; 52 | } 53 | final collisionType = parentLayer.currentCell!.state == CellState.inactive 54 | ? CollisionType.inactive 55 | : CollisionType.passive; 56 | _boundingBox = GroupHitbox( 57 | tag: parentLayer.name, 58 | parentWithGridSupport: parentLayer, 59 | position: rect.topLeft.toVector2(), 60 | size: rect.size.toVector2(), 61 | )..collisionType = collisionType; 62 | parentLayer.add(_boundingBox); 63 | for (final h in _hitboxes) { 64 | if (h is BoundingHitbox) { 65 | h.group = _boundingBox; 66 | } 67 | } 68 | } 69 | 70 | void clear() { 71 | _hitboxes.clear(); 72 | if (_boundingBox.parent != null) { 73 | _boundingBox.removeFromParent(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/components/animation_global/animation_global_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/image_composition.dart'; 3 | import 'package:flame/sprite.dart'; 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | 6 | class SpriteAnimationGlobalComponent extends SpriteAnimationComponent { 7 | SpriteAnimationGlobalComponent({ 8 | required SpriteAnimation animation, 9 | super.position, 10 | super.size, 11 | required this.animationType, 12 | required TickersManager tickersProvider, 13 | }) : animationLocal = animation, 14 | super( 15 | animation: AnimationGlobal( 16 | animation, 17 | animationType, 18 | tickersProvider, 19 | ), 20 | ); 21 | 22 | final SpriteAnimation animationLocal; 23 | final String animationType; 24 | 25 | @override 26 | // ignore: must_call_super 27 | void render(Canvas canvas) { 28 | try { 29 | (animationTicker! as SpriteAnimationTickerGlobal) 30 | .getSpriteOfAnimation(animationLocal) 31 | .render( 32 | canvas, 33 | size: size, 34 | overridePaint: paint, 35 | ); 36 | // ignore: avoid_catches_without_on_clauses 37 | } catch (_) {} 38 | } 39 | } 40 | 41 | class AnimationGlobal extends SpriteAnimation { 42 | AnimationGlobal( 43 | SpriteAnimation animation, 44 | this.animationType, 45 | this.tickersProvider, 46 | ) : super( 47 | animation.frames, 48 | loop: animation.loop, 49 | ); 50 | 51 | final String animationType; 52 | final TickersManager tickersProvider; 53 | 54 | @override 55 | SpriteAnimationTicker createTicker() { 56 | return tickersProvider.getTicker(animationType, this); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/components/animation_global/ticker_global.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/sprite.dart'; 2 | 3 | class SpriteAnimationTickerGlobal extends SpriteAnimationTicker { 4 | SpriteAnimationTickerGlobal(super.spriteAnimation); 5 | 6 | Sprite getSpriteOfAnimation(SpriteAnimation animationLocal) => 7 | animationLocal.frames[currentIndex].sprite; 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/components/animation_global/tickers_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flame/sprite.dart'; 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | 6 | class TickersManager { 7 | final _tickers = HashMap(); 8 | 9 | SpriteAnimationTicker getTicker( 10 | String animationType, 11 | SpriteAnimation animation, 12 | ) { 13 | var ticker = _tickers[animationType]; 14 | return ticker ??= 15 | _tickers[animationType] = SpriteAnimationTickerGlobal(animation); 16 | } 17 | 18 | void update(double dt) { 19 | for (final ticker in _tickers.values) { 20 | ticker.update(dt); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/components/camera_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | 4 | /// Camera wrapper allows to attach [CameraComponent] as a "trackedComponent" 5 | /// in [HasSpatialGridFramework] game. Every movement of the camera will be 6 | /// handled by spatial grid af if it would be simple [PositionComponent]. 7 | /// 8 | /// There is no need to add the component manually into a game. Just 9 | /// specify it as a parameter "trackedComponent" for 10 | /// [HasSpatialGridFramework.initializeSpatialGrid] function 11 | /// 12 | /// With this wrapper there is no need to make manual calls of 13 | /// [HasSpatialGridFramework.onAfterZoom] to make recalculation of visible 14 | /// grid's cells. 15 | /// 16 | /// Please notice, that this wrapper only works with Flame's "experimental" 17 | /// camera API. Old camera does not supported, you need to control everything 18 | /// manually in cause you use old API. 19 | class SpatialGridCameraWrapper extends PositionComponent 20 | with HasGridSupport { 21 | SpatialGridCameraWrapper(this.cameraComponent) { 22 | // ignore: invalid_use_of_internal_member 23 | cameraComponent.viewfinder.transform.offset.addListener(onPositionChange); 24 | // ignore: invalid_use_of_internal_member 25 | cameraComponent.viewfinder.transform.scale.addListener(onAfterZoom); 26 | position.setFrom(cameraComponent.viewfinder.position); 27 | priority = 999999999; 28 | checkOutOfCellBounds = false; 29 | } 30 | 31 | final CameraComponent cameraComponent; 32 | final _previousPosition = Vector2.zero(); 33 | 34 | /// Camera's viewfinder tracking. 35 | void onPositionChange() { 36 | if (_previousPosition != cameraComponent.viewfinder.position) { 37 | _previousPosition.setFrom(position); 38 | position.setFrom(cameraComponent.viewfinder.position); 39 | } 40 | } 41 | 42 | /// Camera's viewfinder zoom tracking 43 | /// Reimplement if you need to do additional actions on zoom change 44 | void onAfterZoom() { 45 | try { 46 | game.onAfterZoom(); 47 | // ignore: avoid_catches_without_on_clauses, empty_catches 48 | } catch (e) {} 49 | } 50 | 51 | @override 52 | void update(double dt) {} 53 | 54 | @override 55 | void onRemove() { 56 | // ignore: invalid_use_of_internal_member 57 | cameraComponent.viewfinder.transform.offset 58 | .removeListener(onPositionChange); 59 | // ignore: invalid_use_of_internal_member 60 | cameraComponent.viewfinder.transform.scale.removeListener(onPositionChange); 61 | super.onRemove(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/components/layers/cell_static_animation_layer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/image_composition.dart'; 3 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | 6 | class StaticAnimationLayerCacheEntry { 7 | StaticAnimationLayerCacheEntry(this.animationComponent); 8 | 9 | SpriteAnimationGlobalComponent animationComponent; 10 | int usageCount = 1; 11 | } 12 | 13 | class CellStaticAnimationLayer extends CellLayer { 14 | CellStaticAnimationLayer( 15 | super.cell, { 16 | super.name, 17 | super.componentsStorageMode, 18 | }); 19 | 20 | SpriteAnimationGlobalComponent? animationComponent; 21 | 22 | static final _compiledLayersCache = {}; 23 | 24 | @override 25 | void render(Canvas canvas) { 26 | animationComponent?.renderTree(canvas); 27 | if (debugMode) { 28 | renderDebugMode(canvas); 29 | for (final child in children) { 30 | if (child.debugMode) { 31 | child.renderTree(canvas); 32 | } 33 | } 34 | } 35 | } 36 | 37 | @override 38 | void compileToSingleLayer(Iterable components) { 39 | final animatedChildren = components.whereType(); 40 | if (animatedChildren.isEmpty) { 41 | removeFromParent(); 42 | return; 43 | } 44 | 45 | final animation = animatedChildren.first.animation; 46 | final ticker = animation?.createTicker(); 47 | if (animation == null || ticker == null) { 48 | return; 49 | } 50 | 51 | animationComponent?.playing = false; 52 | animationComponent?.removeFromParent(); 53 | 54 | final newSprites = []; 55 | 56 | while (ticker.currentIndex < animation.frames.length) { 57 | final sprite = ticker.getSprite(); 58 | final composition = ImageComposition(); 59 | for (final component in animatedChildren) { 60 | final correctedPosition = component.position + (correctionTopLeft * -1); 61 | composition.add(sprite.image, correctedPosition, source: sprite.src); 62 | } 63 | final composedImage = composition.composeSync(); 64 | newSprites.add(Sprite(composedImage)); 65 | ticker.currentIndex++; 66 | } 67 | final spriteAnimation = SpriteAnimation.variableSpriteList( 68 | newSprites, 69 | stepTimes: animation.getVariableStepTimes(), 70 | ); 71 | animationComponent = SpriteAnimationGlobalComponent( 72 | animation: spriteAnimation, 73 | position: correctionTopLeft, 74 | size: newSprites.first.image.size, 75 | animationType: name, 76 | tickersProvider: game.tickersManager, 77 | ); 78 | if (cacheKey.key != null) { 79 | _compiledLayersCache[cacheKey.key!] = 80 | StaticAnimationLayerCacheEntry(animationComponent!); 81 | } 82 | } 83 | 84 | @override 85 | void onResume(double dtElapsedWhileSuspended) { 86 | // isUpdateNeeded = true; 87 | super.onResume(dtElapsedWhileSuspended); 88 | } 89 | 90 | @override 91 | void onRemove() { 92 | final cachedImage = _compiledLayersCache[cacheKey.key]; 93 | if (cachedImage != null) { 94 | cachedImage.usageCount--; 95 | if (cachedImage.usageCount <= 0) { 96 | _disposeAnimationComponent(cachedImage.animationComponent); 97 | _compiledLayersCache.remove(cacheKey.key); 98 | } 99 | animationComponent = null; 100 | } else if (animationComponent != null) { 101 | _disposeAnimationComponent(animationComponent!); 102 | animationComponent = null; 103 | } 104 | super.onRemove(); 105 | } 106 | 107 | void _disposeAnimationComponent( 108 | SpriteAnimationGlobalComponent animationComponent, 109 | ) { 110 | final frames = animationComponent.animation?.frames; 111 | if (frames != null) { 112 | try { 113 | for (final element in frames) { 114 | element.sprite.image.dispose(); 115 | } 116 | // ignore: avoid_catches_without_on_clauses 117 | } catch (_) {} 118 | } 119 | animationComponent.onRemove(); 120 | } 121 | 122 | final _layerCacheKey = LayerCacheKey(); 123 | 124 | @override 125 | LayerCacheKey get cacheKey => _layerCacheKey; 126 | 127 | @override 128 | bool onCheckCache(int key) { 129 | final cache = _compiledLayersCache[key]; 130 | if (cache == null) { 131 | return false; 132 | } 133 | animationComponent = cache.animationComponent; 134 | cache.usageCount++; 135 | if (kDebugMode) { 136 | print('$name: ${cache.usageCount}'); 137 | } 138 | 139 | return true; 140 | } 141 | 142 | static void clearCache() => _compiledLayersCache.clear(); 143 | 144 | } 145 | 146 | extension _VariableStepTimes on SpriteAnimation { 147 | List getVariableStepTimes() { 148 | final times = []; 149 | for (final frame in frames) { 150 | times.add(frame.stepTime); 151 | } 152 | return times; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/components/layers/cell_static_layer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flame/components.dart'; 4 | import 'package:flame/extensions.dart'; 5 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 6 | 7 | class ImageCacheEntry { 8 | ImageCacheEntry(this.image); 9 | 10 | final Image image; 11 | int usageCount = 1; 12 | } 13 | 14 | class CellStaticLayer extends CellLayer { 15 | CellStaticLayer( 16 | super.cell, { 17 | super.name, 18 | super.componentsStorageMode, 19 | }) { 20 | paint.isAntiAlias = false; 21 | paint.filterQuality = FilterQuality.none; 22 | } 23 | 24 | static final _compiledLayersCache = {}; 25 | 26 | final _layerCacheKey = LayerCacheKey(); 27 | 28 | @override 29 | LayerCacheKey get cacheKey => _layerCacheKey; 30 | 31 | Picture? layerPicture; 32 | Image? layerImage; 33 | 34 | static const secondsBetweenImageUpdate = 5; 35 | int _millisecondsBetweenImageUpdate = 0; 36 | bool _renderAsImage = false; 37 | 38 | @override 39 | void render(Canvas canvas) { 40 | switch (renderMode) { 41 | case LayerRenderMode.component: 42 | if (componentsStorageMode == 43 | LayerComponentsStorageMode.internalLayerSet) { 44 | for (final c in alternativeComponentSet) { 45 | c.renderTree(canvas); 46 | } 47 | } else { 48 | for (final c in children) { 49 | c.renderTree(canvas); 50 | } 51 | } 52 | break; 53 | case LayerRenderMode.picture: 54 | if (layerPicture != null) { 55 | canvas.drawPicture(layerPicture!); 56 | } 57 | break; 58 | case LayerRenderMode.image: 59 | if (layerImage != null) { 60 | canvas.drawImage( 61 | layerImage!, 62 | correctionTopLeft.toOffset(), 63 | paint, 64 | ); 65 | } 66 | break; 67 | case LayerRenderMode.auto: 68 | if (_renderAsImage && layerImage != null) { 69 | canvas.drawImage( 70 | layerImage!, 71 | correctionTopLeft.toOffset(), 72 | paint, 73 | ); 74 | } else if (layerPicture != null) { 75 | canvas.drawPicture(layerPicture!); 76 | if (_millisecondsBetweenImageUpdate == 0) { 77 | _millisecondsBetweenImageUpdate = 78 | DateTime.now().millisecondsSinceEpoch; 79 | } else { 80 | final msNew = DateTime.now().millisecondsSinceEpoch; 81 | if ((msNew - _millisecondsBetweenImageUpdate) >= 82 | secondsBetweenImageUpdate * 1000) { 83 | _renderPictureToImage(); 84 | _millisecondsBetweenImageUpdate = 0; 85 | } 86 | } 87 | } 88 | break; 89 | } 90 | } 91 | 92 | void _renderPictureToImage() { 93 | if (layerPicture != null) { 94 | _renderAsImage = true; 95 | final recorder = PictureRecorder(); 96 | final canvas = Canvas(recorder); 97 | correctionDecorator.applyChain( 98 | (canvas) { 99 | canvas.drawPicture(layerPicture!); 100 | }, 101 | canvas, 102 | ); 103 | layerImage = recorder.endRecording().toImageSync( 104 | layerCalculatedSize.width.toInt(), 105 | layerCalculatedSize.height.toInt(), 106 | ); 107 | } 108 | } 109 | 110 | @override 111 | bool onCheckCache(int key) { 112 | if (renderMode != LayerRenderMode.image) { 113 | return false; 114 | } 115 | final cache = _compiledLayersCache[key]; 116 | if (cache == null) { 117 | return false; 118 | } 119 | layerImage = cache.image; 120 | cache.usageCount++; 121 | return true; 122 | } 123 | 124 | @override 125 | void compileToSingleLayer(Iterable components) { 126 | final renderingChildren = components.whereType(); 127 | // if (renderingChildren.isEmpty) { 128 | // return; 129 | // } 130 | 131 | final cell = currentCell; 132 | if (cell == null) { 133 | return; 134 | } 135 | 136 | _millisecondsBetweenImageUpdate = 0; 137 | _renderAsImage = false; 138 | final recorder = PictureRecorder(); 139 | final canvas = Canvas(recorder); 140 | if (renderMode == LayerRenderMode.image) { 141 | for (final component in renderingChildren) { 142 | correctionDecorator.applyChain( 143 | (canvas) { 144 | component.decorator.applyChain(component.render, canvas); 145 | }, 146 | canvas, 147 | ); 148 | } 149 | final newPicture = recorder.endRecording(); 150 | // layerImage?.dispose(); 151 | layerImage = newPicture.toImageSync( 152 | layerCalculatedSize.width.toInt(), 153 | layerCalculatedSize.height.toInt(), 154 | ); 155 | if (cacheKey.key != null) { 156 | _compiledLayersCache[cacheKey.key!] = ImageCacheEntry(layerImage!); 157 | } 158 | newPicture.dispose(); 159 | } else if (renderMode == LayerRenderMode.picture || 160 | renderMode == LayerRenderMode.auto) { 161 | for (final component in renderingChildren) { 162 | component.decorator.applyChain(component.render, canvas); 163 | } 164 | layerPicture?.dispose(); 165 | layerPicture = recorder.endRecording(); 166 | } 167 | } 168 | 169 | @override 170 | void onResume(double dtElapsedWhileSuspended) { 171 | // isUpdateNeeded = true; 172 | super.onResume(dtElapsedWhileSuspended); 173 | } 174 | 175 | @override 176 | void onRemove() { 177 | try { 178 | final cachedImage = _compiledLayersCache[cacheKey.key]; 179 | if (cachedImage != null) { 180 | cachedImage.usageCount--; 181 | if (cachedImage.usageCount <= 0) { 182 | // cachedImage.image.dispose(); 183 | _compiledLayersCache.remove(cacheKey.key); 184 | } 185 | } else { 186 | // layerImage?.dispose(); 187 | layerImage = null; 188 | } 189 | // layerPicture?.dispose(); 190 | layerPicture = null; 191 | 192 | // ignore: avoid_catches_without_on_clauses, empty_catches 193 | } catch (e) {} 194 | super.onRemove(); 195 | } 196 | 197 | static void clearCache() => _compiledLayersCache.clear(); 198 | } 199 | -------------------------------------------------------------------------------- /lib/src/components/layers/cell_trail_layer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flame/components.dart'; 5 | import 'package:flame/extensions.dart'; 6 | import 'package:flame/rendering.dart'; 7 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 8 | 9 | class _FalseCacheKey extends LayerCacheKey { 10 | @override 11 | int? get key => null; 12 | 13 | @override 14 | void add(Component component) {} 15 | 16 | @override 17 | void invalidate() {} 18 | } 19 | 20 | class CellTrailLayer extends CellStaticLayer { 21 | CellTrailLayer( 22 | super.cell, { 23 | super.name, 24 | FadeOutConfig? fadeOutConfig, 25 | }) : super( 26 | componentsStorageMode: LayerComponentsStorageMode.removeAfterCompile, 27 | ) { 28 | this.fadeOutConfig = fadeOutConfig ?? FadeOutConfig(); 29 | } 30 | 31 | final _falseCacheKey = _FalseCacheKey(); 32 | 33 | @override 34 | LayerCacheKey get cacheKey => _falseCacheKey; 35 | 36 | bool get isFadeOut => fadeOutConfig.isFadeOut; 37 | 38 | /// This opacity level is barely visible so there is no reason to keep image 39 | /// in memory 40 | bool get noTrail => _calculatedOpacity < 0.01; 41 | 42 | late FadeOutConfig fadeOutConfig; 43 | 44 | double _calculatedOpacity = 1; 45 | double _fadeOutDt = 0; 46 | double _operationsCount = 0; 47 | 48 | bool _imageRenderInProgress = false; 49 | 50 | bool get doFadeOut => 51 | fadeOutConfig.isFadeOut && 52 | _fadeOutDt * 1000000 >= fadeOutConfig.fadeOutTimeout.inMicroseconds; 53 | 54 | @override 55 | void remove(Component component, {bool internalCall = false}) {} 56 | 57 | @override 58 | FutureOr? add(Component component) { 59 | if (component is HasTrailSupport) { 60 | component.addedToTrailLayer = true; 61 | } 62 | return super.add(component); 63 | } 64 | 65 | @override 66 | void compileToSingleLayer(Iterable components) { 67 | if (isRemovedLayer) { 68 | return; 69 | } 70 | final cell = currentCell; 71 | if (cell == null) { 72 | return; 73 | } 74 | 75 | if (noTrail && nonRenewableComponents.isEmpty) { 76 | isUpdateNeeded = false; 77 | return; 78 | } 79 | if (_imageRenderInProgress) { 80 | return; 81 | } 82 | _imageRenderInProgress = true; 83 | _updateLayerPictureWithFade(); 84 | final newComponentsPicture = _drawNewComponents(); 85 | 86 | final recorder = PictureRecorder(); 87 | final canvas = Canvas(recorder); 88 | if (layerPicture != null) { 89 | canvas.drawPicture(layerPicture!); 90 | } 91 | canvas.drawPicture(newComponentsPicture); 92 | newComponentsPicture.dispose(); 93 | 94 | layerPicture?.dispose(); 95 | layerPicture = recorder.endRecording(); 96 | if (_operationsCount >= fadeOutConfig.operationsLimitToSavePicture) { 97 | final imageSize = layerCalculatedSize; 98 | var recorder = PictureRecorder(); 99 | var canvas = Canvas(recorder); 100 | final decorator = Transform2DDecorator(); 101 | decorator.transform2d.position = correctionTopLeft * -1; 102 | decorator.applyChain( 103 | (canvas) { 104 | canvas.drawPicture(layerPicture!); 105 | }, 106 | canvas, 107 | ); 108 | final newPicture = recorder.endRecording(); 109 | final newImage = newPicture.toImageSync( 110 | imageSize.width.toInt(), 111 | imageSize.height.toInt(), 112 | ); 113 | newPicture.dispose(); 114 | 115 | recorder = PictureRecorder(); 116 | canvas = Canvas(recorder); 117 | canvas.drawImage(newImage, correctionTopLeft.toOffset(), paint); 118 | layerPicture!.dispose(); 119 | layerPicture = recorder.endRecording(); 120 | newImage.dispose(); 121 | if (renderMode == LayerRenderMode.image) { 122 | layerImage = layerPicture!.toImageSync( 123 | imageSize.width.toInt() + correctionTopLeft.x.ceil(), 124 | imageSize.height.toInt() + correctionTopLeft.y.ceil(), 125 | ); 126 | } 127 | _operationsCount = 0; 128 | } 129 | _imageRenderInProgress = false; 130 | } 131 | 132 | void _updateLayerPictureWithFade() { 133 | if (!doFadeOut) { 134 | return; 135 | } 136 | final recorder = PictureRecorder(); 137 | final canvas = Canvas(recorder); 138 | final fadeOutDecorator = 139 | fadeOutConfig.createDecorator(_fadeOutDt) as _FadeOutDecorator; 140 | _calculatedOpacity = _calculatedOpacity * fadeOutDecorator.opacity; 141 | 142 | fadeOutDecorator.applyChain(_drawOldPicture, canvas); 143 | 144 | _fadeOutDt = 0; 145 | layerPicture = recorder.endRecording(); 146 | } 147 | 148 | Picture _drawNewComponents() { 149 | final recorder = PictureRecorder(); 150 | final canvas = Canvas(recorder); 151 | if (nonRenewableComponents.isNotEmpty) { 152 | for (final component in nonRenewableComponents) { 153 | if (component is! PositionComponent || component is BoundingHitbox) { 154 | component.removeFromParent(); 155 | continue; 156 | } 157 | if (component is HasTrailSupport) { 158 | component.renderCalledFromTrailLayer = true; 159 | component.decorator.applyChain(component.render, canvas); 160 | component.renderCalledFromTrailLayer = false; 161 | } else { 162 | component.decorator.applyChain(component.render, canvas); 163 | } 164 | component.removeFromParent(); 165 | } 166 | _calculatedOpacity = 1; 167 | } 168 | nonRenewableComponents.clear(); 169 | _operationsCount++; 170 | return recorder.endRecording(); 171 | } 172 | 173 | @override 174 | void render(Canvas canvas) { 175 | if (layerPicture != null && !noTrail) { 176 | canvas.drawPicture(layerPicture!); 177 | } 178 | if (debugMode) { 179 | _renderDebugCell(canvas); 180 | } 181 | } 182 | 183 | void _renderDebugCell(Canvas canvas) { 184 | final cell = currentCell; 185 | if (cell == null) { 186 | return; 187 | } 188 | final rect = Rect.fromLTWH(0, 0, size.x, size.y); 189 | canvas.drawRect( 190 | rect, 191 | Paint()..color = const Color.fromRGBO(255, 255, 255, 0.2), 192 | ); 193 | } 194 | 195 | void _drawOldPicture(Canvas canvas) { 196 | if (layerPicture != null) { 197 | canvas.drawPicture(layerPicture!); 198 | } 199 | } 200 | 201 | @override 202 | void updateTree(double dt) { 203 | if (nonRenewableComponents.isNotEmpty) { 204 | isUpdateNeeded = true; 205 | updateLayer(dt); 206 | return; 207 | } 208 | 209 | if (noTrail) { 210 | if (layerPicture != null) { 211 | layerPicture!.dispose(); 212 | layerPicture = null; 213 | } 214 | if (layerImage != null) { 215 | layerImage!.dispose(); 216 | layerImage = null; 217 | } 218 | return; 219 | } 220 | 221 | if (fadeOutConfig.isFadeOut) { 222 | _fadeOutDt += dt; 223 | isUpdateNeeded = true; 224 | if (doFadeOut) { 225 | updateLayer(dt); 226 | } 227 | } 228 | } 229 | 230 | @override 231 | void onResume(double dtElapsedWhileSuspended) { 232 | _fadeOutDt += dtElapsedWhileSuspended; 233 | isUpdateNeeded = true; 234 | } 235 | 236 | @override 237 | bool onCheckCache(int key) => false; 238 | } 239 | 240 | class FadeOutConfig { 241 | FadeOutConfig({ 242 | double transparencyPerStep = 0, 243 | this.fadeOutTimeout = Duration.zero, 244 | this.operationsLimitToSavePicture = 3, 245 | }) { 246 | this.transparencyPerStep = transparencyPerStep; 247 | } 248 | 249 | Duration fadeOutTimeout; 250 | double _transparencyPerStep = 1; 251 | 252 | double get transparencyPerStep => _transparencyPerStep; 253 | 254 | set transparencyPerStep(double value) { 255 | assert( 256 | value >= 0 && value <= 1, 257 | 'Transparency must be between 0.0 and 1.0', 258 | ); 259 | _transparencyPerStep = value; 260 | } 261 | 262 | double operationsLimitToSavePicture; 263 | 264 | bool get isFadeOut => 265 | transparencyPerStep > 0 && fadeOutTimeout != Duration.zero; 266 | 267 | Decorator createDecorator(double dt) { 268 | final steps = (dt * 1000000) / fadeOutTimeout.inMicroseconds; 269 | final opacity = 1 - transparencyPerStep * steps; 270 | return _FadeOutDecorator(opacity, steps); 271 | } 272 | } 273 | 274 | class _FadeOutDecorator extends Decorator { 275 | _FadeOutDecorator(double opacity, this.steps) { 276 | this.opacity = opacity; 277 | _paint.isAntiAlias = false; 278 | _paint.filterQuality = FilterQuality.none; 279 | } 280 | 281 | final _paint = Paint(); 282 | 283 | double _opacity = 1; 284 | final double steps; 285 | 286 | set opacity(double value) { 287 | if (value <= 0) { 288 | _opacity = 0; 289 | } else { 290 | _opacity = value; 291 | } 292 | _paint.color = _paint.color.withOpacity(_opacity); 293 | } 294 | 295 | double get opacity => _opacity; 296 | 297 | @override 298 | void apply(void Function(Canvas) draw, Canvas canvas) { 299 | canvas.saveLayer(null, _paint); 300 | draw(canvas); 301 | canvas.restore(); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /lib/src/components/layers/has_trail_support.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/extensions.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | mixin HasTrailSupport on HasGridSupport { 6 | @internal 7 | bool addedToTrailLayer = false; 8 | @internal 9 | bool renderCalledFromTrailLayer = false; 10 | 11 | @override 12 | void render(Canvas canvas) { 13 | if (!addedToTrailLayer || 14 | (addedToTrailLayer && renderCalledFromTrailLayer)) { 15 | super.render(canvas); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/components/layers/layers_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flame/components.dart'; 4 | import 'package:flame/extensions.dart'; 5 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | typedef CustomLayerBuilder = CellLayer Function( 9 | PositionComponent component, 10 | Cell cell, 11 | String layerName, 12 | LayerComponentsStorageMode componentsStorageMode, 13 | ); 14 | 15 | class LayersRootComponent extends Component with UpdateOnDemand {} 16 | 17 | /// The class provides easy-to-use API layer to access game layer's 18 | /// Every layer is added into [layersRootComponent] to optimize priority 19 | /// recalculation. 20 | /// 21 | class LayersManager { 22 | LayersManager(this.game); 23 | 24 | @internal 25 | final layers = HashMap>(); 26 | 27 | HasSpatialGridFramework game; 28 | 29 | final layersRootComponent = {}; 30 | 31 | CustomLayerBuilder? customLayerBuilder; 32 | 33 | /// Adding manually created [CellLayer] into [layersRootComponent]. 34 | /// Usually there is no need to use this function, try [addComponent] instead. 35 | void addLayer(CellLayer layer) { 36 | final cell = layer.currentCell; 37 | if (cell == null) { 38 | throw 'layer must have a cell'; 39 | } 40 | layer.game = game; 41 | if (layers[cell] == null) { 42 | layers[cell] = HashMap(); 43 | } 44 | layers[cell]?[layer.name] = layer; 45 | 46 | var storage = layersRootComponent[layer.priority]; 47 | if (storage == null) { 48 | storage = layersRootComponent[layer.priority] = LayersRootComponent(); 49 | storage.priority = layer.priority; 50 | game.rootComponent.add(storage); 51 | } 52 | storage.add(layer); 53 | } 54 | 55 | /// Removes layer from game tree. 56 | /// Usually there is no need to manually remove any layer. Each layer is 57 | /// managed by the Framework and will be automatically removed in cell's 58 | /// removal or if the layer become empty, without components inside. 59 | void removeLayer({required String name, required Cell cell}) { 60 | final cellLayers = layers[cell]; 61 | if (cellLayers == null) { 62 | return; 63 | } 64 | final layer = cellLayers.remove(name); 65 | layer?.removeFromParent(); 66 | if (cellLayers.isEmpty) { 67 | layers.remove(cell); 68 | } 69 | } 70 | 71 | /// Gets a layer by it's unique [name] and [cell]. 72 | CellLayer? getLayer({required String name, required Cell cell}) => 73 | layers[cell]?[name]; 74 | 75 | /// Most useful function for end-user usage. It adds the [component] into 76 | /// new or existing layer with unique [layerName]. [layerType] enum is the 77 | /// parameter for layer's factory, which type will the new layer be. See 78 | /// [MapLayerType] for future details. 79 | /// Change [priority] to set whole layer's priority. Please note that each 80 | /// [addComponent] call will rewrite it's value to last one. 81 | /// If your component have position in cell's relative coordinate space, 82 | /// change [absolutePosition] to false. 83 | /// If you layer does not contain any collideable components, it is 84 | /// recommended to switch [optimizeCollisions] parameter to 'false'. 85 | /// Change [componentsStorageMode] to 86 | /// [LayerComponentsStorageMode.removeAfterCompile] if you are sure that 87 | /// components will newer be changed, added or removed to the layer. 88 | CellLayer addComponent({ 89 | required PositionComponent component, 90 | required MapLayerType layerType, 91 | required String layerName, 92 | Cell? currentCell, 93 | bool absolutePosition = true, 94 | bool optimizeCollisions = true, 95 | LayerRenderMode renderMode = LayerRenderMode.auto, 96 | LayerComponentsStorageMode componentsStorageMode = 97 | LayerComponentsStorageMode.defaultComponentTree, 98 | int? priority, 99 | }) { 100 | Cell? cell; 101 | if (currentCell == null && component is HasGridSupport) { 102 | cell = component.currentCell ?? game.findCellForComponent(component); 103 | } else if (currentCell != null) { 104 | cell = currentCell; 105 | } 106 | if (cell == null) { 107 | throw 'The "component" should be "HasGridSupport" subtype or ' 108 | '"currentCell" parameter should be passed.'; 109 | } 110 | var layer = getLayer(name: layerName, cell: cell); 111 | final isNew = layer == null; 112 | switch (layerType) { 113 | case MapLayerType.static: 114 | if (isNew) { 115 | layer = CellStaticLayer( 116 | cell, 117 | name: layerName, 118 | componentsStorageMode: componentsStorageMode, 119 | ); 120 | } 121 | break; 122 | case MapLayerType.animated: 123 | if (component is! SpriteAnimationComponent) { 124 | throw 'Component ${component.runtimeType} ' 125 | 'must be SpriteAnimationComponent!'; 126 | } 127 | if (isNew) { 128 | layer = CellStaticAnimationLayer( 129 | cell, 130 | name: layerName, 131 | componentsStorageMode: componentsStorageMode, 132 | ); 133 | } 134 | break; 135 | case MapLayerType.trail: 136 | if (isNew) { 137 | layer = CellTrailLayer( 138 | cell, 139 | name: layerName, 140 | ); 141 | } 142 | break; 143 | case MapLayerType.custom: 144 | if (isNew) { 145 | if (customLayerBuilder == null) { 146 | throw 'Trying to build custom layer without builder being specified'; 147 | } 148 | layer = customLayerBuilder!.call( 149 | component, 150 | cell, 151 | layerName, 152 | componentsStorageMode, 153 | ); 154 | } 155 | break; 156 | } 157 | layer.spatialGrid = game.spatialGrid; 158 | layer.game = game; 159 | 160 | 161 | if (absolutePosition) { 162 | component.position 163 | .setFrom(component.position - cell.rect.topLeft.toVector2()); 164 | } 165 | 166 | if (layerType == MapLayerType.trail) { 167 | layer.add(component); 168 | } else { 169 | if (component.isMounted) { 170 | component.parent = layer; 171 | } else { 172 | layer.add(component); 173 | } 174 | } 175 | 176 | if (isNew) { 177 | layer.priority = priority ?? component.priority; 178 | addLayer(layer); 179 | layer.optimizeCollisions = optimizeCollisions; 180 | layer.renderMode = renderMode; 181 | } 182 | 183 | return layer; 184 | } 185 | 186 | Future waitForComponents() { 187 | final futures = []; 188 | for (final cellLayerList in layers.values) { 189 | for (final layer in cellLayerList.values) { 190 | futures.add(layer.waitForComponents()); 191 | } 192 | } 193 | return Future.wait(futures); 194 | } 195 | 196 | void rescanLayersForUpdate() { 197 | for (final entry in layersRootComponent.entries) { 198 | entry.value.isUpdateNeeded = true; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/src/components/layers/scheduled_layer_operation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @immutable 5 | class ScheduledLayerOperation { 6 | const ScheduledLayerOperation({ 7 | required this.cellLayer, 8 | required this.compileToSingleLayer, 9 | required this.optimizeCollisions, 10 | required this.buildMacroObjects, 11 | this.stateAfterOperation, 12 | }); 13 | 14 | final CellLayer cellLayer; 15 | final bool optimizeCollisions; 16 | final bool compileToSingleLayer; 17 | final bool buildMacroObjects; 18 | final CellState? stateAfterOperation; 19 | 20 | void run() { 21 | if (cellLayer.isRemovedLayer) { 22 | return; 23 | } 24 | Future? future; 25 | if (optimizeCollisions) { 26 | future = cellLayer.collisionOptimizer.optimize(); 27 | } 28 | if (compileToSingleLayer) { 29 | cellLayer.compileToSingleLayer(cellLayer.components); 30 | cellLayer.postCompileActions(); 31 | } 32 | if (stateAfterOperation != null && 33 | cellLayer.currentCell?.state != stateAfterOperation) { 34 | if (future == null) { 35 | cellLayer.currentCell?.setStateInternal(stateAfterOperation!); 36 | } else { 37 | future.then((_) { 38 | cellLayer.currentCell?.setStateInternal(stateAfterOperation!); 39 | if (buildMacroObjects) { 40 | cellLayer.collisionOptimizer.buildMacroObjects(); 41 | } 42 | }); 43 | } 44 | } else if (buildMacroObjects && future != null) { 45 | future.then((_) { 46 | cellLayer.collisionOptimizer.buildMacroObjects(); 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/components/macro_object.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | 3 | abstract interface class MacroObjectInterface { 4 | Vector2 get macroSize; 5 | 6 | Vector2 get macroPosition; 7 | } 8 | 9 | class MacroObject { 10 | MacroObject({required this.size, required this.position}); 11 | 12 | final Vector2 size; 13 | final Vector2 position; 14 | 15 | int _index = 0; 16 | 17 | void _expandTo(MacroObject other) {} 18 | } 19 | 20 | /// хз мне ничего тут не нравится. надо подумать и переписать. 21 | /// Идея в том, чтобы было хранилице объектов, чтобы по хитбоксам 22 | /// раскидать только ссылки, и в случае мерджа двух объектов просто 23 | /// подменялись ссылки 24 | 25 | class MacroObjectsStorage { 26 | final List _objects = []; 27 | final _lastIndex = 0; 28 | 29 | void add(MacroObject object) { 30 | _objects.add(object); 31 | object._index = _objects.length - 1; 32 | } 33 | 34 | void mergeObjects(MacroObject one, MacroObject two) { 35 | one._expandTo(two); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/components/restorable_state_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | 4 | mixin RestorableStateMixin on HasGridSupport { 5 | T? get userData; 6 | 7 | TileBuilderContext? context; 8 | TileCache? tileCache; 9 | 10 | @override 11 | void onSpatialGridInitialized() { 12 | final cell = currentCell; 13 | if (context == null && cell != null) { 14 | TileDataProvider? tileDataProvider; 15 | if (tileCache != null) { 16 | tileDataProvider = 17 | TileDataProvider(tileCache!.tile, tileCache!.tileset, tileCache); 18 | } 19 | context = TileBuilderContext( 20 | absolutePosition: Anchor.center.toOtherAnchorPosition( 21 | boundingBox.aabbCenter, 22 | anchor, 23 | size, 24 | ), 25 | userData: userData, 26 | tileDataProvider: tileDataProvider, 27 | size: size, 28 | cellRect: cell.rect, 29 | contextProvider: cell.spatialGrid.game!.tileBuilderContextProvider, 30 | layerInfo: LayerInfo('game', 0), 31 | ); 32 | game.tileBuilderContextProvider.addContext(context!); 33 | } 34 | } 35 | 36 | void updateContext() { 37 | final cell = currentCell; 38 | if (cell == null) { 39 | throw 'Can not update context without cell'; 40 | } 41 | final ctx = context; 42 | if (ctx == null || ctx.removed) { 43 | return; 44 | } 45 | ctx.size.setFrom(size); 46 | ctx.absolutePosition.setFrom( 47 | Anchor.center.toOtherAnchorPosition( 48 | boundingBox.aabbCenter, 49 | anchor, 50 | size, 51 | ), 52 | ); 53 | ctx.cellRect = cell.rect; 54 | } 55 | 56 | @override 57 | void onRemove() { 58 | updateContext(); 59 | super.onRemove(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/components/tile_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flame/components.dart'; 4 | import 'package:flame/sprite.dart'; 5 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 6 | 7 | class TileComponent extends SpriteComponent 8 | with HasGridSupport, UpdateOnDemand 9 | implements SpriteAnimationComponent { 10 | static Future fromProvider(TileDataProvider provider) async { 11 | final cache = TileCache( 12 | sprite: await provider.getSprite(), 13 | spriteAnimation: await provider.getSpriteAnimation(), 14 | properties: provider.tile.properties, 15 | tile: provider.tile, 16 | tileset: provider.tileset, 17 | ); 18 | return TileComponent(cache); 19 | } 20 | 21 | TileComponent(this.tileCache) { 22 | paint.isAntiAlias = false; 23 | paint.filterQuality = FilterQuality.none; 24 | } 25 | 26 | final TileCache tileCache; 27 | 28 | @override 29 | Sprite? get sprite => tileCache.sprite; 30 | 31 | @override 32 | SpriteAnimation? get animation => tileCache.spriteAnimation; 33 | 34 | @override 35 | bool playing = true; 36 | 37 | @override 38 | bool removeOnFinish = false; 39 | 40 | @override 41 | set animation(SpriteAnimation? animation) {} 42 | 43 | @override 44 | SpriteAnimationTicker? get animationTicker => 45 | tileCache.spriteAnimation?.createTicker(); 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/components/utility/action_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @internal 5 | class ActionNotifier extends ChangeNotifier { 6 | bool _isActionNeeded = true; 7 | 8 | set isActionNeeded(bool doAction) { 9 | _isActionNeeded = doAction; 10 | if (doAction) { 11 | notifyListeners(); 12 | } 13 | } 14 | 15 | bool get isActionNeeded => _isActionNeeded; 16 | } -------------------------------------------------------------------------------- /lib/src/components/utility/debug_component.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flame/components.dart'; 4 | import 'package:flame/image_composition.dart'; 5 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class SpatialGridDebugComponent extends PositionComponent 9 | with HasPaint { 10 | SpatialGridDebugComponent(this.spatialGrid) : super(priority: 10000); 11 | 12 | final SpatialGrid spatialGrid; 13 | bool drawOutOfBoundsCounter = false; 14 | 15 | @override 16 | void updateTree(double dt) {} 17 | 18 | @override 19 | FutureOr onLoad() { 20 | const opacity = 0.5; 21 | final paintFill = Paint(); 22 | paintFill.style = PaintingStyle.fill; 23 | paintFill.color = Colors.lightGreen.withOpacity(opacity); 24 | setPaint('fill', paintFill); 25 | 26 | final paintFillInactive = Paint(); 27 | paintFillInactive.style = PaintingStyle.fill; 28 | paintFillInactive.color = Colors.blueGrey.withOpacity(opacity); 29 | setPaint('inactive', paintFillInactive); 30 | 31 | final paintFillUnloaded = Paint(); 32 | paintFillUnloaded.style = PaintingStyle.fill; 33 | paintFillUnloaded.color = Colors.black54.withOpacity(opacity); 34 | setPaint('unloaded', paintFillUnloaded); 35 | 36 | final paintFillBroken = Paint(); 37 | paintFillBroken.style = PaintingStyle.fill; 38 | paintFillBroken.color = Colors.orange.withOpacity(opacity); 39 | setPaint('broken', paintFillBroken); 40 | 41 | final paintBorder = Paint(); 42 | paintBorder.style = PaintingStyle.stroke; 43 | paintBorder.color = Colors.redAccent.withOpacity(opacity); 44 | paintBorder.strokeWidth = 1; 45 | setPaint('border', paintBorder); 46 | 47 | return super.onLoad(); 48 | } 49 | 50 | @override 51 | void render(Canvas canvas) { 52 | final textPaintOK = TextPaint( 53 | style: const TextStyle( 54 | fontSize: 5.0, 55 | color: Colors.purple, 56 | fontWeight: FontWeight.bold, 57 | ), 58 | ); 59 | final fill = getPaint('fill'); 60 | final inactive = getPaint('inactive'); 61 | final unloaded = getPaint('unloaded'); 62 | final broken = getPaint('broken'); 63 | final border = getPaint('border'); 64 | for (final element in spatialGrid.cells.entries) { 65 | if (element.value.state == CellState.active) { 66 | if (element.value.hasOutOfBoundsComponents && drawOutOfBoundsCounter) { 67 | canvas.drawRect(element.key, broken); 68 | textPaintOK.render( 69 | canvas, 70 | element.value.outOfBoundsCounter.toString(), 71 | element.key.center.toVector2(), 72 | ); 73 | } else { 74 | canvas.drawRect(element.key, fill); 75 | } 76 | } else if (element.value.state == CellState.inactive) { 77 | if (element.value.hasOutOfBoundsComponents && drawOutOfBoundsCounter) { 78 | canvas.drawRect(element.key, broken); 79 | textPaintOK.render( 80 | canvas, 81 | element.value.outOfBoundsCounter.toString(), 82 | element.key.center.toVector2(), 83 | ); 84 | } else { 85 | canvas.drawRect(element.key, inactive); 86 | } 87 | } else { 88 | if (element.value.hasOutOfBoundsComponents && drawOutOfBoundsCounter) { 89 | canvas.drawRect(element.key, broken); 90 | textPaintOK.render( 91 | canvas, 92 | element.value.outOfBoundsCounter.toString(), 93 | element.key.center.toVector2(), 94 | ); 95 | } else { 96 | canvas.drawRect(element.key, unloaded); 97 | } 98 | } 99 | canvas.drawRect(element.key, border); 100 | if (element.value.rawLeft != null) { 101 | final pos = Vector2( 102 | element.key.left + 2, 103 | element.key.bottom - element.key.size.height / 2, 104 | ); 105 | textPaintOK.render(canvas, 'L', pos); 106 | } 107 | if (element.value.rawRight != null) { 108 | final pos = Vector2( 109 | element.key.right - 8, 110 | element.key.bottom - element.key.size.height / 2, 111 | ); 112 | textPaintOK.render(canvas, 'R', pos); 113 | } 114 | if (element.value.rawTop != null) { 115 | final pos = Vector2( 116 | element.key.left + element.key.size.width / 2, 117 | element.key.top + 2, 118 | ); 119 | textPaintOK.render(canvas, 'T', pos); 120 | } 121 | if (element.value.rawBottom != null) { 122 | final pos = Vector2( 123 | element.key.left + element.key.size.width / 2, 124 | element.key.bottom - 15, 125 | ); 126 | textPaintOK.render(canvas, 'B', pos); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/components/utility/on_demand_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/extensions.dart'; 3 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 4 | import 'package:flame_spatial_grid/src/components/utility/action_notifier.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | 7 | mixin RepaintOnDemand on Component { 8 | final _repaintNotifier = ActionNotifier(); 9 | 10 | ChangeNotifier get repaintNotifier => _repaintNotifier; 11 | 12 | bool get isRepaintNeeded => _repaintNotifier.isActionNeeded; 13 | 14 | set isRepaintNeeded(bool repaint) { 15 | _repaintNotifier.isActionNeeded = repaint; 16 | } 17 | 18 | @override 19 | void renderTree(Canvas canvas) { 20 | if (isRepaintNeeded) { 21 | _repaintNotifier.isActionNeeded = false; 22 | super.renderTree(canvas); 23 | } 24 | } 25 | } 26 | 27 | mixin UpdateOnDemand on Component { 28 | final _updateNotifier = ActionNotifier(); 29 | 30 | ChangeNotifier get updateNotifier => _updateNotifier; 31 | 32 | bool get isUpdateNeeded => _updateNotifier.isActionNeeded; 33 | 34 | set isUpdateNeeded(bool update) { 35 | _updateNotifier.isActionNeeded = update; 36 | } 37 | 38 | @override 39 | void updateTree(double dt) { 40 | if (isUpdateNeeded) { 41 | _updateNotifier.isActionNeeded = false; 42 | super.updateTree(dt); 43 | } 44 | } 45 | } 46 | 47 | mixin ComponentWithUpdate on HasGridSupport {} 48 | -------------------------------------------------------------------------------- /lib/src/components/utility/pure_type_check_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 2 | 3 | abstract interface class PureTypeCheckInterface { 4 | /// Provides components type check to filter components at 5 | /// broadphase during collision detection. 6 | /// 7 | /// This is alternative to [HasSpatialGridFramework] method `pureTypeCheck`. 8 | /// It allows you to keep collision rules in component scope 9 | /// but have additional cost for performance. 10 | bool pureTypeCheck(Type other); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/components/utility/scheduler/action_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/src/components/utility/scheduler/scheduler.dart'; 2 | 3 | typedef ScheduledActionFunction = void Function( 4 | double dt, 5 | ScheduledActionType type, 6 | bool permanent, 7 | ); 8 | 9 | class ScheduledActionProvider { 10 | ScheduledActionProvider({ 11 | required this.scheduler, 12 | required this.actionFunction, 13 | }); 14 | 15 | final ActionScheduler scheduler; 16 | final ScheduledActionFunction actionFunction; 17 | 18 | final _scheduled = {}; 19 | 20 | bool runningAction = false; 21 | 22 | void scheduleFunction( 23 | ScheduledActionType type, 24 | ScheduledActionFunction actionFunctionCallback, 25 | ) { 26 | final actionProvider = ScheduledActionProvider( 27 | scheduler: scheduler, 28 | actionFunction: actionFunctionCallback, 29 | ); 30 | actionProvider.scheduleAction(type, false); 31 | } 32 | 33 | void scheduleAction(ScheduledActionType type, bool permanent) { 34 | if (!isScheduled(type, permanent)) { 35 | scheduler.add(this, type, permanent); 36 | _scheduled[type] = permanent; 37 | } 38 | } 39 | 40 | void removeAction(ScheduledActionType type, bool permanent) { 41 | if (isScheduled(type, permanent)) { 42 | scheduler.remove(this, type, permanent); 43 | _scheduled.remove(type); 44 | } 45 | } 46 | 47 | bool isScheduled(ScheduledActionType type, bool permanent) { 48 | return _scheduled.containsKey(type); 49 | } 50 | 51 | void onScheduledAction( 52 | double dt, 53 | ScheduledActionType type, 54 | bool permanent, 55 | ) { 56 | actionFunction(dt, type, permanent); 57 | } 58 | 59 | void onDisposeActionProvider() { 60 | for (final type in _scheduled.keys) { 61 | scheduler.remove(this, type, _scheduled[type]!); 62 | } 63 | _scheduled.clear(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/components/utility/scheduler/scheduler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/src/components/utility/scheduler/action_provider.dart'; 2 | 3 | enum ScheduledActionType { 4 | afterUpdate, 5 | beforeUpdate, 6 | afterLogic, 7 | beforeLogic, 8 | } 9 | 10 | class ActionScheduler { 11 | final _scheduledAfterLogic = []; 12 | final _scheduledAfterLogicPermanent = []; 13 | 14 | final _scheduledBeforeLogic = []; 15 | final _scheduledBeforeLogicPermanent = []; 16 | 17 | final _scheduledAfterUpdate = []; 18 | final _scheduledAfterUpdatePermanent = []; 19 | 20 | final _scheduledBeforeUpdate = []; 21 | final _scheduledBeforeUpdatePermanent = []; 22 | 23 | void add( 24 | ScheduledActionProvider provider, 25 | ScheduledActionType type, 26 | bool permanent, 27 | ) { 28 | switch (type) { 29 | case ScheduledActionType.afterUpdate: 30 | if (permanent) { 31 | _scheduledAfterUpdatePermanent.add(provider); 32 | } else { 33 | _scheduledAfterUpdate.add(provider); 34 | } 35 | break; 36 | case ScheduledActionType.beforeUpdate: 37 | if (permanent) { 38 | _scheduledBeforeUpdatePermanent.add(provider); 39 | } else { 40 | _scheduledBeforeUpdate.add(provider); 41 | } 42 | break; 43 | case ScheduledActionType.afterLogic: 44 | if (permanent) { 45 | _scheduledAfterLogicPermanent.add(provider); 46 | } else { 47 | _scheduledAfterLogic.add(provider); 48 | } 49 | break; 50 | case ScheduledActionType.beforeLogic: 51 | if (permanent) { 52 | _scheduledBeforeLogicPermanent.add(provider); 53 | } else { 54 | _scheduledBeforeLogic.add(provider); 55 | } 56 | break; 57 | } 58 | } 59 | 60 | void remove( 61 | ScheduledActionProvider provider, 62 | ScheduledActionType type, 63 | bool permanent, 64 | ) { 65 | final list = _getStorageByType(type, permanent); 66 | list.remove(provider); 67 | } 68 | 69 | void runActions(double dt, ScheduledActionType type) { 70 | final list = _getStorageByType(type, false); 71 | for (final item in list) { 72 | item.runningAction = true; 73 | item.onScheduledAction(dt, type, false); 74 | item.runningAction = false; 75 | } 76 | list.clear(); 77 | 78 | final listPermanent = _getStorageByType(type, true); 79 | for (final item in listPermanent) { 80 | item.runningAction = true; 81 | item.onScheduledAction(dt, type, true); 82 | item.runningAction = false; 83 | } 84 | } 85 | 86 | List _getStorageByType( 87 | ScheduledActionType type, 88 | bool permanent, 89 | ) { 90 | switch (type) { 91 | case ScheduledActionType.afterUpdate: 92 | return permanent 93 | ? _scheduledAfterUpdatePermanent 94 | : _scheduledAfterUpdate; 95 | case ScheduledActionType.beforeUpdate: 96 | return permanent 97 | ? _scheduledBeforeUpdatePermanent 98 | : _scheduledBeforeUpdate; 99 | case ScheduledActionType.afterLogic: 100 | return permanent ? _scheduledAfterLogicPermanent : _scheduledAfterLogic; 101 | case ScheduledActionType.beforeLogic: 102 | return permanent 103 | ? _scheduledBeforeLogicPermanent 104 | : _scheduledBeforeLogic; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/components/utility/scheduler/with_action_provider_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/src/components/utility/scheduler/action_provider.dart'; 2 | import 'package:flame_spatial_grid/src/components/utility/scheduler/scheduler.dart'; 3 | 4 | mixin WithActionProviderMixin { 5 | ScheduledActionProvider? _scheduledActionProvider; 6 | 7 | ScheduledActionProvider get scheduledActionProvider => 8 | _scheduledActionProvider!; 9 | 10 | void initActionProvider(ScheduledActionProvider provider) { 11 | if (_scheduledActionProvider != null) { 12 | _scheduledActionProvider!.onDisposeActionProvider(); 13 | } 14 | _scheduledActionProvider = provider; 15 | } 16 | 17 | void onScheduledAction( 18 | double dt, 19 | ScheduledActionType type, 20 | bool permanent, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/core/flame_override/base_game.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | 4 | class SpatialGridBaseGame extends FlameGameEx 5 | with HasSpatialGridFramework { 6 | SpatialGridBaseGame({ 7 | super.children, 8 | super.world, 9 | super.camera, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/core/flame_override/component_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flame/components.dart'; 4 | 5 | extension OnLoadGameReference on Component { 6 | FutureOr add(Component component) { 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /lib/src/core/flame_override/component_tree_root_ex.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/game.dart'; 3 | import 'package:flame/src/components/core/component_tree_root.dart'; 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | /// **ComponentTreeRoot** is a component that can be used as a root node of a 8 | /// component tree. 9 | /// 10 | /// This class is just a regular [Component], with some additional 11 | /// functionality, namely: it contains global lifecycle events for the component 12 | /// tree. 13 | class ComponentTreeRootEx extends ComponentTreeRoot { 14 | ComponentTreeRootEx({ 15 | super.children, 16 | super.key, 17 | }); 18 | 19 | 20 | @override 21 | @internal 22 | void enqueueAdd(Component child, Component parent) { 23 | if(child is HasGridSupport && this is FlameGame) { 24 | child.game = this as FlameGame; 25 | } 26 | super.enqueueAdd(child, parent); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/src/core/flame_override/flame_game_ex.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flame/components.dart'; 5 | import 'package:flame/game.dart'; 6 | import 'package:flame/src/components/core/component_tree_root.dart'; 7 | import 'package:flame/src/devtools/dev_tools_service.dart'; 8 | import 'package:flame/src/effects/provider_interfaces.dart'; 9 | import 'package:flame/src/game/game.dart'; 10 | import 'package:flame_spatial_grid/src/core/flame_override/component_tree_root_ex.dart'; 11 | import 'package:flutter/foundation.dart'; 12 | import 'package:meta/meta.dart'; 13 | 14 | /// This is a more complete and opinionated implementation of [Game]. 15 | /// 16 | /// [FlameGame] can be extended to add your game logic, or you can keep the 17 | /// logic in child [Component]s. 18 | /// 19 | /// This is the recommended base class to use for most games made with Flame. 20 | /// It is based on the Flame Component System (also known as FCS). 21 | class FlameGameEx extends ComponentTreeRootEx 22 | with Game 23 | implements ReadOnlySizeProvider, FlameGame { 24 | FlameGameEx({ 25 | super.children, 26 | W? world, 27 | CameraComponent? camera, 28 | }) : assert( 29 | world != null || W == World, 30 | 'The generics type $W does not conform to the type of ' 31 | '${world?.runtimeType ?? 'World'}.', 32 | ), 33 | _world = world ?? World() as W, 34 | _camera = camera ?? CameraComponent() { 35 | assert( 36 | Component.staticGameInstance == null, 37 | '$this instantiated, while another game ${Component.staticGameInstance} ' 38 | 'declares itself to be a singleton', 39 | ); 40 | 41 | if (kDebugMode) { 42 | DevToolsService.initWithGame(this as FlameGame); 43 | } 44 | 45 | _camera.world = _world; 46 | add(_camera); 47 | add(_world); 48 | } 49 | 50 | /// The [World] that the [camera] is rendering. 51 | /// Inside of this world is where most of your components should be added. 52 | /// 53 | /// You don't have to add the world to the tree after setting it here, it is 54 | /// done automatically. 55 | @override 56 | W get world => _world; 57 | 58 | @override 59 | set world(W newWorld) { 60 | if (newWorld == _world) { 61 | return; 62 | } 63 | _world.removeFromParent(); 64 | camera.world = newWorld; 65 | _world = newWorld; 66 | if (_world.parent == null) { 67 | add(_world); 68 | } 69 | } 70 | 71 | W _world; 72 | 73 | /// The component that is responsible for rendering your [world]. 74 | /// 75 | /// In this component you can set different viewports, viewfinders, follow 76 | /// components, set bounds for where the camera can move etc. 77 | /// 78 | /// You don't have to add the CameraComponent to the tree after setting it 79 | /// here, it is done automatically. 80 | /// 81 | /// When setting the camera, if it doesn't already have a world it will be 82 | /// set to match the game's world. 83 | @override 84 | CameraComponent get camera => _camera; 85 | 86 | @override 87 | set camera(CameraComponent newCameraComponent) { 88 | _camera.removeFromParent(); 89 | _camera = newCameraComponent; 90 | if (_camera.parent == null) { 91 | add(_camera); 92 | } 93 | _camera.world ??= world; 94 | } 95 | 96 | CameraComponent _camera; 97 | 98 | @override 99 | @internal 100 | late final List notifiers = []; 101 | 102 | /// This is overwritten to consider the viewport transformation. 103 | /// 104 | /// Which means that this is the logical size of the game screen area as 105 | /// exposed to the canvas after viewport transformations. 106 | /// 107 | /// This does not match the Flutter widget size; for that see [canvasSize]. 108 | @override 109 | Vector2 get size => camera.viewport.virtualSize; 110 | 111 | @override 112 | @internal 113 | FutureOr load() async { 114 | await super.load(); 115 | setLoaded(); 116 | } 117 | 118 | @override 119 | @internal 120 | void mount() { 121 | super.mount(); 122 | setMounted(); 123 | } 124 | 125 | @override 126 | @internal 127 | void finalizeRemoval() { 128 | super.finalizeRemoval(); 129 | setRemoved(); 130 | } 131 | 132 | /// This implementation of render renders each component, making sure the 133 | /// canvas is reset for each one. 134 | /// 135 | /// You can override it further to add more custom behavior. 136 | /// Beware of that if you are rendering components without using this method; 137 | /// you must be careful to save and restore the canvas to avoid components 138 | /// interfering with each others rendering. 139 | @override 140 | @mustCallSuper 141 | void render(Canvas canvas) { 142 | if (parent == null) { 143 | renderTree(canvas); 144 | } 145 | } 146 | 147 | @override 148 | void renderTree(Canvas canvas) { 149 | if (parent != null) { 150 | render(canvas); 151 | } 152 | for (final component in children) { 153 | component.renderTree(canvas); 154 | } 155 | } 156 | 157 | @override 158 | @mustCallSuper 159 | void update(double dt) { 160 | if (parent == null) { 161 | updateTree(dt); 162 | } 163 | } 164 | 165 | @override 166 | void updateTree(double dt) { 167 | processLifecycleEvents(); 168 | if (parent != null) { 169 | update(dt); 170 | } 171 | for (final component in children) { 172 | component.updateTree(dt); 173 | } 174 | processRebalanceEvents(); 175 | } 176 | 177 | /// This passes the new size along to every component in the tree via their 178 | /// [Component.onGameResize] method, enabling each one to make their decision 179 | /// of how to handle the resize event. 180 | /// 181 | /// It also updates the [size] field of the class to be used by later added 182 | /// components and other methods. 183 | /// You can override it further to add more custom behavior, but you should 184 | /// seriously consider calling the super implementation as well. 185 | @override 186 | @mustCallSuper 187 | void onGameResize(Vector2 size) { 188 | super.onGameResize(size); 189 | // This work-around is needed since the camera has the highest priority and 190 | // [size] uses [viewport.virtualSize], so the viewport needs to be updated 191 | // first since users will be using `game.size` in their [onGameResize] 192 | // methods. 193 | camera.viewport.onGameResize(size); 194 | // [onGameResize] is declared both in [Component] and in [Game]. Since 195 | // there is no way to explicitly call the [Component]'s implementation, 196 | // we propagate the event to [FlameGame]'s children manually. 197 | handleResize(size); 198 | for (final child in children) { 199 | child.onParentResize(size); 200 | } 201 | } 202 | 203 | /// Ensure that all pending tree operations finish. 204 | /// 205 | /// This is mainly intended for testing purposes: awaiting on this future 206 | /// ensures that the game is fully loaded, and that all pending operations 207 | /// of adding the components into the tree are fully materialized. 208 | /// 209 | /// Warning: awaiting on a game that was not fully connected will result in an 210 | /// infinite loop. For example, this could occur if you run `x.add(y)` but 211 | /// then forget to mount `x` into the game. 212 | @override 213 | Future ready() async { 214 | var repeat = true; 215 | while (repeat) { 216 | // Give chance to other futures to execute first 217 | await Future.delayed(Duration.zero); 218 | repeat = false; 219 | processLifecycleEvents(); 220 | repeat |= hasLifecycleEvents; 221 | } 222 | } 223 | 224 | /// Whether a point is within the boundaries of the visible part of the game. 225 | @override 226 | bool containsLocalPoint(Vector2 point) { 227 | return point.x >= 0 && 228 | point.y >= 0 && 229 | point.x < canvasSize.x && 230 | point.y < canvasSize.y; 231 | } 232 | 233 | /// Returns the current time in seconds with microseconds precision. 234 | /// 235 | /// This is compatible with the `dt` value used in the [update] method. 236 | @override 237 | double currentTime() { 238 | return DateTime.now().microsecondsSinceEpoch.toDouble() / 239 | Duration.microsecondsPerSecond; 240 | } 241 | 242 | /// Returns a [ComponentsNotifier] for the given type [W]. 243 | /// 244 | /// This method handles duplications, so there will never be 245 | /// more than one [ComponentsNotifier] for a given type, meaning 246 | /// that this method can be called as many times as needed for a type. 247 | @override 248 | ComponentsNotifier componentsNotifier() { 249 | for (final notifier in notifiers) { 250 | if (notifier is ComponentsNotifier) { 251 | return notifier; 252 | } 253 | } 254 | final notifier = ComponentsNotifier( 255 | descendants().whereType().toList(), 256 | ); 257 | notifiers.add(notifier); 258 | return notifier; 259 | } 260 | 261 | @override 262 | @internal 263 | void propagateToApplicableNotifiers( 264 | Component component, 265 | void Function(ComponentsNotifier) callback, 266 | ) { 267 | for (final notifier in notifiers) { 268 | if (notifier.applicable(component)) { 269 | callback(notifier); 270 | } 271 | } 272 | } 273 | 274 | /// Whether the game should pause when the app is backgrounded. 275 | /// 276 | /// On the latest Flutter stable at the time of writing (3.13), 277 | /// this is only working on Android and iOS. 278 | /// 279 | /// Defaults to true. 280 | @override 281 | bool pauseWhenBackgrounded = true; 282 | bool _pausedBecauseBackgrounded = false; 283 | 284 | @override 285 | @mustCallSuper 286 | void lifecycleStateChange(AppLifecycleState state) { 287 | switch (state) { 288 | case AppLifecycleState.resumed: 289 | case AppLifecycleState.inactive: 290 | if (_pausedBecauseBackgrounded) { 291 | resumeEngine(); 292 | } 293 | case AppLifecycleState.paused: 294 | case AppLifecycleState.detached: 295 | case AppLifecycleState.hidden: 296 | if (pauseWhenBackgrounded && !paused) { 297 | pauseEngine(); 298 | _pausedBecauseBackgrounded = true; 299 | } 300 | } 301 | } 302 | 303 | @override 304 | void pauseEngine() { 305 | _pausedBecauseBackgrounded = false; 306 | super.pauseEngine(); 307 | } 308 | 309 | @override 310 | void resumeEngine() { 311 | _pausedBecauseBackgrounded = false; 312 | super.resumeEngine(); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /lib/src/core/vector2_simd.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flame/components.dart'; 4 | 5 | extension Vector2SIMD on Vector2 { 6 | Float64x2 toFloat64x2() => Float64x2(x, y); 7 | 8 | static Vector2 fromFloat64x2(Float64x2 source) => Vector2(source.x, source.y); 9 | } 10 | 11 | extension Float64x2AsVector on Float64x2 { 12 | Float64x2 clone() => Float64x2(x, y); 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/tiled/sprite_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/components.dart'; 2 | import 'package:flame/extensions.dart'; 3 | import 'package:tiled/tiled.dart'; 4 | 5 | extension SpriteLoader on Tile { 6 | Future getSprite(Tileset tileset) { 7 | final src = image?.source ?? tileset.image?.source; 8 | if (src == null) { 9 | throw 'Cant load sprite without image'; 10 | } 11 | 12 | final drawRect = tileset.computeDrawRect(this); 13 | final position = Vector2(drawRect.left.toDouble(), drawRect.top.toDouble()); 14 | final size = Vector2(drawRect.width.toDouble(), drawRect.height.toDouble()); 15 | 16 | return Sprite.load(src, srcPosition: position, srcSize: size); 17 | } 18 | 19 | Future getSpriteAnimation(Tileset tileset) async { 20 | if (animation.isEmpty) { 21 | return null; 22 | } 23 | final src = image?.source ?? tileset.image?.source; 24 | if (src == null) { 25 | throw 'Cant load sprite without image'; 26 | } 27 | 28 | final spriteList = []; 29 | final stepTimes = []; 30 | 31 | final futures = []; 32 | for (final frame in animation) { 33 | final frameTile = Tile(localId: frame.tileId); 34 | final future = frameTile.getSprite(tileset).then((sprite) { 35 | spriteList.add(sprite); 36 | stepTimes.add(frame.duration / 1000); 37 | return sprite; 38 | }); 39 | futures.add(future); 40 | } 41 | 42 | return Future.wait(futures).then((value) { 43 | return SpriteAnimation.variableSpriteList( 44 | spriteList, 45 | stepTimes: stepTimes, 46 | ); 47 | }); 48 | } 49 | 50 | Rect? getCollisionRect() { 51 | final group = objectGroup; 52 | final type = group?.type; 53 | if (type == LayerType.objectGroup && group is ObjectGroup) { 54 | if (group.objects.isNotEmpty) { 55 | final obj = group.objects.first; 56 | return Rect.fromLTWH(obj.x, obj.y, obj.width, obj.height); 57 | } 58 | } 59 | return null; 60 | } 61 | } 62 | 63 | extension ConvertToAnimation on Sprite { 64 | SpriteAnimation toAnimation() => 65 | SpriteAnimation.spriteList([this], stepTime: 100000); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/tiled/tile_builder_context.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/image_composition.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | import 'package:flame_tiled/flame_tiled.dart'; 4 | 5 | /// This class represents the tile's data and the cell's context in which this 6 | /// tile was built 7 | /// [absolutePosition] and [size] represents tile's global position on the game 8 | /// field and it's dimensions. 9 | /// If this context contains information about tiled object, [tiledObject] will 10 | /// be not null. 11 | /// If the context contains information about tile, the [tileDataProvider] 12 | /// will be not null. 13 | /// Use these properties to build custom components for map's tiles or objects. 14 | /// 15 | /// 16 | class TileBuilderContext { 17 | TileBuilderContext({ 18 | this.tileDataProvider, 19 | this.tiledObject, 20 | this.userData, 21 | required this.absolutePosition, 22 | required this.size, 23 | required this.cellRect, 24 | required this.contextProvider, 25 | required this.layerInfo, 26 | }); 27 | 28 | TileBuilderContextProvider contextProvider; 29 | T? userData; 30 | 31 | Rect cellRect; 32 | 33 | ///Tile's position in the global game's coordinates space 34 | Vector2 absolutePosition; 35 | 36 | ///Tiles width and height 37 | Vector2 size; 38 | 39 | /// Tile's most wanted information: the sprite or animation, collision rect 40 | final TileDataProvider? tileDataProvider; 41 | 42 | String get tileTypeName => tileDataProvider?.tile.type ?? ''; 43 | 44 | /// Tiled object's information, if object was processed instead a tile 45 | final TiledObject? tiledObject; 46 | int? priorityOverride; 47 | final LayerInfo layerInfo; 48 | 49 | /// The cell in which the tile should be placed 50 | Cell? get cell { 51 | final providerOwner = contextProvider.parent; 52 | if (providerOwner is TiledMapLoader) { 53 | return providerOwner.game.spatialGrid.cells[cellRect]; 54 | } else if (providerOwner is HasSpatialGridFramework) { 55 | return providerOwner.spatialGrid.cells[cellRect]; 56 | } 57 | return null; 58 | } 59 | 60 | /// If the tile should be removed in the next map load operation. Useful in 61 | /// cause you implementing a destructible game environment and want to 62 | /// preserve you changes between cells unload and restoration 63 | bool get removed => _removed; 64 | bool _removed = false; 65 | 66 | void remove() { 67 | if (cell != null) { 68 | final contextList = contextProvider.getContextListForCell(cell!); 69 | contextList?.remove(this); 70 | _removed = true; 71 | } 72 | } 73 | } 74 | 75 | class LayerInfo { 76 | LayerInfo(this.name, this.priority); 77 | 78 | final String name; 79 | final int priority; 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/tiled/tile_builder_context_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | 6 | class TileBuilderContextProvider { 7 | TileBuilderContextProvider({required this.parent}); 8 | 9 | /// It should be [HasSpatialGridFramework] instance or [TiledMapLoader] 10 | /// instance 11 | final T parent; 12 | 13 | final _contextByCellRect = HashMap>>(); 14 | 15 | HashSet>? getContextListForCell(Cell cell) => 16 | getContextListForCellRect(cell.rect); 17 | 18 | HashSet>? getContextListForCellRect(Rect rect) => 19 | _contextByCellRect[rect]; 20 | 21 | void addContext(TileBuilderContext context) { 22 | var list = HashSet>(); 23 | list = _contextByCellRect[context.cellRect] ??= list; 24 | list.add(context); 25 | } 26 | 27 | void clearContextStorage() => _contextByCellRect.clear(); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/tiled/tile_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flame/components.dart'; 4 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 5 | import 'package:meta/meta.dart'; 6 | import 'package:tiled/tiled.dart'; 7 | 8 | /// This class is a storage of tile's data from tileset. 9 | /// Use [TiledMapLoader.getPreloadedTileData] to get instance of this class. 10 | /// Also read about [TiledMapLoader.preloadTileSets] 11 | @immutable 12 | class TileCache { 13 | TileCache({ 14 | this.sprite, 15 | this.spriteAnimation, 16 | required this.properties, 17 | required this.tile, 18 | required this.tileset, 19 | }) : _collisionRect = tile.getCollisionRect(); 20 | 21 | final Sprite? sprite; 22 | final SpriteAnimation? spriteAnimation; 23 | final CustomProperties properties; 24 | final Tile tile; 25 | final Tileset tileset; 26 | final Rect? _collisionRect; 27 | 28 | Rect? getCollisionRect() => _collisionRect; 29 | 30 | void dispose() { 31 | try { 32 | sprite?.image.dispose(); 33 | // ignore: avoid_catches_without_on_clauses 34 | } catch (_) {} 35 | 36 | final frames = spriteAnimation?.frames; 37 | if (frames != null) { 38 | for (final frame in frames) { 39 | try { 40 | frame.sprite.image.dispose(); 41 | // ignore: avoid_catches_without_on_clauses 42 | } catch (_) {} 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/tiled/tile_data_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flame/collisions.dart'; 5 | import 'package:flame/components.dart'; 6 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 7 | import 'package:flame_tiled/flame_tiled.dart'; 8 | 9 | /// Proxy class, simplifies access to tile's individual data 10 | /// 11 | /// Use [getSprite] to get [Sprite] object. 12 | /// Use [getSpriteAnimation] to get [SpriteAnimation] object of the tile. 13 | /// Use [getCollisionRect] to load [RectangleHitbox] if it had been specified 14 | /// in Tiled 15 | /// 16 | class TileDataProvider { 17 | TileDataProvider(this.tile, this.tileset, [this.cache]); 18 | 19 | Tile tile; 20 | Tileset tileset; 21 | TileCache? cache; 22 | 23 | Rect? getCollisionRect() { 24 | if (cache == null) { 25 | return tile.getCollisionRect(); 26 | } else { 27 | return cache!.getCollisionRect(); 28 | } 29 | } 30 | 31 | Future getSprite() { 32 | final sprite = cache?.sprite; 33 | if (sprite != null) { 34 | return Future.value(sprite); 35 | } else { 36 | return tile.getSprite(tileset); 37 | } 38 | } 39 | 40 | Future getSpriteAnimation() { 41 | final animation = cache?.spriteAnimation; 42 | if (animation != null) { 43 | return Future.value(animation); 44 | } else { 45 | return tile.getSpriteAnimation(tileset); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/tiled/tileset_manager.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: comment_references 2 | 3 | import 'dart:collection'; 4 | 5 | import 'package:flame/flame.dart'; 6 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 7 | import 'package:flame_spatial_grid/src/tiled/tileset_parser.dart'; 8 | import 'package:flame_tiled/flame_tiled.dart'; 9 | // ignore: implementation_imports 10 | import 'package:flame_tiled/src/tile_atlas.dart'; 11 | import 'package:meta/meta.dart'; 12 | 13 | class TilesetManager { 14 | static final _preloadedTileSet = 15 | HashMap>(); 16 | 17 | /// Use this function in tile builder to access tile's [Sprite] 18 | /// or [SpriteAnimation]. 19 | TileCache? getTile(String tileSetName, String tileType) => 20 | _preloadedTileSet[tileSetName]?[tileType]; 21 | 22 | Future loadTileset(String fileName) async { 23 | final tileSet = await TilesetParser.fromFile(fileName); 24 | return _populateCache(tileSet); 25 | } 26 | 27 | Future _populateCache(Tileset tileSet) async { 28 | final tilesetName = tileSet.name; 29 | if (tilesetName == null) { 30 | return; 31 | } 32 | final tilesetCache = 33 | _preloadedTileSet[tilesetName] ?? HashMap(); 34 | if (tilesetCache.isNotEmpty) { 35 | return; 36 | } 37 | for (final tile in tileSet.tiles) { 38 | final tileTypeName = tile.type; 39 | if (tileTypeName == null) { 40 | continue; 41 | } 42 | tilesetCache[tileTypeName] = TileCache( 43 | sprite: await tile.getSprite(tileSet), 44 | spriteAnimation: await tile.getSpriteAnimation(tileSet), 45 | properties: tile.properties, 46 | tileset: tileSet, 47 | tile: tile, 48 | ); 49 | } 50 | _preloadedTileSet[tilesetName] = tilesetCache; 51 | } 52 | 53 | @internal 54 | Future> addFromMap(TiledMap map) { 55 | final futures = []; 56 | for (final tileSet in map.tilesets) { 57 | futures.add(_populateCache(tileSet)); 58 | } 59 | return Future.wait(futures); 60 | } 61 | 62 | static void dispose() { 63 | Flame.images.clearCache(); 64 | TiledAtlas.clearCache(); 65 | for (final map in _preloadedTileSet.values) { 66 | for (final cache in map.values) { 67 | cache.dispose(); 68 | } 69 | map.clear(); 70 | } 71 | _preloadedTileSet.clear(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/tiled/tileset_parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/flame.dart'; 2 | import 'package:flame_tiled/flame_tiled.dart'; 3 | import 'package:xml/xml.dart'; 4 | 5 | class TilesetParser { 6 | static Future fromFile(String fileName) async { 7 | final contents = await Flame.bundle.loadString('assets/tiles/$fileName'); 8 | return parseTsx(contents); 9 | } 10 | 11 | static Tileset parseTsx(String xml) { 12 | final xmlElement = XmlDocument.parse(xml).rootElement; 13 | if (xmlElement.name.local != 'tileset') { 14 | throw 'XML is not in TSX format'; 15 | } 16 | final parser = XmlParser(xmlElement); 17 | return Tileset.parse( 18 | parser, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/tiled/world_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flame/flame.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | class WorldData { 7 | WorldData({ 8 | required this.type, 9 | required this.onlyShowAdjacentMaps, 10 | required this.maps, 11 | }); 12 | 13 | final List maps; 14 | final bool onlyShowAdjacentMaps; 15 | final String type; 16 | 17 | static Future fromFile(String fileName) async { 18 | final source = await Flame.bundle.loadString(fileName); 19 | final dynamic data = jsonDecode(source); 20 | final type = _getField(data, 'type') as String; 21 | final onlyShowAdjacentMaps = 22 | _getField(data, 'onlyShowAdjacentMaps') as bool; 23 | final mapsRaw = _getField(data, 'maps') as Iterable; 24 | final maps = []; 25 | for (final mapData in mapsRaw) { 26 | maps.add( 27 | WorldMapData( 28 | width: _getField(mapData, 'width') as int, 29 | height: _getField(mapData, 'height') as int, 30 | x: _getField(mapData, 'x') as int, 31 | y: _getField(mapData, 'y') as int, 32 | fileName: _getField(mapData, 'fileName') as String, 33 | ), 34 | ); 35 | } 36 | 37 | return WorldData( 38 | type: type, 39 | onlyShowAdjacentMaps: onlyShowAdjacentMaps, 40 | maps: maps, 41 | ); 42 | } 43 | 44 | static dynamic _getField(dynamic data, String fieldName) { 45 | // ignore: avoid_dynamic_calls 46 | final dynamic value = data[fieldName]; 47 | if (value == null) { 48 | throw 'Field "$fieldName" does not exists!'; 49 | } 50 | return value; 51 | } 52 | } 53 | 54 | @immutable 55 | class WorldMapData { 56 | const WorldMapData({ 57 | required this.width, 58 | required this.height, 59 | required this.x, 60 | required this.y, 61 | required this.fileName, 62 | }); 63 | 64 | final int width; 65 | final int height; 66 | final int x; 67 | final int y; 68 | final String fileName; 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/tiled/world_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame/extensions.dart'; 2 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 3 | 4 | typedef MapLoaderFactory = TiledMapLoader Function(); 5 | 6 | class WorldLoader { 7 | WorldLoader({ 8 | required this.fileName, 9 | required this.mapLoader, 10 | this.loadWholeMap = true, 11 | }); 12 | 13 | final String fileName; 14 | WorldData? worldData; 15 | final Map mapLoader; 16 | late final HasSpatialGridFramework game; 17 | final List _maps = []; 18 | 19 | final bool loadWholeMap; 20 | TiledMapLoader? currentMap; 21 | Rect? _previousMapRect; 22 | 23 | bool currentMapChanged = false; 24 | 25 | Future loadWorldData() async { 26 | worldData ??= await WorldData.fromFile('assets/tiles/$fileName'); 27 | } 28 | 29 | Future<(Vector2?, TiledMapLoader?)> searchInitialPosition( 30 | InitialPositionChecker checkFunction, 31 | ) async { 32 | await loadWorldData(); 33 | for (final map in maps) { 34 | final result = await map.searchInitialPosition(checkFunction, fileName); 35 | if (result != null) { 36 | return (result, map); 37 | } 38 | } 39 | return (null, null); 40 | } 41 | 42 | Future init(HasSpatialGridFramework game) async { 43 | this.game = game; 44 | await loadWorldData(); 45 | final futures = []; 46 | for (final map in maps) { 47 | futures.add(map.init(game)); 48 | } 49 | 50 | await Future.wait(futures); 51 | TiledMapLoader.loadedMaps.addAll(maps); 52 | } 53 | 54 | List get maps { 55 | if (_maps.isNotEmpty) { 56 | return _maps; 57 | } 58 | final data = worldData; 59 | if (data == null) { 60 | return _maps; 61 | } 62 | for (final map in data.maps) { 63 | var factory = mapLoader[map.fileName.replaceAll('.tmx', '')]; 64 | if (factory == null) { 65 | final genericMapLoader = mapLoader['all']; 66 | if (genericMapLoader != null) { 67 | factory = genericMapLoader; 68 | } else { 69 | continue; 70 | } 71 | } 72 | final loader = factory.call(); 73 | loader.initialPosition = Vector2(map.x.toDouble(), map.y.toDouble()); 74 | loader.fileName = map.fileName; 75 | _maps.add(loader); 76 | } 77 | 78 | return _maps; 79 | } 80 | 81 | TiledMapLoader? updateCurrentMap(Vector2 position) { 82 | for (final map in maps) { 83 | if (map.mapRect.containsPoint(position)) { 84 | if (_previousMapRect != map.mapRect) { 85 | _previousMapRect = currentMap?.mapRect; 86 | currentMapChanged = true; 87 | currentMap = map; 88 | } 89 | return map; 90 | } 91 | } 92 | currentMap = null; 93 | return null; 94 | } 95 | 96 | Set findNeighbourMaps() { 97 | final centralMap = currentMap; 98 | if (centralMap == null) { 99 | return {}; 100 | } 101 | 102 | final grid = SpatialGrid( 103 | cellSize: centralMap.mapRect.size, 104 | initialPosition: centralMap.mapRect.center.toVector2(), 105 | ); 106 | final centralCell = grid.currentCell; 107 | if (centralCell == null) { 108 | return {}; 109 | } 110 | 111 | final rectToCheck = List.of( 112 | [ 113 | centralCell.right.rect, 114 | centralCell.right.top.rect, 115 | centralCell.right.bottom.rect, 116 | centralCell.left.rect, 117 | centralCell.left.top.rect, 118 | centralCell.left.bottom.rect, 119 | centralCell.top.rect, 120 | centralCell.bottom.rect, 121 | ], 122 | growable: false, 123 | ); 124 | 125 | final neighbours = {}; 126 | for (final rect in rectToCheck) { 127 | for (final map in maps) { 128 | if (map.mapRect.overlaps(rect)) { 129 | neighbours.add(map); 130 | } 131 | } 132 | } 133 | return neighbours; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/ui/loading_progress_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flame_spatial_grid/flame_spatial_grid.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class LoadingProgressManager { 5 | LoadingProgressManager( 6 | this.type, 7 | this.game, { 8 | int? min, 9 | this.max = 100, 10 | }) : min = min ?? LoadingProgressManager.lastProgressMinimum; 11 | 12 | static int lastProgressMinimum = 0; 13 | 14 | final String type; 15 | HasSpatialGridFramework game; 16 | 17 | final int min; 18 | final int max; 19 | 20 | int _progress = 0; 21 | 22 | int get progress => _progress; 23 | 24 | void setProgress(int value, [M? message]) { 25 | final converted = _convertValueForSubProcess(value); 26 | _progress = converted; 27 | game.onLoadingProgress(LoadingProgressMessage(progress, type, message)); 28 | } 29 | 30 | int _convertValueForSubProcess(int value) { 31 | if (min == 0 && max == 100) { 32 | return value; 33 | } 34 | 35 | if (value == 0) { 36 | return min; 37 | } 38 | 39 | final diff = max - min; 40 | return min + (value * diff ~/ 100); 41 | } 42 | 43 | void resetProgress() { 44 | _progress = 0; 45 | lastProgressMinimum = 0; 46 | } 47 | } 48 | 49 | @immutable 50 | class LoadingProgressMessage { 51 | const LoadingProgressMessage(this.progress, this.type, this.data); 52 | 53 | final int progress; 54 | final String type; 55 | final M? data; 56 | } 57 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flame_spatial_grid 2 | description: Spatial partitioning of game field to improve performance of collision detection and rendering 3 | version: 0.9.2 4 | homepage: https://github.com/ASGAlex/flame_spatial_grid 5 | 6 | environment: 7 | sdk: '>=3.0.0 <4.0.0' 8 | 9 | dependencies: 10 | flame: ^1.24.0 11 | flame_tiled: ^2.0.0 12 | meta: ^1.11.0 13 | isolate_manager: ^5.7.1 14 | dart_bloom_filter: ^1.0.0 15 | flat_buffers: ^23.5.26 16 | vector_math: ^2.1.4 17 | collection: ^1.19.0 18 | tiled: ^0.11.0 19 | xml: ^6.5.0 20 | js: ^0.7.1 21 | 22 | flutter: 23 | sdk: flutter 24 | 25 | dev_dependencies: 26 | flutter_lints: ^2.0.0 27 | lints: ^2.0.0 28 | test: ^1.16.0 29 | --------------------------------------------------------------------------------