B/S架构:支持PC、平板、手机等多个平台
(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), ……]
单体架构
实体分类 | 编号 | 类型 | 说明 |
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 | 蓝剑 |
字段 | 类型 | 初始值 | 范围 | 说明 |
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 | 瓦片大小 |
客户端与服务器基于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 | 确认 |
一个世界包含一张地图【静态】
一张地图包含若干ChestArea区域
一个ChestArea区域包含若干Item对象
一张地图包含若干MobArea区域
一张地图包含若干CheckPoint
一个世界包含若干Zone【动态】
一个Zone包含若干NPC对象
一个Zone包含若干Mob对象
一个Zone包含若干Item对象
一个Zone包含若干Player对象
创建一个世界广播服务协程
根据地图的区域个数,每个区域创建一个协程
每个接入用户创建一个Handler协程,每个Handler协程创建一个PlayerHandleLoop协程
(1)Handler协程与PlayerHandleLoop协程通过带缓冲PacketChan通信
(2)Player读取解析PacketChan中的消息,逻辑处理后投递到所属区域对象的zone.EventCh
(3)Player对象调用世界对象,将消息投递到world.BroadcastCh进行世界消息发送(如人数)
(4)世界对象解析world.BroadcastCh中的消息,遍历所有区域对象,将消息投递到zone.EventCh
(5)区域对象读取解析zone.EventCh中的消息,逻辑处理后调用Player对象send方法进行消息发送
(1)通过json Unmarshal进行decode到Map结构体。
(2)根据地图宽高和区域宽高,计算出区域个数
(3)其中Map.collitions表示碰撞的点,结合地图宽高,初始化碰撞二维表
(4)初始化checkpoint Map,checkpoint ID作为KEY。其中checkpoint.S为1的表示为起始区域
- 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区间,如果在则掉落对应物品,否则不掉落。
- 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消息广播消失;
如果是静态物品,则触发定时重刷;
如果是药品,则触发补血;
如果是防具,则广播装备并根据当前防具类型更新当前用户血条;
如果是武器广播装备的同时并装备。
- 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。
- 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,当前位置不在原始位置则移动到原始位置并广播。
- 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选择一个累积伤害最大的玩家进行攻击
ChestArea、MobArea、StaticChest支持
DO、PO拆分
多世界支持
排队与负载支持
账号接入
NPC寻路算法增强
任务与活动
数据持久化
机器人压测脚本
性能metrics监控
……
语言 | 框架 |
c | skynet |
c++ | kbengine/TrinityCore |
golang | leaf |
rust | veloren |