好傢伙,
在遊戲開發,尤其是後端服務的構建過程中,我們常常從一個簡單的想法或原型開始。
代碼直接、功能明確,一切看起來都很好。但隨着項目複雜度的提升,最初的“簡潔”設計往往會變成“僵化”的枷鎖。
0.需求分析
我想我需要一張地圖,作用如下:
1.記錄所有人的位置,
2.快速的拿到某個角色的信息
3.快速拿到某個位置所有角色的信息
4.某個角色在釋放技能時進行索敵,
1.戰場模型
使用一個json文件來描述我們的戰場
{
"mapId": "standard_24_lanes",
"name": "標準24格戰場",
"positions": [
{ "id": 0, "zone": "friendly", "lane": "back" },
{ "id": 1, "zone": "friendly", "lane": "back" },
// ...
{ "id": 6, "zone": "friendly", "lane": "front" },
// ...
{ "id": 12, "zone": "enemy", "lane": "front" },
// ...
{ "id": 18, "zone": "enemy", "lane": "back" }
// ...
]
}
2.建立戰場數據模型
package models
// PositionLayout 定義了單個位置的靜態屬性
type PositionLayout struct {
ID int `json:"id"`
Zone string `json:"zone"`
Lane string `json:"lane"`
}
// MapLayout 代表整個地圖的靜態佈局
type MapLayout struct {
MapID string `json:"mapId"`
Name string `json:"name"`
Positions []PositionLayout `json:"positions"`
}
3.初始化戰場代碼
// BattlePosition 代表戰鬥中一個位置的動態狀態。
type BattlePosition struct {
Layout *models.PositionLayout // 引用靜態佈局信息
Fighters []*Fighter // 存儲當前站在此位置的戰鬥者
}
// Fight 管理兩個戰鬥者之間的戰鬥狀態。
type Fight struct {
Team1 []*Fighter
Team2 []*Fighter
Log strings.Builder
DataLog models.DataLog
round int
Battlefield []*BattlePosition // Battlefield 是一個切片,索引直接對應位置ID
FightersByID map[string]*Fighter // 新增一個用於快速查找的 map
}
// NewFight 創建並初始化一個新的戰鬥實例。
func NewFight(team1Chars, team2Chars map[int]models.Character, layout *models.MapLayout) *Fight {
f := &Fight{
Team1: []*Fighter{},
Team2: []*Fighter{},
DataLog: models.DataLog{Rounds: []models.Round{}},
Battlefield: make([]*BattlePosition, len(layout.Positions)),
FightersByID: make(map[string]*Fighter), // 初始化map
}
// 1. 根據佈局初始化戰場
for i, posLayout := range layout.Positions {
// 複製一份,避免指針問題
layoutCopy := posLayout
f.Battlefield[i] = &BattlePosition{
Layout: &layoutCopy,
Fighters: []*Fighter{}, // 初始化為空
}
}
// 2. 創建戰鬥者並放置到地圖上
placeFighter := func(char models.Character, pos int) *Fighter {
charCopy := char // 創建副本以確保每個fighter有自己的character實例
fighter := &Fighter{
Character: &charCopy,
CurrentHP: charCopy.Attributes.HP,
Position: pos,
IsAlive: true,
}
// 將戰鬥者添加到對應位置的Fighters列表中
if pos >= 0 && pos < len(f.Battlefield) {
f.Battlefield[pos].Fighters = append(f.Battlefield[pos].Fighters, fighter)
}
f.FightersByID[charCopy.HeroID] = fighter // 使用HeroID作為key
return fighter
}
for pos, char := range team1Chars {
fighter := placeFighter(char, pos)
f.Team1 = append(f.Team1, fighter)
}
for pos, char := range team2Chars {
fighter := placeFighter(char, pos)
f.Team2 = append(f.Team2, fighter)
}
return f
}
4.分析
這麼做會有兩個顯而易見的好處:高效與清晰的查詢
針對兩個需求,根據位置找人,或根據玩家id找人
對比我們的舊方法 : 遍歷所有角色,檢查每個角色的 Position 字段是不是xx,遍歷所有角色,檢查每個角色的 HeroID 字段
而現在我們只需要
// 直接通過索引訪問,就像查字典一樣精準
fightersAtPos := f.Battlefield[11].Fighters
// 直接通過Key查找,一步到位
fighter, found := f.FightersByID["hero-111-111"]
噢,這太棒了
5.補充: make()方法説明
make() 是Go語言的一個內置函數,它的作用是預先分配內存並初始化一個特定類型的對象,
主要用於三種類型:切片(slices)、映射(maps)和通道(channels)
make(...)作用:告訴go,請在內存中給我分配一塊連續的空間,這個空間的長度要xxx
| 代碼 | 類型 | 作用 | 現實比喻 |
|---|---|---|---|
| make([]*BattlePosition, 24) | 切片 (Slice) | 創建一個有24個空位的、固定長度的列表。 | 建造一個有24個格子的空貨架。 |
| make(map[string]*Fighter) | 映射 (Map) | 創建一個空的、可動態增長的鍵值對存儲結構。 | 準備一個空的、可以隨時存取檔案的檔案櫃。 |