【精简改造版】大型多人在线游戏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)通讯协议:[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

相关推荐

  1. tcp服务,时刻线

    2024-04-26 22:36:04       36 阅读
  2. C++服务器开发(2):确定服务器框架

    2024-04-26 22:36:04       41 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-26 22:36:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-26 22:36:04       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-26 22:36:04       82 阅读
  4. Python语言-面向对象

    2024-04-26 22:36:04       91 阅读

热门阅读

  1. 字符串简单运算(BigDecimal相关运算)

    2024-04-26 22:36:04       39 阅读
  2. Swift 中如何四舍五入

    2024-04-26 22:36:04       30 阅读
  3. linux文件相关命令

    2024-04-26 22:36:04       31 阅读
  4. MR混合现实实训系统为农学情景实训教学演练

    2024-04-26 22:36:04       28 阅读
  5. Anagrams

    2024-04-26 22:36:04       36 阅读
  6. 每天一个数据分析题(二百八十二)

    2024-04-26 22:36:04       31 阅读
  7. leetCode58. 最后一个单词的长度

    2024-04-26 22:36:04       37 阅读
  8. VL2 异步复位的串联T触发器

    2024-04-26 22:36:04       31 阅读
  9. 【MySql】 MySQL中的MAX()和MIN()函数的高效使用

    2024-04-26 22:36:04       34 阅读
  10. scrollintoview方法滚动距离顶部距离

    2024-04-26 22:36:04       35 阅读
  11. C#中的扩展方法

    2024-04-26 22:36:04       33 阅读