1

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 Answers2

1

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.

caseycrogers
  • 226
  • 2
  • 8
  • Thanks for the tips, but I do not understand the meaning of "current_speed_per_frame_of_ball". How can it be achieved or calculated? Could you explain more to me or give an example or link to read? Thanks again. – Rouhollah Torshizi Dec 09 '20 at 19:14
  • "current_speed_per_frame_of_ball" is how many units the ball moves between each frame. You need this to know if the ball will hit the cursor between the current frame and the next. A higher level note: using animations is probably going to be hacky for your project. They're really not intended for building game logic. You should probably either: A. Build your game in a game engine like Unity (you'll have a much better time) or B. Use lower level classes and manage state in an `onTick` callback. https://api.flutter.dev/flutter/widgets/SingleTickerProviderStateMixin-mixin.html – caseycrogers Dec 09 '20 at 23:37
  • I tried to use suggestion B, **Use lower level classes and manage state in an onTick callback.** but couldn't. for this ,i studied many articles from [flutter.dev](https://flutter.dev) many times. i don't know how to use method for mange an `onTick` callback. may be give a sample code to me? – Rouhollah Torshizi Dec 11 '20 at 11:26
0

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.

caseycrogers
  • 226
  • 2
  • 8
  • Thank you so much for your time for me. I will review ,study and working on the code you wrote, as well as the flame and unity engine. Thanks a lot. – Rouhollah Torshizi Dec 13 '20 at 19:22