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 | 


















