• 【精简改造版】大型多人在线游戏BrowserQuest服务器Golang框架解析(2)——服务端架构


    1.架构选型

    B/S架构:支持PC、平板、手机等多个平台

    2.技术选型

    (1)客户端web技术:

    (2)后台

    • NodeJS(或golang)

    • DB:MongoDB(Metrics)

    (3)通讯类型:websocket

    (4)通讯协议:[type(int), ……]

    3.服务架构类型

    单体架构

    4.数据结构

    4.1 实体类型

    实体分类

    编号

    类型

    说明

    Player

    1

    WARRIOR

    战士

    Mobs

    2

    RAT

    老鼠

    3

    SKELETON

    骷髅

    4

    GOBLIN

    妖精(哥布林)

    5

    OGRE

    食人魔

    6

    SPECTRE

    幽灵、妖怪

    7

    CRAB

    螃蟹

    8

    BAT

    蝙蝠

    9

    WIZARD

    巫师

    10

    EYE

    11

    SNAKE

    12

    SKELETON2

    骷髅2

    13

    BOSS

    14

    DEATHKNIGHT

    死亡骑士

    防具(Armors)

    20

    FIREFOX

    火狐

    21

    CLOTHARMOR

    布衣

    22

    LEATHERARMOR

    皮衣

    23

    MAILARMOR

    铠甲

    24

    PLATEARMOR

    鳞甲

    25

    REDARMOR

    红衣

    26

    GOLDENARMOR

    金色战甲

    Objects

    35

    FLASK

    烧瓶

    36

    BURGER

    汉堡

    37

    CHEST

    箱子

    38

    FIREPOTION

    魔药

    39

    CAKE

    蛋糕

    NPCs

    40

    GUARD

    卫兵

    41

    KING

    国王

    42

    OCTOCAT

    章鱼猫

    43

    VILLAGEGIRL

    村民(女)

    44

    VILLAGER

    村民(男)

    45

    PRIEST

    牧师

    46

    SCIENTIST

    科学家

    47

    AGENT

    特工

    48

    RICK

    干草堆

    49

    NYAN

    50

    SORCERER

    男巫师

    51

    BEACHNPC

    海滨NPC

    52

    FORESTNPC

    森林NPC

    53

    DESERTNPC

    沙漠NPC

    54

    LAVANPC

    火山NPC

    55

    CODER

    程序员

    Weapons

    60

    SWORD1

    剑1

    61

    SWORD2

    剑2

    62

    REDSWORD

    红剑

    63

    GOLDENSWORD

    金剑

    64

    MORNINGSTAR

    晨星

    65

    AXE

    斧子

    66

    BLUESWORD

    蓝剑

    4.2 地图定义

    字段

    类型

    初始值

    范围

    说明

    width

    int

    172

    地图宽

    height

    int

    314

    地图高

    collisions

    list[int]

    碰撞点

    doors

    list[object]

    doors.[].x

    int

    门x坐标

    doors.[].y

    int

    门y坐标

    doors.[].p

    int

    0/1

    doors.[].tcx

    int

    doors.[].tcy

    int

    doors.[].to

    string

    u/d/l/r

    门朝向

    doors.[].tx

    int

    目标x

    doors.[].ty

    int

    目标y

    checkpoints

    list[object]

    checkpoints.[].id

    int

    checkpoints.[].x

    int

    checkpoints.[].y

    int

    checkpoints.[].w

    int

    checkpoints.[].h

    int

    checkpoints.[].s

    int

    0/1

    roamingAreas

    list[object]

    移动区域

    roamingAreas.[].id

    int

    roamingAreas.[].x

    int

    roamingAreas.[].y

    int

    roamingAreas.[].width

    int

    roamingAreas.[].height

    int

    roamingAreas.[].type

    string

    rat、crab、goblin……

    怪物类型

    roamingAreas.[].nb

    int

    数量

    chestAreas

    list[object]

    箱子区域

    chestAreas.[].x

    int

    chestAreas.[].y

    int

    chestAreas.[].w

    int

    chestAreas.[].h

    int

    chestAreas.[].i

    list[int]

    箱子中ItemList

    chestAreas.[].tx

    int

    chestAreas.[].ty

    int

    staticChests

    list[object]

    静态箱子

    staticChests.[].x

    int

    staticChests.[].y

    int

    staticChests.[].i

    list[int]

    箱子中ItemList

    staticEntities

    object

    静态实体

    staticEntities.key

    int-string

    staticEntities.value

    string

    rat、crab、goblin……

    tilesize

    int

    16

    瓦片大小

    5.通讯协议

    5.1 消息类型定义

    客户端与服务器基于websocket连接进行数据收发,详细协议如下:

    通讯类型

    编号

    消息类型

    参数

    含义

    备注

    服务端-->客户端

    1

    WELCOME

    id,name,x,y,hp

    欢迎信息

    4

    MOVE

    id,x,y

    移动信息

    双向消息

    5

    LOOTMOVE

    id,item

    朝向ITEM移动捡取

    双向消息

    7

    ATTACK

    attacker,target

    攻击信息

    双向消息

    2

    SPAWN

    id,kind,x,y

    再生信息

    3

    DESPAWN

    id

    取消再生

    SPAWN_BATCH

    批量再生

    10

    HEALTH

    points,[isRegen]

    健康信息

    11

    CHAT

    id,text

    聊天信息

    双向消息

    13

    EQUIP

    id,itemKind

    装备信息

    14

    DROP

    mobId,id,kind,playersInvolved

    掉落信息

    15

    TELEPORT

    id,x,y

    传送信息

    16

    DAMAGE

    id,dmg

    伤害信息

    17

    POPULATION

    worldPlayers,totalPlayers

    人口数量信息

    19

    LIST

    列表信息

    22

    DESTROY

    id

    销毁信息

    18

    KILL

    mobKind

    杀死信息

    23

    HP

    maxHP

    生命信息

    24

    BLINK

    id

    闪烁

    客户端-->服务端

    0

    HELLO

    player.name,

    招呼

    4

    MOVE

    x,y

    移动

    双向消息

    5

    LOOTMOVE

    x,y,item.id

    移动捡取

    双向消息

    6

    AGGRO

    mob.id

    7

    ATTACK

    mob.id

    攻击

    双向消息

    8

    HIT

    mob.id

    开始攻击

    9

    HURT

    mob.id

    伤害

    11

    CHAT

    text

    聊天

    双向消息

    12

    LOOT

    item.id

    捡取

    15

    TELEPORT

    x,y

    传送

    双向消息

    20

    WHO

    ids

    信息查询

    21

    ZONE

    -

    区域切换

    玩家从一个区域走到另外区域

    25

    OPEN

    chest.id

    打开箱子

    26

    CHECK

    id

    确认

    5.2 协议交互流程

    6.类图

    • 一个世界包含一张地图【静态】

      • 一张地图包含若干ChestArea区域

        • 一个ChestArea区域包含若干Item对象

      • 一张地图包含若干MobArea区域

      • 一张地图包含若干CheckPoint

    • 一个世界包含若干Zone【动态】

      • 一个Zone包含若干NPC对象

      • 一个Zone包含若干Mob对象

      • 一个Zone包含若干Item对象

      • 一个Zone包含若干Player对象

    7.线程模型

    7.1 协程创建

    • 创建一个世界广播服务协程

    • 根据地图的区域个数,每个区域创建一个协程

    • 每个接入用户创建一个Handler协程,每个Handler协程创建一个PlayerHandleLoop协程

    7.2 协程通信

    (1)Handler协程与PlayerHandleLoop协程通过带缓冲PacketChan通信

    (2)Player读取解析PacketChan中的消息,逻辑处理后投递到所属区域对象的zone.EventCh

    (3)Player对象调用世界对象,将消息投递到world.BroadcastCh进行世界消息发送(如人数)

    (4)世界对象解析world.BroadcastCh中的消息,遍历所有区域对象,将消息投递到zone.EventCh

    (5)区域对象读取解析zone.EventCh中的消息,逻辑处理后调用Player对象send方法进行消息发送

    8.游戏详细处理逻辑分析

    8.1地图加载

    (1)通过json Unmarshal进行decode到Map结构体。

    (2)根据地图宽高和区域宽高,计算出区域个数

    (3)其中Map.collitions表示碰撞的点,结合地图宽高,初始化碰撞二维表

    (4)初始化checkpoint Map,checkpoint ID作为KEY。其中checkpoint.S为1的表示为起始区域

    8.2.物品掉落

    1. TypeCrab.ID: &MobProperty{
    2. Drops: map[string]int{
    3. "flask": 50,
    4. "axe": 20,
    5. "leatherarmor": 10,
    6. "firepotion": 5,
    7. },
    8. HP: 60,
    9. ArmorLevel: 2,
    10. WeaponLevel: 1,
    11. },

    Drops表示:flask:50%,axe:20%,leatherarmor:%10,firepotion:5%,不掉落5%

    算法:随机一个[0~99]的值,累计求和,判断是否在Drops区间,如果在则掉落对应物品,否则不掉落。

    8.3.物品捡取

    1. func (z *Zone) onLoot(e *Event) {
    2. itemID := e.Data[0].(int)
    3. p := z.PlayersMap[e.PlayerID]
    4. if p == nil {
    5. return
    6. }
    7. if item := z.ItemsMap[itemID]; item != nil {
    8. despawnEvent := AquireEvent(EventDespawn, itemID)
    9. z.broadcastZone(despawnEvent)
    10. item.IsDestroy = true
    11. if item.IsStatic {
    12. item.RespawnLater(z.EventCh)
    13. }
    14. kind := item.Kind
    15. if kind.ID == TypeFirePotion.ID {
    16. // TODO
    17. } else if IsHealingItem(kind) {
    18. amount := 0
    19. switch kind.ID {
    20. case TypeFlask.ID:
    21. amount = 40
    22. case TypeBurger.ID:
    23. amount = 100
    24. }
    25. if amount > 0 && !p.HasFullHealth() {
    26. p.ReginHealthBy(amount)
    27. healthEvent := AquireEvent(EventHealth, p.HP)
    28. _ = p.send(healthEvent)
    29. }
    30. } else if IsArmor(kind) || IsWeapon(kind) {
    31. equipEvent := AquireEvent(EventEquip, p.ID, kind.ID)
    32. z.broadcastZone(equipEvent)
    33. if IsArmor(kind) {
    34. p.equipArmor(kind.ID)
    35. p.updateHP()
    36. HPEvent := AquireEvent(EventHP, p.MaxHP)
    37. _ = p.send(HPEvent)
    38. } else {
    39. p.equipWeapon(kind.ID)
    40. }
    41. }
    42. }
    43. }

    捡取流程:

    通过EventDespawn消息广播消失;

    • 如果是静态物品,则触发定时重刷;

    • 如果是药品,则触发补血;

    • 如果是防具,则广播装备并根据当前防具类型更新当前用户血条;

    • 如果是武器广播装备的同时并装备。

    8.4.mob跟随

    1. func (m *Mob) ChaseTarget(zoneID string, mp *Map, targetX, targetY int) {
    2. zid := mp.GetGroupIDFromPosition(targetX, targetY)
    3. if zoneID != zid {
    4. m.X, m.Y = targetX, targetY
    5. } else {
    6. pointsAround := make([][2]int, 0)
    7. for _, p := range [][2]int{
    8. [2]int{targetX, targetY + 1},
    9. [2]int{targetX + 1, targetY},
    10. [2]int{targetX, targetY - 1},
    11. [2]int{targetX - 1, targetY},
    12. } { // 沿着玩家上下左右,找到若干个有效的点作为目标
    13. if mp.IsValidPosition(p[0], p[1]) && zoneID == mp.GetGroupIDFromPosition(p[0], p[1]) {
    14. pointsAround = append(pointsAround, p)
    15. }
    16. }
    17. minLen := 999999
    18. minIndex := 0
    19. for i, p := range pointsAround { // 基于有效点,找到其中mob到玩家有效点的一个最小距离
    20. pathLength := (m.X-p[0])*(m.X-p[0]) + (m.Y-p[1])*(m.Y-p[1])
    21. if pathLength <= minLen {
    22. minLen = pathLength
    23. minIndex = i
    24. }
    25. }
    26. m.X, m.Y = pointsAround[minIndex][0], pointsAround[minIndex][1]
    27. }
    28. }

    算法:先找玩家周围有效点,然后从中计算选取一个最短路径点,最短路径通过:(x1-x2)(x1-x2) + (y1-y2)(y1-y2)粗略算出。更新当前mob的X、Y。

    8.5.mob平静期处理

    1. func (z *Zone) onMobCalm(e *Event) {
    2. mobID := e.Data[0].(int)
    3. if mob := z.MobsMap[mobID]; mob != nil {
    4. z.Logger.Println("[DEBUG] Mob", mob, "Calm Down")
    5. mob.RecoveryHP()
    6. for k := range mob.Haters {
    7. delete(mob.Haters, k)
    8. }
    9. mob.TargetID = 0
    10. if mob.X != mob.OriginX || mob.Y != mob.OriginY {
    11. mob.X, mob.Y = mob.OriginX, mob.OriginY
    12. moveEvent := AquireEvent(EventMove, mob.ID, mob.X, mob.Y)
    13. z.broadcastZone(moveEvent)
    14. }
    15. mob.TargetID = 0
    16. }
    17. }

    平静期到时(如果有玩家HIT攻击此mob时,平静期会被重置),mob恢复体力,清除所有Haters,当前位置不在原始位置则移动到原始位置并广播。

    8.6.多人同时攻击

    1. func (m *Mob) AddHate(playerID, damage int) {
    2. m.Haters[playerID] += damage
    3. }
    4. func (m *Mob) ChooseMobTarget() int {
    5. var max, maxPid int
    6. for pid, hate := range m.Haters {
    7. if hate > max {
    8. max = hate
    9. maxPid = pid
    10. }
    11. }
    12. if max <= 0 {
    13. return -1
    14. }
    15. return maxPid
    16. }
    17. func (z *Zone) onMobAttacked(m *Mob, p *Player) {
    18. m.ResetHateLater(z.EventCh)
    19. dmg := DamageFormula(p.WeaponLevel, m.ArmorLevel)
    20. if dmg > 0 {
    21. m.HP -= dmg
    22. if m.HP > 0 {
    23. dmgEvent := AquireEvent(EventDamage, m.ID, dmg)
    24. _ = p.send(dmgEvent)
    25. m.AddHate(p.ID, dmg)
    26. if maxHateTarget := m.ChooseMobTarget(); maxHateTarget > 0 {
    27. if maxHateTarget != m.TargetID {
    28. m.TargetID = maxHateTarget
    29. }
    30. attackEvent := AquireEvent(EventAttack, m.ID, m.TargetID)
    31. z.broadcastZone(attackEvent)
    32. }
    33. } else {
    34. z.Logger.Println("[DEBUG] m", m.ID, "DEAD!")
    35. m.IsDead = true
    36. if dropItem := m.DropItem(); dropItem != nil {
    37. z.Logger.Println("[DEBUG] m", m.ID, "DROP!", dropItem)
    38. dropItem.DespawnLater(z.EventCh)
    39. z.ItemsMap[dropItem.ID] = dropItem
    40. spawnItemEvent := AquireEvent(EventSpawn, dropItem.Pack()...)
    41. z.broadcastZone(spawnItemEvent)
    42. }
    43. z.Logger.Println("[DEBUG] m", m.ID, "DESPAWN LATER!")
    44. m.RespawnLater(z.EventCh)
    45. despawnEvent := AquireEvent(EventDespawn, m.ID)
    46. z.broadcastZone(despawnEvent)
    47. killEvent := AquireEvent(EventKill, m.Kind.ID)
    48. _ = p.send(killEvent)
    49. z.Logger.Println("[DEBUG] m", m.ID, "DESPAWN!")
    50. }
    51. }
    52. }

    所有玩家及伤害累积基于当前被攻击的mob的Haters列表,mob选择一个累积伤害最大的玩家进行攻击

    9.代码还需完善点

    • ChestArea、MobArea、StaticChest支持

    • DO、PO拆分

    • 多世界支持

    • 排队与负载支持

    • 账号接入

    • NPC寻路算法增强

    • 任务与活动

    • 数据持久化

    • 机器人压测脚本

    • 性能metrics监控

    • ……

    10.三方框架

    语言

    框架

    c

    skynet

    c++

    kbengine/TrinityCore

    golang

    leaf

    rust

    veloren

  • 相关阅读:
    threejs CSS3DObject、CSS3DSprite实现文字以及他们的区别
    MySQL面试问题汇总(2022)
    紫光展锐荣评“5G技术创新力企业”,5G赋能千行百业
    汇编语言(6)使用JCC指令构造分支与循环
    SolidWorks三维机械设计软件超实用操作技巧(九)
    MediaSoup简介
    智能井盖:提升城市井盖安全管理效率
    【初阶数据结构】详解树和二叉树(一) - 预备知识(我真的很想进步)
    Javascript中数组对象的splice、slice方法
    Java调用Linux SCP操作(SCPClient)
  • 原文地址:https://blog.csdn.net/eaglewood2005/article/details/137836278