게임 구성 설정

이전 강의
만약 이전 강의를 보고오지 않았다면 이해할 수 없는 내용들이 많습니다.
이전 강의를 보지 않은 분들을 위 이미지를 클릭하여 강의를 보고 와주십시오.

✨게임 보드판 미리보기


이번 포스팅에서는 게임 보드판을 해당 화면처럼 구현할 것입니다. 보드판의 카드들은 돌려서 펼친것 처럼 구현하여 카드가 현재 몇장 남았는지를 보여줍니다. 아래 주요건물 현황과 버튼들, 카드 클릭 이벤트에 관한 내용은 다음 포스팅에서 다루겠습니다.

📐보드판 레이아웃 구조

class _GameScreenState extends State<GameScreen>
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
 
 ...
 String? selectedCardPath; // 카드 선택 관련 변수
 Map<String, int> cardCounts = {}; // 카드 개수 관련 Map
 ...
 late MiniVillGame game;
 
  
  void initState() {
    ...
    
    game = MiniVillGame(widget.numOfPlayers);
    cardCounts = Map.fromIterable(
      game.centerCards,
      key: (item) => item.imagePath,
      value: (item) => item.availableCount,
    );
  }

  
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    double sideSpaceWidth = screenWidth * 0.2;

    ...

    return Scaffold(
        body: Row(
          children: [
            //게임스크린 (플레이어 영역)
            Container(
              ...
            ),
            //게임스크린 (게임 보드 영역)
            Expanded(
                  child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 10.0),
                child: Stack(
                  children: [
                    Column(
                      children: [
                        // 그리드 뷰 영역
                        Expanded(
                          flex: 3,
                          child: LayoutBuilder(
                            builder: (BuildContext context,
                                BoxConstraints constraints) {
                              double itemWidth = constraints.maxWidth / 5;
                              double itemHeight = constraints.maxHeight / 3;
                              return GridView.builder(
                                physics: NeverScrollableScrollPhysics(),
                                gridDelegate:
                                    SliverGridDelegateWithFixedCrossAxisCount(
                                  crossAxisCount: 5,
                                  childAspectRatio: itemWidth / itemHeight,
                                ),
                                itemCount: game.centerCards.length,
                                itemBuilder: (context, index) {
                                  CenterBuildingCard card =
                                      game.centerCards[index];
                                  String cardImagePath = card.imagePath;
                                  return GestureDetector(
                                    onTap: () {
                                      setState(() {
                                        selectedCardPath = cardImagePath;
                                      });
                                    },
                                    child: Container(
                                      margin:
                                          EdgeInsets.symmetric(vertical: 5.0),
                                      decoration: BoxDecoration(
                                        border: Border.all(color: Colors.black),
                                      ),
                                      child: Align(
                                        alignment: Alignment.center,
                                        child: CardStack(
                                            cardImagePath: cardImagePath,
                                            cardCount:
                                                cardCounts[cardImagePath] ?? 6),
                                      ),
                                    ),
                                  );
                                },
                              );
                            },
                          ),
                        ),

                        Expanded(
                          flex: 1,
                          child: Row(
                            // 보드판 아래 주요 건물 현황 및 현재 가진돈, 버튼 2개 배치
                          ),
                        ),
                      ],
                    ),
                    if (selectedCardPath != null)
                      // 카드 클릭 위젯 만들기
                  ],
                ),
              )),
            //게임스크린 (주사위 영역)
            Container(
              ...
            ),
          ],
        ),
      );
  }
  // 커스텀 위젯 생성
  ... 

}

class CardStack extends StatelessWidget {
  final String cardImagePath; // 각 카드의 이미지 경로
  final int cardCount; // 카드의 수

  CardStack({required this.cardImagePath, required this.cardCount});

  
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: List.generate(cardCount, (index) {
        return TransformCard(
          rotation: 0.1 * (3 - index), // 3을 카드의 중간 값으로 사용
          imagePath: cardImagePath,
        );
      }),
    );
  }
}

class TransformCard extends StatelessWidget {
  final double rotation;
  final String imagePath; // 카드 이미지 경로

  TransformCard({
    required this.rotation,
    required this.imagePath,
  });

  
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) 
        double cardHeight = constraints.maxHeight * 0.9;

        return Transform.translate(
          offset: Offset(0, 0),
          child: Transform.rotate(
            angle: rotation,
            child: Container(
              height: cardHeight, // GridView의 높이의 90%
              child: Image.asset(
                imagePath,
                fit: BoxFit.fitHeight, // 이미지가 컨테이너에 맞게 조절됨
              ),
            ),
          ),
        );
      },
    );
  }
}

게임 보드판 레이아웃의 구조는 이렇게 생겼습니다. Stack으로 보드판 전체를 묶은 이유는 게임 페이지 레이아웃-1강의에서 말한 것처럼 카드를 선택했을때 카드 강조와 카드를 구매하기 위한 버튼을 표시하기 위해서 입니다. 이제부터 코드를 따로 떼어내서 자세히 설명드리겠습니다.

📘LayoutBuilder, GridView.builder

LayoutBuilder(
    builder: (BuildContext context,
        BoxConstraints constraints) {
      double itemWidth = constraints.maxWidth / 5;
      double itemHeight = constraints.maxHeight / 3;
      return GridView.builder(
        physics: NeverScrollableScrollPhysics(),
        gridDelegate:
        SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 5,
          childAspectRatio: itemWidth / itemHeight,
        ),
        itemCount: game.centerCards.length,
        itemBuilder: (context, index) {
          CenterBuildingCard card = game.centerCards[index];
          String cardImagePath = card.imagePath;
          return GestureDetector(
            onTap: () {
              setState(() {
                selectedCardPath = cardImagePath;
              });
            },
            child: Container(
              margin: EdgeInsets.symmetric(vertical: 5.0),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.black),
              ),
              child: Align(
                alignment: Alignment.center,
                child: CardStack(
                    cardImagePath: cardImagePath,
                    cardCount:
                    cardCounts[cardImagePath] ?? 6),
              ),
            ),
          );
        },
      );
    },
  ),

해당 레이아웃은 보드판에서 중앙 건물카드들을 배치하는 코드입니다.
해당 코드에서 LayoutBuilder는 주어진 constraints (제약조건) 내에서 위젯을 빌드하는데 사용됩니다. 다시 말해, 해당 위젯의 부모로부터 얼마나 많은 공간을 사용할 수 있는지에 대한 정보를 제공합니다.

플레이어 영역과, 주사위 영역이 Row위젯으로 양 옆의 20%씩 차지하고 있으므로 나머지 중앙의 60%의 영역 전체를 사용할 수 있습니다.

double itemWidth = constraints.maxWidth / 5;
double itemHeight = constraints.maxHeight / 3;

여기서 constraints는 이 위젯의 최대/최소 폭과 높이 정보를 제공합니다.

보드판의 카드를 반응형으로 5 X 3의 형태로 배치할 것이기 때문에 constraints를 이용하여 카드가 배치될 item의 너비와 높이를 이런식으로 설정해줍니다.


GridView.builder는 그리드 뷰를 구성하는 데 필요한 아이템들을 효율적으로 빌드하는 역할을 합니다. 생소한 내용들이 많으므로 한줄씩 설명드리겠습니다.


physics: NeverScrollableScrollPhysics()
: GridView는 기본적으로 스크롤을 지원하기 때문에 스크롤을 막아야 레이아웃이 깔끔해집니다.


gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( ... )
: 그리드뷰의 구조를 지정해주는 코드입니다.


crossAxisCount: 5
: 각 행마다 5개의 아이템이 오도록 만듭니다.


childAspectRatio: itemWidth / itemHeight
: 각 아이템의 너비 대 높이 비율을 앞서 구한 itemWidth와 itemHeight를 이용해서 지정해줍니다.


itemCount: game.centerCards.length
: 앞서 만든 클래스의 중앙 건물 카드의 갯수만큼 GridView의 아이템을 만듭니다.


itemBuilder: (context, index)
: GridView의 각 아이템을 빌드하는 역할을 합니다. 여기서 index는 0부터 itemCount-1만큼의 값을 가집니다. 예를 들자면 GridView의 첫번째 칸의 인덱스는 0, 5번째 칸의 인덱스는 4, 15번째 칸의 인덱스는 14가 지정되며 각 인덱스 값에 따라 그리드 뷰의 각 아이템을 빌드하는 것입니다.

itemBuilder: (context, index) {
   CenterBuildingCard card = game.centerCards[index];
   String cardImagePath = card.imagePath;
   return GestureDetector(
     onTap: () {
       setState(() {
         selectedCardPath = cardImagePath;
       });
     },
     child: Container(
       margin: EdgeInsets.symmetric(vertical: 5.0),
       decoration: BoxDecoration(
         border: Border.all(color: Colors.black),
       ),
       child: Align(
         alignment: Alignment.center,
         child: CardStack(
             cardImagePath: cardImagePath,
             cardCount:
             cardCounts[cardImagePath] ?? 6),
       ),
     ),
   );
 },

card변수에 인덱스 값에 알맞는 카드 정보를 집어넣습니다. 그리고 GridView의 아이템에 Container를 집어넣고 그 안에 CardStack을 가운데 정렬하여 집어넣습니다. 여기서 Container는 5px만큼의 수직 margin값과 검은색 테두리를 가지고 있습니다.


그리고 카드 선택 이벤트를 위해 GestureDetector를 사용하였습니다. 카드 선택 이벤트에 대한 내용은 다음 포스팅에서 다루겠습니다.

📕TransformCard, CardStack

class TransformCard extends StatelessWidget {
  final double rotation;
  final String imagePath; // 카드 이미지 경로

  TransformCard({
    required this.rotation,
    required this.imagePath,
  });

  
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        double cardHeight = constraints.maxHeight * 0.9;
        
        return Transform.translate(
          offset: Offset(0, 0),
          child: Transform.rotate(
            angle: rotation,
            child: Container(
              height: cardHeight, // GridView의 높이의 90%
              child: Image.asset(
                imagePath,
                fit: BoxFit.fitHeight, // 이미지가 컨테이너에 맞게 조절됨
              ),
            ),
          ),
        );
      },
    );
  }
}

class CardStack extends StatelessWidget {
  final String cardImagePath; // 각 카드의 이미지 경로
  final int cardCount; // 카드의 수

  CardStack({required this.cardImagePath, required this.cardCount});

  
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: List.generate(cardCount, (index) {
        return TransformCard(
          rotation: 0.1 * (3 - index), // 3을 카드의 중간 값으로 사용
          imagePath: cardImagePath,
        );
      }),
    );
  }
}

여기서 TransformCard는 카드를 돌리면서 펼치는 것을 표현할때 카드를 돌리는 것을 표현하기 위해 생성한 위젯이며 CardStackTransformCard를 여러장 겹쳐있도록 표현하기 위해서 만든 위젯입니다.


TransformCard에서 Transform.translate를 이용하여 카드의 위치를 조절합니다. 제자리에서 카드를 돌려 표현하기 때문에 위치를 조정하는 offset은 0,0으로 고정하고 각도는 0.3, 0.2, 0.1, 0, -0.1, -0.2도 만큼 회전하여 표현합니다. (1 = 90°, 0.1 = 9°)


여기서 카드를 표시할 Container의 높이는 부모 크기(GridView) 90%만큼 잡고 너비는 이미지의 비율에 맞춰 지정되도록 하였습니다.


이렇게 만든 TransformCard를 이용하여 CardStack위젯을 만듭니다. Stack위젯을 이용하여 TransformCard가 겹치도록 만들고 생성자로 받아오는 카드의 수만큼 카드를 겹치도록 생성해냅니다.

class _GameScreenState extends State<GameScreen>
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
 
 ...
 String? selectedCardPath; // 카드 선택 관련 변수
 Map<String, int> cardCounts = {}; // 카드 개수 관련 Map
 ...
 late MiniVillGame game;
 
  
  void initState() {
    ...
    
    game = MiniVillGame(widget.numOfPlayers);
    cardCounts = Map.fromIterable(
      game.centerCards,
      key: (item) => item.imagePath,
      value: (item) => item.availableCount,
    );
  }
...
return GridView.builder(
	...
	...
	itemBuilder: (context, index) {
	CenterBuildingCard card = game.centerCards[index];
 	String cardImagePath = card.imagePath;
	return GestureDetector(
		onTap: () {
			setState(() {
				selectedCardPath = cardImagePath;
			});
		},
		child: Container(
			...
			child: Align(
			alignment: Alignment.center,
			child: CardStack(
				cardImagePath: cardImagePath,
				cardCount: cardCounts[cardImagePath] ?? 6),
				),
			),
		);
	},
);

이렇게 만든 카드 스택은 해당 코드처럼 사용됩니다. 카드 갯수 관련 Map Instance를 만들고 중앙 건물 카드의 이미지 경로를 key로 사용하여 key값에 맞는 카드 정보의 남은 갯수를 value로 활용합니다. 그리하여 GridView 아이템으로 CardStack을 호출하여 매개변수로 GridView item 인덱스에 맞는 이미지 경로와 그 이미지 경로의 카드 남은 갯수 정보를 넘겨 겹쳐진 카드를 표현합니다.

cardCount: cardCounts[cardImagePath] ?? 6),코드에서 cardImagePathkey값이고 ?? 6key값의 valuenull값일때 default값으로 6을 가져오기 위해서 작성했습니다.




이제 다음 포스팅에는 하단 주요 건물 현황이 오는 위젯과 카드를 클릭했을때 강조효과를 나타내는 코드를 공부해보겠습니다. 기대해주세요😎

profile
백석대학교 소프트웨어학과 4학년 재학중

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN