게임 화면에서 재화가 실시간으로 갱신되게 만들었다. 서버가 값이 바뀔 때마다 TCP 연결로 클라이언트에 보내는데 가끔 화면 숫자와 실제 값이 달랐다.
Table of contents
Open Table of contents
값을 밀어 주고 있었다
서버가 이렇게 보냈다.
{"type": "gold_changed", "gold": 12400}
클라이언트는 받은 값을 화면에 넣는다.
void onGoldChanged(int gold) {
m_gold = gold;
updateGoldLabel();
}
onGoldChanged 는 간단한데 몇 가지 경우에 어긋났다.
먼저 순서가 뒤바뀐다. 처음에는 이것을 전송 탓으로 봤는데 아니었다. TCP 는 보낸 차례대로 받는 쪽에 올려 주는 것이 규격이라 한 연결로만 보내는 한 도착 순서는 안 뒤집힌다.
뒤집히는 자리는 보내기 전이었다. 두 변경이 서로 다른 처리 흐름에서 만들어지면 어느 쪽이 먼저 소켓에 쓰이는지가 안 정해지고 나중에 일어난 변경이 먼저 쓰이면 그 순서 그대로 도착한다.
다음은 놓침이다. 연결이 잠깐 끊기면 그 사이 gold_changed 를 못 받고 다시 붙어도 놓친 값은 안 온다.
마지막은 부분 전달이다. 골드는 오는데 보석은 안 오는 경우가 있었는데 보내는 코드가 항목마다 따로였다.
신호만 보내게 바꿨다
값 대신 바뀌었다만 보냈다.
{"type": "wallet_changed"}
클라이언트는 이것을 받으면 다시 조회한다.
void onWalletChanged() {
requestWallet(); // 서버에 현재 값을 다시 물어본다
}
이러면 앞의 세 가지가 해결된다. wallet_changed 를 몇 번 받든 마지막에 한 번 조회하면 현재 값이고 다시 붙었을 때 한 번 조회하면 맞춰지며 조회는 전체를 받아 온다.
잔액의 실체는 DB 에 있으니 requestWallet 이 그것을 그대로 읽어 오는 것이라 화면과 원장이 어긋날 자리가 없어진다.
조회가 늘어나는 것을 막았다
신호를 받을 때마다 requestWallet 을 부르면 요청이 는다. 연속으로 여러 번 바뀌면 그만큼 조회한다.
void onWalletChanged() {
if (m_walletDirty) return; // 이미 예약됨
m_walletDirty = true;
scheduleOnce(schedule_selector(Wallet::doFetch), 0.3f);
}
void Wallet::doFetch(float dt) {
m_walletDirty = false;
requestWallet();
}
m_walletDirty 로 짧은 시간 안의 신호를 묶었다. 0.3초 안에 신호가 열 번 와도 조회는 한 번이다.
판단 기준 — 무엇을 신호로 보낼지
전부 신호로 바꾸면 조회가 너무 느니 기준을 뒀다.
| 성격 | 방식 |
|---|---|
| 정확해야 하고 자주 안 바뀜 | 신호 → 조회 |
| 자주 바뀌고 조금 틀려도 됨 | 값을 밀어 줌 |
| 한 번만 나오는 것 | 값을 밀어 줌 |
재화는 첫 번째라 requestWallet 으로 정확한 값을 받아야 한다. 다른 사람 위치 같은 것은 두 번째로 초당 여러 번 바뀌고 조금 어긋나도 티가 안 난다. 채팅 메시지는 세 번째인데 그 자체가 내용이라 조회할 것이 없다.
연출용 숫자까지 DB 를 읽으러 가면 조회만 늘고 얻는 것이 없다.
갱신 중 표시와 실패 처리
wallet_changed 를 받고 조회 결과가 오기까지 시간이 있고 그동안 화면은 옛 값이다.
void onWalletChanged() {
m_goldLabel->setOpacity(128); // 흐리게
...
}
void onWalletReceived(int gold) {
m_gold = gold;
updateGoldLabel();
m_goldLabel->setOpacity(255); // 원래대로
}
setOpacity 로 갱신 중이라는 것이 보인다. 값이 잠깐 안 맞아도 사용자가 이해한다.
신호는 받았는데 조회가 실패하면 화면이 계속 옛 값이다.
void onWalletFailed() {
if (m_retry < 3) {
m_retry++;
scheduleOnce(schedule_selector(Wallet::doFetch), 1.0f * m_retry);
return;
}
m_goldLabel->setString("--"); // 값을 못 가져옴
}
m_retry 로 간격을 늘려 가며 세 번 재시도하고 그래도 안 되면 setString 으로 표시를 남겼다. 옛 값을 그대로 보여 주는 것보다 모름이 낫다고 판단했는데 옛 값이면 사용자가 그것을 믿고 행동한다.
정리
- 바뀐 값을 밀어 주면 순서와 놓침과 부분 전달 문제가 생긴다
- 순서가 어긋나는 자리는 전송이 아니라 보내기 전이다
TCP는 한 연결에서 보낸 차례대로 올려 준다- 값 대신 바뀌었다는 사실만 보낸다
- 받는 쪽이 조회하면 세 가지가 한 번에 해결된다
- 조회는 항상 전부를 가져오므로 부분 문제가 없다
- 조회가 늘지 않게 짧은 시간의 신호를 묶는다
- 전부 이렇게 하지 않고 정확도와 빈도로 방식을 가른다
- 신호와 조회 사이에 옛 값이 보이므로 갱신 중임을 표시한다
- 조회가 계속 실패하면 옛 값 대신 모름을 보인다