千千梦丶琪 发表于 2024-7-23 18:17:38

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

1.架构选型

B/S架构:支持PC、平板、手机等多个平台
2.技术选型

(1)客户端web技术:


[*] HTML5 Canvas:支持基于2D平铺的图形引擎
[*] Web workers:允许在不减慢主页UI的情况下初始化大型世界地图。
[*] localStorage:将您角色的进度将及时保存在此中
[*] CSS3 Media Queries:使游戏可以自行调解大小并适应许多装备
[*] HTML5 audio:你可以听到老鼠或骷髅死亡的声音
(2)背景


[*] NodeJS(或golang)
[*] DB:MongoDB(Metrics)
(3)通讯范例:websocket
(4)通讯协议:
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
碰撞点
doors
list

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
checkpoints.[].id
int
checkpoints.[].x
int
checkpoints.[].y
int
checkpoints.[].w
int
checkpoints.[].h
int
checkpoints.[].s
int
0/1
roamingAreas
list
移动地区
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
箱子地区
chestAreas.[].x
int
chestAreas.[].y
int
chestAreas.[].w
int
chestAreas.[].h
int
chestAreas.[].i
list
箱子中ItemList
chestAreas.[].tx
int
chestAreas.[].ty
int
staticChests
list
静态箱子
staticChests.[].x
int
staticChests.[].y
int
staticChests.[].i
list
箱子中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,
康健信息
https://img-blog.csdnimg.cn/direct/2cbaee9aa958442f92cf9fa917161fc1.png 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 协议交互流程

https://img-blog.csdnimg.cn/direct/4ab0abd35cca4c0cb1571ce2213daa25.png
6.类图

https://img-blog.csdnimg.cn/direct/ec95f5776e614c11bf633b7526e47f1b.png


[*] 一个世界包含一张地图【静态】

[*] 一张地图包含若干ChestArea地区

[*] 一个ChestArea地区包含若干Item对象

[*] 一张地图包含若干MobArea地区
[*] 一张地图包含若干CheckPoint

[*] 一个世界包含若干Zone【动态】

[*] 一个Zone包含若干NPC对象
[*] 一个Zone包含若干Mob对象
[*] 一个Zone包含若干Item对象
[*] 一个Zone包含若干Player对象

7.线程模子

https://img-blog.csdnimg.cn/direct/dc6e2db68eb54cbc9c68142d667725a3.png
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.物品掉落

        TypeCrab.ID: &MobProperty{
                Drops: mapint{
                        "flask":      50,
                        "axe":          20,
                        "leatherarmor": 10,
                        "firepotion":   5,
                },
                HP:          60,
                ArmorLevel:2,
                WeaponLevel: 1,
        }, Drops表示:flask:50%,axe:20%,leatherarmor:%10,firepotion:5%,不掉落5%
算法:随机一个的值,累计求和,判定是否在Drops区间,如果在则掉落对应物品,否则不掉落。
8.3.物品捡取

func (z *Zone) onLoot(e *Event) {
        itemID := e.Data.(int)
        p := z.PlayersMap
        if p == nil {
                return
        }
        if item := z.ItemsMap; item != nil {
                despawnEvent := AquireEvent(EventDespawn, itemID)
                z.broadcastZone(despawnEvent)
                item.IsDestroy = true
                if item.IsStatic {
                        item.RespawnLater(z.EventCh)
                }
                kind := item.Kind
                if kind.ID == TypeFirePotion.ID {
                        // TODO
                } else if IsHealingItem(kind) {
                        amount := 0
                        switch kind.ID {
                        case TypeFlask.ID:
                                amount = 40
                        case TypeBurger.ID:
                                amount = 100
                        }
                        if amount > 0 && !p.HasFullHealth() {
                                p.ReginHealthBy(amount)
                                healthEvent := AquireEvent(EventHealth, p.HP)
                                _ = p.send(healthEvent)
                        }
                } else if IsArmor(kind) || IsWeapon(kind) {
                        equipEvent := AquireEvent(EventEquip, p.ID, kind.ID)
                        z.broadcastZone(equipEvent)

                        if IsArmor(kind) {
                                p.equipArmor(kind.ID)
                                p.updateHP()
                                HPEvent := AquireEvent(EventHP, p.MaxHP)
                                _ = p.send(HPEvent)
                        } else {
                                p.equipWeapon(kind.ID)
                        }
                }
        }
} 捡取流程:
通过EventDespawn消息广播消散;


[*] 如果是静态物品,则触发定时重刷;
[*] 如果是药品,则触发补血;
[*] 如果是防具,则广播装备并根据当前防具范例更新当前用户血条;
[*] 如果是武器广播装备的同时并装备。
8.4.mob跟随

func (m *Mob) ChaseTarget(zoneID string, mp *Map, targetX, targetY int) {
        zid := mp.GetGroupIDFromPosition(targetX, targetY)
        if zoneID != zid {
                m.X, m.Y = targetX, targetY
        } else {
                pointsAround := make([]int, 0)
                for _, p := range []int{
                        int{targetX, targetY + 1},
                        int{targetX + 1, targetY},
                        int{targetX, targetY - 1},
                        int{targetX - 1, targetY},
                } { // 沿着玩家上下左右,找到若干个有效的点作为目标
                        if mp.IsValidPosition(p, p) && zoneID == mp.GetGroupIDFromPosition(p, p) {
                                pointsAround = append(pointsAround, p)
                        }
                }
                minLen := 999999
                minIndex := 0
                for i, p := range pointsAround { // 基于有效点,找到其中mob到玩家有效点的一个最小距离
                        pathLength := (m.X-p)*(m.X-p) + (m.Y-p)*(m.Y-p)
                        if pathLength <= minLen {
                                minLen = pathLength
                                minIndex = i
                        }
                }
                m.X, m.Y = pointsAround, pointsAround
        }
} 算法:先找玩家周围有效点,然后从中计算选取一个最短路径点,最短路径通过:(x1-x2)(x1-x2) + (y1-y2)(y1-y2)粗略算出。更新当前mob的X、Y。
8.5.mob平静期处置惩罚

func (z *Zone) onMobCalm(e *Event) {
        mobID := e.Data.(int)
        if mob := z.MobsMap; mob != nil {
                z.Logger.Println(" Mob", mob, "Calm Down")

                mob.RecoveryHP()
                for k := range mob.Haters {
                        delete(mob.Haters, k)
                }
                mob.TargetID = 0
                if mob.X != mob.OriginX || mob.Y != mob.OriginY {
                        mob.X, mob.Y = mob.OriginX, mob.OriginY
                        moveEvent := AquireEvent(EventMove, mob.ID, mob.X, mob.Y)
                        z.broadcastZone(moveEvent)
                }
                mob.TargetID = 0
        }
} 平静期到时(如果有玩家HIT攻击此mob时,平静期会被重置),mob恢复体力,扫除所有Haters,当前位置不在原始位置则移动到原始位置并广播。
8.6.多人同时攻击

func (m *Mob) AddHate(playerID, damage int) {
        m.Haters += damage
}

func (m *Mob) ChooseMobTarget() int {
        var max, maxPid int
        for pid, hate := range m.Haters {
                if hate > max {
                        max = hate
                        maxPid = pid
                }
        }
        if max <= 0 {
                return -1
        }
        return maxPid
}

func (z *Zone) onMobAttacked(m *Mob, p *Player) {
        m.ResetHateLater(z.EventCh)

        dmg := DamageFormula(p.WeaponLevel, m.ArmorLevel)
        if dmg > 0 {
                m.HP -= dmg
                if m.HP > 0 {
                        dmgEvent := AquireEvent(EventDamage, m.ID, dmg)
                        _ = p.send(dmgEvent)

                        m.AddHate(p.ID, dmg)
                        if maxHateTarget := m.ChooseMobTarget(); maxHateTarget > 0 {
                                if maxHateTarget != m.TargetID {
                                        m.TargetID = maxHateTarget
                                }
                                attackEvent := AquireEvent(EventAttack, m.ID, m.TargetID)
                                z.broadcastZone(attackEvent)
                        }
                } else {
                        z.Logger.Println(" m", m.ID, "DEAD!")
                        m.IsDead = true
                        if dropItem := m.DropItem(); dropItem != nil {
                                z.Logger.Println(" m", m.ID, "DROP!", dropItem)
                                dropItem.DespawnLater(z.EventCh)
                                z.ItemsMap = dropItem

                                spawnItemEvent := AquireEvent(EventSpawn, dropItem.Pack()...)
                                z.broadcastZone(spawnItemEvent)
                        }
                        z.Logger.Println(" m", m.ID, "DESPAWN LATER!")
                        m.RespawnLater(z.EventCh)

                        despawnEvent := AquireEvent(EventDespawn, m.ID)
                        z.broadcastZone(despawnEvent)
                        killEvent := AquireEvent(EventKill, m.Kind.ID)
                        _ = p.send(killEvent)
                        z.Logger.Println(" m", m.ID, "DESPAWN!")
                }
        }
} 所有玩家及伤害累积基于当前被攻击的mob的Haters列表,mob选择一个累积伤害最大的玩家举行攻击
9.代码还需美满点



[*] ChestArea、MobArea、StaticChest支持
[*] DO、PO拆分
[*] 多世界支持
[*] 排队与负载支持
[*] 账号接入
[*] NPC寻路算法加强
[*] 使命与活动
[*] 数据长期化
[*] 呆板人压测脚本
[*] 性能metrics监控
[*] ……
10.三方框架

语言
框架
c
skynet
c++
kbengine/TrinityCore
golang
leaf
rust
veloren


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【精简改造版】大型多人在线游戏BrowserQuest服务器Golang框架解析(2)—