들어가며
이번 애니메이션 넣기는 어제부터 시도했는데 오류가 계속 발생해서 정말 힘들게 버그를 잡아냈습니다. 그래도 버그를 잡아내니 참 뿌듯하네요. 그리고 모든 코드를 넣기보단 새롭게 추가된 코드 위주로 넣었습니다.
애니메이션 넣기
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가 일어나지 않았을 때 동작이 취소되는 애니메이션을 볼 수 있습니다.
'게임개발 > Qt' 카테고리의 다른 글
[QT] 애니팡 - 점수 UI 만들기 (완성) (2) | 2024.08.28 |
---|---|
[QT] 애니팡 - 게임 규칙 구현하기 (0) | 2024.08.27 |
[QT] 애니팡 - 마우스 이벤트 받기 (0) | 2024.08.20 |
[QT] 애니팡 - 이미지 배치하기 (0) | 2024.08.16 |