{"id":13528680,"url":"https://github.com/kurtome/leap","last_synced_at":"2025-07-24T01:33:22.313Z","repository":{"id":59117487,"uuid":"451234197","full_name":"kurtome/leap","owner":"kurtome","description":"An opinionated toolkit for creating 2D platformers on top of the Flame engine.","archived":false,"fork":false,"pushed_at":"2025-05-05T03:36:42.000Z","size":7695,"stargazers_count":49,"open_issues_count":6,"forks_count":17,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-06-08T13:43:33.181Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/leap","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kurtome.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-01-23T21:22:28.000Z","updated_at":"2025-06-06T14:57:55.000Z","dependencies_parsed_at":"2023-12-26T03:29:40.087Z","dependency_job_id":"2f5a4b23-43de-4d9e-9ed7-00218da44834","html_url":"https://github.com/kurtome/leap","commit_stats":{"total_commits":12,"total_committers":1,"mean_commits":12.0,"dds":0.0,"last_synced_commit":"e867c321bb5afee5d110ea5ec89aeb4d9551fec4"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/kurtome/leap","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurtome%2Fleap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurtome%2Fleap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurtome%2Fleap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurtome%2Fleap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kurtome","download_url":"https://codeload.github.com/kurtome/leap/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurtome%2Fleap/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266779288,"owners_count":23982896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T07:00:22.712Z","updated_at":"2025-07-24T01:33:22.292Z","avatar_url":"https://github.com/kurtome.png","language":"Dart","funding_links":[],"categories":["Plugins \u0026 Libraries"],"sub_categories":["Examples"],"readme":"# Leap\n\n[![Pub](https://img.shields.io/pub/v/leap.svg?style=popout)](https://pub.dev/packages/leap)\n[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-black.svg)](https://flame-engine.org)\n\nAn opinionated toolkit for creating 2D platformers on top of the\n[Flame engine](https://flame-engine.org/).\n\nJoin the\n[`#leap` channel in the Flame Discord to discuss](https://discord.gg/mTNKQGjh9K).\n\n## WARNING library under development\n\nBe aware that this is still under development and is likely to change\nfrequently, every release could introduce breaking changes up until a `v1.0.0`\nrelease (which may never happen as this is a solo endeavour currently).\n\n## Features\n\n### Level creation via Tiled\n\nLeap uses Tiled tile maps not just for visually rendering the level, but also\nfor imbuing behavior and terrain in the level by creating corresponding Flame\ncomponents automatically from the map's layers.\n\n### Physics\n\nThe crux of this physics engine is based on this post The guide to implementing\n2D platformers:\nhttp://higherorderfun.com/blog/2012/05/20/the-guide-to-implementing-2d-platformers/\n\nThe \"Type #2: Tile Based (Smooth)\" section outlines the overall algorithm.\n\nNote that Leap doesn't use Flame's collision detection system in favor of one\nthat is more specialized and efficient for tile based platformers where every\nhitbox is an axis-aligned bounding box, and special handling can be done for\ntile grid aligned components (such as ground terrain).\n\n#### Efficient collision detection\n\n![image](docs/images/megaman_tile_grid.png)\n\nEssentially all physical objects (`PhsyicalComponent`) in the game have\naxis-aligned bounding boxes (AABBs) for hitboxes, determined by their `size` and\n`position`. The hitbox doesn't necessarily need to match the visual size of the\ncomponent.\n\n✅ Supported tile platformer features:\n\n- Ground terrain\n- One way platforms\n- Slopes\n- Moving platforms\n- Ladders\n\n🚧 Future tile platformer features:\n\n- Friction control for ground tiles (for ice, etc.)\n- Interactive ground tiles\n- Removable ground tiles\n- One way walls\n\n#### Simple physics designed for 2D platformers\n\nLong story short: physics engines like `box2d` are great for emulating realistic\nphysics and terrible for implementing retro-style 2d platformers which are not\nremotely realistic. In order to get the snappy jumps and controls required for a\nresponsive platformer a much more rudimentary physics engine is required.\n\nIn Leap, physical entities have a `velocity` attribute for storing the current\n`x` and `y` velocity, which will automatically update the entity's position. A\nmoving entity colliding with the level terrain will automatically have its\nvelocity set to `0` and position updated to be kept outside the terrain to\nprevent overlap. There is also a global `gravity` rate applied to the `y`\nvelocity every game tick. Static entities will never be moved by velocity or\ngravity.\n\n## Getting started\n\nBefore using Leap, you should be familiar with the following Flame components:\n\n- FlameGame\n- CameraComponent\n- PositionComponent\n- TiledComponent\n\n## Usage\n\n### LeapGame\n\nTo use Leap, your game instance must extend `LeapGame` (which in turn extends\n`FlameGame`). It's recommended to use `game.loadWorldAndMap` to initialize the\ngame's `world` and `map`.\n\n### LeapWorld\n\nAccessible via `LeapGame.world`, this component manages any global logic\nnecessary for the physics engine.\n\n### LeapMap\n\nAccessible via `LeapGame.map`, this component manages the Tiled map and\nautomatically constructs the tiles with proper collision detection for the\nground terrain. See [Tiled map integration](#Tiled map integration) below\n\n### Game code snippet\n\nSee [the standard_platformer example](examples/standard_platformer) for complete\ngame code.\n\n```dart\nvoid main() {\n  runApp(GameWidget(game: MyLeapGame()));\n}\n\nclass MyLeapGame extends LeapGame with HasTappables, HasKeyboardHandlerComponents {\n  late final Player player;\n\n  @override\n  Future\u003cvoid\u003e onLoad() async {\n    await super.onLoad();\n\n    // \"map.tmx\" should be a Tiled map that meets the Leap requirements defined below\n    await loadWorldAndMap('map.tmx', 16);\n    setFixedViewportInTiles(32, 16);\n\n    player = Player();\n    add(player);\n    camera.followComponent(player);\n  }\n}\n```\n\n### PhyiscalEntity physics system\n\nThe physics system for Leap requires that every `Component` that interacts with\nthe game's phyical world extend `PhysicalEntity` and be added to the `LeapWorld`\ncomponent which is accessible via `LeapGame.world`.\n\n#### Characters\n\n`Character` is a `PhysicalEntity` which is intended to be used as the parent\nclass for players, enemies, and possibly even objects in your game. It has the\nconcept of `health` and an `onDeath` function that can be overridden for when\nhealth reaches zero (or negative). It's also possible to set\n`removeOnDeath = true` to automatically remove the character from the game when\nit dies.\n\n##### Entity animations and behaviors\n\nEntities are typically rendered visually as a `SpriteAnimation`, however most\nlikely there is a different animation for different states of the character.\nThat's where `AnchoredAnimationGroup` comes in.\n\nA `AnchoredAnimationGroup` is a specialized `SpriteAnimationGroupComponent`, so\nyou can set all the animations as map on the component and then update the\ncurrent animation with the key in the map for the correct animation.\n\nTypically you want to make use of this by making a subclass of\n`AnchoredAnimationGroup` so all the logic relevant to picking the current\nanimation is self contained.\n\nFor example:\n\n```dart\nclass Player extends JumperCharacter with HasAnimationGroup {\n    Player() {\n        // Behaviors that run before physics\n        ...\n        // Physics\n        add(GravityAccelerationBehavior());\n        add(CollisionDetectionBehavior());\n        // Behaviors that run after physics\n        ...\n        add(ApplyVelocityBehavior());\n        // Rendering related behaviors\n        add(AnimationVelocityFlipBehavior());\n\n        animationGroup = PlayerAnimation();\n    }\n}\n\nenum _AnimationState { walk, jump }\n\nclass PlayerAnimation extends AnchoredAnimationGroup\u003c_AnimationState, Player\u003e {\n    @override\n    Future\u003cvoid\u003e onLoad() async {\n        animations = {\n            _AnimationState.walk: SpriteAnimation(...),\n            _AnimationState.jump: SpriteAnimation(...),\n        };\n        return super.onLoad();\n    }\n\n    @override\n    void update(double dt) {\n       if (character.isWalking) {\n           current = _AnimationState.walking;\n       } else if (character.isJumping) {\n           current = _AnimationState.jumping;\n       }\n       super.update(dt);\n    }\n}\n```\n\n`AnchoredAnimationGroup` also automatically handles positioning the animation to\nbe centered on the parent's hitbox. The positioning can be changed with the\n`hitboxAnchor` property.\n\n`AnchoredAnimationGroup` must be added via the `HasAnimationGroup` mixin in a\n`PhysicalEntity` component, and typically is set to the `animationGroup`\nproperty as well.\n\n##### Death animations\n\nThe recommended way to handle death animations is to add the\n`RemoveOnDeathBehavior` or the `RemoveAfterDeathAnimationBehavior`. If you\ndepend on the death animation finishing, you will also need to:\n\n1. Have a `AnchoredAnimationGroup` set on the character, and make sure it sets\n   the `current` animation to whichever death animation you need.\n2. Make sure the death animation has `loop = false` so it doesn't play forever.\n3. Make sure the rest of your game doesn't interact with it as if it is still\n   alive. The recommened approach for this is to add a custom `Status` to it,\n   possibly with the `IgnoredByWorld` mixin on it. Or you can other entities\n   interacting with it can check `isDead` on it.\n\n#### Status effect system\n\n`PhysicalEntity` components can have statuses (`EntityStatus`) which modify\ntheir behavior. Statuses affect the component they are added to. For example,\nyou could implement a `StarPowerStatus` which when added to your player\ncomponent makes them flash colors become invincible.\n\nSince statuses are themselves components, they can maintain their own state and\nhandle updating themselves or their parent `PhysicalEntity` components. See\n`OnLadderStatus` for an example of this.\n\nThere are mixins on `EntityStatus` which affect the Leap engine's handling of\nthe parent `PhysicalEntity`. See:\n\n- `IgnoredByWorld`\n- `IgnoresVelocity`\n- `IgnoresGravity`\n- `IgnoredByCollisions`\n- `IgnoresAllCollisions`\n- `IgnoresNonSolidCollisions`\n- `IgnoresSolidCollisions`\n\nYou can implement your own mixins on `EntityStatus` which control pieces of\nlogic in your own game.\n\n### Resetting the map on player death\n\nIt's pretty common want to reset the map when the player dies. The recommended\npatter is to reload the map in the `Game` class and add:\n\n```dart\nclass MyGame {\n  ...\n\n  Future\u003cvoid\u003e reloadLevel() async {\n    await loadWorldAndMap(\n      tiledMapPath: 'map.tmx',\n      tiledObjectHandlers: tiledObjectHandlers,\n    );\n\n    // Don't let the camera move outside the bounds of the map, inset\n    // by half the viewport size to the edge of the camera if flush with the\n    // edge of the map.\n    final inset = camera.viewport.virtualSize;\n    camera.setBounds(\n      Rectangle.fromLTWH(\n        inset.x / 2,\n        inset.y / 2,\n        leapMap.width - inset.x,\n        leapMap.height - inset.y,\n      ),\n    );\n  }\n\n\n  @override\n  void onMapUnload(LeapMap map) {\n    player?.removeFromParent();\n  }\n\n  @override\n  void onMapLoaded(LeapMap map) {\n    if (player != null) {\n      world.add(player!);\n      player!.resetPosition();\n    }\n  }\n}\n```\n\nAnd make sure to call `game.reloadLevel()` from the `Player` when the player\ndies.\n\n### Tiled map integration\n\nLeap automatically parses specific features out of specific Tiled layers.\n\n#### Ground layer\n\nLayer must be a Tile Layer named \"Ground\", by default all tiles placed in this\nlayer are assumed to be ground terrain in the physics of the game. This means\nthese tiles will be statically positioned and have a hitbox that matches the\nwidth and height of the tile.\n\nSpecialized ground tiles:\n\n- **Slopes** for terrain the physical can walk up/down like a hill. These tiles\n  must have two custom `int` properties `LeftTop` and `RightTop`. For example, a\n  16x16 pixel tile with `LeftTop = 0` and `RightTop = 8` indicates slope that is\n  ascending when moving from left-to-right. Alternatively the tile can have\n  `LeftBottom` and `RightBottom` for pitched (sloped on the bottom) tile.\n- **One Way Platforms** for terrain the physical entities can move up (e.g.\n  jump) through from all but one direction. These are implemented via\n  `GroundTileHandler` classes, and can therefore use and `class` you want via\n  passing in a map of custom `groundTileHandlers` when loading the map (see\n  below). The most used is `OneWayTopPlatformHandler` which modifies the tile to\n  be phased through from below and the sides, but solid from the top.\n\n##### Custom ground tile handling\n\nTo have complete control over individual tiles in the ground layer, you can use\nthe `class` property in the Tiled editor tileset to hook into the\n`groundTileHandlers` you pass in when loading your map.\n\nIn your `LeapGame`:\n\n```dart\nawait loadWorldAndMap(\n  camera: camera,\n  tiledMapPath: 'map.tmx',\n  groundTileHandlers: {\n    'OneWayTopPlatform': OneWayTopPlatformHandler(),\n    'MyCustomTile': MyCustomTileHandler(),\n  },\n);\n```\n\nAnd your `MyCustomTileHandler`:\n\n```dart\nclass MyCustomTileHandler implements GroundTileHandler {\n  @override\n  LeapMapGroundTile handleGroundTile(LeapMapGroundTile groundTile, LeapMap map) {\n    tile.tags.add('PowerUpTile');\n\n    // Add some extra rendering on top of your special tile.\n    map.add(PowerUpTileAnimationComponent(x: groundTile.x, y: groundTile.y));\n\n    // use the provided tile instance in the map\n    return tile;\n  }\n}\n\n// OR\n\nclass MyCustomTileHandler implements GroundTileHandler {\n  @override\n  LeapMapGroundTile handleGroundTile(LeapMapGroundTile groundTile, LeapMap map) {\n    // MyCustomTile constructor must call the super constructor to initialize\n    // the the LeapMapGroundTile properties\n    return MyCustomTile(\n      groundTile,\n      myCustomProperty: groundTile.tile.properties.getValue\u003cint\u003e('PowerValue'),\n    );\n  }\n}\n```\n\nNote that the `class` property is **always** added to each tile's\n`PhysicalEntity.tags`. So, you can check if your player is walking into a\nspecial type of wall with something like this:\n\n```dart\nclass Player extends PhysicalEntity {\n\n  @override\n  void update(double dt) {\n    super.update(dt);\n\n    if (collisionInfo.right \u0026\u0026 // hitting solid entity on the right\n        input.actionButtonPressed \u0026\u0026 // custom input handling\n        collisionInfo.rightCollision!.tags.contains('MySpecialTile')) {\n      // right collision has a tag we set from Tiled's tileset `class` property\n      // (tag could also be added by your own custom handling)\n      specialInteraction(collisionInfo.rightCollision!);\n    }\n  }\n\n}\n```\n\n#### Metadata layer\n\nLayer must be an Object Group named \"Metadata\", used to place any objects to be\nused in your game like level start/end, enemy spawn points, anything you want.\n\n#### Object Group layers\n\nAny Object Group layer (including the Metadata layer) can include arbitrary\nobjects in them, if you wish to automatically create Flame Components for some\nof those objects you can do so based on the Class string in Tiled. All that is\nrequired is implementing the `TiledObjectFactory` interface and mapping each\nClass string you care about to a factory instance when loading the `LeapMap`,\nfor example...\n\nIn your `LeapGame`:\n\n```dart\nawait loadWorldAndMap(\n  camera: camera,\n  tiledMapPath: 'map.tmx',\n  tiledObjectHandlers: {\n    'Coin': await CoinFactory.createFactory(),\n  },\n);\n```\n\nYour custom factory:\n\n```dart\nclass CoinFactory implements TiledObjectFactory\u003cCoin\u003e {\n  late final SpriteAnimation spriteAnimation;\n\n  CoinFactory(this.spriteAnimation);\n\n  @override\n  void handleObject(TiledObject object, Layer layer, LeapMap map) {\n    final coin = Coin(object, spriteAnimation);\n    map.add(coin);\n  }\n\n  static Future\u003cCoinFactory\u003e createFactory() async {\n    final tileset = await Flame.images.load('my_animated_coin.png');\n    final spriteAnimation = SpriteAnimation.fromFrameData(\n      tileset,\n      SpriteAnimationData.sequenced(...),\n    );\n    return CoinFactory(spriteAnimation);\n  }\n}\n\nclass Coin extends PhysicalEntity {\n  Coin(TiledObject object, this.animation)\n      : super(static: true) {\n    anchor = Anchor.center;\n\n    // Use the position from your Tiled map\n    position = Vector2(object.x, object.y);\n\n    // Use custom properties from your Tiled object\n    value = tiledObject.properties.getValue\u003cint\u003e('CoinValue');\n  }\n\n  ...\n}\n```\n\n#### Other layers\n\nAny other layers will be rendered visually but have no impact on the game\nautomatically. You can add additional custom behavior by accessing the layers\nvia `LeapGame.map.tiledMap` and integrating your own special behavior for tiles\nor objects.\n\n#### Moving platforms\n\nTo create a moving platform, you need to implement your own component which\nextends `MovingPlatform` and provides a Sprite (or some other rendering).\n\nIf you choose to implement this component with a Tiled object (recommended),\nmany of the fields can be directly read from the object's custom properties:\n\n- `MoveSpeedX` (double), speed in tiles per second on the X axis\n- `MoveSpeedY` (double), speed in tiles per second on the y axis\n- `LoopMode` (string), one of `resetAndLoop`, `reverseAndLoop`, `none`\n- `TilePath` (string), a list of grid offsets to define the platforms path of\n  movement. For example, `0,-3;2,0` means the platform will move up 3 tiles and\n  then move right 2 tiles.\n\n#### Ladders\n\nTo create a ladder, you need to implement your own component which extends\n`Ladder` and provides a Sprite (or some other rendering).\n\nLadders also require custom integration with your components which are able to\nclimb the ladder. This is accomplished by adding an `OnLadderStatus` as a child\ncomponent, removing the child component will remove the component from the\nladder.\n\nFor example:\n\n```dart\nclass Player extends PhyiscalEntity {\n\n    void update(double dt) {\n        // These booleans are fabricated for this example,\n        // implement what makes senes for your own system.\n        if (isNearLadder \u0026\u0026 actionButton.isPressed) {\n            add(OnLadderStatus(ladder));\n        } else if (hasStatus\u003cOnLadderStatus\u003e() \u0026\u0026 jumpButton.isPressed) {\n            remove(getStatus\u003cOnLadderStatus\u003e());\n        }\n    }\n\n}\n```\n\nTo see a fully working example, see `Player` in `examples/standard_platformer`.\n\n#### Customizing layer names and classes\n\nEven though the structure explained above should always be followed, the\ndeveloper can ask Leap to use different classes, types, names.\n\nIn order to do so, a custom `LeapConfiguration` can be passed to the game.\n\nExample:\n\n```dart\nclass MyLeapGame extends LeapGame {\n  MyLeapGame() : super(\n    configuration: LeapConfiguration(\n      tiled: const TiledOptions(\n        groundLayerName: 'Ground',\n        metadataLayerName: 'Metadata',\n        playerSpawnClass: 'PlayerSpawn',\n        damageProperty: 'Damage',\n        platformClass: 'Platform',\n        slopeType: 'Slope',\n        slopeRightTopProperty: 'RightTop',\n        slopeLeftTopProperty: 'LeftTop',\n      ),\n    ),\n  );\n}\n```\n\n## Debugging\n\n### Slow motion\n\n`LeapWorld` includes\n[`HasTimeScale`](https://pub.dev/documentation/flame/latest/components/HasTimeScale-mixin.html),\nso you can set `world.timeScale = 0.5` to slow your whole game down to 50% speed\nto make it easier to play test nuanced bugs. (You can use this as slow motion\nfor your game too.)\n\n### Render hitbox\n\n`PhysicalEntity` includes a `debugHitbox` property you can set to automatically\ndraw a box indicating the exact hitbox the collision detection system is using\nfor your entity.\n\n```dart\nclass MyPlayer extends PhysicalEntity {\n\n  @override\n  void update(double dt) {\n    // Draw entity's hitbox\n    debugHitbox = true;\n  }\n\n}\n```\n\n### Render collisions\n\n`PhysicalEntity` includes a `debugCollisions` property you can set to\nautomatically draw a box indicating the hitbox of all other entities it is\ncurrently colliding with.\n\n```dart\nclass MyPlayer extends PhysicalEntity {\n\n  @override\n  void update(double dt) {\n    // Draw entity's collisions\n    debugCollisions = true;\n  }\n\n}\n```\n\n## Roadmap 🚧\n\n- Improved collision detection API.\n  - The current API is fairly awkward, see `CollisionInfo`.\n  - There is no great way to detect collision start or collision end.\n- Improved API for `PhysicalEntity`, `addImpulse` etc.\n- Lots of code clean-up to make usage of Leap more ergonomic and configurable.\n\n## Contributing\n\n1. Ensure any changes pass:\n   - `melos format`\n   - `melos analyze`\n   - `melos test`\n2. Start your PR title with a\n   [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) type\n   (feat:, fix: etc).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkurtome%2Fleap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkurtome%2Fleap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkurtome%2Fleap/lists"}