【精简改造版】大型多人在线游戏BrowserQuest服务器Golang框架解析(2)——服务端架构
创始人
2025-01-18 20:04:50
0

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)通讯协议:[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.物品掉落

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

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

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

8.3.物品捡取

func (z *Zone) onLoot(e *Event) { 	itemID := e.Data[0].(int) 	p := z.PlayersMap[e.PlayerID] 	if p == nil { 		return 	} 	if item := z.ItemsMap[itemID]; 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([][2]int, 0) 		for _, p := range [][2]int{ 			[2]int{targetX, targetY + 1}, 			[2]int{targetX + 1, targetY}, 			[2]int{targetX, targetY - 1}, 			[2]int{targetX - 1, targetY}, 		} { // 沿着玩家上下左右,找到若干个有效的点作为目标 			if mp.IsValidPosition(p[0], p[1]) && zoneID == mp.GetGroupIDFromPosition(p[0], p[1]) { 				pointsAround = append(pointsAround, p) 			} 		} 		minLen := 999999 		minIndex := 0 		for i, p := range pointsAround { // 基于有效点,找到其中mob到玩家有效点的一个最小距离 			pathLength := (m.X-p[0])*(m.X-p[0]) + (m.Y-p[1])*(m.Y-p[1]) 			if pathLength <= minLen { 				minLen = pathLength 				minIndex = i 			} 		} 		m.X, m.Y = pointsAround[minIndex][0], pointsAround[minIndex][1] 	} }

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

8.5.mob平静期处理

func (z *Zone) onMobCalm(e *Event) { 	mobID := e.Data[0].(int) 	if mob := z.MobsMap[mobID]; mob != nil { 		z.Logger.Println("[DEBUG] 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[playerID] += 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("[DEBUG] m", m.ID, "DEAD!") 			m.IsDead = true 			if dropItem := m.DropItem(); dropItem != nil { 				z.Logger.Println("[DEBUG] m", m.ID, "DROP!", dropItem) 				dropItem.DespawnLater(z.EventCh) 				z.ItemsMap[dropItem.ID] = dropItem  				spawnItemEvent := AquireEvent(EventSpawn, dropItem.Pack()...) 				z.broadcastZone(spawnItemEvent) 			} 			z.Logger.Println("[DEBUG] 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("[DEBUG] 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

相关内容

热门资讯

总算清楚(云扑克德州)外挂透明... 总算清楚(云扑克德州)外挂透明挂辅助软件(软件透明挂)第三方教程(2021已更新)(哔哩哔哩)1、云...
aapoker有猫腻!wpk ... aapoker有猫腻!wpk 辅助工具,来玩德州app果然真的有挂,专业教程(有挂攻略)1、用户打开...
九分钟曝光!(AAPOkER)... 九分钟曝光!(AAPOkER)软件透明挂,(aApoker)一般真的有挂,曝光教程(有挂技巧)亲,关...
1分钟曝光!云扑克德州有挂,(... 1分钟曝光!云扑克德州有挂,(鱼扑克)果然真的有挂,可靠教程(有挂体悟)1、完成云扑克德州有挂透视辅...
4分钟方案!(来玩德州)软件透... 4分钟方案!(来玩德州)软件透明挂,(智星德州)总是真的有挂,黑科技教程(有挂技巧)亲,关键说明,智...
盘点一款(WePoke)外挂透... 盘点一款(WePoke)外挂透明挂辅助脚本(软件透明挂)玩家教程(2022已更新)(哔哩哔哩)1、点...
智星德州菠萝偷偷看牌功能!智星... 智星德州菠萝偷偷看牌功能!智星德州有挂,菠萝德州app原来真的有挂,攻略方法(有挂攻略)1、进入到智...
七分钟揭秘!wepower德州... 七分钟揭秘!wepower德州可以设置,(EV扑克)好像真的有挂,攻略教程(有挂熟悉)1、超多福利:...
9分钟详细!(德州app)软件... 9分钟详细!(德州app)软件透明挂,(欢乐棋牌)本来真的有挂,安装教程(有挂介绍)1、欢乐棋牌系统...
wepokeai代打的胜率!云... wepokeai代打的胜率!云扑克有挂,云扑克cloudpoker好像真的有挂,2024新版(有挂教...