본문 바로가기

Dev/Flutter

[Flutter] CustomPainter를 이용한 팩맨 애니메이션 그리기

플러터에 대한 첫 포스팅입니다.

사실 본 블로그에서는 기초적인 세팅보다는 뭔가를 응용해서 필요할 때 바로 사용할 수 있는 것들을 위주로 포스팅을 할까 합니다.

 

이미 플러터 개발 환경 세팅, 위젯 종류 등 잘 설명되어있는 블로그나 문서가 많아서 굳이 재차 포스팅하는 게 의미가 없을 것 같더라고요.

따라서 본 포스팅에서는 독자가 플러터가 뭐하는 녀석인지 이미 파악이 되었음을 가정하겠습니다.

 

본론으로 돌아와서, 본 포스팅에서는 플러터에서 지원하는 CustomPainter 클래스를 이용해 화면상에 팩맨을 그리는 내용을 다루도록 하겠습니다.

 

구현에는 약간의 수학이 필요한데, 여기서는 호도법을 이용해 반복적으로 입을 움직이는 팩맨을 그려보도록 하겠습니다.

본 포스팅의 최종 결과물은 다음과 같습니다.

 

팩맨 애니메이션

 

1. 구현에 필요한 클래스 및 객체 파악하기

우선, 위 결과물 사진을 보았을 때 필요한 클래스를 파악해야 합니다.

반복적으로 움직임을 실행시켜주는 AnimationController가 우선 눈에 띕니다.

팩맨의 입이 규칙적으로 min max 값 사이의 각도로 움직이네요.

이 애니메이션 값을 가지는 Animation 클래스도 필요해 보입니다.

실제로 주어진 값대로 화면에 그림을 그려주는 CustomPaint 객체가 필요하겠습니다.

(CustomPaint는 지금은 몰라도 됩니다.)

 

2. 애니메이션 객체 초기화

class PacManPage extends StatefulWidget {
  . . .
}

class _PacManPageState extends State<PacManPage> with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    // 애니메이션 컨트롤러 초기화
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300), // 300 밀리세컨드, 0.3 초간 실행
    );
    
    // _animationController에서 지정하는 범위내에서 90 에서 0 으로 점차 값이 변화한다.
    _animation = Tween<double>(begin: 90, end: 0).animate(_animationController);
   
   // repeat를 이용해 애니메이션을 반복한다. 
    _animationController.repeat(reverse: true);

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    . . .
}

 

플러터에서는 애니메이션을 사용하기 위해 TickerProviderMixin을 with문으로 믹스인 해야 합니다.

믹스인이란 어떤 클래스를 상속하지 않고 해당 클래스의 기능을 사용하기 위한 선언입니다.

 

플러터에서는 애니메이션을 실행할 때 초당 60회 간격으로 화면을 갱신하는데 이를 틱이라고 부릅니다.

애니메이션 객체와 한 몸이 되는 클래스입니다.

 

 

3. CustomPainter 클래스를 상속하는 PacManPainter 클래스 생성

class PacManPainter extends CustomPainter {
  final Animation listenable;

  PacManPainter({required this.listenable}) : super(repaint: listenable);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = Colors.yellow;
    double sweepAngle = (360.0 - listenable.value) * pi / 180; // 입 벌리는 정도
    double startAngle = (listenable.value / 2) * pi / 180; // 회전

    var p = Path()
      ..moveTo(size.width / 2, size.height / 2) // 중심각의 위치, 주어진 영역의 절반 위치를 중심각으로 지정
      ..arcTo(
          Rect.fromCircle(
            radius: size.height / 2, //반지름, 객체의 크기를 결정한다.
            center: Offset(size.width / 2, size.height / 2), // 객체 자체의 위치
          ),
          startAngle,
          sweepAngle,
          false)
      ..close();

    canvas.drawPath(p, paint);
  }

  @override
  bool shouldRepaint(PacManPainter oldDelegate) {
    return true;
  }
}

 

PacManPainter 클래스는 실질적으로 그림을 묘사하는 클래스인데, 변화에 필요한 값을 받기 위해 listenable이라는 이름으로 애니메이션 객체를 받습니다.

 

PacManPainter는 CustomPainter를 상속할 때 paint와 sholdRepaint 메서드를 반드시 오버라이드 해야 합니다.

paint는 그림을 그릴 때 사용하고 sholdRepaint는 그림을 갱신해서 그려야 할 때 사용합니다.

 

double sweepAngle = (360.0 - listenable.value) * pi / 180; // 입 벌리는 정도
double startAngle = (listenable.value / 2) * pi / 180; // 회전

 

애니메이션에서 가장 중요한 부분입니다.

입을 벌리는 정도와 회전 값을 실시간으로 바꿔줘야 합니다.

이 두 값은 모두 각도인데, 플러터에서 각도 변화 애니메이션을 주기 위해서는 호도법을 이용해 라디안으로 바꿔줘야 합니다.

 

 

라디안에 대한 추가적인 정보가 필요하신 분들은 아래 접은 글을 확인해주세요.

더보기
출처:&nbsp;https://www.pylenin.com/blogs/degree-to-radian/

라디안은 어떤 원의 반지름을 r이라고 했을 때, 반지름 r 만큼의 호 길이를 각도로 표현한 단위입니다.

반지름과 호 길이가 같을 때의 각도를 1 라디안이라고 부릅니다.

 

원둘레를 구하는 공식은 2πr 이므로 라디안으로 표현하자면

2πr = 360° 등식이 성립.

πr = 180° 이 반원의 둘레

r = 180° / π 이항을 이용한 반지름에 대한 라디안

 

즉, 반지름과 호 길이가 같을 때가 1 라디안이므로

1 radian = 180° / π 이 됩니다.

 

따라서 180°는 π 라디안, 360°는 2π 라디안이 됩니다.

이항과 약분을 통해 1° 는  π / 180° 라디안이라는 것을 알 수 있으니

이제 이 값에서 원하는 값을 곱해주면 각도를 라디안으로 환산할 수 있습니다.

그래서 각 변수에 각도 값 value와 pi / 180를 곱해준 것입니다.

 

 

여기서 startAngle값도 입이 벌어지는 것에 비례해 변화를 주어서 항상 팩맨의 입이 오른쪽 정면을 향하도록 했습니다.

만약 startAngle의 값에 변화가 없다면 다음과 같이 보입니다.

 

 

4. CustomPainter 화면에 뿌려주기

이제 벡터를 정의해 화면에 그려줍니다.

var p = Path()
  ..moveTo(
      size.width / 2, size.height / 2) // 중심각의 위치, 주어진 영역의 절반 위치를 중심각으로 지정
  ..arcTo(
      Rect.fromCircle(
        radius: size.height / 2, //반지름, 객체의 크기를 결정한다.
        center: Offset(size.width / 2, size.height / 2), // 객체 자체의 위치
      ),
      startAngle,
      sweepAngle,
      false)
  ..close();
  
  canvas.drawPath(p, paint);

벡터는 Path 클래스를 이용해 그릴 수 있는데

본 포스팅에서는 moveTo와 arcTo 함수만 사용합니다.

moveTo로 중심각의 위치, arcTo로 각도에 대한 호를 그립니다.

 

canvas에 색깔과 벡터를 그려주는 것으로 PacManPainter 클래스를 완성해줍니다.

 

이제 PacManPage 클래스로 돌아와 이 페인터를 선언해줍니다.

여기서 팩맨 그림의 크기를 변수로 빼두면 나중에 크기 바꿀 때 편해지겠죠?

 

class _PacManPageState extends State<PacManPage> with TickerProviderStateMixin {
  
  . . .
  
  final double pacmanSize = 100.0; // px

  . . .

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pacman Animation Example'),
      ),
      backgroundColor: Colors.black, //에니메이션이 잘 보이도록 검은색으로 배경을 칠해준다.
      body: Center(
        // 화면 중앙에 페인트
        child: CustomPaint(
          size: Size(pacmanSize, pacmanSize),
          painter: PacManPainter(listenable: _animation),
        ),
      ),
    );
  }
}

 

이제 간단히 움직이는 팩맨 애니메이션 구현이 완성되었습니다.

 

 

아래는 본 포스팅에서 다룬 코드의 전문입니다.

 

 

이것으로 CustomPainter를 이용한 팩맨 애니메이션 그리기 포스팅을 마치겠습니다. :)

'Dev > Flutter' 카테고리의 다른 글

[Flutter] 중심 좌표에서 특정 각도에 객체 그리기  (0) 2022.04.07