프로젝트에서 보기 →

플러터 모바일 앱 개발 | 모바일 앱 개발자를 위한 Flutter(플러터) 제대로 배우기 Part.4 중급2 핑퐁게임, 영화나라, 장보기 앱 | 취업·실무·창업 | 에어클래스

태그
기술 에어클래스
시작일
종료일
수정일

https://www.youtube.com/watch?v=NixfcD8M1KY

1. 이건 꼭 알아야 한다^1

[? 질문] Flutter로 화면 안에서 **좌표 기반으로 움직이는 게임(핑퐁/브레이크아웃 형태)**을 만들 때, 위젯을 “원하는 절대 위치”에 어떻게 배치하고 움직이나^1
[= 답] Stack + Positioned를 사용해 스택 영역(게임 영역) 안에서 위젯을 left/top/bottom 같은 절대 좌표로 배치하고, 좌표값을 변수로 두고 setState()로 갱신해 “움직이는 것처럼” 만든다.[^6]

[? 질문] 게임에서 공의 이동처럼 “계속 값이 바뀌는” 동작을 Flutter에서 어떤 구조로 구현하나[^14]
[= 답] **Animation(값의 변화)**과 **AnimationController(시간/재생 제어)**를 함께 두고, 보통 initState()에서 1회 초기화한다.[^14]

[? 질문] AnimationController의 vsync는 왜 필요하고, 에러가 나면 어떻게 해결하나[^15]
[= 답] vsync화면의 프레임(예: 60fps)과 애니메이션 업데이트를 동기화하기 위한 것이며, 이를 쓰려면 State가 **SingleTickerProviderStateMixin**을 믹스인으로 상속해야 한다.[^15]


2. 큰 그림^2

이 콘텐츠는 Flutter 중급 과정의 일부로, 간단한 핑퐁(공 튀기기) 게임 화면을 직접 구성하면서 Flutter의 레이아웃(특히 Stack/Positioned), StatefulWidget의 상태 관리, 그리고 애니메이션(Animation/Controller) 기초를 코드로 연결해 설명한다.^2 게임은 공과 막대(배트)를 화면에 올려두고, 좌표를 바꿔 움직이게 하며, 충돌/실패 같은 게임 로직을 만들 준비 단계까지 진행한다.[^3]

  • 좌표 기반 배치: 게임 오브젝트(공/배트)는 Stack 영역 위에 올리고 Positioned로 절대좌표를 지정해야 움직임 구현이 쉬워진다.[^6]
  • 게임 영역 크기 측정: 실제 배치/충돌을 하려면 “게임 영역의 width/height”가 필요하고, 이를 위해 LayoutBuilder의 constraints.maxWidth/maxHeight를 사용한다.[^12]
  • 애니메이션의 2요소: Animation(Tween으로 값 범위 정의) + **AnimationController(duration/forward로 재생 제어)**를 결합해 시간에 따라 값이 변하도록 만든다.[^14]

3. 하나씩 살펴보기[^3]

3.1 오늘 만들 것: 공이 튀는 핑퐁 게임 화면과 조작 방식^1

📸 0:01

강의는 “재밌는 것”으로 핑퐁 게임을 만들겠다고 시작한다.^1 여기서 말하는 핑퐁은 벽돌깨기처럼 벽돌이 있는 게임을 완성하는 게 아니라, 공이 튀기는 애니메이션 중심의 간단한 게임을 만든다는 의미로 소개된다.^1

화면 구성은 다음 요소들을 포함한다.^2

  • 상단에 핑퐁 게임 제목(버전 1.0 형태)
  • 레벨(Level) 표시
  • 스코어(Score) 표시
  • 화면 안에서 **공(ball)**이 움직임
  • 하단에 좌우로 움직이는 막대(bar, bat)

조작 방식은 플랫폼에 따라 다르게 설명한다.[^3]

  • 윈도우 같은 환경이면 화살표 키로 막대를 좌우 이동할 수 있을 것이라고 언급한다.[^3]
  • 모바일에서는 손가락으로 화면을 짚고 좌우로 드래그해서 막대를 움직이게 할 것이라고 설명한다.[^3]

게임 규칙의 윤곽도 함께 제시한다.[^4]

  • 공이 막대에 “딱 맞으면” 튕긴다(반사).[^4]
  • 공을 막대가 따라가지 못해 화면 아래쪽(바닥)에 부딪히면 게임이 끝난다는 식으로 실패 조건을 설명한다.[^4]

[!IMPORTANT] 오늘의 학습 포인트
강의자는 이번 파트에서 주로 애니메이션 기능을 많이 배우게 될 것이라고 명시한다.^2 또한 “간단한 게임을 운용하는 방법”까지 다루겠다고 예고한다.^2


3.2 왜 Stack인가: 움직이는 게임 오브젝트는 절대좌표가 필요하다[^5]

📸 1:49

강의자는 공과 배트 같은 오브젝트를 화면 위에서 자유롭게 움직이기 위해, 위젯을 Stack 위에 올려놓고 그 위에서 좌표로 배치하겠다고 설명한다.[^5]

여기서 Stack을 쓰는 이유를 “좋은 점”으로 풀어서 말한다.[^5]

  • Stack은 그 위에 위젯들을 “쌓아” 올릴 수 있다.[^5]
  • Stack과 함께 Positioned를 사용하면, 스택의 절대 좌표 기준으로 특정 위젯을 원하는 위치(left/top/right/bottom)에 놓을 수 있다.[^5]
  • 공은 좌표로 움직여야 하므로(게임에서는 “좌표 기반 이동”), Stack을 쓰면 이런 움직임이 자유로워진다.[^5]

즉, Column/Row가 흐름(위→아래, 좌→우) 배치라면, Stack은 영역을 겹쳐 쓰며 “좌표로 배치”하는 데 적합하다는 맥락을 준다.[^6]


3.3 StatefulWidget의 initState vs build: 왜 초기화는 한 번, 화면은 여러 번 그리나[^7]

📸 2:24

강의 도중 “StatefulWidget에서 initState()build()가 어떻게 동작하는지”를 다시 짚는다.[^7]

  • initState()초기화 시 한 번만 호출된다.[^7]
  • build()는 화면을 다시 그릴 때마다 여러 번 호출된다.[^7]

이 구분이 중요한 이유는, 뒤에서 애니메이션 컨트롤러 같은 객체를 매번 만들지 않고 초기화 시 1회 설정해야 하기 때문이라는 흐름으로 연결된다.[^14]


3.4 프로젝트 시작: 디폴트 프로젝트를 비우고 기본 화면(Scaffold/AppBar) 만들기[^8]

📸 3:02

강의자는 미리 만들어 둔 “아무것도 없는 디폴트 프로젝트”에서 출발한다.[^8] 그리고 기본 생성 코드(불필요한 부분)를 지우고, MyHomePageStatelessWidget으로 두며 Scaffold를 구성한다.[^8]

구체적으로 진행하는 작업 흐름은 이렇다.[^8]

  1. 기존 코드 “필요 없으니 다 지운다”.[^8]
  2. MyHomePage를 만들고 Scaffold를 둔다.[^8]
  3. AppBar에 타이틀을 넣는데, 아까 화면 예시에서 본 것처럼 **“핑퐁 게임 버전 1.0”**을 넣는다.[^8]
  4. 실행해서 정상 표시를 확인한다.[^8]
  5. debugShowCheckedModeBanner: false(디버그 배너 제거)를 적용해 화면에서 배너가 사라지는 것도 확인한다.[^9]

이 시점에서 “게임은 Scaffold의 body 안에서 이뤄지면 될 것”이라고 방향을 잡는다.[^9]


3.5 게임 영역(Body) 컨테이너와 색: “이 영역이 게임판이 된다”[^10]

📸 4:51

다음 단계로 body에 컨테이너를 하나 넣고, 배경색을 주면 그 영역이 “게임 영역”이 된다고 설명한다.[^10] 즉, 게임이 진행되는 시각적 공간을 우선 확보하는 단계다.[^10]


3.6 공(Ball) 위젯 만들기: 원형은 BoxDecoration + shape: circle[^11]

📸 5:18

3.6.1 Ball을 별도 위젯 파일로 분리[^11]

강의자는 공을 먼저 만들자고 하며, 공은 “별도의 위젯으로 만들겠다”고 한다.[^11] IDE에서 현재 소스 위치 이동 기능을 언급하면서, ball.dart 같은 새 다트 파일을 만들고 StatelessWidget으로 선언한다.[^11]

공은 내부적으로 바뀔 게 많지 않아서, 단순히 “동그란 것”만 있으면 된다고 설명한다.[^11] 그리고 공의 지름(diameter)을 예로 50으로 둔다.[^11]

3.6.2 공을 그리는 방식: 진짜 그래픽보다 위젯 조합으로 먼저 만든다[^11]

강의자는 공을 “그래픽(draw) 명령으로도 만들 수 있다”고 말하면서도, Flutter는 “업무용으로 많이 쓰기 때문에” 흔히는 위젯을 조합해 구현한다고 설명한다.[^11] 나중에 그래픽 명령으로 그리는 방식은 따로 시간을 잡아 알려주겠다고 예고한다.[^11]

3.6.3 Container를 원으로 만드는 핵심: BoxDecoration(shape: BoxShape.circle)[^11]

공을 Container로 만들고, 폭/높이를 지름으로 맞춘 뒤, 단순히 색만 주면 사각형이므로 “동그랗게” 만드는 방법이 필요하다고 한다.[^11]

이를 위해 다음을 사용한다.[^11]

  • Container(width: diameter, height: diameter, decoration: BoxDecoration(...))
  • colorBoxDecoration 안으로 넣는다(예: Colors.amber[400]처럼 0~900 농도 중간값 사용).[^^11]
  • 핵심은 shape: BoxShape.circle 설정이다.[^11]

이렇게 하면 화면에 공이 원형으로 나타난다고 확인한다.[^11]

[!TIP] 원형/모서리 처리의 기본 패턴
Container 자체는 사각형이므로, 원이나 둥근 모서리는 보통 decoration: BoxDecoration(...)에서 shape 또는 borderRadius로 처리한다.[^11]


3.7 배트(Bat) 위젯 만들기: 사각형 막대 + 해상도 대응(가변 크기)[^12]

📸 12:22

3.7.1 Bat도 별도 위젯으로 생성[^12]

공 다음으로 “막대(배트)”를 만든다.[^12] 이것도 bat.dart로 분리하고 StatelessWidget으로 만든다.[^12]

막대는 사각형이라 공보다 쉽다고 말하며, Container로 width/height를 정하고 색을 주면 바로 된다고 설명한다.[^12] 예시로 폭 150, 높이 35 정도를 잡고, 색은 파란색 계열로 지정한다.[^12]

3.7.2 “고정 크기”의 문제: 해상도에 따라 크기 차가 심해진다[^12]

여기서 강의자는 중요한 실무 포인트를 든다.[^12]

  • Ball은 일단 괜찮지만, Bat(막대)는 **화면 해상도에 따라 고정 픽셀 값(예: 150)**을 쓰면 기기별로 체감 크기가 너무 달라질 수 있다.[^12]
  • 그래서 막대를 **가변 크기(화면 크기에 비례)**로 돌리고 싶다고 한다.[^12]

이를 위해 Bat 위젯이 width/height를 매개변수로 받도록 바꾼다.[^12]

  • final double width; final double height;
  • 생성자에서 required this.width, required this.height
  • Container의 width/height에 상수 대신 this.width, this.height 사용
  • 호출하는 쪽에서 Bat(width: 150, height: 30)처럼 넘겨준다.[^12]

이렇게 하면 나중에 화면 크기를 기반으로 계산한 값을 넘길 수 있고, 레벨/아이템에 따라 배트 크기를 바꾸는 확장도 가능하다고 연결한다.[^13]


3.8 Stack + Positioned로 공/배트 배치: left/top/bottom 개념 잡기[^6]

📸 14:19

3.8.1 Stack에 Ball과 Bat을 children으로 겹쳐 올린다[^6]

핑퐁 게임 화면에서는 공은 화면 여기저기, 배트는 보통 바닥에서 좌우로 움직인다.[^6] 이런 배치를 위해 PingPong 위젯(이후 Stateful로 운용)을 만들고, 그 build에서 Stack(children: [...])로 Ball과 Bat을 넣는다.[^6]

그런데 Stack에 그냥 넣으면 둘 다 기본적으로 왼쪽 상단에 붙는 문제가 생긴다고 지적한다.[^6]

3.8.2 위치를 지정하려면 Positioned를 감싼다[^6]

그래서 각 오브젝트를 Positioned로 감싸 위치를 잡는다.[^6] Positioned에서 쓸 수 있는 프로퍼티로:

  • left, top, right, bottom
  • width, height 등이 있다고 확인한다.[^6]

3.8.3 top vs bottom: “위에서 내려오는 거리” vs “아래에서 올라오는 거리”[^6]

강의자는 좌표 개념을 이렇게 정리한다.[^6]

  • top: 화면 위쪽에서 얼마나 내려왔는지
  • left: 화면 왼쪽에서 얼마나 떨어졌는지
  • right: 화면 오른쪽에서 얼마나 떨어졌는지
  • bottom: 화면 아래에서 얼마나 위로 올라왔는지

배트는 바닥에 붙어 있어야 하므로 bottom: 0을 쓰면 된다고 설명한다.[^6]

그리고 예시로 값을 바꿔보며 직관을 만든다.[^6]

  • Ball: left: 100, top: 100이면 공이 그 위치로 이동한다.[^6]
  • Bat: left: 120, bottom: 0이면 바닥에 붙은 채 좌우 위치만 바뀐다.[^6]

결론적으로 “left 값만 바꾸면 좌우 이동, top 값만 바꾸면 상하 이동”이 된다는 걸 확인시킨다.[^6]


3.9 게임에 필요한 상태 변수들: 화면 크기, 공 좌표(x/y), 배트 좌표(x), 배트 크기[^13]

📸 18:26

이제 “벽에 튀기고 바닥에 튀기는” 등의 로직을 하려면, 해당 영역의 정보(넓이/높이)와 오브젝트 좌표가 필요하다고 말한다.[^13] 그래서 게임에 필요한 변수들을 State에 선언한다.[^13]

강의에서 잡는 변수들은 다음 흐름이다.[^13]

3.9.1 게임 영역 크기(width/height)

  • “전체 화면”이 아니라 “게임 영역의 크기”를 구하겠다고 한다.[^13]
  • double width = 0; double height = 0; 식으로 초기값을 0으로 둔다.[^13]

3.9.2 볼 위치: positionX, positionY

공은 움직임에 lefttop만 있으면 되므로 좌표 2개를 둔다고 설명한다.[^13]

  • double positionX = 0;
  • double positionY = 0;[^13]

3.9.3 배트 위치: batPositionX

배트는 항상 바닥(bottom=0)이므로, 좌우 위치만 필요하다고 말한다.[^13]

  • double batPosition = 0;(좌우 x)[^13]

3.9.4 배트 크기: batWidth, batHeight (레벨/아이템 확장 고려)

배트는 레벨이 오르면 줄어들 수 있고, 아이템 먹으면 늘었다 줄었다 할 수 있으니, 크기도 변수로 잡는다고 설명한다.[^13] 공 크기도 나중에 변수로 확장할 수 있다고 덧붙인다.[^13]

  • double batWidth = 0;
  • double batHeight = 0;[^13]

3.10 LayoutBuilder로 “스택이 실제로 차지하는 게임 영역”의 maxWidth/maxHeight 얻기[^12]

📸 12:22

강의자는 게임 영역을 구할 때 단순히 MediaQuery 같은 “전체 화면”이 아니라, 실제 Stack이 할당받은 “순수 영역” 크기를 구해야 할 수 있다고 말한다.[^12] (예: 패딩이 있거나 위/아래에 다른 UI가 있어 영역이 달라지는 경우)[^12]

이를 위해 Stack을 LayoutBuilder로 감싼다.[^12]

LayoutBuilder(builder: (ctx, constraints) { ... return Stack(...); }) 형태로 구성하며, 여기서 constraints.maxWidth, constraints.maxHeight를 꺼내온다고 설명한다.[^12]

그리고 그 값을 앞서 만든 게임 영역 변수에 저장한다.[^12]

  • width = constraints.maxWidth;
  • height = constraints.maxHeight;[^12]

추가로 배트 크기도 이 시점에 화면 크기에 비례해서 계산한다.[^12]

  • batWidth = width / 4; 처럼 “화면 폭의 1/4”를 배트 폭으로 잡음[^12]
  • batHeight = height / 25; 처럼 “화면 높이의 1/25”를 배트 높이로 잡음[^12]

즉, “해상도 대응”을 실제로 구현하는 지점이 LayoutBuilder 기반 계산이다.[^12]

[!IMPORTANT] LayoutBuilder를 쓰는 이유(강의의 논리)
“스택이 할당받은 정확한 영역”을 constraints로 얻고, 그 영역을 기준으로 게임 오브젝트 크기/좌표를 계산해야, 패딩/다른 UI가 추가되더라도 게임 로직이 흔들리지 않는다.[^12]


3.11 Positioned에 ‘상수’ 대신 ‘변수’를 연결: 값 바꾸고 setState하면 다시 그려진다[^13]

📸 18:26

강의자는 이제 Positioned의 left/top/bottom에 숫자를 직접 쓰지 말고, 앞서 만든 변수로 연결해 “좌표를 바꾸면 위치가 바뀌게” 만들자고 한다.[^13]

  • Ball: left: positionX, top: positionY[^13]
  • Bat: left: batPosition, bottom: 0 (바닥 고정)[^13]
  • Bat 위젯 호출 시 크기: Bat(width: batWidth, height: batHeight)[^12]

그리고 중요한 동작 원리를 설명한다.[^13]

  • 나중에 positionX, positionY, batPosition 값이 바뀌고 setState()로 리빌드가 일어나면, Flutter가 다시 그리면서 오브젝트 위치가 업데이트된다.[^13]

다만 단순히 변수 값을 바꿔놓기만 하면 화면은 안 바뀌므로, “다시 리빌드 해줘야 한다”고 말한다.[^13]


3.12 애니메이션 시작: Animation + AnimationController가 필요하다[^14]

📸 28:05

이제 공이 움직이는 것을 시뮬레이션하기 위해 애니메이션을 붙이겠다고 한다.[^14] 강의자는 애니메이션을 위해 필요한 것이 “크게 두 가지”라고 못 박는다.[^14]

  1. Animation: 실제로 변하는 값(예: double)이 시간에 따라 변화하는 결과를 담는다.[^14]
  2. AnimationController: 애니메이션을 시작/정지/진행시키고, duration 등 “시간”을 제어한다.[^14]

그래서 State에 변수를 선언한다.[^14]

  • late Animation<double> animation;
  • late AnimationController controller; (널이 아님을 보장하기 위해 late 사용 언급)[^14]

그리고 이 두 객체는 매번 만드는 것이 아니라, 보통 화면이 처음 그려질 때 딱 한 번 실행되는 initState()에서 초기화한다고 설명한다.[^14]


3.13 AnimationController 초기화: duration과 vsync, 그리고 SingleTickerProviderStateMixin[^15]

📸 31:13

3.13.1 controller = AnimationController(...)

컨트롤러를 만들 때 설정하는 핵심 요소는 다음 두 가지로 전개된다.[^15]

  • duration: Duration(seconds: 3)
    • 공이 어느 지점에서 어느 지점까지 이동하는데 몇 초 걸릴지 같은 “애니메이션 실행 시간”을 의미한다고 설명한다.[^15]
  • vsync: this
    • “수직 동기화(v-sync)”를 현재 화면(State) 기준으로 맞추라는 의미라고 설명한다.[^15]
    • 화면이 예를 들어 60프레임으로 갱신되면, 애니메이션 컨트롤러도 그 프레임에 맞춰 같이 갱신된다는 설명을 덧붙인다.[^15]

3.13.2 vsync 에러의 원인과 해결: 믹스인 상속 필요

vsync: this를 넣었더니 에러가 나는 상황을 보여주면서, 이유를 설명한다.[^15]

  • vsync를 맞추려면 State가 특정 타입(티커 제공자)을 제공해야 한다.[^15]
  • 그래서 State 클래스 선언부에 **SingleTickerProviderStateMixin**을 믹스인으로 추가해야 한다고 한다.[^15]
  • 강의자는 이것이 이전에 배웠던 “믹스인 문법”이라고 연결한다.[^15]

이 믹스인을 추가하면 컨트롤러가 화면과 동기화 대상(vsync)을 얻을 수 있어 에러가 해결된다고 정리한다.[^15]


3.14 Tween으로 값 범위 정의: 0에서 100까지 3초 동안 바뀌는 값[^16]

📸 34:14

컨트롤러가 “시간”을 제공한다면, 애니메이션은 “어떤 값이 어디서 어디까지 바뀌는가”를 정의한다고 설명한다.[^16]

이를 위해 Tween을 사용한다.[^16]

  • Tween은 “between”처럼 시작 값과 끝 값을 지정해 그 사이를 변화시키는 구조라고 말한다.[^16]
  • 예시로 begin: 0, end: 100을 들며 “0에서 100까지 바꿔라”라는 의미라고 설명한다.[^16]
  • 그리고 이 Tween을 animate(controller)로 컨트롤러에 연결한다고 말한다.[^16]

또한 강의자는 Animation<double>과 값 타입을 맞추기 위해 “double로 맞춰준다”고 설명한다.[^16]


3.15 프레임 단위 변화의 직관화: 3초 × 60fps = 180프레임, 한 프레임당 약 0.58 증가[^17]

📸 36:19

강의자는 숫자를 통해 애니메이션이 실제로 어떻게 변하는지 직관을 제공한다.[^17]

  • 화면이 60fps라고 가정한다.[^15]
  • 애니메이션 시간이 3초이면, 총 프레임 수는 대략 3초 × 60 = 180 프레임이라고 계산한다.[^17]
  • 값이 0에서 100까지 180회에 걸쳐 바뀌면, 한 번 업데이트될 때 평균 증가량은 100 / 180 ≈ 0.55~0.58 정도라고 추정해 말한다.[^17]
  • 따라서 매 프레임마다 값이 “탁탁탁” 조금씩 올라가며 Tween 효과가 나타난다고 설명한다.[^17]

이 설명은 “애니메이션이 실제로는 프레임마다 조금씩 값이 바뀌는 반복”임을 이해시키는 역할을 한다.[^17]


3.16 애니메이션 리스너와 출력 확인: 값 변화 이벤트가 여러 번 호출된다[^18]

📸 37:40

강의자는 애니메이션의 값이 바뀔 때마다 어떤 이벤트 리스너를 지정할 수 있다고 말한다.[^18] 그리고 변화가 실제로 일어나는지 확인하기 위해 print를 찍어보는 흐름으로 간다.[^18]

  • 애니메이션이 진행되는 동안 여러 번 호출되며, 콘솔에 출력이 “엄청 나온다”고 말한다.[^19]

또한 “아직 스타트는 안 된다”는 점을 강조한다.[^19] 애니메이션과 컨트롤러를 만들어놓기만 했지, 재생을 시작시키지 않았기 때문이다.[^19]


3.17 애니메이션 시작은 controller.forward(): 0→100으로 진행[^19]

📸 38:19

애니메이션을 실제로 시작시키려면 컨트롤러를 사용해야 한다고 말한다.[^19]

  • controller.forward()를 호출하면 “직진(포워드)”이므로 0에서 100까지 쭉 진행한다고 설명한다.[^19]

그리고 실행하면 출력이 쏟아지는 것을 통해 “진짜로 애니메이션이 재생되며 값이 계속 업데이트됨”을 확인한다.[^19]


3.18 호출 횟수 카운트: CNT 변수를 두고 증가시키기[^20]

📸 38:56

마지막 부분에서 강의자는 출력이 몇 번 발생하는지 세어보자는 식으로, 카운터 변수를 둔다.[^20]

  • int cnt = 0; 같은 변수를 두고[^20]
  • 리스너/콜백에서 cnt++로 증가시키며[^20]
  • forward() 하기 직전에 cnt = 0으로 초기화하는 흐름을 만든다.[^20]

이로써 애니메이션이 프레임/틱 단위로 여러 번 호출됨을 “횟수”로도 확인하려는 의도를 보여준다.[^20]


4. 핵심 통찰[^21]

  1. [h 게임 UI는 “흐름 레이아웃(Column/Row)”보다 “좌표 레이아웃(Stack/Positioned)”이 본질적으로 맞다] 공/배트처럼 계속 움직이는 대상은 절대좌표가 필요하고, Stack/Positioned가 이를 가장 सीधे(직접적으로) 지원한다.[^5]
  2. [h 해상도 대응은 위젯 내부 상수값이 아니라, 게임 영역을 측정한 뒤 비율로 계산하는 게 안전하다] LayoutBuilder의 constraints로 “실제 할당 영역”을 얻고, 그 값을 기반으로 배트 크기 같은 것을 정한다.[^12]
  3. [h 애니메이션은 ‘값의 변화(Animation)’와 ‘시간/재생 제어(AnimationController)’가 역할 분담된다] Tween은 값 범위를, Controller는 duration과 forward 같은 재생을 담당한다.[^14]
  4. [h vsync는 선택이 아니라 구조 요구사항이다] vsync: this를 쓰려면 State가 ticker provider를 제공해야 하며, 이를 위해 SingleTickerProviderStateMixin이 필요하다는 점을 에러-해결 흐름으로 각인시킨다.[^15]
  5. [m 애니메이션을 이해하는 쉬운 방식은 “프레임마다 값이 조금씩 증가한다”로 환원하는 것이다] 3초×60fps=180프레임 같은 계산은 Tween/Controller가 추상적인 개념이 아니라 반복 업데이트라는 감각을 준다.[^17]
  • 실행 관점 행동 항목
    • LayoutBuilder로 constraints.maxWidth/maxHeight를 먼저 확보하고, 게임 오브젝트 크기/좌표 로직을 그 값 기준으로 작성한다.[^12]
    • 움직이는 대상은 Positioned의 left/top/bottom에 “상수”가 아닌 “상태 변수”를 연결하고, 변경 시 setState()로 리빌드되게 만든다.[^13]
    • 애니메이션 도입 시 AnimationControllerinitState()에서 만들고, vsync 에러가 나면 믹스인을 먼저 점검한다.[^15]

5. 헷갈리는 용어 정리[^22]

Stack: 여러 위젯을 겹쳐 올리고, 영역 전체를 활용해 배치할 수 있는 레이아웃. 게임처럼 오브젝트를 겹치거나 자유 위치 배치할 때 유리하다고 설명한다.[^5]
Positioned: Stack 안에서 자식 위젯의 위치를 left/top/right/bottom 같은 값으로 지정하는 위젯.[^6]
LayoutBuilder: 빌드 시점에 자식이 실제로 할당받은 영역의 제약(constraints)을 제공해, maxWidth/maxHeight로 크기를 계산할 수 있게 해주는 도구.[^12]
AnimationController: 애니메이션의 시간(duration)과 재생(forward 등)을 제어하는 컨트롤러.[^14]
Animation: 시간에 따라 변하는 값(여기서는 double)의 스트림/상태를 나타내는 객체로 설명된다.[^14]
Tween: begin~end 사이의 값 변화를 정의하는 도구(0→100 같은 범위 지정)로 소개된다.[^16]
vsync: 화면 프레임(예: 60fps)과 애니메이션 업데이트를 동기화해 깜빡임 없이 맞추는 개념으로 설명된다.[^15]
SingleTickerProviderStateMixin: vsync: this를 가능하게 하려면 State가 상속(믹스인)해야 하는 구성요소로, 에러 해결책으로 제시된다.[^15]



참고(콘텐츠 정보)^1

  • 제목: 플러터 모바일 앱 개발 | 모바일 앱 개발자를 위한 Flutter(플러터) 제대로 배우기 Part.4 중급2 핑퐁게임, 영화나라, 장보기 앱 | 취업·실무·창업 | 에어클래스^1
  • 채널: 에어클래스^1
  • 길이: 39분 28초^1
  • 링크: https://www.youtube.com/watch?v=NixfcD8M1KY^1
  • 제공된 구간: @[00:01] ~ @[39:25] (사용자 제공 트랜스크립트 기준)^1

[^3]: @[00:52]~@[01:20] 제목/레벨/스코어, 조작(윈도우 화살표/모바일 손가락 드래그) 설명 [^4]: @[01:27]~@[01:39] "공이 막대에 맞으면 튕기고… 못 쫓아가면… 끝나는 거죠" [^5]: @[01:49]~@[02:18] "스택… 포지션… 절대 좌표… 공 같은 경우 좌표로 움직여야…" [^6]: @[14:19]~@[17:55] Stack에 children, Positioned의 left/top/right/bottom으로 위치 지정 및 예시 [^7]: @[02:24]~@[02:41] "initState는 초기화… build는 다시 그리면서 여러 번 호출" [^8]: @[03:02]~@[04:09] 디폴트 프로젝트에서 코드 삭제, Scaffold/AppBar 타이틀 "핑퐁 게임 버전 1.0" [^9]: @[04:20]~@[04:34] "debug banner false… 사라졌죠" [^10]: @[04:51]~@[05:08] body에 Container, 색을 주면 게임 영역 [^11]: @[05:18]~@[10:27] Ball 위젯, diameter=50, Container+BoxDecoration, color(amber[400]), shape: circle [^12]: @[12:22]~@[13:44] Bat을 해상도 대응 위해 width/height를 매개변수로 받음 + @[22:01]~@[25:33] LayoutBuilder로 constraints, batWidth=width/4, batHeight=height/25 [^13]: @[18:26]~@[21:39] 게임 변수 선언(화면 크기, 공 x/y, 배트 x, 배트 크기) 및 Positioned에 변수 연결(setState 리빌드 언급 포함 @[27:04]) [^14]: @[28:05]~@[30:19] "애니메이션… 두 가지… Animation과 Controller… initState에서 1회 초기화" [^15]: @[31:13]~@[33:51] vsync 설명(60프레임 동기화), 에러→SingleTickerProviderStateMixin 필요, duration(seconds:3) [^16]: @[34:14]~@[35:56] Tween(begin/end), animate(controller), Animation 타입 맞춤, duration은 controller가 담당, start도 controller가 담당 [^17]: @[36:19]~@[37:34] 3초×60프레임=180프레임, 100/180≈0.58씩 증가 직관 설명 [^18]: @[37:40]~@[38:05] 애니메이션 값 변화 리스너/print로 확인 [^19]: @[38:19]~@[38:51] "스타트는… controller.forward()", 출력이 많이 나옴 [^20]: @[38:56]~@[39:25] cnt 변수 두고 cnt++, forward 직전에 cnt=0 초기화 [^21]: @[01:49]~@[02:18] Stack/Positioned 필요성 + @[22:01]~@[25:33] LayoutBuilder 기반 크기 산정 + @[28:05]~@[35:35] 애니메이션 구조 전개 [^22]: @[14:19]~@[17:10] Positioned 속성 설명 + @[22:28]~@[24:23] LayoutBuilder/constraints + @[31:13]~@[32:33] vsync/mixin 용어 맥락

← 프로젝트에서 보기