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 = trueif item.IsStatic {item.RespawnLater(z.EventCh)}kind := item.Kindif kind.ID == TypeFirePotion.ID {// TODO} else if IsHealingItem(kind) {amount := 0switch kind.ID {case TypeFlask.ID:amount = 40case 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 := 999999minIndex := 0for 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 = pathLengthminIndex = 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 = 0if mob.X != mob.OriginX || mob.Y != mob.OriginY {mob.X, mob.Y = mob.OriginX, mob.OriginYmoveEvent := 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 intfor pid, hate := range m.Haters {if hate > max {max = hatemaxPid = 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 -= dmgif 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 = trueif dropItem := m.DropItem(); dropItem != nil {z.Logger.Println("[DEBUG] m", m.ID, "DROP!", dropItem)dropItem.DespawnLater(z.EventCh)z.ItemsMap[dropItem.ID] = dropItemspawnItemEvent := 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 |