How to reset an animation in specific offset, when animation status is forwarding?

1.1k Views Asked by At

I want to design a simple game in which the ball hits the boxes and the user has to try to bring the ball up with the cursor. When the ball returns, end of ball movement, is the offset at the bottom of the screen, and I want to reset the animation if the ball offset equals the cursor and then give it a new direction, but that never happens.
game play
Please see the values I have printed.
Print values ​​when the ball is coming down.

532.0 is cursor.position.dy and others are positionBall.dy + renderBall.size.height.
Why only when the ball moves up (the moment I tap on the screen) the ball offset and the cursor offset are equal, but not in return?
---update---
When I increase the duration (for example, 10 seconds), or activate the Slow Animations button from the flutter inspector, the numbers get closer to each other, and by adjusting them to the int, the condition is made.

I/flutter (21563): 532.0
I/flutter (21563): 532.45585

I'm really confused and I do not know what is going on in the background.

  void initState() {
    super.initState();
    Offset init = initialBallPosition();
    final g = Provider.of<GameStatus>(context, listen: false);
    var key = ball.key;
    _animationController = AnimationController(duration: Duration(seconds: 1), vsync: this);
    _tweenOffset = Tween<Offset>(begin: init, end: init);
    _animationOffset = _tweenOffset.animate(
      CurvedAnimation(parent: _animationController, curve: Curves.linear),
    )..addListener(() {
        if (_animationController.isAnimating) {
          //if (_animationController.status == AnimationStatus.forward) {
          RenderBox renderBall = key.currentContext.findRenderObject();
          final positionBall = renderBall.localToGlobal(Offset.zero);
          print(cursor.position.dy);
          print(positionBall.dy + renderBall.size.height);
          if (positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270) {
            print('bang');
            colideWithCursor();
          }
        }
        if (_animationController.status == AnimationStatus.completed) {
          if (bottomOfBall().dy == Screen.screenHeight / ball.width) {
            gameOver();
          } else
            collision();
        }
      });
    _animationController.isDismissed;
  }

  @override
  Widget build(BuildContext context) {
    final game = Provider.of<GameStatus>(context, listen: false);

    return Selector<GameStatus, bool>(
        selector: (ctx, game) => game.firstShoot,
        builder: (context, startGame, child) {
          if (startGame) {
            game.ballDirection = 90;
            routing(game.ballDirection);
          }
          return UnconstrainedBox(child: (SlideTransition(position: _animationOffset, child: ball.createBall())));
        });
  }
2

There are 2 best solutions below

3
caseycrogers On

The two numbers are never exactly matching because the animation value is checked every frame and the overlap is occurring between frames.

You probably either want to add a tolerance (eg consider the values to have matched if they're within a certain amount) or create some interpolation logic where you check if the ball is about to collide with the cursor in-between the current frame and the next. eg replace:

positionBall.dy + renderBall.size.height == cursor.position.dy && g.ballDirection == 270

With:

positionBall.dy + renderBall.size.height + <current_speed_per_frame_of_ball> <= cursor.position.dy && g.ballDirection == 270

The important thing here is that the animations aren't actually fluid. An animation doesn't pass from 0.0 continuously through every conceivable value to 1.0. The value of the animation is only calculated when a frame is rendered so the values you'll actually get might be something along the lines of: 0.0, 0.14, 0.30, 0.44, 0.58....0.86, 0.99, 1.0. The exact values will depend on the duration of the animation and the exact times the Flutter framework renders each frame.

1
caseycrogers On

Since you asked (in the comments) for an example using onTick, here's an example app I wrote up for a ball that bounces randomly around the screen. You can tap to randomize it's direction and speed. Right now it kinda hurts your eyes because it's redrawing the ball in a new position on every frame.

You'd probably want to smoothly animate the ball between each change in direction (eg replace Positioned with AnimatedPositioned) to get rid of the eye-strain. This refactor is beyond what I have time to do.

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math.dart' hide Colors;

Random _rng = Random();

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  get randomizedDirection =>
      _randomDirectionWithVelocity((150 + _rng.nextInt(600)).toDouble());

  Ticker _ticker;
  Vector2 _initialDirection;
  Duration prevT = Duration.zero;

  BallModel _ballModel;

  @override
  void dispose() {
    super.dispose();
    _ticker.dispose();
  }

  void _init(Size size) {
    _ballModel = BallModel(
      Vector2(size.width / 2, size.height / 2),
      randomizedDirection,
      16.0,
    );
    _ticker = createTicker((t) {
      // This sets state and forces a rebuild on every frame. A good optimization would be
      // to only build when the ball changes direction and use AnimatedPositioned to fluidly
      // draw the ball between changes in direction.
      setState(() {
        _ballModel.updateBall(t - prevT, size);
      });
      prevT = t;
    });
    _ticker.start();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GestureDetector(
        child: Scaffold(
          body: LayoutBuilder(
            builder: (context, constraints) {
              // Initialize everything here because we need to access the constraints.
              if (_ticker == null) _init(constraints.biggest);
              return Stack(children: [
                Ball(_ballModel),
              ]);
            },
          ),
        ),
        onTap: () => setState(() => _ballModel.v = randomizedDirection),
      ),
    );
  }
}

class BallModel {
  // The current x,y position of the ball.
  Vector2 p;

  // The direction, including speed in pixels per second, of the ball
  Vector2 v;

  // The radius of the ball.
  double r;

  BallModel(this.p, this.v, this.r);

  void updateBall(Duration elapsed, Size size) {
    // Move the ball by v, scaled by what fraction of a second has passed
    // since the last frame.
    p = p + v * (elapsed.inMilliseconds / 1000);
    // If the ball overflows on a given dimension, correct the overflow and update v.
    var newX = _correctOverflow(p.x, r, 0, size.width);
    var newY = _correctOverflow(p.y, r, 0, size.height);
    if (newX != p.x) v.x = -v.x;
    if (newY != p.y) v.y = -v.y;
    p = Vector2(newX, newY);
  }
}

class Ball extends StatelessWidget {
  final BallModel b;

  Ball(this.b);

  @override
  Widget build(BuildContext context) {
    return Positioned(
        left: b.p.x - b.r,
        bottom: b.p.y - b.r,
        child: DecoratedBox(
            decoration:
                BoxDecoration(shape: BoxShape.circle, color: Colors.black)),
        width: 2 * b.r,
        height: 2 * b.r);
  }
}

double _correctOverflow(s, r, lowerBound, upperBound) {
  var underflow = s - r - lowerBound;
  // Reflect s across lowerBound.
  if (underflow < 0) return s - 2 * underflow;
  var overflow = s + r - upperBound;
  // Reflect s across upper bound.
  if (overflow > 0) return s - 2 * overflow;
  // No over or underflow, return s.
  return s;
}

Vector2 _randomDirectionWithVelocity(double velocity) {
  return Vector2(_rng.nextDouble() - .5, _rng.nextDouble() - 0.5).normalized() *
      velocity;
}

Writing game and physics logic from scratch gets really complicated really fast. I encourage you to use a game engine like Unity so that you don't have to build everything yourself. There's also a Flutter based game engine called flame that you could try out: https://github.com/flame-engine/flame.