본문 바로가기
게임개발/Qt

[QT] 애니팡 - 게임 규칙 구현하기

by 거북이 코딩 2024. 8. 27.

게임 규칙 구현하기

Consts.h
#pragma once

#include <string>

namespace Consts
{
	const std::string paths[]{ // 이미지 파일 절대 경로
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\1.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\2.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\3.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\4.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\5.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\6.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\7.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\8.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\9.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\10.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\11.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\12.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\13.png)",
		R"(C:\\Users\\4rest\\Desktop\\Shin\\QtGame\\threeMatchImage\\PNG\\ico\\14.png)",
	};
	const int BOARD_LENGTH = 12;
	const int BOARD_ITEM_SIZE = 60;
	const int MAX_ITEM_TYPES = 11;
}

아이템 종류 개수를 제어하기 위한 상수로 MAX_ITEM_TYPES를 추가하였다.

Board.h
#pragma once

#include <QtWidgets/QGraphicsScene>
#include <QtWidgets/QGraphicsPixmapItem>
#include <QtWidgets/QGraphicsRectItem>

#include <vector>
#include <random>
#include <set>

#include "Item.h"

using MatchPair = std::pair<int, int>;
using MatchSet = std::set<MatchPair>;

class Board : public Item::EventListener // EventListener로 제어하기 위해 인터페이스 상속
{
private:
	QGraphicsScene* _scene;
	QGraphicsRectItem _root; // 보드판 묶음
	std::vector<std::vector<Item*>> _items; // item 배열
	std::random_device _device; // 난수 생성
	std::mt19937 _gen;

public:
	Board(QGraphicsScene* scene);
	~Board();
	void addItem(int row, int column);
	void removeItem(int row, int column);

	void moveItem(int fromRow, int fromColumn, int toRow, int toColumn);
	void moveItem(Item* item, int toRow, int toColumn); // 아이템 위치 이동

	void exchangeItems(int row0, int column0, int row1, int column1); // 아이템 교환
	bool refresh();
	MatchSet matchedItems() const;
	MatchSet matchedItems(int row, int column) const;
	MatchSet matchedItemsHorizontal(int row, int column) const;
	MatchSet matchedItemsVertical(int row, int column) const;

	virtual void itemDragEvent(Item* item, Item::Direction dir) override; // 오버라이딩
};

/*
	items
	1 2 3 -> _items[0], row0
	4 5 6 -> _items[1], row1
	7 8 9 -> _items[2], row2

	_items[0][0] -> 1
	_items[0][1] -> 2
	_items[0][2] -> 3
*/

타입 앨리어스 설정, moveItem 오버로딩, refresh, matched 함수 정의

Board.cpp
#include <random>

#include "Board.h"
#include "Consts.h"
#include "Item.h"

Board::Board(QGraphicsScene* scene)
	: _scene(scene) // scene 설정
	, _gen(_device()) // 난수 생성
{
	_scene->addItem(&_root); // scene에 보드판 묶음 추가

	_root.setX(_scene->sceneRect().width() / 2 // 보드판 위치 설정
		- Consts::BOARD_ITEM_SIZE * Consts::BOARD_LENGTH / 2);
	_root.setY(_scene->sceneRect().height() / 2
		- Consts::BOARD_ITEM_SIZE * Consts::BOARD_LENGTH / 2);

	for (int row = 0; row < Consts::BOARD_LENGTH; ++row) // vector에 item 넣기
	{
		std::vector<Item*> rowItems(Consts::BOARD_LENGTH);
		_items.push_back(rowItems); // 행 삽입

		for (int column = 0; column < Consts::BOARD_LENGTH; ++column)
		{
			addItem(row, column); // 각 열에 item 추가
		}
	}

	while(refresh()); // 처음에 생기는 3match 제거
}

Board::~Board()
{
	for (int row = 0; row < _items.size(); ++row)
	{
		for (int column = 0; column < _items[row].size(); ++column)
		{
			removeItem(row, column);
		}
	}
}

void Board::addItem(int row, int column)
{

	std::uniform_int_distribution<int> dis(0, Consts::MAX_ITEM_TYPES); // 난수를 균등하게 생성
	const std::string& path = Consts::paths[dis(_gen)];

	Item* item = new Item(this, path, row, column, &_root); // EventListener로 자신을 전달, 오버라이딩한 함수가 호출가능해짐.

	item->setPos(column * Consts::BOARD_ITEM_SIZE, row * Consts::BOARD_ITEM_SIZE); // 부모위치를 기준으로 item 위치 설정

	_items[row][column] = item; // 배열에 item 삽입
}

void Board::removeItem(int row, int column)
{
	auto* item = _items[row][column];

	if (item == nullptr) // 존재하지 않는다면 종료
		return;

	_items[row][column] = nullptr; // 벡터 원소 해제
	item->setParentItem(nullptr); // 부모 정보 해제
	_scene->removeItem(item); // scene에서 해제
	delete item; // 동적할당 해제
}

void Board::moveItem(int fromRow, int fromColumn, int toRow, int toColumn)
{
	Item* item = _items[fromRow][fromColumn];
	if (item == nullptr)
		return;
	moveItem(item, toRow, toColumn);
}

void Board::moveItem(Item* item, int toRow, int toColumn)
{
	item->setRow(toRow); // 좌표값 변경
	item->setColumn(toColumn);
	item->setPos(toColumn * Consts::BOARD_ITEM_SIZE, toRow * Consts::BOARD_ITEM_SIZE);
	_items[toRow][toColumn] = item; // 벡터내에서 변경
}

void Board::exchangeItems(int row0, int column0, int row1, int column1)
{
	Item* item0 = _items[row0][column0];
	Item* item1 = _items[row1][column1];

	moveItem(item0, row1, column1); // 교환
	moveItem(item1, row0, column0);
}

bool Board::refresh()
{
	MatchSet matched = matchedItems(); // 3개 이상 연속된 아이템 모음
	if (matched.size() < 3) // 3개 이하라면 종료
		return false;

	for (const auto& pair : matched) // 3개 이상이라면 아이템 제거
	{
		removeItem(pair.first, pair.second);
	}

	// 빈곳에 아이템 내리기
	for (int column = 0; column < _items[0].size(); ++column)
	{
		for (int row = _items.size() - 1; row >= 0; --row)
		{
			if (_items[row][column] != nullptr)
			{
				continue;
			}
			for (int i = row - 1; i >= 0; --i)
			{
				if (_items[i][column] != nullptr)
				{
					moveItem(i, column, row, column);
					_items[i][column] = nullptr;
					break;
				}
			}
		}
	}

	// 새 아이템 채우기
	for (int column = 0; column < _items[0].size(); ++column)
	{
		for (int row = 0; row < _items.size(); ++row)
		{
			if (_items[row][column] == nullptr)
			{
				addItem(row, column);
			}
			else
			{
				break;
			}
		}
	}
	return true;
}

// 3개이상 연속된 아이템을 찾아서 리턴하는 함수
MatchSet Board::matchedItems() const
{
	MatchSet matched;
	for (int row = 0; row < _items.size(); ++row)
	{
		for (int column = 0; column < _items[row].size(); ++column)
		{
			MatchSet m = matchedItems(row, column);
			if (m.size() >= 3)
			{
				matched.insert(m.begin(), m.end());
			}
		}
	}

	return matched;
}

// 한개의 아이템을 기준으로 가로세로 검사한다.
MatchSet Board::matchedItems(int row, int column) const
{
	MatchSet horizontalMatched = matchedItemsHorizontal(row, column);
	MatchSet verticalMatched = matchedItemsVertical(row, column);
	MatchSet matched;
	if (horizontalMatched.size() >= 3)
		matched.insert(horizontalMatched.begin(), horizontalMatched.end());
	if (verticalMatched.size() >= 3)
		matched.insert(verticalMatched.begin(), verticalMatched.end());
	return matched;
}

// 가로 검사
MatchSet Board::matchedItemsHorizontal(int row, int column) const
{
	Item* item = _items[row][column];
	if (item == nullptr)
		return {};

	MatchSet leftMatched;
	for (int i = column - 1; i >= 0; --i)
	{
		if (_items[row][i] && _items[row][i]->path() == item->path())
		{
			leftMatched.insert({ row,i });
		}
		else
		{
			break;
		}
	}

	MatchSet rightMatched;
	for (int i = column + 1; i < _items[row].size(); ++i)
	{
		if (_items[row][i] && _items[row][i]->path() == item->path())
		{
			rightMatched.insert({ row,i });
		}
		else
		{
			break;
		}

	}

	if (leftMatched.size() + rightMatched.size() + 1 >= 3)
	{
		leftMatched.insert(rightMatched.begin(), rightMatched.end());
		leftMatched.insert({ row,column });
		return leftMatched;
	}
	else
	{
		return {};
	}
}

// 세로 검사
MatchSet Board::matchedItemsVertical(int row, int column) const
{
	Item* item = _items[row][column];
	if (item == nullptr)
		return {};

	MatchSet upMatched;
	for (int i = row - 1; i >= 0; --i)
	{
		if (_items[i][column] && _items[i][column]->path() == item->path())
		{
			upMatched.insert({ i,column });
		}
		else
		{
			break;
		}
	}

	MatchSet downMatched;
	for (int i = row + 1; i < _items.size(); ++i)
	{
		if (_items[i][column] && _items[i][column]->path() == item->path())
		{
			downMatched.insert({ i,column });
		}
		else
		{
			break;
		}

	}

	if (upMatched.size() + downMatched.size() + 1 >= 3)
	{
		upMatched.insert(downMatched.begin(), downMatched.end());
		upMatched.insert({ row,column });
		return upMatched;
	}
	else
	{
		return {};
	}
}

void Board::itemDragEvent(Item* item, Item::Direction dir)
{
	int row0 = item->row(); // 기존 아이템
	int column0 = item->column();

	int row1 = row0; // 교환할 아이템
	int column1 = column0;

	switch (dir) // 방향
	{
	case Item::Direction::Left:
		column1--;
		break;
	case Item::Direction::Right:
		column1++;
		break;
	case Item::Direction::Up:
		row1--;
		break;
	case Item::Direction::Down:
		row1++;
		break;
	}

	if (row1 < 0 || column1 < 0) // 보드판 바깥 예외처리
		return;
	if (row1 >= Consts::BOARD_LENGTH || column1 >= Consts::BOARD_LENGTH)
		return;

	Item* item1 = _items[row1][column1];
	if (item1 == nullptr)
		return;

	exchangeItems(row0, column0, row1, column1); // 교환
	while (refresh());
}

같은 아이템 3개가 모이면 사라지고 위에서 블록이 내려와 채우는 로직을 구현했다. 한번 리프레쉬된 이후에 또 3개가 생길 수 있기 때문에 3개가 만들어지지 않을 때까지 반복한다.

Item.h, Item.cpp, main.cpp 는 큰 변화가 없었다.

마치며

가장 오른쪽 열에 노란 사탕 세개가 만들어 질 수 있다. 노란 사탕이 사라지면서 위에 있던 아이템이 내려왔다

사실 오늘 애니메이션 구현하는 것까지 했었는데 Qt 버전차이 때문에 버그가 너무 많이 나와서 일단 게임 규칙 구현까지 기록했습니다. 쉽지 않네요 ㅠㅠ