好傢伙,

在遊戲開發,尤其是後端服務的構建過程中,我們常常從一個簡單的想法或原型開始。

代碼直接、功能明確,一切看起來都很好。但隨着項目複雜度的提升,最初的“簡潔”設計往往會變成“僵化”的枷鎖。


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)

創建一個空的、可動態增長的鍵值對存儲結構。

準備一個空的、可以隨時存取檔案的檔案櫃。