【精简改造版】大型多人在线游戏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

相关内容

热门资讯

SpringMVC实现文件上传... 《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》...
5个程序(WPK数据)外挂透明... 5个程序(WPK数据)外挂透明挂脚本,微扑克软件发牌原理,详细教程(2021已更新)(哔哩哔哩);微...
HTTP协议详解(一) 目录1.什么是HTTP协议?2.HTTP的协议格式使用fiddler抓包工具理解代理查看请求内容3....
八分钟插件!德扑ai机器人哪里... 亲,德州wpk德州这款游戏可以开挂的,确实是有挂的,很多玩家在这款游戏中打牌都会发现很多用户的牌特别...
【sqlserver】已成功与... 问题现象:处理方式:配置管理器,mssqlserver的协...
【面试大全-Nginx】-Ng... 简述一下什么是NginxNginx是一个web服务器和方向代理服务器,用于HTTP、HTTPS、SM...
nginx搭建文件下载服务器无... 背景:想自己搭建一个下载文件服务器,网上很多教程,就跟着做了,搭建的过程中碰到了如下的几个问题,在这...
在Django服务器中部署ce... 一、celery的安装1.1什么是celerycelery分布式消息队列框架,主要描述...
2020新开挂(Wepoke规... 2020新开挂(Wepoke规律)外挂透明挂软件,微扑克辅助软件查得出来,详细教程(2021已更新)...
实践指南:如何将 Spring... 序言SpringBoot 是一个用来简化 Spring 应用初始搭建以及开发过程的框架,...