일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- AAB
- Flutter #플러터 #프로젝트 #파이어베이스 #파이어스토어 #Firebase #FireStore
- 초성게임앱 #플러터앱 #플러터카운트다운
- 플레이스토어 앱등록
- 유니티게임 #상점만들기 #뽑기구현 #케이디리듬게임
- Flutter #플러터 #모바일앱만들기 #GetX
- 복권번호예측기 #Flutter #adMob #광고배너 #리워드형광고
- flutter #android #androidstudio
- GetX #CustomScrollView #Flutter
- 복권번호예측 #Flutter #플러터 #Provider
- 앱번들
- 플러터 #Flutter #파이어베이스 #firebase #firestore #파이어스토어
- Today
- Total
이코노딩
[Flutter Project] 벌칙 모드 구현하기 본문
✔️ 게임 진행에 재미를 위해 벌칙모드를 만들기로 했다.
✅구상 기능
1️⃣벌칙모드를 끄고 킬수 있는 스위치
2️⃣벌칙이 무엇인지 볼 수 있는 바텀시트 및 리스트
3️⃣벌칙을 추가하거나 삭제 할 수 있는 기능
4️⃣기본 벌칙 리스트로 초기화 할 수 있는 리스트
5️⃣shared_preference로 종료후에도 계속 저장 될 수 있는 기능
6️⃣벌칙모드를 키고 게임 오버시 리스트 중 랜덤한 벌칙 지정
✅ 구현하기
1️⃣벌칙모드를 끄고 킬수 있는 스위치
☑️Code
Widget penaltySetting(){
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.auto_awesome, color: const Color(0xffaecdff), size: 24.r,),
SizedBox(width: 10.w,),
Text('벌칙 모드', style: TextStyle(fontSize: 16.sp),),
Obx(()=>
Row(
children: [
IconButton(
onPressed: () => Get.put(PenaltyController()).penaltyInfo(),
icon: Icon(Icons.question_mark_rounded, size: 24.r,color: const Color(0xffaecdff),) ),
Padding(
padding: EdgeInsets.all(16.r),
child: Switch(
activeColor: const Color(0xffaecdff),
onChanged: (bool value) {
changePenalty(value);
},
value: penaltyMode.value,
)
),
],
),
),
]);
}
기본 지원되는 위젯 중 Switch Widget 을 사용하고 옆에
IconButton을 이용해 벌칙모드 설정 페이지로 라우팅되게 구현 하였다.
2️⃣벌칙이 무엇인지 볼 수 있는 바텀시트 및 리스트
3️⃣벌칙을 추가하거나 삭제 할 수 있는 기능
4️⃣기본 벌칙 리스트로 초기화 할 수 있는 리스트
☑️Code
void penaltyInfo() {
Get.bottomSheet(
isScrollControlled: true,
WillPopScope(
onWillPop: (){
if(customizing.value){
Get.put(DialogController()).customAlert('저장 후 나가주세요.');
}else{
Get.back();
}
return Future(() => false);
},
child: SingleChildScrollView(
physics: const ScrollPhysics(),
child: Container(
padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 0),
height: Get.height * 0.8,
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Color(0xffaecdff), width: 5
),
left: BorderSide(
color: Color(0xffaecdff), width: 5
),
right: BorderSide(
color: Color(0xffaecdff), width: 5
),
),
),
child: Obx( ()=> Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('벌칙모드란 ?', style: TextStyle(fontSize: 24.sp,fontWeight: FontWeight.bold, fontFamily: 'OneTitle'),),
IconButton(
icon: Icon(Icons.cancel, size: 40.r, color: const Color(0xffaecdff),),
onPressed: () {
if(customizing.value){
Get.put(DialogController()).customAlert('저장 후 나가주세요.');
}else{
Get.back();
}
},
)
],
),
Padding(padding: EdgeInsets.only(bottom: 15.h),
child: Divider(
endIndent: Get.width/2,
thickness: 1,
color: const Color(0xff6c6c6c),
),),
Text('게임 오버 시 아래 리스트 중\n벌칙이 랜덤하게 생성되요.',style: TextStyle(fontSize: 18.sp),),
Align(
alignment: Alignment.topRight,
child: SizedBox(height: 50.h,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text('${Get.put(PenaltyController()).showList.length} 개',style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.blueAccent,),),
SizedBox(width: 10.w,),
Visibility(
visible: Get.put(PenaltyController()).customizing.value,
child: TextButton(onPressed: (){Get.put(DialogController()).customReset();},
child: Text('초기화',style: TextStyle(color: Colors.redAccent,fontSize: 18.sp),)),
),
],
),
TextButton(onPressed: (){
if(Get.put(PenaltyController()).customizing.value){
if(Get.put(PenaltyController()).showList.isEmpty){
Get.put(DialogController()).customAlert('벌칙리스트가 비어있습니다');
}else{
Get.put(PenaltyController()).savePenalty(Get.put(PenaltyController()).showList);
Get.put(DialogController()).saveCustom();
}
}
Get.put(PenaltyController()).changeCustomizing();
},
child:
Text( Get.put(PenaltyController()).customizing.value ?
'저장' : '편집하기', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.blueAccent,),
),
)
],
),),
),
],
),
AnimatedContainer(
height: Get.put(PenaltyController()).customizing.value ? 50.h : 0,
duration: const Duration(milliseconds: 600),
child: Visibility(
visible: Get.put(PenaltyController()).customizing.value,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 40.h,
width: Get.width - 100.h,
padding: EdgeInsets.all(5.0.r),
child: TextField(
textAlignVertical: TextAlignVertical.center,
controller: Get.put(PenaltyController()).customTextController,
decoration: const InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xffdddddd),
)
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xffaecdff),
)
)
)
),
),
Expanded(
child: GestureDetector(
onTap: (){
Get.put(PenaltyController()).showList.add(Get.put(PenaltyController()).customTextController.text);
//FocusScope.of(context).unfocus();
Get.put(PenaltyController()).customTextController.clear();
},
child: Container(
padding: EdgeInsets.all(5.r),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: const Color(0xffaecdff)
),
child: Text('입력', style: TextStyle(fontSize:10.sp, color: Colors.white),textAlign: TextAlign.center,),
),
),
)
],
),
),
),
Expanded(
child: ListView.separated(
physics: const ScrollPhysics(),
itemBuilder: (BuildContext context, int index){
return Container(
height: 25.h,
alignment: Alignment.bottomLeft,
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: Color(0xffaecdff),
width: 0.5
)
)
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(Get.put(PenaltyController()).basicList[index], style: const TextStyle(fontSize: 18)),
Visibility(
visible: Get.put(PenaltyController()).customizing.value,
child: IconButton(
icon: const Icon(Icons.delete_forever_outlined, color: Colors.redAccent,),
onPressed: (){
Get.put(PenaltyController()).showList.removeAt(index);
},
),)
],
),
);
},
separatorBuilder: (BuildContext context, int index){
return SizedBox(height: 7.h,);
},
itemCount: Get.put(PenaltyController()).showList.length
),
),
SizedBox(height: 10.h,),
],
),
),
),
),
)
);
}
벌칙모드의 설명과 리스트를 표시해주고 리스트가 비어있을때는 에러 방지를 위해 저장하지 못하게 구현,
그리고 저장하지 않고 나가기를 시도할 경우 알려주고 나가기를 막아놨다.
그 외 초기화 버튼 클릭시 경고 알람 저장 버튼 클릭시 저장 알람이 뜨도록 구현하였다.
❌개선 할 점
1. 텍스트 필드를 사용할 때, 키패드 외 터치 시 unfocus가 되는 기능은 필수적이라고 생각함.
하지만 GetX는 context를 사용하지 않기 때문에 unfocus하기 위한 코드가 복잡해짐.
Class로 구현 후, transition효과를 주기로 함.
2. ListView를 사용하면 Size를 지정하든가 List의 길이에 따라 size를 정해주었는데 그냥 CustomScrollView를 사용하기로 함
3. 텍스트 필드위치를 아래쪽으로 옮겨 시각적으로 확인이 쉽게 바꾸려 함.
☑️Code
class PenaltyInfo extends StatelessWidget {
PenaltyInfo({Key? key}) : super(key: key);
final controller = Get.put(PenaltyController());
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
resizeToAvoidBottomInset: true,
body: WillPopScope(
onWillPop: (){
if(controller.customizing.value){
Get.put(DialogController()).customAlert('저장 후 나가주세요.');
}else{
Get.back();
}
return Future(() => false);
},
child: GestureDetector(
onTap: ()=> FocusScope.of(context).unfocus(),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Color(0xffaecdff), width: 5
),
left: BorderSide(
color: Color(0xffaecdff), width: 5
),
right: BorderSide(
color: Color(0xffaecdff), width: 5
),
),
),
child: Column(
children: [
Expanded(
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
SliverAppBar(
backgroundColor: const Color(0xffffffff),
automaticallyImplyLeading: false,
toolbarHeight: 130.h,
centerTitle: false,
floating: true,
pinned: false,
elevation: 0,
flexibleSpace: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('벌칙모드란 ?', style: TextStyle(fontSize: 24.sp,fontWeight: FontWeight.bold, fontFamily: 'OneTitle'),),
IconButton(
icon: Icon(Icons.cancel, size: 40.r, color: const Color(0xffaecdff),),
onPressed: () {
if(controller.customizing.value){
Get.put(DialogController()).customAlert('저장 후 나가주세요.');
}else{
Get.back();
}
},
)
],
),
Padding(padding: EdgeInsets.only(bottom: 15.h),
child: Divider(
endIndent: Get.width/2,
thickness: 1,
color: const Color(0xff6c6c6c),
),),
Text('게임 오버 시 아래 리스트 중\n벌칙이 랜덤하게 생성되요.',style: TextStyle(fontSize: 18.sp),),],
),
),
),
SliverAppBar(
elevation: 0,
backgroundColor: const Color(0xffffffff),
automaticallyImplyLeading: false,
pinned: true,
floating: false,
toolbarHeight: 52.h,
flexibleSpace: Obx(
()=> Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w,vertical: 5.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${controller.showList.length} 개',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.sp, color: const Color(0xffaecdf0)),),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
GestureDetector(
onTap: ()=> controller.changeCustomizing(),
child: Text(
controller.customizing.value ? '저장' : '편집',
style: TextStyle(
fontSize: 20.sp,
fontWeight: controller.customizing.value ? FontWeight.bold : FontWeight.normal,
color: controller.customizing.value ? const Color(0xffaecdff) : const Color(0xffb666ff)
),
),
),
SizedBox(width: 20.w,),
Visibility(
visible: controller.customizing.value,
child: DelayedWidget(
animation: DelayedAnimations.SLIDE_FROM_LEFT,
animationDuration: const Duration(milliseconds: 500),
delayDuration: const Duration(milliseconds: 300),
child: GestureDetector(
onTap: ()=> Get.put(DialogController()).customReset(),
child: Icon(Icons.replay, size: 24.r, color: const Color(0xffFE2E2E),)
)
),
),
],
)
],
),
),
)
),
Obx(
()=> SliverFixedExtentList(
itemExtent: 65.h,
delegate: SliverChildBuilderDelegate(
(context, index) =>
DelayedWidget(
delayDuration: Duration(milliseconds: (100 * controller.showList.length) - (100*index)),
animation: DelayedAnimations.SLIDE_FROM_LEFT,
animationDuration: const Duration(milliseconds: 500),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h),
child: Container(
padding: EdgeInsets.only(left: 10.w),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xffaecdff)),
borderRadius: BorderRadius.circular(12),
),
child:Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(controller.showList[index], style: TextStyle(fontSize: 18.sp)),
Obx(
()=> Visibility(
visible: controller.customizing.value,
child: IconButton(
icon: const Icon(Icons.delete_forever_outlined, color: Colors.redAccent,),
onPressed: (){
controller.showList.removeAt(index);
controller.update();
},
),),
)
],
),
),
)
),
childCount: controller.showList.length
),
),
),
],
),
),
BottomAppBar(
elevation: 0,
child: Obx(
()=> AnimatedContainer(
alignment: Alignment.center,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
color: Color(0xffaecdff)
),
duration: const Duration(milliseconds: 600),
height: controller.customizing.value ? 60.h : 0.h,
width: Get.width,
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: Row(
children: [
Expanded(
child: TextField(
maxLength: 20,
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: 16.sp, color: Colors.white),
controller: controller.customTextController,
cursorColor: const Color(0xffffffff),
decoration: InputDecoration(
counter: Container(),
enabledBorder:const UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xffdddddd),
)
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Color(0xffaecdff),
)
)
)
),
),
GestureDetector(
onTap: (){
controller.showList.insert(0,controller.customTextController.text);
controller.customTextController.clear();
controller.scrollController.animateTo(
controller.scrollController.position.minScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.fastOutSlowIn);
},
child: Container(
padding: EdgeInsets.all(5.r),
decoration: BoxDecoration(
color: const Color(0xffffffff),
border: Border.all(color: const Color(0xffaecdff)),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: const Icon(Icons.arrow_upward, color: Color(0xffaecdff),),
),
)
],
),
),
),
),
),
)
],
),
),
),
),
),
);
}
}
5️⃣shared_preference로 종료후에도 계속 저장 될 수 있는 기능 및 함수
☑️Code
@override
void onInit(){ //init
super.onInit();
loadPenalty();
basicList = ['무반주 댄스 30초 동안 추기', '노래 3소절 부르기',
'옆 테이블(모르는 사람)한테 인사하기', '이번만 봐줄게', '숨겨왔던 썰 하나만',
'오른쪽에 앉은 사람 부탁 하나 들어주기', '왼쪽 사람한테 딱밤 맞기', '인디언 밥!',
'귀여운척 한번 해줘','자신있는 성대모사 하기', '왼쪽 사람이 골라주는 노래 맞춰 춤추기',
'첫사랑 이야기 해줘',
];
//after prefs load
if(customList.isEmpty){
showList.value = basicList;
}else{
showList.value = customList;
}
}
//reset btn F
void resetList(){
showList.clear();
basicList = ['무반주 댄스 30초 동안 추기', '노래 3소절 부르기',
'옆 테이블(모르는 사람)한테 인사하기', '이번만 봐줄게', '숨겨왔던 썰 하나만',
'오른쪽에 앉은 사람 부탁 하나 들어주기', '왼쪽 사람한테 딱밤 맞기', '인디언 밥!',
'귀여운척 한번 해줘','자신있는 성대모사 하기', '왼쪽 사람이 골라주는 노래 맞춰 춤추기',
'첫사랑 이야기 해줘',
];
showList.addAll(basicList);
savePenalty(basicList);
}
loadPenalty() async{
prefs = await SharedPreferences.getInstance();
customList = (prefs?.getStringList('custom') ?? []);
}
savePenalty(List<String> list) async{
prefs = await SharedPreferences.getInstance();
prefs?.setStringList('custom', list);
}
Controller가 생성될 때 shared_preference를 이용해 load할 데이터가 있는 지 확인 후 데이터가 없으면 기본 벌칙리스트가 보여지고 있다면 load된 데이터가 보여지도록 하였다.
basicList를 함수마다 초기화 해주었는데, 삭제 가능한 부분이다.
6️⃣벌칙모드를 키고 게임 오버시 리스트 중 랜덤한 벌칙 지정
☑️Code
void gameOver(int n){
audio.audioGameover();
Get.dialog(
WillPopScope(
onWillPop: ()=>Future(() => false),
child: AlertDialog(
backgroundColor: Colors.transparent,
content: Container(
height: 370.h,
width: Get.width * 0.8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Colors.white,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.warning, color: Colors.red,size: 35.r,),
Text('GameOver', style: TextStyle(fontSize: 20.sp,fontWeight: FontWeight.bold, color: Colors.red),),
SizedBox(height: 20.h,),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(Get.put(SettingController()).playerList[Get.put(WordGameController()).currentPlayer.value].name, style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold),),
Text(' 탈락!', style: TextStyle(fontSize: 18.sp),),
],
),
SizedBox(height: 10.h,),
Get.put(SettingController()).penaltyMode.value ?
Column(
children: [
Text('▼ 벌칙 ▼',style: TextStyle(fontSize: 15.sp , color: Colors.indigoAccent),textAlign: TextAlign.right,),
SizedBox(height: 5.h,),
Text(Get.put(PenaltyController()).showList[Random().nextInt(Get.put(PenaltyController()).showList.length-1)],style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold , color: Colors.red),textAlign: TextAlign.center,),
],
) :
Text('다음에 더 잘해봐요!', style: TextStyle(fontSize: 18.sp),),
SizedBox(height: 30.h,),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: ()=> Get.back(closeOverlays: true),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Container(
decoration: BoxDecoration(
color: const Color(0xffaecdff),
borderRadius: BorderRadius.circular(50),
),
padding: EdgeInsets.fromLTRB(30.w, 20.h, 30.w, 20.h),
child: Center(child: Text('나가기', style: TextStyle(fontSize: 15.sp, color: Colors.white),),),
),
),
),
GestureDetector(
onTap: (){
if(n==1){
Get.put(WordGameController()).makeTitle();
Get.put(TimerController()).time.value = int.parse(Get.put(SettingController()).selectedTimer.value);
Get.put(TimerController()).start();
}else if(n==2){
Get.put(BombGameController()).makeTitle();
Get.put(TimerController()).time.value = Get.put(SettingController()).selectedRandomTime.value;
Get.put(TimerController()).start();
}
Get.back();
},
child: Padding(
padding: EdgeInsets.only(right: 16.w),
child: Container(
decoration: BoxDecoration(
color: const Color(0xffaecdff),
borderRadius: BorderRadius.circular(50),
),
padding: EdgeInsets.fromLTRB(30.w, 20.h, 30.w, 20.h),
child: Center(child: Text('다시하기', style: TextStyle(fontSize: 15.sp,color: Colors.white),),),
),
),
)
],
),
SizedBox(height: 20.h,),
Text('다시하기를 누르면 게임이', style: TextStyle(fontSize: 13.sp),),
Text('[ ${(Get.put(SettingController()).playerList[Get.put(WordGameController()).currentPlayer.value].name)} ]님부터 시작됩니다.', style: TextStyle(fontSize: 13.sp),),
],
),
),
),
));
}
SettingController내의 벌칙모드의 bool값을 확인 후 그냥 랜덤하게 텍스트로 나오게하였다.
✅결과
'프로젝트 > 애미티' 카테고리의 다른 글
[Flutter Project] FireBase와 앱 연동해보기 <앱 등록> (0) | 2023.03.23 |
---|---|
[Flutter Project] Carouse_Slider Package (0) | 2023.03.23 |
[Flutter Project] DropDown Button2 Package (0) | 2023.03.15 |
[Flutter Project] Scroll Navigation Package (0) | 2023.03.15 |
[Flutter Project] 애미티앱 GetX 적용하기 - 초성게임 (0) | 2023.03.07 |