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 |