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

[QT] 애니팡 - 애니메이션 넣기

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

들어가며

 이번 애니메이션 넣기는 어제부터 시도했는데 오류가 계속 발생해서 정말 힘들게 버그를 잡아냈습니다. 그래도 버그를 잡아내니 참 뿌듯하네요. 그리고 모든 코드를 넣기보단 새롭게 추가된 코드 위주로 넣었습니다.

애니메이션 넣기

Consts.h
// 애니메이션 시간 1칸당 200ms
const int ANIMATION_TIME = 200;
Item.h
// 애니메이션을 위한 QObject 상속
class Item : public QGraphicsPixmapItem, public QObject

// EventListener 에 itemMoveFinished 추가
virtual void itemMoveFinished(Item* item0, Item* item1, bool canRevert) = 0;
Board.h
// 3match가 일어나지 않을 경우 교환이 취소되는 flag 추가
void exchangeItems(int row0, int column0, int row1, int column1, bool canRevert);

// 오버라이딩
virtual void itemMoveFinished(Item* item0, Item* item1, bool canRevert) override; 
Item.cpp
// 애니메이션으로 움직이는 함수 기존의 setPos()를 대체함
void Item::moveTo(double toX, double toY)
{
	double diffX = toX - x();
	double diffY = toY - y();

	double time = 0;
	time += qAbs(diffX) / Consts::BOARD_ITEM_SIZE * Consts::ANIMATION_TIME;
	time += qAbs(diffY) / Consts::BOARD_ITEM_SIZE * Consts::ANIMATION_TIME;

	QTimeLine* timer = new QTimeLine(time);

	QGraphicsItemAnimation* animation = new QGraphicsItemAnimation();
	animation->setItem(this);
	animation->setTimeLine(timer);
	animation->setPosAt(0, pos());
	animation->setPosAt(1, QPointF(toX, toY));
	
	connect(timer, &QTimeLine::finished, [this, timer, animation]() {
		timer->deleteLater();
		animation->deleteLater();

		_listener->itemMoveFinished(this, nullptr, false);
		});

	timer->start();
}

// 드래그로 발생하는 moveTo 오버로딩
void Item::moveTo(Item* other, bool canRevert)
{
	double toX = other->x();
	double toY = other->y();

	double diffX = toX - x();
	double diffY = toY - y();

	double time = 0;
	time += qAbs(diffX) / Consts::BOARD_ITEM_SIZE * Consts::ANIMATION_TIME;
	time += qAbs(diffY) / Consts::BOARD_ITEM_SIZE * Consts::ANIMATION_TIME;

	QTimeLine* timer = new QTimeLine(time);

	QGraphicsItemAnimation* animation = new QGraphicsItemAnimation();
	animation->setItem(this);
	animation->setTimeLine(timer);
	animation->setPosAt(0, pos());
	animation->setPosAt(1, QPointF(toX, toY));
	
	connect(timer, &QTimeLine::finished, [this, other,  timer, animation, canRevert]() {
		timer->deleteLater();
		animation->deleteLater();

		_listener->itemMoveFinished(this, other, canRevert);
		});

	timer->start();
}
Board.cpp
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;
				}
			}
		}
	}

	// 순회 하며 열마다 빈곳의 개수를 저장한다.
	std::vector<int> emptyCounts;
	for (int column = 0; column < _items[0].size(); ++column)
	{
		int emptyCount = 0;
		for (int row = 0; row < _items.size(); ++row)
		{
			if (_items[row][column] == nullptr)
			{
				emptyCount++;
			}
			else
			{
				break;
			}
		}
		emptyCounts.push_back(emptyCount);
	}

	// 새 아이템 채우기
	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);

				Item* item = _items[row][column];
				item->setY(item->y() - emptyCounts[column] * Consts::BOARD_ITEM_SIZE); // 빈곳의 개수만큼 위에서 떨어진다.
				moveItem(row, column, row, column);
			}
			else
			{
				break;
			}
		}
	}
	return true;
}

// 아이템의 이동이 끝났을때
void Board::itemMoveFinished(Item* item0, Item* item1, bool canRevert)
{
	if (--_moveCount > 0) // 애니메이션이 실행중이라면
	{
		return;
	}

	if (refresh()) // 3match가 일어났다면
		return;

	if(!canRevert) // 이미 취소된 상황일 경우
		return;

	if (item0 == nullptr || item1 == nullptr) // 둘중 하나가 널일 경우
		return;

	// revert exchange // 3match가 일어나지 않았다면 행동이 취소된다.
	exchangeItems(item0->row(), item0->column(), item1->row(), item1->column(), false);
}

main.cpp 는 변화가 없었습니다.

마치며

 이번 챕터에서 발생한 버그는 애니메이션 때문이었습니다. 애니메이션이 종료되지 않은 시점에서 다른 애니메이션이 실행되면서 이미 삭제된 오브젝트에 접근하는 등의 이유가 원인이었습니다. 그래서 Board의 멤버에 _moveCount를 추가하여 애니메이션이 시작하기 전에 ++, 애니메이션이 끝나기 전에 -- 하여 관리하였습니다. 그래서 애니메이션이 끝났을 때만 refresh()를 해줌으로써 함수의 중복호출 등을 막았습니다. 그리고 문제가 되었던 부분은 이렇습니다.

itemDragEvent
{
	// 교환이 일어남
    // itemMove 함수 호출
    	// 내부적으로 refresh()됨
        
    // refresh() <-- 중복되어 버그가 발생함 삭제해야함
}

 내부적으로 refresh()를 실행했는데 중복으로 실행하게 되면서 버그가 난 것이었습니다. 버그가 난 이유는 refresh()가 실행 중에 다시 refresh() 되면 삭제된 오브젝트에 접근하게 되기 때문입니다. 두 번째 이유는 다음과 같습니다.

exchangeItem
{
    // item1 -> item0 자리로 이동 후 refresh()
    // item0 -> item1 자리로 이동 후 refresh()
    // 교환은 한번만 일어났는데 두번의 refresh()가 발생함
}

 두 블록이 교환되면서 내부적으로 refresh()를 하게 되면 두 번 발생하게 됩니다. 이것을 막기 위해 _moveCount를 설정하여 한 번의 refresh()만 일어나게 했습니다.

 다사다난하고 버그 잡느라 머리카락 좀 빠진 챕터지만 간단한 게임이라도 처음 만들어보며 느끼는 바가 많았습니다. C++로 게임을 짜게 되는 흐름이라던지, 버그를 찾았을 때의 기쁨이라던지, 못 찾을 때의 슬픔이라던지.. 간단하지만 게임을 만들어 보며 배우게 되는 것이 많은 것 같습니다.

 

플레이 영상

 영상을 통해 3 match가 되면서 블록이 내려오는 애니메이션, 3 match가 일어나지 않았을 때 동작이 취소되는 애니메이션을 볼 수 있습니다.