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
강의는 “재밌는 것”으로 핑퐁 게임을 만들겠다고 시작한다.^1 여기서 말하는 핑퐁은 벽돌깨기처럼 벽돌이 있는 게임을 완성하는 게 아니라, 공이 튀기는 애니메이션 중심의 간단한 게임을 만든다는 의미로 소개된다.^1
화면 구성은 다음 요소들을 포함한다.^2
- 상단에 핑퐁 게임 제목(버전 1.0 형태)
- 레벨(Level) 표시
- 스코어(Score) 표시
- 화면 안에서 **공(ball)**이 움직임
- 하단에 좌우로 움직이는 막대(bar, bat)
조작 방식은 플랫폼에 따라 다르게 설명한다.[^3]
- 윈도우 같은 환경이면 화살표 키로 막대를 좌우 이동할 수 있을 것이라고 언급한다.[^3]
- 모바일에서는 손가락으로 화면을 짚고 좌우로 드래그해서 막대를 움직이게 할 것이라고 설명한다.[^3]
게임 규칙의 윤곽도 함께 제시한다.[^4]
- 공이 막대에 “딱 맞으면” 튕긴다(반사).[^4]
- 공을 막대가 따라가지 못해 화면 아래쪽(바닥)에 부딪히면 게임이 끝난다는 식으로 실패 조건을 설명한다.[^4]
[!IMPORTANT] 오늘의 학습 포인트
강의자는 이번 파트에서 주로 애니메이션 기능을 많이 배우게 될 것이라고 명시한다.^2 또한 “간단한 게임을 운용하는 방법”까지 다루겠다고 예고한다.^2
3.2 왜 Stack인가: 움직이는 게임 오브젝트는 절대좌표가 필요하다[^5]
강의자는 공과 배트 같은 오브젝트를 화면 위에서 자유롭게 움직이기 위해, 위젯을 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]
강의 도중 “StatefulWidget에서 initState()와 build()가 어떻게 동작하는지”를 다시 짚는다.[^7]
initState()는 초기화 시 한 번만 호출된다.[^7]build()는 화면을 다시 그릴 때마다 여러 번 호출된다.[^7]
이 구분이 중요한 이유는, 뒤에서 애니메이션 컨트롤러 같은 객체를 매번 만들지 않고 초기화 시 1회 설정해야 하기 때문이라는 흐름으로 연결된다.[^14]
3.4 프로젝트 시작: 디폴트 프로젝트를 비우고 기본 화면(Scaffold/AppBar) 만들기[^8]
강의자는 미리 만들어 둔 “아무것도 없는 디폴트 프로젝트”에서 출발한다.[^8] 그리고 기본 생성 코드(불필요한 부분)를 지우고, MyHomePage를 StatelessWidget으로 두며 Scaffold를 구성한다.[^8]
구체적으로 진행하는 작업 흐름은 이렇다.[^8]
- 기존 코드 “필요 없으니 다 지운다”.[^8]
MyHomePage를 만들고 Scaffold를 둔다.[^8]- AppBar에 타이틀을 넣는데, 아까 화면 예시에서 본 것처럼 **“핑퐁 게임 버전 1.0”**을 넣는다.[^8]
- 실행해서 정상 표시를 확인한다.[^8]
debugShowCheckedModeBanner: false(디버그 배너 제거)를 적용해 화면에서 배너가 사라지는 것도 확인한다.[^9]
이 시점에서 “게임은 Scaffold의 body 안에서 이뤄지면 될 것”이라고 방향을 잡는다.[^9]
3.5 게임 영역(Body) 컨테이너와 색: “이 영역이 게임판이 된다”[^10]
다음 단계로 body에 컨테이너를 하나 넣고, 배경색을 주면 그 영역이 “게임 영역”이 된다고 설명한다.[^10] 즉, 게임이 진행되는 시각적 공간을 우선 확보하는 단계다.[^10]
3.6 공(Ball) 위젯 만들기: 원형은 BoxDecoration + shape: circle[^11]
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(...))color는BoxDecoration안으로 넣는다(예:Colors.amber[400]처럼 0~900 농도 중간값 사용).[^^11]- 핵심은
shape: BoxShape.circle설정이다.[^11]
이렇게 하면 화면에 공이 원형으로 나타난다고 확인한다.[^11]
[!TIP] 원형/모서리 처리의 기본 패턴
Container 자체는 사각형이므로, 원이나 둥근 모서리는 보통decoration: BoxDecoration(...)에서shape또는borderRadius로 처리한다.[^11]
3.7 배트(Bat) 위젯 만들기: 사각형 막대 + 해상도 대응(가변 크기)[^12]
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]
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,bottomwidth,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]
이제 “벽에 튀기고 바닥에 튀기는” 등의 로직을 하려면, 해당 영역의 정보(넓이/높이)와 오브젝트 좌표가 필요하다고 말한다.[^13] 그래서 게임에 필요한 변수들을 State에 선언한다.[^13]
강의에서 잡는 변수들은 다음 흐름이다.[^13]
3.9.1 게임 영역 크기(width/height)
- “전체 화면”이 아니라 “게임 영역의 크기”를 구하겠다고 한다.[^13]
double width = 0; double height = 0;식으로 초기값을 0으로 둔다.[^13]
3.9.2 볼 위치: positionX, positionY
공은 움직임에 left와 top만 있으면 되므로 좌표 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]
강의자는 게임 영역을 구할 때 단순히 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]
강의자는 이제 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]
이제 공이 움직이는 것을 시뮬레이션하기 위해 애니메이션을 붙이겠다고 한다.[^14] 강의자는 애니메이션을 위해 필요한 것이 “크게 두 가지”라고 못 박는다.[^14]
- Animation: 실제로 변하는 값(예: double)이 시간에 따라 변화하는 결과를 담는다.[^14]
- AnimationController: 애니메이션을 시작/정지/진행시키고, duration 등 “시간”을 제어한다.[^14]
그래서 State에 변수를 선언한다.[^14]
late Animation<double> animation;late AnimationController controller;(널이 아님을 보장하기 위해late사용 언급)[^14]
그리고 이 두 객체는 매번 만드는 것이 아니라, 보통 화면이 처음 그려질 때 딱 한 번 실행되는 initState()에서 초기화한다고 설명한다.[^14]
3.13 AnimationController 초기화: duration과 vsync, 그리고 SingleTickerProviderStateMixin[^15]
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]
컨트롤러가 “시간”을 제공한다면, 애니메이션은 “어떤 값이 어디서 어디까지 바뀌는가”를 정의한다고 설명한다.[^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]
강의자는 숫자를 통해 애니메이션이 실제로 어떻게 변하는지 직관을 제공한다.[^17]
- 화면이 60fps라고 가정한다.[^15]
- 애니메이션 시간이 3초이면, 총 프레임 수는 대략
3초 × 60 = 180 프레임이라고 계산한다.[^17] - 값이 0에서 100까지 180회에 걸쳐 바뀌면, 한 번 업데이트될 때 평균 증가량은
100 / 180 ≈ 0.55~0.58정도라고 추정해 말한다.[^17] - 따라서 매 프레임마다 값이 “탁탁탁” 조금씩 올라가며 Tween 효과가 나타난다고 설명한다.[^17]
이 설명은 “애니메이션이 실제로는 프레임마다 조금씩 값이 바뀌는 반복”임을 이해시키는 역할을 한다.[^17]
3.16 애니메이션 리스너와 출력 확인: 값 변화 이벤트가 여러 번 호출된다[^18]
강의자는 애니메이션의 값이 바뀔 때마다 어떤 이벤트 리스너를 지정할 수 있다고 말한다.[^18] 그리고 변화가 실제로 일어나는지 확인하기 위해 print를 찍어보는 흐름으로 간다.[^18]
- 애니메이션이 진행되는 동안 여러 번 호출되며, 콘솔에 출력이 “엄청 나온다”고 말한다.[^19]
또한 “아직 스타트는 안 된다”는 점을 강조한다.[^19] 애니메이션과 컨트롤러를 만들어놓기만 했지, 재생을 시작시키지 않았기 때문이다.[^19]
3.17 애니메이션 시작은 controller.forward(): 0→100으로 진행[^19]
애니메이션을 실제로 시작시키려면 컨트롤러를 사용해야 한다고 말한다.[^19]
controller.forward()를 호출하면 “직진(포워드)”이므로 0에서 100까지 쭉 진행한다고 설명한다.[^19]
그리고 실행하면 출력이 쏟아지는 것을 통해 “진짜로 애니메이션이 재생되며 값이 계속 업데이트됨”을 확인한다.[^19]
3.18 호출 횟수 카운트: CNT 변수를 두고 증가시키기[^20]
마지막 부분에서 강의자는 출력이 몇 번 발생하는지 세어보자는 식으로, 카운터 변수를 둔다.[^20]
int cnt = 0;같은 변수를 두고[^20]- 리스너/콜백에서
cnt++로 증가시키며[^20] forward()하기 직전에cnt = 0으로 초기화하는 흐름을 만든다.[^20]
이로써 애니메이션이 프레임/틱 단위로 여러 번 호출됨을 “횟수”로도 확인하려는 의도를 보여준다.[^20]
4. 핵심 통찰[^21]
- [h 게임 UI는 “흐름 레이아웃(Column/Row)”보다 “좌표 레이아웃(Stack/Positioned)”이 본질적으로 맞다] 공/배트처럼 계속 움직이는 대상은 절대좌표가 필요하고, Stack/Positioned가 이를 가장 सीधे(직접적으로) 지원한다.[^5]
- [h 해상도 대응은 위젯 내부 상수값이 아니라, 게임 영역을 측정한 뒤 비율로 계산하는 게 안전하다] LayoutBuilder의 constraints로 “실제 할당 영역”을 얻고, 그 값을 기반으로 배트 크기 같은 것을 정한다.[^12]
- [h 애니메이션은 ‘값의 변화(Animation)’와 ‘시간/재생 제어(AnimationController)’가 역할 분담된다] Tween은 값 범위를, Controller는 duration과 forward 같은 재생을 담당한다.[^14]
- [h vsync는 선택이 아니라 구조 요구사항이다]
vsync: this를 쓰려면 State가 ticker provider를 제공해야 하며, 이를 위해SingleTickerProviderStateMixin이 필요하다는 점을 에러-해결 흐름으로 각인시킨다.[^15] - [m 애니메이션을 이해하는 쉬운 방식은 “프레임마다 값이 조금씩 증가한다”로 환원하는 것이다] 3초×60fps=180프레임 같은 계산은 Tween/Controller가 추상적인 개념이 아니라 반복 업데이트라는 감각을 준다.[^17]
- 실행 관점 행동 항목
- LayoutBuilder로
constraints.maxWidth/maxHeight를 먼저 확보하고, 게임 오브젝트 크기/좌표 로직을 그 값 기준으로 작성한다.[^12] - 움직이는 대상은 Positioned의
left/top/bottom에 “상수”가 아닌 “상태 변수”를 연결하고, 변경 시setState()로 리빌드되게 만든다.[^13] - 애니메이션 도입 시
AnimationController를initState()에서 만들고,vsync에러가 나면 믹스인을 먼저 점검한다.[^15]
- LayoutBuilder로
5. 헷갈리는 용어 정리[^22]
Stack: 여러 위젯을 겹쳐 올리고, 영역 전체를 활용해 배치할 수 있는 레이아웃. 게임처럼 오브젝트를 겹치거나 자유 위치 배치할 때 유리하다고 설명한다.[^5]
Positioned: Stack 안에서 자식 위젯의 위치를 left/top/right/bottom 같은 값으로 지정하는 위젯.[^6]
LayoutBuilder: 빌드 시점에 자식이 실제로 할당받은 영역의 제약(constraints)을 제공해, maxWidth/maxHeight로 크기를 계산할 수 있게 해주는 도구.[^12]
AnimationController: 애니메이션의 시간(duration)과 재생(forward 등)을 제어하는 컨트롤러.[^14]
Animation
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