用 set 與 get 保存個別狀態¶
章首頁已經確認問題來自資料歸屬,現在直接改寫地鼠。每個 GridObject 除了座標、素材與顯示狀態之外,還有一塊可以自由存放資料的空間:set(key, value) 以一個字串名稱(key)存入數值,get(key, output) 再依同一個名稱把數值讀回變數。這些資料跟著物件走,不同物件即使使用相同的 key,也互不干擾。
讓多隻地鼠保存自己的時間¶
以下的 UpdateMole 不再使用全域 next_move,而是把移動間隔 "interval" 與下一次移動時間 "nextMove" 存在每隻地鼠身上。函式開頭先把兩個值讀出來,判斷是否到了移動時間;移動後再把新的 nextMove 存回去。
void UpdateMole(GameEngine game, GridObject self) {
double interval = 1.0;
double next_move = 0.0;
self.get("interval", interval);
self.get("nextMove", next_move);
if (game.time() < next_move) return;
self.setPosition(game.random(0, game.cols() - 1), game.random(0, game.rows() - 1));
self.set("nextMove", game.time() + interval);
}
建立地鼠時,為每一隻存入不同的移動間隔,就能讓牠們以不同速度移動,而不必為相同行為寫出多個只因數值不同而換名字的函式:
const double intervals[3] = {1.0, 0.8, 1.4};
for (int i = 0; i < 3; ++i) {
GridObject mole = game.addObject("mole", nullptr, UpdateMole);
mole.setPosition(i * 2 + 1, i * 2 + 1);
mole.set("interval", intervals[i]);
mole.set("nextMove", 0.0);
}
這項修改沒有改變 Engine 看待物件的方式,三隻地鼠仍然使用同一個 UpdateMole。真正改變的是資料的歸屬:self 每次指向目前輪到的那隻地鼠,所以 self.set("nextMove", ...) 只會修改那一隻的時間,不會再干擾另外兩隻。
把上面兩段放進第 4 章里程碑二的 main.cpp(取代原本的全域 next_move 與單隻地鼠)後編譯執行,三個紅色方塊應以各自的節奏換位。把某一隻的間隔改成 0.2 再執行,只有那一隻會明顯加快,這就證明三份計時器已經分開。
可以保存哪些型別¶
set() 會記住第一次存入時的型別,之後的 set() 與 get() 都必須使用同一種型別:
| 存入的值 | 讀取時使用的變數 |
|---|---|
整數,例如 self.set("timer", 0) |
int 或 long long |
浮點數,例如 self.set("speed", 1.5) |
double |
布林值,例如 self.set("active", true) |
bool |
字串,例如 self.set("name", "ghost") |
std::string |
std::vector<T> |
相同型別的 std::vector<T> |
型別錯誤時,Grid++ 會丟出含有 key 名稱的錯誤訊息,而不是悄悄把資料轉成別的數值。最常見的錯誤是把整數與浮點數混在一起:self.set("nextMove", 0) 的 0 是整數,之後再用 double 讀取就會出錯。想保存的是小數時,初始值請寫成 0.0。
讀不到資料時¶
get() 會回傳一個整數,告訴我們有沒有找到資料:找到時回傳 0 並把數值寫入第二個參數;找不到時回傳 1,並把第二個參數設為該型別的預設值(數字為 0、布林值為 false、字串為空字串)。
這種「0 代表成功」的慣例和許多 C 語言函式相同。多數情況下,只要在建立物件時先 set() 好初始值,就能直接使用 get() 而不必檢查回傳值;上面的地鼠便是如此,而第 5 章用 "type" 辨認碰撞對象時,也利用了「找不到就得到空字串」這項規則。
保存一串資料¶
需要保存多個同型別數值時,例如一條預先規劃好的巡邏路線,可以存入 std::vector:
std::vector<int> path_x = {1, 2, 3, 3, 3};
self.set("pathX", path_x);
std::vector<int> saved;
self.get("pathX", saved);
vector 會被完整複製進物件,之後修改原本的 path_x 不會影響物件內的那一份;要更新時,先 get() 取出、修改後再 set() 存回去。使用 vector 時記得加上 #include <vector>。
判斷資料該放在哪裡¶
set()/get() 與全域變數並不互相取代,選擇標準是資料描述的對象:
| 情況 | 建議方式 |
|---|---|
| 整局只有一份,例如分數、遊戲階段、剩餘時間 | 全域變數 |
| 所有物件都要讀取的共享資料,例如迷宮 | 全域變數 |
| 每個物件各有一份,例如計時器、方向、生命值 | self.set()/self.get() |
物件的種類或角色,例如 "pacman"、"ghost" |
set("type", ...) |
完整 Pac-Man 範例中的四隻鬼各自保存移動計時器、方向與策略,玩家則保存目前方向與下一個想轉向的方向,因此兩者都把這些資料存在自己身上;剩餘豆子數與遊戲階段屬於整局,則使用全域變數。同一個遊戲同時使用兩種方式是正常的,只要每個選擇都符合資料的實際歸屬。
Overlay 也能保存資料
Overlay 擁有和 GridObject 相同的 set() 與 get(),規則完全一樣。例如一個會閃爍的提示文字,可以把自己的閃爍計時器存在身上。
本節小結¶
set(key, value)把資料存進物件,get(key, output)依相同 key 讀回。- 同一個 key 的型別固定;小數的初始值要寫成
0.0。 get()找到時回傳 0,找不到時回傳 1 並給出預設值。- 屬於單一物件的資料放在物件上,屬於整局的資料使用全域變數。