fluttergame-physicsbox2dflame

Box2D: Projected Trajectory of a ball


I am trying to make a basketball shoot game. My issue is with my projected trajectory line. It seems to be pretty accurate when the initial velocity isn't above ~140. I have tried adjusting my formula a ton, I mostly just followed this tutorial: https://www.iforce2d.net/b2dtut/projected-trajectory

Here is the code; I am not sure if something is wrong with my formula or there is something I am missing in regards to Box2D within Flutter Flame (what I'm using to develop this)

import 'dart:async';
import 'package:bball_blast/Background.dart';
import 'package:bball_blast/entities/Hoop.dart';
import 'package:bball_blast/entities/Wall.dart';
import 'package:bball_blast/entities/ball.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:bball_blast/config.dart';

class BBallBlast extends Forge2DGame with PanDetector, HasGameRef<BBallBlast>{

  BBallBlast(): super(
      gravity: Vector2(0,gravity),
      camera: CameraComponent.withFixedResolution(width: gameWidth, height: gameHeight),
    );

  //vars we need to be visible thoughout entire file
  late Ball ball; 
  double linearImpulseStrengthMult = 12.5;
  late Vector2 impulse;

  //positional vars
  double startPosX = 00;
  double startPosY = 00;
  late Vector2 startPos;

  //Vars for determining how ball should be thrown
  late Offset startOfDrag;
  late Offset currentDragPos;
  late Offset dragBehindBall = Offset.zero;
  bool isDragging = false;

  bool isShot = false;

  @override
  FutureOr<void> onLoad() async {
    //set startPos of ball
    startPos = Vector2(startPosX, startPosY);

    //load images into cache
    await images.loadAllImages();

    //make ballSprite and ball
    Sprite ballImg = await loadSprite('ball.png');
    ball = Ball(this, Vector2(startPosX, startPosY), ballImg);

    //add leftWall and rightWall, and ceiling
    Wall wallLeft = Wall(Vector2(camera.visibleWorldRect.topLeft.dx-1, camera.visibleWorldRect.topLeft.dy), 1.0, gameHeight);
    Wall wallRight = Wall(Vector2(camera.visibleWorldRect.topRight.dx+1, camera.visibleWorldRect.topRight.dy), 1.0, gameHeight);
    Wall ceiling = Wall(Vector2(camera.visibleWorldRect.topLeft.dx-1, camera.visibleWorldRect.topRight.dy-1), gameWidth, 1.0);

    //create hoopimg, hoop, and add it
    Sprite hoopImg = await loadSprite('hoop.png');
    Hoop hoop = Hoop(this, true, hoopImg);

    //add components to game  
    world.addAll([Background(), ball, wallLeft, wallRight, ceiling, hoop]);

    debugMode=true;

    super.onLoad();
  }


  //-----------------------INPUT HANDLING (DRAGS)-----------------------
  @override
  void onPanStart(DragStartInfo info) {
    //when user drags screen, store whre it happened and let program know dragging=true
    startOfDrag = Offset(info.eventPosition.global.x, info.eventPosition.global.y);
    isDragging = true;
    isShot = false;
  }

  @override 
  void onPanUpdate(DragUpdateInfo info) {
    //we get the dragPos, then we get the distance of the drag relative to starting point 
    //then apply it to our "dragBehindBall" which is given to trajectory drawing 
    currentDragPos = Offset(info.eventPosition.global.x, info.eventPosition.global.y);
    double relX = startOfDrag.dx - currentDragPos.dx;
    double relY = startOfDrag.dy - currentDragPos.dy;
    dragBehindBall = Offset((relX), (relY));
  }

  @override
  void onPanEnd(DragEndInfo info) {
    //make ball move when thrown
    ball.body.setType(BodyType.dynamic);
    impulse = Vector2(dragBehindBall.dx, dragBehindBall.dy) * linearImpulseStrengthMult;
    ball.body.applyLinearImpulse(impulse);
    print("SHOT STRENGTH: ${Vector2(dragBehindBall.dx, dragBehindBall.dy) * linearImpulseStrengthMult}");
    print("VEL: ${ball.body.linearVelocity}");
    print("BALL MASS: ${ball.body.mass}");
    //reset necessary vars 
    isDragging=false;
    isShot = true;
    dragBehindBall = Offset(startPosX, startPosY);
  }

  int count = 1;
  @override
  void update(double dt) {
    super.update(dt);
      //print("TIMESTEP: $count VEL: ${ball.body.linearVelocity} \n POS: ${ball.getSuperPosition()}");
  }

  
} 

import 'dart:async';
import 'dart:ui';
import 'package:bball_blast/BBallBlast.dart';
import 'package:bball_blast/config.dart';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';

class Ball extends BodyComponent {
  @override
  final BBallBlast game;
  @override
  final Vector2 position;

  //points to draw for trajectory
  late List<Vector2> points;

  //TODO: CHANGE WHEN YOU CHANGE BODY
  late double velocityRatio = 1/28.274333882308138;

  int steps = 180;


  Ball(this.game, this.position, Sprite sprite) : super (
    renderBody: false,

    //start body as static then set as dynamic when it is shot
    bodyDef: BodyDef()
      ..position = position
      ..type = BodyType.static
      ..linearDamping = 0,

    fixtureDefs: [
      FixtureDef(CircleShape()..radius = 6/2)
        ..restitution = 0.3
        ..density = 1
        //..friction = 0.5
    ],

    //add our sprite
    children: [

      SpriteComponent(
        sprite: sprite,
        size: Vector2 (7, 7),
        anchor: Anchor.center,
      )

    ]
  );

  @override
  Future<void> onLoad() async {
    super.onLoad();
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    //draw projected trajectory with info given by drags 
    if (game.isDragging) {
      //we multiply the input by that number as it's the ratio that converts pixel to velocity
      Vector2 initialVelocity = Vector2(game.dragBehindBall.dx, game.dragBehindBall.dy) * game.linearImpulseStrengthMult * velocityRatio;
      //initialVelocity = _checkVelMax(initialVelocity);
      //print(initialVelocity);
      //get points to draw projected trajectory
      points = trajectoryPoints(initialVelocity, Vector2(game.startPosX, game.startPosY), steps, (1/60)); //60 fps so our dt is 1/60

      Paint paint = Paint()
        ..color = const Color.fromRGBO(244, 67, 54, 1)
        ..strokeWidth = 0.5
        ..style = PaintingStyle.stroke;

      for (int i = 0; i < points.length - 1; i++) {
        canvas.drawLine(
          Offset(points[i].x, points[i].y),
          Offset(points[i + 1].x, points[i + 1].y),
          paint,
        );
      }
    }
  }

  List<Vector2> trajectoryPoints(Vector2 initialVelocity, Vector2 startPos, int steps, double timeStep) {
    List<Vector2> points = [];

    for(int i=0; i<steps+1; i++) {
      //get the timestep for a certain time in place
      double t = i * timeStep;

      //multiply velocity by timestep to get appropriate velocity and grav at ceratain time
      Vector2 stepVel = initialVelocity * t;
      Vector2 stepGrav = Vector2(0.0, 0.5* gravity *(t*t));

      //calculate displacement based on stepVel and stepGrav in relation to starting position
      double xDisplacement = startPos.x + stepVel.x;
      double yDisplacement = startPos.y + stepVel.y + stepGrav.y;

      //form our vectors
      Vector2 pos = Vector2(xDisplacement, yDisplacement);
      points.add(pos);
    }

    return points;
  }

  Vector2 getSuperPosition(){
    return super.position;
  }

}

Thank you for any help someone may provide.


Solution

  • You are hitting the Box2D max velocity, that is why Forge2DGame is zoomed to 10.0 by default, but that zoom level disappears when you use CameraComponent.withFixedResolution.