[Flutter] 공의 자유낙하 시뮬레이션 만들기

RGLie,·2022년 4월 7일
1
post-thumbnail

나의 이전 글들을 보면 알다시피 나는 최근 Flutter에서 Canvas위에 다양한 애니메이션을 만들어보고 있다. 이번에는 공이 실제로 자유낙하하는 애니메이션을 구현해보았다. 이것을 만드는 과정에서 내가 Flutter의 애니메이션에 대해 모르고 착각하고 있었던 부분들을 발견했고 그로 인한 여러 시행착오들을 써보려한다.

Skeleton Code

먼저 Bounce Ball을 구현하기 위한 Skeleton code는 바로 이전의 글의 Drag and Drop 이다.
Flutter Canvas에서 Drag and Drop 구현하기
간단하더라도 인터렉션이 있는게 재밌기 때문이다.

Widgets

child: AnimatedBuilder(
                animation: _animationController,
                builder: (context, child) {
                  return Container(
                    width: 300,
                    height: 300,
                    color: Colors.white70,
                    child: CustomPaint(
                      painter: _paint(ballPath: ball.draw),
                    ),
                  );
                }
              )
class _paint extends CustomPainter {
  final Path ballPath;

  _paint({
    required this.ballPath,
  });

  
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.brown
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    Path path = Path();
    path.addPath(ballPath, Offset.zero);

    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(CustomPainter oldDelegate) => true;

}

GestureDetector의 child를 Animated Builder로 감싸주자. 그리고 CustomPainter를 상속하는 _paint class를 만들어 주었다. 이때, 이전과는 다르게 인자로 Path를 넘겨주었는데, 이는 공을 하나의 객체로 보기 위함이다. (아래에서 조금 더 자세히 언급하겠다.)
ballPath를 그리기 위해 addPath 함수로 path에 추가한다.

Ball Object

공을 하나의 Object로 만들어서 컨트롤할 것이다. 공의 움직임을 조금 더 상세하게 조작해야하기 때문에 변수를 따로 만들시 매우 복잡해지기 때문이다.

class myBall{
  late double xPos;
  late double yPos;
  late double xVel;
  late double yVel;
  late double ballRad;
  late Path draw;
  double baseTime = 0.002;

  myBall.origin(){
    xPos=100;
    yPos=100;
    xVel=0;
    yVel=0;
    ballRad=20;
    draw=Path();
    for(double i=0; i<ballRad-1; i++){
      draw.addOval(Rect.fromCircle(
          center: Offset(
              xPos, yPos
          ),
          radius: i
      ));
    }
  }

  void stop(){
    xVel=0;
    yVel=0;
  }

  void outVel(){
    if(yVel.abs()<10){
      yVel=0;
    }
    if(xVel.abs()<10){
      xVel=0;
    }
  }

  void setPosition(double x, double y){
    xPos=x;
    yPos=y;
  }

  bool isBallRegion(double checkX, double checkY){
    if((pow(xPos-checkX, 2)+pow(yPos-checkY, 2))<=pow(ballRad, 2)){
      return true;
    }
    return false;
  }

  void updateDraw(){
    draw=Path();
    for(double i=0; i<ballRad-1; i++){
      draw.addOval(Rect.fromCircle(
          center: Offset(
            xPos,
            yPos,
          ),
          radius: i
      ));
    }
  }

  void updateAnimation(double animationValue){
    draw=Path();
    for(double i=0; i<ballRad-1; i++){
      draw.addOval(Rect.fromCircle(
          center: Offset(
            xPos + animationValue*xVel*baseTime,
            yPos + animationValue*yVel*baseTime,
          ),
          radius: i
      ));
    }
  }
}

인스턴스 변수로는 공의 위치, 공의 속도값, 그리고 공의 모양인 Path를 선언해주었다. 그리고 이 변수들을 컨트롤하기 위해 몇가지 인스턴스 메소드들도 추가해주었다.

  • stop()은 공의 속도를 모두 0으로 설정한다.
  • outVel()은 공의 속력이 특정 값보다 작을시 공을 멈추게 하기 위함이다. 공이 자유낙하하고 다시 튀어오르는 과정에서 무한히 반복할 수는 없기 때문이다.
  • setPosition()은 공의 위치를 지정하고,
  • isBallRegion()은 인자의 좌표가 공에 포함되는지를 판단한다.
  • updateDraw()와 updateAnimation()은 공의 Path를 수정하여 공을 움직이게 만들어준다.

몇가지 연산을 할 수 있는 메소드들도 있는데 그것은 이 글 마지막에 있는 깃헙 링크에서 확인하길바란다.

Free Fall's Variable

이제 자유낙하를 위한 몇가지 변수들을 선언해주자.

  bool isClick = false;
  bool isClickAfter = true;
  var ball = myBall.origin();
  late AnimationController _animationController;
  double baseTime = 0.016;
  double accel = 1000;
  
  void initState() {
    super.initState();
    _animationController = AnimationController(
        vsync: this,
        duration: Duration(milliseconds: 10)
    );
    _animationController.repeat();
  }

  
  void dispose(){
    _animationController.dispose();
    super.dispose();
  }
  • isClick은 공을 클릭한 상태에서는 움직이지 않도록 하기 위해 필요하다.
  • isClickAfter는 isClick이 true에서 false가 되었을때 마지막 한번의 refresh에서는 isClick이 true처럼 실행되게 만들기 위해 필요하다.
  • baseTime은 공의 움직임을 몇 초에 한번 연산할 것인지를 말해 주는 변수이다. flutter는 60fps로 refresh하니 16ms로 설정했다.
  • accel은 가속도로 현실의 980cm/s^2과 비슷한 1000으로 설정했다.

Free Fall Calculation

builder: (context, child) {
                  if (!isClick) {
                    if (ball.yVel!=0 || isClickAfter) {
                      ball.addYvel(baseTime * accel);
                      ball.subYpos(0.5 * accel * pow(baseTime, 2) - ball.yVel * baseTime);
                      ball.updateAnimation(_animationController.value);
                      isClickAfter=false;
                      if ((ball.yVel>0)&&(ball.yVel.abs()* _animationController.value*baseTime + ball.yPos + ball.ballRad >= 300)) {
                        ball.mulYvel(-0.7);
                        ball.outVel();
                      }
                    }
                  }
                  return Container(
                    width: 300,
                    height: 300,
                    color: Colors.white70,
                    child: CustomPaint(
                      painter: _paint(ballPath: ball.draw),
                    ),
                  );
                }

Animated Builder의 builder 안에 다음과 같은 연산을 넣어주었다. builder가 한번 재생 될때마다 baseTime 만큼 공의 속도가 증가하고 공이 움직인다. 만약, 공의 속도가 양수 (지면을 향한 방향)이고 공의 위치가 바닥을 넘으면 바닥에 충돌했다고 판단하고 y푹 속도에 -0.7을 곱한다. 음수인 이유는 y축 속도의 방향을 바꾸고, 0.7을 곱한 것은 에너지 손실이 있다고 가정한 것이다.

이 부분을 만들기 위해 꽤 많은 시행착오가 있었다. 바로 _animationController.value의 가변성 때문이었다. 같은 Duration을 설정하더라도 실행할때마다 _animationController.value가 연속적이지 않은 값으로 0에서 1이 되었기 때문이다. 이 때문에 충돌임을 연속으로 여러번 감지하여 공의 속도가 한번에 0이 되기도 하였다.(0.7 X 0.7 X ... = 0) 그것을 처리하기 위해 if 문에 여러 추가적인 제한 사항들을 만들어 제대로 작동하게 만들 수 있었다.

Conclusion

나는 이것을 처음에는 이것과 같은 방식으로 addStatusListener를 이용해 구현하고자 했다. 그리고 실제로 구현을 했고 보기에는 잘 동작하였다. (첫번째 구현)

그러나 실행할 수록 렉이 많이 걸림을 느꼈고 이것을 해결하고자 하였다. 그래서 코드를 보다보니 왜 addStatusListener에서 변수를 컨트롤 하려고하지 라는 의문이 생겼다. 그러면 Animated Builder를 쓰는 의미가 없기 때문이다.
나는 지금까지 Animated Builder가 refresh 되는 방식을 모르고 있었던 것이다. Animated Builder 안의 builder 에서 _animationController.value가 변할때마다 같이 refresh 한다. 즉, 나는 Animated Builder를 쓰고 있었던것이 아니라 그냥 특정 주기마다 화면 전체를 다시 build하면서 animation을 만들고 있었던 것이다..! 그러니 렉이 많이 생길 수 밖에..

그래도 지금이라도 깨달아서 다행이라 생각한다.
이제는 이것을 조금 더 발전시켜 물리 엔진에 도전해볼 생각이다.

Full Code

Final Code

profile
Flutter Programmer (github: RGLie)

0개의 댓글