好家伙,
在游戏开发,尤其是后端服务的构建过程中,我们常常从一个简单的想法或原型开始。
代码直接、功能明确,一切看起来都很好。但随着项目复杂度的提升,最初的“简洁”设计往往会变成“僵化”的枷锁。
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) | 创建一个空的、可动态增长的键值对存储结构。 | 准备一个空的、可以随时存取档案的档案柜。 |